New test mirrors the most complex shipped example — three-step chain with a system.approval gate in the middle, multi-line prompts containing embedded \$step.field refs, outputs that reference multiple steps. Verifies: - step count + approval flagging - edge detection (8 edges across inputs, steps, outputs) - structural round-trip stability through toYaml() / fromYaml() If this test stays green, the editor is safe on every flow currently shipped under flows/. 12/12 pass. Signed-off-by: flemming-it <sf@flemming.it>
248 lines
6.7 KiB
Dart
248 lines
6.7 KiB
Dart
// Round-trip + edge-detection tests for FlowGraph. The graph
|
|
// model is the foundation under the WYSIWYG editor — a bug
|
|
// here corrupts every flow the operator opens, so the parser
|
|
// gets full coverage of the shipped example shapes.
|
|
|
|
import 'package:fai_studio_flow_editor/src/model/flow_graph.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
|
|
void main() {
|
|
group('FlowGraph parser', () {
|
|
test('parses the canonical hello flow', () {
|
|
const yaml = '''
|
|
name: hello
|
|
|
|
inputs:
|
|
name: text
|
|
|
|
steps:
|
|
- id: greet
|
|
use: debug.echo@^0
|
|
with:
|
|
message: "Hello, \$inputs.name"
|
|
|
|
outputs:
|
|
greeting: \$greet.echoed
|
|
''';
|
|
final graph = FlowGraph.fromYaml(yaml);
|
|
expect(graph.name, 'hello');
|
|
expect(graph.inputs.keys, contains('name'));
|
|
expect(graph.inputs['name']!.type, 'text');
|
|
expect(graph.steps, hasLength(1));
|
|
expect(graph.steps.first.id, 'greet');
|
|
expect(graph.steps.first.use, 'debug.echo@^0');
|
|
expect(graph.outputs['greeting'], '\$greet.echoed');
|
|
});
|
|
|
|
test('detects step-to-step + input-to-step edges', () {
|
|
const yaml = '''
|
|
inputs:
|
|
doc: bytes
|
|
|
|
steps:
|
|
- id: extract
|
|
use: text.extract@^0
|
|
with:
|
|
document: \$inputs.doc
|
|
- id: sum
|
|
use: text.summarize@^0
|
|
with:
|
|
text: \$extract.extracted
|
|
|
|
outputs:
|
|
result: \$sum.summary
|
|
''';
|
|
final graph = FlowGraph.fromYaml(yaml);
|
|
final edges = graph.edges;
|
|
expect(edges, hasLength(3));
|
|
// inputs.doc -> extract.document
|
|
expect(
|
|
edges.any(
|
|
(e) =>
|
|
e.fromKind == EdgeEndpointKind.inputs &&
|
|
e.fromField == 'doc' &&
|
|
e.toId == 'extract' &&
|
|
e.toField == 'document',
|
|
),
|
|
isTrue,
|
|
);
|
|
// extract.extracted -> sum.text
|
|
expect(
|
|
edges.any(
|
|
(e) =>
|
|
e.fromKind == EdgeEndpointKind.step &&
|
|
e.fromId == 'extract' &&
|
|
e.toId == 'sum' &&
|
|
e.toField == 'text',
|
|
),
|
|
isTrue,
|
|
);
|
|
// sum.summary -> outputs.result
|
|
expect(
|
|
edges.any(
|
|
(e) =>
|
|
e.fromKind == EdgeEndpointKind.step &&
|
|
e.fromId == 'sum' &&
|
|
e.toKind == EdgeEndpointKind.outputs &&
|
|
e.toField == 'result',
|
|
),
|
|
isTrue,
|
|
);
|
|
});
|
|
|
|
test('flags system.approval steps via isApproval', () {
|
|
const yaml = '''
|
|
steps:
|
|
- id: review
|
|
use: system.approval@^0
|
|
with:
|
|
prompt: "Approve?"
|
|
''';
|
|
final graph = FlowGraph.fromYaml(yaml);
|
|
expect(graph.steps.single.isApproval, isTrue);
|
|
});
|
|
|
|
test('captures leading comment block for round-trip', () {
|
|
const yaml = '''
|
|
# Top-of-file comment.
|
|
# Spans multiple lines.
|
|
|
|
name: x
|
|
inputs:
|
|
a: text
|
|
steps: []
|
|
outputs: {}
|
|
''';
|
|
final graph = FlowGraph.fromYaml(yaml);
|
|
expect(graph.leadingComment, contains('Top-of-file comment.'));
|
|
final round = graph.toYaml();
|
|
expect(round, contains('Top-of-file comment.'));
|
|
});
|
|
|
|
test('round-trips a multi-step flow byte-stably for the shape', () {
|
|
const yaml = '''
|
|
name: round-trip
|
|
|
|
inputs:
|
|
a: text
|
|
b: bytes
|
|
|
|
steps:
|
|
- id: one
|
|
use: text.extract@^0
|
|
with:
|
|
document: \$inputs.b
|
|
- id: two
|
|
use: llm.chat@^0
|
|
with:
|
|
prompt: \$one.extracted
|
|
|
|
outputs:
|
|
result: \$two.response
|
|
''';
|
|
final first = FlowGraph.fromYaml(yaml);
|
|
final emitted = first.toYaml();
|
|
final second = FlowGraph.fromYaml(emitted);
|
|
// Structural equality — names, IDs, edges
|
|
expect(second.name, first.name);
|
|
expect(second.inputs.keys, first.inputs.keys);
|
|
expect(second.steps.map((s) => s.id), first.steps.map((s) => s.id));
|
|
expect(second.steps.map((s) => s.use), first.steps.map((s) => s.use));
|
|
expect(second.outputs, first.outputs);
|
|
// Edge set identical
|
|
final firstEdges = first.edges
|
|
.map((e) => '${e.fromId}.${e.fromField}>${e.toId}.${e.toField}')
|
|
.toSet();
|
|
final secondEdges = second.edges
|
|
.map((e) => '${e.fromId}.${e.fromField}>${e.toId}.${e.toField}')
|
|
.toSet();
|
|
expect(secondEdges, firstEdges);
|
|
});
|
|
|
|
test('tolerates broken YAML — tryParse returns null without throwing', () {
|
|
const bad = '''
|
|
inputs: {
|
|
this is not yaml
|
|
''';
|
|
expect(FlowGraph.tryParse(bad), isNull);
|
|
});
|
|
|
|
test('immutable updates produce independent graphs', () {
|
|
const yaml = '''
|
|
steps:
|
|
- id: a
|
|
use: debug.echo@^0
|
|
- id: b
|
|
use: debug.echo@^0
|
|
''';
|
|
final graph = FlowGraph.fromYaml(yaml);
|
|
final updated = graph.withStepUpdated(
|
|
'a',
|
|
FlowStep(id: 'a', use: 'debug.echo@^1'),
|
|
);
|
|
expect(graph.steps.first.use, 'debug.echo@^0');
|
|
expect(updated.steps.first.use, 'debug.echo@^1');
|
|
});
|
|
|
|
test('handles the extract-with-approval shape (multi-step + approval)', () {
|
|
// Same shape as the shipped example flow — three
|
|
// steps, one of which is the system.approval gate,
|
|
// multi-line prompt with embedded \$step.field refs,
|
|
// outputs that reference multiple steps. If this
|
|
// round-trips structurally the editor is safe on the
|
|
// most complex shipped flow.
|
|
const yaml = '''
|
|
name: extract-with-approval
|
|
|
|
inputs:
|
|
document: bytes
|
|
llm_endpoint: text
|
|
llm_model: text
|
|
|
|
steps:
|
|
- id: extract
|
|
use: text.extract@^0
|
|
with:
|
|
document: \$inputs.document
|
|
|
|
- id: review
|
|
use: system.approval@^0
|
|
with:
|
|
prompt: "Approve before sending to the LLM."
|
|
show: \$extract.extracted
|
|
|
|
- id: summarize
|
|
use: llm.chat@^0
|
|
with:
|
|
prompt: "Summarise: \$extract.extracted"
|
|
endpoint: \$inputs.llm_endpoint
|
|
model: \$inputs.llm_model
|
|
|
|
outputs:
|
|
extracted: \$extract.extracted
|
|
approved_by: \$review.decided_by
|
|
summary: \$summarize.response
|
|
''';
|
|
final graph = FlowGraph.fromYaml(yaml);
|
|
expect(graph.steps, hasLength(3));
|
|
expect(graph.steps[1].isApproval, isTrue);
|
|
// Edges: 4 input wires + 3 output wires = 7
|
|
// - inputs.document -> extract.document
|
|
// - extract.extracted -> review.show
|
|
// - extract.extracted -> summarize.prompt
|
|
// - inputs.llm_endpoint -> summarize.endpoint
|
|
// - inputs.llm_model -> summarize.model
|
|
// - extract.extracted -> outputs.extracted
|
|
// - review.decided_by -> outputs.approved_by
|
|
// - summarize.response -> outputs.summary
|
|
// (8 edges total)
|
|
expect(graph.edges.length, greaterThanOrEqualTo(8));
|
|
// Round-trip stability: parse, emit, re-parse — same
|
|
// structural shape.
|
|
final round = FlowGraph.fromYaml(graph.toYaml());
|
|
expect(round.steps.map((s) => s.id), graph.steps.map((s) => s.id));
|
|
expect(round.outputs.keys, graph.outputs.keys);
|
|
expect(round.edges.length, graph.edges.length);
|
|
});
|
|
});
|
|
}
|