// Widget-level proof of the sample handling contract (persona // review 2026-08-27 / sealed-areas rework): // // * The host's sample set (the hub's FlowSummary.sample wire // flag) is the ONLY source of the "Example" chip — no host // info, no chips. The editor's old content-marker scan had // silently drifted from the hub's header and is gone. // * Sample flows collapse under one "Examples (N)" group so // they are never mistaken for the operator's own work; the // group expands only on demand (or when nothing else exists). // * An empty list offers "Import example flows" iff the host // wired the action (sealed areas start empty on purpose). // // Hermetic — temp flows dir, never ~/.chain. import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:chain_studio_flow_editor/chain_studio_flow_editor.dart'; Future _pumpEditor( WidgetTester tester, { required String flowsDir, Set? sampleFlowNames, Future Function()? onImportSamples, }) async { await tester.runAsync(() async { await tester.pumpWidget( MaterialApp( home: FlowEditorPage( flowsDir: flowsDir, sampleFlowNames: sampleFlowNames, onImportSamples: onImportSamples, ), ), ); await Future.delayed(const Duration(milliseconds: 100)); }); await tester.pump(); await tester.pump(); } void main() { late Directory tmp; setUp(() { tmp = Directory.systemTemp.createTempSync('chain_editor_samples_test'); File('${tmp.path}/my-work.yaml').writeAsStringSync( 'name: my-work\nsteps: []\n', ); File('${tmp.path}/hello.yaml').writeAsStringSync( 'name: hello\nsteps: []\n', ); }); tearDown(() { tmp.deleteSync(recursive: true); }); testWidgets('host-reported samples collapse under the examples group', ( tester, ) async { await _pumpEditor( tester, flowsDir: tmp.path, sampleFlowNames: const {'hello'}, ); // Own flow visible, sample hidden behind the collapsed group. expect(find.text('my-work'), findsOneWidget); expect(find.text('hello'), findsNothing); expect(find.text('Examples (1)'), findsOneWidget); await tester.tap(find.text('Examples (1)')); await tester.pump(); expect(find.text('hello'), findsOneWidget); // The revealed sample row carries the chip. expect(find.text('Example'), findsOneWidget); }); testWidgets('no host sample info means no chips and no group', ( tester, ) async { await _pumpEditor(tester, flowsDir: tmp.path, sampleFlowNames: null); expect(find.text('my-work'), findsOneWidget); expect(find.text('hello'), findsOneWidget); expect(find.text('Example'), findsNothing); expect(find.textContaining('Examples ('), findsNothing); }); testWidgets('a samples-only list starts expanded', (tester) async { File('${tmp.path}/my-work.yaml').deleteSync(); await _pumpEditor( tester, flowsDir: tmp.path, sampleFlowNames: const {'hello'}, ); expect(find.text('hello'), findsOneWidget); expect(find.text('Examples (1)'), findsOneWidget); }); testWidgets('the empty list offers the import action when wired', ( tester, ) async { File('${tmp.path}/my-work.yaml').deleteSync(); File('${tmp.path}/hello.yaml').deleteSync(); var imported = 0; await _pumpEditor( tester, flowsDir: tmp.path, onImportSamples: () async { imported++; File('${tmp.path}/hello.yaml').writeAsStringSync( 'name: hello\nsteps: []\n', ); }, ); final button = find.text('Import example flows'); expect(button, findsOneWidget); await tester.runAsync(() async { await tester.tap(button); // Let the import callback, the directory re-list (real IO) // and the FutureBuilder's completion callback all run. await Future.delayed(const Duration(milliseconds: 200)); await tester.pump(); await Future.delayed(const Duration(milliseconds: 100)); }); await tester.pump(); await tester.pump(); expect(imported, 1); // The imported flow appears without a manual refresh. expect(find.text('hello'), findsOneWidget); }); testWidgets('the empty list stays action-free when nothing is wired', ( tester, ) async { File('${tmp.path}/my-work.yaml').deleteSync(); File('${tmp.path}/hello.yaml').deleteSync(); await _pumpEditor(tester, flowsDir: tmp.path); expect(find.text('Import example flows'), findsNothing); }); }