From 35aed05903d17f688d17a989fc693cd88feec103 Mon Sep 17 00:00:00 2001 From: Affaan Mustafa Date: Thu, 12 Feb 2026 17:15:21 -0800 Subject: [PATCH] test: add 6 tests for command validation and session content verification - validate-commands: creates: line skipping, valid cross-refs, unclosed code blocks, valid workflow diagrams - session-end: backtick escaping in session files, tools/files in output --- tests/ci/validators.test.js | 62 +++++++++++++++++++++++++++++++++++ tests/hooks/hooks.test.js | 65 +++++++++++++++++++++++++++++++++++++ 2 files changed, 127 insertions(+) diff --git a/tests/ci/validators.test.js b/tests/ci/validators.test.js index 65592ab4..2c050452 100644 --- a/tests/ci/validators.test.js +++ b/tests/ci/validators.test.js @@ -575,6 +575,68 @@ function runTests() { cleanupTestDir(testDir); cleanupTestDir(agentsDir); cleanupTestDir(skillsDir); })) passed++; else failed++; + if (test('skips command references on creates: lines', () => { + const testDir = createTestDir(); + const agentsDir = createTestDir(); + const skillsDir = createTestDir(); + // "Creates: `/new-table`" should NOT flag /new-table as a broken ref + fs.writeFileSync(path.join(testDir, 'gen.md'), + '# Generator\n\n→ Creates: `/new-table`\nWould create: `/new-endpoint`'); + + const result = runValidatorWithDirs('validate-commands', { + COMMANDS_DIR: testDir, AGENTS_DIR: agentsDir, SKILLS_DIR: skillsDir + }); + assert.strictEqual(result.code, 0, 'Should skip creates: lines'); + cleanupTestDir(testDir); cleanupTestDir(agentsDir); cleanupTestDir(skillsDir); + })) passed++; else failed++; + + if (test('accepts valid cross-reference between commands', () => { + const testDir = createTestDir(); + const agentsDir = createTestDir(); + const skillsDir = createTestDir(); + fs.writeFileSync(path.join(testDir, 'build.md'), '# Build\nSee also `/deploy` for deployment.'); + fs.writeFileSync(path.join(testDir, 'deploy.md'), '# Deploy\nRun `/build` first.'); + + const result = runValidatorWithDirs('validate-commands', { + COMMANDS_DIR: testDir, AGENTS_DIR: agentsDir, SKILLS_DIR: skillsDir + }); + assert.strictEqual(result.code, 0, 'Should accept valid cross-refs'); + assert.ok(result.stdout.includes('Validated 2'), 'Should validate both'); + cleanupTestDir(testDir); cleanupTestDir(agentsDir); cleanupTestDir(skillsDir); + })) passed++; else failed++; + + if (test('checks references in unclosed code blocks', () => { + const testDir = createTestDir(); + const agentsDir = createTestDir(); + const skillsDir = createTestDir(); + // Unclosed code block: the ``` regex won't strip it, so refs inside are checked + fs.writeFileSync(path.join(testDir, 'bad.md'), + '# Command\n\n```\n`/phantom-cmd`\nno closing block'); + + const result = runValidatorWithDirs('validate-commands', { + COMMANDS_DIR: testDir, AGENTS_DIR: agentsDir, SKILLS_DIR: skillsDir + }); + // Unclosed code blocks are NOT stripped, so refs inside are validated + assert.strictEqual(result.code, 1, 'Should check refs in unclosed code blocks'); + assert.ok(result.stderr.includes('phantom-cmd'), 'Should report broken ref from unclosed block'); + cleanupTestDir(testDir); cleanupTestDir(agentsDir); cleanupTestDir(skillsDir); + })) passed++; else failed++; + + if (test('validates valid workflow diagram with known agents', () => { + const testDir = createTestDir(); + const agentsDir = createTestDir(); + const skillsDir = createTestDir(); + fs.writeFileSync(path.join(agentsDir, 'planner.md'), '---\nmodel: sonnet\ntools: Read\n---\n# P'); + fs.writeFileSync(path.join(agentsDir, 'reviewer.md'), '---\nmodel: sonnet\ntools: Read\n---\n# R'); + fs.writeFileSync(path.join(testDir, 'flow.md'), '# Workflow\n\nplanner -> reviewer'); + + const result = runValidatorWithDirs('validate-commands', { + COMMANDS_DIR: testDir, AGENTS_DIR: agentsDir, SKILLS_DIR: skillsDir + }); + assert.strictEqual(result.code, 0, 'Should pass on valid workflow'); + cleanupTestDir(testDir); cleanupTestDir(agentsDir); cleanupTestDir(skillsDir); + })) passed++; else failed++; + // ========================================== // validate-rules.js // ========================================== diff --git a/tests/hooks/hooks.test.js b/tests/hooks/hooks.test.js index 39c9024a..0cf800a7 100644 --- a/tests/hooks/hooks.test.js +++ b/tests/hooks/hooks.test.js @@ -749,6 +749,71 @@ async function runTests() { cleanupTestDir(testDir); })) passed++; else failed++; + if (await asyncTest('escapes backticks in user messages in session file', async () => { + const testDir = createTestDir(); + const transcriptPath = path.join(testDir, 'transcript.jsonl'); + + // User messages with backticks that could break markdown + const lines = [ + '{"type":"user","content":"Fix the `handleAuth` function in `auth.ts`"}', + '{"type":"user","content":"Run `npm test` to verify"}', + ]; + fs.writeFileSync(transcriptPath, lines.join('\n')); + + const stdinJson = JSON.stringify({ transcript_path: transcriptPath }); + const result = await runScript(path.join(scriptsDir, 'session-end.js'), stdinJson, { + HOME: testDir + }); + assert.strictEqual(result.code, 0, 'Should handle backticks without crash'); + + // Find the session file in the temp HOME + const claudeDir = path.join(testDir, '.claude', 'sessions'); + if (fs.existsSync(claudeDir)) { + const files = fs.readdirSync(claudeDir).filter(f => f.endsWith('.tmp')); + if (files.length > 0) { + const content = fs.readFileSync(path.join(claudeDir, files[0]), 'utf8'); + // Backticks should be escaped in the output + assert.ok(content.includes('\\`'), 'Should escape backticks in session file'); + assert.ok(!content.includes('`handleAuth`'), 'Raw backticks should be escaped'); + } + } + cleanupTestDir(testDir); + })) passed++; else failed++; + + if (await asyncTest('session file contains tools used and files modified', async () => { + const testDir = createTestDir(); + const transcriptPath = path.join(testDir, 'transcript.jsonl'); + + const lines = [ + '{"type":"user","content":"Edit the config"}', + '{"type":"tool_use","tool_name":"Edit","tool_input":{"file_path":"/src/config.ts"}}', + '{"type":"tool_use","tool_name":"Read","tool_input":{"file_path":"/src/utils.ts"}}', + '{"type":"tool_use","tool_name":"Write","tool_input":{"file_path":"/src/new-file.ts"}}', + ]; + fs.writeFileSync(transcriptPath, lines.join('\n')); + + const stdinJson = JSON.stringify({ transcript_path: transcriptPath }); + const result = await runScript(path.join(scriptsDir, 'session-end.js'), stdinJson, { + HOME: testDir + }); + assert.strictEqual(result.code, 0); + + const claudeDir = path.join(testDir, '.claude', 'sessions'); + if (fs.existsSync(claudeDir)) { + const files = fs.readdirSync(claudeDir).filter(f => f.endsWith('.tmp')); + if (files.length > 0) { + const content = fs.readFileSync(path.join(claudeDir, files[0]), 'utf8'); + // Should contain files modified (Edit and Write, not Read) + assert.ok(content.includes('/src/config.ts'), 'Should list edited file'); + assert.ok(content.includes('/src/new-file.ts'), 'Should list written file'); + // Should contain tools used + assert.ok(content.includes('Edit'), 'Should list Edit tool'); + assert.ok(content.includes('Read'), 'Should list Read tool'); + } + } + cleanupTestDir(testDir); + })) passed++; else failed++; + // hooks.json validation console.log('\nhooks.json Validation:');