
























@@ -74,8 +74,252 @@ describe("buildWorkspaceSkillStatus", () => {
7474expect(check).toEqual({ path: "channels.discord.token", satisfied: true });
7575expect(check && "value" in check).toBe(false);
7676});
77+78+it("reports prompt and command visibility separately from eligibility", () => {
79+const entry: SkillEntry = {
80+skill: createFixtureSkill({
81+name: "background-only",
82+description: "test",
83+filePath: "/tmp/background-only/SKILL.md",
84+baseDir: "/tmp/background-only",
85+source: "test",
86+}),
87+frontmatter: {},
88+invocation: {
89+userInvocable: false,
90+disableModelInvocation: true,
91+},
92+};
93+94+const report = buildWorkspaceSkillStatus("/tmp/ws", { entries: [entry] });
95+const skill = report.skills[0];
96+expect(skill?.eligible).toBe(true);
97+expect(skill?.modelVisible).toBe(false);
98+expect(skill?.userInvocable).toBe(false);
99+expect(skill?.commandVisible).toBe(false);
100+});
101+102+it("uses default-visible exposure semantics when older entries omit exposure fields", () => {
103+const entry: SkillEntry = {
104+skill: createFixtureSkill({
105+name: "legacy-exposure",
106+description: "test",
107+filePath: "/tmp/legacy-exposure/SKILL.md",
108+baseDir: "/tmp/legacy-exposure",
109+source: "test",
110+}),
111+frontmatter: {},
112+exposure: {
113+includeInRuntimeRegistry: true,
114+} as SkillEntry["exposure"],
115+};
116+117+const report = buildWorkspaceSkillStatus("/tmp/ws", { entries: [entry] });
118+const skill = report.skills[0];
119+expect(skill?.eligible).toBe(true);
120+expect(skill?.modelVisible).toBe(true);
121+expect(skill?.userInvocable).toBe(true);
122+expect(skill?.commandVisible).toBe(true);
123+});
124+125+it("reports skills blocked by an agent skill filter", () => {
126+const alpha: SkillEntry = {
127+skill: createFixtureSkill({
128+name: "alpha",
129+description: "test",
130+filePath: "/tmp/alpha/SKILL.md",
131+baseDir: "/tmp/alpha",
132+source: "test",
133+}),
134+frontmatter: {},
135+};
136+const beta: SkillEntry = {
137+skill: createFixtureSkill({
138+name: "beta",
139+description: "test",
140+filePath: "/tmp/beta/SKILL.md",
141+baseDir: "/tmp/beta",
142+source: "test",
143+}),
144+frontmatter: {},
145+};
146+147+const report = buildWorkspaceSkillStatus("/tmp/ws", {
148+entries: [alpha, beta],
149+agentId: "specialist",
150+config: {
151+agents: {
152+list: [{ id: "specialist", skills: ["alpha"] }],
153+},
154+},
155+});
156+157+expect(report.agentId).toBe("specialist");
158+expect(report.agentSkillFilter).toEqual(["alpha"]);
159+expect(report.skills.find((skill) => skill.name === "alpha")?.blockedByAgentFilter).toBe(false);
160+expect(report.skills.find((skill) => skill.name === "alpha")?.modelVisible).toBe(true);
161+expect(report.skills.find((skill) => skill.name === "beta")?.blockedByAgentFilter).toBe(true);
162+expect(report.skills.find((skill) => skill.name === "beta")?.modelVisible).toBe(false);
163+});
164+165+it("classifies a mixed broken skill pack without flattening visibility reasons", () => {
166+const missingBin = "openclaw-test-definitely-missing-skill-bin";
167+const report = buildWorkspaceSkillStatus("/tmp/ws", {
168+agentId: "specialist",
169+config: {
170+agents: {
171+list: [
172+{
173+id: "specialist",
174+skills: [
175+"ready",
176+"needs-bin",
177+"needs-env",
178+"prompt-hidden",
179+"slash-hidden",
180+"disabled",
181+"bundled-blocked",
182+],
183+},
184+],
185+},
186+skills: {
187+allowBundled: ["some-other-bundled-skill"],
188+entries: {
189+disabled: { enabled: false },
190+},
191+install: {
192+nodeManager: "pnpm",
193+},
194+},
195+},
196+entries: [
197+createEntry("ready"),
198+createEntry("needs-bin", {
199+metadata: {
200+requires: { bins: [missingBin] },
201+install: [
202+{
203+kind: "node",
204+package: "@openclaw/missing-skill-bin",
205+bins: [missingBin],
206+},
207+],
208+},
209+}),
210+createEntry("needs-env", {
211+metadata: {
212+primaryEnv: "OPENCLAW_TEST_MISSING_SKILL_KEY",
213+requires: { env: ["OPENCLAW_TEST_MISSING_SKILL_KEY"] },
214+},
215+}),
216+createEntry("prompt-hidden", {
217+invocation: {
218+userInvocable: true,
219+disableModelInvocation: true,
220+},
221+}),
222+createEntry("slash-hidden", {
223+invocation: {
224+userInvocable: false,
225+disableModelInvocation: false,
226+},
227+}),
228+createEntry("agent-filtered"),
229+createEntry("disabled"),
230+createEntry("bundled-blocked", { source: "openclaw-bundled" }),
231+],
232+});
233+234+const byName = new Map(report.skills.map((skill) => [skill.name, skill]));
235+expect(report.agentSkillFilter).toEqual([
236+"ready",
237+"needs-bin",
238+"needs-env",
239+"prompt-hidden",
240+"slash-hidden",
241+"disabled",
242+"bundled-blocked",
243+]);
244+expect(byName.get("ready")).toMatchObject({
245+eligible: true,
246+modelVisible: true,
247+commandVisible: true,
248+});
249+expect(byName.get("needs-bin")).toMatchObject({
250+eligible: false,
251+modelVisible: false,
252+commandVisible: false,
253+missing: { bins: [missingBin] },
254+install: [
255+{
256+kind: "node",
257+label: "Install @openclaw/missing-skill-bin (pnpm)",
258+bins: [missingBin],
259+},
260+],
261+});
262+expect(byName.get("needs-env")).toMatchObject({
263+eligible: false,
264+primaryEnv: "OPENCLAW_TEST_MISSING_SKILL_KEY",
265+missing: { env: ["OPENCLAW_TEST_MISSING_SKILL_KEY"] },
266+});
267+expect(byName.get("prompt-hidden")).toMatchObject({
268+eligible: true,
269+modelVisible: false,
270+commandVisible: true,
271+});
272+expect(byName.get("slash-hidden")).toMatchObject({
273+eligible: true,
274+modelVisible: true,
275+userInvocable: false,
276+commandVisible: false,
277+});
278+expect(byName.get("agent-filtered")).toMatchObject({
279+eligible: true,
280+blockedByAgentFilter: true,
281+modelVisible: false,
282+commandVisible: false,
283+});
284+expect(byName.get("disabled")).toMatchObject({
285+eligible: false,
286+disabled: true,
287+modelVisible: false,
288+commandVisible: false,
289+});
290+expect(byName.get("bundled-blocked")).toMatchObject({
291+eligible: false,
292+blockedByAllowlist: true,
293+modelVisible: false,
294+commandVisible: false,
295+});
296+});
77297});
78298299+function createEntry(
300+name: string,
301+params: {
302+description?: string;
303+source?: string;
304+metadata?: SkillEntry["metadata"];
305+invocation?: SkillEntry["invocation"];
306+} = {},
307+): SkillEntry {
308+const baseDir = `/tmp/${name}`;
309+return {
310+skill: createFixtureSkill({
311+ name,
312+description: params.description ?? `${name} skill`,
313+filePath: `${baseDir}/SKILL.md`,
314+ baseDir,
315+source: params.source ?? "test",
316+}),
317+frontmatter: {},
318+metadata: params.metadata,
319+invocation: params.invocation,
320+};
321+}
322+79323function createFixtureSkill(params: {
80324name: string;
81325description: string;
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。