From 5c5ec4f990d67ee7edf75b30e4fcbd8e74677aaa Mon Sep 17 00:00:00 2001 From: flemming-it Date: Tue, 9 Jun 2026 09:32:13 +0200 Subject: [PATCH] test(editor): cover quick-fix exposure + driver approval contract MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two new test files: - fix_dialog_test.dart — surface tests for the controller API the new Fix dialog reads: fixesFor() returns the analyzer- attached list, analyzerErrorCount tracks errors, and the English AnalyzerStrings produce the labels the dialog renders ('Install ', 'Add source for …', etc). - inline_approval_test.dart — covers the FlowRunDriver approval hooks added in 0.21.0. Validates the default stubs throw UnsupportedError (so legacy hosts surface in CI rather than at runtime) and that a fake driver implementation can drive the happy + reject paths. 44 editor tests green. Signed-off-by: flemming-it --- test/fix_dialog_test.dart | 67 +++++++++++++++++ test/inline_approval_test.dart | 134 +++++++++++++++++++++++++++++++++ 2 files changed, 201 insertions(+) create mode 100644 test/fix_dialog_test.dart create mode 100644 test/inline_approval_test.dart diff --git a/test/fix_dialog_test.dart b/test/fix_dialog_test.dart new file mode 100644 index 0000000..2e38494 --- /dev/null +++ b/test/fix_dialog_test.dart @@ -0,0 +1,67 @@ +// Surface-level tests for the diagnostic-strip Fix dialog +// support. The dialog itself is a private widget; this file +// exercises the controller-side API the dialog reads (issues, +// fixesFor, analyzerErrorCount, AnalyzerStrings) so a future +// regression on the data flow surfaces in CI. + +import 'package:fai_studio_flow_editor/src/flow_yaml_controller.dart'; +import 'package:fai_studio_flow_editor/src/l10n.dart'; +import 'package:fai_studio_flow_editor/src/quick_fix.dart'; +import 'package:flutter_code_editor/flutter_code_editor.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('FlowYamlCodeController quick-fix exposure', () { + test('fixesFor returns the analyzer-attached list', () async { + final c = FlowYamlCodeController( + availableCapabilities: () => const ['debug.echo'], + storeCapabilities: () => const ['text.classify'], + ); + addTearDown(c.dispose); + c.fullText = ''' +name: hello +steps: + - id: c + use: text.classify@^1 +'''; + // Wait for the analyzer's debounce. + await Future.delayed(const Duration(milliseconds: 700)); + final issues = c.analysisResult.issues; + expect( + issues, + isNotEmpty, + reason: 'analyzer should flag the unknown capability', + ); + final fixes = c.fixesFor(issues.first); + expect(fixes, hasLength(1)); + expect(fixes.first, isA()); + }); + + test('analyzerErrorCount tracks error severity', () async { + final c = FlowYamlCodeController( + availableCapabilities: () => const ['debug.echo'], + ); + addTearDown(c.dispose); + expect(c.analysisResult.issues, isEmpty); + c.fullText = ''' +steps: + - id: x + use: nope@^1 +'''; + await Future.delayed(const Duration(milliseconds: 700)); + final errors = c.analysisResult.issues + .where((i) => i.type == IssueType.error) + .length; + expect(errors, greaterThan(0)); + }); + + test('AnalyzerStrings.english produces the expected fix labels', () { + final strings = AnalyzerStrings.english; + expect(strings.fixInstallCap('text.echo'), 'Install text.echo'); + expect(strings.fixAddSource('text.echo'), 'Add source for text.echo…'); + expect(strings.fixChangeTo('bytes'), 'Change to "bytes"'); + expect(strings.fixUseInstead('text.echo@^0.1'), + 'Use "text.echo@^0.1"'); + }); + }); +} diff --git a/test/inline_approval_test.dart b/test/inline_approval_test.dart new file mode 100644 index 0000000..1067367 --- /dev/null +++ b/test/inline_approval_test.dart @@ -0,0 +1,134 @@ +// Surface-level tests for the FlowRunDriver approval hooks +// added in editor 0.21.0. The card itself is a private widget; +// these tests cover the driver-side contract that the card +// depends on so that legacy / mis-implementing hosts surface +// in CI rather than at runtime. + +import 'dart:async'; +import 'dart:typed_data'; + +import 'package:fai_studio_flow_editor/src/run_driver.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('FlowRunDriver default approval implementation', () { + test('pendingApprovalIdForStep returns null when not overridden', + () async { + final d = _NullDriver(); + final id = await d.pendingApprovalIdForStep( + flowName: 'f', + stepId: 's', + ); + expect(id, isNull); + }); + + test('approveApproval throws UnsupportedError by default', + () async { + final d = _NullDriver(); + await expectLater( + d.approveApproval(approvalId: 'x', reviewer: 'r'), + throwsA(isA()), + ); + }); + + test('rejectApproval throws UnsupportedError by default', + () async { + final d = _NullDriver(); + await expectLater( + d.rejectApproval(approvalId: 'x', reviewer: 'r', reason: 'y'), + throwsA(isA()), + ); + }); + }); + + group('FlowRunDriver fake host implementation', () { + test('full happy path: lookup → approve → state observable', + () async { + final d = _FakeApprovalDriver(); + d.pending['flowA|stepX'] = 'a-123'; + + final id = await d.pendingApprovalIdForStep( + flowName: 'flowA', + stepId: 'stepX', + ); + expect(id, 'a-123'); + + await d.approveApproval(approvalId: 'a-123', reviewer: 'alice'); + expect(d.approvals, hasLength(1)); + expect(d.approvals.first, ('a-123', 'alice')); + }); + + test('reject path carries reason through', () async { + final d = _FakeApprovalDriver(); + d.pending['flowA|stepX'] = 'a-123'; + + await d.rejectApproval( + approvalId: 'a-123', + reviewer: 'alice', + reason: 'missing signature', + ); + expect(d.rejections, hasLength(1)); + expect(d.rejections.first.$3, 'missing signature'); + }); + }); +} + +/// Minimal driver that overrides nothing. Used to verify the +/// default-throw stubs from `run_driver.dart`. +class _NullDriver extends FlowRunDriver { + @override + Future> runFlow({ + required String flowName, + required Map textInputs, + required Map fileInputs, + required Map fileMimes, + }) async => + const {}; + + @override + Stream events() => const Stream.empty(); +} + +/// Studio-shaped driver mock — implements every approval hook +/// + tracks side effects so tests can assert on them. +class _FakeApprovalDriver extends FlowRunDriver { + final Map pending = {}; + final List<(String, String)> approvals = []; + final List<(String, String, String)> rejections = []; + + @override + Future> runFlow({ + required String flowName, + required Map textInputs, + required Map fileInputs, + required Map fileMimes, + }) async => + const {}; + + @override + Stream events() => const Stream.empty(); + + @override + Future pendingApprovalIdForStep({ + required String flowName, + required String stepId, + }) async => + pending['$flowName|$stepId']; + + @override + Future approveApproval({ + required String approvalId, + required String reviewer, + }) async { + approvals.add((approvalId, reviewer)); + } + + @override + Future rejectApproval({ + required String approvalId, + required String reviewer, + required String reason, + }) async { + rejections.add((approvalId, reviewer, reason)); + } +}