* feat: implement compatibility layer foundation (Issue #141) - Add TypeScript project setup with strict mode and Vitest - Implement 20+ Zod validation schemas for OpenAgent format - Create BaseAdapter abstract class with template method pattern - Add context documentation for compatibility layer workflow - Setup ESLint, TypeScript, and test configuration Phase 1 Foundation: 50% complete (subtasks 01-03) Related: #141 * feat: implement AgentLoader with TypeScript (Issue #141) - Add AgentLoader class for loading OAC agent files - Implement parseFrontmatter() for YAML extraction - Add extractSections() for markdown parsing (skills, examples, commands, workflow) - Create custom error classes: AgentLoadError, FrontmatterParseError, ValidationError - Support single file and recursive directory loading - Add metadata merging from agent-metadata.json - Export convenience functions: loadAgent(), loadAgents() - Full Zod schema validation with descriptive error messages - 386 lines, type-safe with strict mode Phase 1 Foundation: 66.7% complete (subtask 04/06) Related: #141 * feat: implement AdapterRegistry with TypeScript (Issue #141) - Add AdapterRegistry class for managing tool adapters - Implement type-safe registration with Map<string, BaseAdapter> - Add alias support for adapter names (case-insensitive) - Create custom error class: AdapterRegistryError - Implement methods: register(), get(), list(), has(), getAll(), unregister(), clear() - Add capability-based adapter discovery: getCapabilities(), findByFeature() - Export singleton registry instance and convenience functions - Support lazy loading of built-in adapters (placeholder for Phase 2) - 416 lines, fully type-safe with strict mode Features: - Duplicate registration prevention - Alias conflict detection - O(1) adapter lookup - Sorted adapter listings - Comprehensive JSDoc documentation Phase 1 Foundation: 83.3% complete (subtask 05/06) Related: #141 * feat: create main entry point index.ts (Issue #141) - Add comprehensive barrel exports for public API - Export all Zod schemas and TypeScript types - Export AgentLoader class and convenience functions - Export AdapterRegistry, singleton, and helpers - Export error classes: AgentLoadError, FrontmatterParseError, ValidationError, AdapterRegistryError - Export BaseAdapter for custom adapter creation - Add package VERSION constant - Include JSDoc documentation with usage examples - Organized exports by category (types, core, adapters) - 168 lines with comprehensive documentation All exports: - Types: All Zod schemas and inferred TypeScript types - Core: AgentLoader, AdapterRegistry, registry singleton - Adapters: BaseAdapter abstract class - Errors: All custom error classes - Utilities: Convenience functions (loadAgent, getAdapter, etc.) Phase 1 Foundation: 100% COMPLETE! 🎉 (6/6 subtasks) Related: #141 * feat: implement ClaudeAdapter with bidirectional conversion (Issue #141) - Implement ClaudeAdapter extending BaseAdapter - Support toOAC() conversion from Claude Code format to OAC - Support fromOAC() conversion from OAC to Claude Code format - Handle .claude/config.json for primary agents - Handle .claude/agents/*.md for subagents with YAML frontmatter - Map Claude Skills system to/from OAC contexts - Map hooks (PreToolUse, PostToolUse, etc.) bidirectionally - Degrade granular OAC permissions to Claude permissionMode - Add warnings for unsupported features (temperature, maxSteps) - Export ClaudeAdapter from main index.ts Capabilities: - supportsMultipleAgents: true - supportsSkills: true - supportsHooks: true - supportsGranularPermissions: false (binary only) - supportsTemperature: false - Model ID mapping (claude-sonnet-4 <-> claude-sonnet-4-20250514) - Tool access mapping (OAC ToolAccess <-> Claude tools array) Subtask: compatibility-layer-141-07 Phase: 2 (Adapters Migration) * feat: implement CursorAdapter with single-file format support (Issue #141) - Implement CursorAdapter extending BaseAdapter - Support toOAC() conversion from .cursorrules to OAC - Support fromOAC() conversion from OAC to .cursorrules format - Handle single-file limitation (no multiple agents) - Implement mergeAgents() for combining multiple OAC agents - Parse optional YAML frontmatter in .cursorrules - Inline context files into .cursorrules content - Add comprehensive warnings for lost features Key limitations: - supportsMultipleAgents: false (single .cursorrules file) - supportsSkills: false (must inline content) - supportsHooks: false (not supported by Cursor) - supportsGranularPermissions: false (binary only) - supportsTemperature: true (limited support) Features: - Model ID mapping (gpt-4, claude-3-opus, etc.) - Tool access parsing and generation - Context file inlining with references - Multi-agent merge capability - Frontmatter parsing for metadata Export CursorAdapter from main index.ts Subtask: compatibility-layer-141-08 Phase: 2 (Adapters Migration) * feat: implement WindsurfAdapter with bidirectional conversion (Issue #141) - Add WindsurfAdapter.ts (514 lines) extending BaseAdapter - Support bidirectional OAC ↔ Windsurf JSON conversion - Implement toOAC() for Windsurf config → OpenAgent parsing - Implement fromOAC() for OpenAgent → Windsurf config generation - Add temperature ↔ creativity setting mapping (low/medium/high) - Add model ID mapping (claude-sonnet-4 ↔ claude-4-sonnet) - Support multi-agent configuration via .windsurf/agents/ - Support context files via .windsurf/context/ - Implement binary permission mapping with degradation warnings - Add priority level normalization (critical/high/medium/low → high/low) - Export WindsurfAdapter in index.ts - All adapters now complete: Claude, Cursor, Windsurf - Compilation successful: 0 errors, 0 warnings Phase 2 (Adapters): 50% complete (3/6 subtasks) Total progress: 28.13% (9/32 subtasks) Code stats: 3,333 lines TypeScript (Foundation + Adapters) * docs: add compatibility layer context files and progress tracking (Issue #141) - Add baseadapter-implementation.md with detailed adapter pattern and testing examples - Add compatibility-layer-development.md with development workflow and best practices - Add compatibility-layer-adapters.md lookup table with adapter specifications - Add compatibility-layer-progress.md tracking Phase 1 completion and Phase 2 status - Add compatibility-learnings.md documenting implementation learnings - Update compatibility-layer.md with latest architecture insights These context files support Phase 2 adapter unit testing and future adapter development. * test: implement comprehensive unit tests for ClaudeAdapter (Issue #141 - Subtask 10) Implemented 80 test cases covering: - toOAC() parsing of config.json and agent.md (YAML frontmatter) - fromOAC() conversion to config.json and agent.md formats - Capabilities and feature validation - Model, tool, skill, hook, and permission mapping - Skill generation from contexts - Edge cases and error handling - Roundtrip conversion validation Coverage achieved: - 96.32% statements (target: 80%+) ✅ - 100% functions (target: 80%+) ✅ - 82.14% branches (target: 75%+) ✅ All test cases follow AAA pattern (Arrange-Act-Assert) and test standards from .opencode/context/core/standards/test-coverage.md * test: implement comprehensive unit tests for CursorAdapter (Issue #141 - Subtask 11) Implemented 78 test cases covering: - toOAC() parsing of plain .cursorrules and YAML frontmatter - fromOAC() conversion to .cursorrules format - Capabilities and feature validation - Agent merging (CURSOR-SPECIFIC - unique feature) - Model, tool mapping - Context inlining - Temperature and configuration handling - Edge cases and error handling - Roundtrip conversion validation Coverage achieved: - 98.55% statements (target: 80%+) ✅ - 100% functions (target: 80%+) ✅ - 89.65% branches (target: 75%+) ✅ Key test areas: - Plain text .cursorrules parsing (no frontmatter) - YAML frontmatter parsing with type coercion - Agent merging (single, two, three+ agents) - Tool union and temperature MAX behavior - Context inlining with proper formatting - Model mapping (Cursor ↔ OAC) All test cases follow AAA pattern and test standards. * test: implement comprehensive unit tests for WindsurfAdapter (Issue #141 - Subtask 12) * feat: implement Phase 3 mappers and translation engine with tests (Issue #141) - Add ToolMapper: maps tool names between OAC and Claude/Cursor/Windsurf - Add PermissionMapper: translates granular to binary permissions - Add ModelMapper: maps AI model IDs between platforms - Add ContextMapper: maps context file paths between platforms - Add CapabilityMatrix: feature compatibility analysis across platforms - Add TranslationEngine: orchestrates all mappers for complete translation Test coverage: - ToolMapper: 100% statements, 100% branches - PermissionMapper: 98% statements, 96% branches - ModelMapper: 99% statements, 90% branches - ContextMapper: 97% statements, 94% branches - CapabilityMatrix: 99% statements, 96% branches - TranslationEngine: 99% statements, 93% branches Total: 485 tests passing (249 new tests added) * docs: update Issue #141 context files to reflect Phase 3 completion - Update progress from 28% to 59.4% (19/32 subtasks) - Mark Phases 1-3 as complete (Foundation, Adapters, Mappers) - Update code stats: 5,799 source lines, 6,322 test lines - Add test coverage metrics: 485 tests, 97-100% on Phase 3 - Update structure with all implemented files - Add executive summary for token-efficient context loading - Document next steps: Phase 4 CLI implementation * docs(context): harvest Claude Code docs and fix MVI compliance (Phase 1) - Harvest external Claude Code documentation into context system: - hooks-system, agent-skills, subagents-system concepts - hook-events, skill-metadata, builtin-subagents lookups - Example files for hooks, skills, and subagents - Plugin architecture and migration guides - Fix 6 critical MVI violations (files > 200 lines): - Split adding-agent.md → adding-agent-basics.md + adding-agent-testing.md - Split adding-skill.md → 3 files (basics, implementation, example) - Split task-delegation.md → 3 files (basics, specialists, caching) - Split external-libraries.md → 3 files (workflow, scenarios, faq) - Split design-assets.md → 4 files (images, icons, fonts, cdn) - Split navigation-design.md → 2 files (basics, templates) - Update navigation files with new references - Remove consumed source docs from to-be-consumed/ Total: 17 new files created, 6 large files replaced, 5 source docs consumed * docs(context): MVI compliance Phase 2 - split large workflow and UI files Split 3 large files into MVI-compliant modules (< 200 lines each): - design-iteration.md (1058 lines) → 9 files: - design-iteration-overview.md (overview, when to use) - design-iteration-plan-file.md (mandatory plan template) - design-iteration-stage-layout.md (Stage 1) - design-iteration-stage-theme.md (Stage 2) - design-iteration-stage-animation.md (Stage 3) - design-iteration-stage-implementation.md (Stage 4) - design-iteration-visual-content.md (Image Specialist) - design-iteration-best-practices.md (quality, troubleshooting) - design-iteration-plan-iterations.md (managing iterations) - animation-patterns.md (753 lines) → 6 files: - animation-basics.md (fundamentals, timing, easing) - animation-components.md (buttons, cards, modals) - animation-chat.md (message, typing animations) - animation-loading.md (skeleton, spinner, progress) - animation-forms.md (focus, validation, scroll) - animation-advanced.md (recipes, best practices) - premium-dark-ui-quick-start.md (638 lines) → 4 files: - premium-dark-ui-colors.md (palette, typography, spacing) - premium-dark-ui-components.md (core building blocks) - premium-dark-ui-layouts.md (hero, features, CTA) - premium-dark-ui-advanced.md (animations, accessibility) Updated navigation files with new references. * feat(cli): implement CLI scaffolding with Commander.js (Issue #141 - Subtask 20) - Add src/cli/index.ts: CLI entry point with convert, validate, migrate, info commands - Add src/cli/types.ts: TypeScript type definitions for CLI options - Add src/cli/utils.ts: Logging, spinner, and formatting utilities - Add "type": "module" to package.json for ESM support - Global options: --verbose, --quiet, --output-format (json|text) - Commands are placeholders pending implementation in subtasks 21-24 * feat(cli): implement convert command with full format support (Issue #141 - Subtask 21) - Add src/cli/commands/convert.ts: Complete convert command implementation - Auto-detect input format from file path and content - Support bidirectional conversion: OAC <-> Cursor/Claude/Windsurf - Output to stdout by default, file with --output flag - Display warnings for feature loss during conversion - Enable registerBuiltInAdapters in AdapterRegistry - Add public constructors to all adapters (CursorAdapter, ClaudeAdapter, WindsurfAdapter) - Fix ESM imports: add .js extensions to all relative imports across codebase All 485 tests passing. * feat(cli): implement validate and info commands (Issue #141 - Subtasks 22, 24) - validate: Checks agent compatibility with target format - Reports compatibility score (0-100%) - Lists preserved/degraded/lost features - Provides suggestions for unsupported features - Exit codes: 0=compatible, 1=partial, 2=incompatible - Supports --strict and --output-format json - info: Displays tool capabilities and feature parity - Shows feature matrix for all platforms - Detailed view for single platform - Platform comparison with --compare - Documentation links - Supports --output-format json * feat(cli): implement migrate command and add migration guides (Issue #141) - Add migrate command for full project migration with batch conversion - Support --dry-run, --force, and --out-dir options - Generate migration reports with success/failure summaries - Preserve directory structure in output Documentation: - Add 5 migration guides (cursor↔oac, claude↔oac, oac→windsurf) - Add comprehensive feature-matrices.md with platform comparisons - Document lossless vs lossy conversion impacts Completes subtasks: 23, 26, 27, 28, 29, 30, 31 * test(cli): add comprehensive integration tests for all CLI commands (Issue #141) - Add convert.test.ts with 23 tests for format conversions - Add validate.test.ts with 21 tests for compatibility validation - Add migrate.test.ts with 25 tests for batch migration - Add info.test.ts with 30 tests for capability queries - Add test fixtures for OAC, Cursor, and Claude formats - Fix empty directory warning in migrate command All 584 tests passing with full coverage of CLI modules. Completes subtask 25 * docs: update README with comprehensive CLI and API documentation (Issue #141) - Add complete CLI command reference with all options - Add programmatic API usage examples - Add feature parity matrix summary - Add links to all migration guides - Add architecture overview - Add custom adapter creation guide Completes subtask 32 - Feature #141 fully implemented * docs: add visual examples and format improvements to README - Add Cursor IDE screenshot (contextscout-cursor.png) - Add Claude Code validation screenshot (claude-code-validation.png) - Format code examples with consistent double quotes - Align feature parity matrix table - Add .cursorrules and bun.lock to .gitignore * fix(lint): resolve all ESLint errors in compatibility layer - Remove async from methods with no await expressions (toOAC, fromOAC, loadFromFile) - Wrap Promise-returning methods with Promise.resolve/reject - Fix unsafe type assertions (replace 'as any' with proper type validation) - Add missing getConfigPath() method to WindsurfAdapter - Validate AgentCategory enum values before assignment - Fix return type compatibility issues All 584 tests passing, 0 ESLint errors (7 acceptable warnings for missing callback return types) * chore: update package.json author and contributors metadata - Update author from string to object format with name and GitHub URL - Add Alexander Daza as contributor with email and GitHub profile - Maintain npm package.json standard format for author/contributors fields
1486 lines
43 KiB
TypeScript
1486 lines
43 KiB
TypeScript
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
|
|
*
|
|
* 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
|
|
*/
|
|
|
|
describe("ClaudeAdapter", () => {
|
|
let adapter: ClaudeAdapter;
|
|
|
|
beforeEach(() => {
|
|
adapter = new ClaudeAdapter();
|
|
});
|
|
|
|
// ============================================================================
|
|
// ADAPTER IDENTITY
|
|
// ============================================================================
|
|
|
|
describe("adapter identity", () => {
|
|
it("has correct name", () => {
|
|
expect(adapter.name).toBe("claude");
|
|
});
|
|
|
|
it("has correct displayName", () => {
|
|
expect(adapter.displayName).toBe("Claude Code");
|
|
});
|
|
|
|
it("returns correct config path", () => {
|
|
expect(adapter.getConfigPath()).toBe(".claude/");
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// CAPABILITIES
|
|
// ============================================================================
|
|
|
|
describe("getCapabilities()", () => {
|
|
it("returns correct capabilities object", () => {
|
|
const capabilities = adapter.getCapabilities();
|
|
|
|
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");
|
|
});
|
|
|
|
it("includes appropriate notes", () => {
|
|
const capabilities = adapter.getCapabilities();
|
|
|
|
expect(capabilities.notes).toBeDefined();
|
|
expect(capabilities.notes?.length).toBeGreaterThan(0);
|
|
expect(capabilities.notes?.some((n) => n.includes("permissions"))).toBe(
|
|
true
|
|
);
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// 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",
|
|
});
|
|
|
|
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");
|
|
});
|
|
|
|
it("parses config with model", async () => {
|
|
const source = JSON.stringify({
|
|
name: "Agent",
|
|
description: "Test",
|
|
model: "claude-sonnet-4-20250514",
|
|
systemPrompt: "Prompt",
|
|
});
|
|
|
|
const result = await adapter.toOAC(source);
|
|
|
|
expect(result.frontmatter.model).toBe("claude-sonnet-4");
|
|
});
|
|
|
|
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);
|
|
|
|
expect(result.frontmatter.tools).toEqual({
|
|
read: true,
|
|
write: true,
|
|
bash: true,
|
|
});
|
|
});
|
|
|
|
it("parses config with tools string", async () => {
|
|
const source = JSON.stringify({
|
|
name: "Agent",
|
|
description: "Test",
|
|
tools: "Read, Write, Edit",
|
|
systemPrompt: "Prompt",
|
|
});
|
|
|
|
const result = await adapter.toOAC(source);
|
|
|
|
expect(result.frontmatter.tools).toEqual({
|
|
read: true,
|
|
write: true,
|
|
edit: true,
|
|
});
|
|
});
|
|
|
|
it("parses config with skills", async () => {
|
|
const source = JSON.stringify({
|
|
name: "Agent",
|
|
description: "Test",
|
|
skills: ["skill1", "skill2"],
|
|
systemPrompt: "Prompt",
|
|
});
|
|
|
|
const result = await adapter.toOAC(source);
|
|
|
|
expect(result.frontmatter.skills).toEqual(["skill1", "skill2"]);
|
|
});
|
|
|
|
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",
|
|
});
|
|
|
|
const result = await adapter.toOAC(source);
|
|
|
|
expect(result.frontmatter.hooks).toBeDefined();
|
|
expect(result.frontmatter.hooks?.length).toBe(1);
|
|
expect(result.frontmatter.hooks?.[0].event).toBe("PreToolUse");
|
|
});
|
|
|
|
it("handles missing optional fields gracefully", async () => {
|
|
const source = JSON.stringify({
|
|
name: "MinimalAgent",
|
|
description: "Minimal",
|
|
});
|
|
|
|
const result = await adapter.toOAC(source);
|
|
|
|
expect(result.frontmatter.name).toBe("MinimalAgent");
|
|
expect(result.systemPrompt).toBe("");
|
|
expect(result.frontmatter.tools).toBeUndefined();
|
|
expect(result.frontmatter.skills).toBeUndefined();
|
|
});
|
|
|
|
it("parses invalid JSON as markdown (subagent fallback)", async () => {
|
|
const source = "not valid json";
|
|
|
|
const result = await adapter.toOAC(source);
|
|
|
|
// Falls back to markdown parsing
|
|
expect(result.frontmatter.mode).toBe("subagent");
|
|
expect(result.systemPrompt).toBe("not valid json");
|
|
});
|
|
|
|
it("parses non-object JSON string as markdown (subagent fallback)", async () => {
|
|
const source = JSON.stringify("just a string");
|
|
|
|
const result = await adapter.toOAC(source);
|
|
|
|
// Falls back to markdown parsing
|
|
expect(result.frontmatter.mode).toBe("subagent");
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// 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);
|
|
|
|
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.");
|
|
});
|
|
|
|
it("parses agent.md with model in frontmatter", async () => {
|
|
const source = `---
|
|
name: Agent
|
|
description: Test
|
|
model: claude-opus-4
|
|
---
|
|
|
|
Prompt`;
|
|
|
|
const result = await adapter.toOAC(source);
|
|
|
|
expect(result.frontmatter.model).toBe("claude-opus-4");
|
|
});
|
|
|
|
it("parses agent.md with tools array in frontmatter", async () => {
|
|
const source = `---
|
|
name: Agent
|
|
description: Test
|
|
tools: ["Read", "Write", "Bash"]
|
|
---
|
|
|
|
Prompt`;
|
|
|
|
const result = await adapter.toOAC(source);
|
|
|
|
expect(result.frontmatter.tools).toEqual({
|
|
read: true,
|
|
write: true,
|
|
bash: true,
|
|
});
|
|
});
|
|
|
|
it("parses agent.md with skills in frontmatter", async () => {
|
|
const source = `---
|
|
name: Agent
|
|
description: Test
|
|
skills: ["skill1", "skill2"]
|
|
---
|
|
|
|
Prompt`;
|
|
|
|
const result = await adapter.toOAC(source);
|
|
|
|
expect(result.frontmatter.skills).toEqual(["skill1", "skill2"]);
|
|
});
|
|
|
|
it("handles agent.md without frontmatter as markdown content", async () => {
|
|
const source = "No frontmatter here, just markdown content";
|
|
|
|
const result = await adapter.toOAC(source);
|
|
|
|
expect(result.systemPrompt).toBe("No frontmatter here, just markdown content");
|
|
expect(result.frontmatter.mode).toBe("subagent");
|
|
});
|
|
|
|
it("preserves multiline system prompt", async () => {
|
|
const source = `---
|
|
name: Agent
|
|
description: Test
|
|
---
|
|
|
|
You are a helpful assistant.
|
|
You should be friendly and professional.
|
|
Always provide detailed responses.`;
|
|
|
|
const result = await adapter.toOAC(source);
|
|
|
|
expect(result.systemPrompt).toContain("You are a helpful assistant");
|
|
expect(result.systemPrompt).toContain("Always provide detailed responses");
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// fromOAC() - CONVERTING TO CLAUDE CONFIG.JSON
|
|
// ============================================================================
|
|
|
|
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,
|
|
});
|
|
|
|
it("converts primary agent to config.json", async () => {
|
|
const agent = createOpenAgent();
|
|
|
|
const result = await adapter.fromOAC(agent);
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.configs).toHaveLength(1);
|
|
expect(result.configs[0].fileName).toBe(".claude/config.json");
|
|
|
|
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");
|
|
});
|
|
|
|
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);
|
|
|
|
expect(config.tools).toEqual(["Read", "Write", "Bash"]);
|
|
});
|
|
|
|
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);
|
|
|
|
expect(config.skills).toEqual(["skill1", "skill2"]);
|
|
});
|
|
|
|
it("includes hooks in config.json", async () => {
|
|
const hook: HookDefinition = {
|
|
event: "PreToolUse",
|
|
matchers: ["*.txt"],
|
|
commands: [{ type: "command", command: "validate" }],
|
|
};
|
|
|
|
const agent = createOpenAgent({
|
|
frontmatter: {
|
|
hooks: [hook],
|
|
},
|
|
});
|
|
|
|
const result = await adapter.fromOAC(agent);
|
|
const config = JSON.parse(result.configs[0].content);
|
|
|
|
expect(config.hooks).toBeDefined();
|
|
expect(config.hooks.PreToolUse).toBeDefined();
|
|
});
|
|
|
|
it("warns when temperature is set (unsupported)", async () => {
|
|
const agent = createOpenAgent({
|
|
frontmatter: {
|
|
temperature: 0.7,
|
|
},
|
|
});
|
|
|
|
const result = await adapter.fromOAC(agent);
|
|
|
|
expect(result.warnings.some((w) => w.includes("temperature"))).toBe(true);
|
|
});
|
|
|
|
it("warns when maxSteps is set (unsupported)", async () => {
|
|
const agent = createOpenAgent({
|
|
frontmatter: {
|
|
maxSteps: 10,
|
|
},
|
|
});
|
|
|
|
const result = await adapter.fromOAC(agent);
|
|
|
|
expect(result.warnings.some((w) => w.includes("maxSteps"))).toBe(true);
|
|
});
|
|
|
|
it("includes validationWarnings in result", async () => {
|
|
const agent = createOpenAgent({
|
|
frontmatter: {
|
|
name: "",
|
|
description: "",
|
|
},
|
|
});
|
|
|
|
const result = await adapter.fromOAC(agent);
|
|
|
|
expect(result.warnings.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it("handles mixed permission rules gracefully", async () => {
|
|
const agent = createOpenAgent({
|
|
frontmatter: {
|
|
permission: {
|
|
read: "allow",
|
|
write: "ask",
|
|
bash: "deny",
|
|
},
|
|
},
|
|
});
|
|
|
|
const result = await adapter.fromOAC(agent);
|
|
|
|
// Should include some form of warning or degradation
|
|
expect(result.warnings.length).toBeGreaterThanOrEqual(0);
|
|
expect(result.success).toBe(true);
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// fromOAC() - CONVERTING TO CLAUDE AGENT.MD (SUBAGENTS)
|
|
// ============================================================================
|
|
|
|
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,
|
|
});
|
|
|
|
it("converts subagent to agent.md file", async () => {
|
|
const agent = createSubagent();
|
|
|
|
const result = await adapter.fromOAC(agent);
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.configs).toHaveLength(1);
|
|
expect(result.configs[0].fileName).toBe(".claude/agents/CodeAnalyzer.md");
|
|
});
|
|
|
|
it("generates proper YAML frontmatter in agent.md", async () => {
|
|
const agent = createSubagent();
|
|
|
|
const result = await adapter.fromOAC(agent);
|
|
const content = result.configs[0].content;
|
|
|
|
expect(content).toMatch(/^---/);
|
|
expect(content).toMatch(/name: "CodeAnalyzer"/);
|
|
expect(content).toMatch(/description: "Analyzes code"/);
|
|
expect(content).toContain("---\n\n");
|
|
});
|
|
|
|
it("includes system prompt in agent.md body", async () => {
|
|
const agent = createSubagent({
|
|
systemPrompt: "Analyze code quality\nCheck for best practices",
|
|
});
|
|
|
|
const result = await adapter.fromOAC(agent);
|
|
const content = result.configs[0].content;
|
|
|
|
expect(content).toContain("Analyze code quality");
|
|
expect(content).toContain("Check for best practices");
|
|
});
|
|
|
|
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;
|
|
|
|
expect(content).toContain("Read");
|
|
expect(content).toContain("Bash");
|
|
expect(content).toContain("tools");
|
|
});
|
|
|
|
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;
|
|
|
|
expect(content).toContain("model");
|
|
expect(content).toContain("claude-opus-4");
|
|
});
|
|
|
|
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;
|
|
|
|
expect(content).toContain("permissionMode");
|
|
expect(content).toContain("bypassPermissions");
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// fromOAC() - SKILLS GENERATION FROM CONTEXTS
|
|
// ============================================================================
|
|
|
|
describe("fromOAC() - generating skills from contexts", () => {
|
|
it("generates skill files from contexts", async () => {
|
|
const agent: OpenAgent = {
|
|
frontmatter: {
|
|
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",
|
|
},
|
|
],
|
|
};
|
|
|
|
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/);
|
|
});
|
|
|
|
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/")
|
|
);
|
|
|
|
expect(skillConfig?.fileName).toMatch(/react-hooks-guide/);
|
|
});
|
|
|
|
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/")
|
|
);
|
|
|
|
expect(skillConfig?.content).toContain("Priority: high");
|
|
});
|
|
|
|
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/")
|
|
);
|
|
|
|
expect(skillConfigs).toHaveLength(3);
|
|
});
|
|
|
|
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/")
|
|
);
|
|
|
|
expect(skillConfig?.content).toContain("Context from");
|
|
expect(skillConfig?.content).toContain("styles.md");
|
|
});
|
|
|
|
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/")
|
|
);
|
|
|
|
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);
|
|
});
|
|
|
|
it("warns about granular permission degradation", () => {
|
|
const agent = createAgent({
|
|
permission: {
|
|
read: { "file1.txt": "allow", "file2.txt": "deny" },
|
|
},
|
|
});
|
|
|
|
const warnings = adapter.validateConversion(agent);
|
|
|
|
expect(
|
|
warnings.some((w) => w.includes("granular permissions"))
|
|
).toBe(true);
|
|
});
|
|
|
|
it("does not warn about simple permission rules", () => {
|
|
const agent = createAgent({
|
|
permission: { read: "allow", write: "allow" },
|
|
});
|
|
|
|
const warnings = adapter.validateConversion(agent);
|
|
|
|
expect(
|
|
warnings.some((w) => w.includes("granular permissions"))
|
|
).toBe(false);
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// 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);
|
|
|
|
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);
|
|
|
|
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);
|
|
|
|
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);
|
|
|
|
expect(result.frontmatter.model).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// MODEL MAPPING (OAC to Claude)
|
|
// ============================================================================
|
|
|
|
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");
|
|
});
|
|
|
|
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();
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// 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,
|
|
});
|
|
});
|
|
|
|
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);
|
|
|
|
expect(Object.keys(result.frontmatter.tools || {})).toContain("read");
|
|
expect(Object.keys(result.frontmatter.tools || {})).toContain("write");
|
|
expect(Object.keys(result.frontmatter.tools || {})).toContain("bash");
|
|
});
|
|
|
|
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("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");
|
|
});
|
|
|
|
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);
|
|
|
|
expect(config.permissionMode).toBe("dontAsk");
|
|
});
|
|
|
|
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);
|
|
|
|
expect(config.permissionMode).toBe("default");
|
|
});
|
|
|
|
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);
|
|
|
|
expect(result.warnings.some((w) => w.includes("permission"))).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);
|
|
|
|
expect(result.frontmatter.skills).toEqual(["skill1", "skill2", "skill3"]);
|
|
});
|
|
|
|
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);
|
|
|
|
expect(result.frontmatter.skills).toEqual(["skill1", "skill2"]);
|
|
});
|
|
|
|
it("handles empty skills gracefully", async () => {
|
|
const source = JSON.stringify({
|
|
name: "Agent",
|
|
description: "Test",
|
|
skills: [],
|
|
systemPrompt: "Prompt",
|
|
});
|
|
|
|
const result = await adapter.toOAC(source);
|
|
|
|
expect(result.frontmatter.skills).toEqual([]);
|
|
});
|
|
|
|
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);
|
|
|
|
expect(config.skills).toEqual(["skill1", "skill2"]);
|
|
});
|
|
|
|
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: [],
|
|
};
|
|
|
|
const result = await adapter.fromOAC(agent);
|
|
const config = JSON.parse(result.configs[0].content);
|
|
|
|
expect(config.skills).toContain("skill1");
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// HOOK MAPPING
|
|
// ============================================================================
|
|
|
|
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);
|
|
});
|
|
|
|
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);
|
|
|
|
expect(config.hooks.PreToolUse).toBeDefined();
|
|
expect(config.hooks.PreToolUse[0].matcher).toBe("*.txt");
|
|
});
|
|
|
|
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);
|
|
|
|
expect(config.hooks.AgentStart[0].matcher).toBe("*");
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// 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));
|
|
|
|
// Verify core properties are preserved
|
|
expect(backToOAC.frontmatter.name).toBe(original.frontmatter.name);
|
|
expect(backToOAC.frontmatter.description).toBe(
|
|
original.frontmatter.description
|
|
);
|
|
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: [],
|
|
};
|
|
|
|
const toClaudeResult = await adapter.fromOAC(original);
|
|
const claudeConfig = JSON.parse(toClaudeResult.configs[0].content);
|
|
const backToOAC = await adapter.toOAC(JSON.stringify(claudeConfig));
|
|
|
|
expect(backToOAC.frontmatter.model).toBe("claude-opus-4");
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// EDGE CASES & ERROR HANDLING
|
|
// ============================================================================
|
|
|
|
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("-");
|
|
});
|
|
|
|
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("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");
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// 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("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);
|
|
|
|
expect(result.capabilities).toBeDefined();
|
|
expect(result.capabilities?.name).toBe("claude");
|
|
});
|
|
|
|
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);
|
|
|
|
expect(result.configs[0].encoding).toBe("utf-8");
|
|
});
|
|
});
|
|
});
|