feat(shell-projection): centralize managed hook command policy (#3450)
* 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
This commit is contained in:
@@ -13,6 +13,7 @@ const {
|
||||
hookCommandNeedsPowerShellCallOperator,
|
||||
formatHookCommandForRuntime,
|
||||
isManagedHookBasename,
|
||||
isManagedHookCommand,
|
||||
projectLocalHookPrefix,
|
||||
projectLegacySettingsHookCommand,
|
||||
projectPortableHookBaseDir,
|
||||
@@ -141,4 +142,48 @@ describe('bug #3439: shell projection module owns managed-hook policy and legacy
|
||||
'/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,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
58
tests/bug-3442-codex-legacy-hooks-json-migration.test.cjs
Normal file
58
tests/bug-3442-codex-legacy-hooks-json-migration.test.cjs
Normal file
@@ -0,0 +1,58 @@
|
||||
'use strict';
|
||||
|
||||
const { describe, test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const path = require('node:path');
|
||||
|
||||
const migration = require(path.join(
|
||||
__dirname,
|
||||
'..',
|
||||
'get-shit-done',
|
||||
'bin',
|
||||
'lib',
|
||||
'installer-migrations',
|
||||
'002-codex-legacy-hooks-json.cjs',
|
||||
));
|
||||
|
||||
describe('bug #3442: codex legacy hooks.json migration consumes shared managed-hook policy', () => {
|
||||
test('plan prunes managed codex hook commands including legacy alias', () => {
|
||||
const configDir = '/Users/me/.codex';
|
||||
const hooksJson = {
|
||||
hooks: [
|
||||
{ command: '"/usr/local/bin/node" "/Users/me/.codex/hooks/gsd-check-update.js"' },
|
||||
{ command: '"/usr/local/bin/node" "/Users/me/.codex/hooks/gsd-update-check.js"' },
|
||||
{ command: '"/usr/local/bin/node" "/Users/me/.codex/hooks/custom-hook.js"' },
|
||||
],
|
||||
};
|
||||
|
||||
const actions = migration.plan({
|
||||
configDir,
|
||||
readJson: () => ({ exists: true, error: null, value: hooksJson }),
|
||||
});
|
||||
|
||||
assert.equal(actions.length, 1);
|
||||
assert.equal(actions[0].type, 'rewrite-json');
|
||||
assert.equal(actions[0].relPath, 'hooks.json');
|
||||
assert.deepEqual(actions[0].value, {
|
||||
hooks: [
|
||||
{ command: '"/usr/local/bin/node" "/Users/me/.codex/hooks/custom-hook.js"' },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test('plan preserves similarly named commands outside the managed hooks directory', () => {
|
||||
const configDir = '/Users/me/.codex';
|
||||
const hooksJson = {
|
||||
hooks: [
|
||||
{ command: '"/usr/local/bin/node" "/tmp/other/hooks/gsd-check-update.js"' },
|
||||
],
|
||||
};
|
||||
|
||||
const actions = migration.plan({
|
||||
configDir,
|
||||
readJson: () => ({ exists: true, error: null, value: hooksJson }),
|
||||
});
|
||||
|
||||
assert.deepEqual(actions, []);
|
||||
});
|
||||
});
|
||||
@@ -17,6 +17,14 @@ const os = require('os');
|
||||
const path = require('path');
|
||||
|
||||
const INSTALL_PATH = path.join(__dirname, '..', 'bin', 'install.js');
|
||||
const PROJECTION_PATH = path.join(
|
||||
__dirname,
|
||||
'..',
|
||||
'get-shit-done',
|
||||
'bin',
|
||||
'lib',
|
||||
'shell-command-projection.cjs',
|
||||
);
|
||||
|
||||
function loadInstaller() {
|
||||
process.env.GSD_TEST_MODE = '1';
|
||||
@@ -34,8 +42,10 @@ function cleanup(dir) {
|
||||
|
||||
describe('installer HOME-relative PATH detection (#2620)', () => {
|
||||
let installer;
|
||||
let projection;
|
||||
before(() => {
|
||||
installer = loadInstaller();
|
||||
projection = require(PROJECTION_PATH);
|
||||
});
|
||||
|
||||
test('homePathCoveredByRc is exported', () => {
|
||||
@@ -257,10 +267,23 @@ describe('installer HOME-relative PATH detection (#2620)', () => {
|
||||
}
|
||||
|
||||
const joined = logs.join('\n');
|
||||
assert.ok(
|
||||
/echo 'export PATH=/.test(joined),
|
||||
`installer should emit absolute export suggestion when rc does not cover globalBin; got:\n${joined}`,
|
||||
assert.equal(
|
||||
typeof projection.projectPersistentPathExportActions,
|
||||
'function',
|
||||
'shell command projection module must export projectPersistentPathExportActions',
|
||||
);
|
||||
const projected = projection.projectPersistentPathExportActions({
|
||||
targetDir: globalBin,
|
||||
platform: process.platform,
|
||||
});
|
||||
assert.ok(Array.isArray(projected.shellActions), 'projected.shellActions must be an array');
|
||||
assert.ok(projected.shellActions.length >= 2, 'expected at least zsh/bash projected actions');
|
||||
for (const action of projected.shellActions) {
|
||||
assert.ok(
|
||||
joined.includes(action.command),
|
||||
`installer should render projected command "${action.command}". Output:\n${joined}`,
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
cleanup(home);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user