// @vitest-environment node import { readFile } from 'fs/promises'; import path from 'path'; import { describe, expect, it, vi } from 'vitest'; import { OpenCodeTaskLogStreamSource } from '../../../../src/main/services/team/taskLogs/stream/OpenCodeTaskLogStreamSource'; import { BoardTaskExactLogChunkBuilder } from '../../../../src/main/services/team/taskLogs/exact/BoardTaskExactLogChunkBuilder'; import type { OpenCodeRuntimeTranscriptResponse } from '../../../../src/main/services/runtime/ClaudeMultimodelBridgeService'; import type { OpenCodeTaskLogAttributionRecord } from '../../../../src/main/services/team/taskLogs/stream/OpenCodeTaskLogAttributionStore'; import type { ParsedMessage } from '../../../../src/main/types'; import type { BoardTaskLogStreamResponse, TeamTask } from '../../../../src/shared/types'; const FIXTURE_PATH = path.resolve( process.cwd(), 'test/fixtures/team/opencode/relay-works-10-jack-projection-transcript.json' ); const RELAY_WORKS_10_TASK: TeamTask = { id: '0b3a0624-5d66-4067-848e-5a74a1720c0d', displayId: '0b3a0624', subject: 'Define calculator arithmetic behavior', owner: 'jack', status: 'completed', createdAt: '2026-04-24T20:29:03.133Z', updatedAt: '2026-04-24T20:29:34.157Z', workIntervals: [ { startedAt: '2026-04-24T20:29:03.133Z', completedAt: '2026-04-24T20:29:34.157Z', }, ], }; const RELAY_WORKS_10_COORDINATION_TASK: TeamTask = { id: 'b5534868-0901-4c9e-9296-2b6e2059a08f', displayId: 'b5534868', subject: 'Split calculator implementation work', owner: 'jack', status: 'in_progress', createdAt: '2026-04-24T20:28:58.000Z', updatedAt: '2026-04-24T20:31:21.876Z', workIntervals: [ { startedAt: '2026-04-24T20:28:58.000Z', }, ], }; async function loadFixtureTranscript(): Promise< NonNullable > { const raw = await readFile(FIXTURE_PATH, 'utf8'); const parsed = JSON.parse(raw) as OpenCodeRuntimeTranscriptResponse; if (parsed.providerId !== 'opencode' || !parsed.transcript) { throw new Error('Invalid OpenCode transcript fixture'); } return parsed.transcript; } function flattenRawMessages(response: BoardTaskLogStreamResponse): ParsedMessage[] { return response.segments.flatMap((segment) => segment.chunks.flatMap((chunk) => chunk.rawMessages) ); } function serializeContent(message: ParsedMessage): string { return typeof message.content === 'string' ? message.content : JSON.stringify(message.content); } function serializeProjectedContent( transcript: NonNullable ): string { return JSON.stringify(transcript.logProjection?.messages ?? []); } function markerTaskIds(messages: ParsedMessage[], markerNames: Set): Set { const taskIds = new Set(); for (const message of messages) { for (const toolCall of message.toolCalls) { if (!markerNames.has(toolCall.name)) { continue; } const input = toolCall.input; if (input && typeof input === 'object' && !Array.isArray(input)) { const taskId = (input as Record).taskId; if (typeof taskId === 'string') { taskIds.add(taskId); } } } } return taskIds; } function createSource(params: { transcript: NonNullable; activeTasks?: TeamTask[]; deletedTasks?: TeamTask[]; attributionRecords?: OpenCodeTaskLogAttributionRecord[]; }) { const bridge = { getOpenCodeTranscript: vi.fn(async () => params.transcript), }; const taskReader = { getTasks: vi.fn(async () => params.activeTasks ?? [RELAY_WORKS_10_TASK]), getDeletedTasks: vi.fn(async () => params.deletedTasks ?? []), }; const attributionStore = { readTaskRecords: vi.fn(async () => params.attributionRecords ?? []), }; const source = new OpenCodeTaskLogStreamSource( bridge as never, { resolve: async () => '/tmp/agent_teams_orchestrator' }, taskReader as never, new BoardTaskExactLogChunkBuilder(), attributionStore ); return { source, bridge, taskReader, attributionStore }; } describe('OpenCodeTaskLogStreamSource real OpenCode fixture e2e', () => { it('builds a task log stream from real OpenCode MCP task markers without leaking unrelated tasks', async () => { const transcript = await loadFixtureTranscript(); const { source, bridge } = createSource({ transcript }); const response = await source.getTaskLogStream('relay-works-10', RELAY_WORKS_10_TASK.id); expect(response).not.toBeNull(); expect(response?.source).toBe('opencode_runtime_fallback'); expect(response?.runtimeProjection).toMatchObject({ provider: 'opencode', mode: 'heuristic', attributionRecordCount: 0, fallbackReason: 'task_tool_markers', }); expect(response?.runtimeProjection?.projectedMessageCount).toBeGreaterThanOrEqual(10); expect(response?.runtimeProjection?.markerMatchCount).toBeGreaterThanOrEqual(4); expect(response?.runtimeProjection?.markerSpanCount).toBe(1); expect(response?.participants).toEqual([ { key: 'member:jack', label: 'jack', role: 'member', isLead: false, isSidechain: true, }, ]); expect(response?.segments).toHaveLength(1); const rawMessages = flattenRawMessages(response as BoardTaskLogStreamResponse); const toolNames = rawMessages.flatMap((message) => message.toolCalls.map((toolCall) => toolCall.name) ); const serialized = rawMessages.map(serializeContent).join('\n'); expect(toolNames).toEqual( expect.arrayContaining([ 'agent-teams_task_start', 'agent-teams_task_add_comment', 'agent-teams_task_complete', ]) ); expect(toolNames).not.toContain('SendMessage'); expect(serialized).toContain('Calculator behavior: digits 0-9 append to display'); expect(serialized).toContain('Noted'); expect(serialized).toContain('Confirmed'); expect(serialized).not.toContain('Keyboard handlers added'); expect(serialized).not.toContain('Logic smoke check'); expect(serialized).not.toContain('#00000000'); expect( markerTaskIds( rawMessages, new Set([ 'agent-teams_task_start', 'agent-teams_task_add_comment', 'agent-teams_task_complete', ]) ) ).toEqual(new Set([RELAY_WORKS_10_TASK.id])); expect(bridge.getOpenCodeTranscript).toHaveBeenCalledWith('/tmp/agent_teams_orchestrator', { teamId: 'relay-works-10', memberName: 'jack', limit: 200, }); }); it('includes real native OpenCode read/bash tools from a task-scoped runtime projection', async () => { const transcript = await loadFixtureTranscript(); const { source } = createSource({ transcript, activeTasks: [RELAY_WORKS_10_COORDINATION_TASK], }); const response = await source.getTaskLogStream( 'relay-works-10', RELAY_WORKS_10_COORDINATION_TASK.id ); expect(response).not.toBeNull(); expect(response?.source).toBe('opencode_runtime_fallback'); expect(response?.runtimeProjection).toMatchObject({ provider: 'opencode', mode: 'heuristic', fallbackReason: 'task_tool_markers', }); expect(response?.runtimeProjection?.boardMcpToolCount).toBeGreaterThan(0); expect(response?.runtimeProjection?.nativeToolCount).toBeGreaterThanOrEqual(2); const rawMessages = flattenRawMessages(response as BoardTaskLogStreamResponse); const toolNames = rawMessages.flatMap((message) => message.toolCalls.map((toolCall) => toolCall.name) ); const serialized = rawMessages.map(serializeContent).join('\n'); expect(toolNames).toEqual(expect.arrayContaining(['read', 'bash'])); expect(toolNames).toEqual( expect.arrayContaining(['agent-teams_task_start', 'agent-teams_task_add_comment']) ); expect(serialized).toContain('package.json'); expect(serialized).toContain('Разбил работу на мелкие задачи'); expect(toolNames).not.toContain('SendMessage'); }); it('uses real attribution UUID bounds before heuristic fallback', async () => { const transcript = await loadFixtureTranscript(); const { source, bridge, attributionStore } = createSource({ transcript, attributionRecords: [ { taskId: RELAY_WORKS_10_TASK.id, memberName: 'jack', scope: 'member_session_window', sessionId: 'ses_23edf9243ffeSNYPWObDloBJyQ', startMessageUuid: 'msg_dc12eb246001iUrCHiLxsvZ3mN', endMessageUuid: 'msg_dc12ed5ec001OIh5Bh9emN2Utj', source: 'launch_runtime', }, ], }); const response = await source.getTaskLogStream('relay-works-10', RELAY_WORKS_10_TASK.id); expect(response?.source).toBe('opencode_runtime_attribution'); expect(response?.runtimeProjection).toEqual({ provider: 'opencode', mode: 'attribution', attributionRecordCount: 1, projectedMessageCount: 10, boardMcpToolCount: 4, nativeToolCount: 0, }); expect(response?.defaultFilter).toBe('member:jack'); expect(response?.segments).toHaveLength(1); const rawMessages = flattenRawMessages(response as BoardTaskLogStreamResponse); const toolNames = rawMessages.flatMap((message) => message.toolCalls.map((toolCall) => toolCall.name) ); const serialized = rawMessages.map(serializeContent).join('\n'); expect(rawMessages.map((message) => message.uuid)).toEqual([ 'msg_dc12eb246001iUrCHiLxsvZ3mN', 'msg_dc12eb261001b8MzfjP5WZGwA1', 'msg_dc12eb261001b8MzfjP5WZGwA1::tool_results', 'msg_dc12ebe27001UFPOASv4SiAr51', 'msg_dc12ebe27001UFPOASv4SiAr51::tool_results', 'msg_dc12ec768001m7G1qMVTexxl2s', 'msg_dc12ec768001m7G1qMVTexxl2s::tool_results', 'msg_dc12ece54001bDAaT7Rt1m6OmN', 'msg_dc12ece54001bDAaT7Rt1m6OmN::tool_results', 'msg_dc12ed5ec001OIh5Bh9emN2Utj', ]); expect(toolNames).toEqual( expect.arrayContaining([ 'agent-teams_task_start', 'agent-teams_task_add_comment', 'agent-teams_task_complete', 'agent-teams_message_send', ]) ); expect(serialized).toContain('Calculator behavior: digits 0-9 append to display'); expect(serialized).toContain('Задача #0b3a0624 завершена'); expect(serialized).not.toContain('Noted'); expect(serialized).not.toContain('Keyboard handlers added'); expect(attributionStore.readTaskRecords).toHaveBeenCalledWith( 'relay-works-10', RELAY_WORKS_10_TASK.id ); expect(bridge.getOpenCodeTranscript).toHaveBeenCalledWith('/tmp/agent_teams_orchestrator', { teamId: 'relay-works-10', memberName: 'jack', limit: 500, }); }); it('can recover a deleted task stream from real OpenCode markers', async () => { const transcript = await loadFixtureTranscript(); const deletedTask = { ...RELAY_WORKS_10_TASK, status: 'deleted', } satisfies TeamTask; const { source } = createSource({ transcript, activeTasks: [], deletedTasks: [deletedTask], }); const response = await source.getTaskLogStream('relay-works-10', deletedTask.id); expect(response?.source).toBe('opencode_runtime_fallback'); expect(response?.participants[0]?.label).toBe('jack'); expect(response?.runtimeProjection?.fallbackReason).toBe('task_tool_markers'); expect(flattenRawMessages(response as BoardTaskLogStreamResponse).length).toBeGreaterThan(0); }); it('does not leak a real OpenCode task stream across explicit team boundaries', async () => { const transcript = await loadFixtureTranscript(); const { source, bridge } = createSource({ transcript }); const response = await source.getTaskLogStream('other-team', RELAY_WORKS_10_TASK.id); expect(response).toBeNull(); expect(bridge.getOpenCodeTranscript).toHaveBeenCalledWith('/tmp/agent_teams_orchestrator', { teamId: 'other-team', memberName: 'jack', limit: 200, }); }); it('falls back to real marker projection when stale attribution does not match the session', async () => { const transcript = await loadFixtureTranscript(); const { source, bridge } = createSource({ transcript, attributionRecords: [ { taskId: RELAY_WORKS_10_TASK.id, memberName: 'jack', scope: 'task_session', sessionId: 'stale-session-id', startMessageUuid: 'msg_dc12eb246001iUrCHiLxsvZ3mN', endMessageUuid: 'msg_dc12ed5ec001OIh5Bh9emN2Utj', source: 'reconcile', }, ], }); const response = await source.getTaskLogStream('relay-works-10', RELAY_WORKS_10_TASK.id); expect(response?.source).toBe('opencode_runtime_fallback'); expect(response?.runtimeProjection).toMatchObject({ provider: 'opencode', mode: 'heuristic', attributionRecordCount: 1, fallbackReason: 'task_tool_markers', }); expect(bridge.getOpenCodeTranscript).toHaveBeenNthCalledWith( 1, '/tmp/agent_teams_orchestrator', { teamId: 'relay-works-10', memberName: 'jack', limit: 500, } ); expect(bridge.getOpenCodeTranscript).toHaveBeenNthCalledWith( 2, '/tmp/agent_teams_orchestrator', { teamId: 'relay-works-10', memberName: 'jack', limit: 200, } ); }); it('captures the OpenCode runtime identity and MCP messaging contract from the real fixture', async () => { const transcript = await loadFixtureTranscript(); const serialized = serializeProjectedContent(transcript); const toolNames = (transcript.logProjection?.messages ?? []).flatMap((message) => message.toolCalls.map((toolCall) => toolCall.name) ); expect(serialized).toContain(''); expect(serialized).toContain('runtimeProvider'); expect(serialized).toContain('opencode'); expect(serialized).toContain('agent-teams_runtime_bootstrap_checkin'); expect(serialized).toContain('agent-teams_member_briefing'); expect(serialized).toContain('agent-teams_message_send'); expect(serialized).toContain('Do not use SendMessage'); expect(serialized).toContain('Do not use runtime_deliver_message for ordinary visible replies'); expect(toolNames).toEqual( expect.arrayContaining([ 'agent-teams_runtime_bootstrap_checkin', 'agent-teams_member_briefing', 'agent-teams_message_send', ]) ); expect(toolNames).not.toContain('SendMessage'); expect(toolNames).not.toContain('runtime_deliver_message'); }); it('keeps real OpenCode projected tool results bounded and linked to assistant tool calls', async () => { const transcript = await loadFixtureTranscript(); const projectedMessages = transcript.logProjection?.messages ?? []; const assistantToolIds = new Set( projectedMessages.flatMap((message) => message.toolCalls.map((toolCall) => toolCall.id)) ); const toolResultMessages = projectedMessages.filter( (message) => message.toolResults.length > 0 ); expect(projectedMessages).toHaveLength(101); expect(toolResultMessages.length).toBeGreaterThan(20); for (const message of toolResultMessages) { expect(message.isMeta).toBe(true); expect(message.sourceToolAssistantUUID).toBeTruthy(); for (const toolResult of message.toolResults) { expect(assistantToolIds.has(toolResult.toolUseId)).toBe(true); const serializedContent = typeof toolResult.content === 'string' ? toolResult.content : JSON.stringify(toolResult.content); expect(serializedContent.length).toBeLessThanOrEqual(8_200); } } }); });