pipe/test/policy_test.dart
trvon fb055c9978
Some checks failed
Lint Commits / commitlint (pull_request) Successful in 19s
Tests / Static Analysis (pull_request) Has been cancelled
Tests / Tests (pull_request) Has been cancelled
merge: bring main into the audit and state-map work
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.
2026-09-24 07:33:53 -06:00

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'));
});
}