chore: add tests

This commit is contained in:
Gautam Tayal 2026-04-11 15:54:16 +05:30
parent f83f45eacb
commit fdf46653fc
4 changed files with 186 additions and 43 deletions

View File

@ -5,6 +5,7 @@ import {
} from "node:fs";
import { join } from "node:path";
import { createSessionManager } from "../../session-manager.js";
import { getWorkspaceOpenCodeConfigPath } from "../../opencode-config.js";
import {
writeMetadata,
readMetadataRaw,
@ -474,6 +475,75 @@ describe("restore", () => {
expect(createCall.launchCommand).toBe("claude --resume abc123");
});
it("passes OPENCODE_CONFIG when restoring OpenCode orchestrators", async () => {
const wsPath = join(tmpDir, "ws-app-orchestrator-opencode-restore");
mkdirSync(join(wsPath, ".ao"), { recursive: true });
writeFileSync(getWorkspaceOpenCodeConfigPath(wsPath), "{}\n", "utf-8");
const mockOpenCodeAgentWithRestore: Agent = {
...mockAgent,
name: "opencode",
getRestoreCommand: vi.fn().mockResolvedValue("opencode --session 'ses_restore'"),
};
const registryWithOpenCodeRestore: PluginRegistry = {
...mockRegistry,
get: vi.fn().mockImplementation((slot: string) => {
if (slot === "runtime") return mockRuntime;
if (slot === "agent") return mockOpenCodeAgentWithRestore;
if (slot === "workspace") return mockWorkspace;
return null;
}),
};
writeMetadata(sessionsDir, "app-orchestrator", {
worktree: wsPath,
branch: "main",
status: "killed",
project: "my-app",
role: "orchestrator",
agent: "opencode",
opencodeSessionId: "ses_restore",
runtimeHandle: JSON.stringify(makeHandle("rt-old")),
});
const configWithOpenCode: OrchestratorConfig = {
...config,
defaults: { ...config.defaults, agent: "opencode" },
projects: {
...config.projects,
"my-app": {
...config.projects["my-app"],
agent: "opencode",
},
},
};
const sm = createSessionManager({
config: configWithOpenCode,
registry: registryWithOpenCodeRestore,
});
await sm.restore("app-orchestrator");
expect(mockOpenCodeAgentWithRestore.getRestoreCommand).toHaveBeenCalledWith(
expect.objectContaining({ workspacePath: wsPath }),
expect.objectContaining({
agentConfig: expect.objectContaining({
opencodeSessionId: "ses_restore",
permissions: "permissionless",
}),
}),
);
expect(mockRuntime.create).toHaveBeenCalledWith(
expect.objectContaining({
environment: expect.objectContaining({
OPENCODE_CONFIG: getWorkspaceOpenCodeConfigPath(wsPath),
}),
}),
);
});
it("falls back to getLaunchCommand when getRestoreCommand returns null", async () => {
const wsPath = join(tmpDir, "ws-app-1");
mkdirSync(wsPath, { recursive: true });

View File

@ -3,6 +3,7 @@ import { mkdirSync, readFileSync, existsSync } from "node:fs";
import { join } from "node:path";
import { createSessionManager } from "../../session-manager.js";
import { validateConfig } from "../../config.js";
import { OPENCODE_INTERNAL_ORCHESTRATOR_AGENT_NAME, getWorkspaceOpenCodeConfigPath } from "../../opencode-config.js";
import {
writeMetadata,
readMetadata,
@ -1709,6 +1710,71 @@ describe("spawn", () => {
expect(readFileSync(promptFile, "utf-8")).toBe("You are the orchestrator.");
});
it("generates .ao/opencode.json and passes OPENCODE_CONFIG for OpenCode orchestrators", async () => {
const opencodeAgent: Agent = {
...mockAgent,
name: "opencode",
};
const registryWithOpenCode: PluginRegistry = {
...mockRegistry,
get: vi.fn().mockImplementation((slot: string) => {
if (slot === "runtime") return mockRuntime;
if (slot === "agent") return opencodeAgent;
if (slot === "workspace") return mockWorkspace;
return null;
}),
};
const configWithOpenCode: OrchestratorConfig = {
...config,
defaults: { ...config.defaults, agent: "opencode" },
projects: {
...config.projects,
"my-app": {
...config.projects["my-app"],
agent: "opencode",
},
},
};
const sm = createSessionManager({
config: configWithOpenCode,
registry: registryWithOpenCode,
});
await sm.spawnOrchestrator({
projectId: "my-app",
systemPrompt: "You are the orchestrator.",
});
const opencodeConfigPath = getWorkspaceOpenCodeConfigPath("/tmp/ws");
expect(existsSync(opencodeConfigPath)).toBe(true);
expect(JSON.parse(readFileSync(opencodeConfigPath, "utf-8"))).toEqual({
$schema: "https://opencode.ai/config.json",
default_agent: OPENCODE_INTERNAL_ORCHESTRATOR_AGENT_NAME,
agent: {
[OPENCODE_INTERNAL_ORCHESTRATOR_AGENT_NAME]: {
description: "Agent Orchestrator internal orchestrator agent",
mode: "primary",
prompt: expect.stringContaining("orchestrator-prompt-app-orchestrator-1.md"),
},
},
});
expect(opencodeAgent.getLaunchCommand).toHaveBeenCalledWith(
expect.objectContaining({
systemPromptFile: expect.stringContaining("orchestrator-prompt-app-orchestrator-1.md"),
}),
);
expect(mockRuntime.create).toHaveBeenCalledWith(
expect.objectContaining({
environment: expect.objectContaining({
OPENCODE_CONFIG: opencodeConfigPath,
}),
}),
);
});
it("throws for unknown project", async () => {
const sm = createSessionManager({ config, registry: mockRegistry });

View File

@ -16,7 +16,7 @@ import { mkdtemp, readFile, rm } from "node:fs/promises";
import { homedir, tmpdir } from "node:os";
import { join } from "node:path";
import { promisify } from "node:util";
import type { ActivityDetection, AgentSessionInfo } from "@aoagents/ao-core";
import { type ActivityDetection, type AgentSessionInfo } from "@aoagents/ao-core";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import opencodePlugin from "@aoagents/ao-plugin-agent-opencode";
import {
@ -216,11 +216,11 @@ describe("getLaunchCommand (integration)", () => {
systemPrompt: "You are an orchestrator",
prompt: "do the task",
});
expect(cmd).toContain("opencode run --format json --title 'AO:test-1' --command true");
expect(cmd).toContain(
`exec opencode --session "$SES_ID" --prompt 'You are an orchestrator
do the task'`,
"opencode run --format json --title 'AO:test-1' --command true",
);
expect(cmd).toContain(
`exec opencode --session "$SES_ID" --prompt 'do the task'`,
);
});
@ -230,9 +230,11 @@ do the task'`,
systemPromptFile: "/tmp/orchestrator-prompt.md",
prompt: "do the task",
});
expect(cmd).toContain("opencode run --format json --title 'AO:test-1' --command true");
expect(cmd).toContain(
"exec opencode --session \"$SES_ID\" --prompt \"$(cat '/tmp/orchestrator-prompt.md'; printf '\\n\\n'; printf %s 'do the task')\"",
"opencode run --format json --title 'AO:test-1' --command true",
);
expect(cmd).toContain(
`exec opencode --session "$SES_ID" --prompt 'do the task'`,
);
});
@ -253,12 +255,11 @@ do the task'`,
model: "gpt-5.2",
prompt: "review this code",
});
expect(cmd).toContain("--agent 'oracle'");
expect(cmd).toContain(
"opencode run --format json --title 'AO:test-1' --agent 'oracle' --model 'gpt-5.2' --command true",
);
expect(cmd).toContain(
"exec opencode --session \"$SES_ID\" --prompt 'You are an expert\n\nreview this code' --agent 'oracle' --model 'gpt-5.2'",
`exec opencode --session "$SES_ID" --prompt 'review this code' --agent 'oracle' --model 'gpt-5.2'`,
);
expect(cmd).toContain("--model 'gpt-5.2'");
});
@ -269,7 +270,6 @@ do the task'`,
systemPrompt: "direct prompt",
systemPromptFile: "/tmp/file-prompt.md",
});
expect(cmd).toContain("\"$(cat '/tmp/file-prompt.md')\"");
expect(cmd).not.toContain("direct prompt");
});
@ -284,7 +284,7 @@ do the task'`,
"opencode run --format json --title 'AO:test-orchestrator' --command true",
);
expect(cmd).toContain(
'exec opencode --session "$SES_ID" --prompt "$(cat \'/tmp/orchestrator-prompt.md\')"',
`exec opencode --session "$SES_ID"`,
);
});
@ -293,7 +293,7 @@ do the task'`,
...baseConfig,
systemPrompt: "it's important",
});
expect(cmd).toContain("'it'\\''s important'");
expect(cmd).not.toContain("'it'\\''s important'");
});
it("escapes path with single quotes in systemPromptFile", () => {
@ -301,7 +301,7 @@ do the task'`,
...baseConfig,
systemPromptFile: "/tmp/it's-prompt.md",
});
expect(cmd).toContain("\"$(cat '/tmp/it'\\''s-prompt.md')\"");
expect(cmd).not.toContain("/tmp/it'\\''s-prompt.md");
});
it("handles prompt with special shell characters", () => {

View File

@ -1,5 +1,6 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import type { Session, RuntimeHandle, AgentLaunchConfig } from "@aoagents/ao-core";
import { mkdirSync, writeFileSync } from "node:fs";
import { type Session, type RuntimeHandle, type AgentLaunchConfig } from "@aoagents/ao-core";
const { mockAppendActivityEntry, mockReadLastActivityEntry, mockRecordTerminalActivity } =
vi.hoisted(() => ({
@ -255,7 +256,9 @@ describe("getLaunchCommand", () => {
makeLaunchConfig({ systemPrompt: "You are an orchestrator" }),
);
expect(cmd).toContain("opencode run --format json --title 'AO:sess-1'");
expect(cmd).toContain("exec opencode --session \"$SES_ID\" --prompt 'You are an orchestrator'");
expect(cmd).toContain('exec opencode --session "$SES_ID"');
expect(cmd).not.toContain("--prompt 'You are an orchestrator'");
expect(cmd).not.toContain("--agent");
});
it("generates command with systemPrompt and task prompt", () => {
@ -263,16 +266,14 @@ describe("getLaunchCommand", () => {
makeLaunchConfig({ systemPrompt: "You are an orchestrator", prompt: "do the task" }),
);
expect(cmd).toContain("opencode run --format json --title 'AO:sess-1'");
expect(cmd).toContain(
`exec opencode --session "$SES_ID" --prompt 'You are an orchestrator
do the task'`,
);
expect(cmd).toContain(`exec opencode --session "$SES_ID" --prompt 'do the task'`);
expect(cmd).not.toContain("--agent");
});
it("escapes single quotes in systemPrompt", () => {
const cmd = agent.getLaunchCommand(makeLaunchConfig({ systemPrompt: "it's important" }));
expect(cmd).toContain("exec opencode --session \"$SES_ID\" --prompt 'it'\\''s important'");
expect(cmd).not.toContain("it'\\''s important");
expect(cmd).not.toContain("--agent");
});
it("handles very long systemPrompt", () => {
@ -285,17 +286,17 @@ do the task'`,
it("generates command with systemPromptFile via shell substitution", () => {
const cmd = agent.getLaunchCommand(makeLaunchConfig({ systemPromptFile: "/tmp/prompt.md" }));
expect(cmd).toContain("opencode run --format json --title 'AO:sess-1'");
expect(cmd).toContain('exec opencode --session "$SES_ID" --prompt "$(cat \'/tmp/prompt.md\')"');
expect(cmd).toContain('exec opencode --session "$SES_ID"');
expect(cmd).not.toContain("$(cat '/tmp/prompt.md')");
expect(cmd).not.toContain("--agent");
});
it("escapes path in systemPromptFile", () => {
const cmd = agent.getLaunchCommand(
makeLaunchConfig({ systemPromptFile: "/tmp/it's-prompt.md" }),
);
expect(cmd).toContain("opencode run --format json --title 'AO:sess-1'");
expect(cmd).toContain(
"exec opencode --session \"$SES_ID\" --prompt \"$(cat '/tmp/it'\\''s-prompt.md')\"",
);
expect(cmd).not.toContain("/tmp/it'\\''s-prompt.md");
expect(cmd).not.toContain("--agent");
});
it("systemPromptFile takes precedence over systemPrompt", () => {
@ -306,13 +307,11 @@ do the task'`,
}),
);
expect(cmd).toContain("opencode run --format json --title 'AO:sess-1'");
expect(cmd).toContain(
'exec opencode --session "$SES_ID" --prompt "$(cat \'/tmp/file-prompt.md\')"',
);
expect(cmd).toContain('exec opencode --session "$SES_ID"');
expect(cmd).not.toContain("direct prompt");
});
it("combines systemPromptFile with subagent and prompt", () => {
it("keeps subagent when systemPromptFile is set", () => {
const cmd = agent.getLaunchCommand(
makeLaunchConfig({
systemPromptFile: "/tmp/orchestrator.md",
@ -324,11 +323,7 @@ do the task'`,
"opencode run --format json --title 'AO:sess-1' --agent 'sisyphus'",
);
expect(cmd).toContain(
"exec opencode --session \"$SES_ID\" --prompt \"$(cat '/tmp/orchestrator.md'; printf '\\n\\n'; printf %s 'fix the bug')\" --agent 'sisyphus'",
);
expect(cmd).toContain("--agent 'sisyphus");
expect(cmd).toContain(
"$(cat '/tmp/orchestrator.md'; printf '\\n\\n'; printf %s 'fix the bug')",
`exec opencode --session "$SES_ID" --prompt 'fix the bug' --agent 'sisyphus'`,
);
});
@ -341,12 +336,11 @@ do the task'`,
}),
);
expect(cmd).toContain("opencode run --format json --title 'AO:my-orchestrator'");
expect(cmd).toContain(
'exec opencode --session "$SES_ID" --prompt "$(cat \'/tmp/orchestrator.md\')"',
);
expect(cmd).toContain('exec opencode --session "$SES_ID"');
expect(cmd).not.toContain("--agent");
});
it("combines systemPromptFile with subagent and prompt - shell escape", () => {
it("keeps prompt separate when systemPromptFile and subagent are both set", () => {
const cmd = agent.getLaunchCommand(
makeLaunchConfig({
systemPromptFile: "/tmp/orchestrator.md",
@ -358,11 +352,9 @@ do the task'`,
"opencode run --format json --title 'AO:sess-1' --agent 'sisyphus'",
);
expect(cmd).toContain(
"exec opencode --session \"$SES_ID\" --prompt \"$(cat '/tmp/orchestrator.md'; printf '\\n\\n'; printf %s 'fix the bug')\" --agent 'sisyphus'",
);
expect(cmd).toContain(
"$(cat '/tmp/orchestrator.md'; printf '\\n\\n'; printf %s 'fix the bug')",
`exec opencode --session "$SES_ID" --prompt 'fix the bug' --agent 'sisyphus'`,
);
expect(cmd).not.toContain("$(cat '/tmp/orchestrator.md'");
});
it("handles prompt with special characters", () => {
@ -764,6 +756,21 @@ describe("getRestoreCommand", () => {
expect(cmd).toContain("--model 'claude-sonnet-4-5-20250929'");
});
it("does not inject an internal agent when workspace opencode.json exists", async () => {
const workspacePath = "/tmp/test-restore-opencode-config";
mkdirSync(`${workspacePath}/.ao`, { recursive: true });
writeFileSync(`${workspacePath}/.ao/opencode.json`, "{}\n", "utf-8");
const cmd = await agent.getRestoreCommand!(
makeSession({
workspacePath,
metadata: { opencodeSessionId: "ses_abc123" },
}),
{ name: "proj", repo: "o/r", path: "/p", defaultBranch: "main", sessionPrefix: "p" },
);
expect(cmd).toBe("opencode --session 'ses_abc123'");
});
it("returns null when no session ID found", async () => {
mockExecFileAsync.mockRejectedValue(new Error("opencode not found"));
const cmd = await agent.getRestoreCommand!(