226 lines
6.6 KiB
Dart
226 lines
6.6 KiB
Dart
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<SyncRunLogRepositoryState> _states;
|
|
int _index = 0;
|
|
bool? lastCleanupArchive;
|
|
|
|
SyncRunLogRepositoryState _current = const SyncRunLogRepositoryState(
|
|
pinned: <SyncRunEvent>[],
|
|
history: <SyncRunEvent>[],
|
|
nextBefore: null,
|
|
isLoading: true,
|
|
errorMessage: null,
|
|
);
|
|
|
|
@override
|
|
SyncRunLogRepositoryState get state => _current;
|
|
|
|
@override
|
|
Future<SyncRunLogRepositoryState> loadInitial({int limit = 50}) async {
|
|
_current = _states[_index];
|
|
return _current;
|
|
}
|
|
|
|
@override
|
|
Future<SyncRunLogRepositoryState> refresh({int limit = 50}) async {
|
|
_index = 0;
|
|
_current = _states[_index];
|
|
return _current;
|
|
}
|
|
|
|
@override
|
|
Future<SyncRunLogRepositoryState> loadMore({int limit = 50}) async {
|
|
if (_index < _states.length - 1) {
|
|
_index++;
|
|
}
|
|
_current = _states[_index];
|
|
return _current;
|
|
}
|
|
|
|
@override
|
|
Future<SyncRunLogRepositoryState> triggerResync() => refresh();
|
|
|
|
@override
|
|
Future<SyncRunLogRepositoryState> 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<void> _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>[
|
|
SyncRunLogRepositoryState(
|
|
pinned: const <SyncRunEvent>[],
|
|
history: <SyncRunEvent>[
|
|
_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>[
|
|
SyncRunLogRepositoryState(
|
|
pinned: <SyncRunEvent>[
|
|
_run(
|
|
id: 9,
|
|
kind: 'backfill',
|
|
startedAt: t0,
|
|
error: '429 rate limited',
|
|
severity: SyncRunSeverity.rateLimit,
|
|
status: SyncRunStatus.failed,
|
|
),
|
|
],
|
|
history: const <SyncRunEvent>[],
|
|
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>[
|
|
SyncRunLogRepositoryState(
|
|
pinned: <SyncRunEvent>[
|
|
_run(
|
|
id: 11,
|
|
kind: 'backfill',
|
|
startedAt: DateTime.utc(2026, 5, 27, 10),
|
|
error: errorText,
|
|
severity: SyncRunSeverity.error,
|
|
status: SyncRunStatus.failed,
|
|
),
|
|
],
|
|
history: const <SyncRunEvent>[],
|
|
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>[
|
|
SyncRunLogRepositoryState(
|
|
pinned: const <SyncRunEvent>[],
|
|
history: <SyncRunEvent>[_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);
|
|
});
|
|
}
|