[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
This commit is contained in:
Mikkel Georgsen 2026-04-01 10:56:39 +02:00
parent f00a30ca21
commit b010708e00
4 changed files with 206 additions and 0 deletions

View file

@ -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");
}

View file

@ -1,3 +1,5 @@
export type { AdapterSkillFormat, AdapterSkillConfig } from "./types.js";
export { resolveAdapterSkillConfig, listAdapterSkillConfigs } from "./adapter-skill-config.js";
export type {
AdapterAgent,
AdapterRuntime,

View file

@ -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;
}

View file

@ -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();
}
}
});
});