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('models a single-class flag state that carries no variants', () { final flag = analysis.flow.flagStates .where((state) => state.name == 'FlagState') .toList(); expect(flag, hasLength(1)); expect(flag.single.owner, 'FlagCubit'); expect(flag.single.fields, contains('loading')); expect(flag.single.defaultsTrue, isTrue); }); test('flag rule reports the unguarded await only', () { final hits = analysis.report.findings .where((finding) => finding.rule == ruleLoadingFlagNotReset) .toList(); // loadGuarded resets the flag in its catch, refreshQuietly never sets it, // and scheduleGuarded's closure guards itself, so only the unguarded method // and the two deliberately unguarded `on<>` handlers survive. expect(hits.map((hit) => hit.data['member']).toSet(), { 'loadUnguarded', 'on', 'on', }); expect(hits, hasLength(3)); final unguarded = hits.singleWhere( (hit) => hit.data['member'] == 'loadUnguarded', ); expect(unguarded.data['stateType'], 'FlagState'); expect(unguarded.severity, PipeSeverity.warning); expect(unguarded.confidence, 'high'); expect(unguarded.evidence, isNotEmpty); expect(unguarded.remediation, isNotNull); // The flag reads as one code span, `FlagState.loading`, not as // `FlagState` immediately followed by a second span. expect(unguarded.message, contains('`FlagState.')); }); test('flag findings carry no false positive for guarded code', () { final members = analysis.report.findings .where((finding) => finding.rule == ruleLoadingFlagNotReset) .map((finding) => finding.data['member']) .toSet(); expect(members, isNot(contains('loadGuarded'))); expect(members, isNot(contains('refreshQuietly'))); }); }