From fdf46653fcb1fdf2dc9fb7b0e66ab243f61e8891 Mon Sep 17 00:00:00 2001 From: Gautam Tayal Date: Sat, 11 Apr 2026 15:54:16 +0530 Subject: [PATCH] chore: add tests --- .../__tests__/session-manager/restore.test.ts | 70 +++++++++++++++++++ .../__tests__/session-manager/spawn.test.ts | 66 +++++++++++++++++ .../src/agent-opencode.integration.test.ts | 26 +++---- .../plugins/agent-opencode/src/index.test.ts | 67 ++++++++++-------- 4 files changed, 186 insertions(+), 43 deletions(-) diff --git a/packages/core/src/__tests__/session-manager/restore.test.ts b/packages/core/src/__tests__/session-manager/restore.test.ts index 9b2e737d6..4d8e9c6ce 100644 --- a/packages/core/src/__tests__/session-manager/restore.test.ts +++ b/packages/core/src/__tests__/session-manager/restore.test.ts @@ -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 }); diff --git a/packages/core/src/__tests__/session-manager/spawn.test.ts b/packages/core/src/__tests__/session-manager/spawn.test.ts index 560386e1f..b4842fb9e 100644 --- a/packages/core/src/__tests__/session-manager/spawn.test.ts +++ b/packages/core/src/__tests__/session-manager/spawn.test.ts @@ -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 }); diff --git a/packages/integration-tests/src/agent-opencode.integration.test.ts b/packages/integration-tests/src/agent-opencode.integration.test.ts index 89c521e4c..62d704b4d 100644 --- a/packages/integration-tests/src/agent-opencode.integration.test.ts +++ b/packages/integration-tests/src/agent-opencode.integration.test.ts @@ -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", () => { diff --git a/packages/plugins/agent-opencode/src/index.test.ts b/packages/plugins/agent-opencode/src/index.test.ts index eef583bac..00c8835c4 100644 --- a/packages/plugins/agent-opencode/src/index.test.ts +++ b/packages/plugins/agent-opencode/src/index.test.ts @@ -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!(