212 lines
6.6 KiB
TypeScript
212 lines
6.6 KiB
TypeScript
import { describe, expect, it, vi } from 'vitest';
|
|
|
|
import { BoardTaskExactLogDetailService } from '../../../../src/main/services/team/taskLogs/exact/BoardTaskExactLogDetailService';
|
|
|
|
import type { BoardTaskActivityRecord } from '../../../../src/main/services/team/taskLogs/activity/BoardTaskActivityRecord';
|
|
import type {
|
|
BoardTaskExactLogBundleCandidate,
|
|
BoardTaskExactLogDetailCandidate,
|
|
} 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: 'msg-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: 'msg-1',
|
|
toolUseId: 'tool-1',
|
|
sourceOrder: 1,
|
|
},
|
|
records,
|
|
anchor: {
|
|
kind: 'tool',
|
|
filePath: '/tmp/task.jsonl',
|
|
messageUuid: 'msg-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('BoardTaskExactLogDetailService', () => {
|
|
it('returns missing when the exact-log read flag is disabled', async () => {
|
|
vi.stubEnv('CLAUDE_TEAM_BOARD_TASK_EXACT_LOGS_READ_ENABLED', 'false');
|
|
const recordSource = { getTaskRecords: vi.fn(async () => []) };
|
|
const service = new BoardTaskExactLogDetailService(
|
|
recordSource as never,
|
|
{ selectSummaries: vi.fn() } as never,
|
|
{ parseFiles: vi.fn() } as never,
|
|
{ selectDetail: vi.fn() } as never,
|
|
{ buildBundleChunks: vi.fn() } as never
|
|
);
|
|
|
|
await expect(
|
|
service.getTaskExactLogDetail('demo', 'task-a', 'tool:/tmp/task.jsonl:tool-1', 'gen-1')
|
|
).resolves.toEqual({ status: 'missing' });
|
|
expect(recordSource.getTaskRecords).not.toHaveBeenCalled();
|
|
vi.unstubAllEnvs();
|
|
});
|
|
|
|
it('returns stale when the expected source generation no longer matches', async () => {
|
|
const records = [makeRecord()];
|
|
const recordSource = { getTaskRecords: vi.fn(async () => records) };
|
|
const summarySelector = {
|
|
selectSummaries: vi.fn(() => [makeCandidate(records)]),
|
|
};
|
|
|
|
const service = new BoardTaskExactLogDetailService(
|
|
recordSource as never,
|
|
summarySelector as never,
|
|
{ parseFiles: vi.fn() } as never,
|
|
{ selectDetail: vi.fn() } as never,
|
|
{ buildBundleChunks: vi.fn() } as never
|
|
);
|
|
|
|
const result = await service.getTaskExactLogDetail('demo', 'task-a', 'tool:/tmp/task.jsonl:tool-1', 'gen-old');
|
|
|
|
expect(result).toEqual({ status: 'stale' });
|
|
});
|
|
|
|
it('returns ok when a matching detail bundle is reconstructed', async () => {
|
|
const records = [makeRecord()];
|
|
const candidate = makeCandidate(records);
|
|
const detailCandidate: BoardTaskExactLogDetailCandidate = {
|
|
id: candidate.id,
|
|
timestamp: candidate.timestamp,
|
|
actor: candidate.actor,
|
|
source: candidate.source,
|
|
records,
|
|
filteredMessages: [],
|
|
};
|
|
|
|
const recordSource = { getTaskRecords: vi.fn(async () => records) };
|
|
const summarySelector = {
|
|
selectSummaries: vi.fn(() => [candidate]),
|
|
};
|
|
const strictParser = {
|
|
parseFiles: vi.fn(async () => new Map([['/tmp/task.jsonl', []]])),
|
|
};
|
|
const detailSelector = {
|
|
selectDetail: vi.fn(() => detailCandidate),
|
|
};
|
|
const chunkBuilder = {
|
|
buildBundleChunks: vi.fn(() => []),
|
|
};
|
|
|
|
const service = new BoardTaskExactLogDetailService(
|
|
recordSource as never,
|
|
summarySelector as never,
|
|
strictParser as never,
|
|
detailSelector as never,
|
|
chunkBuilder as never
|
|
);
|
|
|
|
const result = await service.getTaskExactLogDetail(
|
|
'demo',
|
|
'task-a',
|
|
candidate.id,
|
|
'gen-1'
|
|
);
|
|
|
|
expect(result).toEqual({
|
|
status: 'ok',
|
|
detail: {
|
|
id: candidate.id,
|
|
chunks: [],
|
|
},
|
|
});
|
|
});
|
|
|
|
it('returns missing for non-expandable summaries without parsing transcript content', async () => {
|
|
const records = [makeRecord()];
|
|
const nonExpandableCandidate: BoardTaskExactLogBundleCandidate = {
|
|
...makeCandidate(records),
|
|
canLoadDetail: false,
|
|
};
|
|
const recordSource = { getTaskRecords: vi.fn(async () => records) };
|
|
const summarySelector = {
|
|
selectSummaries: vi.fn(() => [nonExpandableCandidate]),
|
|
};
|
|
const strictParser = {
|
|
parseFiles: vi.fn(async () => new Map()),
|
|
};
|
|
|
|
const service = new BoardTaskExactLogDetailService(
|
|
recordSource as never,
|
|
summarySelector as never,
|
|
strictParser as never,
|
|
{ selectDetail: vi.fn() } as never,
|
|
{ buildBundleChunks: vi.fn() } as never
|
|
);
|
|
|
|
const result = await service.getTaskExactLogDetail('demo', 'task-a', nonExpandableCandidate.id, 'gen-1');
|
|
|
|
expect(result).toEqual({ status: 'missing' });
|
|
expect(strictParser.parseFiles).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('returns missing when strict detail reconstruction fails for malformed transcript data', async () => {
|
|
const records = [makeRecord()];
|
|
const candidate = makeCandidate(records);
|
|
const strictParser = {
|
|
parseFiles: vi.fn(async () => new Map([['/tmp/task.jsonl', []]])),
|
|
};
|
|
const detailSelector = {
|
|
selectDetail: vi.fn(() => null),
|
|
};
|
|
|
|
const service = new BoardTaskExactLogDetailService(
|
|
{ getTaskRecords: vi.fn(async () => records) } as never,
|
|
{ selectSummaries: vi.fn(() => [candidate]) } as never,
|
|
strictParser as never,
|
|
detailSelector as never,
|
|
{ buildBundleChunks: vi.fn() } as never
|
|
);
|
|
|
|
const result = await service.getTaskExactLogDetail('demo', 'task-a', candidate.id, 'gen-1');
|
|
|
|
expect(result).toEqual({ status: 'missing' });
|
|
expect(strictParser.parseFiles).toHaveBeenCalledWith(['/tmp/task.jsonl']);
|
|
expect(detailSelector.selectDetail).toHaveBeenCalled();
|
|
});
|
|
});
|