|
|
@@ -1,25 +1,61 @@
|
|
|
-import { describe, it, expect, beforeEach } from "vitest";
|
|
|
-import { ClaudeAdapter } from "../../../src/adapters/ClaudeAdapter";
|
|
|
-import type {
|
|
|
- OpenAgent,
|
|
|
- AgentFrontmatter,
|
|
|
- HookDefinition,
|
|
|
- SkillReference,
|
|
|
-} from "../../../src/types";
|
|
|
-
|
|
|
/**
|
|
|
- * Unit tests for ClaudeAdapter with 80%+ coverage
|
|
|
+ * Unit tests for ClaudeAdapter — the `plugins/claude-code/` emitter.
|
|
|
+ *
|
|
|
+ * ## What changed, and why the old suite could not simply be edited
|
|
|
*
|
|
|
- * Test strategy:
|
|
|
- * 1. toOAC() - Parse Claude formats to OpenAgent
|
|
|
- * 2. fromOAC() - Convert OpenAgent to Claude formats
|
|
|
- * 3. getCapabilities() - Feature matrix validation
|
|
|
- * 4. validateConversion() - Validation and warnings
|
|
|
- * 5. Helper methods - Model/tool/hook/skill mapping
|
|
|
- * 6. Edge cases - Invalid input, missing fields, empty values
|
|
|
- * 7. Roundtrip - Data integrity checks
|
|
|
+ * This adapter used to emit `.claude/config.json` + `.claude/agents/*.md`. The `config.json`
|
|
|
+ * half was fabricated (Claude Code has no such agent-config file) and the real target is the
|
|
|
+ * plugin tree committed at `plugins/claude-code/`. Roughly half the old suite asserted the
|
|
|
+ * shape of a file that should never have existed, so those tests are gone rather than
|
|
|
+ * retargeted — keeping them would pin a format nothing reads.
|
|
|
+ *
|
|
|
+ * ## The one rule these tests exist to defend
|
|
|
+ *
|
|
|
+ * Claude Code's frontmatter has two flat lists and no scoping: no ordered globs, no `ask`,
|
|
|
+ * no last-match-wins. Canonical agents depend on all three. Emitting MORE permission than
|
|
|
+ * canonical specifies is the single unacceptable outcome, so every projection here is
|
|
|
+ * checked to fail CLOSED and to say out loud what it dropped. A silent widening is the bug
|
|
|
+ * class this file is aimed at — `PermissionMapper`'s permissive default (`hasAllow ||
|
|
|
+ * !hasDeny`) answers `bash: true` for a deny-all-then-allowlist block, which is exactly why
|
|
|
+ * the adapter routes through `core/Capabilities.ts` instead.
|
|
|
*/
|
|
|
|
|
|
+import { describe, it, expect, beforeEach } from "vitest";
|
|
|
+import { readFileSync } from "node:fs";
|
|
|
+import { ClaudeAdapter } from "../../../src/adapters/ClaudeAdapter";
|
|
|
+import { packagePath } from "../../support/pending.js";
|
|
|
+import type { OpenAgent, AgentFrontmatter, HookDefinition } from "../../../src/types";
|
|
|
+
|
|
|
+const FIXTURE_REVIEWER = packagePath("tests/golden/fixtures/fixture-reviewer.md");
|
|
|
+const FIXTURE_PLANNER = packagePath("tests/golden/fixtures/fixture-planner.md");
|
|
|
+
|
|
|
+function fixture(path: string): string {
|
|
|
+ return readFileSync(path, "utf-8");
|
|
|
+}
|
|
|
+
|
|
|
+/** A canonical agent file built around one permission block, for targeted projection tests. */
|
|
|
+function canonical(permission: string, extra = ""): string {
|
|
|
+ return `---
|
|
|
+name: ProbeAgent
|
|
|
+description: A probe agent.
|
|
|
+mode: subagent
|
|
|
+${extra}permission:
|
|
|
+${permission}
|
|
|
+oac:
|
|
|
+ id: probe-agent
|
|
|
+ name: ProbeAgent
|
|
|
+ category: subagents/test
|
|
|
+ type: subagent
|
|
|
+ targets:
|
|
|
+ - claude-code
|
|
|
+---
|
|
|
+
|
|
|
+# ProbeAgent
|
|
|
+
|
|
|
+Body.
|
|
|
+`;
|
|
|
+}
|
|
|
+
|
|
|
describe("ClaudeAdapter", () => {
|
|
|
let adapter: ClaudeAdapter;
|
|
|
|
|
|
@@ -40,1447 +76,833 @@ describe("ClaudeAdapter", () => {
|
|
|
expect(adapter.displayName).toBe("Claude Code");
|
|
|
});
|
|
|
|
|
|
- it("returns correct config path", () => {
|
|
|
- expect(adapter.getConfigPath()).toBe(".claude/");
|
|
|
+ it("returns the plugin tree as its config path, not .claude/", () => {
|
|
|
+ expect(adapter.getConfigPath()).toBe("plugins/claude-code/");
|
|
|
});
|
|
|
});
|
|
|
|
|
|
// ============================================================================
|
|
|
- // CAPABILITIES
|
|
|
+ // OUTPUT LAYOUT
|
|
|
// ============================================================================
|
|
|
|
|
|
- describe("getCapabilities()", () => {
|
|
|
- it("returns correct capabilities object", () => {
|
|
|
- const capabilities = adapter.getCapabilities();
|
|
|
+ describe("output layout", () => {
|
|
|
+ it("emits agents under plugins/claude-code/agents/", async () => {
|
|
|
+ const { path } = await adapter.fromCanonical(fixture(FIXTURE_REVIEWER));
|
|
|
|
|
|
- expect(capabilities.name).toBe("claude");
|
|
|
- expect(capabilities.displayName).toBe("Claude Code");
|
|
|
- expect(capabilities.supportsMultipleAgents).toBe(true);
|
|
|
- expect(capabilities.supportsSkills).toBe(true);
|
|
|
- expect(capabilities.supportsHooks).toBe(true);
|
|
|
- expect(capabilities.supportsGranularPermissions).toBe(false);
|
|
|
- expect(capabilities.supportsContexts).toBe(true);
|
|
|
- expect(capabilities.supportsCustomModels).toBe(true);
|
|
|
- expect(capabilities.supportsTemperature).toBe(false);
|
|
|
- expect(capabilities.supportsMaxSteps).toBe(false);
|
|
|
- expect(capabilities.configFormat).toBe("markdown");
|
|
|
- expect(capabilities.outputStructure).toBe("directory");
|
|
|
+ expect(path).toBe("plugins/claude-code/agents/fixture-reviewer.md");
|
|
|
});
|
|
|
|
|
|
- it("includes appropriate notes", () => {
|
|
|
- const capabilities = adapter.getCapabilities();
|
|
|
+ it("keys the emitted path on oac.id, not the authored display name", async () => {
|
|
|
+ // The canonical ids and the Claude Code filenames genuinely differ across the corpus
|
|
|
+ // (`contextscout` -> `context-scout.md`, `reviewer` -> `code-reviewer.md`). Only the
|
|
|
+ // id is stable identity, so resolving by `name:` would emit the wrong filename.
|
|
|
+ const { path, content } = await adapter.fromCanonical(fixture(FIXTURE_REVIEWER));
|
|
|
|
|
|
- expect(capabilities.notes).toBeDefined();
|
|
|
- expect(capabilities.notes?.length).toBeGreaterThan(0);
|
|
|
- expect(capabilities.notes?.some((n) => n.includes("permissions"))).toBe(
|
|
|
- true
|
|
|
- );
|
|
|
+ expect(path).toContain("fixture-reviewer.md"); // oac.id
|
|
|
+ expect(path).not.toContain("FixtureReviewer"); // frontmatter name
|
|
|
+ expect(content).toMatch(/^name: fixture-reviewer$/m);
|
|
|
});
|
|
|
- });
|
|
|
-
|
|
|
- // ============================================================================
|
|
|
- // toOAC() - PARSING CLAUDE CONFIG.JSON
|
|
|
- // ============================================================================
|
|
|
|
|
|
- describe("toOAC() - parsing config.json", () => {
|
|
|
- it("parses minimal config.json", async () => {
|
|
|
- const source = JSON.stringify({
|
|
|
- name: "TestAgent",
|
|
|
- description: "Test description",
|
|
|
- systemPrompt: "You are helpful",
|
|
|
+ it("emits no .claude/ path from any conversion", async () => {
|
|
|
+ const canonicalResult = await adapter.fromCanonical(fixture(FIXTURE_PLANNER));
|
|
|
+ const oacResult = await adapter.fromOAC({
|
|
|
+ frontmatter: { name: "Agent", description: "Test", mode: "primary" },
|
|
|
+ metadata: { name: "Agent", category: "core", type: "agent" },
|
|
|
+ systemPrompt: "Prompt",
|
|
|
+ contexts: [{ path: "context/a.md", description: "A" }],
|
|
|
});
|
|
|
|
|
|
- const result = await adapter.toOAC(source);
|
|
|
-
|
|
|
- expect(result.frontmatter.name).toBe("TestAgent");
|
|
|
- expect(result.frontmatter.description).toBe("Test description");
|
|
|
- expect(result.systemPrompt).toBe("You are helpful");
|
|
|
- expect(result.frontmatter.mode).toBe("primary");
|
|
|
+ expect(canonicalResult.path).not.toContain(".claude/");
|
|
|
+ for (const config of oacResult.configs) {
|
|
|
+ expect(config.fileName, `${config.fileName} still targets the old layout`).not.toContain(
|
|
|
+ ".claude/"
|
|
|
+ );
|
|
|
+ expect(config.fileName).toMatch(/^plugins\/claude-code\//);
|
|
|
+ }
|
|
|
});
|
|
|
|
|
|
- it("parses config with model", async () => {
|
|
|
- const source = JSON.stringify({
|
|
|
- name: "Agent",
|
|
|
- description: "Test",
|
|
|
- model: "claude-sonnet-4-20250514",
|
|
|
+ it("never emits a config.json", async () => {
|
|
|
+ const result = await adapter.fromOAC({
|
|
|
+ frontmatter: { name: "Agent", description: "Test", mode: "primary" },
|
|
|
+ metadata: { name: "Agent", category: "core", type: "agent" },
|
|
|
systemPrompt: "Prompt",
|
|
|
+ contexts: [],
|
|
|
});
|
|
|
|
|
|
- const result = await adapter.toOAC(source);
|
|
|
-
|
|
|
- expect(result.frontmatter.model).toBe("claude-sonnet-4");
|
|
|
+ expect(result.configs.map((c) => c.fileName)).toEqual([
|
|
|
+ "plugins/claude-code/agents/Agent.md",
|
|
|
+ ]);
|
|
|
});
|
|
|
|
|
|
- it("parses config with tools array", async () => {
|
|
|
- const source = JSON.stringify({
|
|
|
- name: "Agent",
|
|
|
- description: "Test",
|
|
|
- tools: ["Read", "Write", "Bash"],
|
|
|
- systemPrompt: "Prompt",
|
|
|
- });
|
|
|
-
|
|
|
- const result = await adapter.toOAC(source);
|
|
|
+ it("emits one agent file for a primary agent, same as a subagent", async () => {
|
|
|
+ // The old primary/subagent split existed only to choose between config.json and an
|
|
|
+ // agent file. With config.json gone there is exactly one shape.
|
|
|
+ const primary = await adapter.fromCanonical(fixture(FIXTURE_PLANNER)); // mode: primary
|
|
|
|
|
|
- expect(result.frontmatter.tools).toEqual({
|
|
|
- read: true,
|
|
|
- write: true,
|
|
|
- bash: true,
|
|
|
- });
|
|
|
+ expect(primary.path).toBe("plugins/claude-code/agents/fixture-planner.md");
|
|
|
+ expect(primary.content).toMatch(/^---\nname: fixture-planner$/m);
|
|
|
});
|
|
|
+ });
|
|
|
|
|
|
- it("parses config with tools string", async () => {
|
|
|
- const source = JSON.stringify({
|
|
|
- name: "Agent",
|
|
|
- description: "Test",
|
|
|
- tools: "Read, Write, Edit",
|
|
|
- systemPrompt: "Prompt",
|
|
|
- });
|
|
|
+ // ============================================================================
|
|
|
+ // FRONTMATTER SHAPE
|
|
|
+ // ============================================================================
|
|
|
|
|
|
- const result = await adapter.toOAC(source);
|
|
|
+ describe("frontmatter shape", () => {
|
|
|
+ it("emits keys in the committed order: name, description, tools, disallowedTools, model", async () => {
|
|
|
+ const { content } = await adapter.fromCanonical(fixture(FIXTURE_REVIEWER));
|
|
|
+ const keys = content
|
|
|
+ .split("---")[1]!
|
|
|
+ .trim()
|
|
|
+ .split("\n")
|
|
|
+ .map((line) => line.split(":")[0]);
|
|
|
|
|
|
- expect(result.frontmatter.tools).toEqual({
|
|
|
- read: true,
|
|
|
- write: true,
|
|
|
- edit: true,
|
|
|
- });
|
|
|
+ expect(keys).toEqual(["name", "description", "tools", "disallowedTools", "model"]);
|
|
|
});
|
|
|
|
|
|
- it("parses config with skills", async () => {
|
|
|
- const source = JSON.stringify({
|
|
|
- name: "Agent",
|
|
|
- description: "Test",
|
|
|
- skills: ["skill1", "skill2"],
|
|
|
- systemPrompt: "Prompt",
|
|
|
- });
|
|
|
+ it("reproduces the committed frontmatter shape byte-for-byte", async () => {
|
|
|
+ const { content } = await adapter.fromCanonical(fixture(FIXTURE_REVIEWER));
|
|
|
|
|
|
- const result = await adapter.toOAC(source);
|
|
|
+ expect(content.split("---\n\n")[0]).toBe(
|
|
|
+ `---\nname: fixture-reviewer\n` +
|
|
|
+ `description: Reviews code for correctness. A golden-file fixture, not a shipped agent.\n` +
|
|
|
+ `tools: Read, Glob, Grep\n` +
|
|
|
+ `disallowedTools: Write, Edit, Bash, Task\n` +
|
|
|
+ `model: haiku\n`
|
|
|
+ );
|
|
|
+ });
|
|
|
|
|
|
- expect(result.frontmatter.skills).toEqual(["skill1", "skill2"]);
|
|
|
+ it("passes the model through unmapped", async () => {
|
|
|
+ // The committed corpus uses Claude Code's own aliases (`sonnet`, `haiku`). Expanding
|
|
|
+ // them to dated ids (`claude-sonnet-4-20250514`) would break every committed agent.
|
|
|
+ const { content } = await adapter.fromCanonical(fixture(FIXTURE_PLANNER));
|
|
|
+
|
|
|
+ expect(content).toMatch(/^model: sonnet$/m);
|
|
|
});
|
|
|
|
|
|
- it("parses config with hooks", async () => {
|
|
|
- const source = JSON.stringify({
|
|
|
- name: "Agent",
|
|
|
- description: "Test",
|
|
|
- hooks: {
|
|
|
- PreToolUse: [
|
|
|
- {
|
|
|
- matcher: "*.txt",
|
|
|
- hooks: [{ type: "command", command: "validate" }],
|
|
|
- },
|
|
|
- ],
|
|
|
- },
|
|
|
- systemPrompt: "Prompt",
|
|
|
- });
|
|
|
+ it("omits model when the source declares none", async () => {
|
|
|
+ const { content } = await adapter.fromCanonical(canonical(` read:\n "*": "allow"\n`));
|
|
|
|
|
|
- const result = await adapter.toOAC(source);
|
|
|
+ expect(content).not.toMatch(/^model:/m);
|
|
|
+ });
|
|
|
|
|
|
- expect(result.frontmatter.hooks).toBeDefined();
|
|
|
- expect(result.frontmatter.hooks?.length).toBe(1);
|
|
|
- expect(result.frontmatter.hooks?.[0].event).toBe("PreToolUse");
|
|
|
+ it("omits an empty tools list rather than emitting a bare key", async () => {
|
|
|
+ // `tools:` with no value means something different to Claude Code than an absent key.
|
|
|
+ const { content } = await adapter.fromCanonical(canonical(` bash:\n "*": "deny"\n`));
|
|
|
+
|
|
|
+ expect(content).not.toMatch(/^tools:\s*$/m);
|
|
|
+ expect(content).toMatch(/^disallowedTools: Bash$/m);
|
|
|
});
|
|
|
|
|
|
- it("handles missing optional fields gracefully", async () => {
|
|
|
- const source = JSON.stringify({
|
|
|
- name: "MinimalAgent",
|
|
|
- description: "Minimal",
|
|
|
- });
|
|
|
+ it("omits an empty disallowedTools list", async () => {
|
|
|
+ const { content } = await adapter.fromCanonical(canonical(` read:\n "*": "allow"\n`));
|
|
|
|
|
|
- const result = await adapter.toOAC(source);
|
|
|
+ expect(content).toMatch(/^tools: Read$/m);
|
|
|
+ expect(content).not.toMatch(/^disallowedTools:/m);
|
|
|
+ });
|
|
|
|
|
|
- expect(result.frontmatter.name).toBe("MinimalAgent");
|
|
|
- expect(result.systemPrompt).toBe("");
|
|
|
- expect(result.frontmatter.tools).toBeUndefined();
|
|
|
- expect(result.frontmatter.skills).toBeUndefined();
|
|
|
+ it("preserves the body verbatim after the frontmatter", async () => {
|
|
|
+ const { content } = await adapter.fromCanonical(fixture(FIXTURE_REVIEWER));
|
|
|
+
|
|
|
+ expect(content).toContain("# FixtureReviewer");
|
|
|
+ expect(content).toContain("- Report findings, do not fix them.");
|
|
|
+ expect(content.endsWith("- Report findings, do not fix them.\n")).toBe(true);
|
|
|
});
|
|
|
|
|
|
- it("parses invalid JSON as markdown (subagent fallback)", async () => {
|
|
|
- const source = "not valid json";
|
|
|
+ it("renders a multi-line description as a YAML block scalar", async () => {
|
|
|
+ // The committed agents carry multi-line `description: |` blocks with <example> tags.
|
|
|
+ // A naive `key: "value"` would emit a broken single line.
|
|
|
+ const source = canonical(` read:\n "*": "allow"\n`).replace(
|
|
|
+ "description: A probe agent.",
|
|
|
+ 'description: |\n First line.\n user: "quoted colon"\n'
|
|
|
+ );
|
|
|
|
|
|
- const result = await adapter.toOAC(source);
|
|
|
+ const { content } = await adapter.fromCanonical(source);
|
|
|
|
|
|
- // Falls back to markdown parsing
|
|
|
- expect(result.frontmatter.mode).toBe("subagent");
|
|
|
- expect(result.systemPrompt).toBe("not valid json");
|
|
|
+ expect(content).toContain('description: |\n First line.\n user: "quoted colon"\n');
|
|
|
});
|
|
|
|
|
|
- it("parses non-object JSON string as markdown (subagent fallback)", async () => {
|
|
|
- const source = JSON.stringify("just a string");
|
|
|
+ it("quotes a description that would otherwise be ambiguous YAML", async () => {
|
|
|
+ const source = canonical(` read:\n "*": "allow"\n`).replace(
|
|
|
+ "description: A probe agent.",
|
|
|
+ 'description: "*starts with a star"'
|
|
|
+ );
|
|
|
|
|
|
- const result = await adapter.toOAC(source);
|
|
|
+ const { content } = await adapter.fromCanonical(source);
|
|
|
|
|
|
- // Falls back to markdown parsing
|
|
|
- expect(result.frontmatter.mode).toBe("subagent");
|
|
|
+ expect(content).toMatch(/^description: '\*starts with a star'$/m);
|
|
|
});
|
|
|
});
|
|
|
|
|
|
// ============================================================================
|
|
|
- // toOAC() - PARSING CLAUDE AGENT.MD (SUBAGENTS)
|
|
|
- // ============================================================================
|
|
|
-
|
|
|
- describe("toOAC() - parsing agent.md with YAML frontmatter", () => {
|
|
|
- it("parses agent.md with minimal frontmatter", async () => {
|
|
|
- const source = `---
|
|
|
-name: SubAgent
|
|
|
-description: A subagent
|
|
|
----
|
|
|
-
|
|
|
-This is the system prompt for the agent.`;
|
|
|
-
|
|
|
- const result = await adapter.toOAC(source);
|
|
|
+ // TOOL ORDERING
|
|
|
+ // ============================================================================
|
|
|
+
|
|
|
+ describe("tool ordering", () => {
|
|
|
+ it("emits tools in the canonical Read, Write, Edit, Glob, Grep, Bash, WebFetch, Task order", async () => {
|
|
|
+ // Recovered from the 7 committed agents: all 10 of their lists fit this order and it
|
|
|
+ // is the only total order that does. Alphabetical is refuted by context-manager.md
|
|
|
+ // (`Read, Write, Glob, Grep, Bash`); so is ToolAccessSchema field order.
|
|
|
+ const { content } = await adapter.fromCanonical(
|
|
|
+ canonical(
|
|
|
+ ` task:\n "*": "allow"\n` +
|
|
|
+ ` bash:\n "*": "allow"\n` +
|
|
|
+ ` grep:\n "*": "allow"\n` +
|
|
|
+ ` glob:\n "*": "allow"\n` +
|
|
|
+ ` edit:\n "*": "allow"\n` +
|
|
|
+ ` write:\n "*": "allow"\n` +
|
|
|
+ ` read:\n "*": "allow"\n` +
|
|
|
+ ` webfetch:\n "*": "allow"\n`
|
|
|
+ )
|
|
|
+ );
|
|
|
|
|
|
- expect(result.frontmatter.name).toBe("SubAgent");
|
|
|
- expect(result.frontmatter.description).toBe("A subagent");
|
|
|
- expect(result.frontmatter.mode).toBe("subagent");
|
|
|
- expect(result.systemPrompt).toBe("This is the system prompt for the agent.");
|
|
|
+ expect(content).toMatch(
|
|
|
+ /^tools: Read, Write, Edit, Glob, Grep, Bash, WebFetch, Task$/m
|
|
|
+ );
|
|
|
});
|
|
|
|
|
|
- it("parses agent.md with model in frontmatter", async () => {
|
|
|
- const source = `---
|
|
|
-name: Agent
|
|
|
-description: Test
|
|
|
-model: claude-opus-4
|
|
|
----
|
|
|
+ it("orders disallowedTools by the same rule", async () => {
|
|
|
+ const { content } = await adapter.fromCanonical(
|
|
|
+ canonical(
|
|
|
+ ` task:\n "*": "deny"\n` +
|
|
|
+ ` bash:\n "*": "deny"\n` +
|
|
|
+ ` edit:\n "*": "deny"\n` +
|
|
|
+ ` write:\n "*": "deny"\n`
|
|
|
+ )
|
|
|
+ );
|
|
|
|
|
|
-Prompt`;
|
|
|
+ expect(content).toMatch(/^disallowedTools: Write, Edit, Bash, Task$/m);
|
|
|
+ });
|
|
|
|
|
|
- const result = await adapter.toOAC(source);
|
|
|
+ it("does not emit a tool for a capability the source never mentions", async () => {
|
|
|
+ // Ratified rule (02 §1.2.5 case 1): an absent capability means "the target's own
|
|
|
+ // default", so naming it in either list would invent an intent the author never had.
|
|
|
+ const { content } = await adapter.fromCanonical(canonical(` read:\n "*": "allow"\n`));
|
|
|
|
|
|
- expect(result.frontmatter.model).toBe("claude-opus-4");
|
|
|
+ for (const tool of ["Write", "Edit", "Glob", "Grep", "Bash", "WebFetch", "Task"]) {
|
|
|
+ expect(content, `${tool} was invented from silence`).not.toContain(tool);
|
|
|
+ }
|
|
|
});
|
|
|
+ });
|
|
|
|
|
|
- it("parses agent.md with tools array in frontmatter", async () => {
|
|
|
- const source = `---
|
|
|
-name: Agent
|
|
|
-description: Test
|
|
|
-tools: ["Read", "Write", "Bash"]
|
|
|
----
|
|
|
-
|
|
|
-Prompt`;
|
|
|
+ // ============================================================================
|
|
|
+ // PERMISSION PROJECTION — fails closed
|
|
|
+ // ============================================================================
|
|
|
|
|
|
- const result = await adapter.toOAC(source);
|
|
|
+ describe("permission projection", () => {
|
|
|
+ it("fails closed on a deny-all-then-allowlist bash block", async () => {
|
|
|
+ // The live shape: `bash: {"*": deny, "git log*": allow}`. Claude Code has no ordered
|
|
|
+ // -glob equivalent. Answering `tools: Bash` because "an allow rule exists" would hand
|
|
|
+ // it unrestricted shell — the precise failure PermissionMapper's permissive default
|
|
|
+ // produces, and the reason this adapter does not use it.
|
|
|
+ const { content } = await adapter.fromCanonical(fixture(FIXTURE_PLANNER));
|
|
|
|
|
|
- expect(result.frontmatter.tools).toEqual({
|
|
|
- read: true,
|
|
|
- write: true,
|
|
|
- bash: true,
|
|
|
- });
|
|
|
+ expect(content).toMatch(/^disallowedTools:.*\bBash\b/m);
|
|
|
+ expect(content).not.toMatch(/^tools:.*\bBash\b/m);
|
|
|
});
|
|
|
|
|
|
- it("parses agent.md with skills in frontmatter", async () => {
|
|
|
- const source = `---
|
|
|
-name: Agent
|
|
|
-description: Test
|
|
|
-skills: ["skill1", "skill2"]
|
|
|
----
|
|
|
+ it("degrades 'ask' to deny, never to allow", async () => {
|
|
|
+ const { content } = await adapter.fromCanonical(canonical(` bash:\n "*": "ask"\n`));
|
|
|
|
|
|
-Prompt`;
|
|
|
+ expect(content).toMatch(/^disallowedTools: Bash$/m);
|
|
|
+ expect(content).not.toMatch(/^tools:.*Bash/m);
|
|
|
+ });
|
|
|
|
|
|
- const result = await adapter.toOAC(source);
|
|
|
+ it("does not treat an allow-with-exceptions as a plain allow", async () => {
|
|
|
+ const { content } = await adapter.fromCanonical(
|
|
|
+ canonical(` edit:\n "*": "allow"\n "**/*.env*": "deny"\n`)
|
|
|
+ );
|
|
|
|
|
|
- expect(result.frontmatter.skills).toEqual(["skill1", "skill2"]);
|
|
|
+ expect(content).toMatch(/^disallowedTools: Edit$/m);
|
|
|
});
|
|
|
|
|
|
- it("handles agent.md without frontmatter as markdown content", async () => {
|
|
|
- const source = "No frontmatter here, just markdown content";
|
|
|
-
|
|
|
- const result = await adapter.toOAC(source);
|
|
|
+ it("carries a provably uniform allow through as a grant", async () => {
|
|
|
+ const { content, warnings } = await adapter.fromCanonical(
|
|
|
+ canonical(` read:\n "*": "allow"\n`)
|
|
|
+ );
|
|
|
|
|
|
- expect(result.systemPrompt).toBe("No frontmatter here, just markdown content");
|
|
|
- expect(result.frontmatter.mode).toBe("subagent");
|
|
|
+ expect(content).toMatch(/^tools: Read$/m);
|
|
|
+ expect(warnings).toEqual([]);
|
|
|
});
|
|
|
|
|
|
- it("preserves multiline system prompt", async () => {
|
|
|
- const source = `---
|
|
|
-name: Agent
|
|
|
-description: Test
|
|
|
----
|
|
|
+ it("carries a provably uniform deny through as a denial, silently", async () => {
|
|
|
+ // An exact projection loses nothing, so it must not warn — warnings mean loss, and
|
|
|
+ // noise here would train readers to ignore the real ones.
|
|
|
+ const { content, warnings } = await adapter.fromCanonical(
|
|
|
+ canonical(` bash:\n "*": "deny"\n`)
|
|
|
+ );
|
|
|
|
|
|
-You are a helpful assistant.
|
|
|
-You should be friendly and professional.
|
|
|
-Always provide detailed responses.`;
|
|
|
+ expect(content).toMatch(/^disallowedTools: Bash$/m);
|
|
|
+ expect(warnings).toEqual([]);
|
|
|
+ });
|
|
|
+
|
|
|
+ it("never grants a tool whose rules contain any deny", async () => {
|
|
|
+ // Property check over every mixed shape in the live corpus.
|
|
|
+ const shapes = [
|
|
|
+ ` bash:\n "*": "deny"\n "git log*": "allow"\n`,
|
|
|
+ ` bash:\n "git log*": "allow"\n "*": "deny"\n`,
|
|
|
+ ` edit:\n "**/*.env*": "deny"\n "**/*.key": "deny"\n`,
|
|
|
+ ` read:\n "**/*": "deny"\n ".tmp/**": "allow"\n`,
|
|
|
+ ];
|
|
|
|
|
|
- const result = await adapter.toOAC(source);
|
|
|
+ for (const shape of shapes) {
|
|
|
+ const { content } = await adapter.fromCanonical(canonical(shape));
|
|
|
+ const tools = /^tools: (.*)$/m.exec(content)?.[1] ?? "";
|
|
|
|
|
|
- expect(result.systemPrompt).toContain("You are a helpful assistant");
|
|
|
- expect(result.systemPrompt).toContain("Always provide detailed responses");
|
|
|
+ expect(tools, `${shape} leaked a grant`).toBe("");
|
|
|
+ }
|
|
|
});
|
|
|
});
|
|
|
|
|
|
// ============================================================================
|
|
|
- // fromOAC() - CONVERTING TO CLAUDE CONFIG.JSON
|
|
|
+ // WARNINGS — one per lossy projection
|
|
|
// ============================================================================
|
|
|
|
|
|
- describe("fromOAC() - converting to config.json", () => {
|
|
|
- const createOpenAgent = (overrides?: Partial<OpenAgent>): OpenAgent => ({
|
|
|
- frontmatter: {
|
|
|
- name: "TestAgent",
|
|
|
- description: "Test agent",
|
|
|
- mode: "primary",
|
|
|
- model: "claude-sonnet-4",
|
|
|
- tools: { read: true, write: true },
|
|
|
- ...overrides?.frontmatter,
|
|
|
- },
|
|
|
- metadata: {
|
|
|
- name: "TestAgent",
|
|
|
- category: "core",
|
|
|
- type: "agent",
|
|
|
- },
|
|
|
- systemPrompt: "You are helpful",
|
|
|
- contexts: [],
|
|
|
- ...overrides,
|
|
|
+ describe("warnings", () => {
|
|
|
+ it("emits exactly one warning for a single unrepresentable capability", async () => {
|
|
|
+ const { warnings } = await adapter.fromCanonical(
|
|
|
+ canonical(` bash:\n "*": "deny"\n "git log*": "allow"\n`)
|
|
|
+ );
|
|
|
+
|
|
|
+ expect(warnings).toHaveLength(1);
|
|
|
+ expect(warnings[0]).toMatch(/bash/i);
|
|
|
+ expect(warnings[0]).toMatch(/fail-closed/);
|
|
|
});
|
|
|
|
|
|
- it("converts primary agent to config.json", async () => {
|
|
|
- const agent = createOpenAgent();
|
|
|
+ it("counts one warning per lossy capability, and none for the lossless ones", async () => {
|
|
|
+ // read/glob are exact; bash and edit are not. Two losses, two warnings.
|
|
|
+ const { warnings } = await adapter.fromCanonical(
|
|
|
+ canonical(
|
|
|
+ ` read:\n "*": "allow"\n` +
|
|
|
+ ` glob:\n "*": "allow"\n` +
|
|
|
+ ` bash:\n "*": "deny"\n "git log*": "allow"\n` +
|
|
|
+ ` edit:\n "*": "allow"\n "**/*.key": "deny"\n`
|
|
|
+ )
|
|
|
+ );
|
|
|
|
|
|
- const result = await adapter.fromOAC(agent);
|
|
|
+ expect(warnings).toHaveLength(2);
|
|
|
+ expect(warnings.filter((w) => /'bash'/.test(w))).toHaveLength(1);
|
|
|
+ expect(warnings.filter((w) => /'edit'/.test(w))).toHaveLength(1);
|
|
|
+ });
|
|
|
|
|
|
- expect(result.success).toBe(true);
|
|
|
- expect(result.configs).toHaveLength(1);
|
|
|
- expect(result.configs[0].fileName).toBe(".claude/config.json");
|
|
|
+ it("adds a second warning naming 'ask' when a mixed list contains one", async () => {
|
|
|
+ // test-engineer's real block: a test-runner allowlist plus `rm -rf *: ask`.
|
|
|
+ const { warnings } = await adapter.fromCanonical(
|
|
|
+ canonical(` bash:\n "npx vitest *": "allow"\n "rm -rf *": "ask"\n "*": "deny"\n`)
|
|
|
+ );
|
|
|
|
|
|
- const config = JSON.parse(result.configs[0].content);
|
|
|
- expect(config.name).toBe("TestAgent");
|
|
|
- expect(config.description).toBe("Test agent");
|
|
|
- expect(config.model).toBe("claude-sonnet-4-20250514");
|
|
|
+ expect(warnings).toHaveLength(2);
|
|
|
+ expect(warnings.some((w) => /cannot express/.test(w) && /ask/.test(w))).toBe(true);
|
|
|
});
|
|
|
|
|
|
- it("maps tools correctly in config.json", async () => {
|
|
|
- const agent = createOpenAgent({
|
|
|
- frontmatter: {
|
|
|
- tools: { read: true, write: true, bash: true },
|
|
|
- },
|
|
|
- });
|
|
|
-
|
|
|
- const result = await adapter.fromOAC(agent);
|
|
|
- const config = JSON.parse(result.configs[0].content);
|
|
|
+ it("warns when a rule list has no recoverable default", async () => {
|
|
|
+ // context-manager's real `write` block: allow + deny with no "*" rule.
|
|
|
+ const { warnings } = await adapter.fromCanonical(
|
|
|
+ canonical(
|
|
|
+ ` write:\n ".opencode/context/**/*.md": "allow"\n "**/*.env*": "deny"\n`
|
|
|
+ )
|
|
|
+ );
|
|
|
|
|
|
- expect(config.tools).toEqual(["Read", "Write", "Bash"]);
|
|
|
+ expect(warnings).toHaveLength(2);
|
|
|
+ expect(warnings.some((w) => /ambiguous/.test(w))).toBe(true);
|
|
|
});
|
|
|
|
|
|
- it("includes skills in config.json", async () => {
|
|
|
- const agent = createOpenAgent({
|
|
|
- frontmatter: {
|
|
|
- skills: ["skill1", "skill2"],
|
|
|
- },
|
|
|
- });
|
|
|
-
|
|
|
- const result = await adapter.fromOAC(agent);
|
|
|
- const config = JSON.parse(result.configs[0].content);
|
|
|
+ it("warns when a capability has no Claude Code tool at all", async () => {
|
|
|
+ // externalscout's real `skill` block restricts which skills it may invoke. Claude Code
|
|
|
+ // cannot express that; dropping it silently is the widening this suite guards against.
|
|
|
+ const { warnings } = await adapter.fromCanonical(
|
|
|
+ canonical(` skill:\n "*": "deny"\n "*context7*": "allow"\n`)
|
|
|
+ );
|
|
|
|
|
|
- expect(config.skills).toEqual(["skill1", "skill2"]);
|
|
|
+ expect(warnings).toHaveLength(1);
|
|
|
+ expect(warnings[0]).toMatch(/'skill' has no Claude Code tool/);
|
|
|
});
|
|
|
|
|
|
- it("includes hooks in config.json", async () => {
|
|
|
- const hook: HookDefinition = {
|
|
|
- event: "PreToolUse",
|
|
|
- matchers: ["*.txt"],
|
|
|
- commands: [{ type: "command", command: "validate" }],
|
|
|
- };
|
|
|
+ it("warns that temperature and maxSteps cannot be carried", async () => {
|
|
|
+ const { warnings } = await adapter.fromCanonical(
|
|
|
+ canonical(` read:\n "*": "allow"\n`, "temperature: 0.1\nmaxSteps: 10\n")
|
|
|
+ );
|
|
|
|
|
|
- const agent = createOpenAgent({
|
|
|
- frontmatter: {
|
|
|
- hooks: [hook],
|
|
|
- },
|
|
|
- });
|
|
|
+ expect(warnings).toHaveLength(2);
|
|
|
+ expect(warnings.some((w) => w.includes("temperature"))).toBe(true);
|
|
|
+ expect(warnings.some((w) => w.includes("maxSteps"))).toBe(true);
|
|
|
+ });
|
|
|
|
|
|
- const result = await adapter.fromOAC(agent);
|
|
|
- const config = JSON.parse(result.configs[0].content);
|
|
|
+ it("reports no permission loss for an agent whose every capability projects exactly", async () => {
|
|
|
+ // fixture-reviewer's block is uniform-per-capability, so nothing about its permissions
|
|
|
+ // is lost. Its `temperature: 0.1` still is — and that one warning is the whole list.
|
|
|
+ const { warnings } = await adapter.fromCanonical(fixture(FIXTURE_REVIEWER));
|
|
|
|
|
|
- expect(config.hooks).toBeDefined();
|
|
|
- expect(config.hooks.PreToolUse).toBeDefined();
|
|
|
+ expect(warnings).toHaveLength(1);
|
|
|
+ expect(warnings[0]).toContain("temperature");
|
|
|
});
|
|
|
+ });
|
|
|
|
|
|
- it("warns when temperature is set (unsupported)", async () => {
|
|
|
- const agent = createOpenAgent({
|
|
|
- frontmatter: {
|
|
|
- temperature: 0.7,
|
|
|
- },
|
|
|
- });
|
|
|
+ // ============================================================================
|
|
|
+ // DETERMINISM
|
|
|
+ // ============================================================================
|
|
|
|
|
|
- const result = await adapter.fromOAC(agent);
|
|
|
+ describe("determinism", () => {
|
|
|
+ it("emits identical bytes regardless of the source's key order", async () => {
|
|
|
+ const rules = {
|
|
|
+ read: ` read:\n "*": "allow"\n`,
|
|
|
+ bash: ` bash:\n "*": "deny"\n`,
|
|
|
+ write: ` write:\n "*": "deny"\n`,
|
|
|
+ };
|
|
|
|
|
|
- expect(result.warnings.some((w) => w.includes("temperature"))).toBe(true);
|
|
|
- });
|
|
|
+ const forward = await adapter.fromCanonical(
|
|
|
+ canonical(rules.read + rules.bash + rules.write)
|
|
|
+ );
|
|
|
+ const reversed = await adapter.fromCanonical(
|
|
|
+ canonical(rules.write + rules.bash + rules.read)
|
|
|
+ );
|
|
|
|
|
|
- it("warns when maxSteps is set (unsupported)", async () => {
|
|
|
- const agent = createOpenAgent({
|
|
|
- frontmatter: {
|
|
|
- maxSteps: 10,
|
|
|
- },
|
|
|
- });
|
|
|
+ expect(reversed.content).toBe(forward.content);
|
|
|
+ });
|
|
|
|
|
|
- const result = await adapter.fromOAC(agent);
|
|
|
+ it("emits identical bytes across separate adapter instances", async () => {
|
|
|
+ const source = fixture(FIXTURE_REVIEWER);
|
|
|
|
|
|
- expect(result.warnings.some((w) => w.includes("maxSteps"))).toBe(true);
|
|
|
+ expect((await new ClaudeAdapter().fromCanonical(source)).content).toBe(
|
|
|
+ (await new ClaudeAdapter().fromCanonical(source)).content
|
|
|
+ );
|
|
|
});
|
|
|
+ });
|
|
|
|
|
|
- it("includes validationWarnings in result", async () => {
|
|
|
- const agent = createOpenAgent({
|
|
|
- frontmatter: {
|
|
|
- name: "",
|
|
|
- description: "",
|
|
|
- },
|
|
|
- });
|
|
|
+ // ============================================================================
|
|
|
+ // INPUT VALIDATION
|
|
|
+ // ============================================================================
|
|
|
|
|
|
- const result = await adapter.fromOAC(agent);
|
|
|
+ describe("input validation", () => {
|
|
|
+ it("throws a named error when the source lacks an oac: block", async () => {
|
|
|
+ const source = `---\nname: X\ndescription: Y\nmode: subagent\n---\n\nBody\n`;
|
|
|
|
|
|
- expect(result.warnings.length).toBeGreaterThan(0);
|
|
|
+ await expect(adapter.fromCanonical(source)).rejects.toThrow(/not a canonical agent file/);
|
|
|
});
|
|
|
|
|
|
- it("handles mixed permission rules gracefully", async () => {
|
|
|
- const agent = createOpenAgent({
|
|
|
- frontmatter: {
|
|
|
- permission: {
|
|
|
- read: "allow",
|
|
|
- write: "ask",
|
|
|
- bash: "deny",
|
|
|
- },
|
|
|
- },
|
|
|
- });
|
|
|
-
|
|
|
- const result = await adapter.fromOAC(agent);
|
|
|
+ it("names the offending field when the oac: block is malformed", async () => {
|
|
|
+ const source = canonical(` read:\n "*": "allow"\n`).replace(
|
|
|
+ "id: probe-agent",
|
|
|
+ "id: Probe_Agent"
|
|
|
+ );
|
|
|
|
|
|
- // Should include some form of warning or degradation
|
|
|
- expect(result.warnings.length).toBeGreaterThanOrEqual(0);
|
|
|
- expect(result.success).toBe(true);
|
|
|
+ await expect(adapter.fromCanonical(source)).rejects.toThrow(/oac\.id/);
|
|
|
});
|
|
|
});
|
|
|
|
|
|
// ============================================================================
|
|
|
- // fromOAC() - CONVERTING TO CLAUDE AGENT.MD (SUBAGENTS)
|
|
|
+ // CAPABILITIES
|
|
|
// ============================================================================
|
|
|
|
|
|
- describe("fromOAC() - converting to agent.md for subagents", () => {
|
|
|
- const createSubagent = (overrides?: Partial<OpenAgent>): OpenAgent => ({
|
|
|
- frontmatter: {
|
|
|
- name: "CodeAnalyzer",
|
|
|
- description: "Analyzes code",
|
|
|
- mode: "subagent",
|
|
|
- model: "claude-sonnet-4",
|
|
|
- ...overrides?.frontmatter,
|
|
|
- },
|
|
|
- metadata: {
|
|
|
- name: "CodeAnalyzer",
|
|
|
- category: "specialist",
|
|
|
- type: "subagent",
|
|
|
- },
|
|
|
- systemPrompt: "Analyze code quality",
|
|
|
- contexts: [],
|
|
|
- ...overrides,
|
|
|
- });
|
|
|
+ describe("getCapabilities()", () => {
|
|
|
+ it("returns correct capabilities object", () => {
|
|
|
+ const capabilities = adapter.getCapabilities();
|
|
|
|
|
|
- it("converts subagent to agent.md file", async () => {
|
|
|
- const agent = createSubagent();
|
|
|
+ expect(capabilities.name).toBe("claude");
|
|
|
+ expect(capabilities.displayName).toBe("Claude Code");
|
|
|
+ expect(capabilities.supportsMultipleAgents).toBe(true);
|
|
|
+ expect(capabilities.supportsSkills).toBe(true);
|
|
|
+ expect(capabilities.supportsHooks).toBe(true);
|
|
|
+ expect(capabilities.supportsGranularPermissions).toBe(false);
|
|
|
+ expect(capabilities.supportsContexts).toBe(true);
|
|
|
+ expect(capabilities.supportsCustomModels).toBe(true);
|
|
|
+ expect(capabilities.supportsTemperature).toBe(false);
|
|
|
+ expect(capabilities.supportsMaxSteps).toBe(false);
|
|
|
+ expect(capabilities.configFormat).toBe("markdown");
|
|
|
+ expect(capabilities.outputStructure).toBe("directory");
|
|
|
+ });
|
|
|
|
|
|
- const result = await adapter.fromOAC(agent);
|
|
|
+ it("agrees with the CapabilityMatrix rather than restating it", async () => {
|
|
|
+ // These two disagreed before: the matrix called Claude `json`, the adapter `markdown`.
|
|
|
+ // A platform cannot have two answers about itself.
|
|
|
+ const { getToolCapabilities } = await import("../../../src/core/CapabilityMatrix.js");
|
|
|
|
|
|
- expect(result.success).toBe(true);
|
|
|
- expect(result.configs).toHaveLength(1);
|
|
|
- expect(result.configs[0].fileName).toBe(".claude/agents/CodeAnalyzer.md");
|
|
|
+ expect(adapter.getCapabilities().configFormat).toBe(
|
|
|
+ getToolCapabilities("claude").configFormat
|
|
|
+ );
|
|
|
});
|
|
|
|
|
|
- it("generates proper YAML frontmatter in agent.md", async () => {
|
|
|
- const agent = createSubagent();
|
|
|
-
|
|
|
- const result = await adapter.fromOAC(agent);
|
|
|
- const content = result.configs[0].content;
|
|
|
+ it("includes appropriate notes", () => {
|
|
|
+ const capabilities = adapter.getCapabilities();
|
|
|
|
|
|
- expect(content).toMatch(/^---/);
|
|
|
- expect(content).toMatch(/name: "CodeAnalyzer"/);
|
|
|
- expect(content).toMatch(/description: "Analyzes code"/);
|
|
|
- expect(content).toContain("---\n\n");
|
|
|
+ expect(capabilities.notes?.length).toBeGreaterThan(0);
|
|
|
+ expect(capabilities.notes?.some((n) => /permission/i.test(n))).toBe(true);
|
|
|
});
|
|
|
+ });
|
|
|
|
|
|
- it("includes system prompt in agent.md body", async () => {
|
|
|
- const agent = createSubagent({
|
|
|
- systemPrompt: "Analyze code quality\nCheck for best practices",
|
|
|
- });
|
|
|
+ // ============================================================================
|
|
|
+ // toOAC() — the IMPORT direction (still accepts legacy .claude/ shapes)
|
|
|
+ // ============================================================================
|
|
|
|
|
|
- const result = await adapter.fromOAC(agent);
|
|
|
- const content = result.configs[0].content;
|
|
|
+ describe("toOAC() - parsing config.json", () => {
|
|
|
+ it("parses minimal config.json", async () => {
|
|
|
+ const result = await adapter.toOAC(
|
|
|
+ JSON.stringify({
|
|
|
+ name: "TestAgent",
|
|
|
+ description: "Test description",
|
|
|
+ systemPrompt: "You are helpful",
|
|
|
+ })
|
|
|
+ );
|
|
|
|
|
|
- expect(content).toContain("Analyze code quality");
|
|
|
- expect(content).toContain("Check for best practices");
|
|
|
+ expect(result.frontmatter.name).toBe("TestAgent");
|
|
|
+ expect(result.frontmatter.description).toBe("Test description");
|
|
|
+ expect(result.systemPrompt).toBe("You are helpful");
|
|
|
+ expect(result.frontmatter.mode).toBe("primary");
|
|
|
});
|
|
|
|
|
|
- it("includes tools in agent.md frontmatter", async () => {
|
|
|
- const agent = createSubagent({
|
|
|
- frontmatter: {
|
|
|
- tools: { read: true, bash: true },
|
|
|
- },
|
|
|
- });
|
|
|
-
|
|
|
- const result = await adapter.fromOAC(agent);
|
|
|
- const content = result.configs[0].content;
|
|
|
+ it("parses config with tools array", async () => {
|
|
|
+ const result = await adapter.toOAC(
|
|
|
+ JSON.stringify({ name: "Agent", description: "Test", tools: ["Read", "Write", "Bash"] })
|
|
|
+ );
|
|
|
|
|
|
- expect(content).toContain("Read");
|
|
|
- expect(content).toContain("Bash");
|
|
|
- expect(content).toContain("tools");
|
|
|
+ expect(result.frontmatter.tools).toEqual({ read: true, write: true, bash: true });
|
|
|
});
|
|
|
|
|
|
- it("includes model in agent.md frontmatter", async () => {
|
|
|
- const agent = createSubagent({
|
|
|
- frontmatter: {
|
|
|
- model: "claude-opus-4",
|
|
|
- },
|
|
|
- });
|
|
|
-
|
|
|
- const result = await adapter.fromOAC(agent);
|
|
|
- const content = result.configs[0].content;
|
|
|
+ it("parses config with tools string", async () => {
|
|
|
+ const result = await adapter.toOAC(
|
|
|
+ JSON.stringify({ name: "Agent", description: "Test", tools: "Read, Write, Edit" })
|
|
|
+ );
|
|
|
|
|
|
- expect(content).toContain("model");
|
|
|
- expect(content).toContain("claude-opus-4");
|
|
|
+ expect(result.frontmatter.tools).toEqual({ read: true, write: true, edit: true });
|
|
|
});
|
|
|
|
|
|
- it("includes permission mode in agent.md frontmatter", async () => {
|
|
|
- const agent = createSubagent({
|
|
|
- frontmatter: {
|
|
|
- permission: { read: "allow", write: "allow" },
|
|
|
- },
|
|
|
- });
|
|
|
-
|
|
|
- const result = await adapter.fromOAC(agent);
|
|
|
- const content = result.configs[0].content;
|
|
|
+ it("parses config with skills", async () => {
|
|
|
+ const result = await adapter.toOAC(
|
|
|
+ JSON.stringify({ name: "Agent", description: "Test", skills: ["skill1", "skill2"] })
|
|
|
+ );
|
|
|
|
|
|
- expect(content).toContain("permissionMode");
|
|
|
- expect(content).toContain("bypassPermissions");
|
|
|
+ expect(result.frontmatter.skills).toEqual(["skill1", "skill2"]);
|
|
|
});
|
|
|
- });
|
|
|
-
|
|
|
- // ============================================================================
|
|
|
- // fromOAC() - SKILLS GENERATION FROM CONTEXTS
|
|
|
- // ============================================================================
|
|
|
|
|
|
- describe("fromOAC() - generating skills from contexts", () => {
|
|
|
- it("generates skill files from contexts", async () => {
|
|
|
- const agent: OpenAgent = {
|
|
|
- frontmatter: {
|
|
|
+ it("parses config with hooks", async () => {
|
|
|
+ const result = await adapter.toOAC(
|
|
|
+ JSON.stringify({
|
|
|
name: "Agent",
|
|
|
description: "Test",
|
|
|
- mode: "primary",
|
|
|
- },
|
|
|
- metadata: {
|
|
|
- name: "Agent",
|
|
|
- category: "core",
|
|
|
- type: "agent",
|
|
|
- },
|
|
|
- systemPrompt: "Prompt",
|
|
|
- contexts: [
|
|
|
- {
|
|
|
- path: ".opencode/context/skills/python-best-practices.md",
|
|
|
- description: "Python coding standards",
|
|
|
+ hooks: {
|
|
|
+ PreToolUse: [{ matcher: "*.txt", hooks: [{ type: "command", command: "validate" }] }],
|
|
|
},
|
|
|
- ],
|
|
|
- };
|
|
|
-
|
|
|
- const result = await adapter.fromOAC(agent);
|
|
|
-
|
|
|
- expect(result.configs.length).toBeGreaterThan(1);
|
|
|
- const skillConfig = result.configs.find((c) =>
|
|
|
- c.fileName.includes(".claude/skills/")
|
|
|
+ })
|
|
|
);
|
|
|
- expect(skillConfig).toBeDefined();
|
|
|
- expect(skillConfig?.fileName).toMatch(/\.claude\/skills\/.*\/SKILL\.md/);
|
|
|
+
|
|
|
+ expect(result.frontmatter.hooks?.length).toBe(1);
|
|
|
+ expect(result.frontmatter.hooks?.[0].event).toBe("PreToolUse");
|
|
|
+ expect(result.frontmatter.hooks?.[0].matchers).toEqual(["*.txt"]);
|
|
|
});
|
|
|
|
|
|
- it("generates correct skill name from context path", async () => {
|
|
|
- const agent: OpenAgent = {
|
|
|
- frontmatter: {
|
|
|
- name: "Agent",
|
|
|
- description: "Test",
|
|
|
- mode: "primary",
|
|
|
- },
|
|
|
- metadata: {
|
|
|
- name: "Agent",
|
|
|
- category: "core",
|
|
|
- type: "agent",
|
|
|
- },
|
|
|
- systemPrompt: "Prompt",
|
|
|
- contexts: [
|
|
|
- {
|
|
|
- path: "docs/React Hooks Guide.md",
|
|
|
- description: "React hooks documentation",
|
|
|
- },
|
|
|
- ],
|
|
|
- };
|
|
|
-
|
|
|
- const result = await adapter.fromOAC(agent);
|
|
|
- const skillConfig = result.configs.find((c) =>
|
|
|
- c.fileName.includes(".claude/skills/")
|
|
|
+ it("handles missing optional fields gracefully", async () => {
|
|
|
+ const result = await adapter.toOAC(
|
|
|
+ JSON.stringify({ name: "MinimalAgent", description: "Minimal" })
|
|
|
);
|
|
|
|
|
|
- expect(skillConfig?.fileName).toMatch(/react-hooks-guide/);
|
|
|
+ expect(result.frontmatter.name).toBe("MinimalAgent");
|
|
|
+ expect(result.systemPrompt).toBe("");
|
|
|
+ expect(result.frontmatter.tools).toBeUndefined();
|
|
|
+ expect(result.frontmatter.skills).toBeUndefined();
|
|
|
});
|
|
|
|
|
|
- it("includes context priority in skill content", async () => {
|
|
|
- const agent: OpenAgent = {
|
|
|
- frontmatter: {
|
|
|
- name: "Agent",
|
|
|
- description: "Test",
|
|
|
- mode: "primary",
|
|
|
- },
|
|
|
- metadata: {
|
|
|
- name: "Agent",
|
|
|
- category: "core",
|
|
|
- type: "agent",
|
|
|
- },
|
|
|
- systemPrompt: "Prompt",
|
|
|
- contexts: [
|
|
|
- {
|
|
|
- path: "context/important.md",
|
|
|
- priority: "high",
|
|
|
- description: "Important context",
|
|
|
- },
|
|
|
- ],
|
|
|
- };
|
|
|
-
|
|
|
- const result = await adapter.fromOAC(agent);
|
|
|
- const skillConfig = result.configs.find((c) =>
|
|
|
- c.fileName.includes(".claude/skills/")
|
|
|
- );
|
|
|
+ it("parses invalid JSON as markdown (subagent fallback)", async () => {
|
|
|
+ const result = await adapter.toOAC("not valid json");
|
|
|
|
|
|
- expect(skillConfig?.content).toContain("Priority: high");
|
|
|
+ expect(result.frontmatter.mode).toBe("subagent");
|
|
|
+ expect(result.systemPrompt).toBe("not valid json");
|
|
|
});
|
|
|
|
|
|
- it("generates multiple skills from multiple contexts", async () => {
|
|
|
- const agent: OpenAgent = {
|
|
|
- frontmatter: {
|
|
|
- name: "Agent",
|
|
|
- description: "Test",
|
|
|
- mode: "primary",
|
|
|
- },
|
|
|
- metadata: {
|
|
|
- name: "Agent",
|
|
|
- category: "core",
|
|
|
- type: "agent",
|
|
|
- },
|
|
|
- systemPrompt: "Prompt",
|
|
|
- contexts: [
|
|
|
- { path: "context1.md", description: "First" },
|
|
|
- { path: "context2.md", description: "Second" },
|
|
|
- { path: "context3.md", description: "Third" },
|
|
|
- ],
|
|
|
- };
|
|
|
-
|
|
|
- const result = await adapter.fromOAC(agent);
|
|
|
- const skillConfigs = result.configs.filter((c) =>
|
|
|
- c.fileName.includes(".claude/skills/")
|
|
|
+ it("handles null system prompt", async () => {
|
|
|
+ const result = await adapter.toOAC(
|
|
|
+ JSON.stringify({ name: "Agent", description: "Test", systemPrompt: null })
|
|
|
);
|
|
|
|
|
|
- expect(skillConfigs).toHaveLength(3);
|
|
|
+ expect(result.systemPrompt).toBe("");
|
|
|
});
|
|
|
+ });
|
|
|
|
|
|
- it("includes skill metadata when context lacks description", async () => {
|
|
|
- const agent: OpenAgent = {
|
|
|
- frontmatter: {
|
|
|
- name: "Agent",
|
|
|
- description: "Test",
|
|
|
- mode: "primary",
|
|
|
- },
|
|
|
- metadata: {
|
|
|
- name: "Agent",
|
|
|
- category: "core",
|
|
|
- type: "agent",
|
|
|
- },
|
|
|
- systemPrompt: "Prompt",
|
|
|
- contexts: [{ path: ".opencode/context/styles.md" }],
|
|
|
- };
|
|
|
-
|
|
|
- const result = await adapter.fromOAC(agent);
|
|
|
- const skillConfig = result.configs.find((c) =>
|
|
|
- c.fileName.includes(".claude/skills/")
|
|
|
+ describe("toOAC() - parsing agent.md with YAML frontmatter", () => {
|
|
|
+ it("parses agent.md with minimal frontmatter", async () => {
|
|
|
+ const result = await adapter.toOAC(
|
|
|
+ `---\nname: SubAgent\ndescription: A subagent\n---\n\nThis is the system prompt.`
|
|
|
);
|
|
|
|
|
|
- expect(skillConfig?.content).toContain("Context from");
|
|
|
- expect(skillConfig?.content).toContain("styles.md");
|
|
|
+ expect(result.frontmatter.name).toBe("SubAgent");
|
|
|
+ expect(result.frontmatter.description).toBe("A subagent");
|
|
|
+ expect(result.frontmatter.mode).toBe("subagent");
|
|
|
+ expect(result.systemPrompt).toBe("This is the system prompt.");
|
|
|
});
|
|
|
|
|
|
- it("handles skill names with special characters", async () => {
|
|
|
- const agent: OpenAgent = {
|
|
|
- frontmatter: {
|
|
|
- name: "Agent",
|
|
|
- description: "Test",
|
|
|
- mode: "primary",
|
|
|
- },
|
|
|
- metadata: {
|
|
|
- name: "Agent",
|
|
|
- category: "core",
|
|
|
- type: "agent",
|
|
|
- },
|
|
|
- systemPrompt: "Prompt",
|
|
|
- contexts: [
|
|
|
- { path: "docs/Type Script Advanced Rules.md", description: "Test" },
|
|
|
- ],
|
|
|
- };
|
|
|
-
|
|
|
- const result = await adapter.fromOAC(agent);
|
|
|
- const skillConfig = result.configs.find((c) =>
|
|
|
- c.fileName.includes(".claude/skills/")
|
|
|
+ it("parses a committed plugin agent's flat tools list", async () => {
|
|
|
+ const result = await adapter.toOAC(
|
|
|
+ `---\nname: code-reviewer\ndescription: Reviews\ntools: Read, Glob, Grep\nmodel: sonnet\n---\n\nPrompt`
|
|
|
);
|
|
|
|
|
|
- expect(skillConfig).toBeDefined();
|
|
|
- // Should have lowercase and dashed name from path
|
|
|
- expect(skillConfig?.fileName).toMatch(/type-script-advanced-rules/);
|
|
|
- });
|
|
|
- });
|
|
|
-
|
|
|
- // ============================================================================
|
|
|
- // VALIDATION
|
|
|
- // ============================================================================
|
|
|
-
|
|
|
- describe("validateConversion()", () => {
|
|
|
- const createAgent = (overrides?: Partial<AgentFrontmatter>): OpenAgent => ({
|
|
|
- frontmatter: {
|
|
|
- name: "Agent",
|
|
|
- description: "Test",
|
|
|
- mode: "primary",
|
|
|
- ...overrides,
|
|
|
- },
|
|
|
- metadata: { name: "Agent", category: "core", type: "agent" },
|
|
|
- systemPrompt: "Prompt",
|
|
|
- contexts: [],
|
|
|
- });
|
|
|
-
|
|
|
- it("returns no warnings for valid agent", () => {
|
|
|
- const agent = createAgent();
|
|
|
-
|
|
|
- const warnings = adapter.validateConversion(agent);
|
|
|
-
|
|
|
- expect(warnings).toHaveLength(0);
|
|
|
- });
|
|
|
-
|
|
|
- it("warns when name is missing", () => {
|
|
|
- const agent = createAgent({ name: "" });
|
|
|
-
|
|
|
- const warnings = adapter.validateConversion(agent);
|
|
|
-
|
|
|
- expect(warnings.some((w) => w.includes("name"))).toBe(true);
|
|
|
- });
|
|
|
-
|
|
|
- it("warns when description is missing", () => {
|
|
|
- const agent = createAgent({ description: "" });
|
|
|
-
|
|
|
- const warnings = adapter.validateConversion(agent);
|
|
|
-
|
|
|
- expect(warnings.some((w) => w.includes("description"))).toBe(true);
|
|
|
+ expect(result.frontmatter.tools).toEqual({ read: true, glob: true, grep: true });
|
|
|
+ expect(result.frontmatter.model).toBe("claude-sonnet-4");
|
|
|
});
|
|
|
|
|
|
- it("warns about granular permission degradation", () => {
|
|
|
- const agent = createAgent({
|
|
|
- permission: {
|
|
|
- read: { "file1.txt": "allow", "file2.txt": "deny" },
|
|
|
- },
|
|
|
- });
|
|
|
-
|
|
|
- const warnings = adapter.validateConversion(agent);
|
|
|
+ it("handles agent.md without frontmatter as markdown content", async () => {
|
|
|
+ const result = await adapter.toOAC("No frontmatter here, just markdown content");
|
|
|
|
|
|
- expect(
|
|
|
- warnings.some((w) => w.includes("granular permissions"))
|
|
|
- ).toBe(true);
|
|
|
+ expect(result.systemPrompt).toBe("No frontmatter here, just markdown content");
|
|
|
+ expect(result.frontmatter.mode).toBe("subagent");
|
|
|
});
|
|
|
|
|
|
- it("does not warn about simple permission rules", () => {
|
|
|
- const agent = createAgent({
|
|
|
- permission: { read: "allow", write: "allow" },
|
|
|
- });
|
|
|
-
|
|
|
- const warnings = adapter.validateConversion(agent);
|
|
|
+ it("preserves multiline system prompt", async () => {
|
|
|
+ const result = await adapter.toOAC(
|
|
|
+ `---\nname: Agent\ndescription: Test\n---\n\nLine one.\nLine two.\nLine three.`
|
|
|
+ );
|
|
|
|
|
|
- expect(
|
|
|
- warnings.some((w) => w.includes("granular permissions"))
|
|
|
- ).toBe(false);
|
|
|
+ expect(result.systemPrompt).toContain("Line one.");
|
|
|
+ expect(result.systemPrompt).toContain("Line three.");
|
|
|
});
|
|
|
});
|
|
|
|
|
|
- // ============================================================================
|
|
|
- // MODEL MAPPING
|
|
|
- // ============================================================================
|
|
|
-
|
|
|
describe("model mapping (Claude to OAC)", () => {
|
|
|
- it("maps claude-sonnet-4-20250514 to claude-sonnet-4", async () => {
|
|
|
- const source = JSON.stringify({
|
|
|
- name: "Agent",
|
|
|
- description: "Test",
|
|
|
- model: "claude-sonnet-4-20250514",
|
|
|
- systemPrompt: "Prompt",
|
|
|
- });
|
|
|
-
|
|
|
- const result = await adapter.toOAC(source);
|
|
|
+ it("maps dated sonnet id to claude-sonnet-4", async () => {
|
|
|
+ const result = await adapter.toOAC(
|
|
|
+ JSON.stringify({ name: "A", description: "T", model: "claude-sonnet-4-20250514" })
|
|
|
+ );
|
|
|
|
|
|
expect(result.frontmatter.model).toBe("claude-sonnet-4");
|
|
|
});
|
|
|
|
|
|
- it("maps short model names", async () => {
|
|
|
- const source = JSON.stringify({
|
|
|
- name: "Agent",
|
|
|
- description: "Test",
|
|
|
- model: "opus",
|
|
|
- systemPrompt: "Prompt",
|
|
|
- });
|
|
|
-
|
|
|
- const result = await adapter.toOAC(source);
|
|
|
+ it("maps short model aliases", async () => {
|
|
|
+ const result = await adapter.toOAC(
|
|
|
+ JSON.stringify({ name: "A", description: "T", model: "opus" })
|
|
|
+ );
|
|
|
|
|
|
expect(result.frontmatter.model).toBe("claude-opus-4");
|
|
|
});
|
|
|
|
|
|
it("preserves unknown models", async () => {
|
|
|
- const source = JSON.stringify({
|
|
|
- name: "Agent",
|
|
|
- description: "Test",
|
|
|
- model: "claude-custom-model",
|
|
|
- systemPrompt: "Prompt",
|
|
|
- });
|
|
|
-
|
|
|
- const result = await adapter.toOAC(source);
|
|
|
+ const result = await adapter.toOAC(
|
|
|
+ JSON.stringify({ name: "A", description: "T", model: "claude-custom-model" })
|
|
|
+ );
|
|
|
|
|
|
expect(result.frontmatter.model).toBe("claude-custom-model");
|
|
|
});
|
|
|
|
|
|
it("handles missing model gracefully", async () => {
|
|
|
- const source = JSON.stringify({
|
|
|
- name: "Agent",
|
|
|
- description: "Test",
|
|
|
- systemPrompt: "Prompt",
|
|
|
- });
|
|
|
-
|
|
|
- const result = await adapter.toOAC(source);
|
|
|
+ const result = await adapter.toOAC(JSON.stringify({ name: "A", description: "T" }));
|
|
|
|
|
|
expect(result.frontmatter.model).toBeUndefined();
|
|
|
});
|
|
|
});
|
|
|
|
|
|
// ============================================================================
|
|
|
- // MODEL MAPPING (OAC to Claude)
|
|
|
+ // fromOAC() — legacy in-memory interface, retargeted to the plugin layout
|
|
|
// ============================================================================
|
|
|
|
|
|
- describe("model mapping (OAC to Claude)", () => {
|
|
|
- it("maps claude-sonnet-4 to full version", async () => {
|
|
|
- const agent: OpenAgent = {
|
|
|
- frontmatter: {
|
|
|
- name: "Agent",
|
|
|
- description: "Test",
|
|
|
- mode: "primary",
|
|
|
- model: "claude-sonnet-4",
|
|
|
- },
|
|
|
- metadata: { name: "Agent", category: "core", type: "agent" },
|
|
|
- systemPrompt: "Prompt",
|
|
|
- contexts: [],
|
|
|
- };
|
|
|
-
|
|
|
- const result = await adapter.fromOAC(agent);
|
|
|
- const config = JSON.parse(result.configs[0].content);
|
|
|
-
|
|
|
- expect(config.model).toBe("claude-sonnet-4-20250514");
|
|
|
- });
|
|
|
-
|
|
|
- it("preserves other model IDs", async () => {
|
|
|
- const agent: OpenAgent = {
|
|
|
- frontmatter: {
|
|
|
- name: "Agent",
|
|
|
- description: "Test",
|
|
|
- mode: "primary",
|
|
|
- model: "claude-opus-4",
|
|
|
- },
|
|
|
- metadata: { name: "Agent", category: "core", type: "agent" },
|
|
|
- systemPrompt: "Prompt",
|
|
|
- contexts: [],
|
|
|
- };
|
|
|
-
|
|
|
- const result = await adapter.fromOAC(agent);
|
|
|
- const config = JSON.parse(result.configs[0].content);
|
|
|
-
|
|
|
- expect(config.model).toBe("claude-opus-4");
|
|
|
+ describe("fromOAC()", () => {
|
|
|
+ const createAgent = (overrides?: Partial<AgentFrontmatter>): OpenAgent => ({
|
|
|
+ frontmatter: {
|
|
|
+ name: "CodeAnalyzer",
|
|
|
+ description: "Analyzes code",
|
|
|
+ mode: "subagent",
|
|
|
+ ...overrides,
|
|
|
+ },
|
|
|
+ metadata: { name: "CodeAnalyzer", category: "specialist", type: "subagent" },
|
|
|
+ systemPrompt: "Analyze code quality",
|
|
|
+ contexts: [],
|
|
|
});
|
|
|
|
|
|
- it("does not set model when not provided", async () => {
|
|
|
- const agent: OpenAgent = {
|
|
|
- frontmatter: {
|
|
|
- name: "Agent",
|
|
|
- description: "Test",
|
|
|
- mode: "primary",
|
|
|
- },
|
|
|
- metadata: { name: "Agent", category: "core", type: "agent" },
|
|
|
- systemPrompt: "Prompt",
|
|
|
- contexts: [],
|
|
|
- };
|
|
|
-
|
|
|
- const result = await adapter.fromOAC(agent);
|
|
|
- const config = JSON.parse(result.configs[0].content);
|
|
|
-
|
|
|
- expect(config.model).toBeUndefined();
|
|
|
- });
|
|
|
- });
|
|
|
+ it("emits a single agent markdown file", async () => {
|
|
|
+ const result = await adapter.fromOAC(createAgent());
|
|
|
|
|
|
- // ============================================================================
|
|
|
- // TOOL MAPPING
|
|
|
- // ============================================================================
|
|
|
-
|
|
|
- describe("tool mapping and parsing", () => {
|
|
|
- it("parses comma-separated tools string", async () => {
|
|
|
- const source = JSON.stringify({
|
|
|
- name: "Agent",
|
|
|
- description: "Test",
|
|
|
- tools: "Read, Write, Edit, Bash",
|
|
|
- systemPrompt: "Prompt",
|
|
|
- });
|
|
|
-
|
|
|
- const result = await adapter.toOAC(source);
|
|
|
-
|
|
|
- expect(result.frontmatter.tools).toEqual({
|
|
|
- read: true,
|
|
|
- write: true,
|
|
|
- edit: true,
|
|
|
- bash: true,
|
|
|
- });
|
|
|
+ expect(result.success).toBe(true);
|
|
|
+ expect(result.configs).toHaveLength(1);
|
|
|
+ expect(result.configs[0].fileName).toBe("plugins/claude-code/agents/CodeAnalyzer.md");
|
|
|
+ expect(result.configs[0].encoding).toBe("utf-8");
|
|
|
});
|
|
|
|
|
|
- it("normalizes tool names to lowercase", async () => {
|
|
|
- const source = JSON.stringify({
|
|
|
- name: "Agent",
|
|
|
- description: "Test",
|
|
|
- tools: ["Read", "WRITE", "BaSh"],
|
|
|
- systemPrompt: "Prompt",
|
|
|
- });
|
|
|
-
|
|
|
- const result = await adapter.toOAC(source);
|
|
|
+ it("generates flat frontmatter and includes the system prompt", async () => {
|
|
|
+ const result = await adapter.fromOAC(createAgent());
|
|
|
+ const content = result.configs[0].content;
|
|
|
|
|
|
- expect(Object.keys(result.frontmatter.tools || {})).toContain("read");
|
|
|
- expect(Object.keys(result.frontmatter.tools || {})).toContain("write");
|
|
|
- expect(Object.keys(result.frontmatter.tools || {})).toContain("bash");
|
|
|
+ expect(content).toMatch(/^---\nname: CodeAnalyzer\ndescription: Analyzes code\n/);
|
|
|
+ expect(content).toContain("---\n\n");
|
|
|
+ expect(content).toContain("Analyze code quality");
|
|
|
});
|
|
|
|
|
|
- it("capitalizes tools for Claude format", async () => {
|
|
|
- const agent: OpenAgent = {
|
|
|
- frontmatter: {
|
|
|
- name: "Agent",
|
|
|
- description: "Test",
|
|
|
- mode: "primary",
|
|
|
- tools: { read: true, write: true, bash: true },
|
|
|
- },
|
|
|
- metadata: { name: "Agent", category: "core", type: "agent" },
|
|
|
- systemPrompt: "Prompt",
|
|
|
- contexts: [],
|
|
|
- };
|
|
|
-
|
|
|
- const result = await adapter.fromOAC(agent);
|
|
|
- const config = JSON.parse(result.configs[0].content);
|
|
|
-
|
|
|
- expect(config.tools).toEqual(
|
|
|
- expect.arrayContaining(["Read", "Write", "Bash"])
|
|
|
+ it("maps an authored tools map through the canonical ordering", async () => {
|
|
|
+ const result = await adapter.fromOAC(
|
|
|
+ createAgent({ tools: { bash: true, read: true, write: false } })
|
|
|
);
|
|
|
- });
|
|
|
-
|
|
|
- it("omits disabled tools", async () => {
|
|
|
- const agent: OpenAgent = {
|
|
|
- frontmatter: {
|
|
|
- name: "Agent",
|
|
|
- description: "Test",
|
|
|
- mode: "primary",
|
|
|
- tools: { read: true, write: false, bash: true },
|
|
|
- },
|
|
|
- metadata: { name: "Agent", category: "core", type: "agent" },
|
|
|
- systemPrompt: "Prompt",
|
|
|
- contexts: [],
|
|
|
- };
|
|
|
-
|
|
|
- const result = await adapter.fromOAC(agent);
|
|
|
- const config = JSON.parse(result.configs[0].content);
|
|
|
-
|
|
|
- expect(config.tools).not.toContain("Write");
|
|
|
- expect(config.tools).toContain("Read");
|
|
|
- });
|
|
|
- });
|
|
|
|
|
|
- // ============================================================================
|
|
|
- // PERMISSION MAPPING
|
|
|
- // ============================================================================
|
|
|
-
|
|
|
- describe("permission mapping", () => {
|
|
|
- it("maps all-allow permissions to bypassPermissions", async () => {
|
|
|
- const agent: OpenAgent = {
|
|
|
- frontmatter: {
|
|
|
- name: "Agent",
|
|
|
- description: "Test",
|
|
|
- mode: "primary",
|
|
|
- permission: { read: "allow", write: "allow", bash: true },
|
|
|
- },
|
|
|
- metadata: { name: "Agent", category: "core", type: "agent" },
|
|
|
- systemPrompt: "Prompt",
|
|
|
- contexts: [],
|
|
|
- };
|
|
|
-
|
|
|
- const result = await adapter.fromOAC(agent);
|
|
|
- const config = JSON.parse(result.configs[0].content);
|
|
|
-
|
|
|
- expect(config.permissionMode).toBe("bypassPermissions");
|
|
|
+ expect(result.configs[0].content).toMatch(/^tools: Read, Bash$/m);
|
|
|
});
|
|
|
|
|
|
- it("maps all-deny permissions to dontAsk", async () => {
|
|
|
- const agent: OpenAgent = {
|
|
|
- frontmatter: {
|
|
|
- name: "Agent",
|
|
|
- description: "Test",
|
|
|
- mode: "primary",
|
|
|
- permission: { read: "deny", write: "deny", bash: false },
|
|
|
- },
|
|
|
- metadata: { name: "Agent", category: "core", type: "agent" },
|
|
|
- systemPrompt: "Prompt",
|
|
|
- contexts: [],
|
|
|
- };
|
|
|
-
|
|
|
- const result = await adapter.fromOAC(agent);
|
|
|
- const config = JSON.parse(result.configs[0].content);
|
|
|
+ it("projects an authored permission map fail-closed", async () => {
|
|
|
+ const result = await adapter.fromOAC(
|
|
|
+ createAgent({ permission: { bash: { "*": "deny", "git log*": "allow" } } })
|
|
|
+ );
|
|
|
|
|
|
- expect(config.permissionMode).toBe("dontAsk");
|
|
|
+ expect(result.configs[0].content).toMatch(/^disallowedTools: Bash$/m);
|
|
|
+ expect(result.warnings.some((w) => /bash/i.test(w))).toBe(true);
|
|
|
});
|
|
|
|
|
|
- it("maps ask permissions to default", async () => {
|
|
|
- const agent: OpenAgent = {
|
|
|
- frontmatter: {
|
|
|
- name: "Agent",
|
|
|
- description: "Test",
|
|
|
- mode: "primary",
|
|
|
- permission: { read: "ask", write: "allow" },
|
|
|
- },
|
|
|
- metadata: { name: "Agent", category: "core", type: "agent" },
|
|
|
- systemPrompt: "Prompt",
|
|
|
- contexts: [],
|
|
|
- };
|
|
|
-
|
|
|
- const result = await adapter.fromOAC(agent);
|
|
|
- const config = JSON.parse(result.configs[0].content);
|
|
|
+ it("does not emit a permissionMode — Claude Code has no such agent field", async () => {
|
|
|
+ const result = await adapter.fromOAC(
|
|
|
+ createAgent({ permission: { read: "allow", write: "allow" } })
|
|
|
+ );
|
|
|
|
|
|
- expect(config.permissionMode).toBe("default");
|
|
|
+ expect(result.configs[0].content).not.toContain("permissionMode");
|
|
|
+ expect(result.configs[0].content).not.toContain("bypassPermissions");
|
|
|
});
|
|
|
|
|
|
- it("warns on mixed granular permissions", async () => {
|
|
|
- const agent: OpenAgent = {
|
|
|
- frontmatter: {
|
|
|
- name: "Agent",
|
|
|
- description: "Test",
|
|
|
- mode: "primary",
|
|
|
- permission: {
|
|
|
- read: "allow",
|
|
|
- "write.file1": "deny",
|
|
|
- },
|
|
|
- },
|
|
|
- metadata: { name: "Agent", category: "core", type: "agent" },
|
|
|
- systemPrompt: "Prompt",
|
|
|
- contexts: [],
|
|
|
- };
|
|
|
-
|
|
|
- const result = await adapter.fromOAC(agent);
|
|
|
+ it("warns when temperature is set (unsupported)", async () => {
|
|
|
+ const result = await adapter.fromOAC(createAgent({ temperature: 0.7 }));
|
|
|
|
|
|
- expect(result.warnings.some((w) => w.includes("permission"))).toBe(true);
|
|
|
+ expect(result.warnings.some((w) => w.includes("temperature"))).toBe(true);
|
|
|
});
|
|
|
- });
|
|
|
-
|
|
|
- // ============================================================================
|
|
|
- // SKILL MAPPING
|
|
|
- // ============================================================================
|
|
|
-
|
|
|
- describe("skill parsing and mapping", () => {
|
|
|
- it("parses skills as comma-separated string", async () => {
|
|
|
- const source = JSON.stringify({
|
|
|
- name: "Agent",
|
|
|
- description: "Test",
|
|
|
- skills: "skill1, skill2, skill3",
|
|
|
- systemPrompt: "Prompt",
|
|
|
- });
|
|
|
|
|
|
- const result = await adapter.toOAC(source);
|
|
|
+ it("warns when maxSteps is set (unsupported)", async () => {
|
|
|
+ const result = await adapter.fromOAC(createAgent({ maxSteps: 10 }));
|
|
|
|
|
|
- expect(result.frontmatter.skills).toEqual(["skill1", "skill2", "skill3"]);
|
|
|
+ expect(result.warnings.some((w) => w.includes("maxSteps"))).toBe(true);
|
|
|
});
|
|
|
|
|
|
- it("parses skills as array", async () => {
|
|
|
- const source = JSON.stringify({
|
|
|
- name: "Agent",
|
|
|
- description: "Test",
|
|
|
- skills: ["skill1", "skill2"],
|
|
|
- systemPrompt: "Prompt",
|
|
|
- });
|
|
|
-
|
|
|
- const result = await adapter.toOAC(source);
|
|
|
+ it("includes validation warnings for a nameless agent", async () => {
|
|
|
+ const result = await adapter.fromOAC(createAgent({ name: "", description: "" }));
|
|
|
|
|
|
- expect(result.frontmatter.skills).toEqual(["skill1", "skill2"]);
|
|
|
+ expect(result.warnings.length).toBeGreaterThan(0);
|
|
|
});
|
|
|
|
|
|
- it("handles empty skills gracefully", async () => {
|
|
|
- const source = JSON.stringify({
|
|
|
- name: "Agent",
|
|
|
- description: "Test",
|
|
|
- skills: [],
|
|
|
- systemPrompt: "Prompt",
|
|
|
- });
|
|
|
-
|
|
|
- const result = await adapter.toOAC(source);
|
|
|
+ it("includes capabilities in the result", async () => {
|
|
|
+ const result = await adapter.fromOAC(createAgent());
|
|
|
|
|
|
- expect(result.frontmatter.skills).toEqual([]);
|
|
|
+ expect(result.capabilities?.name).toBe("claude");
|
|
|
});
|
|
|
|
|
|
- it("includes skills in config.json", async () => {
|
|
|
- const agent: OpenAgent = {
|
|
|
- frontmatter: {
|
|
|
- name: "Agent",
|
|
|
- description: "Test",
|
|
|
- mode: "primary",
|
|
|
- skills: ["skill1", "skill2"],
|
|
|
- },
|
|
|
- metadata: { name: "Agent", category: "core", type: "agent" },
|
|
|
- systemPrompt: "Prompt",
|
|
|
- contexts: [],
|
|
|
- };
|
|
|
-
|
|
|
- const result = await adapter.fromOAC(agent);
|
|
|
- const config = JSON.parse(result.configs[0].content);
|
|
|
+ it("handles an empty system prompt", async () => {
|
|
|
+ const result = await adapter.fromOAC({ ...createAgent(), systemPrompt: "" });
|
|
|
|
|
|
- expect(config.skills).toEqual(["skill1", "skill2"]);
|
|
|
+ expect(result.success).toBe(true);
|
|
|
+ expect(result.configs[0].content).toBe(
|
|
|
+ "---\nname: CodeAnalyzer\ndescription: Analyzes code\n---\n\n\n"
|
|
|
+ );
|
|
|
});
|
|
|
|
|
|
- it("handles skill objects with name property", async () => {
|
|
|
- const agent: OpenAgent = {
|
|
|
- frontmatter: {
|
|
|
- name: "Agent",
|
|
|
- description: "Test",
|
|
|
- mode: "primary",
|
|
|
- skills: [{ name: "skill1" }],
|
|
|
- },
|
|
|
- metadata: { name: "Agent", category: "core", type: "agent" },
|
|
|
- systemPrompt: "Prompt",
|
|
|
- contexts: [],
|
|
|
+ it("carries hooks nowhere in agent frontmatter", async () => {
|
|
|
+ // Claude Code agent frontmatter accepts name/description/tools/disallowedTools/model
|
|
|
+ // and nothing else. The old adapter wrote a `hooks:` key that Claude Code silently
|
|
|
+ // ignores, which reads as support that does not exist.
|
|
|
+ const hook: HookDefinition = {
|
|
|
+ event: "PreToolUse",
|
|
|
+ matchers: ["*.txt"],
|
|
|
+ commands: [{ type: "command", command: "validate" }],
|
|
|
};
|
|
|
|
|
|
- const result = await adapter.fromOAC(agent);
|
|
|
- const config = JSON.parse(result.configs[0].content);
|
|
|
+ const result = await adapter.fromOAC(createAgent({ hooks: [hook] }));
|
|
|
|
|
|
- expect(config.skills).toContain("skill1");
|
|
|
+ expect(result.configs[0].content).not.toContain("hooks");
|
|
|
});
|
|
|
});
|
|
|
|
|
|
// ============================================================================
|
|
|
- // HOOK MAPPING
|
|
|
+ // SKILLS GENERATION FROM CONTEXTS
|
|
|
// ============================================================================
|
|
|
|
|
|
- describe("hook parsing and mapping", () => {
|
|
|
- it("parses hooks with matchers", async () => {
|
|
|
- const source = JSON.stringify({
|
|
|
- name: "Agent",
|
|
|
- description: "Test",
|
|
|
- hooks: {
|
|
|
- PreToolUse: [
|
|
|
- {
|
|
|
- matcher: "*.txt",
|
|
|
- hooks: [{ type: "command", command: "validate" }],
|
|
|
- },
|
|
|
- ],
|
|
|
- },
|
|
|
- systemPrompt: "Prompt",
|
|
|
- });
|
|
|
-
|
|
|
- const result = await adapter.toOAC(source);
|
|
|
-
|
|
|
- expect(result.frontmatter.hooks).toBeDefined();
|
|
|
- expect(result.frontmatter.hooks![0].matchers).toEqual(["*.txt"]);
|
|
|
- });
|
|
|
-
|
|
|
- it("maps hook commands correctly", async () => {
|
|
|
- const source = JSON.stringify({
|
|
|
- name: "Agent",
|
|
|
- description: "Test",
|
|
|
- hooks: {
|
|
|
- PostToolUse: [
|
|
|
- {
|
|
|
- matcher: "*",
|
|
|
- hooks: [
|
|
|
- { type: "command", command: "log" },
|
|
|
- { type: "command", command: "notify" },
|
|
|
- ],
|
|
|
- },
|
|
|
- ],
|
|
|
- },
|
|
|
- systemPrompt: "Prompt",
|
|
|
- });
|
|
|
-
|
|
|
- const result = await adapter.toOAC(source);
|
|
|
-
|
|
|
- expect(result.frontmatter.hooks![0].commands.length).toBe(2);
|
|
|
+ describe("fromOAC() - generating skills from contexts", () => {
|
|
|
+ const withContexts = (
|
|
|
+ contexts: Array<{ path: string; priority?: string; description?: string }>
|
|
|
+ ): OpenAgent => ({
|
|
|
+ frontmatter: { name: "Agent", description: "Test", mode: "primary" },
|
|
|
+ metadata: { name: "Agent", category: "core", type: "agent" },
|
|
|
+ systemPrompt: "Prompt",
|
|
|
+ contexts,
|
|
|
});
|
|
|
|
|
|
- it("converts hooks back to Claude format", async () => {
|
|
|
- const agent: OpenAgent = {
|
|
|
- frontmatter: {
|
|
|
- name: "Agent",
|
|
|
- description: "Test",
|
|
|
- mode: "primary",
|
|
|
- hooks: [
|
|
|
- {
|
|
|
- event: "PreToolUse",
|
|
|
- matchers: ["*.txt"],
|
|
|
- commands: [{ type: "command", command: "validate" }],
|
|
|
- },
|
|
|
- ],
|
|
|
- },
|
|
|
- metadata: { name: "Agent", category: "core", type: "agent" },
|
|
|
- systemPrompt: "Prompt",
|
|
|
- contexts: [],
|
|
|
- };
|
|
|
-
|
|
|
- const result = await adapter.fromOAC(agent);
|
|
|
- const config = JSON.parse(result.configs[0].content);
|
|
|
+ it("generates skill files under the plugin tree", async () => {
|
|
|
+ const result = await adapter.fromOAC(
|
|
|
+ withContexts([
|
|
|
+ { path: ".opencode/context/skills/python.md", description: "Python standards" },
|
|
|
+ ])
|
|
|
+ );
|
|
|
|
|
|
- expect(config.hooks.PreToolUse).toBeDefined();
|
|
|
- expect(config.hooks.PreToolUse[0].matcher).toBe("*.txt");
|
|
|
+ const skill = result.configs.find((c) => c.fileName.includes("/skills/"));
|
|
|
+ expect(skill?.fileName).toBe("plugins/claude-code/skills/python/SKILL.md");
|
|
|
});
|
|
|
|
|
|
- it("defaults matcher to wildcard", async () => {
|
|
|
- const agent: OpenAgent = {
|
|
|
- frontmatter: {
|
|
|
- name: "Agent",
|
|
|
- description: "Test",
|
|
|
- mode: "primary",
|
|
|
- hooks: [
|
|
|
- {
|
|
|
- event: "AgentStart",
|
|
|
- commands: [{ type: "command", command: "init" }],
|
|
|
- },
|
|
|
- ],
|
|
|
- },
|
|
|
- metadata: { name: "Agent", category: "core", type: "agent" },
|
|
|
- systemPrompt: "Prompt",
|
|
|
- contexts: [],
|
|
|
- };
|
|
|
-
|
|
|
- const result = await adapter.fromOAC(agent);
|
|
|
- const config = JSON.parse(result.configs[0].content);
|
|
|
+ it("generates a slugified skill name from the context path", async () => {
|
|
|
+ const result = await adapter.fromOAC(
|
|
|
+ withContexts([{ path: "docs/React Hooks Guide.md", description: "React docs" }])
|
|
|
+ );
|
|
|
|
|
|
- expect(config.hooks.AgentStart[0].matcher).toBe("*");
|
|
|
+ expect(result.configs.find((c) => c.fileName.includes("/skills/"))?.fileName).toMatch(
|
|
|
+ /react-hooks-guide/
|
|
|
+ );
|
|
|
});
|
|
|
- });
|
|
|
-
|
|
|
- // ============================================================================
|
|
|
- // ROUNDTRIP CONVERSION
|
|
|
- // ============================================================================
|
|
|
-
|
|
|
- describe("roundtrip conversion", () => {
|
|
|
- it("roundtrip primary agent config", async () => {
|
|
|
- // Start with OAC
|
|
|
- const original: OpenAgent = {
|
|
|
- frontmatter: {
|
|
|
- name: "TestAgent",
|
|
|
- description: "A test agent",
|
|
|
- mode: "primary",
|
|
|
- model: "claude-sonnet-4",
|
|
|
- tools: { read: true, write: true },
|
|
|
- skills: ["skill1"],
|
|
|
- },
|
|
|
- metadata: { name: "TestAgent", category: "core", type: "agent" },
|
|
|
- systemPrompt: "You are helpful",
|
|
|
- contexts: [],
|
|
|
- };
|
|
|
|
|
|
- // Convert to Claude
|
|
|
- const toClaudeResult = await adapter.fromOAC(original);
|
|
|
- const claudeConfig = JSON.parse(toClaudeResult.configs[0].content);
|
|
|
-
|
|
|
- // Convert back to OAC
|
|
|
- const backToOAC = await adapter.toOAC(JSON.stringify(claudeConfig));
|
|
|
+ it("includes context priority in skill content", async () => {
|
|
|
+ const result = await adapter.fromOAC(
|
|
|
+ withContexts([{ path: "context/important.md", priority: "high", description: "Ctx" }])
|
|
|
+ );
|
|
|
|
|
|
- // Verify core properties are preserved
|
|
|
- expect(backToOAC.frontmatter.name).toBe(original.frontmatter.name);
|
|
|
- expect(backToOAC.frontmatter.description).toBe(
|
|
|
- original.frontmatter.description
|
|
|
+ expect(result.configs.find((c) => c.fileName.includes("/skills/"))?.content).toContain(
|
|
|
+ "Priority: high"
|
|
|
);
|
|
|
- expect(backToOAC.systemPrompt).toBe(original.systemPrompt);
|
|
|
- expect(backToOAC.frontmatter.tools).toEqual(original.frontmatter.tools);
|
|
|
});
|
|
|
|
|
|
- it("roundtrip preserves model through conversion", async () => {
|
|
|
- const original: OpenAgent = {
|
|
|
- frontmatter: {
|
|
|
- name: "Agent",
|
|
|
- description: "Test",
|
|
|
- mode: "primary",
|
|
|
- model: "claude-opus-4",
|
|
|
- },
|
|
|
- metadata: { name: "Agent", category: "core", type: "agent" },
|
|
|
- systemPrompt: "Prompt",
|
|
|
- contexts: [],
|
|
|
- };
|
|
|
+ it("generates one skill per context", async () => {
|
|
|
+ const result = await adapter.fromOAC(
|
|
|
+ withContexts([{ path: "a.md" }, { path: "b.md" }, { path: "c.md" }])
|
|
|
+ );
|
|
|
|
|
|
- const toClaudeResult = await adapter.fromOAC(original);
|
|
|
- const claudeConfig = JSON.parse(toClaudeResult.configs[0].content);
|
|
|
- const backToOAC = await adapter.toOAC(JSON.stringify(claudeConfig));
|
|
|
+ expect(result.configs.filter((c) => c.fileName.includes("/skills/"))).toHaveLength(3);
|
|
|
+ });
|
|
|
+
|
|
|
+ it("falls back to a generated description when the context lacks one", async () => {
|
|
|
+ const result = await adapter.fromOAC(withContexts([{ path: ".opencode/context/styles.md" }]));
|
|
|
+ const skill = result.configs.find((c) => c.fileName.includes("/skills/"));
|
|
|
|
|
|
- expect(backToOAC.frontmatter.model).toBe("claude-opus-4");
|
|
|
+ expect(skill?.content).toContain("Context from");
|
|
|
+ expect(skill?.content).toContain("styles.md");
|
|
|
});
|
|
|
});
|
|
|
|
|
|
// ============================================================================
|
|
|
- // EDGE CASES & ERROR HANDLING
|
|
|
+ // VALIDATION
|
|
|
// ============================================================================
|
|
|
|
|
|
- describe("edge cases and error handling", () => {
|
|
|
- it("omits empty tools from config", async () => {
|
|
|
- const agent: OpenAgent = {
|
|
|
- frontmatter: {
|
|
|
- name: "Agent",
|
|
|
- description: "Test",
|
|
|
- mode: "primary",
|
|
|
- tools: {},
|
|
|
- },
|
|
|
- metadata: { name: "Agent", category: "core", type: "agent" },
|
|
|
- systemPrompt: "Prompt",
|
|
|
- contexts: [],
|
|
|
- };
|
|
|
-
|
|
|
- const result = await adapter.fromOAC(agent);
|
|
|
- const config = JSON.parse(result.configs[0].content);
|
|
|
-
|
|
|
- // Empty tools should not be included in config
|
|
|
- expect(config.tools === undefined || config.tools?.length === 0).toBe(true);
|
|
|
- });
|
|
|
-
|
|
|
- it("handles null system prompt", async () => {
|
|
|
- const source = JSON.stringify({
|
|
|
- name: "Agent",
|
|
|
- description: "Test",
|
|
|
- systemPrompt: null,
|
|
|
- });
|
|
|
-
|
|
|
- const result = await adapter.toOAC(source);
|
|
|
-
|
|
|
- expect(result.systemPrompt).toBe("");
|
|
|
- });
|
|
|
-
|
|
|
- it("handles empty system prompt", async () => {
|
|
|
- const agent: OpenAgent = {
|
|
|
- frontmatter: {
|
|
|
- name: "Agent",
|
|
|
- description: "Test",
|
|
|
- mode: "primary",
|
|
|
- },
|
|
|
- metadata: { name: "Agent", category: "core", type: "agent" },
|
|
|
- systemPrompt: "",
|
|
|
- contexts: [],
|
|
|
- };
|
|
|
-
|
|
|
- const result = await adapter.fromOAC(agent);
|
|
|
- const config = JSON.parse(result.configs[0].content);
|
|
|
-
|
|
|
- expect(config.systemPrompt).toBe("");
|
|
|
- });
|
|
|
-
|
|
|
- it("handles special characters in names", async () => {
|
|
|
- const agent: OpenAgent = {
|
|
|
- frontmatter: {
|
|
|
- name: "Agent-With-Dashes_and_underscores",
|
|
|
- description: "Test with special chars: @#$",
|
|
|
- mode: "primary",
|
|
|
- },
|
|
|
- metadata: { name: "Agent", category: "core", type: "agent" },
|
|
|
- systemPrompt: "Prompt",
|
|
|
- contexts: [],
|
|
|
- };
|
|
|
-
|
|
|
- const result = await adapter.fromOAC(agent);
|
|
|
-
|
|
|
- expect(result.success).toBe(true);
|
|
|
- const config = JSON.parse(result.configs[0].content);
|
|
|
- expect(config.name).toContain("-");
|
|
|
+ describe("validateConversion()", () => {
|
|
|
+ const createAgent = (overrides?: Partial<AgentFrontmatter>): OpenAgent => ({
|
|
|
+ frontmatter: { name: "Agent", description: "Test", mode: "primary", ...overrides },
|
|
|
+ metadata: { name: "Agent", category: "core", type: "agent" },
|
|
|
+ systemPrompt: "Prompt",
|
|
|
+ contexts: [],
|
|
|
});
|
|
|
|
|
|
- it("handles very long system prompt", async () => {
|
|
|
- const longPrompt = "A".repeat(5000);
|
|
|
- const agent: OpenAgent = {
|
|
|
- frontmatter: {
|
|
|
- name: "Agent",
|
|
|
- description: "Test",
|
|
|
- mode: "primary",
|
|
|
- },
|
|
|
- metadata: { name: "Agent", category: "core", type: "agent" },
|
|
|
- systemPrompt: longPrompt,
|
|
|
- contexts: [],
|
|
|
- };
|
|
|
-
|
|
|
- const result = await adapter.fromOAC(agent);
|
|
|
- const config = JSON.parse(result.configs[0].content);
|
|
|
-
|
|
|
- expect(config.systemPrompt.length).toBe(5000);
|
|
|
+ it("returns no warnings for valid agent", () => {
|
|
|
+ expect(adapter.validateConversion(createAgent())).toHaveLength(0);
|
|
|
});
|
|
|
|
|
|
- it("handles multiline YAML values in frontmatter", async () => {
|
|
|
- const source = `---
|
|
|
-name: Agent
|
|
|
-description: Test description
|
|
|
----
|
|
|
-
|
|
|
-System prompt here`;
|
|
|
-
|
|
|
- const result = await adapter.toOAC(source);
|
|
|
-
|
|
|
- expect(result.frontmatter.name).toBe("Agent");
|
|
|
- expect(result.systemPrompt).toBe("System prompt here");
|
|
|
+ it("warns when name is missing", () => {
|
|
|
+ expect(adapter.validateConversion(createAgent({ name: "" })).some((w) => w.includes("name"))).toBe(
|
|
|
+ true
|
|
|
+ );
|
|
|
});
|
|
|
- });
|
|
|
-
|
|
|
- // ============================================================================
|
|
|
- // CONVERSION RESULT STRUCTURE
|
|
|
- // ============================================================================
|
|
|
-
|
|
|
- describe("conversion result structure", () => {
|
|
|
- it("returns success true on valid conversion", async () => {
|
|
|
- const agent: OpenAgent = {
|
|
|
- frontmatter: {
|
|
|
- name: "Agent",
|
|
|
- description: "Test",
|
|
|
- mode: "primary",
|
|
|
- },
|
|
|
- metadata: { name: "Agent", category: "core", type: "agent" },
|
|
|
- systemPrompt: "Prompt",
|
|
|
- contexts: [],
|
|
|
- };
|
|
|
-
|
|
|
- const result = await adapter.fromOAC(agent);
|
|
|
|
|
|
- expect(result.success).toBe(true);
|
|
|
+ it("warns when description is missing", () => {
|
|
|
+ expect(
|
|
|
+ adapter
|
|
|
+ .validateConversion(createAgent({ description: "" }))
|
|
|
+ .some((w) => w.includes("description"))
|
|
|
+ ).toBe(true);
|
|
|
});
|
|
|
|
|
|
- it("includes capabilities in result", async () => {
|
|
|
- const agent: OpenAgent = {
|
|
|
- frontmatter: {
|
|
|
- name: "Agent",
|
|
|
- description: "Test",
|
|
|
- mode: "primary",
|
|
|
- },
|
|
|
- metadata: { name: "Agent", category: "core", type: "agent" },
|
|
|
- systemPrompt: "Prompt",
|
|
|
- contexts: [],
|
|
|
- };
|
|
|
-
|
|
|
- const result = await adapter.fromOAC(agent);
|
|
|
+ it("warns about granular permission degradation", () => {
|
|
|
+ const warnings = adapter.validateConversion(
|
|
|
+ createAgent({ permission: { read: { "file1.txt": "allow", "file2.txt": "deny" } } })
|
|
|
+ );
|
|
|
|
|
|
- expect(result.capabilities).toBeDefined();
|
|
|
- expect(result.capabilities?.name).toBe("claude");
|
|
|
+ expect(warnings.some((w) => w.includes("granular permissions"))).toBe(true);
|
|
|
});
|
|
|
|
|
|
- it("includes encoding in tool config", async () => {
|
|
|
- const agent: OpenAgent = {
|
|
|
- frontmatter: {
|
|
|
- name: "Agent",
|
|
|
- description: "Test",
|
|
|
- mode: "primary",
|
|
|
- },
|
|
|
- metadata: { name: "Agent", category: "core", type: "agent" },
|
|
|
- systemPrompt: "Prompt",
|
|
|
- contexts: [],
|
|
|
- };
|
|
|
-
|
|
|
- const result = await adapter.fromOAC(agent);
|
|
|
+ it("does not warn about simple permission rules", () => {
|
|
|
+ const warnings = adapter.validateConversion(
|
|
|
+ createAgent({ permission: { read: "allow", write: "allow" } })
|
|
|
+ );
|
|
|
|
|
|
- expect(result.configs[0].encoding).toBe("utf-8");
|
|
|
+ expect(warnings.some((w) => w.includes("granular permissions"))).toBe(false);
|
|
|
});
|
|
|
});
|
|
|
});
|