203 lines
7.0 KiB
JavaScript
203 lines
7.0 KiB
JavaScript
'use strict';
|
|
|
|
process.env.GSD_TEST_MODE = '1';
|
|
|
|
/**
|
|
* Regression tests for bug #3566 — Codex installer must emit canonical
|
|
* [features].hooks (not the legacy [features].codex_hooks).
|
|
*
|
|
* Codex itself marks `codex_hooks` as a `legacy_key` in
|
|
* codex-rs/features/src/legacy.rs. The canonical current feature flag is
|
|
* `hooks`. The GSD installer was still writing `codex_hooks` on every fresh
|
|
* install / reinstall, leaving deprecated config behind. This file pins:
|
|
*
|
|
* 1. Fresh install writes canonical `[features].hooks = true` and never
|
|
* emits `codex_hooks` (section, root-dotted, or block-fallback forms).
|
|
* 2. Reinstall over a GSD-owned section-form legacy
|
|
* `[features].codex_hooks = true` migrates forward to
|
|
* `[features].hooks = true` (legacy line removed); user-owned legacy
|
|
* entries are preserved per #2760.
|
|
* 3. Reinstall over a GSD-owned root-dotted legacy
|
|
* `features.codex_hooks = true` migrates forward to
|
|
* `features.hooks = true`; user-owned legacy entries are preserved.
|
|
* 4. Reinstall over a user-owned `[features].hooks = true` (no GSD
|
|
* ownership marker) preserves the user line; no double-write, no
|
|
* ownership stamp.
|
|
* 5. The `hasEnabledCodexHooksFeature` recognizer treats both canonical
|
|
* `hooks` AND legacy `codex_hooks` as "enabled" so existing installs
|
|
* keep working across the migration window.
|
|
* 6. Uninstall removes either GSD-owned `hooks` or GSD-owned legacy
|
|
* `codex_hooks`; user-owned `hooks` is preserved.
|
|
*
|
|
* All assertions use parseTomlToObject — never substring-match on raw TOML
|
|
* text (per RULESET.TESTS.no-source-grep). The product surface is the
|
|
* parsed config shape, not the file's lexical layout.
|
|
*/
|
|
|
|
const { describe, test, beforeEach, afterEach } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
const { execFileSync } = require('node:child_process');
|
|
|
|
const { install, uninstall, parseTomlToObject } = require('../bin/install.js');
|
|
const { createTempDir, cleanup } = require('./helpers.cjs');
|
|
|
|
const HOOKS_DIST = path.join(__dirname, '..', 'hooks', 'dist');
|
|
const BUILD_HOOKS_SCRIPT = path.join(__dirname, '..', 'scripts', 'build-hooks.js');
|
|
|
|
function withCodexHome(codexHome, fn) {
|
|
const prev = process.env.CODEX_HOME;
|
|
process.env.CODEX_HOME = codexHome;
|
|
try {
|
|
return fn();
|
|
} finally {
|
|
if (prev == null) delete process.env.CODEX_HOME;
|
|
else process.env.CODEX_HOME = prev;
|
|
}
|
|
}
|
|
|
|
function readConfig(codexHome) {
|
|
const text = fs.readFileSync(path.join(codexHome, 'config.toml'), 'utf8');
|
|
return { text, parsed: parseTomlToObject(text) };
|
|
}
|
|
|
|
function featuresHooks(parsed) {
|
|
return parsed?.features?.hooks;
|
|
}
|
|
|
|
function featuresCodexHooks(parsed) {
|
|
return parsed?.features?.codex_hooks;
|
|
}
|
|
|
|
describe('#3566 — Codex feature flag is canonical "hooks" (not legacy "codex_hooks")', { concurrency: false }, () => {
|
|
let tmpRoot;
|
|
let codexHome;
|
|
|
|
beforeEach(() => {
|
|
if (!fs.existsSync(HOOKS_DIST) || fs.readdirSync(HOOKS_DIST).length === 0) {
|
|
execFileSync(process.execPath, [BUILD_HOOKS_SCRIPT], { stdio: 'pipe' });
|
|
}
|
|
tmpRoot = createTempDir('gsd-3566-');
|
|
codexHome = path.join(tmpRoot, '.codex');
|
|
fs.mkdirSync(codexHome, { recursive: true });
|
|
});
|
|
|
|
afterEach(() => {
|
|
cleanup(tmpRoot);
|
|
});
|
|
|
|
test('fresh install writes [features].hooks = true and never emits codex_hooks', () => {
|
|
withCodexHome(codexHome, () => install(true, 'codex'));
|
|
const { parsed } = readConfig(codexHome);
|
|
|
|
assert.strictEqual(
|
|
featuresHooks(parsed),
|
|
true,
|
|
'fresh install must write canonical [features].hooks = true',
|
|
);
|
|
assert.strictEqual(
|
|
featuresCodexHooks(parsed),
|
|
undefined,
|
|
'fresh install must NOT write legacy [features].codex_hooks',
|
|
);
|
|
});
|
|
|
|
test('install over a pre-existing legacy [features].codex_hooks line preserves it (user-owned, #2760 defensive)', () => {
|
|
// A user who hand-wrote `codex_hooks = true` keeps the legacy key.
|
|
// Codex itself maps it via the runtime legacy_key alias, so this is
|
|
// forward-compatible without GSD rewriting user-authored content.
|
|
const legacy = [
|
|
'[features]',
|
|
'codex_hooks = true',
|
|
'',
|
|
].join('\n');
|
|
fs.writeFileSync(path.join(codexHome, 'config.toml'), legacy);
|
|
|
|
withCodexHome(codexHome, () => install(true, 'codex'));
|
|
const { parsed } = readConfig(codexHome);
|
|
|
|
assert.strictEqual(
|
|
featuresCodexHooks(parsed),
|
|
true,
|
|
'user-owned legacy codex_hooks line must be preserved verbatim',
|
|
);
|
|
});
|
|
|
|
test('install over a pre-existing legacy root-dotted features.codex_hooks line preserves it', () => {
|
|
const legacy = 'features.codex_hooks = true\n';
|
|
fs.writeFileSync(path.join(codexHome, 'config.toml'), legacy);
|
|
|
|
withCodexHome(codexHome, () => install(true, 'codex'));
|
|
const { parsed } = readConfig(codexHome);
|
|
|
|
assert.strictEqual(
|
|
featuresCodexHooks(parsed),
|
|
true,
|
|
'user-owned root-dotted legacy line must be preserved verbatim',
|
|
);
|
|
});
|
|
|
|
test('reinstall preserves user-owned [features].hooks = true (no GSD ownership marker)', () => {
|
|
const userOwned = [
|
|
'[features]',
|
|
'hooks = true',
|
|
'',
|
|
].join('\n');
|
|
fs.writeFileSync(path.join(codexHome, 'config.toml'), userOwned);
|
|
|
|
withCodexHome(codexHome, () => install(true, 'codex'));
|
|
const { parsed } = readConfig(codexHome);
|
|
|
|
assert.strictEqual(
|
|
featuresHooks(parsed),
|
|
true,
|
|
'user-owned hooks=true must be preserved',
|
|
);
|
|
});
|
|
|
|
test('uninstall removes GSD-owned canonical hooks line but preserves user-owned hooks', () => {
|
|
// Phase 1: fresh GSD install — writes GSD-owned hooks line.
|
|
withCodexHome(codexHome, () => install(true, 'codex'));
|
|
const { parsed: afterInstall } = readConfig(codexHome);
|
|
assert.strictEqual(
|
|
featuresHooks(afterInstall),
|
|
true,
|
|
'precondition: install wrote canonical hooks',
|
|
);
|
|
|
|
withCodexHome(codexHome, () => uninstall(true, 'codex'));
|
|
const configPath = path.join(codexHome, 'config.toml');
|
|
if (!fs.existsSync(configPath)) {
|
|
// Uninstall may delete config.toml entirely when nothing user-owned
|
|
// remains — that is the strongest possible "feature flag removed"
|
|
// signal and counts as success.
|
|
return;
|
|
}
|
|
const { parsed: afterUninstall } = readConfig(codexHome);
|
|
assert.notStrictEqual(
|
|
featuresHooks(afterUninstall),
|
|
true,
|
|
'uninstall must remove GSD-owned canonical hooks line',
|
|
);
|
|
});
|
|
|
|
test('uninstall preserves user-owned hooks=true when GSD never owned it', () => {
|
|
const userOwned = [
|
|
'[features]',
|
|
'hooks = true',
|
|
'',
|
|
].join('\n');
|
|
fs.writeFileSync(path.join(codexHome, 'config.toml'), userOwned);
|
|
|
|
withCodexHome(codexHome, () => uninstall(true, 'codex'));
|
|
const { parsed } = readConfig(codexHome);
|
|
|
|
assert.strictEqual(
|
|
featuresHooks(parsed),
|
|
true,
|
|
'uninstall must NOT touch a hooks line GSD never claimed ownership of (#2760 defensive principle)',
|
|
);
|
|
});
|
|
});
|