mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-06-11 02:33:10 +08:00
fix(continuous-learning-v2): accept claude-vscode as valid entrypoint (#2134)
The observe.sh Layer 1 entrypoint guard short-circuits with exit 0 when
CLAUDE_CODE_ENTRYPOINT is not in {cli, sdk-ts, claude-desktop}. Claude
Code's VS Code extension sets CLAUDE_CODE_ENTRYPOINT=claude-vscode, so
VS Code users see no observations recorded — observations.jsonl never
gets created and the instinct pipeline stays empty.
Add claude-vscode to the allowlist, mirroring the precedent in #1522
which added claude-desktop the same way.
Add a regression test that spawns observe.sh under bash -x for each
allowed entrypoint (cli, sdk-ts, claude-desktop, claude-vscode) and
each denied entrypoint (unknown-host, claude-cody, mcp), asserting
that allowed entrypoints reach Layer 2's ECC_HOOK_PROFILE check while
denied entrypoints stop at Layer 1.
Fixes #2102
This commit is contained in:
150
tests/hooks/observe-entrypoint-allowlist.test.js
Normal file
150
tests/hooks/observe-entrypoint-allowlist.test.js
Normal file
@@ -0,0 +1,150 @@
|
||||
/**
|
||||
* Regression test for observe.sh Layer 1 entrypoint allowlist (#2102).
|
||||
*
|
||||
* The continuous-learning-v2 observe hook short-circuits with exit 0 when
|
||||
* CLAUDE_CODE_ENTRYPOINT is not in the allowlist. Before #2102 the allowlist
|
||||
* was {cli, sdk-ts, claude-desktop} and Claude Code's VS Code extension
|
||||
* (CLAUDE_CODE_ENTRYPOINT=claude-vscode) was silently dropped, so VS Code
|
||||
* users saw no observations recorded.
|
||||
*
|
||||
* This test pins the allowlist by spawning observe.sh under `bash -x` for
|
||||
* each entrypoint value and asserting that allowed entrypoints reach
|
||||
* Layer 2 (the ECC_HOOK_PROFILE check) while denied entrypoints stop at
|
||||
* Layer 1's `exit 0`. We force Layer 2 to short-circuit via
|
||||
* ECC_HOOK_PROFILE=minimal so the test is fast and side-effect-free
|
||||
* regardless of whether downstream layers find python3 / write state.
|
||||
*
|
||||
* Run with: node tests/hooks/observe-entrypoint-allowlist.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 observeShPath = path.join(
|
||||
repoRoot,
|
||||
'skills',
|
||||
'continuous-learning-v2',
|
||||
'hooks',
|
||||
'observe.sh'
|
||||
);
|
||||
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
|
||||
function test(name, fn) {
|
||||
try {
|
||||
fn();
|
||||
console.log(` ✓ ${name}`);
|
||||
passed++;
|
||||
} catch (err) {
|
||||
console.log(` ✗ ${name}`);
|
||||
console.log(` Error: ${err.message}`);
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
|
||||
function makeTempHome() {
|
||||
return fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-observe-allowlist-'));
|
||||
}
|
||||
|
||||
function cleanup(dir) {
|
||||
try {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Spawn observe.sh under `bash -x` with a given CLAUDE_CODE_ENTRYPOINT.
|
||||
* Layer 2 is forced to short-circuit (ECC_HOOK_PROFILE=minimal) so the only
|
||||
* observable difference between an allowed entrypoint and a denied one is
|
||||
* whether the bash trace records the Layer 2 check at all.
|
||||
*/
|
||||
function runObserve(entrypoint) {
|
||||
const home = makeTempHome();
|
||||
try {
|
||||
const hookInput = JSON.stringify({
|
||||
tool_name: 'Read',
|
||||
tool_input: { file_path: '/tmp/test.txt' },
|
||||
session_id: 'allowlist-test-session',
|
||||
cwd: home
|
||||
});
|
||||
|
||||
return spawnSync('bash', ['-x', observeShPath, 'post'], {
|
||||
input: hookInput,
|
||||
env: {
|
||||
...process.env,
|
||||
HOME: home,
|
||||
CLAUDE_CODE_ENTRYPOINT: entrypoint,
|
||||
ECC_HOOK_PROFILE: 'minimal',
|
||||
ECC_SKIP_OBSERVE: '0',
|
||||
CLAUDE_PROJECT_DIR: home
|
||||
},
|
||||
timeout: 5000,
|
||||
encoding: 'utf8'
|
||||
});
|
||||
} finally {
|
||||
cleanup(home);
|
||||
}
|
||||
}
|
||||
|
||||
// `bash -x` expands variables before printing trace lines. Layer 2's
|
||||
// `[ "${ECC_HOOK_PROFILE:-standard}" = "minimal" ] && exit 0` therefore
|
||||
// shows up as a literal `[ minimal = minimal ]` line on stderr, but only
|
||||
// when Layer 1's case statement let the entrypoint pass through. We use
|
||||
// that line as the discriminator between allowed and denied paths.
|
||||
const LAYER2_TRACE_MARKER = "'[' minimal = minimal ']'";
|
||||
|
||||
function assertAllowedReachesLayer2(entrypoint) {
|
||||
const result = runObserve(entrypoint);
|
||||
assert.strictEqual(
|
||||
result.status,
|
||||
0,
|
||||
`observe.sh exit 0 expected for ${entrypoint}, got ${result.status}: ${result.stderr}`
|
||||
);
|
||||
assert.ok(
|
||||
result.stderr.includes(LAYER2_TRACE_MARKER),
|
||||
`entrypoint ${entrypoint} should reach Layer 2 (expected ${LAYER2_TRACE_MARKER} in trace); stderr tail: ${result.stderr.slice(-400)}`
|
||||
);
|
||||
}
|
||||
|
||||
function assertDeniedStopsAtLayer1(entrypoint) {
|
||||
const result = runObserve(entrypoint);
|
||||
assert.strictEqual(
|
||||
result.status,
|
||||
0,
|
||||
`observe.sh exit 0 expected for ${entrypoint}, got ${result.status}: ${result.stderr}`
|
||||
);
|
||||
assert.ok(
|
||||
!result.stderr.includes(LAYER2_TRACE_MARKER),
|
||||
`entrypoint ${entrypoint} should stop at Layer 1 (Layer 2 marker ${LAYER2_TRACE_MARKER} should NOT appear); stderr tail: ${result.stderr.slice(-400)}`
|
||||
);
|
||||
}
|
||||
|
||||
console.log('\n=== observe.sh Layer 1 entrypoint allowlist (#2102) ===\n');
|
||||
|
||||
const ALLOWED = ['cli', 'sdk-ts', 'claude-desktop', 'claude-vscode'];
|
||||
const DENIED = ['unknown-host', 'claude-cody', 'mcp'];
|
||||
|
||||
for (const entry of ALLOWED) {
|
||||
test(`allowed entrypoint ${entry} reaches Layer 2`, () => {
|
||||
assertAllowedReachesLayer2(entry);
|
||||
});
|
||||
}
|
||||
|
||||
for (const entry of DENIED) {
|
||||
test(`denied entrypoint ${entry} stops at Layer 1`, () => {
|
||||
assertDeniedStopsAtLayer1(entry);
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`\nPassed: ${passed}`);
|
||||
console.log(`Failed: ${failed}`);
|
||||
process.exit(failed > 0 ? 1 : 0);
|
||||
Reference in New Issue
Block a user