From 2d546bedafc4476423cf32610b950331a632b83a Mon Sep 17 00:00:00 2001 From: Mikkel Georgsen Date: Wed, 1 Apr 2026 10:56:39 +0200 Subject: [PATCH] [nexus] test(18-01): add failing tests for adapter skill config resolver - Add AdapterSkillFormat type and AdapterSkillConfig interface to types.ts - Create stub adapter-skill-config.ts (throws not implemented) - Re-export new types and functions from index.ts - Add comprehensive test file covering all 10 adapter types and fallback --- .../adapter-utils/src/adapter-skill-config.ts | 17 ++ packages/adapter-utils/src/index.ts | 2 + packages/adapter-utils/src/types.ts | 33 ++++ .../__tests__/adapter-skill-config.test.ts | 154 ++++++++++++++++++ 4 files changed, 206 insertions(+) create mode 100644 packages/adapter-utils/src/adapter-skill-config.ts create mode 100644 server/src/__tests__/adapter-skill-config.test.ts diff --git a/packages/adapter-utils/src/adapter-skill-config.ts b/packages/adapter-utils/src/adapter-skill-config.ts new file mode 100644 index 00000000..051f835e --- /dev/null +++ b/packages/adapter-utils/src/adapter-skill-config.ts @@ -0,0 +1,17 @@ +import type { AdapterSkillConfig } from "./types.js"; + +/** + * Returns the AdapterSkillConfig for the given adapter type. + * Unknown types return a safe fallback config with supportsInstall: false. + * Never throws. + */ +export function resolveAdapterSkillConfig(_adapterType: string): AdapterSkillConfig { + throw new Error("not implemented"); +} + +/** + * Returns all registered adapter skill configs (one per known adapter type). + */ +export function listAdapterSkillConfigs(): readonly AdapterSkillConfig[] { + throw new Error("not implemented"); +} diff --git a/packages/adapter-utils/src/index.ts b/packages/adapter-utils/src/index.ts index 943db253..318deda5 100644 --- a/packages/adapter-utils/src/index.ts +++ b/packages/adapter-utils/src/index.ts @@ -1,3 +1,5 @@ +export type { AdapterSkillFormat, AdapterSkillConfig } from "./types.js"; +export { resolveAdapterSkillConfig, listAdapterSkillConfigs } from "./adapter-skill-config.js"; export type { AdapterAgent, AdapterRuntime, diff --git a/packages/adapter-utils/src/types.ts b/packages/adapter-utils/src/types.ts index 9337fad0..ab51dd47 100644 --- a/packages/adapter-utils/src/types.ts +++ b/packages/adapter-utils/src/types.ts @@ -354,3 +354,36 @@ export interface CreateConfigValues { heartbeatEnabled: boolean; intervalSec: number; } + +// --------------------------------------------------------------------------- +// Adapter skill config types — maps adapter type to skill directory and capabilities +// --------------------------------------------------------------------------- + +/** Format of skill files recognized by the adapter. */ +export type AdapterSkillFormat = "skill-md" | "none"; + +/** + * Static configuration describing where an adapter stores skills and whether + * the Nexus skill install/uninstall flow is supported for it. + */ +export interface AdapterSkillConfig { + /** The adapter type string (e.g. "claude_local", "hermes_local"). */ + adapterType: string; + /** + * Path to the directory where skills are installed for this adapter. + * Uses `~` as home-directory prefix. Null when skills are not supported. + */ + skillDir: string | null; + /** + * Path to the adapter's native skill directory when it differs from skillDir, + * e.g. when the adapter has its own concept of a skills folder. + * Null for most adapters. + */ + nativeSkillDir?: string | null; + /** Format of skill documents used by this adapter. */ + format: AdapterSkillFormat; + /** Whether the Nexus install/uninstall flow is supported for this adapter. */ + supportsInstall: boolean; + /** Human-readable reason why skills are not supported (when supportsInstall is false). */ + unsupportedReason: string | null; +} diff --git a/server/src/__tests__/adapter-skill-config.test.ts b/server/src/__tests__/adapter-skill-config.test.ts new file mode 100644 index 00000000..3b47fbc9 --- /dev/null +++ b/server/src/__tests__/adapter-skill-config.test.ts @@ -0,0 +1,154 @@ +import { describe, expect, it } from "vitest"; +import { + resolveAdapterSkillConfig, + listAdapterSkillConfigs, +} from "@paperclipai/adapter-utils"; + +// ADAPT-01: resolveAdapterSkillConfig exists and returns AdapterSkillConfig for any string input +describe("resolveAdapterSkillConfig", () => { + // ADAPT-02: claude_local + it("returns correct config for claude_local", () => { + const cfg = resolveAdapterSkillConfig("claude_local"); + expect(cfg.adapterType).toBe("claude_local"); + expect(cfg.skillDir).toBe("~/.claude/skills/"); + expect(cfg.format).toBe("skill-md"); + expect(cfg.supportsInstall).toBe(true); + expect(cfg.unsupportedReason).toBeNull(); + }); + + // ADAPT-03: hermes_local + it("returns correct config for hermes_local", () => { + const cfg = resolveAdapterSkillConfig("hermes_local"); + expect(cfg.adapterType).toBe("hermes_local"); + expect(cfg.skillDir).toBe("~/.hermes/skills/"); + expect(cfg.nativeSkillDir).toBe("~/.hermes/skills/"); + expect(cfg.supportsInstall).toBe(true); + expect(cfg.unsupportedReason).toBeNull(); + }); + + // ADAPT-04: openclaw_gateway + it("returns correct config for openclaw_gateway", () => { + const cfg = resolveAdapterSkillConfig("openclaw_gateway"); + expect(cfg.adapterType).toBe("openclaw_gateway"); + expect(cfg.skillDir).toBe("~/.openclaw/skills/"); + expect(cfg.format).toBe("skill-md"); + expect(cfg.supportsInstall).toBe(true); + expect(cfg.unsupportedReason).toBeNull(); + }); + + // ADAPT-05: codex_local + it("returns correct config for codex_local", () => { + const cfg = resolveAdapterSkillConfig("codex_local"); + expect(cfg.adapterType).toBe("codex_local"); + expect(cfg.skillDir).toBe("~/.agents/skills/"); + expect(cfg.format).toBe("skill-md"); + expect(cfg.supportsInstall).toBe(true); + expect(cfg.unsupportedReason).toBeNull(); + }); + + // ADAPT-06: cursor + it("returns correct config for cursor", () => { + const cfg = resolveAdapterSkillConfig("cursor"); + expect(cfg.adapterType).toBe("cursor"); + expect(cfg.skillDir).toBe("~/.cursor/skills/"); + expect(cfg.format).toBe("skill-md"); + expect(cfg.supportsInstall).toBe(true); + expect(cfg.unsupportedReason).toBeNull(); + }); + + // ADAPT-07: opencode_local + it("returns correct config for opencode_local", () => { + const cfg = resolveAdapterSkillConfig("opencode_local"); + expect(cfg.adapterType).toBe("opencode_local"); + expect(cfg.skillDir).toBe("~/.config/opencode/skills/"); + expect(cfg.format).toBe("skill-md"); + expect(cfg.supportsInstall).toBe(true); + expect(cfg.unsupportedReason).toBeNull(); + }); + + // ADAPT-08: pi_local and gemini_local + it("returns correct config for pi_local", () => { + const cfg = resolveAdapterSkillConfig("pi_local"); + expect(cfg.adapterType).toBe("pi_local"); + expect(cfg.skillDir).toBe("~/.pi/agent/skills/"); + expect(cfg.format).toBe("skill-md"); + expect(cfg.supportsInstall).toBe(true); + }); + + it("returns correct config for gemini_local", () => { + const cfg = resolveAdapterSkillConfig("gemini_local"); + expect(cfg.adapterType).toBe("gemini_local"); + expect(cfg.skillDir).toBe("~/.gemini/skills/"); + expect(cfg.format).toBe("skill-md"); + expect(cfg.supportsInstall).toBe(true); + }); + + // ADAPT-09: process and http — no skills supported + it("returns unsupported config for process adapter", () => { + const cfg = resolveAdapterSkillConfig("process"); + expect(cfg.adapterType).toBe("process"); + expect(cfg.skillDir).toBeNull(); + expect(cfg.format).toBe("none"); + expect(cfg.supportsInstall).toBe(false); + expect(cfg.unsupportedReason).toBeTruthy(); + }); + + it("returns unsupported config for http adapter", () => { + const cfg = resolveAdapterSkillConfig("http"); + expect(cfg.adapterType).toBe("http"); + expect(cfg.skillDir).toBeNull(); + expect(cfg.format).toBe("none"); + expect(cfg.supportsInstall).toBe(false); + expect(cfg.unsupportedReason).toBeTruthy(); + }); + + // ADAPT-10: unknown adapter type returns fallback, does not throw + it("returns fallback config for unknown adapter type without throwing", () => { + const cfg = resolveAdapterSkillConfig("totally_unknown_adapter"); + expect(cfg.adapterType).toBe("totally_unknown_adapter"); + expect(cfg.supportsInstall).toBe(false); + expect(cfg.format).toBe("none"); + // should not throw — just a safe fallback + }); + + it("populates adapterType field for unknown adapter types", () => { + const cfg = resolveAdapterSkillConfig("some_future_adapter"); + expect(cfg.adapterType).toBe("some_future_adapter"); + }); +}); + +// listAdapterSkillConfigs returns all 10 registered configs +describe("listAdapterSkillConfigs", () => { + it("returns an array of all 10 registered adapter configs", () => { + const configs = listAdapterSkillConfigs(); + expect(configs).toHaveLength(10); + }); + + it("contains entries for all expected adapter types", () => { + const configs = listAdapterSkillConfigs(); + const types = configs.map((c) => c.adapterType); + expect(types).toContain("claude_local"); + expect(types).toContain("hermes_local"); + expect(types).toContain("openclaw_gateway"); + expect(types).toContain("codex_local"); + expect(types).toContain("cursor"); + expect(types).toContain("opencode_local"); + expect(types).toContain("pi_local"); + expect(types).toContain("gemini_local"); + expect(types).toContain("process"); + expect(types).toContain("http"); + }); + + it("has no TBD or empty stub entries — all configs are fully populated", () => { + const configs = listAdapterSkillConfigs(); + for (const cfg of configs) { + expect(cfg.adapterType).toBeTruthy(); + expect(cfg.format).toMatch(/^(skill-md|none)$/); + if (cfg.supportsInstall) { + expect(cfg.skillDir).toBeTruthy(); + } else { + expect(cfg.skillDir).toBeNull(); + } + } + }); +});