chain-studio-flow-editor/test/flow_graph_test.dart
flemming-it ad2e5b50d5 test(editor): exercise the extract-with-approval flow shape
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>
2026-06-01 02:01:46 +02:00

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);
});
});
}