agent-ecosystem/test/main/services/team/OpenCodeTaskLogStreamSource.test.ts
2026-04-21 20:28:22 +03:00

1110 lines
35 KiB
TypeScript

import { describe, expect, it, vi } from 'vitest';
import { OpenCodeTaskLogStreamSource } from '../../../../src/main/services/team/taskLogs/stream/OpenCodeTaskLogStreamSource';
import type { TeamTask } from '../../../../src/shared/types';
import type { OpenCodeRuntimeTranscriptLogMessage } from '../../../../src/main/services/runtime/ClaudeMultimodelBridgeService';
import type { OpenCodeTaskLogAttributionRecord } from '../../../../src/main/services/team/taskLogs/stream/OpenCodeTaskLogAttributionStore';
function createTask(overrides: Partial<TeamTask> = {}): TeamTask {
return {
id: 'task-a',
displayId: 'task-a',
subject: 'Investigate failing command',
owner: 'alice',
status: 'in_progress',
createdAt: '2026-04-21T09:59:00.000Z',
updatedAt: '2026-04-21T10:10:00.000Z',
workIntervals: [
{
startedAt: '2026-04-21T10:00:00.000Z',
completedAt: '2026-04-21T10:10:00.000Z',
},
],
...overrides,
};
}
function textLogMessage(
overrides: Pick<OpenCodeRuntimeTranscriptLogMessage, 'uuid' | 'timestamp'> &
Partial<OpenCodeRuntimeTranscriptLogMessage>
): OpenCodeRuntimeTranscriptLogMessage {
const type = overrides.type ?? 'assistant';
return {
uuid: overrides.uuid,
parentUuid: overrides.parentUuid ?? null,
type,
timestamp: overrides.timestamp,
role: overrides.role ?? type,
content: overrides.content ?? [{ type: 'text', text: overrides.uuid }],
isMeta: overrides.isMeta ?? false,
sessionId: overrides.sessionId ?? 'session-opencode',
toolCalls: overrides.toolCalls ?? [],
toolResults: overrides.toolResults ?? [],
...(overrides.sourceToolUseID ? { sourceToolUseID: overrides.sourceToolUseID } : {}),
...(overrides.sourceToolAssistantUUID
? { sourceToolAssistantUUID: overrides.sourceToolAssistantUUID }
: {}),
};
}
function taskMarkerLogMessage({
uuid,
parentUuid = null,
timestamp,
toolName,
input,
}: {
uuid: string;
parentUuid?: string | null;
timestamp: string;
toolName: string;
input: Record<string, unknown>;
}): OpenCodeRuntimeTranscriptLogMessage {
const toolId = `${uuid}-tool`;
return textLogMessage({
uuid,
parentUuid,
timestamp,
content: [
{
type: 'tool_use',
id: toolId,
name: toolName,
input,
},
],
toolCalls: [
{
id: toolId,
name: toolName,
input,
isTask: false,
},
],
});
}
function toolResultLogMessage({
uuid,
parentUuid,
timestamp,
sourceToolAssistantUUID,
}: {
uuid: string;
parentUuid: string;
timestamp: string;
sourceToolAssistantUUID: string;
}): OpenCodeRuntimeTranscriptLogMessage {
const toolUseId = `${sourceToolAssistantUUID}-tool`;
return textLogMessage({
uuid,
parentUuid,
type: 'user',
role: 'user',
timestamp,
content: [
{
type: 'tool_result',
tool_use_id: toolUseId,
content: 'ok',
},
],
isMeta: true,
toolResults: [
{
toolUseId,
content: 'ok',
isError: false,
},
],
sourceToolUseID: toolUseId,
sourceToolAssistantUUID,
});
}
describe('OpenCodeTaskLogStreamSource', () => {
it('projects OpenCode runtime logs into a task stream segment and caches repeated reads', async () => {
const bridge = {
getOpenCodeTranscript: vi.fn(async () => ({
sessionId: 'session-opencode',
logProjection: {
messages: [
{
uuid: 'assistant-1',
parentUuid: 'user-1',
type: 'assistant',
timestamp: '2026-04-21T10:05:00.000Z',
role: 'assistant',
content: [
{ type: 'thinking', thinking: 'Inspecting repository', signature: 'opencode' },
{ type: 'text', text: 'Running the check now.' },
{
type: 'tool_use',
id: 'tool-1',
name: 'bash',
input: { command: 'npm test' },
},
],
isMeta: false,
sessionId: 'session-opencode',
toolCalls: [
{
id: 'tool-1',
name: 'bash',
input: { command: 'npm test' },
isTask: false,
},
],
toolResults: [],
},
{
uuid: 'assistant-1::tool_results',
parentUuid: 'assistant-1',
type: 'user',
timestamp: '2026-04-21T10:05:03.000Z',
role: 'user',
content: [
{
type: 'tool_result',
tool_use_id: 'tool-1',
content: 'ok',
},
],
isMeta: true,
sessionId: 'session-opencode',
toolCalls: [],
toolResults: [
{
toolUseId: 'tool-1',
content: 'ok',
isError: false,
},
],
sourceToolUseID: 'tool-1',
sourceToolAssistantUUID: 'assistant-1',
},
{
uuid: 'assistant-outside-window',
parentUuid: 'user-2',
type: 'assistant',
timestamp: '2026-04-21T08:00:00.000Z',
role: 'assistant',
content: [{ type: 'text', text: 'Old task output' }],
isMeta: false,
sessionId: 'session-opencode',
toolCalls: [],
toolResults: [],
},
],
},
})),
};
const chunkBuilder = {
buildBundleChunks: vi.fn((messages) => [
{
id: 'chunk-1',
kind: 'assistant',
messages,
},
]),
};
const taskReader = {
getTasks: vi.fn(async () => [createTask()]),
getDeletedTasks: vi.fn(async () => []),
};
const source = new OpenCodeTaskLogStreamSource(
bridge as never,
{ resolve: async () => '/tmp/claude' },
taskReader as never,
chunkBuilder as never,
{ readTaskRecords: vi.fn(async () => []) }
);
const first = await source.getTaskLogStream('team-a', 'task-a');
const second = await source.getTaskLogStream('team-a', 'task-a');
expect(first?.source).toBe('opencode_runtime_fallback');
expect(first?.runtimeProjection).toEqual({
provider: 'opencode',
mode: 'heuristic',
attributionRecordCount: 0,
projectedMessageCount: 2,
fallbackReason: 'no_attribution_records',
});
expect(first?.participants).toEqual([
{
key: 'member:alice',
label: 'alice',
role: 'member',
isLead: false,
isSidechain: true,
},
]);
expect(first?.segments).toHaveLength(1);
expect(first?.segments[0]?.actor).toEqual({
memberName: 'alice',
role: 'member',
sessionId: 'session-opencode',
isSidechain: true,
});
expect(chunkBuilder.buildBundleChunks).toHaveBeenCalledTimes(1);
expect(chunkBuilder.buildBundleChunks.mock.calls[0]?.[0]).toHaveLength(2);
expect(
chunkBuilder.buildBundleChunks.mock.calls[0]?.[0].map((message: { uuid: string }) => message.uuid)
).toEqual(['assistant-1', 'assistant-1::tool_results']);
expect(bridge.getOpenCodeTranscript).toHaveBeenCalledTimes(1);
expect(second).toEqual(first);
});
it('returns null when the task has no owner', async () => {
const source = new OpenCodeTaskLogStreamSource(
{ getOpenCodeTranscript: vi.fn() } as never,
{ resolve: async () => '/tmp/claude' },
{
getTasks: async () => [createTask({ owner: undefined })],
getDeletedTasks: async () => [],
} as never,
{ buildBundleChunks: vi.fn() } as never,
{ readTaskRecords: vi.fn(async () => []) }
);
await expect(source.getTaskLogStream('team-a', 'task-a')).resolves.toBeNull();
});
it('narrows owner fallback to OpenCode task tool marker ranges when available', async () => {
const bridge = {
getOpenCodeTranscript: vi.fn(async () => ({
sessionId: 'session-opencode',
logProjection: {
messages: [
{
uuid: 'unrelated-before',
parentUuid: undefined,
type: 'assistant',
timestamp: '2026-04-21T10:01:00.000Z',
role: 'assistant',
content: [{ type: 'text', text: 'Other work before the task marker' }],
isMeta: false,
sessionId: 'session-opencode',
toolCalls: [],
toolResults: [],
},
{
uuid: 'user-task-prompt',
parentUuid: undefined,
type: 'user',
timestamp: '2026-04-21T10:02:00.000Z',
role: 'user',
content: [{ type: 'text', text: 'Start task-a now' }],
isMeta: false,
sessionId: 'session-opencode',
toolCalls: [],
toolResults: [],
},
{
uuid: 'assistant-start',
parentUuid: 'user-task-prompt',
type: 'assistant',
timestamp: '2026-04-21T10:03:00.000Z',
role: 'assistant',
content: [
{
type: 'tool_use',
id: 'tool-start',
name: 'mcp__agent-teams__task_start',
input: { teamName: 'team-a', taskId: 'task-a' },
},
],
isMeta: false,
sessionId: 'session-opencode',
toolCalls: [
{
id: 'tool-start',
name: 'mcp__agent-teams__task_start',
input: { teamName: 'team-a', taskId: 'task-a' },
isTask: false,
},
],
toolResults: [],
},
{
uuid: 'assistant-start::tool_results',
parentUuid: 'assistant-start',
type: 'user',
timestamp: '2026-04-21T10:03:01.000Z',
role: 'user',
content: [
{
type: 'tool_result',
tool_use_id: 'tool-start',
content: 'started',
},
],
isMeta: true,
sessionId: 'session-opencode',
toolCalls: [],
toolResults: [
{
toolUseId: 'tool-start',
content: 'started',
isError: false,
},
],
sourceToolUseID: 'tool-start',
sourceToolAssistantUUID: 'assistant-start',
},
{
uuid: 'assistant-work',
parentUuid: 'assistant-start::tool_results',
type: 'assistant',
timestamp: '2026-04-21T10:04:00.000Z',
role: 'assistant',
content: [{ type: 'text', text: 'Doing the actual work' }],
isMeta: false,
sessionId: 'session-opencode',
toolCalls: [],
toolResults: [],
},
{
uuid: 'assistant-complete',
parentUuid: 'assistant-work',
type: 'assistant',
timestamp: '2026-04-21T10:06:00.000Z',
role: 'assistant',
content: [
{
type: 'tool_use',
id: 'tool-complete',
name: 'mcp__agent-teams__task_complete',
input: { teamName: 'team-a', taskId: 'task-a' },
},
],
isMeta: false,
sessionId: 'session-opencode',
toolCalls: [
{
id: 'tool-complete',
name: 'mcp__agent-teams__task_complete',
input: { teamName: 'team-a', taskId: 'task-a' },
isTask: false,
},
],
toolResults: [],
},
{
uuid: 'assistant-complete::tool_results',
parentUuid: 'assistant-complete',
type: 'user',
timestamp: '2026-04-21T10:06:01.000Z',
role: 'user',
content: [
{
type: 'tool_result',
tool_use_id: 'tool-complete',
content: 'completed',
},
],
isMeta: true,
sessionId: 'session-opencode',
toolCalls: [],
toolResults: [
{
toolUseId: 'tool-complete',
content: 'completed',
isError: false,
},
],
sourceToolUseID: 'tool-complete',
sourceToolAssistantUUID: 'assistant-complete',
},
{
uuid: 'unrelated-after',
parentUuid: 'assistant-complete::tool_results',
type: 'assistant',
timestamp: '2026-04-21T10:07:00.000Z',
role: 'assistant',
content: [{ type: 'text', text: 'Other work after task completion' }],
isMeta: false,
sessionId: 'session-opencode',
toolCalls: [],
toolResults: [],
},
],
},
})),
};
const chunkBuilder = {
buildBundleChunks: vi.fn((messages) => [
{
id: 'chunk-marker',
kind: 'assistant',
messages,
},
]),
};
const source = new OpenCodeTaskLogStreamSource(
bridge as never,
{ resolve: async () => '/tmp/claude' },
{
getTasks: async () => [createTask()],
getDeletedTasks: async () => [],
} as never,
chunkBuilder as never,
{ readTaskRecords: vi.fn(async () => []) }
);
const response = await source.getTaskLogStream('team-a', 'task-a');
expect(response?.source).toBe('opencode_runtime_fallback');
expect(response?.runtimeProjection).toEqual({
provider: 'opencode',
mode: 'heuristic',
attributionRecordCount: 0,
projectedMessageCount: 6,
fallbackReason: 'task_tool_markers',
markerMatchCount: 2,
markerSpanCount: 1,
});
expect(
chunkBuilder.buildBundleChunks.mock.calls[0]?.[0].map((message: { uuid: string }) => message.uuid)
).toEqual([
'user-task-prompt',
'assistant-start',
'assistant-start::tool_results',
'assistant-work',
'assistant-complete',
'assistant-complete::tool_results',
]);
});
it('ignores OpenCode task markers that explicitly belong to another team', async () => {
const bridge = {
getOpenCodeTranscript: vi.fn(async () => ({
sessionId: 'session-opencode',
logProjection: {
messages: [
textLogMessage({
uuid: 'other-team-prompt',
type: 'user',
role: 'user',
timestamp: '2026-04-21T10:01:00.000Z',
}),
taskMarkerLogMessage({
uuid: 'other-team-start',
parentUuid: 'other-team-prompt',
timestamp: '2026-04-21T10:02:00.000Z',
toolName: 'mcp__agent-teams__task_start',
input: { teamName: 'other-team', taskId: 'task-a' },
}),
toolResultLogMessage({
uuid: 'other-team-start::tool_results',
parentUuid: 'other-team-start',
timestamp: '2026-04-21T10:02:01.000Z',
sourceToolAssistantUUID: 'other-team-start',
}),
textLogMessage({
uuid: 'team-a-prompt',
type: 'user',
role: 'user',
timestamp: '2026-04-21T10:03:00.000Z',
}),
taskMarkerLogMessage({
uuid: 'team-a-start',
parentUuid: 'team-a-prompt',
timestamp: '2026-04-21T10:04:00.000Z',
toolName: 'mcp__agent-teams__task_start',
input: { teamName: 'team-a', taskId: 'task-a' },
}),
toolResultLogMessage({
uuid: 'team-a-start::tool_results',
parentUuid: 'team-a-start',
timestamp: '2026-04-21T10:04:01.000Z',
sourceToolAssistantUUID: 'team-a-start',
}),
],
},
})),
};
const chunkBuilder = {
buildBundleChunks: vi.fn((messages) => [
{
id: 'chunk-team-marker',
kind: 'assistant',
messages,
},
]),
};
const source = new OpenCodeTaskLogStreamSource(
bridge as never,
{ resolve: async () => '/tmp/claude' },
{
getTasks: async () => [createTask()],
getDeletedTasks: async () => [],
} as never,
chunkBuilder as never,
{ readTaskRecords: vi.fn(async () => []) }
);
const response = await source.getTaskLogStream('team-a', 'task-a');
expect(response?.runtimeProjection).toEqual({
provider: 'opencode',
mode: 'heuristic',
attributionRecordCount: 0,
projectedMessageCount: 3,
fallbackReason: 'task_tool_markers',
markerMatchCount: 1,
markerSpanCount: 1,
});
expect(
chunkBuilder.buildBundleChunks.mock.calls[0]?.[0].map((message: { uuid: string }) => message.uuid)
).toEqual(['team-a-prompt', 'team-a-start', 'team-a-start::tool_results']);
});
it('keeps separate marker spans for repeated task work cycles without including unrelated gaps', async () => {
const bridge = {
getOpenCodeTranscript: vi.fn(async () => ({
sessionId: 'session-opencode',
logProjection: {
messages: [
textLogMessage({
uuid: 'cycle-1-prompt',
type: 'user',
role: 'user',
timestamp: '2026-04-21T10:01:00.000Z',
}),
taskMarkerLogMessage({
uuid: 'cycle-1-start',
parentUuid: 'cycle-1-prompt',
timestamp: '2026-04-21T10:02:00.000Z',
toolName: 'mcp__agent-teams__task_start',
input: { teamName: 'team-a', taskId: 'task-a' },
}),
toolResultLogMessage({
uuid: 'cycle-1-start::tool_results',
parentUuid: 'cycle-1-start',
timestamp: '2026-04-21T10:02:01.000Z',
sourceToolAssistantUUID: 'cycle-1-start',
}),
textLogMessage({
uuid: 'cycle-1-work',
timestamp: '2026-04-21T10:04:00.000Z',
}),
taskMarkerLogMessage({
uuid: 'cycle-1-complete',
parentUuid: 'cycle-1-work',
timestamp: '2026-04-21T10:08:00.000Z',
toolName: 'mcp__agent-teams__task_complete',
input: { teamName: 'team-a', taskId: 'task-a' },
}),
toolResultLogMessage({
uuid: 'cycle-1-complete::tool_results',
parentUuid: 'cycle-1-complete',
timestamp: '2026-04-21T10:08:01.000Z',
sourceToolAssistantUUID: 'cycle-1-complete',
}),
textLogMessage({
uuid: 'unrelated-between-cycles',
timestamp: '2026-04-21T12:00:00.000Z',
}),
textLogMessage({
uuid: 'cycle-2-prompt',
type: 'user',
role: 'user',
timestamp: '2026-04-21T14:01:00.000Z',
}),
taskMarkerLogMessage({
uuid: 'cycle-2-start',
parentUuid: 'cycle-2-prompt',
timestamp: '2026-04-21T14:02:00.000Z',
toolName: 'mcp__agent-teams__task_start',
input: { teamName: 'team-a', taskId: 'task-a' },
}),
toolResultLogMessage({
uuid: 'cycle-2-start::tool_results',
parentUuid: 'cycle-2-start',
timestamp: '2026-04-21T14:02:01.000Z',
sourceToolAssistantUUID: 'cycle-2-start',
}),
textLogMessage({
uuid: 'cycle-2-work-after-open-marker',
timestamp: '2026-04-21T14:04:00.000Z',
}),
],
},
})),
};
const chunkBuilder = {
buildBundleChunks: vi.fn((messages) => [
{
id: 'chunk-marker-cycles',
kind: 'assistant',
messages,
},
]),
};
const source = new OpenCodeTaskLogStreamSource(
bridge as never,
{ resolve: async () => '/tmp/claude' },
{
getTasks: async () => [
createTask({
updatedAt: '2026-04-21T14:10:00.000Z',
workIntervals: [
{
startedAt: '2026-04-21T10:00:00.000Z',
completedAt: '2026-04-21T10:10:00.000Z',
},
{
startedAt: '2026-04-21T14:00:00.000Z',
completedAt: '2026-04-21T14:10:00.000Z',
},
],
}),
],
getDeletedTasks: async () => [],
} as never,
chunkBuilder as never,
{ readTaskRecords: vi.fn(async () => []) }
);
const response = await source.getTaskLogStream('team-a', 'task-a');
expect(response?.runtimeProjection).toEqual({
provider: 'opencode',
mode: 'heuristic',
attributionRecordCount: 0,
projectedMessageCount: 10,
fallbackReason: 'task_tool_markers',
markerMatchCount: 3,
markerSpanCount: 2,
});
expect(
chunkBuilder.buildBundleChunks.mock.calls[0]?.[0].map((message: { uuid: string }) => message.uuid)
).toEqual([
'cycle-1-prompt',
'cycle-1-start',
'cycle-1-start::tool_results',
'cycle-1-work',
'cycle-1-complete',
'cycle-1-complete::tool_results',
'cycle-2-prompt',
'cycle-2-start',
'cycle-2-start::tool_results',
'cycle-2-work-after-open-marker',
]);
});
it('ignores stale task markers outside current task windows before falling back to time-window logs', async () => {
const bridge = {
getOpenCodeTranscript: vi.fn(async () => ({
sessionId: 'session-opencode',
logProjection: {
messages: [
taskMarkerLogMessage({
uuid: 'stale-start-marker',
timestamp: '2026-04-21T08:00:00.000Z',
toolName: 'mcp__agent-teams__task_start',
input: { teamName: 'team-a', taskId: 'task-a' },
}),
textLogMessage({
uuid: 'current-window-work',
timestamp: '2026-04-21T10:05:00.000Z',
}),
],
},
})),
};
const chunkBuilder = {
buildBundleChunks: vi.fn((messages) => [
{
id: 'chunk-stale-marker',
kind: 'assistant',
messages,
},
]),
};
const source = new OpenCodeTaskLogStreamSource(
bridge as never,
{ resolve: async () => '/tmp/claude' },
{
getTasks: async () => [createTask()],
getDeletedTasks: async () => [],
} as never,
chunkBuilder as never,
{ readTaskRecords: vi.fn(async () => []) }
);
const response = await source.getTaskLogStream('team-a', 'task-a');
expect(response?.runtimeProjection).toEqual({
provider: 'opencode',
mode: 'heuristic',
attributionRecordCount: 0,
projectedMessageCount: 1,
fallbackReason: 'no_attribution_records',
});
expect(
chunkBuilder.buildBundleChunks.mock.calls[0]?.[0].map((message: { uuid: string }) => message.uuid)
).toEqual(['current-window-work']);
});
it('matches hash-prefixed display task refs in OpenCode task tool markers', async () => {
const bridge = {
getOpenCodeTranscript: vi.fn(async () => ({
sessionId: 'session-opencode',
logProjection: {
messages: [
taskMarkerLogMessage({
uuid: 'display-ref-start',
timestamp: '2026-04-21T10:03:00.000Z',
toolName: 'mcp__agent-teams__task_start',
input: { teamName: 'team-a', taskId: '#short123' },
}),
toolResultLogMessage({
uuid: 'display-ref-start::tool_results',
parentUuid: 'display-ref-start',
timestamp: '2026-04-21T10:03:01.000Z',
sourceToolAssistantUUID: 'display-ref-start',
}),
],
},
})),
};
const chunkBuilder = {
buildBundleChunks: vi.fn((messages) => [
{
id: 'chunk-display-ref-marker',
kind: 'assistant',
messages,
},
]),
};
const source = new OpenCodeTaskLogStreamSource(
bridge as never,
{ resolve: async () => '/tmp/claude' },
{
getTasks: async () => [createTask({ id: 'task-canonical', displayId: 'short123' })],
getDeletedTasks: async () => [],
} as never,
chunkBuilder as never,
{ readTaskRecords: vi.fn(async () => []) }
);
const response = await source.getTaskLogStream('team-a', 'task-canonical');
expect(response?.runtimeProjection).toEqual({
provider: 'opencode',
mode: 'heuristic',
attributionRecordCount: 0,
projectedMessageCount: 2,
fallbackReason: 'task_tool_markers',
markerMatchCount: 1,
markerSpanCount: 1,
});
expect(
chunkBuilder.buildBundleChunks.mock.calls[0]?.[0].map((message: { uuid: string }) => message.uuid)
).toEqual(['display-ref-start', 'display-ref-start::tool_results']);
});
it('prefers explicit OpenCode attribution over owner/time-window heuristic', async () => {
const bridge = {
getOpenCodeTranscript: vi.fn(async (_binaryPath, params: { memberName: string }) => {
if (params.memberName !== 'bob') {
throw new Error(`unexpected member ${params.memberName}`);
}
return {
sessionId: 'session-bob',
logProjection: {
messages: [
{
uuid: 'bob-outside',
parentUuid: undefined,
type: 'assistant',
timestamp: '2026-04-21T11:50:00.000Z',
role: 'assistant',
content: [{ type: 'text', text: 'Before attributed window' }],
isMeta: false,
sessionId: 'session-bob',
toolCalls: [],
toolResults: [],
},
{
uuid: 'bob-inside',
parentUuid: undefined,
type: 'assistant',
timestamp: '2026-04-21T12:05:00.000Z',
role: 'assistant',
content: [{ type: 'text', text: 'Explicitly attributed OpenCode work' }],
isMeta: false,
sessionId: 'session-bob',
toolCalls: [],
toolResults: [],
},
],
},
};
}),
};
const chunkBuilder = {
buildBundleChunks: vi.fn((messages) => [
{
id: 'chunk-attributed',
kind: 'assistant',
messages,
},
]),
};
const taskReader = {
getTasks: vi.fn(async () => [createTask()]),
getDeletedTasks: vi.fn(async () => []),
};
const attributionRecords: OpenCodeTaskLogAttributionRecord[] = [
{
taskId: 'task-a',
memberName: 'bob',
scope: 'member_session_window',
sessionId: 'session-bob',
since: '2026-04-21T12:00:00.000Z',
until: '2026-04-21T12:10:00.000Z',
source: 'launch_runtime',
},
];
const source = new OpenCodeTaskLogStreamSource(
bridge as never,
{ resolve: async () => '/tmp/claude' },
taskReader as never,
chunkBuilder as never,
{ readTaskRecords: vi.fn(async () => attributionRecords) }
);
const response = await source.getTaskLogStream('team-a', 'task-a');
expect(response?.source).toBe('opencode_runtime_attribution');
expect(response?.runtimeProjection).toEqual({
provider: 'opencode',
mode: 'attribution',
attributionRecordCount: 1,
projectedMessageCount: 1,
});
expect(response?.participants).toEqual([
{
key: 'member:bob',
label: 'bob',
role: 'member',
isLead: false,
isSidechain: true,
},
]);
expect(response?.defaultFilter).toBe('member:bob');
expect(response?.segments[0]?.actor).toEqual({
memberName: 'bob',
role: 'member',
sessionId: 'session-bob',
isSidechain: true,
});
expect(
chunkBuilder.buildBundleChunks.mock.calls[0]?.[0].map((message: { uuid: string }) => message.uuid)
).toEqual(['bob-inside']);
expect(bridge.getOpenCodeTranscript).toHaveBeenCalledWith('/tmp/claude', {
teamId: 'team-a',
memberName: 'bob',
limit: 500,
});
});
it('falls back to owner heuristic when explicit attribution is stale', async () => {
const bridge = {
getOpenCodeTranscript: vi.fn(async (_binaryPath, params: { memberName: string }) => {
if (params.memberName === 'bob') {
return {
sessionId: 'stale-session',
logProjection: {
messages: [
{
uuid: 'stale-bob',
parentUuid: undefined,
type: 'assistant',
timestamp: '2026-04-21T10:05:00.000Z',
role: 'assistant',
content: [{ type: 'text', text: 'Wrong session' }],
isMeta: false,
sessionId: 'stale-session',
toolCalls: [],
toolResults: [],
},
],
},
};
}
return {
sessionId: 'session-alice',
logProjection: {
messages: [
{
uuid: 'alice-inside',
parentUuid: undefined,
type: 'assistant',
timestamp: '2026-04-21T10:05:00.000Z',
role: 'assistant',
content: [{ type: 'text', text: 'Heuristic owner work' }],
isMeta: false,
sessionId: 'session-alice',
toolCalls: [],
toolResults: [],
},
],
},
};
}),
};
const chunkBuilder = {
buildBundleChunks: vi.fn((messages) => [
{
id: 'chunk-heuristic',
kind: 'assistant',
messages,
},
]),
};
const attributionRecords: OpenCodeTaskLogAttributionRecord[] = [
{
taskId: 'task-a',
memberName: 'bob',
scope: 'member_session_window',
sessionId: 'session-bob',
since: '2026-04-21T10:00:00.000Z',
until: '2026-04-21T10:10:00.000Z',
},
];
const source = new OpenCodeTaskLogStreamSource(
bridge as never,
{ resolve: async () => '/tmp/claude' },
{
getTasks: async () => [createTask()],
getDeletedTasks: async () => [],
} as never,
chunkBuilder as never,
{ readTaskRecords: vi.fn(async () => attributionRecords) }
);
const response = await source.getTaskLogStream('team-a', 'task-a');
expect(response?.source).toBe('opencode_runtime_fallback');
expect(response?.runtimeProjection).toEqual({
provider: 'opencode',
mode: 'heuristic',
attributionRecordCount: 1,
projectedMessageCount: 1,
fallbackReason: 'attribution_no_projected_messages',
});
expect(response?.participants[0]?.label).toBe('alice');
expect(
chunkBuilder.buildBundleChunks.mock.calls.at(-1)?.[0].map((message: { uuid: string }) => message.uuid)
).toEqual(['alice-inside']);
expect(bridge.getOpenCodeTranscript).toHaveBeenNthCalledWith(1, '/tmp/claude', {
teamId: 'team-a',
memberName: 'bob',
limit: 500,
});
expect(bridge.getOpenCodeTranscript).toHaveBeenNthCalledWith(2, '/tmp/claude', {
teamId: 'team-a',
memberName: 'alice',
limit: 200,
});
});
it('keys the short cache by attribution records so new explicit attribution is visible immediately', async () => {
const attributionRecords: OpenCodeTaskLogAttributionRecord[] = [
{
taskId: 'task-a',
memberName: 'bob',
scope: 'member_session_window',
sessionId: 'session-bob',
since: '2026-04-21T12:00:00.000Z',
until: '2026-04-21T12:10:00.000Z',
},
];
const attributionStore = {
readTaskRecords: vi
.fn<() => Promise<OpenCodeTaskLogAttributionRecord[]>>()
.mockResolvedValueOnce([])
.mockResolvedValueOnce(attributionRecords),
};
const bridge = {
getOpenCodeTranscript: vi.fn(async (_binaryPath, params: { memberName: string }) => {
const isBob = params.memberName === 'bob';
return {
sessionId: isBob ? 'session-bob' : 'session-alice',
logProjection: {
messages: [
{
uuid: isBob ? 'bob-new-attribution' : 'alice-old-heuristic',
parentUuid: undefined,
type: 'assistant',
timestamp: isBob
? '2026-04-21T12:05:00.000Z'
: '2026-04-21T10:05:00.000Z',
role: 'assistant',
content: [{ type: 'text', text: isBob ? 'new attribution' : 'old heuristic' }],
isMeta: false,
sessionId: isBob ? 'session-bob' : 'session-alice',
toolCalls: [],
toolResults: [],
},
],
},
};
}),
};
const chunkBuilder = {
buildBundleChunks: vi.fn((messages) => [
{
id: 'chunk',
kind: 'assistant',
messages,
},
]),
};
const source = new OpenCodeTaskLogStreamSource(
bridge as never,
{ resolve: async () => '/tmp/claude' },
{
getTasks: async () => [createTask()],
getDeletedTasks: async () => [],
} as never,
chunkBuilder as never,
attributionStore
);
const first = await source.getTaskLogStream('team-a', 'task-a');
const second = await source.getTaskLogStream('team-a', 'task-a');
expect(first?.source).toBe('opencode_runtime_fallback');
expect(second?.source).toBe('opencode_runtime_attribution');
expect(second?.runtimeProjection).toMatchObject({
provider: 'opencode',
mode: 'attribution',
attributionRecordCount: 1,
projectedMessageCount: 1,
});
expect(bridge.getOpenCodeTranscript).toHaveBeenNthCalledWith(1, '/tmp/claude', {
teamId: 'team-a',
memberName: 'alice',
limit: 200,
});
expect(bridge.getOpenCodeTranscript).toHaveBeenNthCalledWith(2, '/tmp/claude', {
teamId: 'team-a',
memberName: 'bob',
limit: 500,
});
expect(
chunkBuilder.buildBundleChunks.mock.calls.at(-1)?.[0].map((message: { uuid: string }) => message.uuid)
).toEqual(['bob-new-attribution']);
});
});