1import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
2import fs from 'fs';
3import path from 'path';
4import {
5 parseFrontmatter,
6 readFilesRecursive,
7 readSourceFiles,
8 ensureDir,
9 cleanDir,
10 writeFile,
11 generateYamlFrontmatter,
12 readPatterns,
13 replacePlaceholders,
14 prefixSkillReferences
15} from '../../scripts/lib/utils.js';
16
17// Temporary test directory
18const TEST_DIR = path.join(process.cwd(), 'test-tmp');
19
20describe('parseFrontmatter', () => {
21 test('should parse basic frontmatter with simple key-value pairs', () => {
22 const content = `---
23name: test-skill
24description: A test skill
25---
26
27This is the body content.`;
28
29 const result = parseFrontmatter(content);
30 expect(result.frontmatter.name).toBe('test-skill');
31 expect(result.frontmatter.description).toBe('A test skill');
32 expect(result.body).toBe('This is the body content.');
33 });
34
35 test('should parse frontmatter with argument-hint', () => {
36 const content = `---
37name: test-skill
38description: A test skill
39argument-hint: <output> [TARGET=<value>]
40---
41
42Body here.`;
43
44 const result = parseFrontmatter(content);
45 expect(result.frontmatter.name).toBe('test-skill');
46 expect(result.frontmatter['argument-hint']).toBe('<output> [TARGET=<value>]');
47 });
48
49 test('should return empty frontmatter when no frontmatter present', () => {
50 const content = 'Just some content without frontmatter.';
51 const result = parseFrontmatter(content);
52
53 expect(result.frontmatter).toEqual({});
54 expect(result.body).toBe(content);
55 });
56
57 test('should handle empty body', () => {
58 const content = `---
59name: test
60---
61`;
62 const result = parseFrontmatter(content);
63
64 expect(result.frontmatter.name).toBe('test');
65 expect(result.body).toBe('');
66 });
67
68 test('should handle frontmatter with license field', () => {
69 const content = `---
70name: skill-name
71description: A skill
72license: MIT
73---
74
75Skill body.`;
76
77 const result = parseFrontmatter(content);
78 expect(result.frontmatter.license).toBe('MIT');
79 });
80
81 test('should parse user-invocable boolean', () => {
82 const content = `---
83name: test-skill
84user-invocable: true
85---
86
87Body.`;
88
89 const result = parseFrontmatter(content);
90 expect(result.frontmatter['user-invocable']).toBe(true);
91 });
92
93 test('should parse user-invocable as string true (code behavior)', () => {
94 const content = `---
95name: test-skill
96user-invocable: 'true'
97---
98
99Body.`;
100
101 const result = parseFrontmatter(content);
102 // parseFrontmatter strips YAML quotes, so 'true' becomes boolean true
103 expect(result.frontmatter['user-invocable']).toBe(true);
104 });
105
106 test('should parse allowed-tools field', () => {
107 const content = `---
108name: test-skill
109allowed-tools: Bash
110---
111
112Body.`;
113
114 const result = parseFrontmatter(content);
115 expect(result.frontmatter['allowed-tools']).toBe('Bash');
116 });
117});
118
119describe('generateYamlFrontmatter', () => {
120 test('should generate basic frontmatter', () => {
121 const data = {
122 name: 'test-skill',
123 description: 'A test'
124 };
125
126 const result = generateYamlFrontmatter(data);
127 expect(result).toContain('---');
128 expect(result).toContain('name: test-skill');
129 expect(result).toContain('description: A test');
130 });
131
132 test('should generate frontmatter with argument-hint', () => {
133 const data = {
134 name: 'test',
135 description: 'Test skill',
136 'argument-hint': '<output> [TARGET=<value>]'
137 };
138
139 const result = generateYamlFrontmatter(data);
140 expect(result).toContain('argument-hint: <output> [TARGET=<value>]');
141 });
142
143 test('should generate frontmatter with boolean', () => {
144 const data = {
145 name: 'test',
146 description: 'Test',
147 'user-invocable': true
148 };
149
150 const result = generateYamlFrontmatter(data);
151 expect(result).toContain('user-invocable: true');
152 });
153
154 test('should roundtrip: generate and parse back', () => {
155 const original = {
156 name: 'roundtrip-test',
157 description: 'Testing roundtrip',
158 'argument-hint': '<arg1>'
159 };
160
161 const yaml = generateYamlFrontmatter(original);
162 const content = `${yaml}\n\nBody content`;
163 const parsed = parseFrontmatter(content);
164
165 expect(parsed.frontmatter.name).toBe(original.name);
166 expect(parsed.frontmatter.description).toBe(original.description);
167 expect(parsed.frontmatter['argument-hint']).toBe('<arg1>');
168 });
169});
170
171describe('ensureDir', () => {
172 afterEach(() => {
173 if (fs.existsSync(TEST_DIR)) {
174 fs.rmSync(TEST_DIR, { recursive: true, force: true });
175 }
176 });
177
178 test('should create directory if it does not exist', () => {
179 const testPath = path.join(TEST_DIR, 'new-dir');
180 ensureDir(testPath);
181
182 expect(fs.existsSync(testPath)).toBe(true);
183 expect(fs.statSync(testPath).isDirectory()).toBe(true);
184 });
185
186 test('should create nested directories', () => {
187 const testPath = path.join(TEST_DIR, 'level1', 'level2', 'level3');
188 ensureDir(testPath);
189
190 expect(fs.existsSync(testPath)).toBe(true);
191 });
192
193 test('should not throw if directory already exists', () => {
194 const testPath = path.join(TEST_DIR, 'existing');
195 fs.mkdirSync(testPath, { recursive: true });
196
197 expect(() => ensureDir(testPath)).not.toThrow();
198 });
199});
200
201describe('cleanDir', () => {
202 beforeEach(() => {
203 ensureDir(TEST_DIR);
204 });
205
206 afterEach(() => {
207 if (fs.existsSync(TEST_DIR)) {
208 fs.rmSync(TEST_DIR, { recursive: true, force: true });
209 }
210 });
211
212 test('should remove directory and all contents', () => {
213 const filePath = path.join(TEST_DIR, 'test.txt');
214 fs.writeFileSync(filePath, 'content');
215
216 expect(fs.existsSync(filePath)).toBe(true);
217
218 cleanDir(TEST_DIR);
219 expect(fs.existsSync(TEST_DIR)).toBe(false);
220 });
221
222 test('should not throw if directory does not exist', () => {
223 const nonExistent = path.join(TEST_DIR, 'does-not-exist');
224 expect(() => cleanDir(nonExistent)).not.toThrow();
225 });
226
227 test('should remove nested directories', () => {
228 const nestedPath = path.join(TEST_DIR, 'level1', 'level2');
229 ensureDir(nestedPath);
230 fs.writeFileSync(path.join(nestedPath, 'file.txt'), 'content');
231
232 cleanDir(TEST_DIR);
233 expect(fs.existsSync(TEST_DIR)).toBe(false);
234 });
235});
236
237describe('writeFile', () => {
238 afterEach(() => {
239 if (fs.existsSync(TEST_DIR)) {
240 fs.rmSync(TEST_DIR, { recursive: true, force: true });
241 }
242 });
243
244 test('should write file with content', () => {
245 const filePath = path.join(TEST_DIR, 'test.txt');
246 const content = 'Hello, world!';
247
248 writeFile(filePath, content);
249
250 expect(fs.existsSync(filePath)).toBe(true);
251 expect(fs.readFileSync(filePath, 'utf-8')).toBe(content);
252 });
253
254 test('should create parent directories automatically', () => {
255 const filePath = path.join(TEST_DIR, 'nested', 'deep', 'file.txt');
256 writeFile(filePath, 'content');
257
258 expect(fs.existsSync(filePath)).toBe(true);
259 expect(fs.readFileSync(filePath, 'utf-8')).toBe('content');
260 });
261
262 test('should overwrite existing file', () => {
263 const filePath = path.join(TEST_DIR, 'file.txt');
264 writeFile(filePath, 'first');
265 writeFile(filePath, 'second');
266
267 expect(fs.readFileSync(filePath, 'utf-8')).toBe('second');
268 });
269});
270
271describe('readFilesRecursive', () => {
272 beforeEach(() => {
273 ensureDir(TEST_DIR);
274 });
275
276 afterEach(() => {
277 if (fs.existsSync(TEST_DIR)) {
278 fs.rmSync(TEST_DIR, { recursive: true, force: true });
279 }
280 });
281
282 test('should find all markdown files in directory', () => {
283 writeFile(path.join(TEST_DIR, 'file1.md'), 'content1');
284 writeFile(path.join(TEST_DIR, 'file2.md'), 'content2');
285 writeFile(path.join(TEST_DIR, 'file3.txt'), 'not markdown');
286
287 const files = readFilesRecursive(TEST_DIR);
288 expect(files).toHaveLength(2);
289 expect(files.some(f => f.endsWith('file1.md'))).toBe(true);
290 expect(files.some(f => f.endsWith('file2.md'))).toBe(true);
291 });
292
293 test('should find markdown files in nested directories', () => {
294 writeFile(path.join(TEST_DIR, 'root.md'), 'root');
295 writeFile(path.join(TEST_DIR, 'sub', 'nested.md'), 'nested');
296 writeFile(path.join(TEST_DIR, 'sub', 'deep', 'deeper.md'), 'deeper');
297
298 const files = readFilesRecursive(TEST_DIR);
299 expect(files).toHaveLength(3);
300 expect(files.some(f => f.endsWith('root.md'))).toBe(true);
301 expect(files.some(f => f.endsWith('nested.md'))).toBe(true);
302 expect(files.some(f => f.endsWith('deeper.md'))).toBe(true);
303 });
304
305 test('should return empty array for non-existent directory', () => {
306 const files = readFilesRecursive(path.join(TEST_DIR, 'does-not-exist'));
307 expect(files).toEqual([]);
308 });
309
310 test('should return empty array for directory with no markdown files', () => {
311 writeFile(path.join(TEST_DIR, 'file.txt'), 'text');
312 writeFile(path.join(TEST_DIR, 'file.js'), 'code');
313
314 const files = readFilesRecursive(TEST_DIR);
315 expect(files).toEqual([]);
316 });
317});
318
319describe('readSourceFiles', () => {
320 const testRootDir = TEST_DIR;
321
322 beforeEach(() => {
323 ensureDir(testRootDir);
324 });
325
326 afterEach(() => {
327 if (fs.existsSync(testRootDir)) {
328 fs.rmSync(testRootDir, { recursive: true, force: true });
329 }
330 });
331
332 test('should read and parse skill files from directory-based structure', () => {
333 const skillContent = `---
334name: test-skill
335description: A test skill
336license: MIT
337---
338
339Skill instructions here.`;
340
341 const skillDir = path.join(testRootDir, 'source/skills/test-skill');
342 ensureDir(skillDir);
343 fs.writeFileSync(path.join(skillDir, 'SKILL.md'), skillContent);
344
345 const { skills } = readSourceFiles(testRootDir);
346
347 expect(skills).toHaveLength(1);
348 expect(skills[0].name).toBe('test-skill');
349 expect(skills[0].description).toBe('A test skill');
350 expect(skills[0].license).toBe('MIT');
351 expect(skills[0].body).toBe('Skill instructions here.');
352 });
353
354 test('should read skill with user-invocable flag', () => {
355 const skillContent = `---
356name: audit
357description: Run technical quality checks
358user-invocable: true
359---
360
361Audit the code.`;
362
363 const skillDir = path.join(testRootDir, 'source/skills/audit');
364 ensureDir(skillDir);
365 fs.writeFileSync(path.join(skillDir, 'SKILL.md'), skillContent);
366
367 const { skills } = readSourceFiles(testRootDir);
368
369 expect(skills).toHaveLength(1);
370 expect(skills[0].userInvocable).toBe(true);
371 });
372
373 test('should read skill with reference files', () => {
374 const skillContent = `---
375name: impeccable
376description: Impeccable design skill
377---
378
379Impeccable design instructions.`;
380
381 const skillDir = path.join(testRootDir, 'source/skills/impeccable');
382 ensureDir(skillDir);
383 fs.writeFileSync(path.join(skillDir, 'SKILL.md'), skillContent);
384
385 const refDir = path.join(skillDir, 'reference');
386 ensureDir(refDir);
387 fs.writeFileSync(path.join(refDir, 'typography.md'), 'Typography reference content.');
388 fs.writeFileSync(path.join(refDir, 'color.md'), 'Color reference content.');
389
390 const { skills } = readSourceFiles(testRootDir);
391
392 expect(skills).toHaveLength(1);
393 expect(skills[0].references).toHaveLength(2);
394 // References may not be in a specific order due to fs.readdirSync
395 const refNames = skills[0].references.map(r => r.name).sort();
396 expect(refNames).toEqual(['color', 'typography']);
397 });
398
399 test('should use filename as name if not in frontmatter', () => {
400 const skillDir = path.join(testRootDir, 'source/skills/my-skill');
401 ensureDir(skillDir);
402 fs.writeFileSync(path.join(skillDir, 'SKILL.md'), 'Just body, no frontmatter.');
403
404 const { skills } = readSourceFiles(testRootDir);
405
406 expect(skills).toHaveLength(1);
407 expect(skills[0].name).toBe('my-skill');
408 });
409
410 test('should handle empty source directories', () => {
411 ensureDir(path.join(testRootDir, 'source/skills'));
412
413 const { skills } = readSourceFiles(testRootDir);
414
415 expect(skills).toEqual([]);
416 });
417
418 test('should read multiple skills', () => {
419 const skill1Dir = path.join(testRootDir, 'source/skills/skill1');
420 ensureDir(skill1Dir);
421 fs.writeFileSync(path.join(skill1Dir, 'SKILL.md'), '---\nname: skill1\n---\nSkill1');
422
423 const skill2Dir = path.join(testRootDir, 'source/skills/skill2');
424 ensureDir(skill2Dir);
425 fs.writeFileSync(path.join(skill2Dir, 'SKILL.md'), '---\nname: skill2\n---\nSkill2');
426
427 const { skills } = readSourceFiles(testRootDir);
428
429 expect(skills).toHaveLength(2);
430 });
431
432 test('should ignore non-md files in skill directories', () => {
433 const skillDir = path.join(testRootDir, 'source/skills/test-skill');
434 ensureDir(skillDir);
435 fs.writeFileSync(path.join(skillDir, 'SKILL.md'), '---\nname: test-skill\n---\nBody');
436
437 const refDir = path.join(skillDir, 'reference');
438 ensureDir(refDir);
439 fs.writeFileSync(path.join(refDir, 'readme.txt'), 'Not a markdown file');
440 fs.writeFileSync(path.join(refDir, 'typography.md'), 'Valid reference');
441
442 const { skills } = readSourceFiles(testRootDir);
443
444 expect(skills).toHaveLength(1);
445 expect(skills[0].references).toHaveLength(1);
446 expect(skills[0].references[0].name).toBe('typography');
447 });
448
449 test('should handle missing skills directory', () => {
450 const { skills } = readSourceFiles(testRootDir);
451 expect(skills).toEqual([]);
452 });
453
454 test('should parse all frontmatter fields correctly', () => {
455 const skillContent = `---
456name: test-skill
457description: A comprehensive test skill
458license: Apache-2.0
459compatibility: claude-code
460user-invocable: true
461allowed-tools: Bash,Edit
462---
463
464Body content.`;
465
466 const skillDir = path.join(testRootDir, 'source/skills/test-skill');
467 ensureDir(skillDir);
468 fs.writeFileSync(path.join(skillDir, 'SKILL.md'), skillContent);
469
470 const { skills } = readSourceFiles(testRootDir);
471
472 expect(skills[0].name).toBe('test-skill');
473 expect(skills[0].description).toBe('A comprehensive test skill');
474 expect(skills[0].license).toBe('Apache-2.0');
475 expect(skills[0].compatibility).toBe('claude-code');
476 expect(skills[0].userInvocable).toBe(true);
477 expect(skills[0].allowedTools).toBe('Bash,Edit');
478 });
479});
480
481describe('readPatterns', () => {
482 const testRootDir = TEST_DIR;
483
484 beforeEach(() => {
485 ensureDir(testRootDir);
486 });
487
488 afterEach(() => {
489 if (fs.existsSync(testRootDir)) {
490 fs.rmSync(testRootDir, { recursive: true, force: true });
491 }
492 });
493
494 test('should extract DO and DON\'T patterns from SKILL.md', () => {
495 const skillContent = `---
496name: impeccable
497---
498
499### Typography
500**DO**: Use variable fonts for flexibility.
501**DON'T**: Use system fonts like Arial.
502
503### Color & Contrast
504**DO**: Ensure WCAG AA compliance.
505**DON'T**: Use gray text on colored backgrounds.
506
507### Layout & Space
508**DO**: Use consistent spacing scale.
509**DON'T**: Nest cards inside cards.`;
510
511 const skillDir = path.join(testRootDir, 'source/skills/impeccable');
512 ensureDir(skillDir);
513 fs.writeFileSync(path.join(skillDir, 'SKILL.md'), skillContent);
514
515 const { patterns, antipatterns } = readPatterns(testRootDir);
516
517 expect(patterns).toHaveLength(3);
518 expect(antipatterns).toHaveLength(3);
519
520 expect(patterns[0].name).toBe('Typography');
521 expect(patterns[0].items).toContain('Use variable fonts for flexibility.');
522 expect(antipatterns[0].items).toContain('Use system fonts like Arial.');
523 });
524
525 test('should normalize "Color & Theme" to "Color & Contrast"', () => {
526 const skillContent = `---
527name: impeccable
528---
529
530### Color & Theme
531**DO**: Use OKLCH color space.
532**DON'T**: Use pure black.`;
533
534 const skillDir = path.join(testRootDir, 'source/skills/impeccable');
535 ensureDir(skillDir);
536 fs.writeFileSync(path.join(skillDir, 'SKILL.md'), skillContent);
537
538 const { patterns, antipatterns } = readPatterns(testRootDir);
539
540 expect(patterns[0].name).toBe('Color & Contrast');
541 });
542
543 test('should handle missing SKILL.md file', () => {
544 ensureDir(path.join(testRootDir, 'source/skills/impeccable'));
545
546 const { patterns, antipatterns } = readPatterns(testRootDir);
547
548 expect(patterns).toEqual([]);
549 expect(antipatterns).toEqual([]);
550 });
551
552 test('should return patterns in consistent section order', () => {
553 const skillContent = `---
554name: impeccable
555---
556
557### Motion
558**DO**: Use ease-out for natural movement.
559
560### Typography
561**DO**: Use modular scale.
562
563### Color & Contrast
564**DO**: Use tinted neutrals.`;
565
566 const skillDir = path.join(testRootDir, 'source/skills/impeccable');
567 ensureDir(skillDir);
568 fs.writeFileSync(path.join(skillDir, 'SKILL.md'), skillContent);
569
570 const { patterns } = readPatterns(testRootDir);
571
572 // Patterns are returned in predefined section order, not source order
573 // Only sections with content are included
574 expect(patterns[0].name).toBe('Typography');
575 expect(patterns[1].name).toBe('Color & Contrast');
576 expect(patterns[2].name).toBe('Motion');
577 expect(patterns.length).toBe(3);
578 });
579});
580
581describe('replacePlaceholders', () => {
582 test('should replace {{model}} with provider-specific value', () => {
583 expect(replacePlaceholders('Ask {{model}} for help.', 'claude-code')).toBe('Ask Claude for help.');
584 expect(replacePlaceholders('Ask {{model}} for help.', 'gemini')).toBe('Ask Gemini for help.');
585 expect(replacePlaceholders('Ask {{model}} for help.', 'codex')).toBe('Ask GPT for help.');
586 expect(replacePlaceholders('Ask {{model}} for help.', 'cursor')).toBe('Ask the model for help.');
587 expect(replacePlaceholders('Ask {{model}} for help.', 'agents')).toBe('Ask the model for help.');
588 expect(replacePlaceholders('Ask {{model}} for help.', 'kiro')).toBe('Ask Claude for help.');
589 });
590
591 test('should replace {{config_file}} with provider-specific value', () => {
592 expect(replacePlaceholders('See {{config_file}}.', 'claude-code')).toBe('See CLAUDE.md.');
593 expect(replacePlaceholders('See {{config_file}}.', 'cursor')).toBe('See .cursorrules.');
594 expect(replacePlaceholders('See {{config_file}}.', 'gemini')).toBe('See GEMINI.md.');
595 expect(replacePlaceholders('See {{config_file}}.', 'codex')).toBe('See AGENTS.md.');
596 expect(replacePlaceholders('See {{config_file}}.', 'agents')).toBe('See .github/copilot-instructions.md.');
597 expect(replacePlaceholders('See {{config_file}}.', 'kiro')).toBe('See .kiro/settings.json.');
598 });
599
600 test('should replace {{ask_instruction}} with provider-specific value', () => {
601 const result = replacePlaceholders('{{ask_instruction}}', 'claude-code');
602 expect(result).toBe('STOP and call the AskUserQuestion tool to clarify.');
603
604 const cursorResult = replacePlaceholders('{{ask_instruction}}', 'cursor');
605 expect(cursorResult).toBe('ask the user directly to clarify what you cannot infer.');
606 });
607
608 test('should replace {{available_commands}} with command list', () => {
609 const result = replacePlaceholders('Commands: {{available_commands}}', 'claude-code', ['audit', 'polish', 'optimize']);
610 expect(result).toBe('Commands: /audit, /polish, /optimize');
611 });
612
613 test('should exclude impeccable from {{available_commands}}', () => {
614 const result = replacePlaceholders('Commands: {{available_commands}}', 'claude-code', ['audit', 'impeccable', 'polish']);
615 expect(result).toBe('Commands: /audit, /polish');
616 });
617
618 test('should exclude i-impeccable from {{available_commands}}', () => {
619 const result = replacePlaceholders('Commands: {{available_commands}}', 'claude-code', ['i-audit', 'i-impeccable', 'i-polish']);
620 expect(result).toBe('Commands: /i-audit, /i-polish');
621 });
622
623 test('should exclude legacy teach-impeccable from {{available_commands}}', () => {
624 const result = replacePlaceholders('Commands: {{available_commands}}', 'claude-code', ['audit', 'teach-impeccable', 'polish']);
625 expect(result).toBe('Commands: /audit, /polish');
626 });
627
628 test('should exclude legacy i-teach-impeccable from {{available_commands}}', () => {
629 const result = replacePlaceholders('Commands: {{available_commands}}', 'claude-code', ['i-audit', 'i-teach-impeccable', 'i-polish']);
630 expect(result).toBe('Commands: /i-audit, /i-polish');
631 });
632
633 test('should produce empty string for {{available_commands}} with no commands', () => {
634 const result = replacePlaceholders('Commands: {{available_commands}}.', 'claude-code', []);
635 expect(result).toBe('Commands: .');
636 });
637
638 test('should replace multiple placeholders in the same string', () => {
639 const result = replacePlaceholders('{{model}} uses {{config_file}} and {{ask_instruction}}', 'claude-code');
640 expect(result).toBe('Claude uses CLAUDE.md and STOP and call the AskUserQuestion tool to clarify.');
641 });
642
643 test('should replace multiple occurrences of the same placeholder', () => {
644 const result = replacePlaceholders('{{model}} and {{model}} again.', 'gemini');
645 expect(result).toBe('Gemini and Gemini again.');
646 });
647
648 test('should fall back to cursor placeholders for unknown provider', () => {
649 const result = replacePlaceholders('{{model}} {{config_file}}', 'unknown-provider');
650 expect(result).toBe('the model .cursorrules');
651 });
652});
653
654describe('prefixSkillReferences', () => {
655 test('should prefix /skillname command references', () => {
656 const result = prefixSkillReferences('Run /audit to check.', 'i-', ['audit', 'polish']);
657 expect(result).toBe('Run /i-audit to check.');
658 });
659
660 test('should prefix "the skillname skill" references', () => {
661 const result = prefixSkillReferences('Use the audit skill for checks.', 'i-', ['audit', 'polish']);
662 expect(result).toBe('Use the i-audit skill for checks.');
663 });
664
665 test('should prefix multiple different references', () => {
666 const result = prefixSkillReferences('Run /audit then /polish. The audit skill is great.', 'i-', ['audit', 'polish']);
667 expect(result).toContain('/i-audit');
668 expect(result).toContain('/i-polish');
669 expect(result).toContain('The i-audit skill');
670 });
671
672 test('should not partially match longer skill names', () => {
673 const result = prefixSkillReferences('Run /teach-impeccable command.', 'i-', ['teach', 'teach-impeccable']);
674 expect(result).toBe('Run /i-teach-impeccable command.');
675 });
676
677 test('should handle case-insensitive "the X skill" matching', () => {
678 const result = prefixSkillReferences('The audit skill is useful.', 'i-', ['audit']);
679 expect(result).toBe('The i-audit skill is useful.');
680 });
681
682 test('should return content unchanged with empty prefix', () => {
683 const result = prefixSkillReferences('Run /audit.', '', ['audit']);
684 expect(result).toBe('Run /audit.');
685 });
686
687 test('should return content unchanged with empty skill names', () => {
688 const result = prefixSkillReferences('Run /audit.', 'i-', []);
689 expect(result).toBe('Run /audit.');
690 });
691
692 test('should not match /skillname inside longer words', () => {
693 const result = prefixSkillReferences('The /auditing process.', 'i-', ['audit']);
694 // 'auditing' starts with 'audit' but has trailing letters — should NOT match
695 expect(result).toBe('The /auditing process.');
696 });
697
698 test('should match /skillname at end of string', () => {
699 const result = prefixSkillReferences('Run /audit', 'i-', ['audit']);
700 expect(result).toBe('Run /i-audit');
701 });
702
703 test('should match /skillname before punctuation', () => {
704 const result = prefixSkillReferences('Try /audit, /polish.', 'i-', ['audit', 'polish']);
705 expect(result).toContain('/i-audit,');
706 expect(result).toContain('/i-polish.');
707 });
708});