import { existsSync, mkdtempSync, mkdirSync, rmSync, writeFileSync } from "fs"; import { tmpdir } from "os"; import { join } from "path"; import { afterEach, describe, expect, it, vi } from "vitest"; import { filterPendingReinjectForConsume, tryConsumeDeferredReinject } from "../src/reinject"; import { createDefaultSettings } from "../src/settings"; import { createInitialState, trackSkill } from "../src/state"; const tempDirs: string[] = []; afterEach(() => { for (const dir of tempDirs.splice(0)) { rmSync(dir, { recursive: true, force: true }); } }); function tempSkillDir(name: string): { filePath: string; baseDir: string } { const root = mkdtempSync(join(tmpdir(), "pi-skill-reinject-consume-")); tempDirs.push(root); const baseDir = join(root, name); mkdirSync(baseDir, { recursive: true }); const filePath = join(baseDir, "SKILL.md"); writeFileSync(filePath, "# skill\n", "utf8"); return { filePath, baseDir }; } describe("filterPendingReinjectForConsume", () => { it("keeps registered pending skills", () => { const state = createInitialState(); trackSkill(state, { name: "alpha", filePath: "/skills/alpha/SKILL.md", baseDir: "/skills/alpha", source: "slash", }); expect( filterPendingReinjectForConsume( ["alpha"], state, createDefaultSettings(), [{ name: "alpha" }], ), ).toEqual(["alpha"]); }); it("includes unregistered skill from disk when requireRegistered is false", () => { const { filePath, baseDir } = tempSkillDir("loose"); const state = createInitialState(); trackSkill(state, { name: "loose", filePath, baseDir, source: "slash" }); const notify = vi.fn(); const ctx = { hasUI: true, ui: { notify } } as never; expect( filterPendingReinjectForConsume(["loose"], state, createDefaultSettings(), [], ctx), ).toEqual(["loose"]); expect(notify).toHaveBeenCalledWith('skill-reinject: re-injected "loose" from disk', "info"); }); it("skips unregistered skill when requireRegistered is true", () => { const { filePath, baseDir } = tempSkillDir("strict"); const state = createInitialState(); trackSkill(state, { name: "strict", filePath, baseDir, source: "slash" }); const notify = vi.fn(); const ctx = { hasUI: true, ui: { notify } } as never; const settings = createDefaultSettings(); settings.requireRegistered = true; expect(filterPendingReinjectForConsume(["strict"], state, settings, [], ctx)).toEqual([]); expect(notify).toHaveBeenCalledWith( 'skill-reinject: skipped "strict" — no longer registered', "warning", ); }); it("skips when tracked file is missing on disk", () => { const state = createInitialState(); trackSkill(state, { name: "missing", filePath: "/no/such/SKILL.md", baseDir: "/no/such", source: "slash", }); expect(existsSync("/no/such/SKILL.md")).toBe(false); const notify = vi.fn(); const ctx = { hasUI: true, ui: { notify } } as never; expect( filterPendingReinjectForConsume(["missing"], state, createDefaultSettings(), [], ctx), ).toEqual([]); expect(notify).toHaveBeenCalledWith( 'skill-reinject: skipped "missing" — SKILL.md not found on disk', "warning", ); }); }); describe("tryConsumeDeferredReinject loose path", () => { it("injects skill block from tracked filePath when not registered", () => { const { filePath, baseDir } = tempSkillDir("loose"); const state = createInitialState(); trackSkill(state, { name: "loose", filePath, baseDir, source: "slash" }); state.pendingReinject = ["loose"]; const result = tryConsumeDeferredReinject(state, createDefaultSettings(), [], undefined); expect(result?.message?.content).toContain('