

























@@ -0,0 +1,155 @@
1+import { describe, expect, it } from "vitest";
2+import { createCanonicalFixtureSkill } from "../test-support/test-helpers.js";
3+import type { SkillEntry } from "../types.js";
4+import {
5+buildSkillIndex,
6+isSkillPromptVisible,
7+isSkillRuntimeVisible,
8+isSkillUserInvocable,
9+normalizeSkillIndexName,
10+} from "./skill-index.js";
11+12+describe("skill index", () => {
13+it("normalizes skill names for case-insensitive separator-tolerant lookup", () => {
14+expect(normalizeSkillIndexName(" Excel_XLSX/demo ")).toBe("excel-xlsx-demo");
15+expect(normalizeSkillIndexName("Excel XLSX")).toBe("excel-xlsx");
16+expect(normalizeSkillIndexName("@@")).toBe("");
17+});
18+19+it("indexes entries by exact and normalized name without changing input order", () => {
20+const entries = [
21+createEntry("Excel XLSX", { skillKey: "excel_xlsx" }),
22+createEntry("GitHub Review"),
23+];
24+25+const index = buildSkillIndex(entries);
26+27+expect(index.entries.map((entry) => entry.name)).toEqual(["Excel XLSX", "GitHub Review"]);
28+expect(index.byName.get("Excel XLSX")?.entry).toBe(entries[0]);
29+expect(index.byNormalizedName.get("excel-xlsx")?.map((entry) => entry.name)).toEqual([
30+"Excel XLSX",
31+]);
32+expect(index.byNormalizedName.get("github-review")?.map((entry) => entry.name)).toEqual([
33+"GitHub Review",
34+]);
35+});
36+37+it("keeps ambiguous normalized names as multiple index entries", () => {
38+const entries = [
39+createEntry("Excel/XLSX", { skillKey: "excel-slash" }),
40+createEntry("Excel_XLSX", { skillKey: "excel-underscore" }),
41+];
42+43+const index = buildSkillIndex(entries);
44+45+expect(index.byNormalizedName.get("excel-xlsx")?.map((entry) => entry.name)).toEqual([
46+"Excel/XLSX",
47+"Excel_XLSX",
48+]);
49+});
50+51+it("centralizes runtime, prompt, and command exposure policy", () => {
52+const runtimeHidden = createEntry("runtime-hidden", {
53+exposure: {
54+includeInRuntimeRegistry: false,
55+includeInAvailableSkillsPrompt: true,
56+userInvocable: true,
57+},
58+});
59+const promptHidden = createEntry("prompt-hidden", {
60+exposure: {
61+includeInRuntimeRegistry: true,
62+includeInAvailableSkillsPrompt: false,
63+userInvocable: true,
64+},
65+});
66+const commandHidden = createEntry("command-hidden", {
67+exposure: {
68+includeInRuntimeRegistry: true,
69+includeInAvailableSkillsPrompt: true,
70+userInvocable: false,
71+},
72+});
73+const legacyPromptHidden = createEntry("legacy-prompt-hidden", {
74+invocation: { disableModelInvocation: true, userInvocable: true },
75+});
76+77+const index = buildSkillIndex([runtimeHidden, promptHidden, commandHidden, legacyPromptHidden]);
78+79+expect(index.runtimeEntries.map((entry) => entry.skill.name)).toEqual([
80+"prompt-hidden",
81+"command-hidden",
82+"legacy-prompt-hidden",
83+]);
84+expect(index.promptVisibleEntries.map((entry) => entry.skill.name)).toEqual([
85+"runtime-hidden",
86+"command-hidden",
87+]);
88+expect(index.userInvocableEntries.map((entry) => entry.skill.name)).toEqual([
89+"runtime-hidden",
90+"prompt-hidden",
91+"legacy-prompt-hidden",
92+]);
93+expect(isSkillRuntimeVisible(runtimeHidden)).toBe(false);
94+expect(isSkillPromptVisible(legacyPromptHidden)).toBe(false);
95+expect(isSkillUserInvocable(commandHidden)).toBe(false);
96+});
97+98+it("records source, bundled state, skill key, and agent filter state", () => {
99+const bundled = createEntry("bundle", { source: "openclaw-bundled" });
100+const unknownBundled = createEntry("unknown-bundle", { source: "unknown" });
101+const workspace = createEntry("workspace", {
102+source: "openclaw-workspace",
103+skillKey: "workspace-key",
104+});
105+106+const index = buildSkillIndex([bundled, unknownBundled, workspace], {
107+bundledNames: new Set(["unknown-bundle"]),
108+agentSkillFilter: ["workspace"],
109+});
110+111+expect(index.byName.get("bundle")).toMatchObject({
112+source: "openclaw-bundled",
113+bundled: true,
114+agentAllowed: false,
115+});
116+expect(index.byName.get("unknown-bundle")).toMatchObject({
117+source: "unknown",
118+bundled: true,
119+agentAllowed: false,
120+});
121+expect(index.byName.get("workspace")).toMatchObject({
122+source: "openclaw-workspace",
123+bundled: false,
124+skillKey: "workspace-key",
125+agentAllowed: true,
126+});
127+});
128+});
129+130+function createEntry(
131+name: string,
132+opts?: {
133+source?: string;
134+skillKey?: string;
135+exposure?: SkillEntry["exposure"];
136+invocation?: SkillEntry["invocation"];
137+},
138+): SkillEntry {
139+return {
140+skill: createCanonicalFixtureSkill({
141+ name,
142+description: `${name} description`,
143+filePath: `/skills/${name}/SKILL.md`,
144+baseDir: `/skills/${name}`,
145+source: opts?.source ?? "openclaw-workspace",
146+}),
147+frontmatter: {},
148+metadata: opts?.skillKey ? { skillKey: opts.skillKey } : undefined,
149+invocation: opts?.invocation ?? {
150+userInvocable: true,
151+disableModelInvocation: false,
152+},
153+exposure: opts?.exposure,
154+};
155+}
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。