305 lines
9.9 KiB
TypeScript
305 lines
9.9 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
|
|
import { BoardTaskExactLogDetailSelector } from '../../../../src/main/services/team/taskLogs/exact/BoardTaskExactLogDetailSelector';
|
|
|
|
import type { ParsedMessage } from '../../../../src/main/types';
|
|
import type { BoardTaskActivityRecord } from '../../../../src/main/services/team/taskLogs/activity/BoardTaskActivityRecord';
|
|
import type { BoardTaskExactLogBundleCandidate } from '../../../../src/main/services/team/taskLogs/exact/BoardTaskExactLogTypes';
|
|
|
|
function makeRecord(): BoardTaskActivityRecord {
|
|
return {
|
|
id: 'record-1',
|
|
timestamp: '2026-04-12T16:00:00.000Z',
|
|
task: {
|
|
locator: { ref: 'abcd1234', refKind: 'display', canonicalId: 'task-a' },
|
|
resolution: 'resolved',
|
|
},
|
|
linkKind: 'board_action',
|
|
targetRole: 'subject',
|
|
actor: {
|
|
memberName: 'alice',
|
|
role: 'member',
|
|
sessionId: 'session-1',
|
|
agentId: 'agent-1',
|
|
isSidechain: true,
|
|
},
|
|
actorContext: { relation: 'same_task' },
|
|
action: {
|
|
canonicalToolName: 'task_add_comment',
|
|
toolUseId: 'tool-1',
|
|
category: 'comment',
|
|
},
|
|
source: {
|
|
filePath: '/tmp/task.jsonl',
|
|
messageUuid: 'assistant-1',
|
|
toolUseId: 'tool-1',
|
|
sourceOrder: 1,
|
|
},
|
|
};
|
|
}
|
|
|
|
function makeCandidate(records: BoardTaskActivityRecord[]): BoardTaskExactLogBundleCandidate {
|
|
return {
|
|
id: 'tool:/tmp/task.jsonl:tool-1',
|
|
timestamp: '2026-04-12T16:00:00.000Z',
|
|
actor: records[0]!.actor,
|
|
source: {
|
|
filePath: '/tmp/task.jsonl',
|
|
messageUuid: 'assistant-1',
|
|
toolUseId: 'tool-1',
|
|
sourceOrder: 1,
|
|
},
|
|
records,
|
|
anchor: {
|
|
kind: 'tool',
|
|
filePath: '/tmp/task.jsonl',
|
|
messageUuid: 'assistant-1',
|
|
toolUseId: 'tool-1',
|
|
},
|
|
actionLabel: 'Added a comment',
|
|
actionCategory: 'comment',
|
|
canonicalToolName: 'task_add_comment',
|
|
linkKinds: ['board_action'],
|
|
targetRoles: ['subject'],
|
|
canLoadDetail: true,
|
|
sourceGeneration: 'gen-1',
|
|
};
|
|
}
|
|
|
|
describe('BoardTaskExactLogDetailSelector', () => {
|
|
it('keeps the matched tool flow, preserves anchor output, and deduplicates assistant streaming rows anchor-aware', () => {
|
|
const records = [makeRecord()];
|
|
const candidate = makeCandidate(records);
|
|
const parsedMessagesByFile = new Map<string, ParsedMessage[]>([
|
|
[
|
|
'/tmp/task.jsonl',
|
|
[
|
|
{
|
|
uuid: 'assistant-0',
|
|
parentUuid: null,
|
|
type: 'assistant',
|
|
timestamp: new Date('2026-04-12T16:00:00.000Z'),
|
|
role: 'assistant',
|
|
content: [
|
|
{ type: 'thinking', thinking: 'draft' } as never,
|
|
{ type: 'text', text: 'old tool draft' } as never,
|
|
{ type: 'tool_use', id: 'tool-1', name: 'task_add_comment', input: { taskId: 'x' } } as never,
|
|
],
|
|
toolCalls: [],
|
|
toolResults: [],
|
|
requestId: 'req-1',
|
|
isSidechain: true,
|
|
isMeta: false,
|
|
isCompactSummary: false,
|
|
},
|
|
{
|
|
uuid: 'assistant-1',
|
|
parentUuid: null,
|
|
type: 'assistant',
|
|
timestamp: new Date('2026-04-12T16:00:01.000Z'),
|
|
role: 'assistant',
|
|
content: [
|
|
{ type: 'text', text: 'stream tail without anchor tool call' } as never,
|
|
{ type: 'tool_use', id: 'tool-2', name: 'task_get', input: { taskId: 'y' } } as never,
|
|
],
|
|
toolCalls: [],
|
|
toolResults: [],
|
|
requestId: 'req-1',
|
|
isSidechain: true,
|
|
isMeta: false,
|
|
isCompactSummary: false,
|
|
},
|
|
{
|
|
uuid: 'user-1',
|
|
parentUuid: null,
|
|
type: 'user',
|
|
timestamp: new Date('2026-04-12T16:00:02.000Z'),
|
|
role: 'user',
|
|
content: [
|
|
{ type: 'tool_result', tool_use_id: 'tool-1', content: 'ok' } as never,
|
|
{ type: 'tool_result', tool_use_id: 'tool-2', content: 'ignore' } as never,
|
|
],
|
|
toolCalls: [],
|
|
toolResults: [],
|
|
sourceToolUseID: 'tool-1',
|
|
sourceToolAssistantUUID: 'assistant-1',
|
|
toolUseResult: { output: 'kept' },
|
|
requestId: 'req-1',
|
|
isSidechain: true,
|
|
isMeta: false,
|
|
isCompactSummary: false,
|
|
},
|
|
{
|
|
uuid: 'assistant-2',
|
|
parentUuid: null,
|
|
type: 'assistant',
|
|
timestamp: new Date('2026-04-12T16:00:03.000Z'),
|
|
role: 'assistant',
|
|
content: [{ type: 'text', text: 'comment saved' } as never],
|
|
toolCalls: [],
|
|
toolResults: [],
|
|
sourceToolUseID: 'tool-1',
|
|
requestId: 'req-2',
|
|
isSidechain: true,
|
|
isMeta: false,
|
|
isCompactSummary: false,
|
|
},
|
|
],
|
|
],
|
|
]);
|
|
|
|
const detail = new BoardTaskExactLogDetailSelector().selectDetail({
|
|
candidate,
|
|
records,
|
|
parsedMessagesByFile,
|
|
});
|
|
|
|
expect(detail).not.toBeNull();
|
|
expect(detail?.filteredMessages).toHaveLength(3);
|
|
expect(detail?.filteredMessages[0]?.uuid).toBe('assistant-0');
|
|
expect(detail?.filteredMessages[1]?.uuid).toBe('user-1');
|
|
expect(detail?.filteredMessages[2]?.uuid).toBe('assistant-2');
|
|
expect(detail?.filteredMessages[0]?.toolCalls).toHaveLength(1);
|
|
expect(detail?.filteredMessages[1]?.toolResults).toHaveLength(1);
|
|
expect(detail?.filteredMessages[1]?.toolUseResult).toEqual({ output: 'kept' });
|
|
expect(detail?.filteredMessages[1]?.sourceToolAssistantUUID).toBeUndefined();
|
|
expect(detail?.filteredMessages[2]?.sourceToolUseID).toBe('tool-1');
|
|
});
|
|
|
|
it('drops stale derived tool metadata when a message-linked row survives filtering', () => {
|
|
const record = {
|
|
...makeRecord(),
|
|
id: 'record-message-1',
|
|
source: {
|
|
filePath: '/tmp/task.jsonl',
|
|
messageUuid: 'user-2',
|
|
sourceOrder: 2,
|
|
},
|
|
action: undefined,
|
|
} satisfies BoardTaskActivityRecord;
|
|
const candidate: BoardTaskExactLogBundleCandidate = {
|
|
id: 'message:/tmp/task.jsonl:user-2',
|
|
timestamp: '2026-04-12T16:01:00.000Z',
|
|
actor: record.actor,
|
|
source: {
|
|
filePath: '/tmp/task.jsonl',
|
|
messageUuid: 'user-2',
|
|
sourceOrder: 2,
|
|
},
|
|
records: [record],
|
|
anchor: {
|
|
kind: 'message',
|
|
filePath: '/tmp/task.jsonl',
|
|
messageUuid: 'user-2',
|
|
},
|
|
actionLabel: 'Worked on task',
|
|
linkKinds: ['execution'],
|
|
targetRoles: ['subject'],
|
|
canLoadDetail: true,
|
|
sourceGeneration: 'gen-2',
|
|
};
|
|
const parsedMessagesByFile = new Map<string, ParsedMessage[]>([
|
|
[
|
|
'/tmp/task.jsonl',
|
|
[
|
|
{
|
|
uuid: 'user-2',
|
|
parentUuid: null,
|
|
type: 'user',
|
|
timestamp: new Date('2026-04-12T16:01:00.000Z'),
|
|
role: 'user',
|
|
content: [
|
|
{ type: 'text', text: 'status update' } as never,
|
|
{ type: 'tool_result', tool_use_id: 'other-tool', content: 'stale tool result' } as never,
|
|
],
|
|
toolCalls: [],
|
|
toolResults: [],
|
|
sourceToolUseID: 'other-tool',
|
|
sourceToolAssistantUUID: 'assistant-other',
|
|
toolUseResult: { output: 'stale' },
|
|
requestId: 'req-2',
|
|
isSidechain: true,
|
|
isMeta: false,
|
|
isCompactSummary: false,
|
|
},
|
|
],
|
|
],
|
|
]);
|
|
|
|
const detail = new BoardTaskExactLogDetailSelector().selectDetail({
|
|
candidate,
|
|
records: [record],
|
|
parsedMessagesByFile,
|
|
});
|
|
|
|
expect(detail).not.toBeNull();
|
|
expect(detail?.filteredMessages).toHaveLength(1);
|
|
expect(detail?.filteredMessages[0]?.content).toEqual([{ type: 'text', text: 'status update' }]);
|
|
expect(detail?.filteredMessages[0]?.toolResults).toEqual([]);
|
|
expect(detail?.filteredMessages[0]?.sourceToolUseID).toBeUndefined();
|
|
expect(detail?.filteredMessages[0]?.sourceToolAssistantUUID).toBeUndefined();
|
|
expect(detail?.filteredMessages[0]?.toolUseResult).toBeUndefined();
|
|
});
|
|
|
|
it('preserves toolUseResult for a matched tool_result even when sourceToolUseID is absent', () => {
|
|
const records = [makeRecord()];
|
|
const candidate = makeCandidate(records);
|
|
const parsedMessagesByFile = new Map<string, ParsedMessage[]>([
|
|
[
|
|
'/tmp/task.jsonl',
|
|
[
|
|
{
|
|
uuid: 'assistant-1',
|
|
parentUuid: null,
|
|
type: 'assistant',
|
|
timestamp: new Date('2026-04-12T16:00:00.000Z'),
|
|
role: 'assistant',
|
|
content: [
|
|
{ type: 'tool_use', id: 'tool-1', name: 'task_add_comment', input: { taskId: 'x' } } as never,
|
|
],
|
|
toolCalls: [],
|
|
toolResults: [],
|
|
requestId: 'req-1',
|
|
isSidechain: true,
|
|
isMeta: false,
|
|
isCompactSummary: false,
|
|
},
|
|
{
|
|
uuid: 'user-1',
|
|
parentUuid: null,
|
|
type: 'user',
|
|
timestamp: new Date('2026-04-12T16:00:01.000Z'),
|
|
role: 'user',
|
|
content: [
|
|
{ type: 'tool_result', tool_use_id: 'tool-1', content: 'ok' } as never,
|
|
],
|
|
toolCalls: [],
|
|
toolResults: [],
|
|
toolUseResult: {
|
|
toolUseId: 'tool-1',
|
|
content: 'ok',
|
|
},
|
|
requestId: 'req-1',
|
|
isSidechain: true,
|
|
isMeta: false,
|
|
isCompactSummary: false,
|
|
},
|
|
],
|
|
],
|
|
]);
|
|
|
|
const detail = new BoardTaskExactLogDetailSelector().selectDetail({
|
|
candidate,
|
|
records,
|
|
parsedMessagesByFile,
|
|
});
|
|
|
|
expect(detail).not.toBeNull();
|
|
expect(detail?.filteredMessages).toHaveLength(2);
|
|
expect(detail?.filteredMessages[1]?.sourceToolUseID).toBe('tool-1');
|
|
expect(detail?.filteredMessages[1]?.toolUseResult).toEqual({
|
|
toolUseId: 'tool-1',
|
|
content: 'ok',
|
|
});
|
|
});
|
|
});
|