* feat(shell-projection): route persistent PATH hints through projection seam * refactor(shell-projection): unify managed hook command policy * fix(install): guard malformed settings hooks during uninstall * chore(changeset): add fragment for pr 3450 * fix(install): guard malformed settings hook entries
190 lines
6.7 KiB
JavaScript
190 lines
6.7 KiB
JavaScript
'use strict';
|
|
|
|
process.env.GSD_TEST_MODE = '1';
|
|
|
|
const { describe, test } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const path = require('node:path');
|
|
|
|
const projection = require(path.join(__dirname, '..', 'get-shit-done', 'bin', 'lib', 'shell-command-projection.cjs'));
|
|
const install = require(path.join(__dirname, '..', 'bin', 'install.js'));
|
|
|
|
const {
|
|
hookCommandNeedsPowerShellCallOperator,
|
|
formatHookCommandForRuntime,
|
|
isManagedHookBasename,
|
|
isManagedHookCommand,
|
|
projectLocalHookPrefix,
|
|
projectLegacySettingsHookCommand,
|
|
projectPortableHookBaseDir,
|
|
} = projection;
|
|
const { buildHookCommand, rewriteLegacyManagedNodeHookCommands } = install;
|
|
|
|
describe('bug #3413: Shell Command Projection Module uses runtime-aware hook policy', () => {
|
|
test('Gemini on Windows requires PowerShell call operator', () => {
|
|
assert.equal(
|
|
hookCommandNeedsPowerShellCallOperator({ platform: 'win32', runtime: 'gemini' }),
|
|
true,
|
|
);
|
|
assert.equal(
|
|
formatHookCommandForRuntime('"C:/node.exe" "C:/hook.js"', { platform: 'win32', runtime: 'gemini' }),
|
|
'& "C:/node.exe" "C:/hook.js"',
|
|
);
|
|
});
|
|
|
|
test('Claude on Windows stays shell-neutral', () => {
|
|
assert.equal(
|
|
hookCommandNeedsPowerShellCallOperator({ platform: 'win32', runtime: 'claude' }),
|
|
false,
|
|
);
|
|
assert.equal(
|
|
formatHookCommandForRuntime('"C:/node.exe" "C:/hook.js"', { platform: 'win32', runtime: 'claude' }),
|
|
'"C:/node.exe" "C:/hook.js"',
|
|
);
|
|
});
|
|
|
|
test('runtime omitted stays conservative (no PowerShell prefix)', () => {
|
|
assert.equal(
|
|
formatHookCommandForRuntime('"C:/node.exe" "C:/hook.js"', { platform: 'win32' }),
|
|
'"C:/node.exe" "C:/hook.js"',
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('bug #3413: installer hook surfaces consume runtime-aware projection', () => {
|
|
test('buildHookCommand emits shell-neutral Claude hook command on Windows', () => {
|
|
const cmd = buildHookCommand('C:/Users/me/.claude', 'gsd-check-update.js', {
|
|
platform: 'win32',
|
|
runtime: 'claude',
|
|
});
|
|
assert.equal(cmd.startsWith('& '), false, `Claude hook command must not use PowerShell prefix: ${cmd}`);
|
|
});
|
|
|
|
test('rewriteLegacyManagedNodeHookCommands removes stale PowerShell prefix for Claude on Windows', () => {
|
|
const settings = {
|
|
hooks: {
|
|
SessionStart: [{
|
|
hooks: [{ type: 'command', command: '& "/usr/local/bin/node" "C:/Users/me/.claude/hooks/gsd-check-update.js"' }],
|
|
}],
|
|
},
|
|
};
|
|
const changed = rewriteLegacyManagedNodeHookCommands(settings, '"/usr/local/bin/node"', {
|
|
platform: 'win32',
|
|
runtime: 'claude',
|
|
});
|
|
assert.equal(changed, true);
|
|
assert.equal(
|
|
settings.hooks.SessionStart[0].hooks[0].command,
|
|
'"/usr/local/bin/node" "C:/Users/me/.claude/hooks/gsd-check-update.js"',
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('bug #3439: shell projection module owns managed-hook policy and legacy rewrite projection', () => {
|
|
test('isManagedHookBasename is surface-aware', () => {
|
|
assert.equal(isManagedHookBasename('/x/hooks/gsd-check-update.js', { surface: 'settings-json' }), true);
|
|
assert.equal(isManagedHookBasename('/x/hooks/gsd-statusline.js', { surface: 'settings-json' }), true);
|
|
assert.equal(isManagedHookBasename('/x/hooks/gsd-statusline.js', { surface: 'codex-toml' }), false);
|
|
assert.equal(isManagedHookBasename('/x/hooks/custom-hook.js', { surface: 'settings-json' }), false);
|
|
});
|
|
|
|
test('projectLegacySettingsHookCommand preserves non-Windows script token shape', () => {
|
|
const command = projectLegacySettingsHookCommand({
|
|
absoluteRunner: '"/usr/local/bin/node"',
|
|
scriptPath: '/x/hooks/gsd-statusline.js',
|
|
scriptToken: "'/x/hooks/gsd-statusline.js'",
|
|
platform: 'linux',
|
|
runtime: 'claude',
|
|
});
|
|
assert.equal(command, `"/usr/local/bin/node" '/x/hooks/gsd-statusline.js'`);
|
|
});
|
|
|
|
test('projectLegacySettingsHookCommand normalizes Windows managed paths and runtime wrapper policy', () => {
|
|
const command = projectLegacySettingsHookCommand({
|
|
absoluteRunner: '"C:/nvm4w/nodejs/node.exe"',
|
|
scriptPath: 'C:\\Users\\me\\.gemini\\hooks\\gsd-prompt-guard.js',
|
|
scriptToken: "'C:\\Users\\me\\.gemini\\hooks\\gsd-prompt-guard.js'",
|
|
platform: 'win32',
|
|
runtime: 'gemini',
|
|
});
|
|
assert.equal(command, '& "C:/nvm4w/nodejs/node.exe" "C:/Users/me/.gemini/hooks/gsd-prompt-guard.js"');
|
|
});
|
|
|
|
test('projectLocalHookPrefix centralizes runtime-specific project-dir interpolation policy', () => {
|
|
assert.equal(projectLocalHookPrefix({ runtime: 'gemini', dirName: '.gemini' }), '.gemini');
|
|
assert.equal(projectLocalHookPrefix({ runtime: 'antigravity', dirName: '.agent' }), '.agent');
|
|
assert.equal(
|
|
projectLocalHookPrefix({ runtime: 'claude', dirName: '.claude' }),
|
|
'"$CLAUDE_PROJECT_DIR"/.claude',
|
|
);
|
|
});
|
|
|
|
test('projectPortableHookBaseDir centralizes $HOME interpolation policy', () => {
|
|
assert.equal(
|
|
projectPortableHookBaseDir({
|
|
configDir: '/Users/me/.claude',
|
|
homeDir: '/Users/me',
|
|
}),
|
|
'$HOME/.claude',
|
|
);
|
|
assert.equal(
|
|
projectPortableHookBaseDir({
|
|
configDir: 'C:\\Users\\me\\.claude',
|
|
homeDir: 'C:\\Users\\me',
|
|
}),
|
|
'$HOME/.claude',
|
|
);
|
|
assert.equal(
|
|
projectPortableHookBaseDir({
|
|
configDir: '/opt/custom/.claude',
|
|
homeDir: '/Users/me',
|
|
}),
|
|
'/opt/custom/.claude',
|
|
);
|
|
});
|
|
|
|
test('isManagedHookCommand classifies managed settings hooks and leaves user commands untouched', () => {
|
|
assert.equal(
|
|
isManagedHookCommand('"/usr/local/bin/node" "/Users/me/.claude/hooks/gsd-statusline.js"', {
|
|
surface: 'settings-json',
|
|
}),
|
|
true,
|
|
);
|
|
assert.equal(
|
|
isManagedHookCommand('"C:/Program Files/Git/bin/bash.exe" "C:/Users/me/.claude/hooks/gsd-session-state.sh"', {
|
|
surface: 'settings-json',
|
|
}),
|
|
true,
|
|
);
|
|
assert.equal(
|
|
isManagedHookCommand('bash /Users/me/.claude/hooks/custom-lint.sh', {
|
|
surface: 'settings-json',
|
|
}),
|
|
false,
|
|
);
|
|
});
|
|
|
|
test('isManagedHookCommand supports codex surfaces and optional legacy alias matching', () => {
|
|
const command = '"/usr/local/bin/node" "/Users/me/.codex/hooks/gsd-check-update.js"';
|
|
assert.equal(
|
|
isManagedHookCommand(command, {
|
|
surface: 'codex-toml',
|
|
}),
|
|
true,
|
|
);
|
|
assert.equal(
|
|
isManagedHookCommand('"/usr/local/bin/node" "/Users/me/.codex/hooks/gsd-update-check.js"', {
|
|
surface: 'codex-toml',
|
|
}),
|
|
false,
|
|
);
|
|
assert.equal(
|
|
isManagedHookCommand('"/usr/local/bin/node" "/Users/me/.codex/hooks/gsd-update-check.js"', {
|
|
surface: 'codex-toml',
|
|
includeLegacyAliases: true,
|
|
}),
|
|
true,
|
|
);
|
|
});
|
|
});
|