From 72de0a4e2c5d29f46879904a2c6910c3250e6534 Mon Sep 17 00:00:00 2001 From: Affaan Mustafa Date: Fri, 13 Feb 2026 03:44:10 -0800 Subject: [PATCH] test: add 17 tests for validators, hooks, and edge cases (Round 32) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Coverage improvements: - validate-agents: empty frontmatter block, no-content frontmatter, partial frontmatter, mixed valid/invalid agents - validate-rules: directory with .md name (stat.isFile check), deeply nested subdirectory rules - validate-commands: 3-agent workflow chain, broken middle agent - post-edit-typecheck: spaces in paths, shell metacharacters, .tsx - check-console-log: git failure passthrough, large stdin - post-edit-console-warn: console.error only, null tool_input - session-end: empty transcript, whitespace-only transcript Total tests: 686 → 703 --- tests/ci/validators.test.js | 109 ++++++++++++++++++++++++++++++++++++ tests/hooks/hooks.test.js | 104 ++++++++++++++++++++++++++++++++++ 2 files changed, 213 insertions(+) diff --git a/tests/ci/validators.test.js b/tests/ci/validators.test.js index e2d24d9..0d9699d 100644 --- a/tests/ci/validators.test.js +++ b/tests/ci/validators.test.js @@ -1421,6 +1421,115 @@ function runTests() { cleanupTestDir(testDir); })) passed++; else failed++; + // ── Round 32: empty frontmatter & edge cases ── + console.log('\nRound 32: validate-agents (empty frontmatter):'); + + if (test('rejects agent with empty frontmatter block (no key-value pairs)', () => { + const testDir = createTestDir(); + // Blank line between --- markers creates a valid but empty frontmatter block + fs.writeFileSync(path.join(testDir, 'empty-fm.md'), '---\n\n---\n# Agent with empty frontmatter'); + + const result = runValidatorWithDir('validate-agents', 'AGENTS_DIR', testDir); + assert.strictEqual(result.code, 1, 'Should reject empty frontmatter'); + assert.ok(result.stderr.includes('model'), 'Should report missing model'); + assert.ok(result.stderr.includes('tools'), 'Should report missing tools'); + cleanupTestDir(testDir); + })) passed++; else failed++; + + if (test('rejects agent with no content between --- markers (Missing frontmatter)', () => { + const testDir = createTestDir(); + // ---\n--- with no blank line → regex doesn't match → "Missing frontmatter" + fs.writeFileSync(path.join(testDir, 'no-fm.md'), '---\n---\n# Agent'); + + const result = runValidatorWithDir('validate-agents', 'AGENTS_DIR', testDir); + assert.strictEqual(result.code, 1, 'Should reject missing frontmatter'); + assert.ok(result.stderr.includes('Missing frontmatter'), 'Should report missing frontmatter'); + cleanupTestDir(testDir); + })) passed++; else failed++; + + if (test('rejects agent with partial frontmatter (only model, no tools)', () => { + const testDir = createTestDir(); + fs.writeFileSync(path.join(testDir, 'partial.md'), '---\nmodel: haiku\n---\n# Partial agent'); + + const result = runValidatorWithDir('validate-agents', 'AGENTS_DIR', testDir); + assert.strictEqual(result.code, 1, 'Should reject partial frontmatter'); + assert.ok(result.stderr.includes('tools'), 'Should report missing tools'); + assert.ok(!result.stderr.includes('model'), 'Should NOT report model (it is present)'); + cleanupTestDir(testDir); + })) passed++; else failed++; + + if (test('handles multiple agents where only one is invalid', () => { + const testDir = createTestDir(); + fs.writeFileSync(path.join(testDir, 'good.md'), '---\nmodel: sonnet\ntools: Read\n---\n# Good'); + fs.writeFileSync(path.join(testDir, 'bad.md'), '---\nmodel: invalid-model\ntools: Read\n---\n# Bad'); + + const result = runValidatorWithDir('validate-agents', 'AGENTS_DIR', testDir); + assert.strictEqual(result.code, 1, 'Should fail when any agent is invalid'); + assert.ok(result.stderr.includes('bad.md'), 'Should identify the bad file'); + cleanupTestDir(testDir); + })) passed++; else failed++; + + console.log('\nRound 32: validate-rules (non-file entries):'); + + if (test('skips directory entries even if named with .md extension', () => { + const testDir = createTestDir(); + // Create a directory named "tricky.md" — stat.isFile() should skip it + fs.mkdirSync(path.join(testDir, 'tricky.md')); + fs.writeFileSync(path.join(testDir, 'real.md'), '# A real rule'); + + const result = runValidatorWithDir('validate-rules', 'RULES_DIR', testDir); + assert.strictEqual(result.code, 0, 'Should skip directory entries'); + assert.ok(result.stdout.includes('Validated 1'), 'Should count only the real file'); + cleanupTestDir(testDir); + })) passed++; else failed++; + + if (test('handles deeply nested rule in subdirectory', () => { + const testDir = createTestDir(); + const deepDir = path.join(testDir, 'cat1', 'sub1'); + fs.mkdirSync(deepDir, { recursive: true }); + fs.writeFileSync(path.join(deepDir, 'deep-rule.md'), '# Deep nested rule'); + + const result = runValidatorWithDir('validate-rules', 'RULES_DIR', testDir); + assert.strictEqual(result.code, 0, 'Should validate deeply nested rules'); + assert.ok(result.stdout.includes('Validated 1'), 'Should find the nested rule'); + cleanupTestDir(testDir); + })) passed++; else failed++; + + console.log('\nRound 32: validate-commands (agent reference with valid workflow):'); + + if (test('passes workflow with three chained 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, 'tdd-guide.md'), '---\nmodel: sonnet\ntools: Read\n---\n# T'); + fs.writeFileSync(path.join(agentsDir, 'code-reviewer.md'), '---\nmodel: sonnet\ntools: Read\n---\n# C'); + fs.writeFileSync(path.join(testDir, 'flow.md'), '# Flow\n\nplanner -> tdd-guide -> code-reviewer'); + + const result = runValidatorWithDirs('validate-commands', { + COMMANDS_DIR: testDir, AGENTS_DIR: agentsDir, SKILLS_DIR: skillsDir + }); + assert.strictEqual(result.code, 0, 'Should pass on valid 3-agent workflow'); + cleanupTestDir(testDir); cleanupTestDir(agentsDir); cleanupTestDir(skillsDir); + })) passed++; else failed++; + + if (test('detects broken agent in middle of workflow chain', () => { + 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, 'code-reviewer.md'), '---\nmodel: sonnet\ntools: Read\n---\n# C'); + // missing-agent is NOT created + fs.writeFileSync(path.join(testDir, 'flow.md'), '# Flow\n\nplanner -> missing-agent -> code-reviewer'); + + const result = runValidatorWithDirs('validate-commands', { + COMMANDS_DIR: testDir, AGENTS_DIR: agentsDir, SKILLS_DIR: skillsDir + }); + assert.strictEqual(result.code, 1, 'Should detect broken agent in workflow chain'); + assert.ok(result.stderr.includes('missing-agent'), 'Should report the missing agent'); + cleanupTestDir(testDir); cleanupTestDir(agentsDir); cleanupTestDir(skillsDir); + })) passed++; else failed++; + // Summary console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`); process.exit(failed > 0 ? 1 : 0); diff --git a/tests/hooks/hooks.test.js b/tests/hooks/hooks.test.js index ca51982..1d8a018 100644 --- a/tests/hooks/hooks.test.js +++ b/tests/hooks/hooks.test.js @@ -2234,6 +2234,110 @@ async function runTests() { assert.ok(runAllSource.includes('stderr'), 'Should handle stderr output'); })) passed++; else failed++; + // ── Round 32: post-edit-typecheck special characters & check-console-log ── + console.log('\nRound 32: post-edit-typecheck (special character paths):'); + + if (await asyncTest('handles file path with spaces gracefully', async () => { + const testDir = createTestDir(); + const testFile = path.join(testDir, 'my file.ts'); + fs.writeFileSync(testFile, 'const x: number = 1;'); + + const stdinJson = JSON.stringify({ tool_input: { file_path: testFile } }); + const result = await runScript(path.join(scriptsDir, 'post-edit-typecheck.js'), stdinJson); + assert.strictEqual(result.code, 0, 'Should handle spaces in path'); + assert.ok(result.stdout.includes('tool_input'), 'Should pass through data'); + cleanupTestDir(testDir); + })) passed++; else failed++; + + if (await asyncTest('handles file path with shell metacharacters safely', async () => { + const testDir = createTestDir(); + // File name with characters that could be dangerous in shell contexts + const testFile = path.join(testDir, 'test$(echo).ts'); + fs.writeFileSync(testFile, 'const x: number = 1;'); + + const stdinJson = JSON.stringify({ tool_input: { file_path: testFile } }); + const result = await runScript(path.join(scriptsDir, 'post-edit-typecheck.js'), stdinJson); + assert.strictEqual(result.code, 0, 'Should not crash on shell metacharacters'); + // execFileSync prevents shell injection — just verify no crash + assert.ok(result.stdout.includes('tool_input'), 'Should pass through data safely'); + cleanupTestDir(testDir); + })) passed++; else failed++; + + if (await asyncTest('handles .tsx file extension', async () => { + const testDir = createTestDir(); + const testFile = path.join(testDir, 'component.tsx'); + fs.writeFileSync(testFile, 'const App = () =>
Hello
;'); + + const stdinJson = JSON.stringify({ tool_input: { file_path: testFile } }); + const result = await runScript(path.join(scriptsDir, 'post-edit-typecheck.js'), stdinJson); + assert.strictEqual(result.code, 0, 'Should handle .tsx files'); + assert.ok(result.stdout.includes('tool_input'), 'Should pass through data'); + cleanupTestDir(testDir); + })) passed++; else failed++; + + console.log('\nRound 32: check-console-log (edge cases):'); + + if (await asyncTest('passes through data when git commands fail', async () => { + // Run from a non-git directory + const testDir = createTestDir(); + const stdinData = JSON.stringify({ tool_name: 'Write', tool_input: {} }); + const result = await runScript(path.join(scriptsDir, 'check-console-log.js'), stdinData); + assert.strictEqual(result.code, 0, 'Should exit 0'); + assert.ok(result.stdout.includes('tool_name'), 'Should pass through stdin'); + cleanupTestDir(testDir); + })) passed++; else failed++; + + if (await asyncTest('handles very large stdin within limit', async () => { + // Send just under the 1MB limit + const largePayload = JSON.stringify({ tool_name: 'x'.repeat(500000) }); + const result = await runScript(path.join(scriptsDir, 'check-console-log.js'), largePayload); + assert.strictEqual(result.code, 0, 'Should handle large stdin'); + })) passed++; else failed++; + + console.log('\nRound 32: post-edit-console-warn (additional edge cases):'); + + if (await asyncTest('handles file with only console.error (no warning)', async () => { + const testDir = createTestDir(); + const testFile = path.join(testDir, 'errors-only.ts'); + fs.writeFileSync(testFile, 'console.error("this is fine");\nconsole.warn("also fine");'); + + const stdinJson = JSON.stringify({ tool_input: { file_path: testFile } }); + const result = await runScript(path.join(scriptsDir, 'post-edit-console-warn.js'), stdinJson); + assert.ok(!result.stderr.includes('WARNING'), 'Should NOT warn for console.error/warn only'); + cleanupTestDir(testDir); + })) passed++; else failed++; + + if (await asyncTest('handles null tool_input gracefully', async () => { + const stdinJson = JSON.stringify({ tool_input: null }); + const result = await runScript(path.join(scriptsDir, 'post-edit-console-warn.js'), stdinJson); + assert.strictEqual(result.code, 0, 'Should handle null tool_input'); + assert.ok(result.stdout.includes('tool_input'), 'Should pass through data'); + })) passed++; else failed++; + + console.log('\nRound 32: session-end.js (empty transcript):'); + + if (await asyncTest('handles completely empty transcript file', async () => { + const testDir = createTestDir(); + const transcriptPath = path.join(testDir, 'empty.jsonl'); + fs.writeFileSync(transcriptPath, ''); + + const stdinJson = JSON.stringify({ transcript_path: transcriptPath }); + const result = await runScript(path.join(scriptsDir, 'session-end.js'), stdinJson); + assert.strictEqual(result.code, 0, 'Should handle empty transcript'); + cleanupTestDir(testDir); + })) passed++; else failed++; + + if (await asyncTest('handles transcript with only whitespace lines', async () => { + const testDir = createTestDir(); + const transcriptPath = path.join(testDir, 'whitespace.jsonl'); + fs.writeFileSync(transcriptPath, ' \n\n \n'); + + const stdinJson = JSON.stringify({ transcript_path: transcriptPath }); + const result = await runScript(path.join(scriptsDir, 'session-end.js'), stdinJson); + assert.strictEqual(result.code, 0, 'Should handle whitespace-only transcript'); + cleanupTestDir(testDir); + })) passed++; else failed++; + // Summary console.log('\n=== Test Results ==='); console.log(`Passed: ${passed}`);