import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { fileURLToPath } from "node:url"; import { afterEach, expect, it } from "vitest"; import { fauxProvider } from "@earendil-works/pi-ai/providers/faux"; import { createAgentSession, DefaultResourceLoader, ModelRuntime, SessionManager, SettingsManager, type ResolvedSkillInfo } from "@selesai/code"; import type { Skill } from "../../core/skills.ts"; import { compactSkillIndex, eligibleSkills, showSkill } from "./index.ts"; const roots: string[] = []; afterEach(() => { for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true }); }); function rootDir() { const root = mkdtempSync(join(tmpdir(), "selesai-skills-index-")); roots.push(root); return root; } function resolved(name: string, filePath: string, description = `${name} procedure`, disableModelInvocation = false): ResolvedSkillInfo { return { name, filePath, description, scope: "user", disableModelInvocation }; } function promptSkill(name: string): Skill { return { name, description: `${name} procedure`, filePath: `/skills/${name}/SKILL.md`, baseDir: `/skills/${name}`, sourceInfo: { path: `/skills/${name}/SKILL.md`, source: "user", scope: "user", origin: "top-level" }, disableModelInvocation: false }; } it("keeps first eligible resolution and excludes model-ineligible entries", () => { const first = resolved("review", "/first"); expect(eligibleSkills([resolved("hidden", "/hidden", "Hidden", true), first, resolved("review", "/second")])).toEqual([first]); }); it("collapses only a nonempty index with an available show tool without mutating it", () => { const skills = [promptSkill("review"), promptSkill("research")]; expect(compactSkillIndex(skills, false)).toBe(skills); expect(compactSkillIndex([], true)).toEqual([]); const index = compactSkillIndex(skills, true); expect(index).toHaveLength(1); expect(index[0].description).toContain("skill_show"); expect(index[0].description).not.toContain("capability_discover"); expect(skills).toHaveLength(2); }); it("lists and filters metadata without opening files, bounds results, and supports Unicode", async () => { const skills = [resolved("review", "/absent", "Check code"), resolved("日本語", "/absent", "日本語 手順")]; const list = await showSkill(skills, { limit: 1 }); expect(list.details).toMatchObject({ loaded: false, total: 2, truncated: true }); expect(list.content[0].text).toContain("Narrow query"); expect((await showSkill(skills, { query: "日本語 手順" })).content[0].text).toContain("日本語"); expect((await showSkill(skills, { query: "review missing" })).content[0].text).toBe("No eligible skills match."); }); it("loads full instructions and reports relative-reference directory", async () => { const path = join(rootDir(), "SKILL.md"); const body = "---\nname: review\ndescription: Review\n---\nRead examples/example.ts.\n"; writeFileSync(path, body); const result = await showSkill([resolved("review", path)], { name: "review" }); expect(result.details).toMatchObject({ loaded: true, complete: true, filePath: path }); expect(result.content[0].text).toContain(body); expect(result.content[0].text).toContain(join(path, "..")); }); it("rejects arbitrary paths, disabled names, missing files, and cancellation", async () => { const hidden = resolved("hidden", "/absent", "Hidden", true); await expect(showSkill([hidden], { name: "hidden" })).rejects.toThrow("model-ineligible"); await expect(showSkill([], { name: "../../secret" })).rejects.toThrow("Unknown"); await expect(showSkill([resolved("missing", "/absent")], { name: "missing" })).rejects.toThrow(); const controller = new AbortController(); controller.abort(); await expect(showSkill([], {}, controller.signal)).rejects.toThrow(); }); it.each(["bytes", "lines"])("marks %s truncation honestly and supplies the original path", async (mode) => { const path = join(rootDir(), "SKILL.md"); writeFileSync(path, mode === "bytes" ? "x".repeat(60_000) : "instruction\n".repeat(2100)); const result = await showSkill([resolved("large", path)], { name: "large" }); expect(result.details).toMatchObject({ loaded: true, complete: false, filePath: path }); expect(result.content[0].text).toContain("Instructions are incomplete"); expect(result.content[0].text).not.toContain("full instructions are embedded"); expect(Buffer.byteLength(result.content[0].text)).toBeLessThan(53_000); }); it.each([false, true])("real SDK preserves loadout and resolved catalog (excluded show=%s)", async (excluded) => { const root = rootDir(); const cwd = join(root, "project"); const agentDir = join(root, "agent"); mkdirSync(cwd); mkdirSync(join(agentDir, "skills", "review"), { recursive: true }); writeFileSync(join(agentDir, "skills", "review", "SKILL.md"), "---\nname: review\ndescription: Review code carefully\n---\nFull review instructions.\n"); const settingsManager = SettingsManager.inMemory({ compaction: { enabled: false }, retry: { enabled: false } }); const modelRuntime = await ModelRuntime.create({ authPath: join(agentDir, "auth.json"), modelsPath: null, allowModelNetwork: false }); const faux = fauxProvider({ provider: "faux-skills-index", models: [{ id: "skills", contextWindow: 200_000 }] }); modelRuntime.registerProvider(faux.provider.id, { name: faux.provider.name, api: faux.api, apiKey: "faux", streamSimple: faux.provider.streamSimple, models: [...faux.models] }); await modelRuntime.refresh({ allowNetwork: false }); const model = modelRuntime.getModel(faux.provider.id, "skills"); if (!model) throw new Error("Missing faux model"); const loader = new DefaultResourceLoader({ cwd, agentDir, settingsManager, noExtensions: true, additionalExtensionPaths: [fileURLToPath(new URL("./index.ts", import.meta.url))], noContextFiles: true, noThemes: true, noPromptTemplates: true }); await loader.reload(); expect(loader.getExtensions().errors).toEqual([]); const { session } = await createAgentSession({ cwd, agentDir, model, modelRuntime, resourceLoader: loader, sessionManager: SessionManager.inMemory(), settingsManager, ...(excluded ? { tools: ["read"] } : {}) }); try { const baseline = session.getActiveToolNames(); await session.bindExtensions({}); expect(session.getActiveToolNames()).toEqual(baseline); expect(session.systemPrompt).toContain(excluded ? "Review code carefully" : "skill_show"); expect(session.systemPrompt).not.toContain(excluded ? "" : "Review code carefully"); expect(loader.getSkills().skills.some((skill) => skill.name === "review")).toBe(true); await session.extensionRunner.emit({ type: "agent_settled" }); expect(session.getActiveToolNames()).toEqual(baseline); } finally { session.dispose(); } });