Skip to content

Commit a9e4361

Browse files
committed
test(compat-layer): add OpenCodeAdapter unit tests
20 tests covering identity + capabilities, fromOAC for subagent/primary paths (including the tools→permission fallback), context-to-skills emit, hook→plugin script emit, toOAC for opencode.json + agent.md, and validateConversion warnings. Re-running the full vitest suite reports 604/604 passing (was 584). Pre-existing tsc errors mentioned in commit e9d3a83 turned out to be a result of the deps not being installed in that earlier sandbox. After `bun install`, `bunx tsc --noEmit` runs clean against the compat-layer package — no source fixes were needed for that follow-up.
1 parent e708ebb commit a9e4361

1 file changed

Lines changed: 288 additions & 0 deletions

File tree

Lines changed: 288 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,288 @@
1+
import { describe, it, expect, beforeEach } from "vitest";
2+
import { OpenCodeAdapter } from "../../../src/adapters/OpenCodeAdapter";
3+
import type { OpenAgent, AgentFrontmatter } from "../../../src/types";
4+
5+
/**
6+
* Tests for OpenCodeAdapter (peer of ClaudeAdapter, CursorAdapter, WindsurfAdapter).
7+
*
8+
* Coverage targets:
9+
* 1. Adapter identity + capabilities
10+
* 2. fromOAC() — emits .opencode/{agents,opencode.json,skills,plugin}/
11+
* 3. toOAC() — parses opencode agent.md + opencode.json
12+
* 4. validateConversion() — surfaces warnings for unsupported fields
13+
* 5. Permission and hook event mapping
14+
*/
15+
16+
describe("OpenCodeAdapter", () => {
17+
let adapter: OpenCodeAdapter;
18+
19+
beforeEach(() => {
20+
adapter = new OpenCodeAdapter();
21+
});
22+
23+
// ============================================================================
24+
// IDENTITY + CAPABILITIES
25+
// ============================================================================
26+
27+
describe("adapter identity", () => {
28+
it("has correct name", () => {
29+
expect(adapter.name).toBe("opencode");
30+
});
31+
32+
it("has correct displayName", () => {
33+
expect(adapter.displayName).toBe("opencode");
34+
});
35+
36+
it("returns correct config path", () => {
37+
expect(adapter.getConfigPath()).toBe(".opencode/");
38+
});
39+
});
40+
41+
describe("getCapabilities()", () => {
42+
it("declares granular permissions support", () => {
43+
expect(adapter.getCapabilities().supportsGranularPermissions).toBe(true);
44+
});
45+
46+
it("declares skills, hooks, contexts, temperature support", () => {
47+
const caps = adapter.getCapabilities();
48+
expect(caps.supportsSkills).toBe(true);
49+
expect(caps.supportsHooks).toBe(true);
50+
expect(caps.supportsContexts).toBe(true);
51+
expect(caps.supportsTemperature).toBe(true);
52+
});
53+
54+
it("does not claim maxSteps support", () => {
55+
expect(adapter.getCapabilities().supportsMaxSteps).toBe(false);
56+
});
57+
58+
it("uses markdown + directory output structure", () => {
59+
const caps = adapter.getCapabilities();
60+
expect(caps.configFormat).toBe("markdown");
61+
expect(caps.outputStructure).toBe("directory");
62+
});
63+
});
64+
65+
// ============================================================================
66+
// fromOAC — subagent
67+
// ============================================================================
68+
69+
describe("fromOAC() — subagent", () => {
70+
const subagent: OpenAgent = {
71+
frontmatter: {
72+
name: "code-reviewer",
73+
description: "Reviews code",
74+
mode: "subagent",
75+
model: "opus",
76+
permission: { edit: "deny", bash: "deny" },
77+
} as AgentFrontmatter,
78+
metadata: { name: "code-reviewer" },
79+
systemPrompt: "Review code carefully.",
80+
contexts: [],
81+
};
82+
83+
it("emits .opencode/agents/<name>.md", async () => {
84+
const result = await adapter.fromOAC(subagent);
85+
expect(result.success).toBe(true);
86+
expect(result.configs[0].fileName).toBe(".opencode/agents/code-reviewer.md");
87+
});
88+
89+
it("includes name + description + mode in frontmatter", async () => {
90+
const result = await adapter.fromOAC(subagent);
91+
const content = result.configs[0].content;
92+
expect(content).toMatch(/^---\n/);
93+
expect(content).toContain("name: code-reviewer");
94+
expect(content).toContain("description: Reviews code");
95+
expect(content).toContain("mode: subagent");
96+
expect(content).toContain("model: opus");
97+
});
98+
99+
it("renders permission block from granular permissions", async () => {
100+
const result = await adapter.fromOAC(subagent);
101+
const content = result.configs[0].content;
102+
expect(content).toContain("permission:");
103+
expect(content).toContain("edit: deny");
104+
expect(content).toContain("bash: deny");
105+
});
106+
107+
it("preserves system prompt body", async () => {
108+
const result = await adapter.fromOAC(subagent);
109+
expect(result.configs[0].content).toContain("Review code carefully.");
110+
});
111+
});
112+
113+
// ============================================================================
114+
// fromOAC — primary + tools fallback
115+
// ============================================================================
116+
117+
describe("fromOAC() — primary agent without explicit permission", () => {
118+
const primary: OpenAgent = {
119+
frontmatter: {
120+
name: "main",
121+
description: "Primary agent",
122+
mode: "primary",
123+
model: "sonnet",
124+
tools: { read: true, write: true, bash: false },
125+
} as AgentFrontmatter,
126+
metadata: { name: "main" },
127+
systemPrompt: "Primary loop.",
128+
contexts: [],
129+
};
130+
131+
it("emits both opencode.json and agent markdown", async () => {
132+
const result = await adapter.fromOAC(primary);
133+
const fileNames = result.configs.map((c) => c.fileName);
134+
expect(fileNames).toContain(".opencode/opencode.json");
135+
expect(fileNames).toContain(".opencode/agents/main.md");
136+
});
137+
138+
it("falls back to tools->permission mapping when permission absent", async () => {
139+
const result = await adapter.fromOAC(primary);
140+
const md = result.configs.find((c) => c.fileName.endsWith(".md"))!.content;
141+
expect(md).toContain("permission:");
142+
expect(md).toContain("edit: allow");
143+
expect(md).toContain("bash: deny");
144+
});
145+
146+
it("opencode.json includes $schema and model", async () => {
147+
const result = await adapter.fromOAC(primary);
148+
const json = JSON.parse(
149+
result.configs.find((c) => c.fileName.endsWith(".json"))!.content
150+
);
151+
expect(json.$schema).toBe("https://opencode.ai/config.json");
152+
expect(json.model).toBe("sonnet");
153+
});
154+
});
155+
156+
// ============================================================================
157+
// fromOAC — contexts + hooks
158+
// ============================================================================
159+
160+
describe("fromOAC() — contexts emit skills", () => {
161+
it("creates a SKILL.md per context", async () => {
162+
const agent: OpenAgent = {
163+
frontmatter: {
164+
name: "ctx-bearer",
165+
description: "Carries contexts",
166+
mode: "subagent",
167+
} as AgentFrontmatter,
168+
metadata: { name: "ctx-bearer" },
169+
systemPrompt: "",
170+
contexts: [
171+
{ path: "context/standards/code-quality.md", priority: "high", description: "Quality" },
172+
{ path: "context/security.md" },
173+
],
174+
};
175+
const result = await adapter.fromOAC(agent);
176+
const skillFiles = result.configs.filter((c) =>
177+
c.fileName.startsWith(".opencode/skills/")
178+
);
179+
expect(skillFiles.length).toBe(2);
180+
expect(skillFiles[0].fileName).toMatch(/SKILL\.md$/);
181+
});
182+
});
183+
184+
describe("fromOAC() — hooks emit plugin script", () => {
185+
it("emits a plugin/<name>-hooks.ts when hooks are present", async () => {
186+
const agent: OpenAgent = {
187+
frontmatter: {
188+
name: "hooked",
189+
description: "With hooks",
190+
mode: "subagent",
191+
hooks: [
192+
{
193+
event: "PreToolUse",
194+
commands: [{ type: "command", command: "echo hi" }],
195+
},
196+
],
197+
} as AgentFrontmatter,
198+
metadata: { name: "hooked" },
199+
systemPrompt: "",
200+
contexts: [],
201+
};
202+
const result = await adapter.fromOAC(agent);
203+
const plugin = result.configs.find((c) =>
204+
c.fileName.startsWith(".opencode/plugin/")
205+
);
206+
expect(plugin).toBeDefined();
207+
expect(plugin!.fileName).toBe(".opencode/plugin/hooked-hooks.ts");
208+
// PreToolUse maps to opencode's tool.execute.before event.
209+
expect(plugin!.content).toContain('"tool.execute.before"');
210+
});
211+
});
212+
213+
// ============================================================================
214+
// toOAC
215+
// ============================================================================
216+
217+
describe("toOAC() — parse opencode.json", () => {
218+
it("parses minimal opencode.json into a primary OpenAgent", async () => {
219+
const src = JSON.stringify({
220+
name: "primary",
221+
description: "primary agent",
222+
model: "sonnet",
223+
prompt: "do work",
224+
});
225+
const agent = await adapter.toOAC(src);
226+
expect(agent.frontmatter.mode).toBe("primary");
227+
expect(agent.frontmatter.name).toBe("primary");
228+
expect(agent.frontmatter.model).toBe("sonnet");
229+
expect(agent.systemPrompt).toBe("do work");
230+
});
231+
});
232+
233+
describe("toOAC() — parse opencode agent.md", () => {
234+
it("parses subagent frontmatter and body", async () => {
235+
const src = `---
236+
name: my-agent
237+
description: example
238+
mode: subagent
239+
model: haiku
240+
temperature: 0.4
241+
permission:
242+
edit: deny
243+
bash: ask
244+
skills: ["code-review", "context-discovery"]
245+
---
246+
247+
System prompt body.`;
248+
const agent = await adapter.toOAC(src);
249+
expect(agent.frontmatter.mode).toBe("subagent");
250+
expect(agent.frontmatter.model).toBe("haiku");
251+
expect(agent.frontmatter.temperature).toBe(0.4);
252+
expect(agent.frontmatter.permission).toEqual({ edit: "deny", bash: "ask" });
253+
expect(agent.frontmatter.skills).toEqual(["code-review", "context-discovery"]);
254+
expect(agent.systemPrompt).toBe("System prompt body.");
255+
});
256+
});
257+
258+
// ============================================================================
259+
// validateConversion
260+
// ============================================================================
261+
262+
describe("validateConversion()", () => {
263+
it("warns on missing name", () => {
264+
const agent: OpenAgent = {
265+
frontmatter: { name: "", description: "x", mode: "subagent" } as AgentFrontmatter,
266+
metadata: {},
267+
systemPrompt: "",
268+
contexts: [],
269+
};
270+
expect(adapter.validateConversion(agent).join(" ")).toMatch(/name/i);
271+
});
272+
273+
it("warns on maxSteps (unsupported)", () => {
274+
const agent: OpenAgent = {
275+
frontmatter: {
276+
name: "x",
277+
description: "x",
278+
mode: "subagent",
279+
maxSteps: 5,
280+
} as AgentFrontmatter,
281+
metadata: {},
282+
systemPrompt: "",
283+
contexts: [],
284+
};
285+
expect(adapter.validateConversion(agent).join(" ")).toMatch(/maxSteps/);
286+
});
287+
});
288+
});

0 commit comments

Comments
 (0)