Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 105 additions & 0 deletions packages/cli/src/acp-integration/session/Session.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -863,6 +863,111 @@ describe('Session', () => {
);
});

describe('conversation_finished telemetry (#4602 review)', () => {
it('emits conversation_finished once when a turn completes normally', async () => {
const finishedSpy = vi
.spyOn(core, 'logConversationFinishedEvent')
.mockImplementation(() => {});
mockChat.sendMessageStream = vi
.fn()
.mockResolvedValue(createEmptyStream());

await expect(
session.prompt({
sessionId: 'test-session-id',
prompt: [{ type: 'text', text: 'hello' }],
}),
).resolves.toEqual({ stopReason: 'end_turn' });

expect(finishedSpy).toHaveBeenCalledTimes(1);
});

it('still emits conversation_finished when the turn throws (telemetry not lost on the error path)', async () => {
const finishedSpy = vi
.spyOn(core, 'logConversationFinishedEvent')
.mockImplementation(() => {});
mockChat.sendMessageStream = vi
.fn()
.mockRejectedValue(new Error('stream boom'));

// The turn surfaces the failure (rejection or error stopReason); either
// way the finally wrapping the whole turn must have fired the event
// before unwinding — the regression wenshao flagged was that only the
// clean stop-hook path emitted it.
await session
.prompt({
sessionId: 'test-session-id',
prompt: [{ type: 'text', text: 'hello' }],
})
.catch(() => undefined);

expect(finishedSpy).toHaveBeenCalled();
});
});

describe('tool outcome telemetry (#4602 review)', () => {
it('records a soft tool failure (toolResult.error) as error, not success', async () => {
const logToolCallSpy = vi
.spyOn(core, 'logToolCall')
.mockImplementation(() => {});
mockConfig.getApprovalMode = vi.fn().mockReturnValue(ApprovalMode.YOLO);

const tool = {
name: 'read_file',
kind: core.Kind.Read,
build: vi.fn().mockReturnValue({
params: { path: '/tmp/test.txt' },
getDefaultPermission: vi.fn().mockResolvedValue('allow'),
// Soft failure: resolves (does not throw) but carries an error.
execute: vi.fn().mockResolvedValue({
llmContent: 'nope',
returnDisplay: 'failed',
error: { message: 'tool blew up' },
}),
}),
};
mockToolRegistry.getTool.mockReturnValue(tool);

mockChat.sendMessageStream = vi
.fn()
.mockResolvedValueOnce(
createStreamWithChunks([
{
type: core.StreamEventType.CHUNK,
value: {
functionCalls: [
{
id: 'call-1',
name: 'read_file',
args: { path: '/tmp/test.txt' },
},
],
},
},
]),
)
.mockResolvedValueOnce(createEmptyStream());

await session.prompt({
sessionId: 'test-session-id',
prompt: [{ type: 'text', text: 'read the file' }],
});

const toolEvent = logToolCallSpy.mock.calls
.map(
([, ev]) =>
ev as {
function_name?: string;
status?: string;
success?: boolean;
},
)
.find((ev) => ev.function_name === 'read_file');
expect(toolEvent?.status).toBe('error');
expect(toolEvent?.success).toBe(false);
});
});

describe('auto-compress', () => {
it('runs automatic compression before sending an ACP prompt', async () => {
mockChat.sendMessageStream = vi
Expand Down
Loading