fix(editor): match real F∆I YAML type shape + warn on bad types

The 0.15.0 type-token coloring regex hunted for `type: <name>`,
but the F∆I YAML actually shapes types as `<fieldname>: <name>`
under `inputs:` / `outputs:` blocks. Real-world flows
(`hello.yaml`, `extract.yaml`) and module manifests (echo,
text-summarize, …) never produce the `type:` keyword unless
the operator hand-authors the long form.

Fix the regex to match an indented `KEY: VALUE` line where the
value is one of `text|json|bytes|file|number|integer`. Leading
whitespace is required so top-level keys (`name: foo`,
`version: 0.1.0`) can't false-match. This handles both
shapes — the implicit `pdf: bytes` and the explicit
`type: bytes` (used by module schema v2 inputs lists) —
because either way the pattern boils down to "key colon known
type token".

Analyzer also grows a type-token check: any inputs/outputs
field whose value isn't a known type lights up as a warning
("Unknown input type 'byes' …"), modulo `$ref` expressions
(flow outputs) and empty values (operator is mid-keystroke).

Adds `test/flow_analyzer_test.dart` with seven cases covering
empty, valid hello, unknown capability, unknown type, parse
error, version-bare matching, and the empty-installed-list
silence path.

Bumps the package to 0.15.1.

Signed-off-by: flemming-it <sf@flemming.it>
This commit is contained in:
flemming-it 2026-06-04 02:24:29 +02:00
parent 885d2db4e1
commit 911f368362
4 changed files with 162 additions and 7 deletions

View file

@ -0,0 +1,107 @@
// 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: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);
});
});
}