The flow editor is a Ch∆In Studio sub-component (chain product), not a F∆I-vendor package. Rename the package, entry library file, and all package: imports. flutter analyze: clean. Signed-off-by: flemming-it <sf@flemming.it>
222 lines
6.7 KiB
Dart
222 lines
6.7 KiB
Dart
// Behavioural tests for FlowAnalyzer — the YAML-aware
|
|
// AbstractAnalyzer that feeds the flow editor's gutter + the
|
|
// custom CodeController's wavy-underline overrides.
|
|
|
|
import 'package:chain_studio_flow_editor/src/flow_analyzer.dart';
|
|
import 'package:chain_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<InstallCapabilityFix>());
|
|
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<AddModuleSourceFix>());
|
|
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<ReplaceLineValueFix>());
|
|
// 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<ReplaceLineValueFix>());
|
|
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);
|
|
});
|
|
});
|
|
}
|