agent-ecosystem/test/main/services/team/BoardTaskLogStreamService.test.ts
2026-04-27 11:18:30 +03:00

951 lines
32 KiB
TypeScript

import { afterEach, describe, expect, it, vi } from 'vitest';
import { BoardTaskLogStreamService } from '../../../../src/main/services/team/taskLogs/stream/BoardTaskLogStreamService';
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(
id: string,
timestamp: string,
actor: BoardTaskActivityRecord['actor'],
toolUseId?: string,
): BoardTaskActivityRecord {
return {
id,
timestamp,
task: {
locator: { ref: 'abcd1234', refKind: 'display', canonicalId: 'task-a' },
resolution: 'resolved',
},
linkKind: 'board_action',
targetRole: 'subject',
actor,
actorContext: { relation: 'same_task' },
source: {
filePath: '/tmp/task.jsonl',
messageUuid: `${id}-msg`,
...(toolUseId ? { toolUseId } : {}),
sourceOrder: 1,
},
};
}
function makeCandidate(
id: string,
timestamp: string,
actor: BoardTaskActivityRecord['actor'],
toolUseId?: string,
): BoardTaskExactLogBundleCandidate {
const record = makeRecord(id, timestamp, actor, toolUseId);
return {
id,
timestamp,
actor,
source: {
filePath: '/tmp/task.jsonl',
messageUuid: `${id}-msg`,
...(toolUseId ? { toolUseId } : {}),
sourceOrder: 1,
},
records: [record],
anchor: toolUseId
? {
kind: 'tool',
filePath: '/tmp/task.jsonl',
messageUuid: `${id}-msg`,
toolUseId,
}
: {
kind: 'message',
filePath: '/tmp/task.jsonl',
messageUuid: `${id}-msg`,
},
actionLabel: 'Worked on task',
linkKinds: ['board_action'],
targetRoles: ['subject'],
canLoadDetail: true,
sourceGeneration: 'gen-1',
};
}
function makeMessage(uuid: string, timestamp: string, text: string): ParsedMessage {
return {
uuid,
parentUuid: null,
type: 'assistant',
timestamp: new Date(timestamp),
role: 'assistant',
content: [{ type: 'text', text } as never],
toolCalls: [],
toolResults: [],
isSidechain: true,
isMeta: false,
isCompactSummary: false,
};
}
describe('BoardTaskLogStreamService', () => {
afterEach(() => {
vi.unstubAllEnvs();
});
it('returns empty when the stream read flag is disabled', async () => {
vi.stubEnv('CLAUDE_TEAM_BOARD_TASK_EXACT_LOGS_READ_ENABLED', 'false');
const recordSource = {
getTaskRecords: vi.fn(async () => {
throw new Error('should not be called');
}),
};
const service = new BoardTaskLogStreamService(recordSource as never);
await expect(service.getTaskLogStream('demo', 'task-a')).resolves.toEqual({
participants: [],
defaultFilter: 'all',
segments: [],
});
expect(recordSource.getTaskRecords).not.toHaveBeenCalled();
});
it('falls back to OpenCode runtime stream when transcript slices are empty', async () => {
const runtimeFallbackSource = {
getTaskLogStream: vi.fn(async () => ({
participants: [
{
key: 'member:alice',
label: 'alice',
role: 'member' as const,
isLead: false,
isSidechain: true,
},
],
defaultFilter: 'member:alice',
segments: [
{
id: 'opencode:segment-1',
participantKey: 'member:alice',
actor: {
memberName: 'alice',
role: 'member' as const,
sessionId: 'session-opencode',
isSidechain: true,
},
startTimestamp: '2026-04-21T10:00:00.000Z',
endTimestamp: '2026-04-21T10:01:00.000Z',
chunks: [{ id: 'chunk-1' }],
},
],
source: 'opencode_runtime_fallback' as const,
})),
};
const service = new BoardTaskLogStreamService(
{
getTaskRecords: vi.fn(async () => []),
} as never,
undefined as never,
undefined as never,
undefined as never,
undefined as never,
undefined as never,
undefined as never,
runtimeFallbackSource as never
);
const response = await service.getTaskLogStream('demo', 'task-a');
expect(response.source).toBe('opencode_runtime_fallback');
expect(response.segments).toHaveLength(1);
expect(await service.getTaskLogStreamSummary('demo', 'task-a')).toEqual({
segmentCount: 0,
});
expect(runtimeFallbackSource.getTaskLogStream).toHaveBeenCalledTimes(1);
});
it('groups contiguous slices into participant segments and excludes lead slices when member slices exist', async () => {
const tom = {
memberName: 'tom',
role: 'member' as const,
sessionId: 'session-tom',
agentId: 'agent-tom',
isSidechain: true,
};
const alice = {
memberName: 'alice',
role: 'member' as const,
sessionId: 'session-alice',
agentId: 'agent-alice',
isSidechain: true,
};
const lead = {
role: 'lead' as const,
sessionId: 'session-lead',
isSidechain: false,
};
const candidates = [
makeCandidate('c1', '2026-04-12T16:00:00.000Z', tom, 'tool-1'),
makeCandidate('c2', '2026-04-12T16:01:00.000Z', tom, 'tool-2'),
makeCandidate('c3', '2026-04-12T16:02:00.000Z', alice, 'tool-3'),
makeCandidate('c4', '2026-04-12T16:03:00.000Z', lead),
makeCandidate('c5', '2026-04-12T16:04:00.000Z', tom, 'tool-4'),
];
const recordSource = {
getTaskRecords: vi.fn(async () => candidates.flatMap((candidate) => candidate.records)),
};
const summarySelector = {
selectSummaries: vi.fn(() => candidates),
};
const strictParser = {
parseFiles: vi.fn(async () => new Map([['/tmp/task.jsonl', []]])),
};
const detailSelector = {
selectDetail: vi.fn(({ candidate }: { candidate: BoardTaskExactLogBundleCandidate }) => ({
id: candidate.id,
timestamp: candidate.timestamp,
actor: candidate.actor,
source: candidate.source,
records: candidate.records,
filteredMessages: [makeMessage(candidate.id, candidate.timestamp, candidate.id)],
})),
};
const buildBundleChunks = vi.fn((messages: ParsedMessage[]) => [{ id: messages[0]?.uuid }]);
const service = new BoardTaskLogStreamService(
recordSource as never,
summarySelector as never,
strictParser as never,
detailSelector as never,
{ buildBundleChunks } as never,
);
const response = await service.getTaskLogStream('demo', 'task-a');
expect(response.defaultFilter).toBe('all');
expect(response.participants.map((participant) => participant.key)).toEqual([
'member:tom',
'member:alice',
]);
expect(response.segments.map((segment) => segment.participantKey)).toEqual([
'member:tom',
'member:alice',
'member:tom',
]);
expect(buildBundleChunks).toHaveBeenCalledTimes(3);
expect(buildBundleChunks.mock.calls[0]?.[0]).toHaveLength(2);
});
it('returns lightweight segment count without building stream chunks', async () => {
const tom = {
memberName: 'tom',
role: 'member' as const,
sessionId: 'session-tom',
agentId: 'agent-tom',
isSidechain: true,
};
const alice = {
memberName: 'alice',
role: 'member' as const,
sessionId: 'session-alice',
agentId: 'agent-alice',
isSidechain: true,
};
const candidates = [
makeCandidate('c1', '2026-04-12T16:00:00.000Z', tom, 'tool-1'),
makeCandidate('c2', '2026-04-12T16:01:00.000Z', tom, 'tool-2'),
makeCandidate('c3', '2026-04-12T16:02:00.000Z', alice, 'tool-3'),
makeCandidate('c4', '2026-04-12T16:03:00.000Z', tom, 'tool-4'),
];
const recordSource = {
getTaskRecords: vi.fn(async () => candidates.flatMap((candidate) => candidate.records)),
};
const summarySelector = {
selectSummaries: vi.fn(() => candidates),
};
const strictParser = {
parseFiles: vi.fn(async () => new Map([['/tmp/task.jsonl', []]])),
};
const detailSelector = {
selectDetail: vi.fn(({ candidate }: { candidate: BoardTaskExactLogBundleCandidate }) => ({
id: candidate.id,
timestamp: candidate.timestamp,
actor: candidate.actor,
source: candidate.source,
records: candidate.records,
filteredMessages: [makeMessage(candidate.id, candidate.timestamp, candidate.id)],
})),
};
const buildBundleChunks = vi.fn((messages: ParsedMessage[]) => [{ id: messages[0]?.uuid }]);
const service = new BoardTaskLogStreamService(
recordSource as never,
summarySelector as never,
strictParser as never,
detailSelector as never,
{ buildBundleChunks } as never,
);
await expect(service.getTaskLogStreamSummary('demo', 'task-a')).resolves.toEqual({
segmentCount: 3,
});
expect(buildBundleChunks).not.toHaveBeenCalled();
});
it('shares concurrent summary and stream layout work', async () => {
const tom = {
memberName: 'tom',
role: 'member' as const,
sessionId: 'session-tom',
agentId: 'agent-tom',
isSidechain: true,
};
const candidates = [
makeCandidate('c1', '2026-04-12T16:00:00.000Z', tom, 'tool-1'),
makeCandidate('c2', '2026-04-12T16:01:00.000Z', tom, 'tool-2'),
];
const recordSource = {
getTaskRecords: vi.fn(async () => {
await Promise.resolve();
return candidates.flatMap((candidate) => candidate.records);
}),
};
const summarySelector = {
selectSummaries: vi.fn(() => candidates),
};
const strictParser = {
parseFiles: vi.fn(async () => new Map([['/tmp/task.jsonl', []]])),
};
const detailSelector = {
selectDetail: vi.fn(({ candidate }: { candidate: BoardTaskExactLogBundleCandidate }) => ({
id: candidate.id,
timestamp: candidate.timestamp,
actor: candidate.actor,
source: candidate.source,
records: candidate.records,
filteredMessages: [makeMessage(candidate.id, candidate.timestamp, candidate.id)],
})),
};
const buildBundleChunks = vi.fn((messages: ParsedMessage[]) => [{ id: messages[0]?.uuid }]);
const service = new BoardTaskLogStreamService(
recordSource as never,
summarySelector as never,
strictParser as never,
detailSelector as never,
{ buildBundleChunks } as never
);
const [summary, response] = await Promise.all([
service.getTaskLogStreamSummary('demo', 'task-a'),
service.getTaskLogStream('demo', 'task-a'),
]);
expect(summary).toEqual({ segmentCount: 1 });
expect(response.segments).toHaveLength(1);
expect(recordSource.getTaskRecords).toHaveBeenCalledTimes(1);
expect(strictParser.parseFiles).toHaveBeenCalledTimes(1);
});
it('merges duplicate message uuids inside one participant segment before chunk building', async () => {
const tom = {
memberName: 'tom',
role: 'member' as const,
sessionId: 'session-tom',
agentId: 'agent-tom',
isSidechain: true,
};
const candidates = [
makeCandidate('c1', '2026-04-12T16:00:00.000Z', tom, 'tool-1'),
makeCandidate('c2', '2026-04-12T16:00:10.000Z', tom, 'tool-2'),
];
const sharedMessage = {
uuid: 'assistant-shared',
parentUuid: null,
type: 'assistant' as const,
timestamp: new Date('2026-04-12T16:00:00.000Z'),
role: 'assistant',
toolCalls: [],
toolResults: [],
isSidechain: true,
isMeta: false,
isCompactSummary: false,
};
const recordSource = {
getTaskRecords: vi.fn(async () => candidates.flatMap((candidate) => candidate.records)),
};
const summarySelector = {
selectSummaries: vi.fn(() => candidates),
};
const strictParser = {
parseFiles: vi.fn(async () => new Map([['/tmp/task.jsonl', []]])),
};
const detailSelector = {
selectDetail: vi
.fn()
.mockImplementationOnce(() => ({
id: 'c1',
timestamp: '2026-04-12T16:00:00.000Z',
actor: tom,
source: { filePath: '/tmp/task.jsonl', messageUuid: 'assistant-shared', sourceOrder: 1 },
records: candidates[0]!.records,
filteredMessages: [
{
...sharedMessage,
content: [{ type: 'tool_use', id: 'tool-1', name: 'task_get', input: {} } as never],
},
],
}))
.mockImplementationOnce(() => ({
id: 'c2',
timestamp: '2026-04-12T16:00:10.000Z',
actor: tom,
source: { filePath: '/tmp/task.jsonl', messageUuid: 'assistant-shared', sourceOrder: 2 },
records: candidates[1]!.records,
filteredMessages: [
{
...sharedMessage,
content: [{ type: 'text', text: 'task looked up' } as never],
},
],
})),
};
const buildBundleChunks = vi.fn((messages: ParsedMessage[]) => [{ id: messages[0]?.uuid }]);
const service = new BoardTaskLogStreamService(
recordSource as never,
summarySelector as never,
strictParser as never,
detailSelector as never,
{ buildBundleChunks } as never,
);
await service.getTaskLogStream('demo', 'task-a');
expect(buildBundleChunks).toHaveBeenCalledTimes(1);
const mergedMessages = buildBundleChunks.mock.calls[0]?.[0] as ParsedMessage[];
expect(mergedMessages).toHaveLength(1);
expect(mergedMessages[0]?.toolCalls).toHaveLength(1);
expect(Array.isArray(mergedMessages[0]?.content)).toBe(true);
expect(mergedMessages[0]?.content).toHaveLength(2);
});
it('drops tool-anchored assistant output-only messages to avoid noisy raw result blocks', async () => {
const tom = {
memberName: 'tom',
role: 'member' as const,
sessionId: 'session-tom',
agentId: 'agent-tom',
isSidechain: true,
};
const candidate = makeCandidate('c1', '2026-04-12T16:00:00.000Z', tom, 'tool-1');
const recordSource = {
getTaskRecords: vi.fn(async () => candidate.records),
};
const summarySelector = {
selectSummaries: vi.fn(() => [candidate]),
};
const strictParser = {
parseFiles: vi.fn(async () => new Map([['/tmp/task.jsonl', []]])),
};
const detailSelector = {
selectDetail: vi.fn(() => ({
id: 'c1',
timestamp: '2026-04-12T16:00:00.000Z',
actor: tom,
source: { filePath: '/tmp/task.jsonl', messageUuid: 'assistant-tool', toolUseId: 'tool-1', sourceOrder: 1 },
records: candidate.records,
filteredMessages: [
{
uuid: 'assistant-tool',
parentUuid: null,
type: 'assistant' as const,
timestamp: new Date('2026-04-12T16:00:00.000Z'),
role: 'assistant',
content: [{ type: 'tool_use', id: 'tool-1', name: 'task_get', input: {} } as never],
toolCalls: [],
toolResults: [],
isSidechain: true,
isMeta: false,
isCompactSummary: false,
},
{
uuid: 'assistant-output',
parentUuid: 'assistant-tool',
type: 'assistant' as const,
timestamp: new Date('2026-04-12T16:00:01.000Z'),
role: 'assistant',
content: [{ type: 'text', text: '[{\"type\":\"text\",\"text\":\"{\\n \\\"id\\\": \\\"task-a\\\"\\n}\"}]' } as never],
toolCalls: [],
toolResults: [],
sourceToolUseID: 'tool-1',
sourceToolAssistantUUID: 'assistant-tool',
isSidechain: true,
isMeta: false,
isCompactSummary: false,
},
{
uuid: 'user-result',
parentUuid: 'assistant-tool',
type: 'user' as const,
timestamp: new Date('2026-04-12T16:00:02.000Z'),
role: 'user',
content: [{ type: 'tool_result', tool_use_id: 'tool-1', content: 'ok' } as never],
toolCalls: [],
toolResults: [],
sourceToolUseID: 'tool-1',
sourceToolAssistantUUID: 'assistant-tool',
toolUseResult: { toolUseId: 'tool-1', content: 'ok' },
isSidechain: true,
isMeta: false,
isCompactSummary: false,
},
],
})),
};
const buildBundleChunks = vi.fn((messages: ParsedMessage[]) => [{ id: messages[0]?.uuid }]);
const service = new BoardTaskLogStreamService(
recordSource as never,
summarySelector as never,
strictParser as never,
detailSelector as never,
{ buildBundleChunks } as never,
);
await service.getTaskLogStream('demo', 'task-a');
expect(buildBundleChunks).toHaveBeenCalledTimes(1);
const mergedMessages = buildBundleChunks.mock.calls[0]?.[0] as ParsedMessage[];
expect(mergedMessages.map((message) => message.uuid)).toEqual(['assistant-tool', 'user-result']);
});
it('defaults to the single named participant and excludes unnamed lead noise when named task logs exist', async () => {
const tom = {
memberName: 'tom',
role: 'lead' as const,
sessionId: 'session-tom',
isSidechain: false,
};
const unknownLead = {
role: 'unknown' as const,
sessionId: 'session-lead',
isSidechain: false,
};
const candidates = [
makeCandidate('c1', '2026-04-12T16:00:00.000Z', tom, 'tool-1'),
makeCandidate('c2', '2026-04-12T16:01:00.000Z', unknownLead, 'tool-2'),
];
const recordSource = {
getTaskRecords: vi.fn(async () => candidates.flatMap((candidate) => candidate.records)),
};
const summarySelector = {
selectSummaries: vi.fn(() => candidates),
};
const strictParser = {
parseFiles: vi.fn(async () => new Map([['/tmp/task.jsonl', []]])),
};
const detailSelector = {
selectDetail: vi.fn(({ candidate }: { candidate: BoardTaskExactLogBundleCandidate }) => ({
id: candidate.id,
timestamp: candidate.timestamp,
actor: candidate.actor,
source: candidate.source,
records: candidate.records,
filteredMessages: [makeMessage(candidate.id, candidate.timestamp, candidate.id)],
})),
};
const buildBundleChunks = vi.fn((messages: ParsedMessage[]) => [{ id: messages[0]?.uuid }]);
const service = new BoardTaskLogStreamService(
recordSource as never,
summarySelector as never,
strictParser as never,
detailSelector as never,
{ buildBundleChunks } as never,
);
const response = await service.getTaskLogStream('demo', 'task-a');
expect(response.participants.map((participant) => participant.key)).toEqual(['member:tom']);
expect(response.defaultFilter).toBe('member:tom');
expect(response.segments.map((segment) => segment.participantKey)).toEqual(['member:tom']);
});
it('drops empty json-like task_get tool result messages after sanitization', async () => {
const tom = {
memberName: 'tom',
role: 'member' as const,
sessionId: 'session-tom',
agentId: 'agent-tom',
isSidechain: true,
};
const candidate = makeCandidate('c1', '2026-04-12T16:00:00.000Z', tom, 'tool-1');
const recordSource = {
getTaskRecords: vi.fn(async () => candidate.records),
};
const summarySelector = {
selectSummaries: vi.fn(() => [candidate]),
};
const strictParser = {
parseFiles: vi.fn(async () => new Map([['/tmp/task.jsonl', []]])),
};
const detailSelector = {
selectDetail: vi.fn(() => ({
id: 'c1',
timestamp: '2026-04-12T16:00:00.000Z',
actor: tom,
source: { filePath: '/tmp/task.jsonl', messageUuid: 'assistant-tool', toolUseId: 'tool-1', sourceOrder: 1 },
records: candidate.records,
filteredMessages: [
{
uuid: 'assistant-tool',
parentUuid: null,
type: 'assistant' as const,
timestamp: new Date('2026-04-12T16:00:00.000Z'),
role: 'assistant',
content: [{ type: 'tool_use', id: 'tool-1', name: 'task_get', input: {} } as never],
toolCalls: [],
toolResults: [],
isSidechain: true,
isMeta: false,
isCompactSummary: false,
},
{
uuid: 'user-result',
parentUuid: 'assistant-tool',
type: 'user' as const,
timestamp: new Date('2026-04-12T16:00:02.000Z'),
role: 'user',
content: [
{
type: 'tool_result',
tool_use_id: 'tool-1',
content: [{ type: 'text', text: '{\n \"id\": \"task-a\"\n}' } as never],
} as never,
],
toolCalls: [],
toolResults: [],
sourceToolUseID: 'tool-1',
sourceToolAssistantUUID: 'assistant-tool',
toolUseResult: { toolUseId: 'tool-1', content: '{\n \"id\": \"task-a\"\n}' },
isSidechain: true,
isMeta: false,
isCompactSummary: false,
},
],
})),
};
const buildBundleChunks = vi.fn((messages: ParsedMessage[]) => [{ id: messages[0]?.uuid }]);
const service = new BoardTaskLogStreamService(
recordSource as never,
summarySelector as never,
strictParser as never,
detailSelector as never,
{ buildBundleChunks } as never,
);
await service.getTaskLogStream('demo', 'task-a');
const mergedMessages = buildBundleChunks.mock.calls[0]?.[0] as ParsedMessage[];
const toolResultMessage = mergedMessages.find((message) => message.uuid === 'user-result');
expect(toolResultMessage).toBeUndefined();
expect(mergedMessages.map((message) => message.uuid)).toEqual(['assistant-tool']);
});
it('drops read-only slices when the same participant has more meaningful task logs', async () => {
const tom = {
memberName: 'tom',
role: 'lead' as const,
sessionId: 'session-tom',
isSidechain: false,
};
const readCandidate = { ...makeCandidate('c1', '2026-04-12T16:00:00.000Z', tom, 'tool-1'), actionCategory: 'read' as const, canonicalToolName: 'task_get' };
const commentCandidate = { ...makeCandidate('c2', '2026-04-12T16:01:00.000Z', tom, 'tool-2'), actionCategory: 'comment' as const, canonicalToolName: 'task_add_comment' };
const recordSource = {
getTaskRecords: vi.fn(async () => [...readCandidate.records, ...commentCandidate.records]),
};
const summarySelector = {
selectSummaries: vi.fn(() => [readCandidate, commentCandidate]),
};
const strictParser = {
parseFiles: vi.fn(async () => new Map([['/tmp/task.jsonl', []]])),
};
const detailSelector = {
selectDetail: vi.fn(({ candidate }: { candidate: BoardTaskExactLogBundleCandidate }) => ({
id: candidate.id,
timestamp: candidate.timestamp,
actor: candidate.actor,
source: candidate.source,
records: candidate.records,
filteredMessages: [makeMessage(candidate.id, candidate.timestamp, candidate.id)],
})),
};
const buildBundleChunks = vi.fn((messages: ParsedMessage[]) => [{ id: messages[0]?.uuid }]);
const service = new BoardTaskLogStreamService(
recordSource as never,
summarySelector as never,
strictParser as never,
detailSelector as never,
{ buildBundleChunks } as never,
);
const response = await service.getTaskLogStream('demo', 'task-a');
expect(response.segments).toHaveLength(1);
expect(buildBundleChunks).toHaveBeenCalledTimes(1);
const mergedMessages = buildBundleChunks.mock.calls[0]?.[0] as ParsedMessage[];
expect(mergedMessages.map((message) => message.uuid)).toEqual(['c2']);
});
it('extracts task_add_comment text from json-like tool result payload', async () => {
const tom = {
memberName: 'tom',
role: 'lead' as const,
sessionId: 'session-tom',
isSidechain: false,
};
const candidate = {
...makeCandidate('c1', '2026-04-12T16:00:00.000Z', tom, 'tool-1'),
actionCategory: 'comment' as const,
canonicalToolName: 'task_add_comment',
};
const recordSource = {
getTaskRecords: vi.fn(async () => candidate.records),
};
const summarySelector = {
selectSummaries: vi.fn(() => [candidate]),
};
const strictParser = {
parseFiles: vi.fn(async () => new Map([['/tmp/task.jsonl', []]])),
};
const detailSelector = {
selectDetail: vi.fn(() => ({
id: 'c1',
timestamp: '2026-04-12T16:00:00.000Z',
actor: tom,
source: { filePath: '/tmp/task.jsonl', messageUuid: 'assistant-tool', toolUseId: 'tool-1', sourceOrder: 1 },
records: candidate.records,
filteredMessages: [
{
uuid: 'assistant-tool',
parentUuid: null,
type: 'assistant' as const,
timestamp: new Date('2026-04-12T16:00:00.000Z'),
role: 'assistant',
content: [{ type: 'tool_use', id: 'tool-1', name: 'task_add_comment', input: {} } as never],
toolCalls: [],
toolResults: [],
isSidechain: false,
isMeta: false,
isCompactSummary: false,
},
{
uuid: 'user-result',
parentUuid: 'assistant-tool',
type: 'user' as const,
timestamp: new Date('2026-04-12T16:00:02.000Z'),
role: 'user',
content: [
{
type: 'tool_result',
tool_use_id: 'tool-1',
content: [{ type: 'text', text: '{\"comment\":{\"text\":\"useful comment\"}}' } as never],
} as never,
],
toolCalls: [],
toolResults: [],
sourceToolUseID: 'tool-1',
sourceToolAssistantUUID: 'assistant-tool',
toolUseResult: { toolUseId: 'tool-1', content: '{"comment":{"text":"useful comment"}}' },
isSidechain: false,
isMeta: false,
isCompactSummary: false,
},
],
})),
};
const buildBundleChunks = vi.fn((messages: ParsedMessage[]) => [{ id: messages[0]?.uuid }]);
const service = new BoardTaskLogStreamService(
recordSource as never,
summarySelector as never,
strictParser as never,
detailSelector as never,
{ buildBundleChunks } as never,
);
await service.getTaskLogStream('demo', 'task-a');
const mergedMessages = buildBundleChunks.mock.calls[0]?.[0] as ParsedMessage[];
const toolResultMessage = mergedMessages.find((message) => message.uuid === 'user-result');
const content = Array.isArray(toolResultMessage?.content) ? toolResultMessage.content : [];
expect(content[0]).toMatchObject({
type: 'tool_result',
tool_use_id: 'tool-1',
content: 'useful comment',
});
expect(toolResultMessage?.toolUseResult).toEqual({ toolUseId: 'tool-1', content: 'useful comment' });
});
it('sanitizes SendMessage json payloads into a concise human-readable result', async () => {
const bob = {
memberName: 'bob',
role: 'member' as const,
sessionId: 'session-bob',
agentId: 'agent-bob',
isSidechain: true,
};
const candidate = {
...makeCandidate('c1', '2026-04-12T16:00:00.000Z', bob, 'tool-send'),
actionCategory: 'execution' as const,
canonicalToolName: 'SendMessage',
};
const recordSource = {
getTaskRecords: vi.fn(async () => candidate.records),
};
const summarySelector = {
selectSummaries: vi.fn(() => [candidate]),
};
const strictParser = {
parseFiles: vi.fn(async () => new Map([['/tmp/task.jsonl', []]])),
};
const detailSelector = {
selectDetail: vi.fn(() => ({
id: 'c1',
timestamp: '2026-04-12T16:00:00.000Z',
actor: bob,
source: {
filePath: '/tmp/task.jsonl',
messageUuid: 'assistant-send',
toolUseId: 'tool-send',
sourceOrder: 1,
},
records: candidate.records,
filteredMessages: [
{
uuid: 'assistant-send',
parentUuid: null,
type: 'assistant' as const,
timestamp: new Date('2026-04-12T16:00:00.000Z'),
role: 'assistant',
content: [
{
type: 'tool_use',
id: 'tool-send',
name: 'SendMessage',
input: { to: 'team-lead', summary: '#abc done' },
} as never,
],
toolCalls: [],
toolResults: [],
isSidechain: false,
isMeta: false,
isCompactSummary: false,
},
{
uuid: 'user-send-result',
parentUuid: 'assistant-send',
type: 'user' as const,
timestamp: new Date('2026-04-12T16:00:02.000Z'),
role: 'user',
content: [
{
type: 'tool_result',
tool_use_id: 'tool-send',
content: [
{
type: 'text',
text: JSON.stringify({
success: true,
message: "Message sent to team-lead's inbox",
routing: {
target: '@team-lead',
summary: '#abc done',
content: 'Detailed body that should not leak into the preview.',
},
}),
} as never,
],
} as never,
],
toolCalls: [],
toolResults: [
{
toolUseId: 'tool-send',
content: [
{
type: 'text',
text: JSON.stringify({
success: true,
message: "Message sent to team-lead's inbox",
routing: {
target: '@team-lead',
summary: '#abc done',
content: 'Detailed body that should not leak into the preview.',
},
}),
},
],
isError: false,
},
],
sourceToolUseID: 'tool-send',
sourceToolAssistantUUID: 'assistant-send',
toolUseResult: {
success: true,
message: "Message sent to team-lead's inbox",
routing: {
target: '@team-lead',
summary: '#abc done',
content: 'Detailed body that should not leak into the preview.',
},
},
isSidechain: false,
isMeta: false,
isCompactSummary: false,
},
],
})),
};
const buildBundleChunks = vi.fn((messages: ParsedMessage[]) => [{ id: messages[0]?.uuid }]);
const service = new BoardTaskLogStreamService(
recordSource as never,
summarySelector as never,
strictParser as never,
detailSelector as never,
{ buildBundleChunks } as never,
);
await service.getTaskLogStream('demo', 'task-a');
const mergedMessages = buildBundleChunks.mock.calls[0]?.[0] as ParsedMessage[];
const toolResultMessage = mergedMessages.find((message) => message.uuid === 'user-send-result');
const content = Array.isArray(toolResultMessage?.content) ? toolResultMessage.content : [];
expect(content[0]).toMatchObject({
type: 'tool_result',
tool_use_id: 'tool-send',
content: "Message sent to team-lead's inbox - #abc done",
});
expect(toolResultMessage?.toolResults).toEqual([
{
toolUseId: 'tool-send',
content: "Message sent to team-lead's inbox - #abc done",
isError: false,
},
]);
});
});