mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-04-30 22:13:28 +08:00
fix: route continuous learning observe hooks through node
This commit is contained in:
committed by
Affaan Mustafa
parent
2006d2ee77
commit
3fadc37802
194
tests/hooks/continuous-learning-observe-runner.test.js
Normal file
194
tests/hooks/continuous-learning-observe-runner.test.js
Normal file
@@ -0,0 +1,194 @@
|
||||
/**
|
||||
* Tests for continuous-learning-v2 observe hook dispatch.
|
||||
*
|
||||
* Run with: node tests/hooks/continuous-learning-observe-runner.test.js
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const assert = require('assert');
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
const { spawnSync } = require('child_process');
|
||||
|
||||
const repoRoot = path.resolve(__dirname, '..', '..');
|
||||
const hooksJsonPath = path.join(repoRoot, 'hooks', 'hooks.json');
|
||||
const runWithFlagsPath = path.join(repoRoot, 'scripts', 'hooks', 'run-with-flags.js');
|
||||
const observeRunner = require(path.join(repoRoot, 'scripts', 'hooks', 'observe-runner.js'));
|
||||
|
||||
function test(name, fn) {
|
||||
try {
|
||||
fn();
|
||||
console.log(` \u2713 ${name}`);
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.log(` \u2717 ${name}`);
|
||||
console.log(` Error: ${err.message}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function loadHook(id) {
|
||||
const hookGroups = JSON.parse(fs.readFileSync(hooksJsonPath, 'utf8')).hooks;
|
||||
const hooks = Object.values(hookGroups).flat();
|
||||
const hook = hooks.find(candidate => candidate.id === id);
|
||||
assert.ok(hook, `Expected ${id} in hooks/hooks.json`);
|
||||
assert.ok(Array.isArray(hook.hooks), `Expected ${id} to define hook commands`);
|
||||
assert.strictEqual(hook.hooks.length, 1, `Expected ${id} to have one command`);
|
||||
return hook.hooks[0].command;
|
||||
}
|
||||
|
||||
function withTempPluginRoot(fn) {
|
||||
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-observe-runner-'));
|
||||
try {
|
||||
fs.mkdirSync(path.join(tempRoot, 'scripts', 'hooks'), { recursive: true });
|
||||
fs.mkdirSync(path.join(tempRoot, 'scripts', 'lib'), { recursive: true });
|
||||
fs.copyFileSync(
|
||||
path.join(repoRoot, 'scripts', 'lib', 'hook-flags.js'),
|
||||
path.join(tempRoot, 'scripts', 'lib', 'hook-flags.js')
|
||||
);
|
||||
return fn(tempRoot);
|
||||
} finally {
|
||||
fs.rmSync(tempRoot, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
function withEnv(vars, fn) {
|
||||
const saved = {};
|
||||
for (const [key, value] of Object.entries(vars)) {
|
||||
saved[key] = process.env[key];
|
||||
if (value === undefined) {
|
||||
delete process.env[key];
|
||||
} else {
|
||||
process.env[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
return fn();
|
||||
} finally {
|
||||
for (const [key, value] of Object.entries(saved)) {
|
||||
if (value === undefined) {
|
||||
delete process.env[key];
|
||||
} else {
|
||||
process.env[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function writeFakeObserveScript(tempRoot) {
|
||||
const scriptPath = path.join(tempRoot, 'skills', 'continuous-learning-v2', 'hooks', 'observe.sh');
|
||||
fs.mkdirSync(path.dirname(scriptPath), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
scriptPath,
|
||||
[
|
||||
'#!/usr/bin/env bash',
|
||||
'input="$(cat)"',
|
||||
'printf "phase=%s input=%s root=%s" "$1" "$input" "${CLAUDE_PLUGIN_ROOT:-}"',
|
||||
''
|
||||
].join('\n'),
|
||||
'utf8'
|
||||
);
|
||||
fs.chmodSync(scriptPath, 0o755);
|
||||
}
|
||||
|
||||
function runWithFlags(tempRoot, hookId, relScriptPath, stdin) {
|
||||
return spawnSync(process.execPath, [runWithFlagsPath, hookId, relScriptPath, 'standard,strict'], {
|
||||
input: stdin,
|
||||
encoding: 'utf8',
|
||||
env: {
|
||||
...process.env,
|
||||
CLAUDE_PLUGIN_ROOT: tempRoot,
|
||||
ECC_HOOK_PROFILE: 'standard'
|
||||
},
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
timeout: 10000
|
||||
});
|
||||
}
|
||||
|
||||
function runTests() {
|
||||
console.log('\n=== Testing continuous-learning observe hook dispatch ===\n');
|
||||
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
|
||||
if (test('observe hooks use node-mode runner instead of shell-mode dispatch', () => {
|
||||
for (const hookId of ['pre:observe:continuous-learning', 'post:observe:continuous-learning']) {
|
||||
const command = loadHook(hookId);
|
||||
const phase = hookId.startsWith('pre:') ? 'pre:observe' : 'post:observe';
|
||||
|
||||
assert.ok(command.includes(`node scripts/hooks/run-with-flags.js ${phase} scripts/hooks/observe-runner.js standard,strict`));
|
||||
assert.ok(!command.includes('shell scripts/hooks/run-with-flags-shell.sh'), `${hookId} should not use shell-mode bootstrap`);
|
||||
assert.ok(!command.includes('skills/continuous-learning-v2/hooks/observe.sh'), `${hookId} should not call observe.sh directly from hooks.json`);
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('run-with-flags passes hookId to direct run exports', () => {
|
||||
withTempPluginRoot(tempRoot => {
|
||||
const scriptPath = path.join(tempRoot, 'scripts', 'hooks', 'capture-hook-id.js');
|
||||
fs.writeFileSync(
|
||||
scriptPath,
|
||||
[
|
||||
"'use strict';",
|
||||
'module.exports.run = function run(raw, options) {',
|
||||
' return { stdout: JSON.stringify({ raw, hookId: options.hookId, truncated: options.truncated }) };',
|
||||
'};',
|
||||
''
|
||||
].join('\n'),
|
||||
'utf8'
|
||||
);
|
||||
|
||||
const result = runWithFlags(tempRoot, 'post:observe', 'scripts/hooks/capture-hook-id.js', '{"ok":true}');
|
||||
assert.strictEqual(result.status, 0, result.stderr);
|
||||
const payload = JSON.parse(result.stdout);
|
||||
assert.deepStrictEqual(payload, { raw: '{"ok":true}', hookId: 'post:observe', truncated: false });
|
||||
});
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('observe-runner derives the observe phase from the hook id', () => {
|
||||
assert.strictEqual(observeRunner.getPhaseFromHookId('pre:observe'), 'pre');
|
||||
assert.strictEqual(observeRunner.getPhaseFromHookId('post:observe'), 'post');
|
||||
assert.strictEqual(observeRunner.getPhaseFromHookId('pre:observe:continuous-learning'), 'pre');
|
||||
assert.strictEqual(observeRunner.getPhaseFromHookId('unknown'), null);
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('observe-runner invokes observe.sh with phase, stdin, and plugin root', () => {
|
||||
withTempPluginRoot(tempRoot => {
|
||||
writeFakeObserveScript(tempRoot);
|
||||
const env = fs.existsSync('/bin/sh') ? { BASH: '/bin/sh' } : {};
|
||||
withEnv(env, () => {
|
||||
const output = observeRunner.run('payload', {
|
||||
hookId: 'pre:observe',
|
||||
pluginRoot: tempRoot
|
||||
});
|
||||
|
||||
assert.strictEqual(output.exitCode, 0, output.stderr);
|
||||
assert.strictEqual(output.stdout, `phase=pre input=payload root=${tempRoot}`);
|
||||
});
|
||||
});
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('observe-runner fails open when no shell runtime is available', () => {
|
||||
withTempPluginRoot(tempRoot => {
|
||||
writeFakeObserveScript(tempRoot);
|
||||
withEnv({ BASH: '', PATH: '' }, () => {
|
||||
const output = observeRunner.run('payload', {
|
||||
hookId: 'post:observe',
|
||||
pluginRoot: tempRoot
|
||||
});
|
||||
|
||||
assert.strictEqual(output.exitCode, 0);
|
||||
assert.ok(!Object.prototype.hasOwnProperty.call(output, 'stdout'), 'disabled observe should preserve stdin via runner passthrough');
|
||||
assert.ok(output.stderr.includes('shell runtime unavailable'));
|
||||
});
|
||||
});
|
||||
})) passed++; else failed++;
|
||||
|
||||
console.log(`\nPassed: ${passed}`);
|
||||
console.log(`Failed: ${failed}`);
|
||||
process.exit(failed > 0 ? 1 : 0);
|
||||
}
|
||||
|
||||
runTests();
|
||||
Reference in New Issue
Block a user