import 'package:cyberhybridhub/admin/models/market_history_admin_config.dart'; import 'package:cyberhybridhub/admin/models/sync_run_event.dart'; import 'package:cyberhybridhub/admin/repositories/sync_run_log_repository.dart'; import 'package:cyberhybridhub/admin/screens/market_history_log_screen.dart'; import 'package:cyberhybridhub/admin/widgets/sync_run_expansion_tile.dart'; import 'package:cyberhybridhub/theme/app_theme.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; /// FLUTTER-TDD-PLAN.md Section 9 — automated definition-of-done checks. class _AcceptanceController implements SyncRunLogController { _AcceptanceController(this._states); final List _states; int _index = 0; bool? lastCleanupArchive; SyncRunLogRepositoryState _current = const SyncRunLogRepositoryState( pinned: [], history: [], nextBefore: null, isLoading: true, errorMessage: null, ); @override SyncRunLogRepositoryState get state => _current; @override Future loadInitial({int limit = 50}) async { _current = _states[_index]; return _current; } @override Future refresh({int limit = 50}) async { _index = 0; _current = _states[_index]; return _current; } @override Future loadMore({int limit = 50}) async { if (_index < _states.length - 1) { _index++; } _current = _states[_index]; return _current; } @override Future triggerResync() => refresh(); @override Future triggerCleanup({bool archive = false}) async { lastCleanupArchive = archive; return refresh(); } } SyncRunEvent _run({ required int id, required String kind, required DateTime startedAt, String? error, SyncRunSeverity severity = SyncRunSeverity.ok, SyncRunStatus status = SyncRunStatus.success, }) { return SyncRunEvent( id: id, kind: kind, startedAt: startedAt, finishedAt: startedAt.add(const Duration(minutes: 1)), rowsWritten: kind == 'cleanup' ? 0 : 100, rowsRemoved: kind == 'cleanup' ? 50 : 0, error: error, severity: severity, status: status, durationMs: 60000, summary: error ?? 'summary $id', ); } Future _pump( WidgetTester tester, _AcceptanceController controller, ) async { await tester.pumpWidget( MaterialApp( theme: buildAppTheme(), home: MarketHistoryLogScreen( controller: controller, now: DateTime.utc(2026, 5, 27, 12), autoRefreshInterval: null, ), ), ); await tester.pumpAndSettle(); } void main() { testWidgets('newest history row appears first', (WidgetTester tester) async { final DateTime t0 = DateTime.utc(2026, 5, 27, 10); final _AcceptanceController controller = _AcceptanceController( [ SyncRunLogRepositoryState( pinned: const [], history: [ _run(id: 2, kind: 'cleanup', startedAt: t0, error: null), _run( id: 1, kind: 'backfill', startedAt: t0.subtract(const Duration(hours: 2)), ), ], nextBefore: null, isLoading: false, errorMessage: null, ), ], ); await _pump(tester, controller); expect(find.textContaining('summary 2'), findsOneWidget); }); testWidgets('pinned unresolved failures appear in needs attention', ( WidgetTester tester, ) async { final DateTime t0 = DateTime.utc(2026, 5, 27, 10); final _AcceptanceController controller = _AcceptanceController( [ SyncRunLogRepositoryState( pinned: [ _run( id: 9, kind: 'backfill', startedAt: t0, error: '429 rate limited', severity: SyncRunSeverity.rateLimit, status: SyncRunStatus.failed, ), ], history: const [], nextBefore: null, isLoading: false, errorMessage: null, ), ], ); await _pump(tester, controller); expect(find.byKey(const Key('pinned-section')), findsOneWidget); expect(find.byKey(const Key('sync-run-9')), findsOneWidget); }); testWidgets('expanded row shows full error without raw payload labels', ( WidgetTester tester, ) async { const String errorText = 'AlpacaMarketDataException: batch failed 500'; final _AcceptanceController controller = _AcceptanceController( [ SyncRunLogRepositoryState( pinned: [ _run( id: 11, kind: 'backfill', startedAt: DateTime.utc(2026, 5, 27, 10), error: errorText, severity: SyncRunSeverity.error, status: SyncRunStatus.failed, ), ], history: const [], nextBefore: null, isLoading: false, errorMessage: null, ), ], ); await _pump(tester, controller); await tester.tap(find.byType(ExpansionTile)); await tester.pumpAndSettle(); expect(find.text(errorText), findsWidgets); expect(find.textContaining('"raw"'), findsNothing); expect(find.textContaining('bars'), findsNothing); }); testWidgets('cleanup archive toggle passes archive flag when enabled', ( WidgetTester tester, ) async { final DateTime t0 = DateTime.utc(2026, 5, 27, 10); final _AcceptanceController controller = _AcceptanceController( [ SyncRunLogRepositoryState( pinned: const [], history: [_run(id: 3, kind: 'cleanup', startedAt: t0)], nextBefore: null, isLoading: false, errorMessage: null, config: const MarketHistoryAdminConfig( archiveEnabled: true, windowDays: 5, retentionDays: 5, syncEnabled: true, ), ), ], ); await _pump(tester, controller); await tester.tap(find.byKey(const Key('actions-menu'))); await tester.pumpAndSettle(); await tester.tap(find.byKey(const Key('action-cleanup'))); await tester.pumpAndSettle(); await tester.tap(find.byKey(const Key('cleanup-archive-toggle'))); await tester.pumpAndSettle(); await tester.tap(find.byKey(const Key('cleanup-confirm'))); await tester.pumpAndSettle(); expect(controller.lastCleanupArchive, isTrue); }); }