// Behavioural tests for FlowAnalyzer — the YAML-aware // AbstractAnalyzer that feeds the flow editor's gutter + the // custom CodeController's wavy-underline overrides. import 'package:fai_studio_flow_editor/src/flow_analyzer.dart'; import 'package:fai_studio_flow_editor/src/quick_fix.dart'; import 'package:flutter_code_editor/flutter_code_editor.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:highlight/languages/yaml.dart'; Code _wrap(String text) => Code(text: text, language: yaml); void main() { group('FlowAnalyzer', () { test('empty buffer reports no issues', () async { final a = FlowAnalyzer(availableCapabilities: () => const []); final r = await a.analyze(_wrap('')); expect(r.issues, isEmpty); }); test('valid hello flow reports no issues', () async { final a = FlowAnalyzer( availableCapabilities: () => const ['debug.echo'], ); final r = await a.analyze(_wrap(''' name: hello inputs: name: text steps: - id: greet use: debug.echo@^0 with: message: \$inputs.name outputs: greeting: \$greet.echoed ''')); expect(r.issues, isEmpty); }); test('flags unknown capability on the use line', () async { final a = FlowAnalyzer( availableCapabilities: () => const ['debug.echo'], ); final r = await a.analyze(_wrap(''' name: hello inputs: msg: text steps: - id: t use: text.translate@^1 outputs: {} ''')); expect(r.issues, hasLength(1)); expect(r.issues.first.message, contains('text.translate')); expect(r.issues.first.type, IssueType.error); }); test('flags unknown input type', () async { final a = FlowAnalyzer(availableCapabilities: () => const []); final r = await a.analyze(_wrap(''' name: hello inputs: pdf: byes steps: [] outputs: {} ''')); expect(r.issues, hasLength(1)); expect(r.issues.first.message, contains('byes')); expect(r.issues.first.type, IssueType.warning); }); test('YAML parse error pins to the offending line', () async { final a = FlowAnalyzer(availableCapabilities: () => const []); // Unterminated quoted string — a real parse error rather // than a structural oddity YAML happens to tolerate. final r = await a.analyze(_wrap('name: "unterminated\n')); expect(r.issues, hasLength(1)); expect(r.issues.first.type, IssueType.error); expect(r.issues.first.message, startsWith('YAML:')); }); test('matches the bare capability id ignoring @version', () async { final a = FlowAnalyzer( availableCapabilities: () => const ['text.echo'], ); final r = await a.analyze(_wrap(''' name: x steps: - id: e use: text.echo@^2 ''')); expect(r.issues, isEmpty); }); test('empty installed list silences capability checks', () async { // When the host hasn't supplied a list (Studio still // booting, or running in CLI-only mode), we deliberately // don't false-positive every `use:` row. final a = FlowAnalyzer(availableCapabilities: () => const []); final r = await a.analyze(_wrap(''' steps: - id: e use: text.echo@^2 ''')); expect(r.issues, isEmpty); }); test('attaches InstallCapabilityFix when the capability is in the store', () async { final a = FlowAnalyzer( availableCapabilities: () => const ['debug.echo'], storeCapabilities: () => const ['text.classify'], ); final r = await a.analyze(_wrap(''' name: x steps: - id: c use: text.classify@^1 ''')); expect(r.issues, hasLength(1)); final fixes = a.fixesFor(r.issues.first); expect(fixes, hasLength(1)); final fix = fixes.first; expect(fix, isA()); expect((fix as InstallCapabilityFix).capability, 'text.classify@^1'); }); test('falls back to AddModuleSourceFix when unknown and not in store', () async { final a = FlowAnalyzer( availableCapabilities: () => const ['debug.echo'], storeCapabilities: () => const ['text.classify'], ); final r = await a.analyze(_wrap(''' name: x steps: - id: c use: htw.private/secret@^0.1 ''')); expect(r.issues, hasLength(1)); final fixes = a.fixesFor(r.issues.first); expect(fixes, hasLength(1)); expect(fixes.first, isA()); expect(r.issues.first.message, contains('Not in the store')); }); test('did-you-mean wins over install/add-source when a near miss exists', () async { // Installed bare `text.echo`; user typed `text.echi` (edit-distance 1). // The fix list should carry the Replace-as-primary; no install or // add-source needed because the analyzer already found the answer. final a = FlowAnalyzer( availableCapabilities: () => const ['text.echo@^0.1'], storeCapabilities: () => const [], ); final r = await a.analyze(_wrap(''' name: x steps: - id: t use: text.echi@^0.1 ''')); expect(r.issues, hasLength(1)); final fixes = a.fixesFor(r.issues.first); expect(fixes.first, isA()); // Replacement preserves the version constraint by reusing the // installed entry's `@^0.1`. expect( (fixes.first as ReplaceLineValueFix).replacement, 'text.echo@^0.1', ); }); test('records worst severity per step for canvas highlighting', () async { final a = FlowAnalyzer( availableCapabilities: () => const ['debug.echo'], storeCapabilities: () => const [], ); await a.analyze(_wrap(''' name: x inputs: pdf: byes steps: - id: bad use: nope@^1 ''')); expect(a.stepSeverity['bad'], IssueType.error); expect(a.stepSeverity[kInputsNodeId], IssueType.warning); }); test('suggests closest valid type for a misspelled value', () async { final a = FlowAnalyzer(availableCapabilities: () => const []); final r = await a.analyze(_wrap(''' name: x inputs: pdf: byes steps: [] ''')); expect(r.issues, hasLength(1)); final fixes = a.fixesFor(r.issues.first); expect(fixes, hasLength(1)); final fix = fixes.first; expect(fix, isA()); expect((fix as ReplaceLineValueFix).replacement, 'bytes'); expect(fix.line, r.issues.first.line); }); test('emits no fix when the typo is far from every known type', () async { final a = FlowAnalyzer(availableCapabilities: () => const []); final r = await a.analyze(_wrap(''' name: x inputs: pdf: zonglefax steps: [] ''')); expect(r.issues, hasLength(1)); final fixes = a.fixesFor(r.issues.first); expect(fixes, isEmpty); }); }); }