utils.test.js

  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});