mirror of
https://github.com/affaan-m/ECC.git
synced 2026-10-15 12:45:29 +02:00
fix(gateguard): extract the arm tree without handing tar a drive letter
materializeTree passed the archive as an absolute path. GNU tar, which Git for Windows bundles and puts first on PATH in Git Bash, reads an -f argument containing a colon as a host:path remote spec and fails with "Cannot connect to C: resolve failed". bsdtar, the tar in system32 that PowerShell resolves, has no remote-spec parsing, so the fault only appeared under a bash-based shell - and there it failed a committed test, not just a dry run. Name the archive relatively and run tar with dest as its cwd, which extracts identically under both tars and makes -C unnecessary. git archive keeps the absolute -o path, which git handles on Windows. Two tests, because the environment-dependent kind of fault needs both: one injects a spawn and asserts no tar argument is absolute or drive-lettered, which holds the invariant on any platform; the other prepends Git's bundled GNU tar to PATH and extracts into a drive-letter destination, so the original failure is reproducible from PowerShell too, where it was previously invisible.
This commit is contained in:
@@ -45,8 +45,8 @@ function run(rawInput) {
|
||||
module.exports = { run };
|
||||
`;
|
||||
|
||||
function git(repoRoot, args) {
|
||||
const result = spawnSync('git', ['-C', repoRoot, ...args], { encoding: 'utf8', shell: false, timeout: 60000 });
|
||||
function git(repoRoot, args, spawn = spawnSync) {
|
||||
const result = spawn('git', ['-C', repoRoot, ...args], { encoding: 'utf8', shell: false, timeout: 60000 });
|
||||
if (result.status !== 0 || result.error) throw new Error(`git ${args[0]} failed: ${String(result.stderr || result.error).trim()}`);
|
||||
return result.stdout;
|
||||
}
|
||||
@@ -57,12 +57,18 @@ function resolveRef(repoRoot, ref) {
|
||||
}
|
||||
|
||||
/** Extracts scripts/ at a commit into dest and adds the arm wrapper; returns dest. */
|
||||
function materializeTree(repoRoot, sha, dest) {
|
||||
function materializeTree(repoRoot, sha, dest, spawn = spawnSync) {
|
||||
fs.mkdirSync(dest, { recursive: true });
|
||||
const tar = path.join(dest, 'scripts.tar');
|
||||
git(repoRoot, ['archive', '--format=tar', '-o', tar, sha, 'scripts']);
|
||||
const extract = spawnSync('tar', ['-xf', tar, '-C', dest], { encoding: 'utf8', shell: false, timeout: 60000 });
|
||||
fs.rmSync(tar, { force: true });
|
||||
const tarName = 'scripts.tar';
|
||||
git(repoRoot, ['archive', '--format=tar', '-o', path.join(dest, tarName), sha, 'scripts'], spawn);
|
||||
// Name the archive relatively and run tar with dest as cwd, so no argument carries a
|
||||
// "C:\..." drive letter. GNU tar (Git Bash / MSYS on Windows) reads an archive argument
|
||||
// containing a colon as a "host:path" remote spec and dies with "Cannot connect to C:
|
||||
// resolve failed"; bsdtar (C:\Windows\system32\tar.exe) does not, which is why the
|
||||
// absolute path only broke under GNU tar. A relative -f plus cwd extracts identically
|
||||
// under both and makes -C unnecessary.
|
||||
const extract = spawn('tar', ['-xf', tarName], { cwd: dest, encoding: 'utf8', shell: false, timeout: 60000 });
|
||||
fs.rmSync(path.join(dest, tarName), { force: true });
|
||||
if (extract.status !== 0 || extract.error) throw new Error(`tar failed: ${String(extract.stderr || extract.error).trim()}`);
|
||||
fs.copyFileSync(path.join(__dirname, 'arm-patch.js'), path.join(dest, 'arm-patch.js'));
|
||||
fs.writeFileSync(path.join(dest, ...ARM_HOOK.split('/')), WRAPPER);
|
||||
|
||||
@@ -52,6 +52,17 @@ function gitAvailable() {
|
||||
return result.status === 0 && !result.error;
|
||||
}
|
||||
|
||||
/** Directory of the GNU tar that Git for Windows bundles, or null. GNU tar reads `C:\...` as a remote host:path. */
|
||||
function gitBundledGnuTarDir() {
|
||||
if (process.platform !== 'win32') return null;
|
||||
const execPath = spawnSync('git', ['--exec-path'], { encoding: 'utf8' });
|
||||
if (execPath.status !== 0 || execPath.error) return null;
|
||||
// <git>/mingw64/libexec/git-core -> <git>/usr/bin
|
||||
const dir = path.resolve(execPath.stdout.trim(), '..', '..', '..', 'usr', 'bin');
|
||||
const version = spawnSync(path.join(dir, 'tar.exe'), ['--version'], { encoding: 'utf8' });
|
||||
return version.status === 0 && /GNU tar/.test(version.stdout) ? dir : null;
|
||||
}
|
||||
|
||||
console.log('\n=== Testing gateguard-effectiveness ===\n');
|
||||
|
||||
const scenarios = lib.loadScenarios();
|
||||
@@ -227,6 +238,49 @@ test('arguments require an output folder, a model and the real-provider opt-in',
|
||||
assert.deepStrictEqual(options.arms, arms.DEFAULT_ARMS);
|
||||
});
|
||||
|
||||
test('materializeTree hands tar no host path, only a relative archive name run inside dest', () => {
|
||||
const work = tempDir('gg-eff-tarargs-');
|
||||
try {
|
||||
const dest = path.join(work, 'tree');
|
||||
const calls = [];
|
||||
const spawn = (command, args, options) => {
|
||||
calls.push({ command, args, cwd: options.cwd });
|
||||
if (command === 'git') fs.writeFileSync(args[args.indexOf('-o') + 1], '');
|
||||
if (command === 'tar') fs.mkdirSync(path.join(dest, 'scripts', 'hooks'), { recursive: true });
|
||||
return { status: 0, stdout: '', stderr: '' };
|
||||
};
|
||||
arms.materializeTree(ROOT, 'deadbeef', dest, spawn);
|
||||
const tar = calls.find(call => call.command === 'tar');
|
||||
assert.strictEqual(tar.cwd, dest);
|
||||
for (const arg of tar.args) {
|
||||
assert.ok(!path.isAbsolute(arg) && !/^[A-Za-z]:/.test(arg), `tar argument looks like a host path: ${arg}`);
|
||||
}
|
||||
} finally {
|
||||
fs.rmSync(work, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
const gnuTarDir = gitAvailable() ? gitBundledGnuTarDir() : null;
|
||||
if (gnuTarDir) {
|
||||
test('materializeTree extracts into a drive-letter destination when GNU tar is first on PATH', () => {
|
||||
const work = tempDir('gg-eff-tar-');
|
||||
const savedPath = process.env.PATH;
|
||||
try {
|
||||
const dest = path.join(work, 'tree');
|
||||
assert.match(dest, /^[A-Za-z]:\\/);
|
||||
process.env.PATH = `${gnuTarDir}${path.delimiter}${savedPath}`;
|
||||
const tree = arms.materializeTree(ROOT, arms.resolveRef(ROOT, 'HEAD'), dest);
|
||||
assert.strictEqual(tree, dest);
|
||||
assert.ok(fs.existsSync(path.join(dest, 'scripts', 'hooks', 'gateguard-fact-force.js')));
|
||||
assert.ok(fs.existsSync(path.join(dest, ...arms.ARM_HOOK.split('/'))));
|
||||
assert.ok(!fs.existsSync(path.join(dest, 'scripts.tar')), 'the archive is removed after extraction');
|
||||
} finally {
|
||||
process.env.PATH = savedPath;
|
||||
fs.rmSync(work, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (gitAvailable()) {
|
||||
test('a trial wires the arm hook, counts the denial and grades the result (test double for Claude)', () => {
|
||||
const work = tempDir('gg-eff-trial-');
|
||||
|
||||
Reference in New Issue
Block a user