mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-04-05 08:43:29 +08:00
test: add 22 tests for validators, skill-create-output, and package-manager edge cases
This commit is contained in:
@@ -1181,6 +1181,160 @@ function runTests() {
|
|||||||
cleanupTestDir(testDir);
|
cleanupTestDir(testDir);
|
||||||
})) passed++; else failed++;
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
// ── Round 27: hook validation edge cases ──
|
||||||
|
console.log('\nvalidate-hooks.js (Round 27 edge cases):');
|
||||||
|
|
||||||
|
if (test('rejects array command with empty string element', () => {
|
||||||
|
const testDir = createTestDir();
|
||||||
|
const hooksFile = path.join(testDir, 'hooks.json');
|
||||||
|
fs.writeFileSync(hooksFile, JSON.stringify({
|
||||||
|
hooks: {
|
||||||
|
PreToolUse: [{ matcher: 'test', hooks: [{ type: 'command', command: ['node', '', 'script.js'] }] }]
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
const result = runValidatorWithDir('validate-hooks', 'HOOKS_FILE', hooksFile);
|
||||||
|
assert.strictEqual(result.code, 1, 'Should reject array with empty string element');
|
||||||
|
assert.ok(result.stderr.includes('command'), 'Should report command field error');
|
||||||
|
cleanupTestDir(testDir);
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('rejects 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 hi', timeout: -5 }] }]
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
const result = runValidatorWithDir('validate-hooks', 'HOOKS_FILE', hooksFile);
|
||||||
|
assert.strictEqual(result.code, 1, 'Should reject negative timeout');
|
||||||
|
assert.ok(result.stderr.includes('timeout'), 'Should report timeout error');
|
||||||
|
cleanupTestDir(testDir);
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('rejects non-boolean async field', () => {
|
||||||
|
const testDir = createTestDir();
|
||||||
|
const hooksFile = path.join(testDir, 'hooks.json');
|
||||||
|
fs.writeFileSync(hooksFile, JSON.stringify({
|
||||||
|
hooks: {
|
||||||
|
PostToolUse: [{ matcher: 'test', hooks: [{ type: 'command', command: 'echo ok', async: 'yes' }] }]
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
const result = runValidatorWithDir('validate-hooks', 'HOOKS_FILE', hooksFile);
|
||||||
|
assert.strictEqual(result.code, 1, 'Should reject non-boolean async');
|
||||||
|
assert.ok(result.stderr.includes('async'), 'Should report async type error');
|
||||||
|
cleanupTestDir(testDir);
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('reports correct index for error in deeply nested hook', () => {
|
||||||
|
const testDir = createTestDir();
|
||||||
|
const hooksFile = path.join(testDir, 'hooks.json');
|
||||||
|
const manyHooks = [];
|
||||||
|
for (let i = 0; i < 5; i++) {
|
||||||
|
manyHooks.push({ type: 'command', command: 'echo ok' });
|
||||||
|
}
|
||||||
|
// Add an invalid hook at index 5
|
||||||
|
manyHooks.push({ type: 'command', command: '' });
|
||||||
|
fs.writeFileSync(hooksFile, JSON.stringify({
|
||||||
|
hooks: {
|
||||||
|
PreToolUse: [{ matcher: 'test', hooks: manyHooks }]
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
const result = runValidatorWithDir('validate-hooks', 'HOOKS_FILE', hooksFile);
|
||||||
|
assert.strictEqual(result.code, 1, 'Should fail on invalid hook at high index');
|
||||||
|
assert.ok(result.stderr.includes('hooks[5]'), 'Should report correct hook index 5');
|
||||||
|
cleanupTestDir(testDir);
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('validates node -e with escaped quotes in inline JS', () => {
|
||||||
|
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 "const x = 1 + 2; process.exit(0)"' }] }]
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
const result = runValidatorWithDir('validate-hooks', 'HOOKS_FILE', hooksFile);
|
||||||
|
assert.strictEqual(result.code, 0, 'Should pass valid multi-statement inline JS');
|
||||||
|
cleanupTestDir(testDir);
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('accepts multiple valid event types in single hooks file', () => {
|
||||||
|
const testDir = createTestDir();
|
||||||
|
const hooksFile = path.join(testDir, 'hooks.json');
|
||||||
|
fs.writeFileSync(hooksFile, JSON.stringify({
|
||||||
|
hooks: {
|
||||||
|
PreToolUse: [{ matcher: 'test', hooks: [{ type: 'command', command: 'echo pre' }] }],
|
||||||
|
PostToolUse: [{ matcher: 'test', hooks: [{ type: 'command', command: 'echo post' }] }],
|
||||||
|
Stop: [{ matcher: 'test', hooks: [{ type: 'command', command: 'echo stop' }] }]
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
const result = runValidatorWithDir('validate-hooks', 'HOOKS_FILE', hooksFile);
|
||||||
|
assert.strictEqual(result.code, 0, 'Should accept multiple valid event types');
|
||||||
|
assert.ok(result.stdout.includes('3'), 'Should report 3 matchers validated');
|
||||||
|
cleanupTestDir(testDir);
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
// ── Round 27: command validation edge cases ──
|
||||||
|
console.log('\nvalidate-commands.js (Round 27 edge cases):');
|
||||||
|
|
||||||
|
if (test('validates multiple command refs on same non-creates line', () => {
|
||||||
|
const testDir = createTestDir();
|
||||||
|
const agentsDir = createTestDir();
|
||||||
|
const skillsDir = createTestDir();
|
||||||
|
// Create two valid commands
|
||||||
|
fs.writeFileSync(path.join(testDir, 'cmd-a.md'), '# Command A\nBasic command.');
|
||||||
|
fs.writeFileSync(path.join(testDir, 'cmd-b.md'), '# Command B\nBasic command.');
|
||||||
|
// Create a third command that references both on one line
|
||||||
|
fs.writeFileSync(path.join(testDir, 'cmd-c.md'),
|
||||||
|
'# Command C\nUse `/cmd-a` and `/cmd-b` together.');
|
||||||
|
|
||||||
|
const result = runValidatorWithDirs('validate-commands', {
|
||||||
|
COMMANDS_DIR: testDir, AGENTS_DIR: agentsDir, SKILLS_DIR: skillsDir
|
||||||
|
});
|
||||||
|
assert.strictEqual(result.code, 0, 'Should pass when multiple refs on same line are all valid');
|
||||||
|
cleanupTestDir(testDir); cleanupTestDir(agentsDir); cleanupTestDir(skillsDir);
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('fails when one of multiple refs on same line is invalid', () => {
|
||||||
|
const testDir = createTestDir();
|
||||||
|
const agentsDir = createTestDir();
|
||||||
|
const skillsDir = createTestDir();
|
||||||
|
// Only cmd-a exists
|
||||||
|
fs.writeFileSync(path.join(testDir, 'cmd-a.md'), '# Command A\nBasic command.');
|
||||||
|
// cmd-c references cmd-a (valid) and cmd-z (invalid) on same line
|
||||||
|
fs.writeFileSync(path.join(testDir, 'cmd-c.md'),
|
||||||
|
'# Command C\nUse `/cmd-a` and `/cmd-z` together.');
|
||||||
|
|
||||||
|
const result = runValidatorWithDirs('validate-commands', {
|
||||||
|
COMMANDS_DIR: testDir, AGENTS_DIR: agentsDir, SKILLS_DIR: skillsDir
|
||||||
|
});
|
||||||
|
assert.strictEqual(result.code, 1, 'Should fail when any ref is invalid');
|
||||||
|
assert.ok(result.stderr.includes('cmd-z'), 'Should report the invalid reference');
|
||||||
|
cleanupTestDir(testDir); cleanupTestDir(agentsDir); cleanupTestDir(skillsDir);
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('code blocks are stripped before checking references', () => {
|
||||||
|
const testDir = createTestDir();
|
||||||
|
const agentsDir = createTestDir();
|
||||||
|
const skillsDir = createTestDir();
|
||||||
|
// Reference inside a code block should not be validated
|
||||||
|
fs.writeFileSync(path.join(testDir, 'cmd-x.md'),
|
||||||
|
'# Command X\n```\n`/nonexistent-cmd` in code block\n```\nEnd.');
|
||||||
|
|
||||||
|
const result = runValidatorWithDirs('validate-commands', {
|
||||||
|
COMMANDS_DIR: testDir, AGENTS_DIR: agentsDir, SKILLS_DIR: skillsDir
|
||||||
|
});
|
||||||
|
assert.strictEqual(result.code, 0, 'Should ignore command refs inside code blocks');
|
||||||
|
cleanupTestDir(testDir); cleanupTestDir(agentsDir); cleanupTestDir(skillsDir);
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
// --- validate-skills.js: mixed valid/invalid ---
|
// --- validate-skills.js: mixed valid/invalid ---
|
||||||
console.log('\nvalidate-skills.js (mixed dirs):');
|
console.log('\nvalidate-skills.js (mixed dirs):');
|
||||||
|
|
||||||
|
|||||||
@@ -930,6 +930,69 @@ function runTests() {
|
|||||||
assert.doesNotThrow(() => new RegExp(pattern), 'Should produce valid regex with escaped parens');
|
assert.doesNotThrow(() => new RegExp(pattern), 'Should produce valid regex with escaped parens');
|
||||||
})) passed++; else failed++;
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
// ── Round 27: input validation and escapeRegex edge cases ──
|
||||||
|
console.log('\ngetRunCommand (non-string input):');
|
||||||
|
|
||||||
|
if (test('rejects undefined script name', () => {
|
||||||
|
assert.throws(() => pm.getRunCommand(undefined), /non-empty string/);
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('rejects numeric script name', () => {
|
||||||
|
assert.throws(() => pm.getRunCommand(123), /non-empty string/);
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('rejects boolean script name', () => {
|
||||||
|
assert.throws(() => pm.getRunCommand(true), /non-empty string/);
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
console.log('\ngetExecCommand (non-string binary):');
|
||||||
|
|
||||||
|
if (test('rejects undefined binary name', () => {
|
||||||
|
assert.throws(() => pm.getExecCommand(undefined), /non-empty string/);
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('rejects numeric binary name', () => {
|
||||||
|
assert.throws(() => pm.getExecCommand(42), /non-empty string/);
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
console.log('\ngetCommandPattern (escapeRegex completeness):');
|
||||||
|
|
||||||
|
if (test('escapes all regex metacharacters in action', () => {
|
||||||
|
// All regex metacharacters: . * + ? ^ $ { } ( ) | [ ] \
|
||||||
|
const action = 'test.*+?^${}()|[]\\';
|
||||||
|
const pattern = pm.getCommandPattern(action);
|
||||||
|
// Should produce a valid regex without throwing
|
||||||
|
assert.doesNotThrow(() => new RegExp(pattern), 'Should produce valid regex');
|
||||||
|
// Should match the literal string
|
||||||
|
const regex = new RegExp(pattern);
|
||||||
|
assert.ok(regex.test(`npm run ${action}`), 'Should match literal metacharacters');
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('escapeRegex preserves alphanumeric chars', () => {
|
||||||
|
const pattern = pm.getCommandPattern('simple-test');
|
||||||
|
const regex = new RegExp(pattern);
|
||||||
|
assert.ok(regex.test('npm run simple-test'), 'Should match simple action name');
|
||||||
|
assert.ok(!regex.test('npm run simpleXtest'), 'Dash should not match arbitrary char');
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
console.log('\ngetPackageManager (global config edge cases):');
|
||||||
|
|
||||||
|
if (test('ignores global config with non-string packageManager', () => {
|
||||||
|
// This tests the path through loadConfig where packageManager is not a valid PM name
|
||||||
|
const originalEnv = process.env.CLAUDE_PACKAGE_MANAGER;
|
||||||
|
try {
|
||||||
|
delete process.env.CLAUDE_PACKAGE_MANAGER;
|
||||||
|
// getPackageManager should fall through to default when no valid config exists
|
||||||
|
const result = pm.getPackageManager({ projectDir: os.tmpdir() });
|
||||||
|
assert.ok(result.name, 'Should return a package manager name');
|
||||||
|
assert.ok(result.config, 'Should return config object');
|
||||||
|
} finally {
|
||||||
|
if (originalEnv !== undefined) {
|
||||||
|
process.env.CLAUDE_PACKAGE_MANAGER = originalEnv;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
// Summary
|
// Summary
|
||||||
console.log('\n=== Test Results ===');
|
console.log('\n=== Test Results ===');
|
||||||
console.log(`Passed: ${passed}`);
|
console.log(`Passed: ${passed}`);
|
||||||
|
|||||||
@@ -274,6 +274,51 @@ function runTests() {
|
|||||||
}
|
}
|
||||||
})) passed++; else failed++;
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
// ── Round 27: box and progressBar edge cases ──
|
||||||
|
console.log('\nbox() content overflow:');
|
||||||
|
|
||||||
|
if (test('box does not crash when content line exceeds width', () => {
|
||||||
|
const output = new SkillCreateOutput('repo', { width: 30 });
|
||||||
|
// Force a very long instinct name that exceeds width
|
||||||
|
const logs = captureLog(() => output.instincts([
|
||||||
|
{ name: 'this-is-an-extremely-long-instinct-name-that-clearly-exceeds-width', confidence: 0.9 },
|
||||||
|
]));
|
||||||
|
// Math.max(0, padding) should prevent RangeError
|
||||||
|
assert.ok(logs.length > 0, 'Should produce output without RangeError');
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('patterns renders negative confidence without crash', () => {
|
||||||
|
const output = new SkillCreateOutput('repo');
|
||||||
|
// confidence -0.1 => percent -10 — Math.max(0, ...) should clamp filled to 0
|
||||||
|
const logs = captureLog(() => output.patterns([
|
||||||
|
{ name: 'Negative', trigger: 'never', confidence: -0.1, evidence: 'impossible' },
|
||||||
|
]));
|
||||||
|
const combined = stripAnsi(logs.join('\n'));
|
||||||
|
assert.ok(combined.includes('-10%'), 'Should show -10%');
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('header does not crash with very long repo name', () => {
|
||||||
|
const longRepo = 'A'.repeat(100);
|
||||||
|
const output = new SkillCreateOutput(longRepo);
|
||||||
|
// Math.max(0, 55 - stripAnsi(subtitle).length) protects against negative repeat
|
||||||
|
const logs = captureLog(() => output.header());
|
||||||
|
assert.ok(logs.length > 0, 'Should produce output without crash');
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('stripAnsi handles nested ANSI codes with multi-digit params', () => {
|
||||||
|
// Simulate bold + color + reset
|
||||||
|
const ansiStr = '\x1b[1m\x1b[36mBold Cyan\x1b[0m\x1b[0m';
|
||||||
|
const stripped = stripAnsi(ansiStr);
|
||||||
|
assert.strictEqual(stripped, 'Bold Cyan', 'Should strip all nested ANSI sequences');
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('footer produces output', () => {
|
||||||
|
const output = new SkillCreateOutput('repo');
|
||||||
|
const logs = captureLog(() => output.footer());
|
||||||
|
const combined = stripAnsi(logs.join('\n'));
|
||||||
|
assert.ok(combined.includes('Powered by'), 'Should include attribution text');
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
// Summary
|
// Summary
|
||||||
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
|
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
|
||||||
process.exit(failed > 0 ? 1 : 0);
|
process.exit(failed > 0 ? 1 : 0);
|
||||||
|
|||||||
Reference in New Issue
Block a user