agent-ecosystem/test/main/services/team/openCodeSemanticScenarioHarness.ts

468 lines
14 KiB
TypeScript

import { promises as fs } from 'fs';
import * as path from 'path';
import { OpenCodeTeamRuntimeAdapter } from '../../../../src/main/services/team/runtime/OpenCodeTeamRuntimeAdapter';
import { TeamTaskWriter } from '../../../../src/main/services/team/TeamTaskWriter';
import type {
OpenCodeLaunchTeamCommandBody,
OpenCodeLaunchTeamCommandData,
OpenCodeSendMessageCommandBody,
OpenCodeSendMessageCommandData,
} from '../../../../src/main/services/team/opencode/bridge/OpenCodeBridgeCommandContract';
import type { OpenCodeTeamLaunchReadiness } from '../../../../src/main/services/team/opencode/readiness/OpenCodeTeamLaunchReadiness';
import { REQUIRED_AGENT_TEAMS_APP_TOOL_IDS } from '../../../../src/main/services/team/opencode/mcp/OpenCodeMcpToolAvailability';
import type {
OpenCodeTeamRuntimeBridgePort,
OpenCodeTeamRuntimeMessageInput,
} from '../../../../src/main/services/team/runtime';
import type {
TeamLaunchRuntimeAdapter,
TeamRuntimeLaunchInput,
TeamRuntimeLaunchResult,
TeamRuntimeMemberLaunchEvidence,
TeamRuntimePrepareResult,
TeamRuntimeReconcileInput,
TeamRuntimeReconcileResult,
TeamRuntimeStopInput,
TeamRuntimeStopResult,
} from '../../../../src/main/services/team/runtime/TeamRuntimeAdapter';
import type { AgentActionMode, TaskRef, TeamCreateRequest, TeamTask } from '../../../../src/shared/types';
const FIXTURE_PATH = path.join(
process.cwd(),
'test',
'fixtures',
'team',
'opencode',
'semantic-realistic-scenario.json'
);
export interface OpenCodeSemanticScenario {
teamNamePrefix: string;
displayName: string;
description: string;
teamPromptLines: string[];
members: Array<{
name: string;
role: string;
workflowLines: string[];
}>;
projectFiles: Array<{
path: string;
contentLines: string[];
}>;
tasks: Array<{
taskId: string;
displayId: string;
subject: string;
owner: string;
comment: string;
}>;
directDelivery: {
memberName: string;
replyRecipient: string;
actionMode: AgentActionMode;
taskIndex: number;
expectedReplyToken: string;
textLines: string[];
};
peerDelivery: {
senderName: string;
recipientName: string;
replyRecipient: string;
actionMode: AgentActionMode;
taskIndex: number;
peerToken: string;
expectedReplyToken: string;
textLines: string[];
};
}
export interface CapturedOpenCodeBridge {
readonly launchCommands: OpenCodeLaunchTeamCommandBody[];
readonly messageCommands: OpenCodeSendMessageCommandBody[];
readonly bridge: OpenCodeTeamRuntimeBridgePort;
}
export class CapturingOpenCodeRuntimeAdapter implements TeamLaunchRuntimeAdapter {
readonly providerId = 'opencode' as const;
readonly launchInputs: TeamRuntimeLaunchInput[] = [];
async prepare(input: TeamRuntimeLaunchInput): Promise<TeamRuntimePrepareResult> {
return {
ok: true,
providerId: 'opencode',
modelId: input.model ?? null,
diagnostics: [],
warnings: [],
};
}
async launch(input: TeamRuntimeLaunchInput): Promise<TeamRuntimeLaunchResult> {
this.launchInputs.push(input);
return {
runId: input.runId,
teamName: input.teamName,
launchPhase: 'finished',
teamLaunchState: 'clean_success',
members: Object.fromEntries(
input.expectedMembers.map((member, index) => [
member.name,
buildConfirmedMemberEvidence(member.name, member.model ?? input.model ?? null, index),
])
),
warnings: [],
diagnostics: ['captured OpenCode launch input'],
};
}
async reconcile(input: TeamRuntimeReconcileInput): Promise<TeamRuntimeReconcileResult> {
return {
runId: input.runId,
teamName: input.teamName,
launchPhase: 'reconciled',
teamLaunchState: 'clean_success',
members: Object.fromEntries(
input.expectedMembers.map((member, index) => [
member.name,
buildConfirmedMemberEvidence(member.name, member.model ?? null, index),
])
),
snapshot: input.previousLaunchState,
warnings: [],
diagnostics: ['captured OpenCode reconcile input'],
};
}
async stop(input: TeamRuntimeStopInput): Promise<TeamRuntimeStopResult> {
return {
runId: input.runId,
teamName: input.teamName,
stopped: true,
members: {},
warnings: [],
diagnostics: ['captured OpenCode stop input'],
};
}
}
export async function loadOpenCodeSemanticScenario(): Promise<OpenCodeSemanticScenario> {
const parsed = JSON.parse(await fs.readFile(FIXTURE_PATH, 'utf8')) as OpenCodeSemanticScenario;
if (!Array.isArray(parsed.members) || parsed.members.length < 2) {
throw new Error('OpenCode semantic scenario requires at least two members.');
}
if (!Array.isArray(parsed.tasks) || parsed.tasks.length < 2) {
throw new Error('OpenCode semantic scenario requires at least two tasks.');
}
return parsed;
}
export async function materializeOpenCodeScenarioProject(
scenario: OpenCodeSemanticScenario,
projectPath: string
): Promise<void> {
for (const file of scenario.projectFiles) {
const targetPath = path.join(projectPath, file.path);
await fs.mkdir(path.dirname(targetPath), { recursive: true });
await fs.writeFile(targetPath, `${file.contentLines.join('\n')}\n`, 'utf8');
}
}
export async function materializeOpenCodeScenarioTasks(input: {
scenario: OpenCodeSemanticScenario;
teamName: string;
projectPath: string;
}): Promise<void> {
const writer = new TeamTaskWriter();
const createdAt = '2026-04-21T00:00:00.000Z';
for (const task of input.scenario.tasks) {
const record: TeamTask = {
id: task.taskId,
displayId: task.displayId,
subject: task.subject,
description: task.comment,
owner: task.owner,
createdBy: 'lead',
status: 'in_progress',
projectPath: input.projectPath,
createdAt,
updatedAt: createdAt,
comments: [
{
id: `${task.taskId}-comment`,
author: 'lead',
text: task.comment,
createdAt,
type: 'regular',
},
],
};
await writer.createTask(input.teamName, record);
}
}
export function buildOpenCodeScenarioTeamRequest(input: {
scenario: OpenCodeSemanticScenario;
teamName: string;
projectPath: string;
model: string;
memberNames?: string[];
}): TeamCreateRequest {
const memberNames = new Set(input.memberNames ?? input.scenario.members.map((member) => member.name));
return {
teamName: input.teamName,
displayName: input.scenario.displayName,
description: input.scenario.description,
cwd: input.projectPath,
providerId: 'opencode',
model: input.model,
skipPermissions: true,
prompt: input.scenario.teamPromptLines.join('\n'),
members: input.scenario.members
.filter((member) => memberNames.has(member.name))
.map((member) => ({
name: member.name,
role: member.role,
workflow: member.workflowLines.join('\n'),
providerId: 'opencode' as const,
model: input.model,
})),
};
}
export function buildScenarioRuntimeMessageInput(input: {
scenario: OpenCodeSemanticScenario;
teamName: string;
projectPath: string;
runId?: string;
laneId?: string;
kind: 'direct' | 'peer';
}): OpenCodeTeamRuntimeMessageInput {
if (input.kind === 'direct') {
const delivery = input.scenario.directDelivery;
return {
runId: input.runId,
teamName: input.teamName,
laneId: input.laneId ?? 'primary',
memberName: delivery.memberName,
cwd: input.projectPath,
text: delivery.textLines.join('\n'),
messageId: `semantic-direct-${delivery.expectedReplyToken}`,
replyRecipient: delivery.replyRecipient,
actionMode: delivery.actionMode,
taskRefs: [taskRefForScenario(input.scenario, delivery.taskIndex, input.teamName)],
};
}
const delivery = input.scenario.peerDelivery;
return {
runId: input.runId,
teamName: input.teamName,
laneId: input.laneId ?? 'primary',
memberName: delivery.senderName,
cwd: input.projectPath,
text: delivery.textLines.join('\n'),
messageId: `semantic-peer-${delivery.peerToken}`,
replyRecipient: delivery.replyRecipient,
actionMode: delivery.actionMode,
taskRefs: [taskRefForScenario(input.scenario, delivery.taskIndex, input.teamName)],
};
}
export function taskRefForScenario(
scenario: OpenCodeSemanticScenario,
taskIndex: number,
teamName: string
): TaskRef {
const task = scenario.tasks[taskIndex];
if (!task) {
throw new Error(`OpenCode semantic scenario task index ${taskIndex} is missing.`);
}
return {
taskId: task.taskId,
displayId: task.displayId,
teamName,
};
}
export function createCapturingOpenCodeBridge(modelId: string): CapturedOpenCodeBridge {
const launchCommands: OpenCodeLaunchTeamCommandBody[] = [];
const messageCommands: OpenCodeSendMessageCommandBody[] = [];
return {
launchCommands,
messageCommands,
bridge: {
checkOpenCodeTeamLaunchReadiness: async () => readyOpenCodeReadiness(modelId),
getLastOpenCodeRuntimeSnapshot: () => ({
providerId: 'opencode',
binaryPath: '/opt/homebrew/bin/opencode',
binaryFingerprint: 'version:1.14.19',
version: '1.14.19',
capabilitySnapshotId: 'capability-semantic-contract',
}),
launchOpenCodeTeam: async (command) => {
launchCommands.push(command);
return buildReadyLaunchData(command, modelId);
},
sendOpenCodeTeamMessage: async (command) => {
messageCommands.push(command);
return buildAcceptedMessageData(command);
},
},
};
}
export async function dumpOpenCodePromptArtifacts(input: {
outputDir: string;
launchInput: TeamRuntimeLaunchInput;
launchCommand: OpenCodeLaunchTeamCommandBody;
messageCommands: OpenCodeSendMessageCommandBody[];
}): Promise<void> {
await fs.mkdir(input.outputDir, { recursive: true });
const summary = {
teamName: input.launchInput.teamName,
launchPromptChars: input.launchInput.prompt?.length ?? 0,
leadPromptChars: input.launchCommand.leadPrompt?.length ?? 0,
memberPromptChars: input.launchCommand.members.map((member) => ({
name: member.name,
chars: member.prompt?.length ?? 0,
})),
messagePromptChars: input.messageCommands.map((command) => ({
memberName: command.memberName,
chars: command.text.length,
actionMode: command.actionMode ?? null,
taskRefs: command.taskRefs ?? [],
})),
};
await fs.writeFile(path.join(input.outputDir, 'summary.json'), `${JSON.stringify(summary, null, 2)}\n`);
await fs.writeFile(
path.join(input.outputDir, 'launch-command.json'),
`${JSON.stringify(input.launchCommand, null, 2)}\n`
);
await fs.writeFile(
path.join(input.outputDir, 'message-commands.json'),
`${JSON.stringify(input.messageCommands, null, 2)}\n`
);
}
export function createOpenCodeRuntimeAdapterFromCapture(
capture: CapturedOpenCodeBridge
): OpenCodeTeamRuntimeAdapter {
return new OpenCodeTeamRuntimeAdapter(capture.bridge);
}
export function parseOpenCodeE2EModelList(): string[] {
const raw = process.env.OPENCODE_E2E_MODELS?.trim();
if (!raw) {
const single = process.env.OPENCODE_E2E_MODEL?.trim();
return single ? [single] : ['opencode/big-pickle'];
}
return raw
.split(',')
.map((model) => model.trim())
.filter(Boolean);
}
function readyOpenCodeReadiness(modelId: string): OpenCodeTeamLaunchReadiness {
return {
state: 'ready',
launchAllowed: true,
modelId,
availableModels: [modelId],
opencodeVersion: '1.14.19',
installMethod: 'brew',
binaryPath: '/opt/homebrew/bin/opencode',
hostHealthy: true,
appMcpConnected: true,
requiredToolsPresent: true,
permissionBridgeReady: true,
runtimeStoresReady: true,
supportLevel: 'production_supported',
missing: [],
diagnostics: [],
evidence: {
capabilitiesReady: true,
mcpToolProofRoute: '/experimental/tool/ids',
observedMcpTools: [...REQUIRED_AGENT_TEAMS_APP_TOOL_IDS],
runtimeStoreReadinessReason: 'runtime_store_manifest_valid',
},
};
}
function buildReadyLaunchData(
command: OpenCodeLaunchTeamCommandBody,
modelId: string
): OpenCodeLaunchTeamCommandData {
return {
runId: command.runId,
teamLaunchState: 'ready',
durableCheckpoints: [
{ name: 'required_tools_proven', observedAt: '2026-04-21T00:00:00.000Z' },
{ name: 'delivery_ready', observedAt: '2026-04-21T00:00:00.000Z' },
{ name: 'run_ready', observedAt: '2026-04-21T00:00:00.000Z' },
],
members: Object.fromEntries(
command.members.map((member, index) => [
member.name,
{
sessionId: `semantic-session-${member.name}`,
launchState: 'confirmed_alive' as const,
runtimePid: 31_000 + index,
model: modelId,
evidence: [
{ kind: 'required_tools_proven', observedAt: '2026-04-21T00:00:00.000Z' },
{ kind: 'delivery_ready', observedAt: '2026-04-21T00:00:00.000Z' },
{ kind: 'member_ready', observedAt: '2026-04-21T00:00:00.000Z' },
{ kind: 'run_ready', observedAt: '2026-04-21T00:00:00.000Z' },
],
diagnostics: [],
},
])
),
warnings: [],
diagnostics: [],
};
}
function buildAcceptedMessageData(
command: OpenCodeSendMessageCommandBody
): OpenCodeSendMessageCommandData {
return {
accepted: true,
sessionId: `semantic-session-${command.memberName}`,
memberName: command.memberName,
runtimePid: 41_000,
prePromptCursor: 'semantic-pre-prompt-cursor',
responseObservation: {
state: 'responded_tool_call',
deliveredUserMessageId: command.messageId ?? null,
assistantMessageId: 'semantic-assistant-message',
toolCallNames: ['agent-teams_message_send'],
visibleMessageToolCallId: 'semantic-tool-call',
visibleReplyMessageId: command.messageId ?? null,
visibleReplyCorrelation: command.messageId ? 'relayOfMessageId' : null,
latestAssistantPreview: 'semantic accepted message',
reason: null,
},
diagnostics: [],
};
}
function buildConfirmedMemberEvidence(
memberName: string,
model: string | null,
index: number
): TeamRuntimeMemberLaunchEvidence {
return {
memberName,
providerId: 'opencode',
launchState: 'confirmed_alive',
agentToolAccepted: true,
runtimeAlive: true,
bootstrapConfirmed: true,
hardFailure: false,
sessionId: `semantic-session-${memberName}`,
runtimePid: 21_000 + index,
diagnostics: [`captured OpenCode launch ready${model ? ` for ${model}` : ''}`],
};
}