Merge remote-tracking branch 'origin/main' into feat/optimize-biome-hooks

# Conflicts:
#	tests/hooks/hooks.test.js
#	tests/run-all.js
This commit is contained in:
Affaan Mustafa
2026-03-10 20:25:22 -07:00
29 changed files with 2705 additions and 167 deletions

View File

@@ -1927,7 +1927,7 @@ function runTests() {
PreToolUse: [{
matcher: 'Write',
hooks: [{
type: 'intercept',
type: 'command',
command: 'echo test',
async: 'yes' // Should be boolean, not string
}]
@@ -1947,7 +1947,7 @@ function runTests() {
PostToolUse: [{
matcher: 'Edit',
hooks: [{
type: 'intercept',
type: 'command',
command: 'echo test',
timeout: -5 // Must be non-negative
}]
@@ -2105,6 +2105,31 @@ function runTests() {
cleanupTestDir(testDir);
})) passed++; else failed++;
console.log('\nRound 82b: validate-hooks (current official events and hook types):');
if (test('accepts UserPromptSubmit with omitted matcher and prompt/http/agent hooks', () => {
const testDir = createTestDir();
const hooksJson = JSON.stringify({
hooks: {
UserPromptSubmit: [
{
hooks: [
{ type: 'prompt', prompt: 'Summarize the request.' },
{ type: 'agent', prompt: 'Review for security issues.', model: 'gpt-5.4' },
{ type: 'http', url: 'https://example.com/hooks', headers: { Authorization: 'Bearer token' } }
]
}
]
}
});
const hooksFile = path.join(testDir, 'hooks.json');
fs.writeFileSync(hooksFile, hooksJson);
const result = runValidatorWithDir('validate-hooks', 'HOOKS_FILE', hooksFile);
assert.strictEqual(result.code, 0, 'Should accept current official hook event/type combinations');
cleanupTestDir(testDir);
})) passed++; else failed++;
// ── Round 83: validate-agents whitespace-only field, validate-skills empty SKILL.md ──
console.log('\nRound 83: validate-agents (whitespace-only frontmatter field value):');

View File

@@ -63,6 +63,29 @@ function runScript(scriptPath, input = '', env = {}) {
});
}
function runShellScript(scriptPath, args = [], input = '', env = {}, cwd = process.cwd()) {
return new Promise((resolve, reject) => {
const proc = spawn('bash', [scriptPath, ...args], {
cwd,
env: { ...process.env, ...env },
stdio: ['pipe', 'pipe', 'pipe']
});
let stdout = '';
let stderr = '';
if (input) {
proc.stdin.write(input);
}
proc.stdin.end();
proc.stdout.on('data', data => stdout += data);
proc.stderr.on('data', data => stderr += data);
proc.on('close', code => resolve({ code, stdout, stderr }));
proc.on('error', reject);
});
}
// Create a temporary test directory
function createTestDir() {
const testDir = path.join(os.tmpdir(), `hooks-test-${Date.now()}`);
@@ -2070,11 +2093,47 @@ async function runTests() {
passed++;
else failed++;
if (
await asyncTest('matches .tsx extension for type checking', async () => {
const testDir = createTestDir();
const testFile = path.join(testDir, 'component.tsx');
fs.writeFileSync(testFile, 'const x: number = 1;');
if (await asyncTest('observe.sh falls back to legacy output fields when tool_response is null', async () => {
const homeDir = createTestDir();
const projectDir = createTestDir();
const observePath = path.join(__dirname, '..', '..', 'skills', 'continuous-learning-v2', 'hooks', 'observe.sh');
const payload = JSON.stringify({
tool_name: 'Bash',
tool_input: { command: 'echo hello' },
tool_response: null,
tool_output: 'legacy output',
session_id: 'session-123',
cwd: projectDir
});
try {
const result = await runShellScript(observePath, ['post'], payload, {
HOME: homeDir,
CLAUDE_PROJECT_DIR: projectDir
}, projectDir);
assert.strictEqual(result.code, 0, `observe.sh should exit successfully, stderr: ${result.stderr}`);
const projectsDir = path.join(homeDir, '.claude', 'homunculus', 'projects');
const projectIds = fs.readdirSync(projectsDir);
assert.strictEqual(projectIds.length, 1, 'observe.sh should create one project-scoped observation directory');
const observationsPath = path.join(projectsDir, projectIds[0], 'observations.jsonl');
const observations = fs.readFileSync(observationsPath, 'utf8').trim().split('\n').filter(Boolean);
assert.ok(observations.length > 0, 'observe.sh should append at least one observation');
const observation = JSON.parse(observations[0]);
assert.strictEqual(observation.output, 'legacy output', 'observe.sh should fall back to legacy tool_output when tool_response is null');
} finally {
cleanupTestDir(homeDir);
cleanupTestDir(projectDir);
}
})) passed++; else failed++;
if (await asyncTest('matches .tsx extension for type checking', async () => {
const testDir = createTestDir();
const testFile = path.join(testDir, 'component.tsx');
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);
@@ -2986,6 +3045,7 @@ async function runTests() {
assert.ok(!runAllSource.includes('execSync'), 'Should not use execSync');
// Verify it shows stderr
assert.ok(runAllSource.includes('stderr'), 'Should handle stderr output');
assert.ok(runAllSource.includes('result.status !== 0'), 'Should treat non-zero child exits as failures');
})
)
passed++;

View File

@@ -10,22 +10,25 @@ const path = require('path');
const fs = require('fs');
const testsDir = __dirname;
const testFiles = [
'lib/utils.test.js',
'lib/package-manager.test.js',
'lib/session-manager.test.js',
'lib/session-aliases.test.js',
'lib/project-detect.test.js',
'lib/resolve-formatter.test.js',
'hooks/hooks.test.js',
'hooks/evaluate-session.test.js',
'hooks/suggest-compact.test.js',
'integration/hooks.test.js',
'ci/validators.test.js',
'scripts/claw.test.js',
'scripts/setup-package-manager.test.js',
'scripts/skill-create-output.test.js'
];
/**
* Discover all *.test.js files under testsDir (relative paths for stable output order).
*/
function discoverTestFiles(dir, baseDir = dir, acc = []) {
const entries = fs.readdirSync(dir, { withFileTypes: true });
for (const e of entries) {
const full = path.join(dir, e.name);
const rel = path.relative(baseDir, full);
if (e.isDirectory()) {
discoverTestFiles(full, baseDir, acc);
} else if (e.isFile() && e.name.endsWith('.test.js')) {
acc.push(rel);
}
}
return acc.sort();
}
const testFiles = discoverTestFiles(testsDir);
const BOX_W = 58; // inner width between ║ delimiters
const boxLine = s => `${s.padEnd(BOX_W)}`;
@@ -68,6 +71,17 @@ for (const testFile of testFiles) {
if (passedMatch) totalPassed += parseInt(passedMatch[1], 10);
if (failedMatch) totalFailed += parseInt(failedMatch[1], 10);
if (result.error) {
console.log(`${testFile} failed to start: ${result.error.message}`);
totalFailed += failedMatch ? 0 : 1;
continue;
}
if (result.status !== 0) {
console.log(`${testFile} exited with status ${result.status}`);
totalFailed += failedMatch ? 0 : 1;
}
}
totalTests = totalPassed + totalFailed;