@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.
@@ -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
+ });