mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-12 14:16:28 +03:00
The critique report and the AskUserQuestion call shipped in one assistant message, so the report stayed hidden until the user answered the picker and the command read as if it had never run. Reorder critique's persistence steps so the temp-file cleanup runs after the report and trend line are sent. That cleanup now ends the message carrying the report, leaving the questions to open a fresh one. Both critique.md and overdrive.md state the constraint and why it exists, so the ordering is not an unexplained sequence a model can optimize away. Overdrive additionally moves its direction descriptions inside the question options, where the user is actually reading them. Also fix the ask_instruction splices. The placeholder is a complete sentence, but five call sites spliced it mid-sentence and shipped text like "stop and STOP and call the AskUserQuestion tool to clarify. before expanding it". Every call site is now sentence-initial and the twelve lowercase provider values are capitalized to match, with a comment in utils.js pinning the contract. Record a workflow-contract baseline for the current model lineup. The two failures seen while validating this change are pre-existing: bolder refinement fails on deepseek-v4-flash identically with bolder.md reverted to HEAD, and redesign replaces DESIGN is flaky on assertions driven by new-work.md, which this change does not touch. Prepared with AI assistance (Claude Code). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
697 lines
23 KiB
JavaScript
697 lines
23 KiB
JavaScript
import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
|
|
import fs from 'fs';
|
|
import path from 'path';
|
|
import {
|
|
parseFrontmatter,
|
|
readFilesRecursive,
|
|
readSourceFiles,
|
|
ensureDir,
|
|
cleanDir,
|
|
writeFile,
|
|
generateYamlFrontmatter,
|
|
readPatterns,
|
|
replacePlaceholders,
|
|
replaceScriptProviderMarker,
|
|
} from '../../scripts/lib/utils.js';
|
|
|
|
// Temporary test directory
|
|
const TEST_DIR = path.join(process.cwd(), 'test-tmp');
|
|
|
|
describe('parseFrontmatter', () => {
|
|
test('should parse basic frontmatter with simple key-value pairs', () => {
|
|
const content = `---
|
|
name: test-skill
|
|
description: A test skill
|
|
---
|
|
|
|
This is the body content.`;
|
|
|
|
const result = parseFrontmatter(content);
|
|
expect(result.frontmatter.name).toBe('test-skill');
|
|
expect(result.frontmatter.description).toBe('A test skill');
|
|
expect(result.body).toBe('This is the body content.');
|
|
});
|
|
|
|
test('should parse frontmatter with argument-hint', () => {
|
|
const content = `---
|
|
name: test-skill
|
|
description: A test skill
|
|
argument-hint: <output> [TARGET=<value>]
|
|
---
|
|
|
|
Body here.`;
|
|
|
|
const result = parseFrontmatter(content);
|
|
expect(result.frontmatter.name).toBe('test-skill');
|
|
expect(result.frontmatter['argument-hint']).toBe('<output> [TARGET=<value>]');
|
|
});
|
|
|
|
test('should return empty frontmatter when no frontmatter present', () => {
|
|
const content = 'Just some content without frontmatter.';
|
|
const result = parseFrontmatter(content);
|
|
|
|
expect(result.frontmatter).toEqual({});
|
|
expect(result.body).toBe(content);
|
|
});
|
|
|
|
test('should handle empty body', () => {
|
|
const content = `---
|
|
name: test
|
|
---
|
|
`;
|
|
const result = parseFrontmatter(content);
|
|
|
|
expect(result.frontmatter.name).toBe('test');
|
|
expect(result.body).toBe('');
|
|
});
|
|
|
|
test('should handle frontmatter with license field', () => {
|
|
const content = `---
|
|
name: skill-name
|
|
description: A skill
|
|
license: MIT
|
|
---
|
|
|
|
Skill body.`;
|
|
|
|
const result = parseFrontmatter(content);
|
|
expect(result.frontmatter.license).toBe('MIT');
|
|
});
|
|
|
|
test('should parse user-invocable boolean', () => {
|
|
const content = `---
|
|
name: test-skill
|
|
user-invocable: true
|
|
---
|
|
|
|
Body.`;
|
|
|
|
const result = parseFrontmatter(content);
|
|
expect(result.frontmatter['user-invocable']).toBe(true);
|
|
});
|
|
|
|
test('should parse quoted user-invocable boolean as true', () => {
|
|
const content = `---
|
|
name: test-skill
|
|
user-invocable: 'true'
|
|
---
|
|
|
|
Body.`;
|
|
|
|
const result = parseFrontmatter(content);
|
|
expect(result.frontmatter['user-invocable']).toBe(true);
|
|
});
|
|
|
|
test('should keep quoted non-user-invocable booleans as plain strings', () => {
|
|
const content = `---
|
|
name: test-skill
|
|
description: 'true'
|
|
---
|
|
|
|
Body.`;
|
|
|
|
const result = parseFrontmatter(content);
|
|
expect(result.frontmatter.description).toBe('true');
|
|
});
|
|
|
|
test('should parse allowed-tools field', () => {
|
|
const content = `---
|
|
name: test-skill
|
|
allowed-tools: Bash
|
|
---
|
|
|
|
Body.`;
|
|
|
|
const result = parseFrontmatter(content);
|
|
expect(result.frontmatter['allowed-tools']).toBe('Bash');
|
|
});
|
|
});
|
|
|
|
describe('generateYamlFrontmatter', () => {
|
|
test('should generate basic frontmatter', () => {
|
|
const data = {
|
|
name: 'test-skill',
|
|
description: 'A test'
|
|
};
|
|
|
|
const result = generateYamlFrontmatter(data);
|
|
expect(result).toContain('---');
|
|
expect(result).toContain('name: test-skill');
|
|
expect(result).toContain('description: A test');
|
|
});
|
|
|
|
test('should generate frontmatter with argument-hint', () => {
|
|
const data = {
|
|
name: 'test',
|
|
description: 'Test skill',
|
|
'argument-hint': '<output> [TARGET=<value>]'
|
|
};
|
|
|
|
const result = generateYamlFrontmatter(data);
|
|
expect(result).toContain('argument-hint: <output> [TARGET=<value>]');
|
|
});
|
|
|
|
test('should generate frontmatter with boolean', () => {
|
|
const data = {
|
|
name: 'test',
|
|
description: 'Test',
|
|
'user-invocable': true
|
|
};
|
|
|
|
const result = generateYamlFrontmatter(data);
|
|
expect(result).toContain('user-invocable: true');
|
|
});
|
|
|
|
test('should roundtrip: generate and parse back', () => {
|
|
const original = {
|
|
name: 'roundtrip-test',
|
|
description: 'Testing roundtrip',
|
|
'argument-hint': '<arg1>'
|
|
};
|
|
|
|
const yaml = generateYamlFrontmatter(original);
|
|
const content = `${yaml}\n\nBody content`;
|
|
const parsed = parseFrontmatter(content);
|
|
|
|
expect(parsed.frontmatter.name).toBe(original.name);
|
|
expect(parsed.frontmatter.description).toBe(original.description);
|
|
expect(parsed.frontmatter['argument-hint']).toBe('<arg1>');
|
|
});
|
|
|
|
test('should quote strings containing colon-space (breaks plain scalars)', () => {
|
|
const data = {
|
|
name: 'impeccable',
|
|
description: 'Design fluency. Also handles: critique, audit. Commands: craft, polish.'
|
|
};
|
|
|
|
const result = generateYamlFrontmatter(data);
|
|
// Must be wrapped in quotes so YAML parsers don't mis-read the inner `: ` as a mapping
|
|
expect(result).toContain('description: "Design fluency. Also handles: critique, audit. Commands: craft, polish."');
|
|
|
|
// Roundtrip through our parser should recover the original string intact
|
|
const parsed = parseFrontmatter(`${result}\n\nbody`);
|
|
expect(parsed.frontmatter.description).toBe(data.description);
|
|
});
|
|
|
|
test('should quote strings starting with YAML flow indicators', () => {
|
|
const data = {
|
|
name: 'test',
|
|
'argument-hint': '[command] [target]'
|
|
};
|
|
|
|
const result = generateYamlFrontmatter(data);
|
|
expect(result).toContain('argument-hint: "[command] [target]"');
|
|
});
|
|
|
|
test('should not quote plain strings without special chars', () => {
|
|
const data = {
|
|
name: 'simple',
|
|
description: 'A plain description with no colons or hashes'
|
|
};
|
|
|
|
const result = generateYamlFrontmatter(data);
|
|
expect(result).toContain('description: A plain description with no colons or hashes');
|
|
expect(result).not.toContain('"A plain');
|
|
});
|
|
});
|
|
|
|
describe('ensureDir', () => {
|
|
afterEach(() => {
|
|
if (fs.existsSync(TEST_DIR)) {
|
|
fs.rmSync(TEST_DIR, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test('should create directory if it does not exist', () => {
|
|
const testPath = path.join(TEST_DIR, 'new-dir');
|
|
ensureDir(testPath);
|
|
|
|
expect(fs.existsSync(testPath)).toBe(true);
|
|
expect(fs.statSync(testPath).isDirectory()).toBe(true);
|
|
});
|
|
|
|
test('should create nested directories', () => {
|
|
const testPath = path.join(TEST_DIR, 'level1', 'level2', 'level3');
|
|
ensureDir(testPath);
|
|
|
|
expect(fs.existsSync(testPath)).toBe(true);
|
|
});
|
|
|
|
test('should not throw if directory already exists', () => {
|
|
const testPath = path.join(TEST_DIR, 'existing');
|
|
fs.mkdirSync(testPath, { recursive: true });
|
|
|
|
expect(() => ensureDir(testPath)).not.toThrow();
|
|
});
|
|
});
|
|
|
|
describe('cleanDir', () => {
|
|
beforeEach(() => {
|
|
ensureDir(TEST_DIR);
|
|
});
|
|
|
|
afterEach(() => {
|
|
if (fs.existsSync(TEST_DIR)) {
|
|
fs.rmSync(TEST_DIR, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test('should remove directory and all contents', () => {
|
|
const filePath = path.join(TEST_DIR, 'test.txt');
|
|
fs.writeFileSync(filePath, 'content');
|
|
|
|
expect(fs.existsSync(filePath)).toBe(true);
|
|
|
|
cleanDir(TEST_DIR);
|
|
expect(fs.existsSync(TEST_DIR)).toBe(false);
|
|
});
|
|
|
|
test('should not throw if directory does not exist', () => {
|
|
const nonExistent = path.join(TEST_DIR, 'does-not-exist');
|
|
expect(() => cleanDir(nonExistent)).not.toThrow();
|
|
});
|
|
|
|
test('should remove nested directories', () => {
|
|
const nestedPath = path.join(TEST_DIR, 'level1', 'level2');
|
|
ensureDir(nestedPath);
|
|
fs.writeFileSync(path.join(nestedPath, 'file.txt'), 'content');
|
|
|
|
cleanDir(TEST_DIR);
|
|
expect(fs.existsSync(TEST_DIR)).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('writeFile', () => {
|
|
afterEach(() => {
|
|
if (fs.existsSync(TEST_DIR)) {
|
|
fs.rmSync(TEST_DIR, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test('should write file with content', () => {
|
|
const filePath = path.join(TEST_DIR, 'test.txt');
|
|
const content = 'Hello, world!';
|
|
|
|
writeFile(filePath, content);
|
|
|
|
expect(fs.existsSync(filePath)).toBe(true);
|
|
expect(fs.readFileSync(filePath, 'utf-8')).toBe(content);
|
|
});
|
|
|
|
test('should create parent directories automatically', () => {
|
|
const filePath = path.join(TEST_DIR, 'nested', 'deep', 'file.txt');
|
|
writeFile(filePath, 'content');
|
|
|
|
expect(fs.existsSync(filePath)).toBe(true);
|
|
expect(fs.readFileSync(filePath, 'utf-8')).toBe('content');
|
|
});
|
|
|
|
test('should overwrite existing file', () => {
|
|
const filePath = path.join(TEST_DIR, 'file.txt');
|
|
writeFile(filePath, 'first');
|
|
writeFile(filePath, 'second');
|
|
|
|
expect(fs.readFileSync(filePath, 'utf-8')).toBe('second');
|
|
});
|
|
});
|
|
|
|
describe('readFilesRecursive', () => {
|
|
beforeEach(() => {
|
|
ensureDir(TEST_DIR);
|
|
});
|
|
|
|
afterEach(() => {
|
|
if (fs.existsSync(TEST_DIR)) {
|
|
fs.rmSync(TEST_DIR, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test('should find all markdown files in directory', () => {
|
|
writeFile(path.join(TEST_DIR, 'file1.md'), 'content1');
|
|
writeFile(path.join(TEST_DIR, 'file2.md'), 'content2');
|
|
writeFile(path.join(TEST_DIR, 'file3.txt'), 'not markdown');
|
|
|
|
const files = readFilesRecursive(TEST_DIR);
|
|
expect(files).toHaveLength(2);
|
|
expect(files.some(f => f.endsWith('file1.md'))).toBe(true);
|
|
expect(files.some(f => f.endsWith('file2.md'))).toBe(true);
|
|
});
|
|
|
|
test('should find markdown files in nested directories', () => {
|
|
writeFile(path.join(TEST_DIR, 'root.md'), 'root');
|
|
writeFile(path.join(TEST_DIR, 'sub', 'nested.md'), 'nested');
|
|
writeFile(path.join(TEST_DIR, 'sub', 'deep', 'deeper.md'), 'deeper');
|
|
|
|
const files = readFilesRecursive(TEST_DIR);
|
|
expect(files).toHaveLength(3);
|
|
expect(files.some(f => f.endsWith('root.md'))).toBe(true);
|
|
expect(files.some(f => f.endsWith('nested.md'))).toBe(true);
|
|
expect(files.some(f => f.endsWith('deeper.md'))).toBe(true);
|
|
});
|
|
|
|
test('should return empty array for non-existent directory', () => {
|
|
const files = readFilesRecursive(path.join(TEST_DIR, 'does-not-exist'));
|
|
expect(files).toEqual([]);
|
|
});
|
|
|
|
test('should return empty array for directory with no markdown files', () => {
|
|
writeFile(path.join(TEST_DIR, 'file.txt'), 'text');
|
|
writeFile(path.join(TEST_DIR, 'file.js'), 'code');
|
|
|
|
const files = readFilesRecursive(TEST_DIR);
|
|
expect(files).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe('readSourceFiles', () => {
|
|
const testRootDir = TEST_DIR;
|
|
|
|
beforeEach(() => {
|
|
ensureDir(testRootDir);
|
|
});
|
|
|
|
afterEach(() => {
|
|
if (fs.existsSync(testRootDir)) {
|
|
fs.rmSync(testRootDir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test('should read and parse SKILL.md from skill/', () => {
|
|
const skillContent = `---
|
|
name: test-skill
|
|
description: A test skill
|
|
license: MIT
|
|
---
|
|
|
|
Skill instructions here.`;
|
|
|
|
const skillDir = path.join(testRootDir, 'skill');
|
|
ensureDir(skillDir);
|
|
fs.writeFileSync(path.join(skillDir, 'SKILL.src.md'), skillContent);
|
|
|
|
const { skills } = readSourceFiles(testRootDir);
|
|
|
|
expect(skills).toHaveLength(1);
|
|
expect(skills[0].name).toBe('test-skill');
|
|
expect(skills[0].description).toBe('A test skill');
|
|
expect(skills[0].license).toBe('MIT');
|
|
expect(skills[0].body).toBe('Skill instructions here.');
|
|
});
|
|
|
|
test('should read skill with user-invocable flag', () => {
|
|
const skillContent = `---
|
|
name: audit
|
|
description: Run technical quality checks
|
|
user-invocable: true
|
|
---
|
|
|
|
Audit the code.`;
|
|
|
|
const skillDir = path.join(testRootDir, 'skill');
|
|
ensureDir(skillDir);
|
|
fs.writeFileSync(path.join(skillDir, 'SKILL.src.md'), skillContent);
|
|
|
|
const { skills } = readSourceFiles(testRootDir);
|
|
|
|
expect(skills).toHaveLength(1);
|
|
expect(skills[0].userInvocable).toBe(true);
|
|
});
|
|
|
|
test('should read skill with quoted user-invocable flag', () => {
|
|
const skillContent = `---
|
|
name: audit
|
|
description: Run technical quality checks
|
|
user-invocable: 'true'
|
|
---
|
|
|
|
Audit the code.`;
|
|
|
|
const skillDir = path.join(testRootDir, 'skill');
|
|
ensureDir(skillDir);
|
|
fs.writeFileSync(path.join(skillDir, 'SKILL.src.md'), skillContent);
|
|
|
|
const { skills } = readSourceFiles(testRootDir);
|
|
|
|
expect(skills).toHaveLength(1);
|
|
expect(skills[0].userInvocable).toBe(true);
|
|
});
|
|
|
|
test('should read skill with reference files', () => {
|
|
const skillContent = `---
|
|
name: impeccable
|
|
description: Impeccable design skill
|
|
---
|
|
|
|
Impeccable design instructions.`;
|
|
|
|
const skillDir = path.join(testRootDir, 'skill');
|
|
ensureDir(skillDir);
|
|
fs.writeFileSync(path.join(skillDir, 'SKILL.src.md'), skillContent);
|
|
|
|
const refDir = path.join(skillDir, 'reference');
|
|
ensureDir(refDir);
|
|
fs.writeFileSync(path.join(refDir, 'typography.md'), 'Typography reference content.');
|
|
fs.writeFileSync(path.join(refDir, 'color.md'), 'Color reference content.');
|
|
|
|
const { skills } = readSourceFiles(testRootDir);
|
|
|
|
expect(skills).toHaveLength(1);
|
|
expect(skills[0].references).toHaveLength(2);
|
|
// References may not be in a specific order due to fs.readdirSync
|
|
const refNames = skills[0].references.map(r => r.name).sort();
|
|
expect(refNames).toEqual(['color', 'typography']);
|
|
});
|
|
|
|
test('should fall back to "impeccable" when frontmatter has no name', () => {
|
|
const skillDir = path.join(testRootDir, 'skill');
|
|
ensureDir(skillDir);
|
|
fs.writeFileSync(path.join(skillDir, 'SKILL.src.md'), 'Just body, no frontmatter.');
|
|
|
|
const { skills } = readSourceFiles(testRootDir);
|
|
|
|
expect(skills).toHaveLength(1);
|
|
expect(skills[0].name).toBe('impeccable');
|
|
});
|
|
|
|
test('should ignore non-md files in skill/reference', () => {
|
|
const skillDir = path.join(testRootDir, 'skill');
|
|
ensureDir(skillDir);
|
|
fs.writeFileSync(path.join(skillDir, 'SKILL.src.md'), '---\nname: test-skill\n---\nBody');
|
|
|
|
const refDir = path.join(skillDir, 'reference');
|
|
ensureDir(refDir);
|
|
fs.writeFileSync(path.join(refDir, 'readme.txt'), 'Not a markdown file');
|
|
fs.writeFileSync(path.join(refDir, 'typography.md'), 'Valid reference');
|
|
|
|
const { skills } = readSourceFiles(testRootDir);
|
|
|
|
expect(skills).toHaveLength(1);
|
|
expect(skills[0].references).toHaveLength(1);
|
|
expect(skills[0].references[0].name).toBe('typography');
|
|
});
|
|
|
|
test('should read nested skill script files with portable relative names', () => {
|
|
const skillDir = path.join(testRootDir, 'skill');
|
|
ensureDir(skillDir);
|
|
fs.writeFileSync(path.join(skillDir, 'SKILL.src.md'), '---\nname: test-skill\n---\nBody');
|
|
|
|
const scriptsDir = path.join(skillDir, 'scripts');
|
|
ensureDir(path.join(scriptsDir, 'live'));
|
|
fs.writeFileSync(path.join(scriptsDir, 'context.mjs'), 'export const context = true;\n');
|
|
fs.writeFileSync(path.join(scriptsDir, 'live/session-store.mjs'), 'export const nested = true;\n');
|
|
fs.writeFileSync(path.join(scriptsDir, 'config.json'), '{"local":true}\n');
|
|
|
|
const { skills } = readSourceFiles(testRootDir);
|
|
const scripts = skills[0].scripts;
|
|
const scriptNames = scripts.map(script => script.name).sort();
|
|
|
|
expect(scriptNames).toEqual(['context.mjs', 'live/session-store.mjs']);
|
|
expect(scripts.find(script => script.name === 'live/session-store.mjs').content).toContain('nested = true');
|
|
});
|
|
|
|
test('should handle missing skill directory', () => {
|
|
const { skills } = readSourceFiles(testRootDir);
|
|
expect(skills).toEqual([]);
|
|
});
|
|
|
|
test('should parse all frontmatter fields correctly', () => {
|
|
const skillContent = `---
|
|
name: test-skill
|
|
description: A comprehensive test skill
|
|
license: Apache-2.0
|
|
compatibility: claude-code
|
|
user-invocable: true
|
|
allowed-tools: Bash,Edit
|
|
---
|
|
|
|
Body content.`;
|
|
|
|
const skillDir = path.join(testRootDir, 'skill');
|
|
ensureDir(skillDir);
|
|
fs.writeFileSync(path.join(skillDir, 'SKILL.src.md'), skillContent);
|
|
|
|
const { skills } = readSourceFiles(testRootDir);
|
|
|
|
expect(skills[0].name).toBe('test-skill');
|
|
expect(skills[0].description).toBe('A comprehensive test skill');
|
|
expect(skills[0].license).toBe('Apache-2.0');
|
|
expect(skills[0].compatibility).toBe('claude-code');
|
|
expect(skills[0].userInvocable).toBe(true);
|
|
expect(skills[0].allowedTools).toBe('Bash,Edit');
|
|
});
|
|
});
|
|
|
|
describe('readPatterns', () => {
|
|
const testRootDir = TEST_DIR;
|
|
|
|
beforeEach(() => {
|
|
ensureDir(testRootDir);
|
|
});
|
|
|
|
afterEach(() => {
|
|
if (fs.existsSync(testRootDir)) {
|
|
fs.rmSync(testRootDir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test('returns the curated DO/DON\'T pattern categories', () => {
|
|
// v3.0: readPatterns no longer parses SKILL.md. It returns a hand-curated
|
|
// catalog (CURATED_CATEGORIES) that powers the homepage Antidote teaser.
|
|
const { patterns, antipatterns } = readPatterns(testRootDir);
|
|
|
|
expect(patterns).toHaveLength(6);
|
|
expect(antipatterns).toHaveLength(6);
|
|
|
|
expect(patterns[0].name).toBe('Typography');
|
|
expect(patterns[0].items.length).toBeGreaterThan(0);
|
|
expect(antipatterns[0].name).toBe('Typography');
|
|
expect(antipatterns[0].items.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
test('returns categories in the curated order', () => {
|
|
const { patterns } = readPatterns(testRootDir);
|
|
expect(patterns.map((p) => p.name)).toEqual([
|
|
'Typography',
|
|
'Color & Contrast',
|
|
'Layout & Space',
|
|
'Visual Details',
|
|
'Motion',
|
|
'Interaction',
|
|
]);
|
|
});
|
|
|
|
test('ignores its arguments (curated, not extracted from any SKILL.md)', () => {
|
|
const fromBogusRoot = readPatterns('/nonexistent/path/xyz');
|
|
const fromRealRoot = readPatterns(testRootDir);
|
|
expect(fromBogusRoot.patterns.map((p) => p.name)).toEqual(
|
|
fromRealRoot.patterns.map((p) => p.name)
|
|
);
|
|
expect(fromBogusRoot.patterns).toHaveLength(6);
|
|
});
|
|
});
|
|
|
|
describe('replacePlaceholders', () => {
|
|
test('should replace {{model}} with provider-specific value', () => {
|
|
expect(replacePlaceholders('Ask {{model}} for help.', 'claude-code')).toBe('Ask Claude for help.');
|
|
expect(replacePlaceholders('Ask {{model}} for help.', 'gemini')).toBe('Ask Gemini for help.');
|
|
expect(replacePlaceholders('Ask {{model}} for help.', 'codex')).toBe('Ask GPT for help.');
|
|
expect(replacePlaceholders('Ask {{model}} for help.', 'cursor')).toBe('Ask the model for help.');
|
|
expect(replacePlaceholders('Ask {{model}} for help.', 'agents')).toBe('Ask the model for help.');
|
|
expect(replacePlaceholders('Ask {{model}} for help.', 'kiro')).toBe('Ask Claude for help.');
|
|
});
|
|
|
|
test('should replace {{config_file}} with provider-specific value', () => {
|
|
expect(replacePlaceholders('See {{config_file}}.', 'claude-code')).toBe('See CLAUDE.md.');
|
|
expect(replacePlaceholders('See {{config_file}}.', 'cursor')).toBe('See .cursorrules.');
|
|
expect(replacePlaceholders('See {{config_file}}.', 'gemini')).toBe('See GEMINI.md.');
|
|
expect(replacePlaceholders('See {{config_file}}.', 'codex')).toBe('See AGENTS.md.');
|
|
expect(replacePlaceholders('See {{config_file}}.', 'agents')).toBe('See .github/copilot-instructions.md.');
|
|
expect(replacePlaceholders('See {{config_file}}.', 'kiro')).toBe('See .kiro/settings.json.');
|
|
});
|
|
|
|
test('should replace {{ask_instruction}} with provider-specific value', () => {
|
|
const result = replacePlaceholders('{{ask_instruction}}', 'claude-code');
|
|
expect(result).toBe('STOP and call the AskUserQuestion tool to clarify.');
|
|
|
|
const cursorResult = replacePlaceholders('{{ask_instruction}}', 'cursor');
|
|
expect(cursorResult).toBe('Ask the user directly to clarify what you cannot infer.');
|
|
});
|
|
|
|
test('should replace {{available_commands}} with command list', () => {
|
|
const result = replacePlaceholders('Commands: {{available_commands}}', 'claude-code', ['audit', 'polish', 'optimize']);
|
|
expect(result).toBe('Commands: /audit, /polish, /optimize');
|
|
});
|
|
|
|
test('should exclude impeccable from {{available_commands}}', () => {
|
|
const result = replacePlaceholders('Commands: {{available_commands}}', 'claude-code', ['audit', 'impeccable', 'polish']);
|
|
expect(result).toBe('Commands: /audit, /polish');
|
|
});
|
|
|
|
test('should exclude legacy teach-impeccable from {{available_commands}}', () => {
|
|
const result = replacePlaceholders('Commands: {{available_commands}}', 'claude-code', ['audit', 'teach-impeccable', 'polish']);
|
|
expect(result).toBe('Commands: /audit, /polish');
|
|
});
|
|
|
|
test('lists /impeccable sub-commands for {{available_commands}} when no command names are passed', () => {
|
|
// v3.0 single-skill architecture: with no command names, the list falls back
|
|
// to the IMPECCABLE_SUB_COMMANDS sub-commands rendered as `/impeccable <sub>`.
|
|
const result = replacePlaceholders('Commands: {{available_commands}}', 'claude-code', []);
|
|
expect(result.startsWith('Commands: /impeccable ')).toBe(true);
|
|
expect(result).toContain('/impeccable audit');
|
|
expect(result).toContain('/impeccable polish');
|
|
});
|
|
|
|
test('should replace multiple placeholders in the same string', () => {
|
|
const result = replacePlaceholders('{{model}} uses {{config_file}} and {{ask_instruction}}', 'claude-code');
|
|
expect(result).toBe('Claude uses CLAUDE.md and STOP and call the AskUserQuestion tool to clarify.');
|
|
});
|
|
|
|
test('should replace multiple occurrences of the same placeholder', () => {
|
|
const result = replacePlaceholders('{{model}} and {{model}} again.', 'gemini');
|
|
expect(result).toBe('Gemini and Gemini again.');
|
|
});
|
|
|
|
test('should fall back to cursor placeholders for unknown provider', () => {
|
|
const result = replacePlaceholders('{{model}} {{config_file}}', 'unknown-provider');
|
|
expect(result).toBe('the model .cursorrules');
|
|
});
|
|
|
|
test('should replace Codex command invocations without rewriting paths', () => {
|
|
const source = [
|
|
'Run /impeccable audit.',
|
|
'Use `/impeccable polish` next.',
|
|
'.github/hooks/impeccable.json',
|
|
'.codex/skills/impeccable/scripts/context.mjs',
|
|
'https://example.com/impeccable',
|
|
].join('\n');
|
|
|
|
const result = replacePlaceholders(source, 'codex', [], ['impeccable']);
|
|
|
|
expect(result).toContain('Run $impeccable audit.');
|
|
expect(result).toContain('Use `$impeccable polish` next.');
|
|
expect(result).toContain('.github/hooks/impeccable.json');
|
|
expect(result).toContain('.codex/skills/impeccable/scripts/context.mjs');
|
|
expect(result).toContain('https://example.com/impeccable');
|
|
});
|
|
});
|
|
|
|
describe('replaceScriptProviderMarker', () => {
|
|
test('renders only the explicit provider declarations', () => {
|
|
const source = [
|
|
"export const IMPECCABLE_COMMAND_PREFIX = '/'; // @impeccable-provider-command-prefix",
|
|
"export const IMPECCABLE_PROVIDER_ID = 'source'; // @impeccable-provider-id",
|
|
'const regex = /impeccable\\b/gi;',
|
|
"const runtime = '/src/lib/impeccable/__runtime.js';",
|
|
"const text = 'Run /impeccable audit';",
|
|
].join('\n');
|
|
|
|
const result = replaceScriptProviderMarker(source, 'codex', 'agents');
|
|
|
|
expect(result).toContain('export const IMPECCABLE_COMMAND_PREFIX = "$";');
|
|
expect(result).toContain('export const IMPECCABLE_PROVIDER_ID = "agents";');
|
|
expect(result).toContain('const regex = /impeccable\\b/gi;');
|
|
expect(result).toContain("const runtime = '/src/lib/impeccable/__runtime.js';");
|
|
expect(result).toContain("const text = 'Run /impeccable audit';");
|
|
});
|
|
});
|