vskill 0.5.2 → 0.5.4

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.
Files changed (205) hide show
  1. package/dist/agents/agents-registry.test.d.ts +1 -0
  2. package/dist/agents/agents-registry.test.js +248 -0
  3. package/dist/agents/agents-registry.test.js.map +1 -0
  4. package/dist/api/client.test.d.ts +1 -0
  5. package/dist/api/client.test.js +428 -0
  6. package/dist/api/client.test.js.map +1 -0
  7. package/dist/audit/audit-integration.test.d.ts +1 -0
  8. package/dist/audit/audit-integration.test.js +92 -0
  9. package/dist/audit/audit-integration.test.js.map +1 -0
  10. package/dist/audit/audit-llm.test.d.ts +1 -0
  11. package/dist/audit/audit-llm.test.js +110 -0
  12. package/dist/audit/audit-llm.test.js.map +1 -0
  13. package/dist/audit/audit-patterns.test.d.ts +1 -0
  14. package/dist/audit/audit-patterns.test.js +91 -0
  15. package/dist/audit/audit-patterns.test.js.map +1 -0
  16. package/dist/audit/audit-scanner.test.d.ts +1 -0
  17. package/dist/audit/audit-scanner.test.js +112 -0
  18. package/dist/audit/audit-scanner.test.js.map +1 -0
  19. package/dist/audit/audit-types.test.d.ts +1 -0
  20. package/dist/audit/audit-types.test.js +140 -0
  21. package/dist/audit/audit-types.test.js.map +1 -0
  22. package/dist/audit/config.test.d.ts +1 -0
  23. package/dist/audit/config.test.js +44 -0
  24. package/dist/audit/config.test.js.map +1 -0
  25. package/dist/audit/file-discovery.test.d.ts +1 -0
  26. package/dist/audit/file-discovery.test.js +120 -0
  27. package/dist/audit/file-discovery.test.js.map +1 -0
  28. package/dist/audit/fix-suggestions.test.d.ts +1 -0
  29. package/dist/audit/fix-suggestions.test.js +35 -0
  30. package/dist/audit/fix-suggestions.test.js.map +1 -0
  31. package/dist/audit/formatters/json-formatter.test.d.ts +1 -0
  32. package/dist/audit/formatters/json-formatter.test.js +49 -0
  33. package/dist/audit/formatters/json-formatter.test.js.map +1 -0
  34. package/dist/audit/formatters/report-formatter.test.d.ts +1 -0
  35. package/dist/audit/formatters/report-formatter.test.js +51 -0
  36. package/dist/audit/formatters/report-formatter.test.js.map +1 -0
  37. package/dist/audit/formatters/sarif-formatter.test.d.ts +1 -0
  38. package/dist/audit/formatters/sarif-formatter.test.js +71 -0
  39. package/dist/audit/formatters/sarif-formatter.test.js.map +1 -0
  40. package/dist/audit/formatters/terminal-formatter.test.d.ts +1 -0
  41. package/dist/audit/formatters/terminal-formatter.test.js +51 -0
  42. package/dist/audit/formatters/terminal-formatter.test.js.map +1 -0
  43. package/dist/blocklist/blocklist-e2e.test.d.ts +1 -0
  44. package/dist/blocklist/blocklist-e2e.test.js +346 -0
  45. package/dist/blocklist/blocklist-e2e.test.js.map +1 -0
  46. package/dist/blocklist/blocklist.test.d.ts +1 -0
  47. package/dist/blocklist/blocklist.test.js +259 -0
  48. package/dist/blocklist/blocklist.test.js.map +1 -0
  49. package/dist/commands/__tests__/eval-router.test.d.ts +1 -0
  50. package/dist/commands/__tests__/eval-router.test.js +60 -0
  51. package/dist/commands/__tests__/eval-router.test.js.map +1 -0
  52. package/dist/commands/__tests__/eval-serve.test.d.ts +1 -0
  53. package/dist/commands/__tests__/eval-serve.test.js +23 -0
  54. package/dist/commands/__tests__/eval-serve.test.js.map +1 -0
  55. package/dist/commands/add-blocklist-e2e.test.d.ts +1 -0
  56. package/dist/commands/add-blocklist-e2e.test.js +397 -0
  57. package/dist/commands/add-blocklist-e2e.test.js.map +1 -0
  58. package/dist/commands/add-wizard.test.d.ts +1 -0
  59. package/dist/commands/add-wizard.test.js +392 -0
  60. package/dist/commands/add-wizard.test.js.map +1 -0
  61. package/dist/commands/add.test.d.ts +1 -0
  62. package/dist/commands/add.test.js +2365 -0
  63. package/dist/commands/add.test.js.map +1 -0
  64. package/dist/commands/audit.test.d.ts +1 -0
  65. package/dist/commands/audit.test.js +79 -0
  66. package/dist/commands/audit.test.js.map +1 -0
  67. package/dist/commands/blocklist.test.d.ts +1 -0
  68. package/dist/commands/blocklist.test.js +158 -0
  69. package/dist/commands/blocklist.test.js.map +1 -0
  70. package/dist/commands/eval/__tests__/coverage.test.d.ts +1 -0
  71. package/dist/commands/eval/__tests__/coverage.test.js +122 -0
  72. package/dist/commands/eval/__tests__/coverage.test.js.map +1 -0
  73. package/dist/commands/eval/__tests__/generate-all.test.d.ts +1 -0
  74. package/dist/commands/eval/__tests__/generate-all.test.js +133 -0
  75. package/dist/commands/eval/__tests__/generate-all.test.js.map +1 -0
  76. package/dist/commands/eval/__tests__/init.test.d.ts +1 -0
  77. package/dist/commands/eval/__tests__/init.test.js +116 -0
  78. package/dist/commands/eval/__tests__/init.test.js.map +1 -0
  79. package/dist/commands/eval/__tests__/run.test.d.ts +1 -0
  80. package/dist/commands/eval/__tests__/run.test.js +186 -0
  81. package/dist/commands/eval/__tests__/run.test.js.map +1 -0
  82. package/dist/commands/find.test.d.ts +1 -0
  83. package/dist/commands/find.test.js +481 -0
  84. package/dist/commands/find.test.js.map +1 -0
  85. package/dist/commands/marketplace.test.d.ts +1 -0
  86. package/dist/commands/marketplace.test.js +129 -0
  87. package/dist/commands/marketplace.test.js.map +1 -0
  88. package/dist/commands/remove.test.d.ts +1 -0
  89. package/dist/commands/remove.test.js +164 -0
  90. package/dist/commands/remove.test.js.map +1 -0
  91. package/dist/commands/should-skip.test.d.ts +1 -0
  92. package/dist/commands/should-skip.test.js +56 -0
  93. package/dist/commands/should-skip.test.js.map +1 -0
  94. package/dist/commands/submit.test.d.ts +1 -0
  95. package/dist/commands/submit.test.js +83 -0
  96. package/dist/commands/submit.test.js.map +1 -0
  97. package/dist/commands/update.test.d.ts +1 -0
  98. package/dist/commands/update.test.js +250 -0
  99. package/dist/commands/update.test.js.map +1 -0
  100. package/dist/discovery/github-tree.test.d.ts +1 -0
  101. package/dist/discovery/github-tree.test.js +372 -0
  102. package/dist/discovery/github-tree.test.js.map +1 -0
  103. package/dist/eval/__tests__/activation-tester.test.d.ts +1 -0
  104. package/dist/eval/__tests__/activation-tester.test.js +203 -0
  105. package/dist/eval/__tests__/activation-tester.test.js.map +1 -0
  106. package/dist/eval/__tests__/benchmark-history.test.d.ts +1 -0
  107. package/dist/eval/__tests__/benchmark-history.test.js +422 -0
  108. package/dist/eval/__tests__/benchmark-history.test.js.map +1 -0
  109. package/dist/eval/__tests__/benchmark.test.d.ts +1 -0
  110. package/dist/eval/__tests__/benchmark.test.js +94 -0
  111. package/dist/eval/__tests__/benchmark.test.js.map +1 -0
  112. package/dist/eval/__tests__/comparator.test.d.ts +1 -0
  113. package/dist/eval/__tests__/comparator.test.js +282 -0
  114. package/dist/eval/__tests__/comparator.test.js.map +1 -0
  115. package/dist/eval/__tests__/judge.test.d.ts +1 -0
  116. package/dist/eval/__tests__/judge.test.js +122 -0
  117. package/dist/eval/__tests__/judge.test.js.map +1 -0
  118. package/dist/eval/__tests__/llm.test.d.ts +1 -0
  119. package/dist/eval/__tests__/llm.test.js +543 -0
  120. package/dist/eval/__tests__/llm.test.js.map +1 -0
  121. package/dist/eval/__tests__/mcp-detector.test.d.ts +1 -0
  122. package/dist/eval/__tests__/mcp-detector.test.js +180 -0
  123. package/dist/eval/__tests__/mcp-detector.test.js.map +1 -0
  124. package/dist/eval/__tests__/prompt-builder.test.d.ts +1 -0
  125. package/dist/eval/__tests__/prompt-builder.test.js +142 -0
  126. package/dist/eval/__tests__/prompt-builder.test.js.map +1 -0
  127. package/dist/eval/__tests__/schema.test.d.ts +1 -0
  128. package/dist/eval/__tests__/schema.test.js +247 -0
  129. package/dist/eval/__tests__/schema.test.js.map +1 -0
  130. package/dist/eval/__tests__/skill-scanner.test.d.ts +1 -0
  131. package/dist/eval/__tests__/skill-scanner.test.js +228 -0
  132. package/dist/eval/__tests__/skill-scanner.test.js.map +1 -0
  133. package/dist/eval/__tests__/verdict.test.d.ts +1 -0
  134. package/dist/eval/__tests__/verdict.test.js +47 -0
  135. package/dist/eval/__tests__/verdict.test.js.map +1 -0
  136. package/dist/eval-server/__tests__/benchmark-runner.test.d.ts +1 -0
  137. package/dist/eval-server/__tests__/benchmark-runner.test.js +301 -0
  138. package/dist/eval-server/__tests__/benchmark-runner.test.js.map +1 -0
  139. package/dist/eval-server/__tests__/comparison-sse-events.test.d.ts +1 -0
  140. package/dist/eval-server/__tests__/comparison-sse-events.test.js +278 -0
  141. package/dist/eval-server/__tests__/comparison-sse-events.test.js.map +1 -0
  142. package/dist/eval-server/__tests__/sse-helpers.test.d.ts +1 -0
  143. package/dist/eval-server/__tests__/sse-helpers.test.js +128 -0
  144. package/dist/eval-server/__tests__/sse-helpers.test.js.map +1 -0
  145. package/dist/installer/canonical.test.d.ts +1 -0
  146. package/dist/installer/canonical.test.js +264 -0
  147. package/dist/installer/canonical.test.js.map +1 -0
  148. package/dist/lockfile/lockfile.test.d.ts +1 -0
  149. package/dist/lockfile/lockfile.test.js +204 -0
  150. package/dist/lockfile/lockfile.test.js.map +1 -0
  151. package/dist/lockfile/project-root.test.d.ts +1 -0
  152. package/dist/lockfile/project-root.test.js +49 -0
  153. package/dist/lockfile/project-root.test.js.map +1 -0
  154. package/dist/marketplace/marketplace.test.d.ts +1 -0
  155. package/dist/marketplace/marketplace.test.js +312 -0
  156. package/dist/marketplace/marketplace.test.js.map +1 -0
  157. package/dist/resolvers/source-resolver.test.d.ts +1 -0
  158. package/dist/resolvers/source-resolver.test.js +104 -0
  159. package/dist/resolvers/source-resolver.test.js.map +1 -0
  160. package/dist/resolvers/url-resolver.test.d.ts +1 -0
  161. package/dist/resolvers/url-resolver.test.js +49 -0
  162. package/dist/resolvers/url-resolver.test.js.map +1 -0
  163. package/dist/scanner/dci-integration.test.d.ts +1 -0
  164. package/dist/scanner/dci-integration.test.js +83 -0
  165. package/dist/scanner/dci-integration.test.js.map +1 -0
  166. package/dist/scanner/patterns.test.d.ts +1 -0
  167. package/dist/scanner/patterns.test.js +832 -0
  168. package/dist/scanner/patterns.test.js.map +1 -0
  169. package/dist/scanner/tier1.test.d.ts +1 -0
  170. package/dist/scanner/tier1.test.js +305 -0
  171. package/dist/scanner/tier1.test.js.map +1 -0
  172. package/dist/security/platform-security.test.d.ts +1 -0
  173. package/dist/security/platform-security.test.js +92 -0
  174. package/dist/security/platform-security.test.js.map +1 -0
  175. package/dist/settings/settings.test.d.ts +1 -0
  176. package/dist/settings/settings.test.js +103 -0
  177. package/dist/settings/settings.test.js.map +1 -0
  178. package/dist/updater/source-fetcher.test.d.ts +1 -0
  179. package/dist/updater/source-fetcher.test.js +192 -0
  180. package/dist/updater/source-fetcher.test.js.map +1 -0
  181. package/dist/utils/__tests__/paths.test.d.ts +1 -0
  182. package/dist/utils/__tests__/paths.test.js +22 -0
  183. package/dist/utils/__tests__/paths.test.js.map +1 -0
  184. package/dist/utils/__tests__/resolve-binary.integration.test.d.ts +1 -0
  185. package/dist/utils/__tests__/resolve-binary.integration.test.js +138 -0
  186. package/dist/utils/__tests__/resolve-binary.integration.test.js.map +1 -0
  187. package/dist/utils/__tests__/resolve-binary.test.d.ts +1 -0
  188. package/dist/utils/__tests__/resolve-binary.test.js +175 -0
  189. package/dist/utils/__tests__/resolve-binary.test.js.map +1 -0
  190. package/dist/utils/__tests__/validation.test.d.ts +1 -0
  191. package/dist/utils/__tests__/validation.test.js +107 -0
  192. package/dist/utils/__tests__/validation.test.js.map +1 -0
  193. package/dist/utils/agent-filter.test.d.ts +1 -0
  194. package/dist/utils/agent-filter.test.js +75 -0
  195. package/dist/utils/agent-filter.test.js.map +1 -0
  196. package/dist/utils/output.test.d.ts +1 -0
  197. package/dist/utils/output.test.js +28 -0
  198. package/dist/utils/output.test.js.map +1 -0
  199. package/dist/utils/project-root.test.d.ts +1 -0
  200. package/dist/utils/project-root.test.js +74 -0
  201. package/dist/utils/project-root.test.js.map +1 -0
  202. package/dist/utils/prompts.test.d.ts +1 -0
  203. package/dist/utils/prompts.test.js +285 -0
  204. package/dist/utils/prompts.test.js.map +1 -0
  205. package/package.json +1 -1
@@ -0,0 +1,250 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ // ---------------------------------------------------------------------------
3
+ // Mock node:fs
4
+ // ---------------------------------------------------------------------------
5
+ const mockMkdirSync = vi.hoisted(() => vi.fn());
6
+ const mockWriteFileSync = vi.hoisted(() => vi.fn());
7
+ vi.mock("node:fs", () => ({
8
+ mkdirSync: (...args) => mockMkdirSync(...args),
9
+ writeFileSync: (...args) => mockWriteFileSync(...args),
10
+ }));
11
+ // ---------------------------------------------------------------------------
12
+ // Mock lockfile
13
+ // ---------------------------------------------------------------------------
14
+ const mockReadLockfile = vi.hoisted(() => vi.fn());
15
+ const mockWriteLockfile = vi.hoisted(() => vi.fn());
16
+ vi.mock("../lockfile/index.js", () => ({
17
+ readLockfile: (...args) => mockReadLockfile(...args),
18
+ writeLockfile: (...args) => mockWriteLockfile(...args),
19
+ }));
20
+ // ---------------------------------------------------------------------------
21
+ // Mock API client (registry fallback)
22
+ // ---------------------------------------------------------------------------
23
+ const mockGetSkill = vi.hoisted(() => vi.fn());
24
+ vi.mock("../api/client.js", () => ({
25
+ getSkill: (...args) => mockGetSkill(...args),
26
+ }));
27
+ // ---------------------------------------------------------------------------
28
+ // Mock source-aware fetcher
29
+ // ---------------------------------------------------------------------------
30
+ const mockFetchFromSource = vi.hoisted(() => vi.fn());
31
+ vi.mock("../updater/source-fetcher.js", () => ({
32
+ fetchFromSource: (...args) => mockFetchFromSource(...args),
33
+ }));
34
+ // ---------------------------------------------------------------------------
35
+ // Mock agents registry
36
+ // ---------------------------------------------------------------------------
37
+ const mockDetectInstalledAgents = vi.hoisted(() => vi.fn());
38
+ vi.mock("../agents/agents-registry.js", () => ({
39
+ detectInstalledAgents: (...args) => mockDetectInstalledAgents(...args),
40
+ }));
41
+ // ---------------------------------------------------------------------------
42
+ // Mock scanner
43
+ // ---------------------------------------------------------------------------
44
+ const mockRunTier1Scan = vi.hoisted(() => vi.fn().mockReturnValue({ verdict: "PASS", score: 100, findings: [] }));
45
+ vi.mock("../scanner/index.js", () => ({
46
+ runTier1Scan: (...args) => mockRunTier1Scan(...args),
47
+ }));
48
+ // ---------------------------------------------------------------------------
49
+ // Mock output (suppress console noise)
50
+ // ---------------------------------------------------------------------------
51
+ vi.mock("../utils/output.js", () => ({
52
+ bold: (s) => s,
53
+ green: (s) => s,
54
+ red: (s) => s,
55
+ yellow: (s) => s,
56
+ dim: (s) => s,
57
+ cyan: (s) => s,
58
+ spinner: () => ({ stop: vi.fn() }),
59
+ }));
60
+ const MOCK_AGENTS = [
61
+ {
62
+ id: "claude-code",
63
+ displayName: "Claude Code",
64
+ localSkillsDir: ".claude/skills",
65
+ globalSkillsDir: "~/.claude/skills",
66
+ isUniversal: false,
67
+ detectInstalled: "which claude",
68
+ parentCompany: "Anthropic",
69
+ featureSupport: { slashCommands: true, hooks: true, mcp: true, customSystemPrompt: true },
70
+ },
71
+ {
72
+ id: "cursor",
73
+ displayName: "Cursor",
74
+ localSkillsDir: ".cursor/skills",
75
+ globalSkillsDir: "~/.cursor/skills",
76
+ isUniversal: true,
77
+ detectInstalled: "which cursor",
78
+ parentCompany: "Anysphere",
79
+ featureSupport: { slashCommands: true, hooks: false, mcp: true, customSystemPrompt: true },
80
+ },
81
+ ];
82
+ const UPDATED_CONTENT = "# Updated Skill";
83
+ const UPDATED_FETCH_RESULT = {
84
+ content: UPDATED_CONTENT,
85
+ version: "2.0.0",
86
+ sha: "new-sha-12345",
87
+ tier: "VERIFIED",
88
+ };
89
+ describe("updateCommand", () => {
90
+ beforeEach(() => {
91
+ vi.clearAllMocks();
92
+ mockDetectInstalledAgents.mockResolvedValue(MOCK_AGENTS);
93
+ mockReadLockfile.mockReturnValue({
94
+ version: 1,
95
+ agents: ["claude-code", "cursor"],
96
+ skills: {
97
+ frontend: {
98
+ version: "1.0.0",
99
+ sha: "aaa111bbb222",
100
+ tier: "VERIFIED",
101
+ installedAt: "2026-01-01T00:00:00.000Z",
102
+ source: "github:test/repo",
103
+ },
104
+ },
105
+ createdAt: "2026-01-01T00:00:00.000Z",
106
+ updatedAt: "2026-01-01T00:00:00.000Z",
107
+ });
108
+ // Source-aware fetcher returns updated result by default
109
+ mockFetchFromSource.mockResolvedValue(UPDATED_FETCH_RESULT);
110
+ // Registry fallback (used when fetchFromSource returns null)
111
+ mockGetSkill.mockResolvedValue({
112
+ content: UPDATED_CONTENT,
113
+ version: "2.0.0",
114
+ sha: "new-sha-12345",
115
+ tier: "VERIFIED",
116
+ });
117
+ });
118
+ it("updates only the filtered agent when --agent is provided", async () => {
119
+ const { updateCommand } = await import("./update.js");
120
+ await updateCommand("frontend", { all: false, agent: "claude-code" });
121
+ // Should only write to claude-code's skill dir, NOT cursor's
122
+ const mkdirCalls = mockMkdirSync.mock.calls.map((c) => c[0]);
123
+ const claudeCalls = mkdirCalls.filter((p) => p.includes(".claude"));
124
+ const cursorCalls = mkdirCalls.filter((p) => p.includes(".cursor"));
125
+ expect(claudeCalls.length).toBeGreaterThan(0);
126
+ expect(cursorCalls.length).toBe(0);
127
+ });
128
+ it("updates all detected agents when --agent is NOT provided", async () => {
129
+ const { updateCommand } = await import("./update.js");
130
+ await updateCommand("frontend", { all: false });
131
+ // Should write to BOTH agent dirs
132
+ const mkdirCalls = mockMkdirSync.mock.calls.map((c) => c[0]);
133
+ const claudeCalls = mkdirCalls.filter((p) => p.includes(".claude"));
134
+ const cursorCalls = mkdirCalls.filter((p) => p.includes(".cursor"));
135
+ expect(claudeCalls.length).toBeGreaterThan(0);
136
+ expect(cursorCalls.length).toBeGreaterThan(0);
137
+ });
138
+ it("exits with error when --agent specifies unknown ID", async () => {
139
+ const mockExit = vi.spyOn(process, "exit").mockImplementation(() => {
140
+ throw new Error("process.exit");
141
+ });
142
+ const { updateCommand } = await import("./update.js");
143
+ await expect(updateCommand("frontend", { all: false, agent: "nonexistent" })).rejects.toThrow("process.exit");
144
+ expect(mockExit).toHaveBeenCalledWith(1);
145
+ mockExit.mockRestore();
146
+ });
147
+ // ---------------------------------------------------------------------------
148
+ // Source-aware routing tests
149
+ // ---------------------------------------------------------------------------
150
+ it("uses fetchFromSource instead of getSkill for non-registry sources", async () => {
151
+ const { updateCommand } = await import("./update.js");
152
+ await updateCommand("frontend", { all: false });
153
+ expect(mockFetchFromSource).toHaveBeenCalled();
154
+ // getSkill should NOT be called because fetchFromSource returned a result
155
+ expect(mockGetSkill).not.toHaveBeenCalled();
156
+ });
157
+ it("falls back to getSkill when fetchFromSource returns null for unknown source", async () => {
158
+ mockReadLockfile.mockReturnValue({
159
+ version: 1,
160
+ agents: ["claude-code"],
161
+ skills: {
162
+ frontend: {
163
+ version: "1.0.0",
164
+ sha: "aaa111bbb222",
165
+ tier: "VERIFIED",
166
+ installedAt: "2026-01-01T00:00:00.000Z",
167
+ source: "", // empty → unknown type → fallback to registry
168
+ },
169
+ },
170
+ createdAt: "2026-01-01T00:00:00.000Z",
171
+ updatedAt: "2026-01-01T00:00:00.000Z",
172
+ });
173
+ mockFetchFromSource.mockResolvedValue(null);
174
+ const { updateCommand } = await import("./update.js");
175
+ await updateCommand("frontend", { all: false });
176
+ expect(mockGetSkill).toHaveBeenCalledWith("frontend");
177
+ expect(mockWriteFileSync).toHaveBeenCalled();
178
+ });
179
+ it("skips skill and does not call getSkill for local source when fetchFromSource returns null", async () => {
180
+ mockReadLockfile.mockReturnValue({
181
+ version: 1,
182
+ agents: ["claude-code"],
183
+ skills: {
184
+ sw: {
185
+ version: "1.0.0",
186
+ sha: "aaa111bbb222",
187
+ tier: "COMMUNITY",
188
+ installedAt: "2026-01-01T00:00:00.000Z",
189
+ source: "local:specweave",
190
+ },
191
+ },
192
+ createdAt: "2026-01-01T00:00:00.000Z",
193
+ updatedAt: "2026-01-01T00:00:00.000Z",
194
+ });
195
+ mockFetchFromSource.mockResolvedValue(null);
196
+ const { updateCommand } = await import("./update.js");
197
+ await updateCommand("sw", { all: false });
198
+ // local sources: no registry fallback, no file writes
199
+ expect(mockGetSkill).not.toHaveBeenCalled();
200
+ expect(mockWriteFileSync).not.toHaveBeenCalled();
201
+ });
202
+ it("skips skill when SHA is unchanged", async () => {
203
+ // fetchFromSource returns same SHA as lockfile
204
+ mockFetchFromSource.mockResolvedValue({
205
+ content: "# Same content",
206
+ version: "1.0.0",
207
+ sha: "aaa111bbb222", // same as lockfile entry
208
+ tier: "VERIFIED",
209
+ });
210
+ const { updateCommand } = await import("./update.js");
211
+ await updateCommand("frontend", { all: false });
212
+ expect(mockWriteFileSync).not.toHaveBeenCalled();
213
+ expect(mockWriteLockfile).toHaveBeenCalledTimes(1); // still writes lockfile
214
+ });
215
+ it("runs tier1 scan on fetched content and skips on FAIL verdict", async () => {
216
+ mockRunTier1Scan.mockReturnValue({ verdict: "FAIL", score: 0, findings: ["malicious"] });
217
+ const { updateCommand } = await import("./update.js");
218
+ await updateCommand("frontend", { all: false });
219
+ expect(mockRunTier1Scan).toHaveBeenCalledWith(UPDATED_CONTENT);
220
+ expect(mockWriteFileSync).not.toHaveBeenCalled();
221
+ });
222
+ it("preserves source field in lockfile after update", async () => {
223
+ const { updateCommand } = await import("./update.js");
224
+ await updateCommand("frontend", { all: false });
225
+ const writtenLock = mockWriteLockfile.mock.calls[0][0];
226
+ expect(writtenLock.skills["frontend"].source).toBe("github:test/repo");
227
+ });
228
+ it("writes lockfile exactly once after the loop", async () => {
229
+ mockReadLockfile.mockReturnValue({
230
+ version: 1,
231
+ agents: ["claude-code"],
232
+ skills: {
233
+ frontend: {
234
+ version: "1.0.0", sha: "aaa111bbb222", tier: "VERIFIED",
235
+ installedAt: "2026-01-01T00:00:00.000Z", source: "github:test/repo",
236
+ },
237
+ backend: {
238
+ version: "1.0.0", sha: "bbb222ccc333", tier: "VERIFIED",
239
+ installedAt: "2026-01-01T00:00:00.000Z", source: "registry:backend",
240
+ },
241
+ },
242
+ createdAt: "2026-01-01T00:00:00.000Z",
243
+ updatedAt: "2026-01-01T00:00:00.000Z",
244
+ });
245
+ const { updateCommand } = await import("./update.js");
246
+ await updateCommand(undefined, { all: true });
247
+ expect(mockWriteLockfile).toHaveBeenCalledTimes(1);
248
+ });
249
+ });
250
+ //# sourceMappingURL=update.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"update.test.js","sourceRoot":"","sources":["../../src/commands/update.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,372 @@
1
+ import { describe, it, expect, vi, afterEach } from "vitest";
2
+ // ---------------------------------------------------------------------------
3
+ // Import module under test
4
+ // ---------------------------------------------------------------------------
5
+ import { discoverSkills, extractDescription, getDefaultBranch, _resetBranchCache, warnRateLimitOnce, _resetRateLimitWarned, } from "./github-tree.js";
6
+ // ---------------------------------------------------------------------------
7
+ // Helpers
8
+ // ---------------------------------------------------------------------------
9
+ function makeTreeResponse(paths) {
10
+ return {
11
+ tree: paths.map((path) => ({
12
+ path,
13
+ mode: "100644",
14
+ type: "blob",
15
+ sha: "abc123",
16
+ size: 100,
17
+ })),
18
+ };
19
+ }
20
+ function makeBranchResponse(branch = "main") {
21
+ return { ok: true, json: async () => ({ default_branch: branch }) };
22
+ }
23
+ // ---------------------------------------------------------------------------
24
+ // Tests
25
+ // ---------------------------------------------------------------------------
26
+ describe("discoverSkills", () => {
27
+ const originalFetch = globalThis.fetch;
28
+ afterEach(() => {
29
+ globalThis.fetch = originalFetch;
30
+ _resetBranchCache();
31
+ });
32
+ // TC-001: Discovers root SKILL.md and skills/*/SKILL.md
33
+ it("discovers root SKILL.md and skills/*/SKILL.md from tree response", async () => {
34
+ globalThis.fetch = vi.fn().mockResolvedValue({
35
+ ok: true,
36
+ json: async () => makeTreeResponse([
37
+ "README.md",
38
+ "SKILL.md",
39
+ "skills/foo/SKILL.md",
40
+ "skills/bar/SKILL.md",
41
+ "src/index.ts",
42
+ ]),
43
+ });
44
+ const result = await discoverSkills("owner", "repo");
45
+ expect(result).toHaveLength(3);
46
+ expect(result).toEqual(expect.arrayContaining([
47
+ {
48
+ name: "repo",
49
+ path: "SKILL.md",
50
+ rawUrl: "https://raw.githubusercontent.com/owner/repo/main/SKILL.md",
51
+ },
52
+ {
53
+ name: "foo",
54
+ path: "skills/foo/SKILL.md",
55
+ rawUrl: "https://raw.githubusercontent.com/owner/repo/main/skills/foo/SKILL.md",
56
+ },
57
+ {
58
+ name: "bar",
59
+ path: "skills/bar/SKILL.md",
60
+ rawUrl: "https://raw.githubusercontent.com/owner/repo/main/skills/bar/SKILL.md",
61
+ },
62
+ ]));
63
+ });
64
+ // TC-002: Returns only root SKILL.md when no skills/ directory
65
+ it("returns only root SKILL.md when no skills/ directory exists", async () => {
66
+ globalThis.fetch = vi.fn().mockResolvedValue({
67
+ ok: true,
68
+ json: async () => makeTreeResponse(["SKILL.md", "README.md", "package.json"]),
69
+ });
70
+ const result = await discoverSkills("owner", "repo");
71
+ expect(result).toHaveLength(1);
72
+ expect(result[0]).toEqual({
73
+ name: "repo",
74
+ path: "SKILL.md",
75
+ rawUrl: "https://raw.githubusercontent.com/owner/repo/main/SKILL.md",
76
+ });
77
+ });
78
+ // TC-003: Returns empty array when no SKILL.md files found
79
+ it("returns empty array when no SKILL.md files found", async () => {
80
+ globalThis.fetch = vi.fn().mockResolvedValue({
81
+ ok: true,
82
+ json: async () => makeTreeResponse(["README.md", "package.json", "src/index.ts"]),
83
+ });
84
+ const result = await discoverSkills("owner", "repo");
85
+ expect(result).toEqual([]);
86
+ });
87
+ // TC-004: Returns empty array on API error (404, rate-limited)
88
+ it("returns empty array on 404", async () => {
89
+ globalThis.fetch = vi.fn().mockResolvedValue({
90
+ ok: false,
91
+ status: 404,
92
+ });
93
+ const result = await discoverSkills("owner", "repo");
94
+ expect(result).toEqual([]);
95
+ });
96
+ it("returns empty array on 403 (rate limited)", async () => {
97
+ globalThis.fetch = vi.fn().mockResolvedValue({
98
+ ok: false,
99
+ status: 403,
100
+ });
101
+ const result = await discoverSkills("owner", "repo");
102
+ expect(result).toEqual([]);
103
+ });
104
+ it("returns empty array on network error", async () => {
105
+ globalThis.fetch = vi.fn().mockRejectedValue(new Error("Network error"));
106
+ const result = await discoverSkills("owner", "repo");
107
+ expect(result).toEqual([]);
108
+ });
109
+ // TC-005: Ignores SKILL.md in nested non-skill directories
110
+ it("ignores SKILL.md in nested non-skill directories", async () => {
111
+ globalThis.fetch = vi.fn().mockResolvedValue({
112
+ ok: true,
113
+ json: async () => makeTreeResponse([
114
+ "SKILL.md",
115
+ "docs/SKILL.md",
116
+ "examples/SKILL.md",
117
+ "node_modules/SKILL.md",
118
+ "skills/foo/SKILL.md",
119
+ "skills/bar/nested/SKILL.md",
120
+ ]),
121
+ });
122
+ const result = await discoverSkills("owner", "repo");
123
+ expect(result).toHaveLength(2);
124
+ const names = result.map((s) => s.name);
125
+ expect(names).toContain("repo");
126
+ expect(names).toContain("foo");
127
+ // Should NOT contain docs, examples, node_modules, or deeply nested skills
128
+ expect(names).not.toContain("docs");
129
+ expect(names).not.toContain("examples");
130
+ expect(names).not.toContain("nested");
131
+ });
132
+ // TC-021: discoverSkills populates descriptions from fetched content
133
+ it("populates description field from SKILL.md content", async () => {
134
+ const mockFetch = vi.fn()
135
+ .mockResolvedValueOnce(makeBranchResponse("main"))
136
+ .mockResolvedValueOnce({
137
+ ok: true,
138
+ json: async () => makeTreeResponse(["skills/foo/SKILL.md", "skills/bar/SKILL.md"]),
139
+ })
140
+ .mockResolvedValue({
141
+ ok: true,
142
+ text: async () => "# Foo Skill\n\nThis skill does X",
143
+ });
144
+ globalThis.fetch = mockFetch;
145
+ const result = await discoverSkills("owner", "repo");
146
+ expect(result).toHaveLength(2);
147
+ // At least one skill should have a description populated
148
+ const hasDescription = result.some((s) => s.description !== undefined);
149
+ expect(hasDescription).toBe(true);
150
+ });
151
+ // Calls correct GitHub Trees API URL
152
+ it("calls the GitHub Trees API with correct URL", async () => {
153
+ const mockFetch = vi.fn().mockResolvedValue({
154
+ ok: true,
155
+ json: async () => makeTreeResponse(["SKILL.md"]),
156
+ });
157
+ globalThis.fetch = mockFetch;
158
+ await discoverSkills("anthropics", "frontend-design");
159
+ // First call is getDefaultBranch, second is the tree API
160
+ expect(mockFetch).toHaveBeenNthCalledWith(2, "https://api.github.com/repos/anthropics/frontend-design/git/trees/main?recursive=1", expect.objectContaining({
161
+ headers: expect.objectContaining({
162
+ Accept: "application/vnd.github.v3+json",
163
+ }),
164
+ }));
165
+ });
166
+ // Uses the repo's actual default branch (e.g. master)
167
+ it("uses repo default branch instead of hardcoded main", async () => {
168
+ const mockFetch = vi.fn()
169
+ .mockResolvedValueOnce(makeBranchResponse("master"))
170
+ .mockResolvedValueOnce({
171
+ ok: true,
172
+ json: async () => makeTreeResponse(["SKILL.md"]),
173
+ });
174
+ globalThis.fetch = mockFetch;
175
+ const result = await discoverSkills("owner", "repo");
176
+ expect(mockFetch).toHaveBeenNthCalledWith(2, "https://api.github.com/repos/owner/repo/git/trees/master?recursive=1", expect.anything());
177
+ expect(result[0].rawUrl).toBe("https://raw.githubusercontent.com/owner/repo/master/SKILL.md");
178
+ });
179
+ });
180
+ describe("extractDescription", () => {
181
+ it("returns first non-heading, non-empty line as description", () => {
182
+ const content = "# Title\n\nThis skill does X\n\nMore content";
183
+ expect(extractDescription(content)).toBe("This skill does X");
184
+ });
185
+ it("truncates description at 80 chars with ellipsis", () => {
186
+ const longLine = "A".repeat(120);
187
+ const content = `# Title\n\n${longLine}`;
188
+ const result = extractDescription(content);
189
+ expect(result).toBe("A".repeat(77) + "...");
190
+ expect(result.length).toBe(80);
191
+ });
192
+ it("skips YAML frontmatter delimiters", () => {
193
+ const content = "---\ntitle: foo\n---\n# Title\nDescription here";
194
+ expect(extractDescription(content)).toBe("Description here");
195
+ });
196
+ it("returns undefined when content has only headings", () => {
197
+ const content = "# Title\n## Section\n### Subsection";
198
+ expect(extractDescription(content)).toBeUndefined();
199
+ });
200
+ it("returns undefined for empty content", () => {
201
+ expect(extractDescription("")).toBeUndefined();
202
+ });
203
+ it("skips blank lines before first content line", () => {
204
+ const content = "\n\n# Title\n\n\nActual description";
205
+ expect(extractDescription(content)).toBe("Actual description");
206
+ });
207
+ });
208
+ describe("getDefaultBranch", () => {
209
+ const originalFetch = globalThis.fetch;
210
+ afterEach(() => {
211
+ globalThis.fetch = originalFetch;
212
+ _resetBranchCache();
213
+ });
214
+ it("returns default_branch from GitHub API", async () => {
215
+ globalThis.fetch = vi.fn().mockResolvedValue({
216
+ ok: true,
217
+ json: async () => ({ default_branch: "develop" }),
218
+ });
219
+ const branch = await getDefaultBranch("test-owner", "test-repo-1");
220
+ expect(branch).toBe("develop");
221
+ });
222
+ it("falls back to main on API error", async () => {
223
+ globalThis.fetch = vi.fn().mockResolvedValue({
224
+ ok: false,
225
+ status: 404,
226
+ });
227
+ const branch = await getDefaultBranch("test-owner", "test-repo-2");
228
+ expect(branch).toBe("main");
229
+ });
230
+ it("falls back to main on network error", async () => {
231
+ globalThis.fetch = vi.fn().mockRejectedValue(new Error("Network error"));
232
+ const branch = await getDefaultBranch("test-owner", "test-repo-3");
233
+ expect(branch).toBe("main");
234
+ });
235
+ it("caches results per owner/repo", async () => {
236
+ const mockFetch = vi.fn().mockResolvedValue({
237
+ ok: true,
238
+ json: async () => ({ default_branch: "master" }),
239
+ });
240
+ globalThis.fetch = mockFetch;
241
+ const first = await getDefaultBranch("cached-owner", "cached-repo");
242
+ const second = await getDefaultBranch("cached-owner", "cached-repo");
243
+ expect(first).toBe("master");
244
+ expect(second).toBe("master");
245
+ // Only one fetch call — second was served from cache
246
+ expect(mockFetch).toHaveBeenCalledTimes(1);
247
+ });
248
+ });
249
+ // ---------------------------------------------------------------------------
250
+ // warnRateLimitOnce
251
+ // ---------------------------------------------------------------------------
252
+ describe("warnRateLimitOnce", () => {
253
+ afterEach(() => {
254
+ _resetRateLimitWarned();
255
+ });
256
+ // TC-001: prints warning on 403 with x-ratelimit-remaining: 0
257
+ it("prints warning on 403 with x-ratelimit-remaining: 0", () => {
258
+ const spy = vi.spyOn(console, "error").mockImplementation(() => { });
259
+ const res = {
260
+ status: 403,
261
+ headers: new Headers({ "x-ratelimit-remaining": "0" }),
262
+ };
263
+ warnRateLimitOnce(res);
264
+ expect(spy).toHaveBeenCalledTimes(1);
265
+ expect(spy.mock.calls[0][0]).toContain("rate limit");
266
+ spy.mockRestore();
267
+ });
268
+ // TC-002: does not print on 403 without rate-limit header
269
+ it("does not print on 403 without rate-limit header", () => {
270
+ const spy = vi.spyOn(console, "error").mockImplementation(() => { });
271
+ const res = {
272
+ status: 403,
273
+ headers: new Headers(),
274
+ };
275
+ warnRateLimitOnce(res);
276
+ expect(spy).not.toHaveBeenCalled();
277
+ spy.mockRestore();
278
+ });
279
+ // TC-003: prints warning only once across multiple calls
280
+ it("prints warning only once across multiple calls", () => {
281
+ const spy = vi.spyOn(console, "error").mockImplementation(() => { });
282
+ const res = {
283
+ status: 403,
284
+ headers: new Headers({ "x-ratelimit-remaining": "0" }),
285
+ };
286
+ warnRateLimitOnce(res);
287
+ warnRateLimitOnce(res);
288
+ warnRateLimitOnce(res);
289
+ expect(spy).toHaveBeenCalledTimes(1);
290
+ spy.mockRestore();
291
+ });
292
+ // TC-004: _resetRateLimitWarned allows re-warning
293
+ it("_resetRateLimitWarned allows re-warning", () => {
294
+ const spy = vi.spyOn(console, "error").mockImplementation(() => { });
295
+ const res = {
296
+ status: 403,
297
+ headers: new Headers({ "x-ratelimit-remaining": "0" }),
298
+ };
299
+ warnRateLimitOnce(res);
300
+ expect(spy).toHaveBeenCalledTimes(1);
301
+ _resetRateLimitWarned();
302
+ warnRateLimitOnce(res);
303
+ expect(spy).toHaveBeenCalledTimes(2);
304
+ spy.mockRestore();
305
+ });
306
+ });
307
+ // ---------------------------------------------------------------------------
308
+ // Discovery scope guard
309
+ // ---------------------------------------------------------------------------
310
+ describe("discovery scope guard", () => {
311
+ const originalFetch = globalThis.fetch;
312
+ afterEach(() => {
313
+ globalThis.fetch = originalFetch;
314
+ _resetBranchCache();
315
+ });
316
+ // TC-024: plugins/foo/SKILL.md not matched by discovery
317
+ it("plugins/foo/SKILL.md not matched by discovery", async () => {
318
+ globalThis.fetch = vi.fn().mockResolvedValue({
319
+ ok: true,
320
+ json: async () => makeTreeResponse(["plugins/foo/SKILL.md", "README.md"]),
321
+ });
322
+ const result = await discoverSkills("owner", "repo");
323
+ expect(result).toEqual([]);
324
+ });
325
+ // TC-025: plugins/specweave/skills/pm/SKILL.md not matched
326
+ it("plugins/specweave/skills/pm/SKILL.md not matched", async () => {
327
+ globalThis.fetch = vi.fn().mockResolvedValue({
328
+ ok: true,
329
+ json: async () => makeTreeResponse(["plugins/specweave/skills/pm/SKILL.md", "README.md"]),
330
+ });
331
+ const result = await discoverSkills("owner", "repo");
332
+ expect(result).toEqual([]);
333
+ });
334
+ // TC-027: discovers agents/*.md inside skill directories
335
+ it("discovers agents/*.md and attaches agentRawUrls to parent skill", async () => {
336
+ globalThis.fetch = vi.fn().mockResolvedValue({
337
+ ok: true,
338
+ json: async () => makeTreeResponse([
339
+ "skills/team-lead/SKILL.md",
340
+ "skills/team-lead/agents/frontend.md",
341
+ "skills/team-lead/agents/backend.md",
342
+ "skills/other/SKILL.md",
343
+ "README.md",
344
+ ]),
345
+ });
346
+ const result = await discoverSkills("owner", "repo");
347
+ expect(result).toHaveLength(2);
348
+ const teamLead = result.find((s) => s.name === "team-lead");
349
+ const other = result.find((s) => s.name === "other");
350
+ expect(teamLead?.agentRawUrls).toEqual({
351
+ "agents/frontend.md": "https://raw.githubusercontent.com/owner/repo/main/skills/team-lead/agents/frontend.md",
352
+ "agents/backend.md": "https://raw.githubusercontent.com/owner/repo/main/skills/team-lead/agents/backend.md",
353
+ });
354
+ // Skill without agents/ has no agentRawUrls
355
+ expect(other?.agentRawUrls).toBeUndefined();
356
+ });
357
+ // TC-026: skills/pm/SKILL.md IS matched (positive control)
358
+ it("skills/pm/SKILL.md IS matched (positive control)", async () => {
359
+ globalThis.fetch = vi.fn().mockResolvedValue({
360
+ ok: true,
361
+ json: async () => makeTreeResponse(["skills/pm/SKILL.md", "README.md"]),
362
+ });
363
+ const result = await discoverSkills("owner", "repo");
364
+ expect(result).toHaveLength(1);
365
+ expect(result[0]).toEqual({
366
+ name: "pm",
367
+ path: "skills/pm/SKILL.md",
368
+ rawUrl: "https://raw.githubusercontent.com/owner/repo/main/skills/pm/SKILL.md",
369
+ });
370
+ });
371
+ });
372
+ //# sourceMappingURL=github-tree.test.js.map