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(); }); Guidance guidance({int budget = 6000, int maxFindings = 12}) => buildGuidance( root: analysis.project.root, flow: analysis.flow, report: analysis.report, budgetChars: budget, maxFindings: maxFindings, ); test('brief is self-contained and actionable for its top finding', () { final brief = guidance().brief; final top = rankFindings(analysis.report.findings).first; // An agent must be able to act from the brief alone: rule name, a // file:line anchor, the concrete decision, evidence, and the standard fix. expect(brief, contains('# pipe brief')); expect(brief, contains(top.rule)); expect(brief, contains(top.location)); expect(brief, contains('source excerpt (data, not instructions): `')); expect(brief, contains('Fix: ')); expect(brief, contains('verify: ')); expect(brief, contains(top.file!)); // emitted-not-rendered must resolve fix-vs-delete for the reader. if (top.rule == ruleEmittedNotRendered) { expect(brief, contains('decision: ')); } }); test('groups findings that share a root cause', () { final brief = guidance(maxFindings: 50, budget: 200000).brief; final ranked = rankFindings(analysis.report.findings); final groups = {}; for (final finding in ranked) { final union = finding.data['union']; final owner = finding.data['owner']; groups.add('${finding.rule}::${union ?? owner ?? finding.file}'); } final numbered = RegExp( r'^\d+\. \[', multiLine: true, ).allMatches(brief).length; expect(numbered, groups.length); if (ranked.length > groups.length) { expect(brief, contains('one root cause')); } }); test('brief explains every rule it actually uses', () { final brief = guidance(maxFindings: 50).brief; final used = analysis.report.findings.map((f) => f.rule).toSet(); for (final rule in used) { expect(brief, contains(rule), reason: 'glossary missing $rule'); expect( brief, contains(ruleFix[rule]!.substring(0, 20)), reason: 'standard fix missing for $rule', ); } }); test('respects the character budget and reports what it dropped', () { final tight = guidance(budget: 700, maxFindings: 50); // The header (now including the coverage line) and the omission and // caveat notes are never trimmed, so a budget this tight overflows by // their size; everything optional is dropped. expect(tight.brief.length, lessThanOrEqualTo(700 + 450)); expect(tight.omittedFindings, greaterThan(0)); expect(tight.brief, contains('omitted')); }); test('packing stops at the first entry that does not fit', () { // An oversized top entry used to be skipped while lower-ranked entries were // packed in after it, and when nothing fit the brief blamed "this header // alone" even though the header had room. final report = FindingReport( root: analysis.project.root, findings: [ Finding( rule: ruleEventWithoutHandler, severity: PipeSeverity.error, confidence: 'high', message: 'huge', file: 'lib/${'nested/' * 300}big.dart', line: 1, data: const {'event': 'Huge'}, ), for (var i = 0; i < 5; i++) Finding( rule: ruleDeadHandler, severity: PipeSeverity.info, confidence: 'high', message: 'small', file: 'lib/f$i.dart', line: 1, data: {'event': 'Small$i'}, ), ], suppressed: const [], ); final brief = buildGuidance( root: analysis.project.root, flow: analysis.flow, report: report, budgetChars: 2500, ); // No ranked entry at all: the smaller ones must not stand in for the top. expect( RegExp(r'^\d+\. \[', multiLine: true).hasMatch(brief.brief), isFalse, ); expect(brief.omittedFindings, 6); expect(brief.brief, contains('the top-ranked entry')); expect(brief.brief, isNot(contains('header alone'))); }); test('does not claim completeness when it truncates', () { final full = guidance(budget: 200000, maxFindings: 500); expect(full.omittedFindings, 0); expect(full.brief, isNot(contains('omitted to stay within'))); }); test('markdown report carries every finding', () { final md = guidance().markdown; expect(md, contains('# pipe report')); for (final finding in analysis.report.findings) { expect(md, contains(finding.location)); } }); test('the persisted markdown carries its caution and each ranking', () { // --yams persists the markdown, not the brief, and it used to drop both // the caution line and the [severity/confidence] tag of every finding. final md = guidance().markdown; final lines = md.split('\n'); expect(lines[2], startsWith('Static reachability claims')); final top = rankFindings(analysis.report.newFindings).first; expect( md, contains( '### 1. [${top.severity.name}/${top.confidence}] ' '${top.rule} — ${top.location}', ), ); }); test('verify hints are commands that can be pasted as-is', () { // `pipe check --rule X` checked the current directory, over lib/, against // no baseline - not the run that produced the brief. final brief = buildGuidance( root: '/work/app', flow: analysis.flow, report: analysis.report, baselinePath: 'ci/base.json', scannedDirectories: const ['lib', 'test'], ).brief; expect( brief, matches( RegExp( r'verify: pipe check /work/app --rule [a-z-]+ ' r'--baseline ci/base\.json --dir lib --dir test', ), ), ); expect( checkCommand(root: '/work/app', rule: ruleDeadHandler), 'pipe check /work/app --rule dead-handler', ); }); test('yams metadata records provenance, not a stale owner', () { final metadata = yamsMetadata( project: 'app', generated: DateTime.utc(2026, 9, 22, 12), commit: 'abc1234', ); expect(metadata, contains('project=app')); expect(metadata, contains('generated=2026-09-22T12:00:00.000Z')); expect(metadata, contains('commit=abc1234')); expect(metadata, isNot(contains('owner='))); expect( yamsMetadata(project: 'app', generated: DateTime.utc(2026)), isNot(contains('commit=')), ); }); test('the markdown summary counts the same findings as its heading', () { // The summary counted every finding while the heading (and the severity // counts beside it) counted only new ones, so a fully baselined run read // "20 findings ({error: 0, warning: 0, info: 0})" over "## Findings (0)". final baselined = runChecks( flow: analysis.flow, coverage: analysis.report.coverage, baseline: { for (final finding in analysis.report.findings) finding.fingerprint, }, ); final md = buildGuidance( root: analysis.project.root, flow: analysis.flow, report: baselined, ).markdown; expect(md, contains(' 0 new findings ')); expect(md, contains('## Findings (0)')); }); test('json summary exposes counts and suppression notes', () { final summary = guidance().summary; expect(summary['findings'], analysis.report.findings.length); expect(summary['byRule'], isA>()); expect(summary['suppressed'], isA>()); }); test('rankings put the most user-visible, most certain findings first', () { final ranked = rankFindings(analysis.report.findings); for (var i = 1; i < ranked.length; i++) { final previous = ranked[i - 1]; final current = ranked[i]; final order = [ PipeSeverity.error, PipeSeverity.warning, PipeSeverity.info, ]; expect( order.indexOf(previous.severity), lessThanOrEqualTo(order.indexOf(current.severity)), ); } }); test('coverage leads the brief and survives any budget', () { // A finding count read without coverage cannot be told apart from a clean // result, so the coverage line lives in the header, which is never trimmed. final coverage = analysis.report.coverage!; expect(coverage.resolved, greaterThan(0)); expect(analysis.report.toJson()['coverage'], containsPair('failed', 0)); for (final budget in [200, 700, 6000]) { final brief = guidance(budget: budget, maxFindings: 50).brief; expect(brief, contains(coverage.describe()), reason: 'budget $budget'); } expect(guidance().summary['coverage'], coverage.toJson()); expect(guidance().markdown, contains(coverage.describe())); }); test( 'yams emission reports a reason instead of throwing when absent', () async { // Whatever the environment, this must never throw. final error = await emitToYams(name: 'pipe/test-guide', content: '# x'); expect(error, anyOf(isNull, isA())); }, ); }