import 'dart:async'; import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:term_remote_ctl/app/app.dart'; import 'package:term_remote_ctl/core/network/agent_api_client.dart'; import 'package:term_remote_ctl/core/network/agent_connection_providers.dart'; import 'package:term_remote_ctl/features/projects/project.dart'; import 'package:term_remote_ctl/features/projects/project_detail_page.dart'; import 'package:term_remote_ctl/features/projects/project_repository.dart'; import 'package:term_remote_ctl/features/sessions/session.dart'; import 'package:term_remote_ctl/features/sessions/session_list_page.dart'; import 'package:term_remote_ctl/features/sessions/session_repository.dart'; import 'package:term_remote_ctl/features/terminal/terminal_page.dart'; import 'package:term_remote_ctl/features/terminal/terminal_socket_session.dart'; import 'package:xterm/xterm.dart'; void main() { testWidgets('shows the project-first shell', (tester) async { await _pumpApp( tester, projectRepository: _FakeProjectRepository(), sessionRepository: _FakeSessionRepository(), ); final materialApp = tester.widget(find.byType(MaterialApp)); final agentUrlField = tester.widget( find.byType(TextField).first, ); expect(find.text('Projects'), findsOneWidget); expect(materialApp.theme?.brightness, Brightness.dark); expect(materialApp.theme?.scaffoldBackgroundColor, isNot(Colors.white)); expect(materialApp.theme?.scaffoldBackgroundColor, const Color(0xFF05070A)); expect(materialApp.theme?.colorScheme.primary, const Color(0xFFC2A574)); expect(find.byKey(const Key('project_page_header')), findsOneWidget); expect(find.text('Agent base URL'), findsOneWidget); expect(agentUrlField.controller?.text, 'http://100.81.30.82:5067'); expect(agentUrlField.decoration?.hintText, 'http://100.81.30.82:5067'); expect(find.byKey(const Key('agent_connection_readout')), findsOneWidget); expect( find.text( 'Project requests use this base origin: http://100.81.30.82:5067.', ), findsOneWidget, ); expect(find.text('codex-main'), findsOneWidget); expect(find.text(r'C:\repo\codex-main'), findsOneWidget); expect( find.text('Launch terminals in known working directories.'), findsOneWidget, ); expect(find.text('Stored terminal workspaces'), findsOneWidget); expect(find.byKey(const Key('project_tile_project-1')), findsOneWidget); expect(find.widgetWithText(FilledButton, 'Open terminal'), findsOneWidget); }); testWidgets('project card opens a terminal without an extra prompt', ( tester, ) async { final sessionRepository = _FakeSessionRepository(); await _pumpApp( tester, projectRepository: _FakeProjectRepository(), sessionRepository: sessionRepository, ); await _openProjectTerminal(tester); expect(sessionRepository.lastCreatedProjectId, 'project-1'); expect(find.text('codex-main'), findsOneWidget); expect(find.text(r'C:\repo\codex-main'), findsOneWidget); expect(find.byKey(const Key('terminal_header_panel')), findsOneWidget); expect(find.byKey(const Key('terminal_surface_panel')), findsOneWidget); expect(find.byKey(const Key('terminal_command_deck')), findsOneWidget); expect(find.byKey(const Key('terminal_status_summary')), findsOneWidget); expect( find.byKey(const Key('terminal_toggle_actions_button')), findsOneWidget, ); }); testWidgets('project list deletes a project after confirmation', ( tester, ) async { final projectRepository = _FakeProjectRepository.withProjects([ Project( projectId: 'project-1', name: 'codex-main', workingDirectory: r'C:\repo\codex-main', createdAtUtc: DateTime.parse('2026-03-30T10:00:00Z'), updatedAtUtc: DateTime.parse('2026-03-30T10:00:00Z'), ), ]); await _pumpApp( tester, projectRepository: projectRepository, sessionRepository: _FakeSessionRepository(), ); expect(find.text('codex-main'), findsOneWidget); await tester.tap(find.byKey(const Key('project_delete_button_project-1'))); await tester.pumpAndSettle(); await tester.tap(find.widgetWithText(FilledButton, 'Delete')); await tester.pumpAndSettle(); expect(projectRepository.deletedProjectIds, ['project-1']); expect(find.text('codex-main'), findsNothing); }); testWidgets( 'project card shows recent sessions, expands, and deletes a single session', (tester) async { final projectRepository = _FakeProjectRepository.withRecentSessions({ 'project-1': [ _session('session-1', 'alpha'), _session('session-2', 'beta'), _session('session-3', 'gamma'), _session('session-4', 'delta'), ], }); final sessionRepository = _FakeSessionRepository.withSessions([ _session('session-1', 'alpha'), _session('session-2', 'beta'), _session('session-3', 'gamma'), _session('session-4', 'delta'), ])..onDeleteSession = projectRepository.removeRecentSession; await _pumpApp( tester, projectRepository: projectRepository, sessionRepository: sessionRepository, ); expect(find.text('alpha'), findsOneWidget); expect(find.text('beta'), findsOneWidget); expect(find.text('gamma'), findsOneWidget); expect(find.text('delta'), findsNothing); expect(find.text('Recent sessions'), findsOneWidget); expect(find.text('Show more'), findsOneWidget); await tester.ensureVisible(find.text('Show more')); await tester.pumpAndSettle(); await tester.tap(find.text('Show more')); await tester.pumpAndSettle(); expect(find.text('delta'), findsOneWidget); await tester.ensureVisible( find.byKey(const Key('project_session_delete_session-2')), ); await tester.tap( find.byKey(const Key('project_session_delete_session-2')), ); await tester.pumpAndSettle(); await tester.tap(find.widgetWithText(FilledButton, 'Delete')); await tester.pumpAndSettle(); expect(sessionRepository.deletedSessionIds, contains('session-2')); expect(find.text('beta'), findsNothing); }, ); testWidgets('project detail page avoids overflow on narrow screens', ( tester, ) async { tester.view.devicePixelRatio = 1; tester.view.physicalSize = const Size(393, 852); addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); await tester.pumpWidget( ProviderScope( overrides: [ agentApiClientProvider.overrideWithValue(_FakeAgentApiClient()), projectRepositoryProvider.overrideWithValue( _FakeProjectRepository.withRecentSessions({ 'project-1': [_session('session-1', 'alpha')], }), ), sessionRepositoryProvider.overrideWithValue(_FakeSessionRepository()), terminalSocketSessionFactoryProvider.overrideWithValue( TerminalSocketSessionFactory( transportFactory: (_) => _FakeTerminalSocketTransport(autoAttach: true), ), ), ], child: MaterialApp( home: ProjectDetailPage( project: Project( projectId: 'project-1', name: 'tt', workingDirectory: r'D:\App\python\robot_face_rec', createdAtUtc: DateTime.parse('2026-03-30T10:00:00Z'), updatedAtUtc: DateTime.parse('2026-03-30T10:00:00Z'), ), ), ), ), ); await tester.pumpAndSettle(); expect(tester.takeException(), isNull); expect(find.text('Recent sessions'), findsOneWidget); }); testWidgets( 'terminal page keeps tools hidden until the user opens the tools sheet', (tester) async { await _pumpApp( tester, projectRepository: _FakeProjectRepository(), sessionRepository: _FakeSessionRepository(), ); await _openProjectTerminal(tester); expect(find.byKey(const Key('terminal_tools_sheet')), findsNothing); expect( find.byKey(const Key('terminal_toggle_actions_button')), findsOneWidget, ); expect(find.byKey(const Key('terminal_action_bar')), findsOneWidget); expect(find.byKey(const Key('terminal_send_button')), findsOneWidget); expect(find.byKey(const Key('terminal_command_deck')), findsOneWidget); expect(find.text('Browse mode'), findsOneWidget); await tester.tap(find.byKey(const Key('terminal_toggle_actions_button'))); await tester.pumpAndSettle(); expect(find.byKey(const Key('terminal_tools_sheet')), findsOneWidget); expect(find.text('Reconnect'), findsOneWidget); expect(find.text('Latest'), findsOneWidget); }, ); testWidgets('terminal tools expose quick terminal keys', (tester) async { final transportFactory = _QueuedTerminalSocketTransportFactory(); await _pumpApp( tester, projectRepository: _FakeProjectRepository(), sessionRepository: _FakeSessionRepository(), socketFactory: TerminalSocketSessionFactory( transportFactory: transportFactory.create, ), ); await _openProjectTerminal(tester); expect(find.byKey(const Key('terminal_quick_key_esc')), findsOneWidget); expect(find.byKey(const Key('terminal_quick_key_tab')), findsOneWidget); expect(find.byKey(const Key('terminal_quick_key_ctrl_c')), findsOneWidget); expect(find.byKey(const Key('terminal_quick_key_ctrl_d')), findsOneWidget); expect(find.byKey(const Key('terminal_quick_key_ctrl_l')), findsOneWidget); expect(find.byKey(const Key('terminal_quick_key_up')), findsOneWidget); expect(find.byKey(const Key('terminal_quick_key_down')), findsOneWidget); expect(find.byKey(const Key('terminal_quick_key_left')), findsOneWidget); expect(find.byKey(const Key('terminal_quick_key_right')), findsOneWidget); await tester.tap(find.byKey(const Key('terminal_quick_key_esc'))); await tester.pump(); expect( transportFactory.createdTransports.single.sentMessages.last, contains('"input":"\\u001b"'), ); }); testWidgets('terminal direct input mode is explicit and toggleable', ( tester, ) async { await _pumpApp( tester, projectRepository: _FakeProjectRepository(), sessionRepository: _FakeSessionRepository(), ); await _openProjectTerminal(tester); expect(find.text('Browse mode'), findsOneWidget); expect(find.text('Direct input enabled'), findsNothing); await tester.tap(find.byKey(const Key('terminal_direct_input_toggle'))); await tester.pumpAndSettle(); expect(find.text('Direct input enabled'), findsOneWidget); expect(find.text('Browse mode'), findsNothing); await tester.tap(find.byKey(const Key('terminal_direct_input_toggle'))); await tester.pumpAndSettle(); expect(find.text('Browse mode'), findsOneWidget); }); testWidgets( 'project launch surfaces a friendly message when the working directory is invalid', (tester) async { await _pumpApp( tester, projectRepository: _FakeProjectRepository(), sessionRepository: _FailingSessionRepository(), ); await tester.tap(find.widgetWithText(FilledButton, 'Open terminal')); await tester.pumpAndSettle(); expect( find.text( 'Working directory does not exist. Update the project path and try again.', ), findsOneWidget, ); }, ); testWidgets( 'terminal page shows diagnostic entries for input and output activity', (tester) async { final transportFactory = _QueuedTerminalSocketTransportFactory(); await _pumpApp( tester, projectRepository: _FakeProjectRepository(), sessionRepository: _FakeSessionRepository(), socketFactory: TerminalSocketSessionFactory( transportFactory: transportFactory.create, ), ); await _openProjectTerminal(tester); await tester.enterText(find.byType(TextField).last, 'dir'); await tester.tap(find.byKey(const Key('terminal_send_button'))); await tester.pumpAndSettle(); await tester.tap(find.byKey(const Key('terminal_quick_key_ctrl_l'))); await tester.pumpAndSettle(); transportFactory.createdTransports.single.emit('command-output'); await tester.pumpAndSettle(); await tester.tap(find.byKey(const Key('terminal_toggle_actions_button'))); await tester.pumpAndSettle(); await tester.tap(find.byKey(const Key('terminal_diagnostics_button'))); await tester.pumpAndSettle(); expect(find.textContaining('ui.input.quick | Ctrl+L'), findsOneWidget); expect(find.textContaining('socket.input.tx | '), findsWidgets); expect( find.textContaining('socket.frame.rx | command-output'), findsOneWidget, ); }, ); testWidgets('terminal send keeps the command input focused', (tester) async { final transportFactory = _QueuedTerminalSocketTransportFactory(); await _pumpApp( tester, projectRepository: _FakeProjectRepository(), sessionRepository: _FakeSessionRepository(), socketFactory: TerminalSocketSessionFactory( transportFactory: transportFactory.create, ), ); await _openProjectTerminal(tester); final commandField = find.byType(TextField).last; final editableField = find.byType(EditableText).last; await tester.tap(commandField); await tester.pumpAndSettle(); expect( tester.widget(editableField).focusNode.hasFocus, isTrue, ); await tester.enterText(commandField, 'dir'); await tester.tap(find.byKey(const Key('terminal_send_button'))); await tester.pumpAndSettle(); expect( tester.widget(editableField).focusNode.hasFocus, isTrue, ); expect(tester.widget(commandField).controller?.text, isEmpty); }); testWidgets('terminal keyboard submit keeps the command input focused', ( tester, ) async { final transportFactory = _QueuedTerminalSocketTransportFactory(); await _pumpApp( tester, projectRepository: _FakeProjectRepository(), sessionRepository: _FakeSessionRepository(), socketFactory: TerminalSocketSessionFactory( transportFactory: transportFactory.create, ), ); await _openProjectTerminal(tester); final commandField = find.byType(TextField).last; final editableField = find.byType(EditableText).last; await tester.tap(commandField); await tester.pumpAndSettle(); await tester.enterText(commandField, 'dir'); await tester.testTextInput.receiveAction(TextInputAction.send); await tester.pumpAndSettle(); expect( tester.widget(editableField).focusNode.hasFocus, isTrue, ); expect(tester.widget(commandField).controller?.text, isEmpty); }); testWidgets('terminal done action keeps the command input focused', ( tester, ) async { final transportFactory = _QueuedTerminalSocketTransportFactory(); await _pumpApp( tester, projectRepository: _FakeProjectRepository(), sessionRepository: _FakeSessionRepository(), socketFactory: TerminalSocketSessionFactory( transportFactory: transportFactory.create, ), ); await _openProjectTerminal(tester); final commandField = find.byType(TextField).last; final editableField = find.byType(EditableText).last; await tester.tap(commandField); await tester.pumpAndSettle(); await tester.enterText(commandField, 'dir'); await tester.testTextInput.receiveAction(TextInputAction.done); await tester.pumpAndSettle(); expect( tester.widget(editableField).focusNode.hasFocus, isTrue, ); expect(tester.widget(commandField).controller?.text, isEmpty); }); testWidgets('terminal direct input keyboard action sends carriage return', ( tester, ) async { final transportFactory = _QueuedTerminalSocketTransportFactory(); await _pumpApp( tester, projectRepository: _FakeProjectRepository(), sessionRepository: _FakeSessionRepository(), socketFactory: TerminalSocketSessionFactory( transportFactory: transportFactory.create, ), ); await _openProjectTerminal(tester); final commandField = find.byType(TextField).last; final editableField = find.byType(EditableText).last; await tester.tap(find.byKey(const Key('terminal_direct_input_toggle'))); await tester.pumpAndSettle(); await tester.tap(commandField); await tester.pumpAndSettle(); await tester.enterText(commandField, 'pwd'); await tester.pumpAndSettle(); await tester.testTextInput.receiveAction(TextInputAction.done); await tester.pumpAndSettle(); expect( transportFactory.createdTransports.single.sentMessages.last, contains(r'"input":"\r"'), ); expect( tester.widget(editableField).focusNode.hasFocus, isTrue, ); }); testWidgets('terminal page reconnects after the socket closes', ( tester, ) async { final transportFactory = _QueuedTerminalSocketTransportFactory(); await _pumpApp( tester, projectRepository: _FakeProjectRepository(), sessionRepository: _FakeSessionRepository(), socketFactory: TerminalSocketSessionFactory( transportFactory: transportFactory.create, ), ); await _openProjectTerminal(tester); expect(find.text('codex-main'), findsOneWidget); expect(transportFactory.createCount, 1); await transportFactory.createdTransports.first.close(); await tester.pump(); await tester.tap(find.byKey(const Key('terminal_toggle_actions_button'))); await tester.pumpAndSettle(); expect(find.text('Connection lost. Reconnecting...'), findsOneWidget); await tester.pump(const Duration(seconds: 2)); await tester.pumpAndSettle(); expect(transportFactory.createCount, 2); expect(find.text('codex-main'), findsOneWidget); }); testWidgets('terminal page reconnects when the app resumes', (tester) async { final transportFactory = _QueuedTerminalSocketTransportFactory(); await _pumpApp( tester, projectRepository: _FakeProjectRepository(), sessionRepository: _FakeSessionRepository(), socketFactory: TerminalSocketSessionFactory( transportFactory: transportFactory.create, ), ); await _openProjectTerminal(tester); expect(transportFactory.createCount, 1); tester.binding.handleAppLifecycleStateChanged(AppLifecycleState.inactive); await tester.pump(); tester.binding.handleAppLifecycleStateChanged(AppLifecycleState.hidden); await tester.pump(); tester.binding.handleAppLifecycleStateChanged(AppLifecycleState.paused); await tester.pump(); tester.binding.handleAppLifecycleStateChanged(AppLifecycleState.hidden); await tester.pump(); tester.binding.handleAppLifecycleStateChanged(AppLifecycleState.inactive); await tester.pump(); tester.binding.handleAppLifecycleStateChanged(AppLifecycleState.resumed); await tester.pump(); await tester.pump(const Duration(seconds: 2)); await tester.pumpAndSettle(); expect(transportFactory.createCount, 2); expect(find.text('codex-main'), findsOneWidget); }); testWidgets('terminal page can open another terminal for the same project', ( tester, ) async { final sessionRepository = _FakeSessionRepository(); await _pumpApp( tester, projectRepository: _FakeProjectRepository(), sessionRepository: sessionRepository, ); await _openProjectTerminal(tester); expect(sessionRepository.createCount, 1); await tester.tap(find.byKey(const Key('terminal_toggle_actions_button'))); await tester.pumpAndSettle(); await tester.tap(find.text('New terminal')); await tester.pumpAndSettle(); expect(sessionRepository.createCount, 2); expect(find.text('codex-main'), findsOneWidget); }); testWidgets( 'terminal page lets the user return to live mode and load older history', (tester) async { final apiClient = _SequencedHistoryAgentApiClient( responses: [ { 'sessionId': 'session-1', 'lines': ['one', 'two'], 'hasMoreAbove': true, }, { 'sessionId': 'session-1', 'lines': ['zero', 'one', 'two'], 'hasMoreAbove': false, }, ], ); await _pumpApp( tester, projectRepository: _FakeProjectRepository(), sessionRepository: _FakeSessionRepository(), apiClient: apiClient, ); await _openProjectTerminal(tester); expect(find.text('Scrollback | 2 lines'), findsNothing); await tester.tap(find.text('Live | 2 lines')); await tester.pumpAndSettle(); expect(find.text('Scrollback | 2 lines'), findsOneWidget); expect(find.text('Load older lines'), findsOneWidget); await tester.ensureVisible(find.text('Load older lines')); await tester.tap(find.text('Load older lines')); await tester.pumpAndSettle(); expect(apiClient.requestedLineCounts, [200, 400]); expect(find.text('3 lines loaded'), findsOneWidget); }, ); testWidgets( 'terminal attach replay keeps the cursor on the last restored line', (tester) async { final apiClient = _SequencedHistoryAgentApiClient( responses: [ { 'sessionId': 'session-1', 'lines': ['one', 'two'], 'hasMoreAbove': true, }, { 'sessionId': 'session-1', 'lines': ['zero', 'one', 'two'], 'hasMoreAbove': false, }, ], ); final transportFactory = _QueuedTerminalSocketTransportFactory( startupFrames: const [ '{"type":"attached","sessionId":"session-1"}', 'one\r\ntwo', ], ); await _pumpApp( tester, projectRepository: _FakeProjectRepository(), sessionRepository: _FakeSessionRepository(), apiClient: apiClient, socketFactory: TerminalSocketSessionFactory( transportFactory: transportFactory.create, ), ); await _openProjectTerminal(tester); var terminal = tester .widget(find.byType(TerminalView)) .terminal; expect(terminal.buffer.cursorY, 1); expect(terminal.buffer.cursorX, 3); expect(terminal.buffer.getText(), contains('one\ntwo')); await tester.tap(find.textContaining('Live |')); await tester.pumpAndSettle(); await tester.ensureVisible(find.text('Load older lines')); await tester.tap(find.text('Load older lines')); await tester.pumpAndSettle(); terminal = tester .widget(find.byType(TerminalView)) .terminal; expect(terminal.buffer.getText(), contains('one\ntwo')); expect(terminal.buffer.getText(), isNot(contains('zero'))); expect(terminal.buffer.cursorY, 1); expect(terminal.buffer.cursorX, 3); }, ); testWidgets( 're-entering an existing session restores the terminal cursor to the last line', (tester) async { final transportFactory = _QueuedTerminalSocketTransportFactory( startupFrames: const [ '{"type":"attached","sessionId":"session-1"}', 'one\r\ntwo', ], ); final session = _session('session-1', 'codex-main'); await _pumpTerminalPage( tester, session: session, socketFactory: TerminalSocketSessionFactory( transportFactory: transportFactory.create, ), ); var terminal = tester .widget(find.byType(TerminalView)) .terminal; expect(terminal.buffer.getText(), contains('one\ntwo')); expect(terminal.buffer.cursorY, 1); expect(terminal.buffer.cursorX, 3); await tester.pumpWidget(const SizedBox.shrink()); await tester.pumpAndSettle(); await _pumpTerminalPage( tester, session: session, socketFactory: TerminalSocketSessionFactory( transportFactory: transportFactory.create, ), ); terminal = tester .widget(find.byType(TerminalView)) .terminal; expect(transportFactory.createCount, 2); expect(terminal.buffer.getText(), contains('one\ntwo')); expect(terminal.buffer.cursorY, 1); expect(terminal.buffer.cursorX, 3); }, ); testWidgets('session list deletes a session after confirmation', ( tester, ) async { final sessionRepository = _FakeSessionRepository.withSessions([ Session( sessionId: 'session-1', name: 'codex-main', status: 'created', createdAtUtc: DateTime.parse('2026-03-30T10:00:00Z'), updatedAtUtc: DateTime.parse('2026-03-30T10:00:00Z'), ), ]); await tester.pumpWidget( ProviderScope( overrides: [ agentApiClientProvider.overrideWithValue(_FakeAgentApiClient()), sessionRepositoryProvider.overrideWithValue(sessionRepository), ], child: const MaterialApp(home: SessionListPage()), ), ); await tester.pumpAndSettle(); expect(find.byKey(const Key('session_page_header')), findsOneWidget); expect(find.text('codex-main'), findsOneWidget); expect(find.text('Reusable terminal sessions'), findsOneWidget); expect(find.byKey(const Key('session_tile_session-1')), findsOneWidget); await tester.tap(find.byKey(const Key('session_delete_button_session-1'))); await tester.pumpAndSettle(); await tester.tap(find.widgetWithText(FilledButton, 'Delete')); await tester.pumpAndSettle(); expect(sessionRepository.deletedSessionIds, ['session-1']); expect(find.text('codex-main'), findsNothing); }); } Future _pumpApp( WidgetTester tester, { required _FakeProjectRepository projectRepository, required SessionRepository sessionRepository, AgentApiClient? apiClient, TerminalSocketSessionFactory? socketFactory, }) async { await tester.pumpWidget( ProviderScope( overrides: [ agentApiClientProvider.overrideWithValue( apiClient ?? _FakeAgentApiClient(), ), projectRepositoryProvider.overrideWithValue(projectRepository), sessionRepositoryProvider.overrideWithValue(sessionRepository), terminalSocketSessionFactoryProvider.overrideWithValue( socketFactory ?? TerminalSocketSessionFactory( transportFactory: (_) => _FakeTerminalSocketTransport(autoAttach: true), ), ), ], child: const TermRemoteCtlApp(), ), ); await tester.pumpAndSettle(); } Future _pumpTerminalPage( WidgetTester tester, { required Session session, AgentApiClient? apiClient, TerminalSocketSessionFactory? socketFactory, }) async { await tester.pumpWidget( ProviderScope( overrides: [ agentApiClientProvider.overrideWithValue( apiClient ?? _FakeAgentApiClient(), ), terminalSocketSessionFactoryProvider.overrideWithValue( socketFactory ?? TerminalSocketSessionFactory( transportFactory: (_) => _FakeTerminalSocketTransport(autoAttach: true), ), ), ], child: MaterialApp( home: TerminalPage( session: session, agentBaseUri: Uri.parse('http://100.81.30.82:5067'), ), ), ), ); await tester.pumpAndSettle(); } Future _openProjectTerminal(WidgetTester tester) async { await tester.tap(find.widgetWithText(FilledButton, 'Open terminal')); await tester.pumpAndSettle(); } class _FakeProjectRepository extends ProjectRepository { _FakeProjectRepository() : _projects = [ Project( projectId: 'project-1', name: 'codex-main', workingDirectory: r'C:\repo\codex-main', createdAtUtc: DateTime.parse('2026-03-30T10:00:00Z'), updatedAtUtc: DateTime.parse('2026-03-30T10:00:00Z'), ), ], _recentSessionsByProject = const {}, super(_FakeAgentApiClient()); _FakeProjectRepository.withProjects(List projects) : _projects = List.of(projects), _recentSessionsByProject = const {}, super(_FakeAgentApiClient()); _FakeProjectRepository.withRecentSessions( Map> recentSessionsByProject, ) : _projects = [ Project( projectId: 'project-1', name: 'codex-main', workingDirectory: r'C:\repo\codex-main', createdAtUtc: DateTime.parse('2026-03-30T10:00:00Z'), updatedAtUtc: DateTime.parse('2026-03-30T10:00:00Z'), ), ], _recentSessionsByProject = { for (final entry in recentSessionsByProject.entries) entry.key: List.of(entry.value), }, super(_FakeAgentApiClient()); final List _projects; final Map> _recentSessionsByProject; final List deletedProjectIds = []; @override Future> listProjects() async => List.of(_projects); @override Future getProjectDetail(String projectId) async { final project = _projects.singleWhere( (entry) => entry.projectId == projectId, ); return ProjectDetail( project: project, recentSessions: List.of( _recentSessionsByProject[projectId] ?? const [], ), ); } @override Future deleteProject(String projectId) async { deletedProjectIds.add(projectId); _projects.removeWhere((project) => project.projectId == projectId); } void removeRecentSession(String sessionId) { for (final sessions in _recentSessionsByProject.values) { sessions.removeWhere((session) => session.sessionId == sessionId); } } } class _FakeSessionRepository extends SessionRepository { _FakeSessionRepository() : _sessions = [], super(_FakeAgentApiClient()); _FakeSessionRepository.withSessions(List sessions) : _sessions = List.of(sessions), super(_FakeAgentApiClient()); String? lastCreatedProjectId; int createCount = 0; void Function(String sessionId)? onDeleteSession; final List deletedSessionIds = []; final List _sessions; @override Future> listSessions() async => List.of(_sessions); @override Future createSession({ String? name, String? projectId, String? workingDirectory, }) async { createCount += 1; lastCreatedProjectId = projectId; return Session( sessionId: 'session-$createCount', name: name ?? 'codex-main', status: 'created', projectId: projectId, workingDirectory: workingDirectory ?? r'C:\repo\codex-main', createdAtUtc: DateTime.parse('2026-03-30T10:00:00Z'), updatedAtUtc: DateTime.parse('2026-03-30T10:00:00Z'), ); } @override Future deleteSession(String sessionId) async { deletedSessionIds.add(sessionId); _sessions.removeWhere((session) => session.sessionId == sessionId); onDeleteSession?.call(sessionId); } } Session _session(String sessionId, String name) { return Session( sessionId: sessionId, name: name, status: 'created', projectId: 'project-1', workingDirectory: r'C:\repo\codex-main', createdAtUtc: DateTime.parse('2026-03-30T10:00:00Z'), updatedAtUtc: DateTime.parse('2026-03-30T10:00:00Z'), ); } class _FailingSessionRepository extends SessionRepository { _FailingSessionRepository() : super(_FakeAgentApiClient()); @override Future> listSessions() async => const []; @override Future createSession({ String? name, String? projectId, String? workingDirectory, }) async { throw DioException.badResponse( statusCode: 400, requestOptions: RequestOptions(path: '/api/sessions'), response: Response>( requestOptions: RequestOptions(path: '/api/sessions'), statusCode: 400, data: const {'error': 'invalid_working_directory'}, ), ); } } class _FakeAgentApiClient extends AgentApiClient { _FakeAgentApiClient() : super(Uri.parse('http://100.81.30.82:5067')); @override Future> getSessionHistory( String sessionId, { int lineCount = 200, }) async { return { 'sessionId': sessionId, 'lines': ['one', 'two'], 'hasMoreAbove': true, }; } } class _SequencedHistoryAgentApiClient extends _FakeAgentApiClient { _SequencedHistoryAgentApiClient({ required List> responses, }) : _responses = responses; final List> _responses; final requestedLineCounts = []; int _index = 0; @override Future> getSessionHistory( String sessionId, { int lineCount = 200, }) async { requestedLineCounts.add(lineCount); final response = _responses[_index]; if (_index < _responses.length - 1) { _index += 1; } return response; } } class _FakeTerminalSocketTransport implements TerminalSocketTransport { _FakeTerminalSocketTransport({ this.autoAttach = false, this.startupFrames = const [], }) { if (autoAttach && startupFrames.isEmpty) { Future.microtask(() { emit('{"type":"attached","sessionId":"session-1"}'); }); } else if (startupFrames.isNotEmpty) { Future.microtask(() { for (final frame in startupFrames) { emit(frame); } }); } } final bool autoAttach; final List startupFrames; final _incoming = StreamController.broadcast(); final sentMessages = []; @override Stream get stream => _incoming.stream; @override void send(String message) { sentMessages.add(message); } @override Future close() async { await _incoming.close(); } void emit(String message) { _incoming.add(message); } } class _QueuedTerminalSocketTransportFactory { _QueuedTerminalSocketTransportFactory({ this.startupFrames = const [], }); final List startupFrames; final createdTransports = <_FakeTerminalSocketTransport>[]; int createCount = 0; TerminalSocketTransport create(Uri _) { final transport = _FakeTerminalSocketTransport( autoAttach: startupFrames.isEmpty, startupFrames: startupFrames, ); createdTransports.add(transport); createCount += 1; return transport; } }