4fa4f13558
Co-authored-by: iamlukethedev <iamlukethedev@users.noreply.github.com>
275 lines
8.2 KiB
TypeScript
275 lines
8.2 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
|
|
import type { SkillStatusEntry } from "@/lib/skills/types";
|
|
import {
|
|
deriveAgentSkillDisplayState,
|
|
buildAgentSkillsAllowlistSet,
|
|
buildSkillMissingDetails,
|
|
buildSkillReasons,
|
|
canRemoveSkill,
|
|
deriveAgentSkillsAccessMode,
|
|
deriveSkillReadinessState,
|
|
filterOsCompatibleSkills,
|
|
groupSkillsBySource,
|
|
hasInstallableMissingBinary,
|
|
isBundledBlockedSkill,
|
|
isSkillOsIncompatible,
|
|
normalizeAgentSkillsAllowlist,
|
|
resolvePreferredInstallOption,
|
|
} from "@/lib/skills/presentation";
|
|
|
|
const createSkill = (overrides: Partial<SkillStatusEntry>): SkillStatusEntry => ({
|
|
name: "skill",
|
|
description: "",
|
|
source: "openclaw-workspace",
|
|
bundled: false,
|
|
filePath: "/tmp/workspace/skill/SKILL.md",
|
|
baseDir: "/tmp/workspace/skill",
|
|
skillKey: "skill",
|
|
always: false,
|
|
disabled: false,
|
|
blockedByAllowlist: false,
|
|
eligible: true,
|
|
requirements: { bins: [], anyBins: [], env: [], config: [], os: [] },
|
|
missing: { bins: [], anyBins: [], env: [], config: [], os: [] },
|
|
configChecks: [],
|
|
install: [],
|
|
...overrides,
|
|
});
|
|
|
|
describe("skills presentation helpers", () => {
|
|
it("groups skills by source with stable ordering", () => {
|
|
const groups = groupSkillsBySource([
|
|
createSkill({ name: "other", source: "custom-source" }),
|
|
createSkill({ name: "installed", source: "openclaw-managed" }),
|
|
createSkill({ name: "workspace", source: "openclaw-workspace" }),
|
|
createSkill({ name: "bundled", source: "openclaw-bundled", bundled: true }),
|
|
createSkill({ name: "extra", source: "openclaw-extra" }),
|
|
]);
|
|
|
|
expect(groups.map((group) => group.id)).toEqual([
|
|
"workspace",
|
|
"built-in",
|
|
"installed",
|
|
"extra",
|
|
"other",
|
|
]);
|
|
expect(groups[0]?.skills.map((skill) => skill.name)).toEqual(["workspace"]);
|
|
expect(groups[1]?.skills.map((skill) => skill.name)).toEqual(["bundled"]);
|
|
});
|
|
|
|
it("builds explicit missing detail lines", () => {
|
|
const details = buildSkillMissingDetails(
|
|
createSkill({
|
|
eligible: false,
|
|
missing: {
|
|
bins: ["playwright"],
|
|
anyBins: ["chromium", "chrome"],
|
|
env: ["GITHUB_TOKEN"],
|
|
config: ["browser.enabled"],
|
|
os: ["linux"],
|
|
},
|
|
})
|
|
);
|
|
|
|
expect(details).toEqual([
|
|
"Missing tools: playwright",
|
|
"Missing one-of tools (install any): chromium | chrome",
|
|
"Missing env vars (set in gateway env): GITHUB_TOKEN",
|
|
"Missing config values (set in openclaw.json): browser.enabled",
|
|
"Requires OS: Linux",
|
|
]);
|
|
});
|
|
|
|
it("builds reasons from policy and missing requirements", () => {
|
|
const reasons = buildSkillReasons(
|
|
createSkill({
|
|
eligible: false,
|
|
disabled: true,
|
|
blockedByAllowlist: true,
|
|
missing: {
|
|
bins: ["playwright"],
|
|
anyBins: [],
|
|
env: [],
|
|
config: [],
|
|
os: [],
|
|
},
|
|
})
|
|
);
|
|
|
|
expect(reasons).toEqual(["disabled", "blocked by allowlist", "missing tools"]);
|
|
});
|
|
|
|
it("detects_os_incompatibility_from_missing_os_requirements", () => {
|
|
expect(
|
|
isSkillOsIncompatible(
|
|
createSkill({
|
|
missing: {
|
|
bins: [],
|
|
anyBins: [],
|
|
env: [],
|
|
config: [],
|
|
os: ["darwin"],
|
|
},
|
|
})
|
|
)
|
|
).toBe(true);
|
|
expect(
|
|
isSkillOsIncompatible(
|
|
createSkill({
|
|
missing: {
|
|
bins: [],
|
|
anyBins: [],
|
|
env: [],
|
|
config: [],
|
|
os: [" "],
|
|
},
|
|
})
|
|
)
|
|
).toBe(false);
|
|
});
|
|
|
|
it("filters_out_os_incompatible_skills_while_preserving_order", () => {
|
|
const filtered = filterOsCompatibleSkills([
|
|
createSkill({ name: "github", missing: { bins: [], anyBins: [], env: [], config: [], os: [] } }),
|
|
createSkill({
|
|
name: "apple-notes",
|
|
missing: { bins: [], anyBins: [], env: [], config: [], os: ["darwin"] },
|
|
}),
|
|
createSkill({ name: "slack", missing: { bins: [], anyBins: [], env: [], config: [], os: [] } }),
|
|
]);
|
|
|
|
expect(filtered.map((skill) => skill.name)).toEqual(["github", "slack"]);
|
|
});
|
|
|
|
it("detects bundled blocked skills", () => {
|
|
expect(
|
|
isBundledBlockedSkill(
|
|
createSkill({
|
|
source: "openclaw-bundled",
|
|
bundled: true,
|
|
eligible: false,
|
|
})
|
|
)
|
|
).toBe(true);
|
|
expect(isBundledBlockedSkill(createSkill({ bundled: true, eligible: true }))).toBe(false);
|
|
});
|
|
|
|
it("detects installable missing binaries including anyBins overlap", () => {
|
|
const skill = createSkill({
|
|
eligible: false,
|
|
missing: {
|
|
bins: [],
|
|
anyBins: ["chromium", "chrome"],
|
|
env: [],
|
|
config: [],
|
|
os: [],
|
|
},
|
|
install: [
|
|
{
|
|
id: "install-chromium",
|
|
kind: "download",
|
|
label: "Install chromium",
|
|
bins: ["chromium"],
|
|
},
|
|
],
|
|
});
|
|
|
|
expect(hasInstallableMissingBinary(skill)).toBe(true);
|
|
expect(resolvePreferredInstallOption(skill)?.id).toBe("install-chromium");
|
|
});
|
|
|
|
it("selects_install_option_that_matches_missing_bins", () => {
|
|
const skill = createSkill({
|
|
eligible: false,
|
|
missing: {
|
|
bins: ["gh"],
|
|
anyBins: [],
|
|
env: [],
|
|
config: [],
|
|
os: [],
|
|
},
|
|
install: [
|
|
{
|
|
id: "install-other",
|
|
kind: "download",
|
|
label: "Install other tool",
|
|
bins: ["other"],
|
|
},
|
|
{
|
|
id: "install-gh",
|
|
kind: "brew",
|
|
label: "Install gh",
|
|
bins: ["gh"],
|
|
},
|
|
],
|
|
});
|
|
|
|
expect(resolvePreferredInstallOption(skill)?.id).toBe("install-gh");
|
|
});
|
|
|
|
it("marks only gateway-managed and workspace skill sources as removable", () => {
|
|
expect(canRemoveSkill(createSkill({ source: "openclaw-managed" }))).toBe(true);
|
|
expect(canRemoveSkill(createSkill({ source: "openclaw-workspace" }))).toBe(true);
|
|
expect(canRemoveSkill(createSkill({ source: "agents-skills-personal" }))).toBe(false);
|
|
expect(canRemoveSkill(createSkill({ source: "agents-skills-project" }))).toBe(false);
|
|
expect(canRemoveSkill(createSkill({ source: "openclaw-bundled", bundled: true }))).toBe(
|
|
false
|
|
);
|
|
expect(canRemoveSkill(createSkill({ source: "openclaw-extra" }))).toBe(false);
|
|
});
|
|
|
|
it("derives agent access mode from allowlist shape", () => {
|
|
expect(deriveAgentSkillsAccessMode(undefined)).toBe("all");
|
|
expect(deriveAgentSkillsAccessMode([])).toBe("none");
|
|
expect(deriveAgentSkillsAccessMode([" ", "github"])).toBe("selected");
|
|
});
|
|
|
|
it("normalizes allowlist values and creates a lookup set", () => {
|
|
expect(normalizeAgentSkillsAllowlist([" github ", "github", "slack", " "])).toEqual([
|
|
"github",
|
|
"slack",
|
|
]);
|
|
expect(buildAgentSkillsAllowlistSet([" github ", "slack"]).has("github")).toBe(true);
|
|
expect(buildAgentSkillsAllowlistSet([" github ", "slack"]).has("browser")).toBe(false);
|
|
});
|
|
|
|
it("classifies readiness with disabled and unavailable precedence", () => {
|
|
expect(deriveSkillReadinessState(createSkill({ disabled: true, eligible: false }))).toBe(
|
|
"disabled-globally"
|
|
);
|
|
expect(
|
|
deriveSkillReadinessState(
|
|
createSkill({
|
|
eligible: false,
|
|
missing: { bins: [], anyBins: [], env: [], config: [], os: ["darwin"] },
|
|
})
|
|
)
|
|
).toBe("unavailable");
|
|
expect(
|
|
deriveSkillReadinessState(
|
|
createSkill({
|
|
eligible: false,
|
|
blockedByAllowlist: true,
|
|
})
|
|
)
|
|
).toBe("unavailable");
|
|
expect(
|
|
deriveSkillReadinessState(
|
|
createSkill({
|
|
eligible: false,
|
|
missing: { bins: ["gh"], anyBins: [], env: [], config: [], os: [] },
|
|
})
|
|
)
|
|
).toBe("needs-setup");
|
|
expect(deriveSkillReadinessState(createSkill({ eligible: true }))).toBe("ready");
|
|
});
|
|
|
|
it("maps readiness into agent display states", () => {
|
|
expect(deriveAgentSkillDisplayState("ready")).toBe("ready");
|
|
expect(deriveAgentSkillDisplayState("needs-setup")).toBe("setup-required");
|
|
expect(deriveAgentSkillDisplayState("disabled-globally")).toBe("setup-required");
|
|
expect(deriveAgentSkillDisplayState("unavailable")).toBe("not-supported");
|
|
});
|
|
});
|