main gained six PRs (#1-#6) while the audit fixes and the state-map seam were in progress. Four of main's commits fixed the same bugs this branch did, independently; where they overlap, the more complete version wins: - Fingerprints: main's (a0844bb) - catchSite (`member#index` among all catches), union/owner as identity, a legacy fingerprint so version-1 baselines still match, baseline "version": 2. This branch's `catch` ordinal is dropped; its fingerprint_test still guards the result. - verify --rule: main's verifyAnalysis (cf9e0b7), which also reports findings in skipped files as NOT SCANNED. - Unreadable SDK: both kept - this branch's up-front probe gives the early, specific error; main's catch around the analyzer (d6b6bcb) is the backstop. - Riverpod: main's "a ref.listen is not a render" (f6411dc) and this branch's AsyncError patterns / stored-value tracking compose; the fixture keeps both sets of cases. Ported onto the model-only rules (the seam), since main edited code this branch had moved into front-end facts: - excerpt.dart's literal-free, bounded quoting (a prompt-injection guard) now also covers front-end facts: SwallowedFailure.handler and NonSubscribingRead.excerpt, and the C++ front end elides string literals in handler evidence the same way. - ScanCoverage: runChecks stays source-free; the model carries coverage (`scanCoverage` in the state JSON, written by both front ends), so `check --model` states coverage like a source check. - main's softened wording (rendered-not-emitted "probably unreachable", the remove-policy dead-handler text, notifier-field-unread) and the backticked flag message. Docs: main's README grew the Firestore query inventory and the output contract; the README stays short, the inventory moves to docs/queries.md, and the contract, exit codes, verify and baseline details join docs/usage.md. Checked against origin/main on resolved mirrors of the dogfood apps: nuwayze (102), dex (0) and word_alive (0) identical; tick differs only by the intended riverpod stored-value fix. On the fixture every difference is one of this branch's fixes. 214 tests pass.
119 lines
4.5 KiB
Dart
119 lines
4.5 KiB
Dart
import 'package:pipe/pipe.dart';
|
|
import 'package:test/test.dart';
|
|
|
|
const String fixturePath = 'test/fixtures/stuck_loading';
|
|
|
|
/// The project default is to WIRE UP: an unreachable state, handler or control is
|
|
/// usually an unfinished feature, so guidance must lead with making it reachable
|
|
/// and treat removal as the exception. `--policy remove` flips that for teams
|
|
/// that would rather delete.
|
|
void main() {
|
|
late ProjectAnalysis analysis;
|
|
|
|
setUpAll(() async {
|
|
final project = await loadProject(fixturePath);
|
|
analysis = await analyzeProject(project, concurrency: 1);
|
|
});
|
|
|
|
tearDownAll(() async {
|
|
await analysis.dispose();
|
|
});
|
|
|
|
FindingReport reportWith(FixPolicy policy) =>
|
|
runChecks(flow: analysis.flow, policy: policy);
|
|
|
|
String briefWith(FixPolicy policy) => buildGuidance(
|
|
root: '/root',
|
|
flow: analysis.flow,
|
|
report: reportWith(policy),
|
|
budgetChars: 200000,
|
|
maxFindings: 50,
|
|
policy: policy,
|
|
).brief;
|
|
|
|
test('wire-up is the default and leads the dead-handler remediation', () {
|
|
final dead = reportWith(
|
|
FixPolicy.wireUp,
|
|
).findings.where((finding) => finding.rule == ruleDeadHandler).toList();
|
|
expect(dead, isNotEmpty);
|
|
expect(dead.first.remediation, startsWith('Wire it up'));
|
|
expect(dead.first.remediation, contains('explicit approval'));
|
|
});
|
|
|
|
test('the default also leads the unread-member remediation', () {
|
|
final unread = reportWith(FixPolicy.wireUp).findings
|
|
.where((finding) => finding.rule == ruleNotifierFieldUnread)
|
|
.toList();
|
|
expect(unread, isNotEmpty);
|
|
expect(unread.first.remediation, startsWith('Wire it up'));
|
|
});
|
|
|
|
test('remove flips the wording', () {
|
|
final dead = reportWith(
|
|
FixPolicy.remove,
|
|
).findings.where((finding) => finding.rule == ruleDeadHandler).toList();
|
|
expect(dead.first.remediation, startsWith('Remove the dead handler'));
|
|
|
|
final unread = reportWith(FixPolicy.remove).findings
|
|
.where((finding) => finding.rule == ruleNotifierFieldUnread)
|
|
.toList();
|
|
expect(unread.first.remediation, isNot(startsWith('Wire it up')));
|
|
});
|
|
|
|
test('the brief defaults to wiring up an unwired union', () {
|
|
final brief = briefWith(FixPolicy.wireUp);
|
|
// The fixture's ItemState is emitted but rendered nowhere, and nothing
|
|
// outside the bloc references it, so the guidance must say to wire it up.
|
|
expect(brief, contains('wire it up'));
|
|
expect(brief, isNot(contains('safe to delete')));
|
|
});
|
|
|
|
test('the brief says removal only under the remove policy', () {
|
|
final brief = briefWith(FixPolicy.remove);
|
|
expect(brief, contains('safe to delete'));
|
|
expect(brief, isNot(contains('wire it up')));
|
|
});
|
|
|
|
test('the dead-handler section is framed by the policy', () {
|
|
expect(
|
|
briefWith(FixPolicy.wireUp),
|
|
contains('Unreachable handlers'),
|
|
reason: 'wire-up frames dead handlers as needing a dispatching control',
|
|
);
|
|
expect(briefWith(FixPolicy.remove), contains('Cleanly deletable'));
|
|
});
|
|
|
|
test('the legend follows the policy too', () {
|
|
// The legend's fixes were one constant map that ignored --policy, so under
|
|
// wire-up it still led with "Remove the dead handler" while every finding
|
|
// said removal needed explicit approval.
|
|
expect(ruleFixFor(FixPolicy.wireUp)[ruleDeadHandler], startsWith('Wire'));
|
|
expect(
|
|
ruleFixFor(FixPolicy.wireUp)[ruleEmittedNotRendered],
|
|
startsWith('Wire'),
|
|
);
|
|
expect(
|
|
ruleFixFor(FixPolicy.remove)[ruleDeadHandler],
|
|
startsWith('Remove the dead handler'),
|
|
);
|
|
expect(briefWith(FixPolicy.wireUp), isNot(contains('Fix: Remove')));
|
|
expect(ruleFixFor(FixPolicy.wireUp).keys, containsAll(allRules));
|
|
expect(ruleFixFor(FixPolicy.remove).keys, containsAll(allRules));
|
|
});
|
|
|
|
test('removal claims are scoped to the scanned directories', () {
|
|
// The scan defaults to --dir lib, so "nothing outside the bloc references
|
|
// it" and "safe even without product context" claimed more than was
|
|
// checked: a handler dispatched only from test/ read as safe to delete.
|
|
for (final policy in FixPolicy.values) {
|
|
final brief = briefWith(policy);
|
|
expect(brief, contains('nothing under lib'), reason: '$policy');
|
|
expect(brief, isNot(contains('without product context')));
|
|
expect(brief, isNot(contains('nothing outside the bloc')));
|
|
}
|
|
final dead = reportWith(
|
|
FixPolicy.remove,
|
|
).findings.firstWhere((finding) => finding.rule == ruleDeadHandler);
|
|
expect(dead.remediation, contains('nothing outside lib'));
|
|
});
|
|
}
|