fix(opencode): harden runtime projection delivery
This commit is contained in:
parent
dec0eaba18
commit
2f37be4bd0
13 changed files with 3631 additions and 37 deletions
|
|
@ -15,8 +15,13 @@ export const agentBlocks = controllerModule.agentBlocks;
|
|||
|
||||
export function getController(teamName: string, claudeDir?: string) {
|
||||
const forcedClaudeDir = process.env[FORCED_CLAUDE_DIR_ENV]?.trim();
|
||||
let resolvedClaudeDir = claudeDir;
|
||||
if (forcedClaudeDir) {
|
||||
resolvedClaudeDir = forcedClaudeDir;
|
||||
}
|
||||
|
||||
return createController({
|
||||
teamName,
|
||||
...(forcedClaudeDir ? { claudeDir: forcedClaudeDir } : claudeDir ? { claudeDir } : {}),
|
||||
...(resolvedClaudeDir ? { claudeDir: resolvedClaudeDir } : {}),
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,9 @@ import {
|
|||
createLegacyRuntimeFallbackCliExtensionCapabilities,
|
||||
} from '@shared/utils/providerExtensionCapabilities';
|
||||
import { filterVisibleProviderRuntimeModels } from '@shared/utils/providerModelVisibility';
|
||||
import { mkdtemp, readFile, rm } from 'fs/promises';
|
||||
import { tmpdir } from 'os';
|
||||
import path from 'path';
|
||||
|
||||
import { resolveGeminiRuntimeAuth } from './geminiRuntimeAuth';
|
||||
import { buildProviderAwareCliEnv } from './providerAwareCliEnv';
|
||||
|
|
@ -843,17 +846,26 @@ export class ClaudeMultimodelBridgeService {
|
|||
params.teamId,
|
||||
'--member',
|
||||
params.memberName,
|
||||
'--projection-only',
|
||||
];
|
||||
if (typeof params.limit === 'number') {
|
||||
args.push('--limit', String(params.limit));
|
||||
}
|
||||
|
||||
const { stdout } = await execCli(binaryPath, args, {
|
||||
timeout: PROVIDER_STATUS_TIMEOUT_MS,
|
||||
env,
|
||||
});
|
||||
const parsed = extractJsonObject<OpenCodeRuntimeTranscriptResponse>(stdout);
|
||||
return parsed.providerId === 'opencode' ? (parsed.transcript ?? null) : null;
|
||||
const outputDir = await mkdtemp(path.join(tmpdir(), 'opencode-transcript-'));
|
||||
const outputPath = path.join(outputDir, 'transcript.json');
|
||||
try {
|
||||
await execCli(binaryPath, [...args, '--output', outputPath], {
|
||||
timeout: PROVIDER_STATUS_TIMEOUT_MS,
|
||||
env,
|
||||
});
|
||||
const parsed = extractJsonObject<OpenCodeRuntimeTranscriptResponse>(
|
||||
await readFile(outputPath, 'utf8')
|
||||
);
|
||||
return parsed.providerId === 'opencode' ? (parsed.transcript ?? null) : null;
|
||||
} finally {
|
||||
await rm(outputDir, { recursive: true, force: true }).catch(() => undefined);
|
||||
}
|
||||
}
|
||||
|
||||
private async verifyOpenCodeModel(
|
||||
|
|
|
|||
|
|
@ -290,6 +290,7 @@ import type {
|
|||
PersistedTeamLaunchSnapshot,
|
||||
PersistedTeamLaunchSummary,
|
||||
ProviderModelLaunchIdentity,
|
||||
TaskRef,
|
||||
TeamAgentRuntimeBackendType,
|
||||
TeamAgentRuntimeDiagnosticSeverity,
|
||||
TeamAgentRuntimeEntry,
|
||||
|
|
@ -314,7 +315,6 @@ import type {
|
|||
TeamProvisioningState,
|
||||
TeamRuntimeState,
|
||||
TeamTask,
|
||||
TaskRef,
|
||||
ToolActivityEventPayload,
|
||||
ToolApprovalAutoResolved,
|
||||
ToolApprovalEvent,
|
||||
|
|
@ -2035,6 +2035,12 @@ function normalizeTeamMemberProviderId(providerId: unknown): TeamProviderId | un
|
|||
return normalizeOptionalTeamProviderId(providerId);
|
||||
}
|
||||
|
||||
function normalizeTeamProviderLike(providerId: unknown): TeamProviderId | undefined {
|
||||
return normalizeOptionalTeamProviderId(
|
||||
typeof providerId === 'string' ? providerId.trim().toLowerCase() : providerId
|
||||
);
|
||||
}
|
||||
|
||||
function buildEffectiveTeamMemberSpec(
|
||||
member: TeamMemberInput,
|
||||
defaults: {
|
||||
|
|
@ -4508,15 +4514,11 @@ export class TeamProvisioningService {
|
|||
);
|
||||
const configProvider = (configMember as { provider?: unknown } | undefined)?.provider;
|
||||
const metaProvider = (metaMember as { provider?: unknown } | undefined)?.provider;
|
||||
const normalizeProviderLike = (value: unknown) =>
|
||||
normalizeOptionalTeamProviderId(
|
||||
typeof value === 'string' ? value.trim().toLowerCase() : value
|
||||
);
|
||||
const providerId =
|
||||
normalizeProviderLike(metaMember?.providerId) ??
|
||||
normalizeProviderLike(metaProvider) ??
|
||||
normalizeProviderLike(configMember?.providerId) ??
|
||||
normalizeProviderLike(configProvider) ??
|
||||
normalizeTeamProviderLike(metaMember?.providerId) ??
|
||||
normalizeTeamProviderLike(metaProvider) ??
|
||||
normalizeTeamProviderLike(configMember?.providerId) ??
|
||||
normalizeTeamProviderLike(configProvider) ??
|
||||
inferTeamProviderIdFromModel(metaMember?.model ?? configMember?.model);
|
||||
return providerId === 'opencode';
|
||||
}
|
||||
|
|
@ -4551,15 +4553,11 @@ export class TeamProvisioningService {
|
|||
);
|
||||
const configProvider = (configMember as { provider?: unknown } | undefined)?.provider;
|
||||
const metaProvider = (metaMember as { provider?: unknown } | undefined)?.provider;
|
||||
const normalizeProviderLike = (value: unknown) =>
|
||||
normalizeOptionalTeamProviderId(
|
||||
typeof value === 'string' ? value.trim().toLowerCase() : value
|
||||
);
|
||||
const providerId =
|
||||
normalizeProviderLike(metaMember?.providerId) ??
|
||||
normalizeProviderLike(metaProvider) ??
|
||||
normalizeProviderLike(configMember?.providerId) ??
|
||||
normalizeProviderLike(configProvider) ??
|
||||
normalizeTeamProviderLike(metaMember?.providerId) ??
|
||||
normalizeTeamProviderLike(metaProvider) ??
|
||||
normalizeTeamProviderLike(configMember?.providerId) ??
|
||||
normalizeTeamProviderLike(configProvider) ??
|
||||
inferTeamProviderIdFromModel(metaMember?.model ?? configMember?.model);
|
||||
if (providerId !== 'opencode') {
|
||||
return { delivered: false, reason: 'recipient_is_not_opencode' };
|
||||
|
|
@ -4605,17 +4603,25 @@ export class TeamProvisioningService {
|
|||
const trackedRunId = this.resolveDeliverableTrackedRuntimeRunId(teamName);
|
||||
const trackedRun = trackedRunId ? this.runs.get(trackedRunId) : null;
|
||||
let liveSecondaryLaneRunId: string | null = null;
|
||||
let trackedSecondaryLanePresent = false;
|
||||
let trackedSecondaryLaneSnapshotKnown = false;
|
||||
if (
|
||||
trackedRun &&
|
||||
laneIdentity.laneKind === 'secondary' &&
|
||||
laneIdentity.laneOwnerProviderId === 'opencode'
|
||||
) {
|
||||
const liveLane = trackedRun.mixedSecondaryLanes.find(
|
||||
const secondaryLanes = trackedRun.mixedSecondaryLanes;
|
||||
trackedSecondaryLaneSnapshotKnown = secondaryLanes.length > 0;
|
||||
const liveLane = secondaryLanes.find(
|
||||
(lane) =>
|
||||
lane.laneId === laneIdentity.laneId ||
|
||||
lane.member.name.trim().toLowerCase() === normalizedMemberName.toLowerCase()
|
||||
);
|
||||
liveSecondaryLaneRunId = liveLane?.runId?.trim() || null;
|
||||
trackedSecondaryLanePresent = liveLane != null;
|
||||
liveSecondaryLaneRunId = liveLane ? trackedRunId : null;
|
||||
if (!liveLane && trackedSecondaryLaneSnapshotKnown) {
|
||||
return { delivered: false, reason: 'opencode_runtime_not_active' };
|
||||
}
|
||||
}
|
||||
const runtimeRunId =
|
||||
laneIdentity.laneKind === 'secondary' && laneIdentity.laneOwnerProviderId === 'opencode'
|
||||
|
|
@ -4623,7 +4629,20 @@ export class TeamProvisioningService {
|
|||
(await this.resolveCurrentOpenCodeRuntimeRunId(teamName, laneIdentity.laneId)))
|
||||
: (trackedRunId ??
|
||||
(await this.resolveCurrentOpenCodeRuntimeRunId(teamName, laneIdentity.laneId)));
|
||||
if (!runtimeRunId) {
|
||||
let runtimeActive = Boolean(runtimeRunId);
|
||||
if (!runtimeActive) {
|
||||
if (
|
||||
trackedRun &&
|
||||
laneIdentity.laneKind === 'secondary' &&
|
||||
laneIdentity.laneOwnerProviderId === 'opencode' &&
|
||||
!trackedSecondaryLanePresent &&
|
||||
trackedSecondaryLaneSnapshotKnown
|
||||
) {
|
||||
return { delivered: false, reason: 'opencode_runtime_not_active' };
|
||||
}
|
||||
runtimeActive = await this.isOpenCodeRuntimeLaneIndexActive(teamName, laneIdentity.laneId);
|
||||
}
|
||||
if (!runtimeActive) {
|
||||
return { delivered: false, reason: 'opencode_runtime_not_active' };
|
||||
}
|
||||
|
||||
|
|
@ -4840,6 +4859,16 @@ export class TeamProvisioningService {
|
|||
return durableRunId || null;
|
||||
}
|
||||
|
||||
private async isOpenCodeRuntimeLaneIndexActive(
|
||||
teamName: string,
|
||||
laneId: string
|
||||
): Promise<boolean> {
|
||||
const laneIndex = await readOpenCodeRuntimeLaneIndex(getTeamsBasePath(), teamName).catch(
|
||||
() => null
|
||||
);
|
||||
return laneIndex?.lanes[laneId]?.state === 'active';
|
||||
}
|
||||
|
||||
private async resolveOpenCodeRuntimeLaneId(params: {
|
||||
teamName: string;
|
||||
runId: string;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
import { randomUUID } from 'crypto';
|
||||
|
||||
import type { AgentActionMode, TaskRef } from '@shared/types/team';
|
||||
|
||||
import type {
|
||||
OpenCodeBridgeRuntimeSnapshot,
|
||||
OpenCodeLaunchTeamCommandBody,
|
||||
|
|
@ -26,6 +24,7 @@ import type {
|
|||
TeamRuntimeStopInput,
|
||||
TeamRuntimeStopResult,
|
||||
} from './TeamRuntimeAdapter';
|
||||
import type { AgentActionMode, TaskRef } from '@shared/types/team';
|
||||
|
||||
export interface OpenCodeTeamRuntimeBridgePort {
|
||||
checkOpenCodeTeamLaunchReadiness(input: {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { extractToolCalls, extractToolResults } from '@main/utils/toolExtraction
|
|||
import { isLeadMember as isLeadMemberCheck } from '@shared/utils/leadDetection';
|
||||
import { getTaskDisplayId } from '@shared/utils/taskIdentity';
|
||||
|
||||
import { canonicalizeAgentTeamsToolName } from '../../agentTeamsToolNames';
|
||||
import { TeamTaskReader } from '../../TeamTaskReader';
|
||||
import { BoardTaskActivityRecordSource } from '../activity/BoardTaskActivityRecordSource';
|
||||
import { TeamTranscriptSourceLocator } from '../discovery/TeamTranscriptSourceLocator';
|
||||
|
|
@ -11,7 +12,6 @@ import { BoardTaskExactLogStrictParser } from '../exact/BoardTaskExactLogStrictP
|
|||
import { BoardTaskExactLogSummarySelector } from '../exact/BoardTaskExactLogSummarySelector';
|
||||
import { isBoardTaskExactLogsReadEnabled } from '../exact/featureGates';
|
||||
import { getBoardTaskExactLogFileVersions } from '../exact/fileVersions';
|
||||
import { canonicalizeAgentTeamsToolName } from '../../agentTeamsToolNames';
|
||||
|
||||
import { OpenCodeTaskLogStreamSource } from './OpenCodeTaskLogStreamSource';
|
||||
|
||||
|
|
|
|||
|
|
@ -324,6 +324,55 @@ function collectTaskMarkerCalls(
|
|||
});
|
||||
}
|
||||
|
||||
function markerInputReferencesTaskInDifferentExplicitTeam(
|
||||
input: unknown,
|
||||
teamName: string,
|
||||
taskRefs: Set<string>
|
||||
): boolean {
|
||||
if (taskRefs.size === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const normalizedTeamName = normalizeTaskRef(teamName);
|
||||
const explicitTeamRefs = collectExplicitRefsForKeys(input, TEAM_REFERENCE_KEYS);
|
||||
if (
|
||||
!normalizedTeamName ||
|
||||
explicitTeamRefs.size === 0 ||
|
||||
explicitTeamRefs.has(normalizedTeamName)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const explicitTaskRefs = collectExplicitRefsForKeys(input, TASK_REFERENCE_KEYS);
|
||||
return explicitTaskRefs.size > 0
|
||||
? refsIntersect(explicitTaskRefs, taskRefs)
|
||||
: valueReferencesTask(input, taskRefs);
|
||||
}
|
||||
|
||||
function hasForeignTeamTaskMarker(
|
||||
projectedMessages: OpenCodeRuntimeTranscriptLogMessage[],
|
||||
teamName: string,
|
||||
task: TeamTask
|
||||
): boolean {
|
||||
const taskRefs = buildTaskRefSet(task);
|
||||
if (taskRefs.size === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return projectedMessages
|
||||
.map(toParsedMessage)
|
||||
.filter((message): message is ParsedMessage => message !== null)
|
||||
.some((message) =>
|
||||
message.toolCalls.some((toolCall) => {
|
||||
const toolName = canonicalizeAgentTeamsToolName(toolCall.name ?? '').toLowerCase();
|
||||
return (
|
||||
TASK_LOG_MARKER_TOOL_NAMES.has(toolName) &&
|
||||
markerInputReferencesTaskInDifferentExplicitTeam(toolCall.input, teamName, taskRefs)
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function isTerminalTaskMarkerCall(markerCall: TaskMarkerCall): boolean {
|
||||
if (TERMINAL_TASK_MARKER_TOOL_NAMES.has(markerCall.toolName)) {
|
||||
return true;
|
||||
|
|
@ -923,6 +972,9 @@ export class OpenCodeTaskLogStreamSource {
|
|||
}
|
||||
|
||||
const markerProjection = buildTaskMarkerProjection(projectedMessages, teamName, task);
|
||||
if (!markerProjection && hasForeignTeamTaskMarker(projectedMessages, teamName, task)) {
|
||||
return null;
|
||||
}
|
||||
const timeWindows = markerProjection ? [] : buildTaskTimeWindows(task);
|
||||
const projectionReason: HeuristicFallbackReason = markerProjection
|
||||
? 'task_tool_markers'
|
||||
|
|
|
|||
|
|
@ -19,8 +19,8 @@ import {
|
|||
hasMemberLaunchDiagnosticsError,
|
||||
} from '@renderer/utils/memberLaunchDiagnostics';
|
||||
import { getRuntimeMemorySourceLabel } from '@renderer/utils/memberRuntimeSummary';
|
||||
import { deriveTaskDisplayId } from '@shared/utils/taskIdentity';
|
||||
import { isLeadMember } from '@shared/utils/leadDetection';
|
||||
import { deriveTaskDisplayId } from '@shared/utils/taskIdentity';
|
||||
import {
|
||||
AlertTriangle,
|
||||
Ban,
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
* Backward compatibility:
|
||||
* - legacy fenced blocks: ```info_for_agent ... ```
|
||||
* - legacy xml-like blocks: <agent-block> ... </agent-block>
|
||||
* - OpenCode runtime-only delivery blocks
|
||||
*/
|
||||
export const AGENT_BLOCK_TAG = 'info_for_agent';
|
||||
export const AGENT_BLOCK_OPEN = `<${AGENT_BLOCK_TAG}>`;
|
||||
|
|
@ -22,7 +23,11 @@ export const AGENT_BLOCK_CLOSE = `</${AGENT_BLOCK_TAG}>`;
|
|||
const CURRENT_AGENT_BLOCK_PATTERN = '\\n?<info_for_agent>\\n?[\\s\\S]*?\\n?<\\/info_for_agent>\\n?';
|
||||
const LEGACY_FENCED_AGENT_BLOCK_PATTERN = '\\n?```' + AGENT_BLOCK_TAG + '\\n[\\s\\S]*?\\n```\\n?';
|
||||
const LEGACY_XML_AGENT_BLOCK_PATTERN = '\\n?<agent-block>\\n?[\\s\\S]*?\\n?<\\/agent-block>\\n?';
|
||||
const AGENT_BLOCK_PATTERN = `(?:${CURRENT_AGENT_BLOCK_PATTERN}|${LEGACY_FENCED_AGENT_BLOCK_PATTERN}|${LEGACY_XML_AGENT_BLOCK_PATTERN})`;
|
||||
const OPENCODE_RUNTIME_IDENTITY_BLOCK_PATTERN =
|
||||
'\\n?<opencode_runtime_identity>\\n?[\\s\\S]*?\\n?<\\/opencode_runtime_identity>\\n?';
|
||||
const OPENCODE_APP_MESSAGE_DELIVERY_BLOCK_PATTERN =
|
||||
'\\n?<opencode_app_message_delivery>\\n?[\\s\\S]*?\\n?<\\/opencode_app_message_delivery>\\n?';
|
||||
const AGENT_BLOCK_PATTERN = `(?:${CURRENT_AGENT_BLOCK_PATTERN}|${LEGACY_FENCED_AGENT_BLOCK_PATTERN}|${LEGACY_XML_AGENT_BLOCK_PATTERN}|${OPENCODE_RUNTIME_IDENTITY_BLOCK_PATTERN}|${OPENCODE_APP_MESSAGE_DELIVERY_BLOCK_PATTERN})`;
|
||||
|
||||
/**
|
||||
* Creates a new RegExp for matching agent blocks.
|
||||
|
|
@ -63,6 +68,18 @@ export function unwrapAgentBlock(block: string): string {
|
|||
return trimmed.slice(legacyXmlOpen.length, -legacyXmlClose.length).trim();
|
||||
}
|
||||
|
||||
const opencodeRuntimeOpen = '<opencode_runtime_identity>';
|
||||
const opencodeRuntimeClose = '</opencode_runtime_identity>';
|
||||
if (trimmed.startsWith(opencodeRuntimeOpen) && trimmed.endsWith(opencodeRuntimeClose)) {
|
||||
return trimmed.slice(opencodeRuntimeOpen.length, -opencodeRuntimeClose.length).trim();
|
||||
}
|
||||
|
||||
const opencodeDeliveryOpen = '<opencode_app_message_delivery>';
|
||||
const opencodeDeliveryClose = '</opencode_app_message_delivery>';
|
||||
if (trimmed.startsWith(opencodeDeliveryOpen) && trimmed.endsWith(opencodeDeliveryClose)) {
|
||||
return trimmed.slice(opencodeDeliveryOpen.length, -opencodeDeliveryClose.length).trim();
|
||||
}
|
||||
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
|
|
|
|||
2873
test/fixtures/team/opencode/relay-works-10-jack-projection-transcript.json
vendored
Normal file
2873
test/fixtures/team/opencode/relay-works-10-jack-projection-transcript.json
vendored
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -1,5 +1,6 @@
|
|||
// @vitest-environment node
|
||||
import type { PathLike } from 'fs';
|
||||
import { readFile as readFileFixture, writeFile } from 'fs/promises';
|
||||
import * as path from 'path';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import {
|
||||
|
|
@ -810,15 +811,20 @@ describe('ClaudeMultimodelBridgeService', () => {
|
|||
});
|
||||
|
||||
it('loads projected OpenCode transcript data through the runtime transcript command', async () => {
|
||||
execCliMock.mockImplementation((_binaryPath, args) => {
|
||||
execCliMock.mockImplementation(async (_binaryPath, args) => {
|
||||
const normalizedArgs = Array.isArray(args) ? args.join(' ') : '';
|
||||
|
||||
if (
|
||||
normalizedArgs
|
||||
=== 'runtime transcript --json --provider opencode --team team-a --member alice --limit 20'
|
||||
normalizedArgs.startsWith(
|
||||
'runtime transcript --json --provider opencode --team team-a --member alice --projection-only --limit 20 --output '
|
||||
)
|
||||
) {
|
||||
return Promise.resolve({
|
||||
stdout: JSON.stringify({
|
||||
const outputIndex = Array.isArray(args) ? args.indexOf('--output') : -1;
|
||||
const outputPath =
|
||||
outputIndex >= 0 && Array.isArray(args) ? String(args[outputIndex + 1] ?? '') : '';
|
||||
await writeFile(
|
||||
outputPath,
|
||||
JSON.stringify({
|
||||
schemaVersion: 1,
|
||||
providerId: 'opencode',
|
||||
transcript: {
|
||||
|
|
@ -856,6 +862,10 @@ describe('ClaudeMultimodelBridgeService', () => {
|
|||
},
|
||||
},
|
||||
}),
|
||||
'utf8'
|
||||
);
|
||||
return Promise.resolve({
|
||||
stdout: '',
|
||||
stderr: '',
|
||||
exitCode: 0,
|
||||
});
|
||||
|
|
@ -896,6 +906,71 @@ describe('ClaudeMultimodelBridgeService', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('loads a large real OpenCode projection fixture through output-file transcript delivery', async () => {
|
||||
const fixturePath = path.resolve(
|
||||
process.cwd(),
|
||||
'test/fixtures/team/opencode/relay-works-10-jack-projection-transcript.json'
|
||||
);
|
||||
const fixtureRaw = await readFileFixture(fixturePath, 'utf8');
|
||||
|
||||
execCliMock.mockImplementation(async (_binaryPath, args) => {
|
||||
const normalizedArgs = Array.isArray(args) ? args.join(' ') : '';
|
||||
|
||||
if (
|
||||
normalizedArgs.startsWith(
|
||||
'runtime transcript --json --provider opencode --team relay-works-10 --member jack --projection-only --limit 200 --output '
|
||||
)
|
||||
) {
|
||||
const outputIndex = Array.isArray(args) ? args.indexOf('--output') : -1;
|
||||
const outputPath =
|
||||
outputIndex >= 0 && Array.isArray(args) ? String(args[outputIndex + 1] ?? '') : '';
|
||||
await writeFile(outputPath, fixtureRaw, 'utf8');
|
||||
return Promise.resolve({
|
||||
stdout: '',
|
||||
stderr: '',
|
||||
exitCode: 0,
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.reject(new Error(`Unexpected execCli call: ${normalizedArgs}`));
|
||||
});
|
||||
|
||||
const { ClaudeMultimodelBridgeService } =
|
||||
await import('@main/services/runtime/ClaudeMultimodelBridgeService');
|
||||
const service = new ClaudeMultimodelBridgeService();
|
||||
|
||||
const transcript = await service.getOpenCodeTranscript('/mock/agent_teams_orchestrator', {
|
||||
teamId: 'relay-works-10',
|
||||
memberName: 'jack',
|
||||
limit: 200,
|
||||
});
|
||||
|
||||
const projectedMessages = transcript?.logProjection?.messages ?? [];
|
||||
const toolNames = projectedMessages.flatMap((message) =>
|
||||
message.toolCalls.map((toolCall) => toolCall.name)
|
||||
);
|
||||
|
||||
expect(fixtureRaw.length).toBeGreaterThan(64_000);
|
||||
expect(transcript?.sessionId).toBe('ses_23edf9243ffeSNYPWObDloBJyQ');
|
||||
expect(transcript?.messageCount).toBe(65);
|
||||
expect(transcript?.toolCallCount).toBe(36);
|
||||
expect(transcript?.messages).toEqual([]);
|
||||
expect(projectedMessages).toHaveLength(101);
|
||||
expect(toolNames).toEqual(
|
||||
expect.arrayContaining([
|
||||
'agent-teams_runtime_bootstrap_checkin',
|
||||
'agent-teams_member_briefing',
|
||||
'agent-teams_message_send',
|
||||
'agent-teams_task_start',
|
||||
'agent-teams_task_add_comment',
|
||||
'agent-teams_task_complete',
|
||||
'bash',
|
||||
'read',
|
||||
])
|
||||
);
|
||||
expect(toolNames).not.toContain('SendMessage');
|
||||
});
|
||||
|
||||
it('verifies OpenCode models through execution-grade runtime probes', async () => {
|
||||
execCliMock.mockImplementation((_binaryPath, args) => {
|
||||
const normalizedArgs = Array.isArray(args) ? args.join(' ') : '';
|
||||
|
|
|
|||
|
|
@ -0,0 +1,373 @@
|
|||
// @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',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
async function loadFixtureTranscript(): Promise<
|
||||
NonNullable<OpenCodeRuntimeTranscriptResponse['transcript']>
|
||||
> {
|
||||
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<OpenCodeRuntimeTranscriptResponse['transcript']>
|
||||
): string {
|
||||
return JSON.stringify(transcript.logProjection?.messages ?? []);
|
||||
}
|
||||
|
||||
function markerTaskIds(messages: ParsedMessage[], markerNames: Set<string>): Set<string> {
|
||||
const taskIds = new Set<string>();
|
||||
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<string, unknown>).taskId;
|
||||
if (typeof taskId === 'string') {
|
||||
taskIds.add(taskId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return taskIds;
|
||||
}
|
||||
|
||||
function createSource(params: {
|
||||
transcript: NonNullable<OpenCodeRuntimeTranscriptResponse['transcript']>;
|
||||
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('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,
|
||||
});
|
||||
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('<opencode_runtime_identity>');
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,141 @@
|
|||
import { readFile } from 'fs/promises';
|
||||
import path from 'path';
|
||||
import React, { act } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { afterEach, 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 { TooltipProvider } from '../../../../../src/renderer/components/ui/tooltip';
|
||||
|
||||
import type { OpenCodeRuntimeTranscriptResponse } from '../../../../../src/main/services/runtime/ClaudeMultimodelBridgeService';
|
||||
import type { BoardTaskLogStreamResponse, TeamTask } from '../../../../../src/shared/types';
|
||||
|
||||
const TEAM_NAME = 'relay-works-10';
|
||||
const TASK_ID = '0b3a0624-5d66-4067-848e-5a74a1720c0d';
|
||||
const FIXTURE_PATH = path.resolve(
|
||||
process.cwd(),
|
||||
'test/fixtures/team/opencode/relay-works-10-jack-projection-transcript.json'
|
||||
);
|
||||
|
||||
const apiState = {
|
||||
getTaskLogStream: vi.fn<
|
||||
(teamName: string, taskId: string) => Promise<BoardTaskLogStreamResponse>
|
||||
>(),
|
||||
onTeamChange: vi.fn<(callback: (event: unknown, data: unknown) => void) => () => void>(),
|
||||
};
|
||||
|
||||
vi.mock('@renderer/api', () => ({
|
||||
api: {
|
||||
teams: {
|
||||
getTaskLogStream: (...args: Parameters<typeof apiState.getTaskLogStream>) =>
|
||||
apiState.getTaskLogStream(...args),
|
||||
onTeamChange: (...args: Parameters<typeof apiState.onTeamChange>) =>
|
||||
apiState.onTeamChange(...args),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
import { TaskLogStreamSection } from '@renderer/components/team/taskLogs/TaskLogStreamSection';
|
||||
|
||||
const RELAY_WORKS_10_TASK: TeamTask = {
|
||||
id: TASK_ID,
|
||||
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',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
async function loadFixtureTranscript(): Promise<
|
||||
NonNullable<OpenCodeRuntimeTranscriptResponse['transcript']>
|
||||
> {
|
||||
const parsed = JSON.parse(await readFile(FIXTURE_PATH, 'utf8')) as OpenCodeRuntimeTranscriptResponse;
|
||||
if (parsed.providerId !== 'opencode' || !parsed.transcript) {
|
||||
throw new Error('Invalid OpenCode transcript fixture');
|
||||
}
|
||||
return parsed.transcript;
|
||||
}
|
||||
|
||||
async function buildFixtureStream(): Promise<BoardTaskLogStreamResponse> {
|
||||
const transcript = await loadFixtureTranscript();
|
||||
const source = new OpenCodeTaskLogStreamSource(
|
||||
{
|
||||
getOpenCodeTranscript: vi.fn(async () => transcript),
|
||||
} as never,
|
||||
{ resolve: async () => '/tmp/agent_teams_orchestrator' },
|
||||
{
|
||||
getTasks: vi.fn(async () => [RELAY_WORKS_10_TASK]),
|
||||
getDeletedTasks: vi.fn(async () => []),
|
||||
} as never,
|
||||
new BoardTaskExactLogChunkBuilder(),
|
||||
{ readTaskRecords: vi.fn(async () => []) }
|
||||
);
|
||||
const stream = await source.getTaskLogStream(TEAM_NAME, TASK_ID);
|
||||
if (!stream) {
|
||||
throw new Error('Expected OpenCode fixture stream');
|
||||
}
|
||||
return stream;
|
||||
}
|
||||
|
||||
function flushMicrotasks(): Promise<void> {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
describe('TaskLogStreamSection OpenCode real fixture e2e', () => {
|
||||
afterEach(() => {
|
||||
document.body.innerHTML = '';
|
||||
apiState.getTaskLogStream.mockReset();
|
||||
apiState.onTeamChange.mockReset();
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it('renders real OpenCode task activity through the UI log stream', async () => {
|
||||
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
||||
apiState.onTeamChange.mockImplementation(() => () => undefined);
|
||||
apiState.getTaskLogStream.mockResolvedValueOnce(await buildFixtureStream());
|
||||
|
||||
const host = document.createElement('div');
|
||||
document.body.appendChild(host);
|
||||
const root = createRoot(host);
|
||||
|
||||
await act(async () => {
|
||||
root.render(
|
||||
React.createElement(
|
||||
TooltipProvider,
|
||||
null,
|
||||
React.createElement(TaskLogStreamSection, {
|
||||
teamName: TEAM_NAME,
|
||||
taskId: TASK_ID,
|
||||
liveEnabled: false,
|
||||
})
|
||||
)
|
||||
);
|
||||
await flushMicrotasks();
|
||||
await flushMicrotasks();
|
||||
});
|
||||
|
||||
const text = host.textContent ?? '';
|
||||
expect(text).toContain('Task Log Stream');
|
||||
expect(text).toContain('matched task tool markers');
|
||||
expect(text).toContain('Agent');
|
||||
expect(text).toContain('Calculator behavior');
|
||||
expect(text).toContain('Задача #0b3a0624 завершена');
|
||||
expect(text).not.toContain('Keyboard handlers added');
|
||||
expect(text).not.toContain('Logic smoke check');
|
||||
expect(text).not.toContain('#00000000');
|
||||
expect(text).not.toContain('SendMessage');
|
||||
|
||||
await act(async () => {
|
||||
root.unmount();
|
||||
await flushMicrotasks();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -29,4 +29,22 @@ describe('agentBlocks', () => {
|
|||
expect(unwrapAgentBlock('```info_for_agent\ninside\n```')).toBe('inside');
|
||||
expect(unwrapAgentBlock('<agent-block>\ninside\n</agent-block>')).toBe('inside');
|
||||
});
|
||||
|
||||
it('strips OpenCode runtime-only delivery blocks from display text', () => {
|
||||
const text = [
|
||||
'<opencode_runtime_identity>',
|
||||
'Do not use SendMessage.',
|
||||
'</opencode_runtime_identity>',
|
||||
'<opencode_app_message_delivery>',
|
||||
'Use agent-teams_message_send.',
|
||||
'</opencode_app_message_delivery>',
|
||||
'Human-visible task text',
|
||||
].join('\n');
|
||||
|
||||
expect(stripAgentBlocks(text)).toBe('Human-visible task text');
|
||||
expect(extractAgentBlockContents(text)).toEqual([
|
||||
'Do not use SendMessage.',
|
||||
'Use agent-teams_message_send.',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue