test(codex): align hook assertions with hooks.json
This commit is contained in:
@@ -81,6 +81,23 @@ function writeCodexConfig(codexHome, content) {
|
||||
fs.writeFileSync(path.join(codexHome, 'config.toml'), content, 'utf8');
|
||||
}
|
||||
|
||||
function readHooksSessionStartCommands(codexHome) {
|
||||
const hooksPath = path.join(codexHome, 'hooks.json');
|
||||
if (!fs.existsSync(hooksPath)) return [];
|
||||
const raw = fs.readFileSync(hooksPath, 'utf8').trim();
|
||||
if (!raw) return [];
|
||||
const parsed = JSON.parse(raw);
|
||||
const table = (parsed.hooks && typeof parsed.hooks === 'object' && !Array.isArray(parsed.hooks))
|
||||
? parsed.hooks
|
||||
: parsed;
|
||||
const sessionStart = Array.isArray(table.SessionStart) ? table.SessionStart : [];
|
||||
return sessionStart.flatMap((entry) =>
|
||||
(Array.isArray(entry?.hooks) ? entry.hooks : [])
|
||||
.map((hook) => hook && hook.command)
|
||||
.filter((cmd) => typeof cmd === 'string')
|
||||
);
|
||||
}
|
||||
|
||||
function countMatches(content, pattern) {
|
||||
return (content.match(pattern) || []).length;
|
||||
}
|
||||
@@ -1409,14 +1426,19 @@ describe('Codex install hook configuration (e2e)', () => {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test('Codex install copies hook file that is referenced in config.toml (#2153)', () => {
|
||||
test('Codex install copies hook file that is referenced in hooks.json (#2153)', () => {
|
||||
// Regression test: Codex install writes gsd-check-update hook reference into
|
||||
// config.toml but must also copy the hook file to ~/$CODEX_HOME/hooks/
|
||||
// hooks.json and must also copy the hook file to ~/$CODEX_HOME/hooks/
|
||||
runCodexInstall(codexHome);
|
||||
|
||||
const configContent = readCodexConfig(codexHome);
|
||||
// config.toml must reference the hook
|
||||
assert.ok(configContent.includes('gsd-check-update.js'), 'config.toml references gsd-check-update.js');
|
||||
assert.ok(!configContent.includes('gsd-check-update.js'), 'config.toml does not carry managed SessionStart hooks');
|
||||
const hooksJsonCommands = readHooksSessionStartCommands(codexHome);
|
||||
assert.equal(
|
||||
hooksJsonCommands.some((cmd) => cmd.includes('gsd-check-update.js')),
|
||||
true,
|
||||
'hooks.json references gsd-check-update.js'
|
||||
);
|
||||
// The hook file must physically exist at the referenced path
|
||||
const hookFile = path.join(codexHome, 'hooks', 'gsd-check-update.js');
|
||||
assert.ok(
|
||||
@@ -1430,11 +1452,8 @@ describe('Codex install hook configuration (e2e)', () => {
|
||||
|
||||
const content = readCodexConfig(codexHome);
|
||||
assert.ok(content.includes('[features]\ncodex_hooks = true\n'), 'writes codex_hooks feature');
|
||||
// Codex 0.124.0+ nested schema: [[hooks.SessionStart]] + [[hooks.SessionStart.hooks]]
|
||||
const parsed = parseTomlToObject(content);
|
||||
assert.ok(parsed.hooks && Array.isArray(parsed.hooks.SessionStart), 'writes [[hooks.SessionStart]] AoT');
|
||||
assert.ok(Array.isArray(parsed.hooks.SessionStart[0].hooks), 'writes [[hooks.SessionStart.hooks]] sub-table');
|
||||
assert.strictEqual(parsed.hooks.SessionStart[0].hooks[0].type, 'command', 'handler type is "command"');
|
||||
assert.ok(!parsed.hooks || !Array.isArray(parsed.hooks.SessionStart), 'config.toml does not carry managed SessionStart hooks');
|
||||
// #3017: handler command now uses the absolute Node binary path so
|
||||
// GUI/minimal-PATH runtimes can resolve it. The shape is
|
||||
// "<absolute-node-path>" "<hook-path>"
|
||||
@@ -1444,14 +1463,11 @@ describe('Codex install hook configuration (e2e)', () => {
|
||||
const expectedRunner = JSON.parse(resolveNodeRunner());
|
||||
const expectedHookPath = path.join(codexHome, 'hooks', 'gsd-check-update.js').replace(/\\/g, '/');
|
||||
const expectedCommand = `"${expectedRunner}" "${expectedHookPath}"`;
|
||||
assert.strictEqual(
|
||||
parsed.hooks.SessionStart[0].hooks[0].command,
|
||||
expectedCommand,
|
||||
'handler command must use absolute node runner pointing at gsd-check-update.js (#3017)'
|
||||
);
|
||||
assert.ok(!Array.isArray(parsed.hooks), 'no flat [[hooks]] AoT emitted');
|
||||
const hooksJsonCommands = readHooksSessionStartCommands(codexHome);
|
||||
const gsdCommands = hooksJsonCommands.filter((cmd) => cmd.includes('gsd-check-update.js'));
|
||||
assert.strictEqual(gsdCommands.length, 1, 'writes one GSD update hook in hooks.json');
|
||||
assert.strictEqual(gsdCommands[0], expectedCommand, 'handler command must use absolute node runner pointing at gsd-check-update.js (#3017)');
|
||||
assert.strictEqual(countMatches(content, /^codex_hooks = true$/gm), 1, 'writes one codex_hooks key');
|
||||
assert.strictEqual(countMatches(content, /gsd-check-update\.js/g), 1, 'writes one GSD update hook');
|
||||
assertNoDraftRootKeys(content);
|
||||
assertUsesOnlyEol(content, '\n');
|
||||
});
|
||||
@@ -1533,7 +1549,9 @@ describe('Codex install hook configuration (e2e)', () => {
|
||||
assert.ok(content.includes('# user comment'), 'preserves user comment');
|
||||
assert.ok(content.includes('[model]\nname = "o3"'), 'preserves model section');
|
||||
assert.ok(content.includes('command = "echo custom"'), 'preserves custom hook');
|
||||
assert.strictEqual(countMatches(content, /gsd-check-update\.js/g), 1, 'adds one GSD update hook');
|
||||
const hooksJsonCommands = readHooksSessionStartCommands(codexHome);
|
||||
const gsdEntries = hooksJsonCommands.filter((cmd) => cmd.includes('gsd-check-update.js'));
|
||||
assert.strictEqual(gsdEntries.length, 1, 'adds one GSD update hook in hooks.json');
|
||||
assertNoDraftRootKeys(content);
|
||||
});
|
||||
|
||||
@@ -1670,7 +1688,9 @@ describe('Codex install hook configuration (e2e)', () => {
|
||||
assert.ok(!content.includes('codex_hooks = false'), 'removes false codex_hooks value');
|
||||
assert.ok(content.includes('other_feature = true'), 'preserves other feature keys');
|
||||
assert.ok(content.includes('command = "echo custom"'), 'preserves custom hook');
|
||||
assert.strictEqual(countMatches(content, /gsd-check-update\.js/g), 1, 'does not duplicate GSD update hook');
|
||||
const hooksJsonCommands = readHooksSessionStartCommands(codexHome);
|
||||
const gsdEntries = hooksJsonCommands.filter((cmd) => cmd.includes('gsd-check-update.js'));
|
||||
assert.strictEqual(gsdEntries.length, 1, 'does not duplicate GSD update hook in hooks.json');
|
||||
assertNoDraftRootKeys(content);
|
||||
});
|
||||
|
||||
@@ -1712,7 +1732,9 @@ describe('Codex install hook configuration (e2e)', () => {
|
||||
assert.strictEqual(countMatches(content, /^"codex_hooks" = true$/gm), 1, 'normalizes the quoted codex_hooks key to true');
|
||||
assert.strictEqual(countMatches(content, /^\[features\]\s*$/gm), 0, 'does not prepend a second bare features table');
|
||||
assert.ok(content.includes('other_feature = true'), 'preserves existing feature keys');
|
||||
assert.strictEqual(countMatches(content, /gsd-check-update\.js/g), 1, 'keeps one GSD update hook');
|
||||
const hooksJsonCommands = readHooksSessionStartCommands(codexHome);
|
||||
const gsdEntries = hooksJsonCommands.filter((cmd) => cmd.includes('gsd-check-update.js'));
|
||||
assert.strictEqual(gsdEntries.length, 1, 'keeps one GSD update hook in hooks.json');
|
||||
assertNoDraftRootKeys(content);
|
||||
});
|
||||
|
||||
@@ -1733,7 +1755,9 @@ describe('Codex install hook configuration (e2e)', () => {
|
||||
assert.ok(content.includes('[features."a#b"]\nenabled = true'), 'preserves the quoted nested features table');
|
||||
assert.strictEqual(countMatches(content, /^\[features\]\s*$/gm), 1, 'adds one real top-level features table');
|
||||
assert.strictEqual(countMatches(content, /^codex_hooks = true$/gm), 1, 'adds one codex_hooks key');
|
||||
assert.strictEqual(countMatches(content, /gsd-check-update\.js/g), 1, 'remains idempotent for the GSD hook block');
|
||||
const hooksJsonCommands = readHooksSessionStartCommands(codexHome);
|
||||
const gsdEntries = hooksJsonCommands.filter((cmd) => cmd.includes('gsd-check-update.js'));
|
||||
assert.strictEqual(gsdEntries.length, 1, 'remains idempotent for the GSD hook block in hooks.json');
|
||||
assertNoDraftRootKeys(content);
|
||||
});
|
||||
|
||||
@@ -1753,7 +1777,9 @@ describe('Codex install hook configuration (e2e)', () => {
|
||||
assert.strictEqual(countMatches(content, /^\[features\]\s*$/gm), 0, 'does not add a [features] table');
|
||||
assert.strictEqual(countMatches(content, /^features\.codex_hooks = true$/gm), 1, 'adds one dotted codex_hooks key');
|
||||
assert.ok(content.includes('features.other_feature = true'), 'preserves existing dotted features key');
|
||||
assert.strictEqual(countMatches(content, /gsd-check-update\.js/g), 1, 'adds one GSD update hook for dotted codex_hooks and remains idempotent');
|
||||
const hooksJsonCommands = readHooksSessionStartCommands(codexHome);
|
||||
const gsdEntries = hooksJsonCommands.filter((cmd) => cmd.includes('gsd-check-update.js'));
|
||||
assert.strictEqual(gsdEntries.length, 1, 'adds one GSD update hook for dotted codex_hooks and remains idempotent');
|
||||
assertNoDraftRootKeys(content);
|
||||
});
|
||||
|
||||
@@ -1817,7 +1843,9 @@ describe('Codex install hook configuration (e2e)', () => {
|
||||
assert.strictEqual(countMatches(content, /^features\."codex_hooks" = true$/gm), 1, 'normalizes the quoted dotted key to true');
|
||||
assert.strictEqual(countMatches(content, /^features\.codex_hooks = true$/gm), 0, 'does not append a bare dotted duplicate');
|
||||
assert.ok(content.includes('features.other_feature = true'), 'preserves other dotted features keys');
|
||||
assert.strictEqual(countMatches(content, /gsd-check-update\.js/g), 1, 'adds one GSD update hook for quoted dotted codex_hooks and remains idempotent');
|
||||
const hooksJsonCommands = readHooksSessionStartCommands(codexHome);
|
||||
const gsdEntries = hooksJsonCommands.filter((cmd) => cmd.includes('gsd-check-update.js'));
|
||||
assert.strictEqual(gsdEntries.length, 1, 'adds one GSD update hook for quoted dotted codex_hooks and remains idempotent');
|
||||
assertNoDraftRootKeys(content);
|
||||
});
|
||||
|
||||
@@ -1932,7 +1960,9 @@ describe('Codex install hook configuration (e2e)', () => {
|
||||
assert.strictEqual(countMatches(content, /^codex_hooks = true$/gm), 1, 'replaces the multiline basic-string assignment with one true value');
|
||||
assert.ok(!content.includes('multiline-basic-sentinel'), 'removes multiline basic-string continuation lines');
|
||||
assert.ok(content.includes('other_feature = true'), 'preserves following feature keys');
|
||||
assert.strictEqual(countMatches(content, /gsd-check-update\.js/g), 1, 'remains idempotent for the GSD hook block');
|
||||
const hooksJsonCommands = readHooksSessionStartCommands(codexHome);
|
||||
const gsdEntries = hooksJsonCommands.filter((cmd) => cmd.includes('gsd-check-update.js'));
|
||||
assert.strictEqual(gsdEntries.length, 1, 'remains idempotent for the GSD hook block in hooks.json');
|
||||
assertNoDraftRootKeys(content);
|
||||
});
|
||||
|
||||
@@ -1957,7 +1987,9 @@ describe('Codex install hook configuration (e2e)', () => {
|
||||
assert.strictEqual(countMatches(content, /^codex_hooks = true$/gm), 1, 'replaces the multiline literal-string assignment with one true value');
|
||||
assert.ok(!content.includes('multiline-literal-sentinel'), 'removes multiline literal-string continuation lines');
|
||||
assert.ok(content.includes('other_feature = true'), 'preserves following feature keys');
|
||||
assert.strictEqual(countMatches(content, /gsd-check-update\.js/g), 1, 'remains idempotent for the GSD hook block');
|
||||
const hooksJsonCommands = readHooksSessionStartCommands(codexHome);
|
||||
const gsdEntries = hooksJsonCommands.filter((cmd) => cmd.includes('gsd-check-update.js'));
|
||||
assert.strictEqual(gsdEntries.length, 1, 'remains idempotent for the GSD hook block in hooks.json');
|
||||
assertNoDraftRootKeys(content);
|
||||
});
|
||||
|
||||
@@ -1983,7 +2015,9 @@ describe('Codex install hook configuration (e2e)', () => {
|
||||
assert.ok(!content.includes('array-sentinel-1'), 'removes multiline array continuation lines');
|
||||
assert.ok(!content.includes('array-sentinel-2'), 'removes multiline array continuation lines');
|
||||
assert.ok(content.includes('other_feature = true'), 'preserves following feature keys');
|
||||
assert.strictEqual(countMatches(content, /gsd-check-update\.js/g), 1, 'remains idempotent for the GSD hook block');
|
||||
const hooksJsonCommands = readHooksSessionStartCommands(codexHome);
|
||||
const gsdEntries = hooksJsonCommands.filter((cmd) => cmd.includes('gsd-check-update.js'));
|
||||
assert.strictEqual(gsdEntries.length, 1, 'remains idempotent for the GSD hook block in hooks.json');
|
||||
assertNoDraftRootKeys(content);
|
||||
});
|
||||
|
||||
@@ -2028,7 +2062,9 @@ describe('Codex install hook configuration (e2e)', () => {
|
||||
assert.strictEqual(countMatches(content, /^codex_hooks = true$/gm), 1, 'keeps one codex_hooks = true');
|
||||
assert.ok(content.includes('other_feature = true'), 'preserves other feature keys');
|
||||
assert.strictEqual(countMatches(content, /echo custom-after-command/g), 1, 'preserves non-GSD hook exactly once');
|
||||
assert.strictEqual(countMatches(content, /gsd-check-update\.js/g), 1, 'keeps one GSD update hook');
|
||||
const hooksJsonCommands = readHooksSessionStartCommands(codexHome);
|
||||
const gsdEntries = hooksJsonCommands.filter((cmd) => cmd.includes('gsd-check-update.js'));
|
||||
assert.strictEqual(gsdEntries.length, 1, 'keeps one GSD update hook in hooks.json');
|
||||
assertUsesOnlyEol(content, '\r\n');
|
||||
assertNoDraftRootKeys(content);
|
||||
});
|
||||
@@ -2051,7 +2087,9 @@ describe('Codex install hook configuration (e2e)', () => {
|
||||
assert.strictEqual(countMatches(content, /^\[features\]\s*$/gm), 1, 'keeps one [features] section');
|
||||
assert.strictEqual(countMatches(content, /^codex_hooks = true # keep me$/gm), 1, 'preserves the commented true value');
|
||||
assert.ok(content.includes('other_feature = true'), 'preserves other feature keys');
|
||||
assert.strictEqual(countMatches(content, /gsd-check-update\.js/g), 1, 'adds the GSD update hook once');
|
||||
const hooksJsonCommands = readHooksSessionStartCommands(codexHome);
|
||||
const gsdEntries = hooksJsonCommands.filter((cmd) => cmd.includes('gsd-check-update.js'));
|
||||
assert.strictEqual(gsdEntries.length, 1, 'adds the GSD update hook once in hooks.json');
|
||||
assertNoDraftRootKeys(content);
|
||||
});
|
||||
|
||||
@@ -2065,13 +2103,14 @@ describe('Codex install hook configuration (e2e)', () => {
|
||||
// [features] is inserted after top-level lines, before [model] — not prepended
|
||||
assert.ok(content.includes('# first line wins\n\n[features]\ncodex_hooks = true\n'), 'inserts features after top-level lines using first newline style');
|
||||
assert.ok(content.includes(`# GSD Agent Configuration — managed by get-shit-done installer\n`), 'writes the managed agent block using the first newline style');
|
||||
// Structural check: nested schema must be present regardless of mixed EOL
|
||||
// Structural check: managed SessionStart hooks live in hooks.json.
|
||||
const parsedMixed = parseTomlToObject(content);
|
||||
assert.ok(parsedMixed.hooks && Array.isArray(parsedMixed.hooks.SessionStart), 'writes [[hooks.SessionStart]] AoT with first-newline style');
|
||||
assert.ok(Array.isArray(parsedMixed.hooks.SessionStart[0].hooks), 'writes [[hooks.SessionStart.hooks]] sub-table');
|
||||
assert.ok(!parsedMixed.hooks || !Array.isArray(parsedMixed.hooks.SessionStart), 'does not write managed SessionStart hooks to config.toml');
|
||||
const hooksJsonCommands = readHooksSessionStartCommands(codexHome);
|
||||
const gsdEntries = hooksJsonCommands.filter((cmd) => cmd.includes('gsd-check-update.js'));
|
||||
assert.strictEqual(gsdEntries.length, 1, 'writes one managed SessionStart hook to hooks.json');
|
||||
assert.ok(content.includes('[model]\r\nname = "o3"'), 'preserves the existing CRLF model lines');
|
||||
assert.strictEqual(countMatches(content, /^codex_hooks = true$/gm), 1, 'remains idempotent on repeated installs');
|
||||
assert.strictEqual(countMatches(content, /gsd-check-update\.js/g), 1, 'does not duplicate the GSD hook block');
|
||||
assertNoDraftRootKeys(content);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user