262 lines
7.8 KiB
TypeScript
262 lines
7.8 KiB
TypeScript
import { describe, expect, it, vi } from 'vitest';
|
|
|
|
import { TeamTaskStallSnapshotSource } from '../../../../../src/main/services/team/stallMonitor/TeamTaskStallSnapshotSource';
|
|
|
|
describe('TeamTaskStallSnapshotSource', () => {
|
|
it('returns null when transcript context is unavailable', async () => {
|
|
const source = new TeamTaskStallSnapshotSource(
|
|
{ getContext: vi.fn(async () => null) } as never,
|
|
{} as never,
|
|
{} as never,
|
|
{} as never,
|
|
{} as never,
|
|
{} as never,
|
|
{} as never,
|
|
{} as never
|
|
);
|
|
|
|
await expect(source.getSnapshot('demo')).resolves.toBeNull();
|
|
});
|
|
|
|
it('builds one batched snapshot and narrows exact/freshness reads to work and started-review candidates', async () => {
|
|
const activeTasks = [
|
|
{ id: 'task-a', subject: 'A', status: 'in_progress' },
|
|
{
|
|
id: 'task-b',
|
|
subject: 'B',
|
|
status: 'completed',
|
|
reviewState: 'review',
|
|
historyEvents: [
|
|
{
|
|
id: 'evt-review-requested',
|
|
type: 'review_requested',
|
|
timestamp: '2026-04-19T12:00:00.000Z',
|
|
from: 'none',
|
|
to: 'review',
|
|
reviewer: 'alice',
|
|
},
|
|
],
|
|
},
|
|
];
|
|
const deletedTasks = [{ id: 'task-deleted', subject: 'D', status: 'deleted' }];
|
|
const transcriptContext = {
|
|
projectDir: '/tmp/project',
|
|
projectId: 'project-id',
|
|
config: {
|
|
members: [
|
|
{ name: 'team-lead', role: 'team lead', providerId: 'codex' },
|
|
{ name: 'alice', role: 'Developer', model: 'qwen/qwen3-coder' },
|
|
],
|
|
} as never,
|
|
sessionIds: ['session-a'],
|
|
transcriptFiles: ['/tmp/project/session-a.jsonl', '/tmp/project/session-b.jsonl'],
|
|
};
|
|
const rawMessages = [{ uuid: 'm1' }];
|
|
const recordsByTaskId = new Map([
|
|
[
|
|
'task-a',
|
|
[
|
|
{
|
|
id: 'r1',
|
|
source: {
|
|
filePath: '/tmp/project/session-b.jsonl',
|
|
},
|
|
},
|
|
],
|
|
],
|
|
[
|
|
'task-b',
|
|
[
|
|
{
|
|
id: 'r2',
|
|
source: {
|
|
filePath: '/tmp/project/session-a.jsonl',
|
|
},
|
|
},
|
|
],
|
|
],
|
|
]);
|
|
const freshnessByTaskId = new Map([
|
|
['task-a', { taskId: 'task-a', updatedAt: '2026-04-19T12:00:00.000Z', filePath: '/tmp/fresh.json' }],
|
|
]);
|
|
const exactRowsByFilePath = new Map([['/tmp/project/session-b.jsonl', []]]);
|
|
|
|
const locator = {
|
|
getContext: vi.fn(async () => transcriptContext),
|
|
};
|
|
const taskReader = {
|
|
getTasks: vi.fn(async () => activeTasks),
|
|
getDeletedTasks: vi.fn(async () => deletedTasks),
|
|
};
|
|
const kanbanManager = {
|
|
getState: vi.fn(async () => ({
|
|
teamName: 'demo',
|
|
reviewers: ['alice'],
|
|
tasks: {
|
|
'task-b': {
|
|
column: 'review',
|
|
movedAt: '2026-04-19T12:00:00.000Z',
|
|
reviewer: 'alice',
|
|
},
|
|
},
|
|
})),
|
|
};
|
|
const transcriptReader = {
|
|
readFiles: vi.fn(async () => rawMessages),
|
|
};
|
|
const batchIndexer = {
|
|
buildIndex: vi.fn(() => recordsByTaskId),
|
|
};
|
|
const freshnessReader = {
|
|
readSignals: vi.fn(async () => freshnessByTaskId),
|
|
};
|
|
const exactRowReader = {
|
|
parseFiles: vi.fn(async () => exactRowsByFilePath),
|
|
};
|
|
const membersMetaStore = {
|
|
getMembers: vi.fn(async () => [{ name: 'alice', providerId: 'opencode' }]),
|
|
};
|
|
const openCodeEvidenceSource = {
|
|
readEvidence: vi.fn(async () => ({
|
|
recordsByTaskId: new Map(),
|
|
exactRowsByFilePath: new Map(),
|
|
})),
|
|
};
|
|
|
|
const source = new TeamTaskStallSnapshotSource(
|
|
locator as never,
|
|
taskReader as never,
|
|
kanbanManager as never,
|
|
transcriptReader as never,
|
|
batchIndexer as never,
|
|
freshnessReader as never,
|
|
exactRowReader as never,
|
|
membersMetaStore as never,
|
|
openCodeEvidenceSource as never
|
|
);
|
|
|
|
const snapshot = await source.getSnapshot('demo');
|
|
|
|
expect(snapshot).not.toBeNull();
|
|
expect(batchIndexer.buildIndex).toHaveBeenCalledWith({
|
|
teamName: 'demo',
|
|
tasks: [...activeTasks, ...deletedTasks],
|
|
messages: rawMessages,
|
|
});
|
|
expect(freshnessReader.readSignals).toHaveBeenCalledWith('/tmp/project', ['task-a', 'task-b']);
|
|
expect(exactRowReader.parseFiles).toHaveBeenCalledWith(['/tmp/project/session-a.jsonl', '/tmp/project/session-b.jsonl']);
|
|
expect(openCodeEvidenceSource.readEvidence).toHaveBeenCalledWith({
|
|
teamName: 'demo',
|
|
tasks: [activeTasks[0], activeTasks[1]],
|
|
providerByMemberName: new Map([
|
|
['team-lead', 'codex'],
|
|
['alice', 'opencode'],
|
|
]),
|
|
});
|
|
expect(snapshot?.inProgressTasks.map((task) => task.id)).toEqual(['task-a']);
|
|
expect(snapshot?.reviewOpenTasks.map((task) => task.id)).toEqual(['task-b']);
|
|
expect(snapshot?.leadName).toBe('team-lead');
|
|
expect(snapshot?.providerByMemberName).toEqual(
|
|
new Map([
|
|
['team-lead', 'codex'],
|
|
['alice', 'opencode'],
|
|
])
|
|
);
|
|
expect(snapshot?.resolvedReviewersByTaskId.get('task-b')).toEqual({
|
|
reviewer: 'alice',
|
|
source: 'kanban_state',
|
|
});
|
|
expect(snapshot?.recordsByTaskId).toBe(recordsByTaskId);
|
|
});
|
|
|
|
it('merges OpenCode runtime evidence even when no Claude transcript files are available', async () => {
|
|
const task = {
|
|
id: 'task-open',
|
|
displayId: 'opencode1',
|
|
subject: 'OpenCode task',
|
|
status: 'in_progress',
|
|
owner: 'bob',
|
|
};
|
|
const openCodeRecord = {
|
|
id: 'opencode-rec',
|
|
timestamp: '2026-04-19T12:00:00.000Z',
|
|
source: {
|
|
filePath: 'opencode-runtime:demo:bob',
|
|
sourceOrder: 1,
|
|
},
|
|
};
|
|
const openCodeRows = [
|
|
{
|
|
filePath: 'opencode-runtime:demo:bob',
|
|
sourceOrder: 1,
|
|
messageUuid: 'msg-open',
|
|
timestamp: '2026-04-19T12:00:00.000Z',
|
|
parsedMessage: {
|
|
uuid: 'msg-open',
|
|
parentUuid: null,
|
|
type: 'assistant',
|
|
timestamp: new Date('2026-04-19T12:00:00.000Z'),
|
|
content: '',
|
|
isSidechain: true,
|
|
isMeta: false,
|
|
toolCalls: [],
|
|
toolResults: [],
|
|
},
|
|
toolUseIds: [],
|
|
toolResultIds: [],
|
|
},
|
|
];
|
|
const source = new TeamTaskStallSnapshotSource(
|
|
{
|
|
getContext: vi.fn(async () => ({
|
|
projectDir: '/tmp/project',
|
|
projectId: 'project-id',
|
|
config: {
|
|
members: [
|
|
{ name: 'team-lead', role: 'team lead', providerId: 'codex' },
|
|
{ name: 'bob', role: 'Developer', providerId: 'opencode' },
|
|
],
|
|
},
|
|
sessionIds: [],
|
|
transcriptFiles: [],
|
|
})),
|
|
} as never,
|
|
{
|
|
getTasks: vi.fn(async () => [task]),
|
|
getDeletedTasks: vi.fn(async () => []),
|
|
} as never,
|
|
{
|
|
getState: vi.fn(async () => ({ teamName: 'demo', tasks: {} })),
|
|
} as never,
|
|
{
|
|
readFiles: vi.fn(async () => {
|
|
throw new Error('transcript reader should not be called');
|
|
}),
|
|
} as never,
|
|
{
|
|
buildIndex: vi.fn(() => new Map()),
|
|
} as never,
|
|
{
|
|
readSignals: vi.fn(async () => new Map()),
|
|
} as never,
|
|
{
|
|
parseFiles: vi.fn(async () => new Map()),
|
|
} as never,
|
|
{
|
|
getMembers: vi.fn(async () => []),
|
|
} as never,
|
|
{
|
|
readEvidence: vi.fn(async () => ({
|
|
recordsByTaskId: new Map([['task-open', [openCodeRecord]]]),
|
|
exactRowsByFilePath: new Map([['opencode-runtime:demo:bob', openCodeRows]]),
|
|
})),
|
|
} as never
|
|
);
|
|
|
|
const snapshot = await source.getSnapshot('demo');
|
|
|
|
expect(snapshot?.recordsByTaskId.get('task-open')).toEqual([openCodeRecord]);
|
|
expect(snapshot?.exactRowsByFilePath.get('opencode-runtime:demo:bob')).toEqual(openCodeRows);
|
|
expect(snapshot?.transcriptFiles).toEqual([]);
|
|
});
|
|
});
|