blob: 935ab1356eb453a69e494799578afdb7e6abe08e [file]
@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);
});
});
}