From 4de776341ee553144884fcac7945d2acb194568c Mon Sep 17 00:00:00 2001 From: Affaan Mustafa Date: Tue, 10 Mar 2026 19:14:07 -0700 Subject: [PATCH] fix: handle null tool_response fallback --- .../continuous-learning-v2/hooks/observe.sh | 4 +- tests/hooks/hooks.test.js | 60 +++++++++++++++++++ 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/skills/continuous-learning-v2/hooks/observe.sh b/skills/continuous-learning-v2/hooks/observe.sh index 653d56a3..33ec6f04 100755 --- a/skills/continuous-learning-v2/hooks/observe.sh +++ b/skills/continuous-learning-v2/hooks/observe.sh @@ -124,7 +124,9 @@ try: # Extract fields - Claude Code hook format tool_name = data.get("tool_name", data.get("tool", "unknown")) tool_input = data.get("tool_input", data.get("input", {})) - tool_output = data.get("tool_response", data.get("tool_output", data.get("output", ""))) + tool_output = data.get("tool_response") + if tool_output is None: + tool_output = data.get("tool_output", data.get("output", "")) session_id = data.get("session_id", "unknown") tool_use_id = data.get("tool_use_id", "") cwd = data.get("cwd", "") diff --git a/tests/hooks/hooks.test.js b/tests/hooks/hooks.test.js index a5ecaaa0..e13e8193 100644 --- a/tests/hooks/hooks.test.js +++ b/tests/hooks/hooks.test.js @@ -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()}`); @@ -1777,6 +1800,43 @@ async function runTests() { assert.ok(stdout.trim().length > 0, 'CLV2_PYTHON_CMD should export a resolved interpreter path'); })) passed++; else failed++; + 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');