@elizaos/plugin-agent-skills 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +372 -0
- package/dist/index.js +3698 -0
- package/package.json +83 -0
- package/src/__tests__/clawhub.test.ts +722 -0
- package/src/__tests__/integration.test.ts +465 -0
- package/src/__tests__/parser.test.ts +304 -0
- package/src/__tests__/skill-eligibility.test.ts +575 -0
- package/src/__tests__/skill-precedence.test.ts +592 -0
- package/src/__tests__/storage.test.ts +549 -0
- package/src/actions/get-skill-details.ts +127 -0
- package/src/actions/get-skill-guidance.ts +388 -0
- package/src/actions/run-skill-script.ts +200 -0
- package/src/actions/search-skills.ts +106 -0
- package/src/actions/sync-catalog.ts +88 -0
- package/src/index.ts +124 -0
- package/src/parser.ts +478 -0
- package/src/plugin.ts +118 -0
- package/src/providers/skills.ts +443 -0
- package/src/services/install.ts +628 -0
- package/src/services/skills.ts +2363 -0
- package/src/storage.ts +544 -0
- package/src/types.ts +582 -0
- package/tsconfig.json +17 -0
- package/tsup.config.ts +18 -0
|
@@ -0,0 +1,592 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill Precedence Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests for skill loading precedence: workspace > managed > bundled.
|
|
5
|
+
* Tests for skill override detection.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
|
9
|
+
import * as fs from "node:fs";
|
|
10
|
+
import * as path from "node:path";
|
|
11
|
+
import * as os from "node:os";
|
|
12
|
+
|
|
13
|
+
// ============================================================================
|
|
14
|
+
// Types
|
|
15
|
+
// ============================================================================
|
|
16
|
+
|
|
17
|
+
type SkillSource = "workspace" | "managed" | "bundled";
|
|
18
|
+
|
|
19
|
+
interface LoadedSkill {
|
|
20
|
+
slug: string;
|
|
21
|
+
name: string;
|
|
22
|
+
description: string;
|
|
23
|
+
version: string;
|
|
24
|
+
content: string;
|
|
25
|
+
path: string;
|
|
26
|
+
source: SkillSource;
|
|
27
|
+
bundledDir?: string;
|
|
28
|
+
loadedAt: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface SkillOverrideInfo {
|
|
32
|
+
slug: string;
|
|
33
|
+
activeSource: SkillSource;
|
|
34
|
+
overriddenSources: SkillSource[];
|
|
35
|
+
paths: Record<SkillSource, string | undefined>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ============================================================================
|
|
39
|
+
// Precedence Logic (for testing)
|
|
40
|
+
// ============================================================================
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Skill source precedence order (higher index = higher priority)
|
|
44
|
+
*/
|
|
45
|
+
const SOURCE_PRECEDENCE: SkillSource[] = ["bundled", "managed", "workspace"];
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Get the precedence level for a source (higher = more priority)
|
|
49
|
+
*/
|
|
50
|
+
function getSourcePrecedence(source: SkillSource): number {
|
|
51
|
+
return SOURCE_PRECEDENCE.indexOf(source);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Compare two skill sources, returns true if sourceA has higher precedence
|
|
56
|
+
*/
|
|
57
|
+
function hasHigherPrecedence(sourceA: SkillSource, sourceB: SkillSource): boolean {
|
|
58
|
+
return getSourcePrecedence(sourceA) > getSourcePrecedence(sourceB);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Skill loader that respects precedence
|
|
63
|
+
*/
|
|
64
|
+
class PrecedenceSkillLoader {
|
|
65
|
+
private skills: Map<string, LoadedSkill> = new Map();
|
|
66
|
+
private overrides: Map<string, SkillOverrideInfo> = new Map();
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Load a skill with precedence checking
|
|
70
|
+
*/
|
|
71
|
+
loadSkill(skill: LoadedSkill): boolean {
|
|
72
|
+
const existing = this.skills.get(skill.slug);
|
|
73
|
+
|
|
74
|
+
// Track override info
|
|
75
|
+
let overrideInfo = this.overrides.get(skill.slug);
|
|
76
|
+
if (!overrideInfo) {
|
|
77
|
+
overrideInfo = {
|
|
78
|
+
slug: skill.slug,
|
|
79
|
+
activeSource: skill.source,
|
|
80
|
+
overriddenSources: [],
|
|
81
|
+
paths: { workspace: undefined, managed: undefined, bundled: undefined },
|
|
82
|
+
};
|
|
83
|
+
this.overrides.set(skill.slug, overrideInfo);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
overrideInfo.paths[skill.source] = skill.path;
|
|
87
|
+
|
|
88
|
+
if (existing) {
|
|
89
|
+
if (hasHigherPrecedence(skill.source, existing.source)) {
|
|
90
|
+
// New skill has higher precedence, override
|
|
91
|
+
overrideInfo.overriddenSources.push(existing.source);
|
|
92
|
+
overrideInfo.activeSource = skill.source;
|
|
93
|
+
this.skills.set(skill.slug, skill);
|
|
94
|
+
return true;
|
|
95
|
+
} else {
|
|
96
|
+
// Existing skill has higher or equal precedence, skip
|
|
97
|
+
overrideInfo.overriddenSources.push(skill.source);
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// No existing skill, just add
|
|
103
|
+
this.skills.set(skill.slug, skill);
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Get a loaded skill by slug
|
|
109
|
+
*/
|
|
110
|
+
getSkill(slug: string): LoadedSkill | undefined {
|
|
111
|
+
return this.skills.get(slug);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Get all loaded skills
|
|
116
|
+
*/
|
|
117
|
+
getAllSkills(): LoadedSkill[] {
|
|
118
|
+
return Array.from(this.skills.values());
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Get override info for a skill
|
|
123
|
+
*/
|
|
124
|
+
getOverrideInfo(slug: string): SkillOverrideInfo | undefined {
|
|
125
|
+
return this.overrides.get(slug);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Get all skills that are overriding others
|
|
130
|
+
*/
|
|
131
|
+
getOverridingSkills(): SkillOverrideInfo[] {
|
|
132
|
+
return Array.from(this.overrides.values()).filter(
|
|
133
|
+
(info) => info.overriddenSources.length > 0
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Check if a skill is from a specific source
|
|
139
|
+
*/
|
|
140
|
+
isFromSource(slug: string, source: SkillSource): boolean {
|
|
141
|
+
const skill = this.skills.get(slug);
|
|
142
|
+
return skill?.source === source;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Get skills by source
|
|
147
|
+
*/
|
|
148
|
+
getSkillsBySource(source: SkillSource): LoadedSkill[] {
|
|
149
|
+
return Array.from(this.skills.values()).filter((s) => s.source === source);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Clear all loaded skills
|
|
154
|
+
*/
|
|
155
|
+
clear(): void {
|
|
156
|
+
this.skills.clear();
|
|
157
|
+
this.overrides.clear();
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Simulate skill discovery and loading with precedence
|
|
163
|
+
*/
|
|
164
|
+
function loadSkillsWithPrecedence(
|
|
165
|
+
skillsBySource: {
|
|
166
|
+
workspace?: LoadedSkill[];
|
|
167
|
+
managed?: LoadedSkill[];
|
|
168
|
+
bundled?: LoadedSkill[];
|
|
169
|
+
},
|
|
170
|
+
loadOrder: SkillSource[] = ["bundled", "managed", "workspace"]
|
|
171
|
+
): PrecedenceSkillLoader {
|
|
172
|
+
const loader = new PrecedenceSkillLoader();
|
|
173
|
+
|
|
174
|
+
// Load skills in specified order
|
|
175
|
+
for (const source of loadOrder) {
|
|
176
|
+
const skills = skillsBySource[source] || [];
|
|
177
|
+
for (const skill of skills) {
|
|
178
|
+
loader.loadSkill({ ...skill, source });
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return loader;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ============================================================================
|
|
186
|
+
// Test Utilities
|
|
187
|
+
// ============================================================================
|
|
188
|
+
|
|
189
|
+
function createTestSkill(
|
|
190
|
+
slug: string,
|
|
191
|
+
source: SkillSource,
|
|
192
|
+
version: string = "1.0.0"
|
|
193
|
+
): LoadedSkill {
|
|
194
|
+
return {
|
|
195
|
+
slug,
|
|
196
|
+
name: `${slug} Skill`,
|
|
197
|
+
description: `A ${source} skill called ${slug}`,
|
|
198
|
+
version,
|
|
199
|
+
content: `# ${slug}\nContent from ${source}`,
|
|
200
|
+
path: `/test/${source}/${slug}`,
|
|
201
|
+
source,
|
|
202
|
+
bundledDir: source === "bundled" ? "/test/bundled" : undefined,
|
|
203
|
+
loadedAt: Date.now(),
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// ============================================================================
|
|
208
|
+
// Tests
|
|
209
|
+
// ============================================================================
|
|
210
|
+
|
|
211
|
+
describe("Skill Precedence", () => {
|
|
212
|
+
describe("Source Precedence Order", () => {
|
|
213
|
+
it("should have correct precedence order", () => {
|
|
214
|
+
expect(getSourcePrecedence("bundled")).toBe(0);
|
|
215
|
+
expect(getSourcePrecedence("managed")).toBe(1);
|
|
216
|
+
expect(getSourcePrecedence("workspace")).toBe(2);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it("should identify workspace as highest precedence", () => {
|
|
220
|
+
expect(hasHigherPrecedence("workspace", "managed")).toBe(true);
|
|
221
|
+
expect(hasHigherPrecedence("workspace", "bundled")).toBe(true);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it("should identify managed as higher than bundled", () => {
|
|
225
|
+
expect(hasHigherPrecedence("managed", "bundled")).toBe(true);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it("should identify bundled as lowest precedence", () => {
|
|
229
|
+
expect(hasHigherPrecedence("bundled", "managed")).toBe(false);
|
|
230
|
+
expect(hasHigherPrecedence("bundled", "workspace")).toBe(false);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it("should return false for equal precedence", () => {
|
|
234
|
+
expect(hasHigherPrecedence("workspace", "workspace")).toBe(false);
|
|
235
|
+
expect(hasHigherPrecedence("managed", "managed")).toBe(false);
|
|
236
|
+
expect(hasHigherPrecedence("bundled", "bundled")).toBe(false);
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
describe("PrecedenceSkillLoader", () => {
|
|
241
|
+
let loader: PrecedenceSkillLoader;
|
|
242
|
+
|
|
243
|
+
beforeEach(() => {
|
|
244
|
+
loader = new PrecedenceSkillLoader();
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it("should load first skill of a slug", () => {
|
|
248
|
+
const skill = createTestSkill("my-skill", "bundled");
|
|
249
|
+
const loaded = loader.loadSkill(skill);
|
|
250
|
+
|
|
251
|
+
expect(loaded).toBe(true);
|
|
252
|
+
expect(loader.getSkill("my-skill")).toBeDefined();
|
|
253
|
+
expect(loader.getSkill("my-skill")?.source).toBe("bundled");
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it("should override bundled with managed", () => {
|
|
257
|
+
const bundledSkill = createTestSkill("my-skill", "bundled");
|
|
258
|
+
const managedSkill = createTestSkill("my-skill", "managed");
|
|
259
|
+
|
|
260
|
+
loader.loadSkill(bundledSkill);
|
|
261
|
+
const overridden = loader.loadSkill(managedSkill);
|
|
262
|
+
|
|
263
|
+
expect(overridden).toBe(true);
|
|
264
|
+
expect(loader.getSkill("my-skill")?.source).toBe("managed");
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it("should override managed with workspace", () => {
|
|
268
|
+
const managedSkill = createTestSkill("my-skill", "managed");
|
|
269
|
+
const workspaceSkill = createTestSkill("my-skill", "workspace");
|
|
270
|
+
|
|
271
|
+
loader.loadSkill(managedSkill);
|
|
272
|
+
const overridden = loader.loadSkill(workspaceSkill);
|
|
273
|
+
|
|
274
|
+
expect(overridden).toBe(true);
|
|
275
|
+
expect(loader.getSkill("my-skill")?.source).toBe("workspace");
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it("should override bundled with workspace", () => {
|
|
279
|
+
const bundledSkill = createTestSkill("my-skill", "bundled");
|
|
280
|
+
const workspaceSkill = createTestSkill("my-skill", "workspace");
|
|
281
|
+
|
|
282
|
+
loader.loadSkill(bundledSkill);
|
|
283
|
+
const overridden = loader.loadSkill(workspaceSkill);
|
|
284
|
+
|
|
285
|
+
expect(overridden).toBe(true);
|
|
286
|
+
expect(loader.getSkill("my-skill")?.source).toBe("workspace");
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it("should not override workspace with managed", () => {
|
|
290
|
+
const workspaceSkill = createTestSkill("my-skill", "workspace");
|
|
291
|
+
const managedSkill = createTestSkill("my-skill", "managed");
|
|
292
|
+
|
|
293
|
+
loader.loadSkill(workspaceSkill);
|
|
294
|
+
const overridden = loader.loadSkill(managedSkill);
|
|
295
|
+
|
|
296
|
+
expect(overridden).toBe(false);
|
|
297
|
+
expect(loader.getSkill("my-skill")?.source).toBe("workspace");
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it("should not override managed with bundled", () => {
|
|
301
|
+
const managedSkill = createTestSkill("my-skill", "managed");
|
|
302
|
+
const bundledSkill = createTestSkill("my-skill", "bundled");
|
|
303
|
+
|
|
304
|
+
loader.loadSkill(managedSkill);
|
|
305
|
+
const overridden = loader.loadSkill(bundledSkill);
|
|
306
|
+
|
|
307
|
+
expect(overridden).toBe(false);
|
|
308
|
+
expect(loader.getSkill("my-skill")?.source).toBe("managed");
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it("should not override workspace with bundled", () => {
|
|
312
|
+
const workspaceSkill = createTestSkill("my-skill", "workspace");
|
|
313
|
+
const bundledSkill = createTestSkill("my-skill", "bundled");
|
|
314
|
+
|
|
315
|
+
loader.loadSkill(workspaceSkill);
|
|
316
|
+
const overridden = loader.loadSkill(bundledSkill);
|
|
317
|
+
|
|
318
|
+
expect(overridden).toBe(false);
|
|
319
|
+
expect(loader.getSkill("my-skill")?.source).toBe("workspace");
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it("should track override info", () => {
|
|
323
|
+
const bundledSkill = createTestSkill("my-skill", "bundled");
|
|
324
|
+
const managedSkill = createTestSkill("my-skill", "managed");
|
|
325
|
+
const workspaceSkill = createTestSkill("my-skill", "workspace");
|
|
326
|
+
|
|
327
|
+
loader.loadSkill(bundledSkill);
|
|
328
|
+
loader.loadSkill(managedSkill);
|
|
329
|
+
loader.loadSkill(workspaceSkill);
|
|
330
|
+
|
|
331
|
+
const info = loader.getOverrideInfo("my-skill");
|
|
332
|
+
|
|
333
|
+
expect(info).toBeDefined();
|
|
334
|
+
expect(info?.activeSource).toBe("workspace");
|
|
335
|
+
expect(info?.overriddenSources).toContain("bundled");
|
|
336
|
+
expect(info?.overriddenSources).toContain("managed");
|
|
337
|
+
expect(info?.paths.workspace).toBeDefined();
|
|
338
|
+
expect(info?.paths.managed).toBeDefined();
|
|
339
|
+
expect(info?.paths.bundled).toBeDefined();
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it("should get all overriding skills", () => {
|
|
343
|
+
loader.loadSkill(createTestSkill("skill-a", "bundled"));
|
|
344
|
+
loader.loadSkill(createTestSkill("skill-a", "workspace"));
|
|
345
|
+
loader.loadSkill(createTestSkill("skill-b", "managed"));
|
|
346
|
+
|
|
347
|
+
const overriding = loader.getOverridingSkills();
|
|
348
|
+
|
|
349
|
+
expect(overriding.length).toBe(1);
|
|
350
|
+
expect(overriding[0].slug).toBe("skill-a");
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it("should load multiple different skills", () => {
|
|
354
|
+
loader.loadSkill(createTestSkill("skill-a", "bundled"));
|
|
355
|
+
loader.loadSkill(createTestSkill("skill-b", "managed"));
|
|
356
|
+
loader.loadSkill(createTestSkill("skill-c", "workspace"));
|
|
357
|
+
|
|
358
|
+
expect(loader.getAllSkills()).toHaveLength(3);
|
|
359
|
+
expect(loader.getSkill("skill-a")).toBeDefined();
|
|
360
|
+
expect(loader.getSkill("skill-b")).toBeDefined();
|
|
361
|
+
expect(loader.getSkill("skill-c")).toBeDefined();
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
it("should check source correctly", () => {
|
|
365
|
+
loader.loadSkill(createTestSkill("skill-a", "bundled"));
|
|
366
|
+
loader.loadSkill(createTestSkill("skill-b", "managed"));
|
|
367
|
+
loader.loadSkill(createTestSkill("skill-c", "workspace"));
|
|
368
|
+
|
|
369
|
+
expect(loader.isFromSource("skill-a", "bundled")).toBe(true);
|
|
370
|
+
expect(loader.isFromSource("skill-a", "managed")).toBe(false);
|
|
371
|
+
expect(loader.isFromSource("skill-b", "managed")).toBe(true);
|
|
372
|
+
expect(loader.isFromSource("skill-c", "workspace")).toBe(true);
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
it("should get skills by source", () => {
|
|
376
|
+
loader.loadSkill(createTestSkill("skill-a", "bundled"));
|
|
377
|
+
loader.loadSkill(createTestSkill("skill-b", "bundled"));
|
|
378
|
+
loader.loadSkill(createTestSkill("skill-c", "managed"));
|
|
379
|
+
loader.loadSkill(createTestSkill("skill-d", "workspace"));
|
|
380
|
+
|
|
381
|
+
expect(loader.getSkillsBySource("bundled")).toHaveLength(2);
|
|
382
|
+
expect(loader.getSkillsBySource("managed")).toHaveLength(1);
|
|
383
|
+
expect(loader.getSkillsBySource("workspace")).toHaveLength(1);
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
it("should clear all skills", () => {
|
|
387
|
+
loader.loadSkill(createTestSkill("skill-a", "bundled"));
|
|
388
|
+
loader.loadSkill(createTestSkill("skill-b", "managed"));
|
|
389
|
+
|
|
390
|
+
loader.clear();
|
|
391
|
+
|
|
392
|
+
expect(loader.getAllSkills()).toHaveLength(0);
|
|
393
|
+
expect(loader.getOverrideInfo("skill-a")).toBeUndefined();
|
|
394
|
+
});
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
describe("loadSkillsWithPrecedence()", () => {
|
|
398
|
+
it("should load skills in correct order for workspace override", () => {
|
|
399
|
+
const bundledSkills = [createTestSkill("common", "bundled", "1.0.0")];
|
|
400
|
+
const workspaceSkills = [createTestSkill("common", "workspace", "2.0.0")];
|
|
401
|
+
|
|
402
|
+
const loader = loadSkillsWithPrecedence({
|
|
403
|
+
bundled: bundledSkills,
|
|
404
|
+
workspace: workspaceSkills,
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
const skill = loader.getSkill("common");
|
|
408
|
+
expect(skill?.source).toBe("workspace");
|
|
409
|
+
expect(skill?.version).toBe("2.0.0");
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
it("should respect custom load order", () => {
|
|
413
|
+
const bundledSkills = [createTestSkill("skill", "bundled")];
|
|
414
|
+
const managedSkills = [createTestSkill("skill", "managed")];
|
|
415
|
+
|
|
416
|
+
// Load in reverse order (workspace first, bundled last)
|
|
417
|
+
// Bundled should NOT override workspace
|
|
418
|
+
const loader = loadSkillsWithPrecedence(
|
|
419
|
+
{
|
|
420
|
+
bundled: bundledSkills,
|
|
421
|
+
managed: managedSkills,
|
|
422
|
+
},
|
|
423
|
+
["managed", "bundled"] // Load managed first, then try bundled
|
|
424
|
+
);
|
|
425
|
+
|
|
426
|
+
// Managed was loaded first, bundled can't override it
|
|
427
|
+
expect(loader.getSkill("skill")?.source).toBe("managed");
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
it("should load unique skills from all sources", () => {
|
|
431
|
+
const loader = loadSkillsWithPrecedence({
|
|
432
|
+
bundled: [createTestSkill("bundled-only", "bundled")],
|
|
433
|
+
managed: [createTestSkill("managed-only", "managed")],
|
|
434
|
+
workspace: [createTestSkill("workspace-only", "workspace")],
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
expect(loader.getAllSkills()).toHaveLength(3);
|
|
438
|
+
expect(loader.getSkillsBySource("bundled")).toHaveLength(1);
|
|
439
|
+
expect(loader.getSkillsBySource("managed")).toHaveLength(1);
|
|
440
|
+
expect(loader.getSkillsBySource("workspace")).toHaveLength(1);
|
|
441
|
+
});
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
describe("Skill Override Detection", () => {
|
|
445
|
+
it("should detect when workspace overrides bundled", () => {
|
|
446
|
+
const loader = new PrecedenceSkillLoader();
|
|
447
|
+
|
|
448
|
+
loader.loadSkill(createTestSkill("skill", "bundled"));
|
|
449
|
+
loader.loadSkill(createTestSkill("skill", "workspace"));
|
|
450
|
+
|
|
451
|
+
const info = loader.getOverrideInfo("skill");
|
|
452
|
+
|
|
453
|
+
expect(info?.activeSource).toBe("workspace");
|
|
454
|
+
expect(info?.overriddenSources).toContain("bundled");
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
it("should detect when managed overrides bundled", () => {
|
|
458
|
+
const loader = new PrecedenceSkillLoader();
|
|
459
|
+
|
|
460
|
+
loader.loadSkill(createTestSkill("skill", "bundled"));
|
|
461
|
+
loader.loadSkill(createTestSkill("skill", "managed"));
|
|
462
|
+
|
|
463
|
+
const info = loader.getOverrideInfo("skill");
|
|
464
|
+
|
|
465
|
+
expect(info?.activeSource).toBe("managed");
|
|
466
|
+
expect(info?.overriddenSources).toContain("bundled");
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
it("should track all paths even when overridden", () => {
|
|
470
|
+
const loader = new PrecedenceSkillLoader();
|
|
471
|
+
|
|
472
|
+
loader.loadSkill(createTestSkill("skill", "bundled"));
|
|
473
|
+
loader.loadSkill(createTestSkill("skill", "managed"));
|
|
474
|
+
loader.loadSkill(createTestSkill("skill", "workspace"));
|
|
475
|
+
|
|
476
|
+
const info = loader.getOverrideInfo("skill");
|
|
477
|
+
|
|
478
|
+
expect(info?.paths.bundled).toBe("/test/bundled/skill");
|
|
479
|
+
expect(info?.paths.managed).toBe("/test/managed/skill");
|
|
480
|
+
expect(info?.paths.workspace).toBe("/test/workspace/skill");
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
it("should not mark non-overriding skills as overriding", () => {
|
|
484
|
+
const loader = new PrecedenceSkillLoader();
|
|
485
|
+
|
|
486
|
+
loader.loadSkill(createTestSkill("unique-skill", "bundled"));
|
|
487
|
+
|
|
488
|
+
const info = loader.getOverrideInfo("unique-skill");
|
|
489
|
+
|
|
490
|
+
expect(info?.overriddenSources).toHaveLength(0);
|
|
491
|
+
});
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
describe("Edge Cases", () => {
|
|
495
|
+
it("should handle empty skill lists", () => {
|
|
496
|
+
const loader = loadSkillsWithPrecedence({});
|
|
497
|
+
|
|
498
|
+
expect(loader.getAllSkills()).toHaveLength(0);
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
it("should handle single source only", () => {
|
|
502
|
+
const loader = loadSkillsWithPrecedence({
|
|
503
|
+
managed: [
|
|
504
|
+
createTestSkill("skill-a", "managed"),
|
|
505
|
+
createTestSkill("skill-b", "managed"),
|
|
506
|
+
],
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
expect(loader.getAllSkills()).toHaveLength(2);
|
|
510
|
+
expect(loader.getSkillsBySource("managed")).toHaveLength(2);
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
it("should handle same skill loaded multiple times from same source", () => {
|
|
514
|
+
const loader = new PrecedenceSkillLoader();
|
|
515
|
+
|
|
516
|
+
const skill1 = createTestSkill("skill", "bundled");
|
|
517
|
+
const skill2 = createTestSkill("skill", "bundled");
|
|
518
|
+
skill2.version = "2.0.0";
|
|
519
|
+
|
|
520
|
+
loader.loadSkill(skill1);
|
|
521
|
+
loader.loadSkill(skill2); // Should not override (same precedence)
|
|
522
|
+
|
|
523
|
+
expect(loader.getSkill("skill")?.version).toBe("1.0.0");
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
it("should handle special characters in skill slugs", () => {
|
|
527
|
+
const loader = new PrecedenceSkillLoader();
|
|
528
|
+
|
|
529
|
+
loader.loadSkill(createTestSkill("my-skill-2", "bundled"));
|
|
530
|
+
loader.loadSkill(createTestSkill("my_skill_v2", "managed"));
|
|
531
|
+
|
|
532
|
+
expect(loader.getSkill("my-skill-2")).toBeDefined();
|
|
533
|
+
expect(loader.getSkill("my_skill_v2")).toBeDefined();
|
|
534
|
+
});
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
describe("Real-World Scenarios", () => {
|
|
538
|
+
it("should allow user to customize bundled skill", () => {
|
|
539
|
+
// Bundled skill provides default behavior
|
|
540
|
+
const bundledGitSkill = createTestSkill("git", "bundled");
|
|
541
|
+
bundledGitSkill.content = "Default git instructions";
|
|
542
|
+
|
|
543
|
+
// User creates workspace override with custom instructions
|
|
544
|
+
const workspaceGitSkill = createTestSkill("git", "workspace");
|
|
545
|
+
workspaceGitSkill.content = "Custom git instructions for this project";
|
|
546
|
+
|
|
547
|
+
const loader = loadSkillsWithPrecedence({
|
|
548
|
+
bundled: [bundledGitSkill],
|
|
549
|
+
workspace: [workspaceGitSkill],
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
const activeSkill = loader.getSkill("git");
|
|
553
|
+
expect(activeSkill?.content).toBe("Custom git instructions for this project");
|
|
554
|
+
expect(activeSkill?.source).toBe("workspace");
|
|
555
|
+
|
|
556
|
+
// Original is still trackable
|
|
557
|
+
const info = loader.getOverrideInfo("git");
|
|
558
|
+
expect(info?.paths.bundled).toBeDefined();
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
it("should allow installed skill to override bundled", () => {
|
|
562
|
+
const bundledDocker = createTestSkill("docker", "bundled");
|
|
563
|
+
bundledDocker.version = "1.0.0";
|
|
564
|
+
|
|
565
|
+
// User installs newer version from registry
|
|
566
|
+
const installedDocker = createTestSkill("docker", "managed");
|
|
567
|
+
installedDocker.version = "2.0.0";
|
|
568
|
+
|
|
569
|
+
const loader = loadSkillsWithPrecedence({
|
|
570
|
+
bundled: [bundledDocker],
|
|
571
|
+
managed: [installedDocker],
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
expect(loader.getSkill("docker")?.version).toBe("2.0.0");
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
it("should give user full control with workspace skills", () => {
|
|
578
|
+
const loader = loadSkillsWithPrecedence({
|
|
579
|
+
bundled: [createTestSkill("skill", "bundled")],
|
|
580
|
+
managed: [createTestSkill("skill", "managed")],
|
|
581
|
+
workspace: [createTestSkill("skill", "workspace")],
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
// Workspace always wins
|
|
585
|
+
expect(loader.getSkill("skill")?.source).toBe("workspace");
|
|
586
|
+
|
|
587
|
+
// Both bundled and managed were overridden
|
|
588
|
+
const info = loader.getOverrideInfo("skill");
|
|
589
|
+
expect(info?.overriddenSources).toHaveLength(2);
|
|
590
|
+
});
|
|
591
|
+
});
|
|
592
|
+
});
|