fix(gemini): make Windows hooks and agent tools valid

This commit is contained in:
Tom Boucher
2026-05-10 17:39:06 -04:00
committed by GitHub
parent ab84538712
commit 19295f5ab6
6 changed files with 145 additions and 19 deletions

View File

@@ -103,6 +103,26 @@ describe('Bug #2979: buildHookCommand for .js hooks emits absolute node runner',
});
});
describe('Bug #3362: Windows PowerShell hook commands use the call operator', () => {
test('global install: .js hook command starts with & so quoted runners execute in PowerShell', () => {
const cmd = buildHookCommand('C:/Program Files/Gemini/.gemini', 'gsd-check-update.js', {
platform: 'win32',
});
assert.ok(cmd.startsWith('& '), `PowerShell commands need call operator, got: ${cmd}`);
assert.ok(cmd.includes('"C:/Program Files/Gemini/.gemini/hooks/gsd-check-update.js"'));
});
test('portable install: .js hook command also uses & on Windows PowerShell', () => {
const home = require('node:os').homedir().replace(/\\/g, '/');
const cmd = buildHookCommand(`${home}/.gemini`, 'gsd-check-update.js', {
portableHooks: true,
platform: 'win32',
});
assert.ok(cmd.startsWith('& '), `PowerShell commands need call operator, got: ${cmd}`);
assert.equal(parseHookCommand(cmd.slice(2)).hookPath, '$HOME/.gemini/hooks/gsd-check-update.js');
});
});
describe('Bug #2979: buildHookCommand for .sh hooks still uses bare "bash" (POSIX std PATH always has /bin)', () => {
test('.sh hook runner is exactly "bash" — bash is in /usr/bin:/bin and resolves under minimal PATH', () => {
const cmd = buildHookCommand('/tmp/.claude', 'gsd-session-state.sh');
@@ -154,6 +174,55 @@ describe('Bug #2979 (#3002 CR): rewriteLegacyManagedNodeHookCommands rewrites ba
assert.equal(settings.hooks.SessionStart[0].hooks[0].command, before);
});
test('adds PowerShell call operator to existing quoted managed hooks on Windows', () => {
const settings = {
hooks: {
SessionStart: [{
hooks: [{ type: 'command', command: '"/usr/local/bin/node" "C:/Program Files/Gemini/.gemini/hooks/gsd-check-update.js"' }],
}],
},
};
const runner = '"/usr/local/bin/node"';
const changed = rewriteLegacyManagedNodeHookCommands(settings, runner, { platform: 'win32' });
assert.equal(changed, true);
assert.equal(
settings.hooks.SessionStart[0].hooks[0].command,
'& "/usr/local/bin/node" "C:/Program Files/Gemini/.gemini/hooks/gsd-check-update.js"',
);
});
test('does NOT double-prefix managed hooks that already use the PowerShell call operator', () => {
const settings = {
hooks: {
SessionStart: [{
hooks: [{ type: 'command', command: '& "/usr/local/bin/node" "C:/Program Files/Gemini/.gemini/hooks/gsd-check-update.js"' }],
}],
},
};
const runner = '"/usr/local/bin/node"';
const before = settings.hooks.SessionStart[0].hooks[0].command;
const changed = rewriteLegacyManagedNodeHookCommands(settings, runner, { platform: 'win32' });
assert.equal(changed, false);
assert.equal(settings.hooks.SessionStart[0].hooks[0].command, before);
});
test('rewrites PowerShell bare-node managed hooks to absolute runner without dropping &', () => {
const settings = {
hooks: {
SessionStart: [{
hooks: [{ type: 'command', command: '& node "C:/Users/me/.gemini/hooks/gsd-check-update.js"' }],
}],
},
};
const runner = '"/usr/local/bin/node"';
const changed = rewriteLegacyManagedNodeHookCommands(settings, runner, { platform: 'win32' });
assert.equal(changed, true);
assert.equal(
settings.hooks.SessionStart[0].hooks[0].command,
'& "/usr/local/bin/node" "C:/Users/me/.gemini/hooks/gsd-check-update.js"',
);
});
test('does NOT touch user-authored bare-node hooks (filename not in managed allowlist)', () => {
const settings = {
hooks: {