@lumenflow/cli 1.5.0 → 2.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/dist/__tests__/backlog-prune.test.js +478 -0
- package/dist/__tests__/deps-operations.test.js +206 -0
- package/dist/__tests__/file-operations.test.js +906 -0
- package/dist/__tests__/git-operations.test.js +668 -0
- package/dist/__tests__/guards-validation.test.js +416 -0
- package/dist/__tests__/init-plan.test.js +340 -0
- package/dist/__tests__/lumenflow-upgrade.test.js +107 -0
- package/dist/__tests__/metrics-cli.test.js +619 -0
- package/dist/__tests__/rotate-progress.test.js +127 -0
- package/dist/__tests__/session-coordinator.test.js +109 -0
- package/dist/__tests__/state-bootstrap.test.js +432 -0
- package/dist/__tests__/trace-gen.test.js +115 -0
- package/dist/backlog-prune.js +299 -0
- package/dist/deps-add.js +215 -0
- package/dist/deps-remove.js +94 -0
- package/dist/file-delete.js +236 -0
- package/dist/file-edit.js +247 -0
- package/dist/file-read.js +197 -0
- package/dist/file-write.js +220 -0
- package/dist/git-branch.js +187 -0
- package/dist/git-diff.js +177 -0
- package/dist/git-log.js +230 -0
- package/dist/git-status.js +208 -0
- package/dist/guard-locked.js +169 -0
- package/dist/guard-main-branch.js +202 -0
- package/dist/guard-worktree-commit.js +160 -0
- package/dist/init-plan.js +337 -0
- package/dist/lumenflow-upgrade.js +178 -0
- package/dist/metrics-cli.js +433 -0
- package/dist/rotate-progress.js +247 -0
- package/dist/session-coordinator.js +300 -0
- package/dist/state-bootstrap.js +307 -0
- package/dist/trace-gen.js +331 -0
- package/dist/validate-agent-skills.js +218 -0
- package/dist/validate-agent-sync.js +148 -0
- package/dist/validate-backlog-sync.js +152 -0
- package/dist/validate-skills-spec.js +206 -0
- package/dist/validate.js +230 -0
- package/dist/wu-recover.js +329 -0
- package/dist/wu-status.js +188 -0
- package/package.json +37 -7
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file guards-validation.test.ts
|
|
3
|
+
* @description Tests for guard and validation CLI tools (WU-1111)
|
|
4
|
+
*
|
|
5
|
+
* Tests cover:
|
|
6
|
+
* - guard-worktree-commit: Prevents WU commits from main checkout
|
|
7
|
+
* - guard-locked: Prevents changes to locked WUs
|
|
8
|
+
* - validate: Main WU YAML validator
|
|
9
|
+
* - validate-agent-skills: Validates agent skill definitions
|
|
10
|
+
* - validate-agent-sync: Validates agent sync state
|
|
11
|
+
* - validate-backlog-sync: Validates backlog.md is in sync with WU YAML files
|
|
12
|
+
* - validate-skills-spec: Validates skills spec format
|
|
13
|
+
*/
|
|
14
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
15
|
+
import { mkdirSync, writeFileSync, rmSync } from 'node:fs';
|
|
16
|
+
import path from 'node:path';
|
|
17
|
+
import os from 'node:os';
|
|
18
|
+
// Test utilities
|
|
19
|
+
function createTempDir() {
|
|
20
|
+
const tmpDir = path.join(os.tmpdir(), `guards-test-${Date.now()}`);
|
|
21
|
+
mkdirSync(tmpDir, { recursive: true });
|
|
22
|
+
return tmpDir;
|
|
23
|
+
}
|
|
24
|
+
function cleanupTempDir(dir) {
|
|
25
|
+
try {
|
|
26
|
+
rmSync(dir, { recursive: true, force: true });
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
// Ignore cleanup errors
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
// ============================================================================
|
|
33
|
+
// guard-locked tests
|
|
34
|
+
// ============================================================================
|
|
35
|
+
describe('guard-locked', () => {
|
|
36
|
+
let tmpDir;
|
|
37
|
+
beforeEach(() => {
|
|
38
|
+
tmpDir = createTempDir();
|
|
39
|
+
});
|
|
40
|
+
afterEach(() => {
|
|
41
|
+
cleanupTempDir(tmpDir);
|
|
42
|
+
});
|
|
43
|
+
describe('isWULocked', () => {
|
|
44
|
+
it('should return true when WU has locked: true', async () => {
|
|
45
|
+
const { isWULocked } = await import('../guard-locked.js');
|
|
46
|
+
const wuYaml = `
|
|
47
|
+
id: WU-123
|
|
48
|
+
title: Test WU
|
|
49
|
+
status: done
|
|
50
|
+
locked: true
|
|
51
|
+
`;
|
|
52
|
+
const wuPath = path.join(tmpDir, 'WU-123.yaml');
|
|
53
|
+
writeFileSync(wuPath, wuYaml);
|
|
54
|
+
const result = isWULocked(wuPath);
|
|
55
|
+
expect(result).toBe(true);
|
|
56
|
+
});
|
|
57
|
+
it('should return false when WU has locked: false', async () => {
|
|
58
|
+
const { isWULocked } = await import('../guard-locked.js');
|
|
59
|
+
const wuYaml = `
|
|
60
|
+
id: WU-456
|
|
61
|
+
title: Test WU
|
|
62
|
+
status: in_progress
|
|
63
|
+
locked: false
|
|
64
|
+
`;
|
|
65
|
+
const wuPath = path.join(tmpDir, 'WU-456.yaml');
|
|
66
|
+
writeFileSync(wuPath, wuYaml);
|
|
67
|
+
const result = isWULocked(wuPath);
|
|
68
|
+
expect(result).toBe(false);
|
|
69
|
+
});
|
|
70
|
+
it('should return false when WU has no locked field', async () => {
|
|
71
|
+
const { isWULocked } = await import('../guard-locked.js');
|
|
72
|
+
const wuYaml = `
|
|
73
|
+
id: WU-789
|
|
74
|
+
title: Test WU
|
|
75
|
+
status: ready
|
|
76
|
+
`;
|
|
77
|
+
const wuPath = path.join(tmpDir, 'WU-789.yaml');
|
|
78
|
+
writeFileSync(wuPath, wuYaml);
|
|
79
|
+
const result = isWULocked(wuPath);
|
|
80
|
+
expect(result).toBe(false);
|
|
81
|
+
});
|
|
82
|
+
it('should throw when WU file does not exist', async () => {
|
|
83
|
+
const { isWULocked } = await import('../guard-locked.js');
|
|
84
|
+
const wuPath = path.join(tmpDir, 'WU-999.yaml');
|
|
85
|
+
expect(() => isWULocked(wuPath)).toThrow(/not found/);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
describe('assertWUNotLocked', () => {
|
|
89
|
+
it('should not throw when WU is not locked', async () => {
|
|
90
|
+
const { assertWUNotLocked } = await import('../guard-locked.js');
|
|
91
|
+
const wuYaml = `
|
|
92
|
+
id: WU-100
|
|
93
|
+
title: Unlocked WU
|
|
94
|
+
status: in_progress
|
|
95
|
+
locked: false
|
|
96
|
+
`;
|
|
97
|
+
const wuPath = path.join(tmpDir, 'WU-100.yaml');
|
|
98
|
+
writeFileSync(wuPath, wuYaml);
|
|
99
|
+
expect(() => assertWUNotLocked(wuPath)).not.toThrow();
|
|
100
|
+
});
|
|
101
|
+
it('should throw with descriptive message when WU is locked', async () => {
|
|
102
|
+
const { assertWUNotLocked } = await import('../guard-locked.js');
|
|
103
|
+
const wuYaml = `
|
|
104
|
+
id: WU-200
|
|
105
|
+
title: Locked WU
|
|
106
|
+
status: done
|
|
107
|
+
locked: true
|
|
108
|
+
`;
|
|
109
|
+
const wuPath = path.join(tmpDir, 'WU-200.yaml');
|
|
110
|
+
writeFileSync(wuPath, wuYaml);
|
|
111
|
+
expect(() => assertWUNotLocked(wuPath)).toThrow(/WU-200.*locked/i);
|
|
112
|
+
});
|
|
113
|
+
it('should include wu:unlock suggestion in error message', async () => {
|
|
114
|
+
const { assertWUNotLocked } = await import('../guard-locked.js');
|
|
115
|
+
const wuYaml = `
|
|
116
|
+
id: WU-300
|
|
117
|
+
title: Locked WU
|
|
118
|
+
status: done
|
|
119
|
+
locked: true
|
|
120
|
+
`;
|
|
121
|
+
const wuPath = path.join(tmpDir, 'WU-300.yaml');
|
|
122
|
+
writeFileSync(wuPath, wuYaml);
|
|
123
|
+
expect(() => assertWUNotLocked(wuPath)).toThrow(/wu:unlock/);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
// ============================================================================
|
|
128
|
+
// guard-worktree-commit tests
|
|
129
|
+
// ============================================================================
|
|
130
|
+
describe('guard-worktree-commit', () => {
|
|
131
|
+
describe('shouldBlockCommit', () => {
|
|
132
|
+
it('should block commits with WU prefix from main checkout', async () => {
|
|
133
|
+
const { shouldBlockCommit } = await import('../guard-worktree-commit.js');
|
|
134
|
+
const result = shouldBlockCommit({
|
|
135
|
+
commitMessage: 'wu(WU-123): add feature',
|
|
136
|
+
isMainCheckout: true,
|
|
137
|
+
isInWorktree: false,
|
|
138
|
+
});
|
|
139
|
+
expect(result.blocked).toBe(true);
|
|
140
|
+
expect(result.reason).toMatch(/worktree/i);
|
|
141
|
+
});
|
|
142
|
+
it('should allow commits with WU prefix from worktree', async () => {
|
|
143
|
+
const { shouldBlockCommit } = await import('../guard-worktree-commit.js');
|
|
144
|
+
const result = shouldBlockCommit({
|
|
145
|
+
commitMessage: 'wu(WU-123): add feature',
|
|
146
|
+
isMainCheckout: false,
|
|
147
|
+
isInWorktree: true,
|
|
148
|
+
});
|
|
149
|
+
expect(result.blocked).toBe(false);
|
|
150
|
+
});
|
|
151
|
+
it('should allow non-WU commits from main checkout', async () => {
|
|
152
|
+
const { shouldBlockCommit } = await import('../guard-worktree-commit.js');
|
|
153
|
+
const result = shouldBlockCommit({
|
|
154
|
+
commitMessage: 'chore: update dependencies',
|
|
155
|
+
isMainCheckout: true,
|
|
156
|
+
isInWorktree: false,
|
|
157
|
+
});
|
|
158
|
+
expect(result.blocked).toBe(false);
|
|
159
|
+
});
|
|
160
|
+
it('should detect WU prefix case-insensitively', async () => {
|
|
161
|
+
const { shouldBlockCommit } = await import('../guard-worktree-commit.js');
|
|
162
|
+
const result = shouldBlockCommit({
|
|
163
|
+
commitMessage: 'WU(wu-456): something',
|
|
164
|
+
isMainCheckout: true,
|
|
165
|
+
isInWorktree: false,
|
|
166
|
+
});
|
|
167
|
+
expect(result.blocked).toBe(true);
|
|
168
|
+
});
|
|
169
|
+
it('should block feat(WU-xxx) pattern from main', async () => {
|
|
170
|
+
const { shouldBlockCommit } = await import('../guard-worktree-commit.js');
|
|
171
|
+
const result = shouldBlockCommit({
|
|
172
|
+
commitMessage: 'feat(WU-789): new feature',
|
|
173
|
+
isMainCheckout: true,
|
|
174
|
+
isInWorktree: false,
|
|
175
|
+
});
|
|
176
|
+
expect(result.blocked).toBe(true);
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
// ============================================================================
|
|
181
|
+
// validate-agent-skills tests
|
|
182
|
+
// ============================================================================
|
|
183
|
+
describe('validate-agent-skills', () => {
|
|
184
|
+
let tmpDir;
|
|
185
|
+
beforeEach(() => {
|
|
186
|
+
tmpDir = createTempDir();
|
|
187
|
+
mkdirSync(path.join(tmpDir, '.claude', 'skills'), { recursive: true });
|
|
188
|
+
});
|
|
189
|
+
afterEach(() => {
|
|
190
|
+
cleanupTempDir(tmpDir);
|
|
191
|
+
});
|
|
192
|
+
describe('validateSkillFile', () => {
|
|
193
|
+
it('should pass for valid skill with all required fields', async () => {
|
|
194
|
+
const { validateSkillFile } = await import('../validate-agent-skills.js');
|
|
195
|
+
const skillContent = `# My Skill
|
|
196
|
+
|
|
197
|
+
**Source**: some-doc.md
|
|
198
|
+
|
|
199
|
+
## When to Use
|
|
200
|
+
Use this skill when you need to do something.
|
|
201
|
+
|
|
202
|
+
## Core Concepts
|
|
203
|
+
Important concepts here.
|
|
204
|
+
`;
|
|
205
|
+
const skillPath = path.join(tmpDir, '.claude', 'skills', 'my-skill', 'SKILL.md');
|
|
206
|
+
mkdirSync(path.dirname(skillPath), { recursive: true });
|
|
207
|
+
writeFileSync(skillPath, skillContent);
|
|
208
|
+
const result = validateSkillFile(skillPath);
|
|
209
|
+
expect(result.valid).toBe(true);
|
|
210
|
+
expect(result.errors).toHaveLength(0);
|
|
211
|
+
});
|
|
212
|
+
it('should fail for skill missing "When to Use" section', async () => {
|
|
213
|
+
const { validateSkillFile } = await import('../validate-agent-skills.js');
|
|
214
|
+
const skillContent = `# My Skill
|
|
215
|
+
|
|
216
|
+
**Source**: some-doc.md
|
|
217
|
+
|
|
218
|
+
## Core Concepts
|
|
219
|
+
Concepts only, no when-to-use.
|
|
220
|
+
`;
|
|
221
|
+
const skillPath = path.join(tmpDir, '.claude', 'skills', 'bad-skill', 'SKILL.md');
|
|
222
|
+
mkdirSync(path.dirname(skillPath), { recursive: true });
|
|
223
|
+
writeFileSync(skillPath, skillContent);
|
|
224
|
+
const result = validateSkillFile(skillPath);
|
|
225
|
+
expect(result.valid).toBe(false);
|
|
226
|
+
expect(result.errors.some((e) => /When to Use/i.test(e))).toBe(true);
|
|
227
|
+
});
|
|
228
|
+
it('should fail for skill missing title heading', async () => {
|
|
229
|
+
const { validateSkillFile } = await import('../validate-agent-skills.js');
|
|
230
|
+
const skillContent = `No title heading here
|
|
231
|
+
|
|
232
|
+
## When to Use
|
|
233
|
+
Some content.
|
|
234
|
+
`;
|
|
235
|
+
const skillPath = path.join(tmpDir, '.claude', 'skills', 'no-title', 'SKILL.md');
|
|
236
|
+
mkdirSync(path.dirname(skillPath), { recursive: true });
|
|
237
|
+
writeFileSync(skillPath, skillContent);
|
|
238
|
+
const result = validateSkillFile(skillPath);
|
|
239
|
+
expect(result.valid).toBe(false);
|
|
240
|
+
expect(result.errors.some((e) => /title|heading/i.test(e))).toBe(true);
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
describe('validateAllSkills', () => {
|
|
244
|
+
it('should validate all skills in directory', async () => {
|
|
245
|
+
const { validateAllSkills } = await import('../validate-agent-skills.js');
|
|
246
|
+
// Create two valid skills
|
|
247
|
+
const skill1Path = path.join(tmpDir, '.claude', 'skills', 'skill-one', 'SKILL.md');
|
|
248
|
+
const skill2Path = path.join(tmpDir, '.claude', 'skills', 'skill-two', 'SKILL.md');
|
|
249
|
+
mkdirSync(path.dirname(skill1Path), { recursive: true });
|
|
250
|
+
mkdirSync(path.dirname(skill2Path), { recursive: true });
|
|
251
|
+
const validContent = `# Valid Skill
|
|
252
|
+
|
|
253
|
+
## When to Use
|
|
254
|
+
Use this.
|
|
255
|
+
`;
|
|
256
|
+
writeFileSync(skill1Path, validContent);
|
|
257
|
+
writeFileSync(skill2Path, validContent);
|
|
258
|
+
const results = validateAllSkills(path.join(tmpDir, '.claude', 'skills'));
|
|
259
|
+
expect(results.totalValid).toBe(2);
|
|
260
|
+
expect(results.totalInvalid).toBe(0);
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
// ============================================================================
|
|
265
|
+
// validate-agent-sync tests
|
|
266
|
+
// ============================================================================
|
|
267
|
+
describe('validate-agent-sync', () => {
|
|
268
|
+
let tmpDir;
|
|
269
|
+
beforeEach(() => {
|
|
270
|
+
tmpDir = createTempDir();
|
|
271
|
+
});
|
|
272
|
+
afterEach(() => {
|
|
273
|
+
cleanupTempDir(tmpDir);
|
|
274
|
+
});
|
|
275
|
+
describe('validateAgentSync', () => {
|
|
276
|
+
it('should pass when agent files exist and match expected structure', async () => {
|
|
277
|
+
const { validateAgentSync } = await import('../validate-agent-sync.js');
|
|
278
|
+
// Create expected agent directory structure
|
|
279
|
+
const agentDir = path.join(tmpDir, '.claude', 'agents');
|
|
280
|
+
mkdirSync(agentDir, { recursive: true });
|
|
281
|
+
const agentDef = `{
|
|
282
|
+
"name": "test-agent",
|
|
283
|
+
"description": "A test agent"
|
|
284
|
+
}`;
|
|
285
|
+
writeFileSync(path.join(agentDir, 'test-agent.json'), agentDef);
|
|
286
|
+
const result = await validateAgentSync({ cwd: tmpDir });
|
|
287
|
+
expect(result.valid).toBe(true);
|
|
288
|
+
});
|
|
289
|
+
it('should fail when agent directory is missing', async () => {
|
|
290
|
+
const { validateAgentSync } = await import('../validate-agent-sync.js');
|
|
291
|
+
// No agent directory created
|
|
292
|
+
const result = await validateAgentSync({ cwd: tmpDir });
|
|
293
|
+
expect(result.valid).toBe(false);
|
|
294
|
+
expect(result.errors.some((e) => /agents.*not found|missing/i.test(e))).toBe(true);
|
|
295
|
+
});
|
|
296
|
+
it('should warn when agent definitions have no description', async () => {
|
|
297
|
+
const { validateAgentSync } = await import('../validate-agent-sync.js');
|
|
298
|
+
const agentDir = path.join(tmpDir, '.claude', 'agents');
|
|
299
|
+
mkdirSync(agentDir, { recursive: true });
|
|
300
|
+
const agentDef = `{
|
|
301
|
+
"name": "no-desc-agent"
|
|
302
|
+
}`;
|
|
303
|
+
writeFileSync(path.join(agentDir, 'no-desc.json'), agentDef);
|
|
304
|
+
const result = await validateAgentSync({ cwd: tmpDir });
|
|
305
|
+
expect(result.warnings.some((w) => /description/i.test(w))).toBe(true);
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
// ============================================================================
|
|
310
|
+
// validate-backlog-sync tests
|
|
311
|
+
// ============================================================================
|
|
312
|
+
describe('validate-backlog-sync', () => {
|
|
313
|
+
let tmpDir;
|
|
314
|
+
beforeEach(() => {
|
|
315
|
+
tmpDir = createTempDir();
|
|
316
|
+
mkdirSync(path.join(tmpDir, 'docs', '04-operations', 'tasks', 'wu'), { recursive: true });
|
|
317
|
+
});
|
|
318
|
+
afterEach(() => {
|
|
319
|
+
cleanupTempDir(tmpDir);
|
|
320
|
+
});
|
|
321
|
+
describe('validateBacklogSync', () => {
|
|
322
|
+
it('should pass when backlog.md lists all WU YAML files', async () => {
|
|
323
|
+
const { validateBacklogSync } = await import('../validate-backlog-sync.js');
|
|
324
|
+
// Create WU files
|
|
325
|
+
writeFileSync(path.join(tmpDir, 'docs', '04-operations', 'tasks', 'wu', 'WU-001.yaml'), 'id: WU-001\ntitle: First WU\nstatus: ready');
|
|
326
|
+
writeFileSync(path.join(tmpDir, 'docs', '04-operations', 'tasks', 'wu', 'WU-002.yaml'), 'id: WU-002\ntitle: Second WU\nstatus: done');
|
|
327
|
+
// Create backlog.md that references both
|
|
328
|
+
const backlogContent = `# Backlog
|
|
329
|
+
|
|
330
|
+
## Ready
|
|
331
|
+
- WU-001: First WU
|
|
332
|
+
|
|
333
|
+
## Done
|
|
334
|
+
- WU-002: Second WU
|
|
335
|
+
`;
|
|
336
|
+
writeFileSync(path.join(tmpDir, 'docs', '04-operations', 'tasks', 'backlog.md'), backlogContent);
|
|
337
|
+
const result = await validateBacklogSync({ cwd: tmpDir });
|
|
338
|
+
expect(result.valid).toBe(true);
|
|
339
|
+
});
|
|
340
|
+
it('should fail when WU exists but not in backlog.md', async () => {
|
|
341
|
+
const { validateBacklogSync } = await import('../validate-backlog-sync.js');
|
|
342
|
+
// Create WU file
|
|
343
|
+
writeFileSync(path.join(tmpDir, 'docs', '04-operations', 'tasks', 'wu', 'WU-001.yaml'), 'id: WU-001\ntitle: Missing WU\nstatus: ready');
|
|
344
|
+
// Create backlog.md that doesn't reference the WU
|
|
345
|
+
const backlogContent = `# Backlog
|
|
346
|
+
|
|
347
|
+
## Ready
|
|
348
|
+
(empty)
|
|
349
|
+
`;
|
|
350
|
+
writeFileSync(path.join(tmpDir, 'docs', '04-operations', 'tasks', 'backlog.md'), backlogContent);
|
|
351
|
+
const result = await validateBacklogSync({ cwd: tmpDir });
|
|
352
|
+
expect(result.valid).toBe(false);
|
|
353
|
+
expect(result.errors.some((e) => /WU-001.*not found.*backlog/i.test(e))).toBe(true);
|
|
354
|
+
});
|
|
355
|
+
});
|
|
356
|
+
});
|
|
357
|
+
// ============================================================================
|
|
358
|
+
// validate-skills-spec tests
|
|
359
|
+
// ============================================================================
|
|
360
|
+
describe('validate-skills-spec', () => {
|
|
361
|
+
let tmpDir;
|
|
362
|
+
beforeEach(() => {
|
|
363
|
+
tmpDir = createTempDir();
|
|
364
|
+
});
|
|
365
|
+
afterEach(() => {
|
|
366
|
+
cleanupTempDir(tmpDir);
|
|
367
|
+
});
|
|
368
|
+
describe('validateSkillsSpec', () => {
|
|
369
|
+
it('should pass for skill with required sections', async () => {
|
|
370
|
+
const { validateSkillsSpec } = await import('../validate-skills-spec.js');
|
|
371
|
+
const skillSpec = `# Skill Name
|
|
372
|
+
|
|
373
|
+
## When to Use
|
|
374
|
+
Describe when to activate this skill.
|
|
375
|
+
|
|
376
|
+
## Key Concepts
|
|
377
|
+
Important concepts.
|
|
378
|
+
|
|
379
|
+
## Examples
|
|
380
|
+
Show examples.
|
|
381
|
+
`;
|
|
382
|
+
const skillPath = path.join(tmpDir, 'SKILL.md');
|
|
383
|
+
writeFileSync(skillPath, skillSpec);
|
|
384
|
+
const result = validateSkillsSpec(skillPath);
|
|
385
|
+
expect(result.valid).toBe(true);
|
|
386
|
+
});
|
|
387
|
+
it('should fail for skill missing required "When to Use" section', async () => {
|
|
388
|
+
const { validateSkillsSpec } = await import('../validate-skills-spec.js');
|
|
389
|
+
const skillSpec = `# Skill Name
|
|
390
|
+
|
|
391
|
+
## Key Concepts
|
|
392
|
+
Concepts but no when-to-use.
|
|
393
|
+
`;
|
|
394
|
+
const skillPath = path.join(tmpDir, 'SKILL.md');
|
|
395
|
+
writeFileSync(skillPath, skillSpec);
|
|
396
|
+
const result = validateSkillsSpec(skillPath);
|
|
397
|
+
expect(result.valid).toBe(false);
|
|
398
|
+
expect(result.errors.some((e) => /When to Use/i.test(e))).toBe(true);
|
|
399
|
+
});
|
|
400
|
+
it('should warn for skill without examples section', async () => {
|
|
401
|
+
const { validateSkillsSpec } = await import('../validate-skills-spec.js');
|
|
402
|
+
const skillSpec = `# Skill Name
|
|
403
|
+
|
|
404
|
+
## When to Use
|
|
405
|
+
When needed.
|
|
406
|
+
|
|
407
|
+
## Key Concepts
|
|
408
|
+
Concepts only.
|
|
409
|
+
`;
|
|
410
|
+
const skillPath = path.join(tmpDir, 'SKILL.md');
|
|
411
|
+
writeFileSync(skillPath, skillSpec);
|
|
412
|
+
const result = validateSkillsSpec(skillPath);
|
|
413
|
+
expect(result.warnings.some((w) => /Examples/i.test(w))).toBe(true);
|
|
414
|
+
});
|
|
415
|
+
});
|
|
416
|
+
});
|