| @TestOn('chrome') |
| library; |
| |
| import 'package:frontend/features/agent/logic/run/agent_session.dart'; |
| import 'package:frontend/features/agent/models/agent_pending_interaction.dart'; |
| import 'package:frontend/features/agent/models/agent_timeline_item.dart'; |
| import 'package:frontend/features/agent/models/agent_tool_event.dart'; |
| import 'package:frontend/features/agent/models/agent_tool_id.dart'; |
| import 'package:frontend/features/shared/models/prompt.dart'; |
| import 'package:genkit/genkit.dart'; |
| import 'package:test/test.dart'; |
| |
| import '../agent_tool_event_fixture.dart'; |
| |
| String _timelineMessage(AgentTimelineItem item) { |
| return switch (item) { |
| AgentThinkingTimelineItem(:final message) || |
| AgentMessageTimelineItem(:final message) || |
| AgentPlanTimelineItem(:final message) => message, |
| _ => throw StateError('Expected message timeline item.'), |
| }; |
| } |
| |
| AgentToolEvent _timelineToolEvent(AgentTimelineItem item) { |
| return switch (item) { |
| AgentToolTimelineItem(:final toolEvent) => toolEvent, |
| _ => throw StateError('Expected tool-event timeline item.'), |
| }; |
| } |
| |
| void main() { |
| group('AgentSession', () { |
| test('history starts with a single system message', () { |
| final session = AgentSession(); |
| |
| expect(session.currentModelContext, hasLength(1)); |
| expect(session.currentModelContext.single.role, Role.system); |
| expect( |
| session.currentModelContext.single.text, |
| contains('You are VibePadAgent, an interactive coding agent inside VibePad'), |
| ); |
| expect(session.snapshot.timeline, isEmpty); |
| expect(session.snapshot.changedFiles, isEmpty); |
| expect(session.snapshot.errorMessage, isNull); |
| }); |
| |
| test('appending a user message updates both timeline and model context', () { |
| final session = AgentSession(); |
| |
| session.appendUserPrompt(Prompt.fromString('What does lib/main.dart do?')); |
| |
| expect(session.currentModelContext, hasLength(2)); |
| expect(session.currentModelContext.last.role, Role.user); |
| expect(session.currentModelContext.last.text, 'What does lib/main.dart do?'); |
| expect(session.snapshot.timeline, hasLength(1)); |
| expect(session.snapshot.timeline.single, isA<AgentPromptTimelineItem>()); |
| expect( |
| (session.snapshot.timeline.single as AgentPromptTimelineItem).prompt.toMessage(), |
| 'What does lib/main.dart do?', |
| ); |
| }); |
| |
| test('successful runs commit generated history and timeline items', () { |
| final session = AgentSession(); |
| session.appendUserPrompt(Prompt.fromString('Inspect lib/main.dart')); |
| |
| session.appendMessages([ |
| Message( |
| role: Role.model, |
| content: [TextPart(text: 'Sure.')], |
| ), |
| ]); |
| session.addTimelineItem( |
| AgentTimelineItem.toolEvent( |
| buildTestToolEvent( |
| toolId: AgentToolId.readFile, |
| status: ToolStatus.success, |
| summary: 'Read file lib/main.dart', |
| targetPath: 'lib/main.dart', |
| ), |
| ), |
| ); |
| session.finishRun(assistantMessage: 'Sure.'); |
| |
| expect(session.currentModelContext, hasLength(3)); |
| expect(session.currentModelContext.last.role, Role.model); |
| expect(session.currentModelContext.last.text, 'Sure.'); |
| expect(session.snapshot.timeline, hasLength(3)); |
| expect(session.snapshot.timeline[1], isA<AgentToolTimelineItem>()); |
| expect(_timelineToolEvent(session.snapshot.timeline[1]).toolName, 'readFile'); |
| expect(_timelineMessage(session.snapshot.timeline[2]), 'Sure.'); |
| expect(session.snapshot.changedFiles, isEmpty); |
| expect(session.snapshot.errorMessage, isNull); |
| }); |
| |
| test('successful write tools are tracked in changedFiles for the current run', () { |
| final session = AgentSession(); |
| session.appendUserPrompt(Prompt.fromString('Update lib/main.dart')); |
| |
| session.addChangedFile('lib/main.dart'); |
| session.addChangedFile('lib/helper.dart'); |
| session.finishRun(); |
| |
| expect(session.snapshot.changedFiles, [ |
| 'lib/main.dart', |
| 'lib/helper.dart', |
| ]); |
| }); |
| |
| test('failed runs keep committed model history and add an error item', () { |
| final session = AgentSession(systemPrompt: 'system prompt'); |
| |
| // First run: successful. |
| session.appendUserPrompt(Prompt.fromString('previous question')); |
| session.appendMessages([ |
| Message( |
| role: Role.model, |
| content: [TextPart(text: 'previous answer')], |
| ), |
| ]); |
| session.finishRun(assistantMessage: 'previous answer'); |
| |
| // Second run: fails. |
| session.appendUserPrompt(Prompt.fromString('new question')); |
| session.finishRun(errorMessage: 'boom'); |
| |
| // Model context keeps system + prev user + prev model + new user = 4. |
| expect(session.currentModelContext, hasLength(4)); |
| expect(session.currentModelContext.last.role, Role.user); |
| expect(session.currentModelContext.last.text, 'new question'); |
| expect(session.snapshot.timeline, hasLength(4)); |
| expect((session.snapshot.timeline[2] as AgentPromptTimelineItem).prompt.toMessage(), 'new question'); |
| expect(session.snapshot.timeline.last, isA<AgentErrorTimelineItem>()); |
| expect(session.snapshot.errorMessage, 'boom'); |
| }); |
| |
| test('failed runs keep tool events and changed files from the current run', () { |
| final session = AgentSession(systemPrompt: 'system prompt'); |
| session.appendUserPrompt(Prompt.fromString('Patch the file')); |
| |
| session.appendMessages([ |
| Message( |
| role: Role.model, |
| content: [ |
| ToolRequestPart( |
| toolRequest: ToolRequest(name: 'applyPatch', input: const {}), |
| ), |
| ], |
| ), |
| Message( |
| role: Role.tool, |
| content: [ |
| ToolResponsePart( |
| toolResponse: ToolResponse(name: 'applyPatch', output: const {'success': true}), |
| ), |
| ], |
| ), |
| ]); |
| session.addChangedFile('lib/main.dart'); |
| session.addTimelineItem( |
| AgentTimelineItem.toolEvent( |
| buildTestToolEvent( |
| toolId: AgentToolId.applyPatch, |
| status: ToolStatus.success, |
| summary: 'applyPatch lib/main.dart - 1 replacements', |
| targetPath: 'lib/main.dart', |
| patchReplacementCount: 1, |
| ), |
| ), |
| ); |
| session.finishRun(errorMessage: 'boom'); |
| |
| expect(session.snapshot.timeline, hasLength(3)); |
| expect(session.snapshot.timeline[1], isA<AgentToolTimelineItem>()); |
| expect(session.snapshot.changedFiles, ['lib/main.dart']); |
| expect(session.snapshot.errorMessage, 'boom'); |
| expect(session.currentModelContext, hasLength(4)); |
| expect(session.currentModelContext[2].role, Role.model); |
| expect(session.currentModelContext.last.role, Role.tool); |
| }); |
| |
| test('reset clears visible timeline and error state', () { |
| final session = AgentSession(); |
| session.appendUserPrompt(Prompt.fromString('Update lib/main.dart')); |
| session.finishRun(errorMessage: 'boom'); |
| |
| session.reset(); |
| |
| expect(session.currentModelContext, hasLength(1)); |
| expect(session.snapshot.timeline, isEmpty); |
| expect(session.snapshot.errorMessage, isNull); |
| }); |
| |
| test('pending interactions are exposed in snapshots and cleared by reset', () { |
| final session = AgentSession(); |
| |
| session.setPendingInteraction( |
| const AgentPendingPlanExitInteraction(id: 'plan-1'), |
| ); |
| |
| expect(session.snapshot.pendingInteraction?.id, 'plan-1'); |
| |
| session.reset(); |
| |
| expect(session.snapshot.pendingInteraction, isNull); |
| }); |
| }); |
| } |