src / mcp / skillStatus.test.ts
src / mcp / skillStatus.test.ts
import assert from "node:assert/strict";
import crypto from "node:crypto";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import test from "node:test";
import { checkSkillStatus, createSkillNoticeProvider } from "./skillStatus.js";
import type { McpResultPreset } from "./core-bundle.mjs";
const bionicPreset: McpResultPreset = { name: "bionic", includeBase64Preview: false, writeHtmlReportForMultipleResults: true, scratchpadFolderRequired: true };
const unslothPreset: McpResultPreset = { name: "unsloth", includeBase64Preview: true, writeHtmlReportForMultipleResults: false, scratchpadFolderRequired: true };
/** Builds a throwaway projectRoot with skills/<skillName>/SKILL.<preset>.md, isolated from the real repo. */
function makeProjectRoot(skillContent: string, presetName: string, skillName: "analyse-image" | "image-display" = "analyse-image"): string {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "analyse-image-skillstatus-test-"));
const skillDir = path.join(root, "skills", skillName);
fs.mkdirSync(skillDir, { recursive: true });
fs.writeFileSync(path.join(skillDir, `SKILL.${presetName}.md`), skillContent);
return root;
}
function makeHomeDir(): string {
return fs.mkdtempSync(path.join(os.tmpdir(), "analyse-image-skillstatus-home-"));
}
function sha256(content: string): string {
return crypto.createHash("sha256").update(content).digest("hex");
}
test("checkSkillStatus returns disabled when useSkills is false, without touching the filesystem", () => {
const status = checkSkillStatus(bionicPreset, false, "analyse-image", { projectRoot: "/nonexistent", homeDir: "/nonexistent" });
assert.deepEqual(status, { state: "disabled" });
});
test("checkSkillStatus returns not-installed when the client-side SKILL.md is missing", () => {
const projectRoot = makeProjectRoot("content", "bionic");
const homeDir = makeHomeDir();
const status = checkSkillStatus(bionicPreset, true, "analyse-image", { projectRoot, homeDir });
assert.equal(status.state, "not-installed");
assert.ok("installScriptPath" in status && status.installScriptPath.endsWith("install/install_bionic_skills.sh"));
});
test("checkSkillStatus returns current when installed content matches the local cache's canonical file", () => {
const projectRoot = makeProjectRoot("same content", "bionic");
const homeDir = makeHomeDir();
const installedDir = path.join(homeDir, ".lmstudio", "skills", "analyse-image");
fs.mkdirSync(installedDir, { recursive: true });
fs.writeFileSync(path.join(installedDir, "SKILL.md"), "same content");
assert.deepEqual(checkSkillStatus(bionicPreset, true, "analyse-image", { projectRoot, homeDir }), { state: "current" });
});
test("checkSkillStatus returns stale with userModified=true when the installed copy was never synced by us (no .synced-hash marker)", () => {
const projectRoot = makeProjectRoot("new content", "bionic");
const homeDir = makeHomeDir();
const installedDir = path.join(homeDir, ".lmstudio", "skills", "analyse-image");
fs.mkdirSync(installedDir, { recursive: true });
fs.writeFileSync(path.join(installedDir, "SKILL.md"), "old content");
const status = checkSkillStatus(bionicPreset, true, "analyse-image", { projectRoot, homeDir });
assert.equal(status.state, "stale");
assert.equal(status.state === "stale" && status.userModified, true);
});
test("checkSkillStatus returns stale with userModified=false when the installed copy still matches our own last-written hash (safe to auto-update)", () => {
const projectRoot = makeProjectRoot("new content", "bionic");
const homeDir = makeHomeDir();
const installedDir = path.join(homeDir, ".lmstudio", "skills", "analyse-image");
fs.mkdirSync(installedDir, { recursive: true });
fs.writeFileSync(path.join(installedDir, "SKILL.md"), "old content");
fs.writeFileSync(path.join(installedDir, ".synced-hash"), sha256("old content"));
const status = checkSkillStatus(bionicPreset, true, "analyse-image", { projectRoot, homeDir });
assert.equal(status.state, "stale");
assert.equal(status.state === "stale" && status.userModified, false);
});
test("checkSkillStatus returns stale with userModified=true when the installed copy differs from our own last-written hash (human edited it)", () => {
const projectRoot = makeProjectRoot("new content", "bionic");
const homeDir = makeHomeDir();
const installedDir = path.join(homeDir, ".lmstudio", "skills", "analyse-image");
fs.mkdirSync(installedDir, { recursive: true });
fs.writeFileSync(path.join(installedDir, "SKILL.md"), "hand-edited content");
fs.writeFileSync(path.join(installedDir, ".synced-hash"), sha256("old content")); // what WE wrote, not what's there now
const status = checkSkillStatus(bionicPreset, true, "analyse-image", { projectRoot, homeDir });
assert.equal(status.state, "stale");
assert.equal(status.state === "stale" && status.userModified, true);
});
test("checkSkillStatus uses ~/.agents/skills for unsloth, not ~/.lmstudio/skills", () => {
const projectRoot = makeProjectRoot("same content", "unsloth");
const homeDir = makeHomeDir();
const installedDir = path.join(homeDir, ".agents", "skills", "analyse-image");
fs.mkdirSync(installedDir, { recursive: true });
fs.writeFileSync(path.join(installedDir, "SKILL.md"), "same content");
assert.deepEqual(checkSkillStatus(unslothPreset, true, "analyse-image", { projectRoot, homeDir }), { state: "current" });
});
test("checkSkillStatus fails open (current) when the local cache's canonical file is missing, never throws", () => {
const projectRoot = fs.mkdtempSync(path.join(os.tmpdir(), "analyse-image-skillstatus-empty-"));
const homeDir = makeHomeDir();
const installedDir = path.join(homeDir, ".lmstudio", "skills", "analyse-image");
fs.mkdirSync(installedDir, { recursive: true });
fs.writeFileSync(path.join(installedDir, "SKILL.md"), "whatever");
assert.deepEqual(checkSkillStatus(bionicPreset, true, "analyse-image", { projectRoot, homeDir }), { state: "current" });
});
test("checkSkillStatus checks the shared 'image-display' skill's own content, but still points installScriptPath at analyse-image's install scripts (image-display has none of its own)", () => {
const projectRoot = makeProjectRoot("image-display content", "bionic", "image-display");
const homeDir = makeHomeDir();
const status = checkSkillStatus(bionicPreset, true, "image-display", { projectRoot, homeDir });
assert.equal(status.state, "not-installed");
assert.ok("installScriptPath" in status && status.installScriptPath.includes(path.join("skills", "analyse-image", "install", "install_bionic_skills.sh")));
});
test("checkSkillStatus treats 'analyse-image' and 'image-display' as independent — one current, one stale", () => {
const projectRoot = fs.mkdtempSync(path.join(os.tmpdir(), "analyse-image-skillstatus-dual-"));
fs.mkdirSync(path.join(projectRoot, "skills", "analyse-image"), { recursive: true });
fs.writeFileSync(path.join(projectRoot, "skills", "analyse-image", "SKILL.bionic.md"), "ai content");
fs.mkdirSync(path.join(projectRoot, "skills", "image-display"), { recursive: true });
fs.writeFileSync(path.join(projectRoot, "skills", "image-display", "SKILL.bionic.md"), "new id content");
const homeDir = makeHomeDir();
fs.mkdirSync(path.join(homeDir, ".lmstudio", "skills", "analyse-image"), { recursive: true });
fs.writeFileSync(path.join(homeDir, ".lmstudio", "skills", "analyse-image", "SKILL.md"), "ai content");
fs.mkdirSync(path.join(homeDir, ".lmstudio", "skills", "image-display"), { recursive: true });
fs.writeFileSync(path.join(homeDir, ".lmstudio", "skills", "image-display", "SKILL.md"), "old id content");
assert.deepEqual(checkSkillStatus(bionicPreset, true, "analyse-image", { projectRoot, homeDir }), { state: "current" });
assert.equal(checkSkillStatus(bionicPreset, true, "image-display", { projectRoot, homeDir }).state, "stale");
});
test("createSkillNoticeProvider returns the notice exactly once, then undefined", () => {
const getNotice = createSkillNoticeProvider({ state: "not-installed", installScriptPath: "/repo/skills/analyse-image/install/install_bionic_skills.sh" }, "analyse-image");
const first = getNotice();
assert.ok(first?.includes("'analyse-image'"));
assert.ok(first?.includes("install_bionic_skills.sh"));
assert.ok(first?.includes("USE_SKILLS=false"));
assert.equal(getNotice(), undefined);
assert.equal(getNotice(), undefined);
});
test("createSkillNoticeProvider mentions re-running the install script for a stale, non-user-modified skill", () => {
const getNotice = createSkillNoticeProvider({ state: "stale", installScriptPath: "/repo/skills/analyse-image/install/install_unsloth_skills.sh", userModified: false }, "analyse-image");
const first = getNotice();
assert.ok(first?.includes("out of date"));
assert.ok(first?.includes("install_unsloth_skills.sh"));
assert.equal(getNotice(), undefined);
});
test("createSkillNoticeProvider mentions the user's edits were preserved for a stale, user-modified skill", () => {
const getNotice = createSkillNoticeProvider({ state: "stale", installScriptPath: "/repo/skills/analyse-image/install/install_unsloth_skills.sh", userModified: true }, "analyse-image");
const first = getNotice();
assert.ok(first?.includes("manually edited"));
assert.ok(first?.includes("left untouched"));
});
test("createSkillNoticeProvider names 'image-display' when checking that skill", () => {
const getNotice = createSkillNoticeProvider({ state: "not-installed", installScriptPath: "/repo/skills/analyse-image/install/install_bionic_skills.sh" }, "image-display");
assert.ok(getNotice()?.includes("'image-display'"));
});
test("createSkillNoticeProvider never produces a notice for disabled or current — for the repo's own skill", () => {
assert.equal(createSkillNoticeProvider({ state: "disabled" }, "analyse-image")(), undefined);
assert.equal(createSkillNoticeProvider({ state: "current" }, "analyse-image")(), undefined);
});
test("createSkillNoticeProvider still reminds once for 'image-display' even when current — a passive SKILL.md mention alone wasn't enough in real Bionic traces", () => {
const getNotice = createSkillNoticeProvider({ state: "current" }, "image-display");
const first = getNotice();
assert.ok(first?.includes("'image-display'"));
assert.ok(first?.includes("once this session"));
assert.equal(getNotice(), undefined);
});
test("createSkillNoticeProvider never reminds for 'image-display' when disabled", () => {
assert.equal(createSkillNoticeProvider({ state: "disabled" }, "image-display")(), undefined);
});
import assert from "node:assert/strict";
import crypto from "node:crypto";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import test from "node:test";
import { checkSkillStatus, createSkillNoticeProvider } from "./skillStatus.js";
import type { McpResultPreset } from "./core-bundle.mjs";
const bionicPreset: McpResultPreset = { name: "bionic", includeBase64Preview: false, writeHtmlReportForMultipleResults: true, scratchpadFolderRequired: true };
const unslothPreset: McpResultPreset = { name: "unsloth", includeBase64Preview: true, writeHtmlReportForMultipleResults: false, scratchpadFolderRequired: true };
/** Builds a throwaway projectRoot with skills/<skillName>/SKILL.<preset>.md, isolated from the real repo. */
function makeProjectRoot(skillContent: string, presetName: string, skillName: "analyse-image" | "image-display" = "analyse-image"): string {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "analyse-image-skillstatus-test-"));
const skillDir = path.join(root, "skills", skillName);
fs.mkdirSync(skillDir, { recursive: true });
fs.writeFileSync(path.join(skillDir, `SKILL.${presetName}.md`), skillContent);
return root;
}
function makeHomeDir(): string {
return fs.mkdtempSync(path.join(os.tmpdir(), "analyse-image-skillstatus-home-"));
}
function sha256(content: string): string {
return crypto.createHash("sha256").update(content).digest("hex");
}
test("checkSkillStatus returns disabled when useSkills is false, without touching the filesystem", () => {
const status = checkSkillStatus(bionicPreset, false, "analyse-image", { projectRoot: "/nonexistent", homeDir: "/nonexistent" });
assert.deepEqual(status, { state: "disabled" });
});
test("checkSkillStatus returns not-installed when the client-side SKILL.md is missing", () => {
const projectRoot = makeProjectRoot("content", "bionic");
const homeDir = makeHomeDir();
const status = checkSkillStatus(bionicPreset, true, "analyse-image", { projectRoot, homeDir });
assert.equal(status.state, "not-installed");
assert.ok("installScriptPath" in status && status.installScriptPath.endsWith("install/install_bionic_skills.sh"));
});
test("checkSkillStatus returns current when installed content matches the local cache's canonical file", () => {
const projectRoot = makeProjectRoot("same content", "bionic");
const homeDir = makeHomeDir();
const installedDir = path.join(homeDir, ".lmstudio", "skills", "analyse-image");
fs.mkdirSync(installedDir, { recursive: true });
fs.writeFileSync(path.join(installedDir, "SKILL.md"), "same content");
assert.deepEqual(checkSkillStatus(bionicPreset, true, "analyse-image", { projectRoot, homeDir }), { state: "current" });
});
test("checkSkillStatus returns stale with userModified=true when the installed copy was never synced by us (no .synced-hash marker)", () => {
const projectRoot = makeProjectRoot("new content", "bionic");
const homeDir = makeHomeDir();
const installedDir = path.join(homeDir, ".lmstudio", "skills", "analyse-image");
fs.mkdirSync(installedDir, { recursive: true });
fs.writeFileSync(path.join(installedDir, "SKILL.md"), "old content");
const status = checkSkillStatus(bionicPreset, true, "analyse-image", { projectRoot, homeDir });
assert.equal(status.state, "stale");
assert.equal(status.state === "stale" && status.userModified, true);
});
test("checkSkillStatus returns stale with userModified=false when the installed copy still matches our own last-written hash (safe to auto-update)", () => {
const projectRoot = makeProjectRoot("new content", "bionic");
const homeDir = makeHomeDir();
const installedDir = path.join(homeDir, ".lmstudio", "skills", "analyse-image");
fs.mkdirSync(installedDir, { recursive: true });
fs.writeFileSync(path.join(installedDir, "SKILL.md"), "old content");
fs.writeFileSync(path.join(installedDir, ".synced-hash"), sha256("old content"));
const status = checkSkillStatus(bionicPreset, true, "analyse-image", { projectRoot, homeDir });
assert.equal(status.state, "stale");
assert.equal(status.state === "stale" && status.userModified, false);
});
test("checkSkillStatus returns stale with userModified=true when the installed copy differs from our own last-written hash (human edited it)", () => {
const projectRoot = makeProjectRoot("new content", "bionic");
const homeDir = makeHomeDir();
const installedDir = path.join(homeDir, ".lmstudio", "skills", "analyse-image");
fs.mkdirSync(installedDir, { recursive: true });
fs.writeFileSync(path.join(installedDir, "SKILL.md"), "hand-edited content");
fs.writeFileSync(path.join(installedDir, ".synced-hash"), sha256("old content")); // what WE wrote, not what's there now
const status = checkSkillStatus(bionicPreset, true, "analyse-image", { projectRoot, homeDir });
assert.equal(status.state, "stale");
assert.equal(status.state === "stale" && status.userModified, true);
});
test("checkSkillStatus uses ~/.agents/skills for unsloth, not ~/.lmstudio/skills", () => {
const projectRoot = makeProjectRoot("same content", "unsloth");
const homeDir = makeHomeDir();
const installedDir = path.join(homeDir, ".agents", "skills", "analyse-image");
fs.mkdirSync(installedDir, { recursive: true });
fs.writeFileSync(path.join(installedDir, "SKILL.md"), "same content");
assert.deepEqual(checkSkillStatus(unslothPreset, true, "analyse-image", { projectRoot, homeDir }), { state: "current" });
});
test("checkSkillStatus fails open (current) when the local cache's canonical file is missing, never throws", () => {
const projectRoot = fs.mkdtempSync(path.join(os.tmpdir(), "analyse-image-skillstatus-empty-"));
const homeDir = makeHomeDir();
const installedDir = path.join(homeDir, ".lmstudio", "skills", "analyse-image");
fs.mkdirSync(installedDir, { recursive: true });
fs.writeFileSync(path.join(installedDir, "SKILL.md"), "whatever");
assert.deepEqual(checkSkillStatus(bionicPreset, true, "analyse-image", { projectRoot, homeDir }), { state: "current" });
});
test("checkSkillStatus checks the shared 'image-display' skill's own content, but still points installScriptPath at analyse-image's install scripts (image-display has none of its own)", () => {
const projectRoot = makeProjectRoot("image-display content", "bionic", "image-display");
const homeDir = makeHomeDir();
const status = checkSkillStatus(bionicPreset, true, "image-display", { projectRoot, homeDir });
assert.equal(status.state, "not-installed");
assert.ok("installScriptPath" in status && status.installScriptPath.includes(path.join("skills", "analyse-image", "install", "install_bionic_skills.sh")));
});
test("checkSkillStatus treats 'analyse-image' and 'image-display' as independent — one current, one stale", () => {
const projectRoot = fs.mkdtempSync(path.join(os.tmpdir(), "analyse-image-skillstatus-dual-"));
fs.mkdirSync(path.join(projectRoot, "skills", "analyse-image"), { recursive: true });
fs.writeFileSync(path.join(projectRoot, "skills", "analyse-image", "SKILL.bionic.md"), "ai content");
fs.mkdirSync(path.join(projectRoot, "skills", "image-display"), { recursive: true });
fs.writeFileSync(path.join(projectRoot, "skills", "image-display", "SKILL.bionic.md"), "new id content");
const homeDir = makeHomeDir();
fs.mkdirSync(path.join(homeDir, ".lmstudio", "skills", "analyse-image"), { recursive: true });
fs.writeFileSync(path.join(homeDir, ".lmstudio", "skills", "analyse-image", "SKILL.md"), "ai content");
fs.mkdirSync(path.join(homeDir, ".lmstudio", "skills", "image-display"), { recursive: true });
fs.writeFileSync(path.join(homeDir, ".lmstudio", "skills", "image-display", "SKILL.md"), "old id content");
assert.deepEqual(checkSkillStatus(bionicPreset, true, "analyse-image", { projectRoot, homeDir }), { state: "current" });
assert.equal(checkSkillStatus(bionicPreset, true, "image-display", { projectRoot, homeDir }).state, "stale");
});
test("createSkillNoticeProvider returns the notice exactly once, then undefined", () => {
const getNotice = createSkillNoticeProvider({ state: "not-installed", installScriptPath: "/repo/skills/analyse-image/install/install_bionic_skills.sh" }, "analyse-image");
const first = getNotice();
assert.ok(first?.includes("'analyse-image'"));
assert.ok(first?.includes("install_bionic_skills.sh"));
assert.ok(first?.includes("USE_SKILLS=false"));
assert.equal(getNotice(), undefined);
assert.equal(getNotice(), undefined);
});
test("createSkillNoticeProvider mentions re-running the install script for a stale, non-user-modified skill", () => {
const getNotice = createSkillNoticeProvider({ state: "stale", installScriptPath: "/repo/skills/analyse-image/install/install_unsloth_skills.sh", userModified: false }, "analyse-image");
const first = getNotice();
assert.ok(first?.includes("out of date"));
assert.ok(first?.includes("install_unsloth_skills.sh"));
assert.equal(getNotice(), undefined);
});
test("createSkillNoticeProvider mentions the user's edits were preserved for a stale, user-modified skill", () => {
const getNotice = createSkillNoticeProvider({ state: "stale", installScriptPath: "/repo/skills/analyse-image/install/install_unsloth_skills.sh", userModified: true }, "analyse-image");
const first = getNotice();
assert.ok(first?.includes("manually edited"));
assert.ok(first?.includes("left untouched"));
});
test("createSkillNoticeProvider names 'image-display' when checking that skill", () => {
const getNotice = createSkillNoticeProvider({ state: "not-installed", installScriptPath: "/repo/skills/analyse-image/install/install_bionic_skills.sh" }, "image-display");
assert.ok(getNotice()?.includes("'image-display'"));
});
test("createSkillNoticeProvider never produces a notice for disabled or current — for the repo's own skill", () => {
assert.equal(createSkillNoticeProvider({ state: "disabled" }, "analyse-image")(), undefined);
assert.equal(createSkillNoticeProvider({ state: "current" }, "analyse-image")(), undefined);
});
test("createSkillNoticeProvider still reminds once for 'image-display' even when current — a passive SKILL.md mention alone wasn't enough in real Bionic traces", () => {
const getNotice = createSkillNoticeProvider({ state: "current" }, "image-display");
const first = getNotice();
assert.ok(first?.includes("'image-display'"));
assert.ok(first?.includes("once this session"));
assert.equal(getNotice(), undefined);
});
test("createSkillNoticeProvider never reminds for 'image-display' when disabled", () => {
assert.equal(createSkillNoticeProvider({ state: "disabled" }, "image-display")(), undefined);
});