chore: add tests
This commit is contained in:
parent
f83f45eacb
commit
fdf46653fc
|
|
@ -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 });
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
|
||||
|
|
|
|||
|
|
@ -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", () => {
|
||||
|
|
|
|||
|
|
@ -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!(
|
||||
|
|
|
|||
Loading…
Reference in New Issue