Files
ECC/tests/lib/extracted-resolver-wsl.test.js
T
Gaurav Dubey dc9bb9f2d9 fix(hooks): keep inline resolver backslash-free so hooks parse on Windows
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
2026-10-26 18:39:04 +00:00

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;