pipe/test/riverpod_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

93 lines
3.1 KiB
Dart

import 'package:pipe/pipe.dart';
import 'package:test/test.dart';
const String fixturePath = 'test/fixtures/stuck_loading';
void main() {
late ProjectAnalysis analysis;
setUpAll(() async {
final project = await loadProject(fixturePath);
analysis = await analyzeProject(project, concurrency: 1);
});
tearDownAll(() async {
await analysis.dispose();
});
test('discovers Riverpod providers and their value types', () {
final names = analysis.flow.riverpod.providers.map((p) => p.name).toSet();
expect(
names,
containsAll(<String>['unhandledProvider', 'handledProvider']),
);
final unhandled = analysis.flow.riverpod.providers.firstWhere(
(p) => p.name == 'unhandledProvider',
);
expect(unhandled.kind, 'FutureProvider');
expect(unhandled.isAsync, isTrue);
expect(unhandled.valueType, 'UnhandledDoc');
});
test('records watch sites with their AsyncValue handling', () {
final sites = analysis.flow.riverpod.sitesFor('unhandledProvider');
expect(sites, hasLength(1));
expect(sites.single.watches, isTrue);
expect(sites.single.readsValue, isTrue);
expect(sites.single.handlesError, isFalse);
expect(sites.single.handlesLoading, isFalse);
});
test('reports the unhandled provider and not the handled one', () {
final hits = analysis.report.findings
.where((f) => f.rule == ruleRiverpodAsyncGap)
.toList();
// The two deliberate gaps: a direct read and a read through a variable.
expect(hits.map((f) => f.data['provider']).toSet(), {
'unhandledProvider',
'storedUnhandledProvider',
});
expect(hits, hasLength(2));
final unhandled = hits.singleWhere(
(f) => f.data['provider'] == 'unhandledProvider',
);
expect(
unhandled.data['missing'],
containsAll(<String>['error', 'loading']),
);
// A non-bool value type keeps this at high confidence and warning severity.
expect(unhandled.confidence, 'high');
expect(unhandled.severity, PipeSeverity.warning);
expect(unhandled.evidence, isNotEmpty);
expect(unhandled.remediation, isNotNull);
});
test('the handled provider produces no gap', () {
final providers = analysis.report.findings
.where((f) => f.rule == ruleRiverpodAsyncGap)
.map((f) => f.data['provider'])
.toSet();
expect(providers, isNot(contains('handledProvider')));
expect(
analysis.flow.riverpod.gaps.map((g) => g.provider.name),
isNot(contains('handledProvider')),
);
});
test('a provider consumed only by ref.listen is not a render gap', () {
// A listener mirrors the value into a side effect and draws nothing, so
// it cannot leave a blank or stale screen. Seen when dogfooding on
// word_alive: an analytics user-property listener was reported as a UI
// read with unhandled loading and error.
final sites = analysis.flow.riverpod.sitesFor('listenOnlyProvider');
expect(sites, hasLength(1));
expect(sites.single.watches, isFalse);
expect(sites.single.readsValue, isTrue);
expect(
analysis.flow.riverpod.gaps.map((g) => g.provider.name),
isNot(contains('listenOnlyProvider')),
);
});
}