mirror of
https://github.com/affaan-m/ECC.git
synced 2026-11-05 07:35:51 +00:00
On native Windows, Claude Code runs each hook as `bash.exe -c "<command>"` through the Windows command line. That extra unescape layer collapses the backslashes in the WSL path conversion of the inline plugin-root resolver, so `.replace(/\\/g,'/')` reaches node as `.replace(/\/g,'/')` and every hook fails with `SyntaxError: missing ) after argument list`. Build the backslash separator with String.fromCharCode(92) and split/join instead of regex escapes, so the resolver contains no backslash. Conversion behaviour is unchanged. Regenerate every embedded copy (hooks.json, codex-hooks.json, commands/*.md) and refresh hooks.metadata.json fingerprints. Add a test that the resolver and each hook `node -e` program are backslash-free, and run the shipped SessionStart command through bash with backslashes collapsed the way Windows does. Fixes #3746
139 lines
5.8 KiB
JavaScript
139 lines
5.8 KiB
JavaScript
'use strict';
|
|
const assert = require('assert');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const { spawnSync } = require('child_process');
|
|
let passed = 0;
|
|
let failed = 0;
|
|
function test(name, run) {
|
|
try { run(); passed++; console.log(`PASS ${name}`); }
|
|
catch (error) { failed++; console.error(`FAIL ${name}: ${error.stack}`); }
|
|
}
|
|
// These tests execute extracted JavaScript with simulated POSIX path/OS/filesystem APIs.
|
|
// Serialized Bash quoting is covered separately by serialized-hook-wsl.test.js.
|
|
test('INLINE_RESOLVE extracted expression converts Windows CLAUDE_CONFIG_DIR on simulated WSL', () => {
|
|
const { INLINE_RESOLVE } = require('../../scripts/lib/resolve-ecc-root');
|
|
// INLINE_RESOLVE is backslash-free, so it embeds verbatim in bash double-quoted `node -e "..."` strings.
|
|
const jsResolveExpr = INLINE_RESOLVE;
|
|
const scriptContent = `
|
|
'use strict';
|
|
const Module = require('module');
|
|
const realOs = require('os');
|
|
const realFs = require('fs');
|
|
const posixPath = require('path').posix;
|
|
const mockedFs = new Proxy(realFs, {
|
|
get(target, prop) {
|
|
if (prop === 'readdirSync') return () => [];
|
|
if (prop === 'readFileSync') {
|
|
return function(fp, enc) {
|
|
if (fp === '/proc/version') return 'Linux version 5.15.0-microsoft-standard-WSL2';
|
|
return target.readFileSync(fp, enc);
|
|
};
|
|
}
|
|
return typeof target[prop] === 'function' ? target[prop].bind(target) : target[prop];
|
|
},
|
|
});
|
|
const mockedOs = new Proxy(realOs, {
|
|
get(target, prop) {
|
|
if (prop === 'platform') return () => 'linux';
|
|
if (prop === 'homedir') return () => '/home/wsluser';
|
|
return typeof target[prop] === 'function' ? target[prop].bind(target) : target[prop];
|
|
},
|
|
});
|
|
const originalLoad = Module._load;
|
|
Module._load = function(request, parent, isMain) {
|
|
if (request === 'fs') return mockedFs;
|
|
if (request === 'os') return mockedOs;
|
|
if (request === 'path') return posixPath;
|
|
if (request.endsWith('/scripts/lib/resolve-ecc-root')) {
|
|
const error = new Error('No installed resolver in this fixture');
|
|
error.code = 'MODULE_NOT_FOUND';
|
|
throw error;
|
|
}
|
|
return originalLoad.call(this, request, parent, isMain);
|
|
};
|
|
process.env.CLAUDE_PLUGIN_ROOT = '';
|
|
process.env.CLAUDE_CONFIG_DIR = 'C:\\\\Users\\\\x\\\\.claude';
|
|
const result = ${jsResolveExpr};
|
|
process.stdout.write(result + '\\n');
|
|
`;
|
|
|
|
const result = spawnSync(process.execPath, ['-e', scriptContent], { encoding: 'utf8', timeout: 10000, env: { PATH: process.env.PATH, SystemRoot: process.env.SystemRoot } });
|
|
assert.strictEqual(result.status, 0, `Resolver subprocess failed: ${result.stderr}`);
|
|
assert.strictEqual(result.stdout.trim(), '/mnt/c/Users/x/.claude');
|
|
});
|
|
|
|
test('codex-hooks.json extracted resolver converts Windows CLAUDE_CONFIG_DIR to /mnt/... on simulated WSL', () => {
|
|
const config = JSON.parse(fs.readFileSync(path.join(__dirname, '..', '..', 'hooks', 'codex-hooks.json'), 'utf8'));
|
|
const hookEvents = config.hooks || {};
|
|
|
|
let hookResolverJS = null;
|
|
outer: for (const entries of Object.values(hookEvents)) {
|
|
if (!Array.isArray(entries)) continue;
|
|
for (const entry of entries) {
|
|
if (!entry.hooks) continue;
|
|
for (const hook of entry.hooks) {
|
|
if (typeof hook.command !== 'string') continue;
|
|
if (hook.command.includes('CLAUDE_CONFIG_DIR') && hook.command.includes('proc/version')) {
|
|
const m = hook.command.match(/const r=(\(function\(\)\{.*?\}\)\(\))/s);
|
|
if (m) { hookResolverJS = m[1]; break outer; }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
assert.ok(hookResolverJS, 'Could not extract inline resolver IIFE from codex-hooks.json');
|
|
const jsResolveExpr = hookResolverJS;
|
|
|
|
const scriptContent = `
|
|
'use strict';
|
|
const Module = require('module');
|
|
const realOs = require('os');
|
|
const realFs = require('fs');
|
|
const posixPath = require('path').posix;
|
|
const mockedFs = new Proxy(realFs, {
|
|
get(target, prop) {
|
|
if (prop === 'readdirSync') return () => [];
|
|
if (prop === 'readFileSync') {
|
|
return function(fp, enc) {
|
|
if (fp === '/proc/version') return 'Linux version 5.15.0-microsoft-standard-WSL2';
|
|
return target.readFileSync(fp, enc);
|
|
};
|
|
}
|
|
return typeof target[prop] === 'function' ? target[prop].bind(target) : target[prop];
|
|
},
|
|
});
|
|
const mockedOs = new Proxy(realOs, {
|
|
get(target, prop) {
|
|
if (prop === 'platform') return () => 'linux';
|
|
if (prop === 'homedir') return () => '/home/wsluser';
|
|
return typeof target[prop] === 'function' ? target[prop].bind(target) : target[prop];
|
|
},
|
|
});
|
|
const originalLoad = Module._load;
|
|
Module._load = function(request, parent, isMain) {
|
|
if (request === 'fs') return mockedFs;
|
|
if (request === 'os') return mockedOs;
|
|
if (request === 'path') return posixPath;
|
|
if (request.endsWith('/scripts/lib/resolve-ecc-root')) {
|
|
const error = new Error('No installed resolver in this fixture');
|
|
error.code = 'MODULE_NOT_FOUND';
|
|
throw error;
|
|
}
|
|
return originalLoad.call(this, request, parent, isMain);
|
|
};
|
|
process.env.CLAUDE_PLUGIN_ROOT = '';
|
|
process.env.CLAUDE_CONFIG_DIR = 'C:\\\\Users\\\\test\\\\.claude';
|
|
const r = ${jsResolveExpr};
|
|
process.stdout.write(r + '\\n');
|
|
`;
|
|
|
|
const result = spawnSync(process.execPath, ['-e', scriptContent], { encoding: 'utf8', timeout: 10000, env: { PATH: process.env.PATH, SystemRoot: process.env.SystemRoot } });
|
|
assert.strictEqual(result.status, 0, `Hook resolver subprocess failed: ${result.stderr}`);
|
|
assert.strictEqual(result.stdout.trim(), '/mnt/c/Users/test/.claude');
|
|
});
|
|
|
|
|
|
console.log(`Passed: ${passed}`);
|
|
console.log(`Failed: ${failed}`);
|
|
process.exitCode = failed ? 1 : 0;
|