Files
everything-claude-code/tests/ci/validators.test.js
Affaan Mustafa e5f1c58c11 test: add regression tests for empty frontmatter field rejection
Add 2 tests verifying validate-agents correctly rejects agents with
empty model and empty tools values in YAML frontmatter.
2026-02-12 16:58:27 -08:00

620 lines
26 KiB
JavaScript

/**
* Tests for CI validator scripts
*
* Tests both success paths (against the real project) and error paths
* (against temporary fixture directories via wrapper scripts).
*
* Run with: node tests/ci/validators.test.js
*/
const assert = require('assert');
const path = require('path');
const fs = require('fs');
const os = require('os');
const { execFileSync } = require('child_process');
const validatorsDir = path.join(__dirname, '..', '..', 'scripts', 'ci');
// Test helpers
function test(name, fn) {
try {
fn();
console.log(` \u2713 ${name}`);
return true;
} catch (err) {
console.log(` \u2717 ${name}`);
console.log(` Error: ${err.message}`);
return false;
}
}
function createTestDir() {
return fs.mkdtempSync(path.join(os.tmpdir(), 'ci-validator-test-'));
}
function cleanupTestDir(testDir) {
fs.rmSync(testDir, { recursive: true, force: true });
}
/**
* Run a validator script via a wrapper that overrides its directory constant.
* This allows testing error cases without modifying real project files.
*
* @param {string} validatorName - e.g., 'validate-agents'
* @param {string} dirConstant - the constant name to override (e.g., 'AGENTS_DIR')
* @param {string} overridePath - the temp directory to use
* @returns {{code: number, stdout: string, stderr: string}}
*/
function runValidatorWithDir(validatorName, dirConstant, overridePath) {
const validatorPath = path.join(validatorsDir, `${validatorName}.js`);
// Read the validator source, replace the directory constant, and run as a wrapper
let source = fs.readFileSync(validatorPath, 'utf8');
// Remove the shebang line
source = source.replace(/^#!.*\n/, '');
// Replace the directory constant with our override path
const dirRegex = new RegExp(`const ${dirConstant} = .*?;`);
source = source.replace(dirRegex, `const ${dirConstant} = ${JSON.stringify(overridePath)};`);
try {
const stdout = execFileSync('node', ['-e', source], {
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'],
timeout: 10000,
});
return { code: 0, stdout, stderr: '' };
} catch (err) {
return {
code: err.status || 1,
stdout: err.stdout || '',
stderr: err.stderr || '',
};
}
}
/**
* Run a validator script with multiple directory overrides.
* @param {string} validatorName
* @param {Record<string, string>} overrides - map of constant name to path
*/
function runValidatorWithDirs(validatorName, overrides) {
const validatorPath = path.join(validatorsDir, `${validatorName}.js`);
let source = fs.readFileSync(validatorPath, 'utf8');
source = source.replace(/^#!.*\n/, '');
for (const [constant, overridePath] of Object.entries(overrides)) {
const dirRegex = new RegExp(`const ${constant} = .*?;`);
source = source.replace(dirRegex, `const ${constant} = ${JSON.stringify(overridePath)};`);
}
try {
const stdout = execFileSync('node', ['-e', source], {
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'],
timeout: 10000,
});
return { code: 0, stdout, stderr: '' };
} catch (err) {
return {
code: err.status || 1,
stdout: err.stdout || '',
stderr: err.stderr || '',
};
}
}
/**
* Run a validator script directly (tests real project)
*/
function runValidator(validatorName) {
const validatorPath = path.join(validatorsDir, `${validatorName}.js`);
try {
const stdout = execFileSync('node', [validatorPath], {
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'],
timeout: 15000,
});
return { code: 0, stdout, stderr: '' };
} catch (err) {
return {
code: err.status || 1,
stdout: err.stdout || '',
stderr: err.stderr || '',
};
}
}
function runTests() {
console.log('\n=== Testing CI Validators ===\n');
let passed = 0;
let failed = 0;
// ==========================================
// validate-agents.js
// ==========================================
console.log('validate-agents.js:');
if (test('passes on real project agents', () => {
const result = runValidator('validate-agents');
assert.strictEqual(result.code, 0, `Should pass, got stderr: ${result.stderr}`);
assert.ok(result.stdout.includes('Validated'), 'Should output validation count');
})) passed++; else failed++;
if (test('fails on agent without frontmatter', () => {
const testDir = createTestDir();
fs.writeFileSync(path.join(testDir, 'bad-agent.md'), '# No frontmatter here\nJust content.');
const result = runValidatorWithDir('validate-agents', 'AGENTS_DIR', testDir);
assert.strictEqual(result.code, 1, 'Should exit 1 for missing frontmatter');
assert.ok(result.stderr.includes('Missing frontmatter'), 'Should report missing frontmatter');
cleanupTestDir(testDir);
})) passed++; else failed++;
if (test('fails on agent missing required model field', () => {
const testDir = createTestDir();
fs.writeFileSync(path.join(testDir, 'no-model.md'), '---\ntools: Read, Write\n---\n# Agent');
const result = runValidatorWithDir('validate-agents', 'AGENTS_DIR', testDir);
assert.strictEqual(result.code, 1, 'Should exit 1 for missing model');
assert.ok(result.stderr.includes('model'), 'Should report missing model field');
cleanupTestDir(testDir);
})) passed++; else failed++;
if (test('fails on agent missing required tools field', () => {
const testDir = createTestDir();
fs.writeFileSync(path.join(testDir, 'no-tools.md'), '---\nmodel: sonnet\n---\n# Agent');
const result = runValidatorWithDir('validate-agents', 'AGENTS_DIR', testDir);
assert.strictEqual(result.code, 1, 'Should exit 1 for missing tools');
assert.ok(result.stderr.includes('tools'), 'Should report missing tools field');
cleanupTestDir(testDir);
})) passed++; else failed++;
if (test('passes on valid agent with all required fields', () => {
const testDir = createTestDir();
fs.writeFileSync(path.join(testDir, 'good-agent.md'), '---\nmodel: sonnet\ntools: Read, Write\n---\n# Agent');
const result = runValidatorWithDir('validate-agents', 'AGENTS_DIR', testDir);
assert.strictEqual(result.code, 0, 'Should pass for valid agent');
assert.ok(result.stdout.includes('Validated 1'), 'Should report 1 validated');
cleanupTestDir(testDir);
})) passed++; else failed++;
if (test('handles frontmatter with BOM and CRLF', () => {
const testDir = createTestDir();
const content = '\uFEFF---\r\nmodel: sonnet\r\ntools: Read, Write\r\n---\r\n# Agent';
fs.writeFileSync(path.join(testDir, 'bom-agent.md'), content);
const result = runValidatorWithDir('validate-agents', 'AGENTS_DIR', testDir);
assert.strictEqual(result.code, 0, 'Should handle BOM and CRLF');
cleanupTestDir(testDir);
})) passed++; else failed++;
if (test('handles frontmatter with colons in values', () => {
const testDir = createTestDir();
fs.writeFileSync(path.join(testDir, 'colon-agent.md'), '---\nmodel: claude-sonnet-4-5-20250929\ntools: Read, Write, Bash\n---\n# Agent');
const result = runValidatorWithDir('validate-agents', 'AGENTS_DIR', testDir);
assert.strictEqual(result.code, 0, 'Should handle colons in values');
cleanupTestDir(testDir);
})) passed++; else failed++;
if (test('skips non-md files', () => {
const testDir = createTestDir();
fs.writeFileSync(path.join(testDir, 'readme.txt'), 'Not an agent');
fs.writeFileSync(path.join(testDir, 'valid.md'), '---\nmodel: sonnet\ntools: Read\n---\n# Agent');
const result = runValidatorWithDir('validate-agents', 'AGENTS_DIR', testDir);
assert.strictEqual(result.code, 0, 'Should only validate .md files');
assert.ok(result.stdout.includes('Validated 1'), 'Should count only .md files');
cleanupTestDir(testDir);
})) passed++; else failed++;
if (test('exits 0 when directory does not exist', () => {
const result = runValidatorWithDir('validate-agents', 'AGENTS_DIR', '/nonexistent/dir');
assert.strictEqual(result.code, 0, 'Should skip when no agents dir');
assert.ok(result.stdout.includes('skipping'), 'Should say skipping');
})) passed++; else failed++;
if (test('rejects agent with empty model value', () => {
const testDir = createTestDir();
fs.writeFileSync(path.join(testDir, 'empty.md'), '---\nmodel:\ntools: Read, Write\n---\n# Empty model');
const result = runValidatorWithDir('validate-agents', 'AGENTS_DIR', testDir);
assert.strictEqual(result.code, 1, 'Should reject empty model');
assert.ok(result.stderr.includes('model'), 'Should mention model field');
cleanupTestDir(testDir);
})) passed++; else failed++;
if (test('rejects agent with empty tools value', () => {
const testDir = createTestDir();
fs.writeFileSync(path.join(testDir, 'empty.md'), '---\nmodel: claude-sonnet-4-5-20250929\ntools:\n---\n# Empty tools');
const result = runValidatorWithDir('validate-agents', 'AGENTS_DIR', testDir);
assert.strictEqual(result.code, 1, 'Should reject empty tools');
assert.ok(result.stderr.includes('tools'), 'Should mention tools field');
cleanupTestDir(testDir);
})) passed++; else failed++;
// ==========================================
// validate-hooks.js
// ==========================================
console.log('\nvalidate-hooks.js:');
if (test('passes on real project hooks.json', () => {
const result = runValidator('validate-hooks');
assert.strictEqual(result.code, 0, `Should pass, got stderr: ${result.stderr}`);
assert.ok(result.stdout.includes('Validated'), 'Should output validation count');
})) passed++; else failed++;
if (test('exits 0 when hooks.json does not exist', () => {
const result = runValidatorWithDir('validate-hooks', 'HOOKS_FILE', '/nonexistent/hooks.json');
assert.strictEqual(result.code, 0, 'Should skip when no hooks.json');
})) passed++; else failed++;
if (test('fails on invalid JSON', () => {
const testDir = createTestDir();
const hooksFile = path.join(testDir, 'hooks.json');
fs.writeFileSync(hooksFile, '{ not valid json }}}');
const result = runValidatorWithDir('validate-hooks', 'HOOKS_FILE', hooksFile);
assert.strictEqual(result.code, 1, 'Should fail on invalid JSON');
assert.ok(result.stderr.includes('Invalid JSON'), 'Should report invalid JSON');
cleanupTestDir(testDir);
})) passed++; else failed++;
if (test('fails on invalid event type', () => {
const testDir = createTestDir();
const hooksFile = path.join(testDir, 'hooks.json');
fs.writeFileSync(hooksFile, JSON.stringify({
hooks: {
InvalidEventType: [{ matcher: 'test', hooks: [{ type: 'command', command: 'echo hi' }] }]
}
}));
const result = runValidatorWithDir('validate-hooks', 'HOOKS_FILE', hooksFile);
assert.strictEqual(result.code, 1, 'Should fail on invalid event type');
assert.ok(result.stderr.includes('Invalid event type'), 'Should report invalid event type');
cleanupTestDir(testDir);
})) passed++; else failed++;
if (test('fails on hook entry missing type field', () => {
const testDir = createTestDir();
const hooksFile = path.join(testDir, 'hooks.json');
fs.writeFileSync(hooksFile, JSON.stringify({
hooks: {
PreToolUse: [{ matcher: 'test', hooks: [{ command: 'echo hi' }] }]
}
}));
const result = runValidatorWithDir('validate-hooks', 'HOOKS_FILE', hooksFile);
assert.strictEqual(result.code, 1, 'Should fail on missing type');
assert.ok(result.stderr.includes('type'), 'Should report missing type');
cleanupTestDir(testDir);
})) passed++; else failed++;
if (test('fails on hook entry missing command field', () => {
const testDir = createTestDir();
const hooksFile = path.join(testDir, 'hooks.json');
fs.writeFileSync(hooksFile, JSON.stringify({
hooks: {
PreToolUse: [{ matcher: 'test', hooks: [{ type: 'command' }] }]
}
}));
const result = runValidatorWithDir('validate-hooks', 'HOOKS_FILE', hooksFile);
assert.strictEqual(result.code, 1, 'Should fail on missing command');
assert.ok(result.stderr.includes('command'), 'Should report missing command');
cleanupTestDir(testDir);
})) passed++; else failed++;
if (test('fails on invalid async field type', () => {
const testDir = createTestDir();
const hooksFile = path.join(testDir, 'hooks.json');
fs.writeFileSync(hooksFile, JSON.stringify({
hooks: {
PreToolUse: [{ matcher: 'test', hooks: [{ type: 'command', command: 'echo', async: 'yes' }] }]
}
}));
const result = runValidatorWithDir('validate-hooks', 'HOOKS_FILE', hooksFile);
assert.strictEqual(result.code, 1, 'Should fail on non-boolean async');
assert.ok(result.stderr.includes('async'), 'Should report async type error');
cleanupTestDir(testDir);
})) passed++; else failed++;
if (test('fails on negative timeout', () => {
const testDir = createTestDir();
const hooksFile = path.join(testDir, 'hooks.json');
fs.writeFileSync(hooksFile, JSON.stringify({
hooks: {
PreToolUse: [{ matcher: 'test', hooks: [{ type: 'command', command: 'echo', timeout: -5 }] }]
}
}));
const result = runValidatorWithDir('validate-hooks', 'HOOKS_FILE', hooksFile);
assert.strictEqual(result.code, 1, 'Should fail on negative timeout');
assert.ok(result.stderr.includes('timeout'), 'Should report timeout error');
cleanupTestDir(testDir);
})) passed++; else failed++;
if (test('fails on invalid inline JS syntax', () => {
const testDir = createTestDir();
const hooksFile = path.join(testDir, 'hooks.json');
fs.writeFileSync(hooksFile, JSON.stringify({
hooks: {
PreToolUse: [{ matcher: 'test', hooks: [{ type: 'command', command: 'node -e "function {"' }] }]
}
}));
const result = runValidatorWithDir('validate-hooks', 'HOOKS_FILE', hooksFile);
assert.strictEqual(result.code, 1, 'Should fail on invalid inline JS');
assert.ok(result.stderr.includes('invalid inline JS'), 'Should report JS syntax error');
cleanupTestDir(testDir);
})) passed++; else failed++;
if (test('passes valid inline JS commands', () => {
const testDir = createTestDir();
const hooksFile = path.join(testDir, 'hooks.json');
fs.writeFileSync(hooksFile, JSON.stringify({
hooks: {
PreToolUse: [{ matcher: 'test', hooks: [{ type: 'command', command: 'node -e "console.log(1+2)"' }] }]
}
}));
const result = runValidatorWithDir('validate-hooks', 'HOOKS_FILE', hooksFile);
assert.strictEqual(result.code, 0, 'Should pass valid inline JS');
cleanupTestDir(testDir);
})) passed++; else failed++;
if (test('validates array command format', () => {
const testDir = createTestDir();
const hooksFile = path.join(testDir, 'hooks.json');
fs.writeFileSync(hooksFile, JSON.stringify({
hooks: {
PreToolUse: [{ matcher: 'test', hooks: [{ type: 'command', command: ['node', '-e', 'console.log(1)'] }] }]
}
}));
const result = runValidatorWithDir('validate-hooks', 'HOOKS_FILE', hooksFile);
assert.strictEqual(result.code, 0, 'Should accept array command format');
cleanupTestDir(testDir);
})) passed++; else failed++;
if (test('validates legacy array format', () => {
const testDir = createTestDir();
const hooksFile = path.join(testDir, 'hooks.json');
fs.writeFileSync(hooksFile, JSON.stringify([
{ matcher: 'test', hooks: [{ type: 'command', command: 'echo ok' }] }
]));
const result = runValidatorWithDir('validate-hooks', 'HOOKS_FILE', hooksFile);
assert.strictEqual(result.code, 0, 'Should accept legacy array format');
cleanupTestDir(testDir);
})) passed++; else failed++;
if (test('fails on matcher missing hooks array', () => {
const testDir = createTestDir();
const hooksFile = path.join(testDir, 'hooks.json');
fs.writeFileSync(hooksFile, JSON.stringify({
hooks: {
PreToolUse: [{ matcher: 'test' }]
}
}));
const result = runValidatorWithDir('validate-hooks', 'HOOKS_FILE', hooksFile);
assert.strictEqual(result.code, 1, 'Should fail on missing hooks array');
cleanupTestDir(testDir);
})) passed++; else failed++;
// ==========================================
// validate-skills.js
// ==========================================
console.log('\nvalidate-skills.js:');
if (test('passes on real project skills', () => {
const result = runValidator('validate-skills');
assert.strictEqual(result.code, 0, `Should pass, got stderr: ${result.stderr}`);
assert.ok(result.stdout.includes('Validated'), 'Should output validation count');
})) passed++; else failed++;
if (test('exits 0 when directory does not exist', () => {
const result = runValidatorWithDir('validate-skills', 'SKILLS_DIR', '/nonexistent/dir');
assert.strictEqual(result.code, 0, 'Should skip when no skills dir');
})) passed++; else failed++;
if (test('fails on skill directory without SKILL.md', () => {
const testDir = createTestDir();
fs.mkdirSync(path.join(testDir, 'broken-skill'));
// No SKILL.md inside
const result = runValidatorWithDir('validate-skills', 'SKILLS_DIR', testDir);
assert.strictEqual(result.code, 1, 'Should fail on missing SKILL.md');
assert.ok(result.stderr.includes('Missing SKILL.md'), 'Should report missing SKILL.md');
cleanupTestDir(testDir);
})) passed++; else failed++;
if (test('fails on empty SKILL.md', () => {
const testDir = createTestDir();
const skillDir = path.join(testDir, 'empty-skill');
fs.mkdirSync(skillDir);
fs.writeFileSync(path.join(skillDir, 'SKILL.md'), '');
const result = runValidatorWithDir('validate-skills', 'SKILLS_DIR', testDir);
assert.strictEqual(result.code, 1, 'Should fail on empty SKILL.md');
assert.ok(result.stderr.includes('Empty'), 'Should report empty file');
cleanupTestDir(testDir);
})) passed++; else failed++;
if (test('passes on valid skill directory', () => {
const testDir = createTestDir();
const skillDir = path.join(testDir, 'good-skill');
fs.mkdirSync(skillDir);
fs.writeFileSync(path.join(skillDir, 'SKILL.md'), '# My Skill\nDescription here.');
const result = runValidatorWithDir('validate-skills', 'SKILLS_DIR', testDir);
assert.strictEqual(result.code, 0, 'Should pass for valid skill');
assert.ok(result.stdout.includes('Validated 1'), 'Should report 1 validated');
cleanupTestDir(testDir);
})) passed++; else failed++;
if (test('ignores non-directory entries', () => {
const testDir = createTestDir();
fs.writeFileSync(path.join(testDir, 'not-a-skill.md'), '# README');
const skillDir = path.join(testDir, 'real-skill');
fs.mkdirSync(skillDir);
fs.writeFileSync(path.join(skillDir, 'SKILL.md'), '# Skill');
const result = runValidatorWithDir('validate-skills', 'SKILLS_DIR', testDir);
assert.strictEqual(result.code, 0, 'Should ignore non-directory entries');
assert.ok(result.stdout.includes('Validated 1'), 'Should count only directories');
cleanupTestDir(testDir);
})) passed++; else failed++;
// ==========================================
// validate-commands.js
// ==========================================
console.log('\nvalidate-commands.js:');
if (test('passes on real project commands', () => {
const result = runValidator('validate-commands');
assert.strictEqual(result.code, 0, `Should pass, got stderr: ${result.stderr}`);
assert.ok(result.stdout.includes('Validated'), 'Should output validation count');
})) passed++; else failed++;
if (test('exits 0 when directory does not exist', () => {
const result = runValidatorWithDir('validate-commands', 'COMMANDS_DIR', '/nonexistent/dir');
assert.strictEqual(result.code, 0, 'Should skip when no commands dir');
})) passed++; else failed++;
if (test('fails on empty command file', () => {
const testDir = createTestDir();
fs.writeFileSync(path.join(testDir, 'empty.md'), '');
const result = runValidatorWithDir('validate-commands', 'COMMANDS_DIR', testDir);
assert.strictEqual(result.code, 1, 'Should fail on empty file');
assert.ok(result.stderr.includes('Empty'), 'Should report empty file');
cleanupTestDir(testDir);
})) passed++; else failed++;
if (test('passes on valid command files', () => {
const testDir = createTestDir();
fs.writeFileSync(path.join(testDir, 'deploy.md'), '# Deploy\nDeploy the application.');
fs.writeFileSync(path.join(testDir, 'test.md'), '# Test\nRun all tests.');
const result = runValidatorWithDir('validate-commands', 'COMMANDS_DIR', testDir);
assert.strictEqual(result.code, 0, 'Should pass for valid commands');
assert.ok(result.stdout.includes('Validated 2'), 'Should report 2 validated');
cleanupTestDir(testDir);
})) passed++; else failed++;
if (test('ignores non-md files', () => {
const testDir = createTestDir();
fs.writeFileSync(path.join(testDir, 'script.js'), 'console.log(1)');
fs.writeFileSync(path.join(testDir, 'valid.md'), '# Command');
const result = runValidatorWithDir('validate-commands', 'COMMANDS_DIR', testDir);
assert.strictEqual(result.code, 0, 'Should ignore non-md files');
assert.ok(result.stdout.includes('Validated 1'), 'Should count only .md files');
cleanupTestDir(testDir);
})) passed++; else failed++;
if (test('detects broken command cross-reference', () => {
const testDir = createTestDir();
const agentsDir = createTestDir();
const skillsDir = createTestDir();
fs.writeFileSync(path.join(testDir, 'my-cmd.md'), '# Command\nUse `/nonexistent-cmd` to do things.');
const result = runValidatorWithDirs('validate-commands', {
COMMANDS_DIR: testDir, AGENTS_DIR: agentsDir, SKILLS_DIR: skillsDir
});
assert.strictEqual(result.code, 1, 'Should fail on broken command ref');
assert.ok(result.stderr.includes('nonexistent-cmd'), 'Should report broken command');
cleanupTestDir(testDir); cleanupTestDir(agentsDir); cleanupTestDir(skillsDir);
})) passed++; else failed++;
if (test('detects broken agent path reference', () => {
const testDir = createTestDir();
const agentsDir = createTestDir();
const skillsDir = createTestDir();
fs.writeFileSync(path.join(testDir, 'cmd.md'), '# Command\nAgent: `agents/fake-agent.md`');
const result = runValidatorWithDirs('validate-commands', {
COMMANDS_DIR: testDir, AGENTS_DIR: agentsDir, SKILLS_DIR: skillsDir
});
assert.strictEqual(result.code, 1, 'Should fail on broken agent ref');
assert.ok(result.stderr.includes('fake-agent'), 'Should report broken agent');
cleanupTestDir(testDir); cleanupTestDir(agentsDir); cleanupTestDir(skillsDir);
})) passed++; else failed++;
if (test('skips references inside fenced code blocks', () => {
const testDir = createTestDir();
const agentsDir = createTestDir();
const skillsDir = createTestDir();
fs.writeFileSync(path.join(testDir, 'cmd.md'),
'# Command\n\n```\nagents/example-agent.md\n`/example-cmd`\n```\n');
const result = runValidatorWithDirs('validate-commands', {
COMMANDS_DIR: testDir, AGENTS_DIR: agentsDir, SKILLS_DIR: skillsDir
});
assert.strictEqual(result.code, 0, 'Should skip refs inside code blocks');
cleanupTestDir(testDir); cleanupTestDir(agentsDir); cleanupTestDir(skillsDir);
})) passed++; else failed++;
if (test('detects broken workflow agent reference', () => {
const testDir = createTestDir();
const agentsDir = createTestDir();
const skillsDir = createTestDir();
fs.writeFileSync(path.join(agentsDir, 'planner.md'), '---\nmodel: sonnet\ntools: Read\n---\n# A');
fs.writeFileSync(path.join(testDir, 'cmd.md'), '# Command\nWorkflow:\nplanner -> ghost-agent');
const result = runValidatorWithDirs('validate-commands', {
COMMANDS_DIR: testDir, AGENTS_DIR: agentsDir, SKILLS_DIR: skillsDir
});
assert.strictEqual(result.code, 1, 'Should fail on broken workflow agent');
assert.ok(result.stderr.includes('ghost-agent'), 'Should report broken workflow agent');
cleanupTestDir(testDir); cleanupTestDir(agentsDir); cleanupTestDir(skillsDir);
})) passed++; else failed++;
// ==========================================
// validate-rules.js
// ==========================================
console.log('\nvalidate-rules.js:');
if (test('passes on real project rules', () => {
const result = runValidator('validate-rules');
assert.strictEqual(result.code, 0, `Should pass, got stderr: ${result.stderr}`);
assert.ok(result.stdout.includes('Validated'), 'Should output validation count');
})) passed++; else failed++;
if (test('exits 0 when directory does not exist', () => {
const result = runValidatorWithDir('validate-rules', 'RULES_DIR', '/nonexistent/dir');
assert.strictEqual(result.code, 0, 'Should skip when no rules dir');
})) passed++; else failed++;
if (test('fails on empty rule file', () => {
const testDir = createTestDir();
fs.writeFileSync(path.join(testDir, 'empty.md'), '');
const result = runValidatorWithDir('validate-rules', 'RULES_DIR', testDir);
assert.strictEqual(result.code, 1, 'Should fail on empty rule file');
assert.ok(result.stderr.includes('Empty'), 'Should report empty file');
cleanupTestDir(testDir);
})) passed++; else failed++;
if (test('passes on valid rule files', () => {
const testDir = createTestDir();
fs.writeFileSync(path.join(testDir, 'coding.md'), '# Coding Rules\nUse immutability.');
const result = runValidatorWithDir('validate-rules', 'RULES_DIR', testDir);
assert.strictEqual(result.code, 0, 'Should pass for valid rules');
assert.ok(result.stdout.includes('Validated 1'), 'Should report 1 validated');
cleanupTestDir(testDir);
})) passed++; else failed++;
// Summary
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);
}
runTests();