Merge branch 'next' into fix/2073-antigravity-reviewer-block
This commit is contained in:
@@ -60,21 +60,34 @@ const { resolveInstallPlan } = require('../gsd-core/bin/lib/runtime-config-adapt
|
||||
|
||||
function runCodexInstall(codexHome, cwd = path.join(__dirname, '..')) {
|
||||
const previousCodeHome = process.env.CODEX_HOME;
|
||||
const previousHome = process.env.HOME;
|
||||
const previousUserProfile = process.env.USERPROFILE;
|
||||
const previousCwd = process.cwd();
|
||||
process.env.CODEX_HOME = codexHome;
|
||||
// #2088: Codex skills now install to the canonical $HOME/.agents/skills root
|
||||
// (os.homedir()-relative, independent of CODEX_HOME — per codex core-skills
|
||||
// loader.rs). Sandbox HOME to codexHome so skills land under the temp dir
|
||||
// (codexHome/.agents/skills) instead of polluting the developer's real home.
|
||||
process.env.HOME = codexHome;
|
||||
process.env.USERPROFILE = codexHome;
|
||||
|
||||
try {
|
||||
process.chdir(cwd);
|
||||
return install(true, 'codex');
|
||||
} finally {
|
||||
process.chdir(previousCwd);
|
||||
if (previousCodeHome === undefined) {
|
||||
delete process.env.CODEX_HOME;
|
||||
} else {
|
||||
process.env.CODEX_HOME = previousCodeHome;
|
||||
}
|
||||
if (previousCodeHome === undefined) delete process.env.CODEX_HOME;
|
||||
else process.env.CODEX_HOME = previousCodeHome;
|
||||
if (previousHome === undefined) delete process.env.HOME;
|
||||
else process.env.HOME = previousHome;
|
||||
if (previousUserProfile === undefined) delete process.env.USERPROFILE;
|
||||
else process.env.USERPROFILE = previousUserProfile;
|
||||
}
|
||||
}
|
||||
// #2088: the canonical Codex skill-install root, sandboxed under codexHome.
|
||||
function codexSkillsRoot(codexHome) {
|
||||
return path.join(codexHome, '.agents', 'skills');
|
||||
}
|
||||
|
||||
function readCodexConfig(codexHome) {
|
||||
return fs.readFileSync(path.join(codexHome, 'config.toml'), 'utf8');
|
||||
@@ -722,17 +735,19 @@ describe('generateCodexConfigBlock', () => {
|
||||
assert.ok(result.startsWith(GSD_CODEX_MARKER), 'starts with marker');
|
||||
});
|
||||
|
||||
test('does not include feature flags or agents table header', () => {
|
||||
test('emits the [agents] max_depth tuning block but no feature flags (#2088)', () => {
|
||||
const result = generateCodexConfigBlock(agents);
|
||||
assert.ok(!result.includes('[features]'), 'no features table');
|
||||
assert.ok(!result.includes('multi_agent'), 'no multi_agent');
|
||||
assert.ok(!result.includes('default_mode_request_user_input'), 'no request_user_input');
|
||||
// Should not have bare [agents] table header (only [agents.<name>] structs).
|
||||
assert.ok(!result.match(/^\[agents\]\s*$/m), 'no bare [agents] table');
|
||||
// #2088: the managed block DOES pin dispatch depth via a bare [agents]
|
||||
// AgentsToml scalar table (coexisting with the [agents.<name>] role structs).
|
||||
assert.match(result, /^\[agents\]$/m, 'emits the [agents] tuning table');
|
||||
assert.match(result, /^max_depth = 1$/m, 'pins max_depth = 1');
|
||||
// Should not emit [[agents]] sequence format (rejected by Codex 0.124.0).
|
||||
assert.ok(!result.includes('[[agents]]'), 'no [[agents]] sequence format');
|
||||
assert.ok(!result.includes('max_threads'), 'no max_threads');
|
||||
assert.ok(!result.includes('max_depth'), 'no max_depth');
|
||||
// Only max_depth is managed — max_threads is intentionally left to the user.
|
||||
assert.ok(!result.includes('max_threads'), 'no max_threads (only max_depth is GSD-managed)');
|
||||
});
|
||||
|
||||
test('#2727: emits [agents.<name>] struct format (Codex 0.120.0+, replaces #2645 [[agents]])', () => {
|
||||
@@ -2721,7 +2736,7 @@ describe('cleanupCodexSkillMetadataSidecars (#1326)', () => {
|
||||
const codexHome = path.join(tmpDir, 'codex-home');
|
||||
fs.mkdirSync(codexHome, { recursive: true });
|
||||
runCodexInstall(codexHome);
|
||||
const skillsDir = path.join(codexHome, 'skills');
|
||||
const skillsDir = codexSkillsRoot(codexHome);
|
||||
assert.ok(fs.existsSync(skillsDir), 'Codex install must create a skills/ directory');
|
||||
const gsdSkillDirs = fs.readdirSync(skillsDir, { withFileTypes: true })
|
||||
.filter(e => e.isDirectory() && e.name.startsWith('gsd-') && e.name !== 'gsd-dev-preferences');
|
||||
@@ -2798,12 +2813,28 @@ before(() => {
|
||||
|
||||
describe('#2698: CRLF stale gsd-update-check block is removed on Codex reinstall', () => {
|
||||
let tmpDir;
|
||||
let _previousHome;
|
||||
let _previousUserProfile;
|
||||
|
||||
beforeEach(() => {
|
||||
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-crlf-install-2698-'));
|
||||
// #2088 (ADR-1239 upgrade 3): Codex's skills-kind `home: ".agents"` override
|
||||
// applies to BOTH global and local scope and resolves via os.homedir(). This
|
||||
// describe block calls install(false, 'codex') (local scope) directly —
|
||||
// without sandboxing HOME/USERPROFILE to tmpDir, that in-process install
|
||||
// would materialize a full gsd-* skill set into the developer/CI machine's
|
||||
// REAL $HOME/.agents/skills instead of the temp dir.
|
||||
_previousHome = process.env.HOME;
|
||||
_previousUserProfile = process.env.USERPROFILE;
|
||||
process.env.HOME = tmpDir;
|
||||
process.env.USERPROFILE = tmpDir;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (_previousHome === undefined) delete process.env.HOME;
|
||||
else process.env.HOME = _previousHome;
|
||||
if (_previousUserProfile === undefined) delete process.env.USERPROFILE;
|
||||
else process.env.USERPROFILE = _previousUserProfile;
|
||||
// Use the shared 5s Windows-EBUSY retry budget instead of inline 1s.
|
||||
cleanup(tmpDir);
|
||||
});
|
||||
@@ -2993,7 +3024,16 @@ if (previousGsdTestMode === undefined) {
|
||||
function runCodexInstall(codexHome, cwd = path.join(__dirname, '..')) {
|
||||
const previousCodeHome = process.env.CODEX_HOME;
|
||||
const previousCwd = process.cwd();
|
||||
// #2088 (ADR-1239 upgrade 3): Codex skills now install to the canonical
|
||||
// $HOME/.agents/skills root (os.homedir()-relative, independent of
|
||||
// CODEX_HOME). Sandbox HOME (and USERPROFILE) to codexHome so this
|
||||
// in-process install never materializes skills under the developer/CI
|
||||
// machine's real home directory.
|
||||
const previousHome = process.env.HOME;
|
||||
const previousUserProfile = process.env.USERPROFILE;
|
||||
process.env.CODEX_HOME = codexHome;
|
||||
process.env.HOME = codexHome;
|
||||
process.env.USERPROFILE = codexHome;
|
||||
try {
|
||||
process.chdir(cwd);
|
||||
return install(true, 'codex');
|
||||
@@ -3004,6 +3044,10 @@ function runCodexInstall(codexHome, cwd = path.join(__dirname, '..')) {
|
||||
} else {
|
||||
process.env.CODEX_HOME = previousCodeHome;
|
||||
}
|
||||
if (previousHome === undefined) delete process.env.HOME;
|
||||
else process.env.HOME = previousHome;
|
||||
if (previousUserProfile === undefined) delete process.env.USERPROFILE;
|
||||
else process.env.USERPROFILE = previousUserProfile;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4583,7 +4627,16 @@ before(() => {
|
||||
function runCodexInstall(codexHome) {
|
||||
const previousCodexHome = process.env.CODEX_HOME;
|
||||
const previousCwd = process.cwd();
|
||||
// #2088 (ADR-1239 upgrade 3): Codex skills now install to the canonical
|
||||
// $HOME/.agents/skills root (os.homedir()-relative, independent of
|
||||
// CODEX_HOME). Sandbox HOME (and USERPROFILE) to codexHome so this
|
||||
// in-process install never materializes skills under the developer/CI
|
||||
// machine's real home directory.
|
||||
const previousHome = process.env.HOME;
|
||||
const previousUserProfile = process.env.USERPROFILE;
|
||||
process.env.CODEX_HOME = codexHome;
|
||||
process.env.HOME = codexHome;
|
||||
process.env.USERPROFILE = codexHome;
|
||||
try {
|
||||
process.chdir(path.join(__dirname, '..'));
|
||||
return install(true, 'codex');
|
||||
@@ -4594,6 +4647,10 @@ function runCodexInstall(codexHome) {
|
||||
} else {
|
||||
process.env.CODEX_HOME = previousCodexHome;
|
||||
}
|
||||
if (previousHome === undefined) delete process.env.HOME;
|
||||
else process.env.HOME = previousHome;
|
||||
if (previousUserProfile === undefined) delete process.env.USERPROFILE;
|
||||
else process.env.USERPROFILE = previousUserProfile;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4908,7 +4965,7 @@ describe('#3245 — idempotent rollback reverts skills/, agents/, and VERSION',
|
||||
assert.strictEqual(threw, true, 'install must throw when validation fails');
|
||||
|
||||
// skills/ — GSD writes gsd-* subdirs here. All must be absent after rollback.
|
||||
const skillsDir = path.join(codexHome, 'skills');
|
||||
const skillsDir = codexSkillsRoot(codexHome);
|
||||
if (fs.existsSync(skillsDir)) {
|
||||
const gsdSkills = fs.readdirSync(skillsDir, { withFileTypes: true })
|
||||
.filter(e => e.isDirectory() && e.name.startsWith('gsd-'));
|
||||
@@ -4963,7 +5020,7 @@ describe('#3245 — idempotent rollback reverts skills/, agents/, and VERSION',
|
||||
assert.strictEqual(threw, true, 'install must throw when validation fails (very early failure)');
|
||||
// Rollback removes all gsd-* skill dirs it wrote. Even if skills/ was
|
||||
// created during the install, no gsd-* dirs should survive after rollback.
|
||||
const skillsDir = path.join(codexHome, 'skills');
|
||||
const skillsDir = codexSkillsRoot(codexHome);
|
||||
const remainingGsdSkills = fs.existsSync(skillsDir)
|
||||
? fs.readdirSync(skillsDir, { withFileTypes: true })
|
||||
.filter((e) => e.isDirectory() && e.name.startsWith('gsd-'))
|
||||
@@ -4978,7 +5035,7 @@ describe('#3245 — idempotent rollback reverts skills/, agents/, and VERSION',
|
||||
|
||||
test('rollback does not remove pre-existing user skills that GSD did not write', () => {
|
||||
// If the user has a custom skill dir (not gsd-*) it must survive rollback.
|
||||
const skillsDir = path.join(codexHome, 'skills');
|
||||
const skillsDir = codexSkillsRoot(codexHome);
|
||||
const userSkill = path.join(skillsDir, 'my-custom-skill');
|
||||
fs.mkdirSync(userSkill, { recursive: true });
|
||||
fs.writeFileSync(path.join(userSkill, 'SKILL.md'), '# Custom\n', 'utf8');
|
||||
@@ -5248,7 +5305,16 @@ describe('#3285 — install succeeds when config.toml contains hooks.state entri
|
||||
function runCodexInstall() {
|
||||
const previousCodexHome = process.env.CODEX_HOME;
|
||||
const previousCwd = process.cwd();
|
||||
// #2088 (ADR-1239 upgrade 3): Codex skills now install to the canonical
|
||||
// $HOME/.agents/skills root (os.homedir()-relative, independent of
|
||||
// CODEX_HOME). Sandbox HOME (and USERPROFILE) to tmpDir so this
|
||||
// in-process install never materializes skills under the developer/CI
|
||||
// machine's real home directory.
|
||||
const previousHome = process.env.HOME;
|
||||
const previousUserProfile = process.env.USERPROFILE;
|
||||
process.env.CODEX_HOME = codexHome;
|
||||
process.env.HOME = tmpDir;
|
||||
process.env.USERPROFILE = tmpDir;
|
||||
try {
|
||||
process.chdir(path.join(__dirname, '..'));
|
||||
return install(true, 'codex');
|
||||
@@ -5259,6 +5325,10 @@ describe('#3285 — install succeeds when config.toml contains hooks.state entri
|
||||
} else {
|
||||
process.env.CODEX_HOME = previousCodexHome;
|
||||
}
|
||||
if (previousHome === undefined) delete process.env.HOME;
|
||||
else process.env.HOME = previousHome;
|
||||
if (previousUserProfile === undefined) delete process.env.USERPROFILE;
|
||||
else process.env.USERPROFILE = previousUserProfile;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5930,11 +6000,24 @@ describe('#3426 uninstall: gsd-check-update.cmd is removed from hooks dir on uni
|
||||
|
||||
function withCodexHome(dir, fn) {
|
||||
const prev = process.env.CODEX_HOME;
|
||||
// #2088 (ADR-1239 upgrade 3): Codex skills now resolve an alternate install
|
||||
// home rooted at the REAL os.homedir() ($HOME/.agents), independent of
|
||||
// CODEX_HOME. Fake $HOME (and $USERPROFILE) too so this in-process install
|
||||
// never touches the developer/CI machine's real home directory — confined
|
||||
// entirely to `dir`, which the caller cleans up.
|
||||
const prevHome = process.env.HOME;
|
||||
const prevUserProfile = process.env.USERPROFILE;
|
||||
process.env.CODEX_HOME = dir;
|
||||
process.env.HOME = dir;
|
||||
process.env.USERPROFILE = dir;
|
||||
try { return fn(); }
|
||||
finally {
|
||||
if (prev == null) delete process.env.CODEX_HOME;
|
||||
else process.env.CODEX_HOME = prev;
|
||||
if (prevHome == null) delete process.env.HOME;
|
||||
else process.env.HOME = prevHome;
|
||||
if (prevUserProfile == null) delete process.env.USERPROFILE;
|
||||
else process.env.USERPROFILE = prevUserProfile;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6050,12 +6133,27 @@ const BUILD_HOOKS_SCRIPT = path.join(__dirname, '..', 'scripts', 'build-hooks.js
|
||||
|
||||
function withCodexHome(codexHome, fn) {
|
||||
const prev = process.env.CODEX_HOME;
|
||||
// #2088 (ADR-1239 upgrade 3): Codex skills now resolve an alternate install
|
||||
// home rooted at the REAL os.homedir() ($HOME/.agents), independent of
|
||||
// CODEX_HOME. Fake $HOME (and $USERPROFILE) too — using the sandbox root
|
||||
// (codexHome's parent, since codexHome is conventionally `<tmpRoot>/.codex`
|
||||
// in this file) — so this in-process install never touches the developer/CI
|
||||
// machine's real home directory. tmpRoot is reclaimed by the caller's afterEach.
|
||||
const prevHome = process.env.HOME;
|
||||
const prevUserProfile = process.env.USERPROFILE;
|
||||
const fakeHome = path.dirname(codexHome);
|
||||
process.env.CODEX_HOME = codexHome;
|
||||
process.env.HOME = fakeHome;
|
||||
process.env.USERPROFILE = fakeHome;
|
||||
try {
|
||||
return fn();
|
||||
} finally {
|
||||
if (prev == null) delete process.env.CODEX_HOME;
|
||||
else process.env.CODEX_HOME = prev;
|
||||
if (prevHome == null) delete process.env.HOME;
|
||||
else process.env.HOME = prevHome;
|
||||
if (prevUserProfile == null) delete process.env.USERPROFILE;
|
||||
else process.env.USERPROFILE = prevUserProfile;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6107,22 +6205,32 @@ describe('#3427 + #3433 — Codex installer avoids duplicate skills and mixed ho
|
||||
|
||||
withCodexHome(codexHome, () => install(true, 'codex'));
|
||||
|
||||
const skillsDir = path.join(codexHome, 'skills');
|
||||
const entries = fs.existsSync(skillsDir)
|
||||
? fs.readdirSync(skillsDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name)
|
||||
// #2088: the managed gsd-* skill surface now regenerates at the
|
||||
// canonical $HOME/.agents/skills root (fakeHome === tmpRoot here — see
|
||||
// withCodexHome above), not under the legacy $CODEX_HOME/skills.
|
||||
const newSkillsDir = codexSkillsRoot(tmpRoot);
|
||||
const newEntries = fs.existsSync(newSkillsDir)
|
||||
? fs.readdirSync(newSkillsDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name)
|
||||
: [];
|
||||
|
||||
// #3562: $gsd-* commands are discoverable only when skills/gsd-*/SKILL.md
|
||||
// exists. The installer must regenerate (not remove) the managed gsd-*
|
||||
// directories.
|
||||
assert.equal(entries.includes('gsd-help'), true);
|
||||
const refreshedBody = fs.readFileSync(path.join(skillsDir, 'gsd-help', 'SKILL.md'), 'utf8');
|
||||
// #3562: $gsd-* commands are discoverable only when
|
||||
// .agents/skills/gsd-*/SKILL.md exists. The installer must regenerate
|
||||
// (not remove) the managed gsd-* directories.
|
||||
assert.equal(newEntries.includes('gsd-help'), true);
|
||||
const refreshedBody = fs.readFileSync(path.join(newSkillsDir, 'gsd-help', 'SKILL.md'), 'utf8');
|
||||
assert.notEqual(refreshedBody, legacySkillBody, 'stale legacy body must be overwritten');
|
||||
const frontmatter = parseFrontmatter(refreshedBody);
|
||||
assert.equal(frontmatter.name, 'gsd-help', 'refreshed SKILL.md frontmatter must declare name: gsd-help');
|
||||
|
||||
// Unrelated user skills are preserved — the regen scope is `gsd-*` only.
|
||||
assert.equal(entries.includes('custom-user-skill'), true);
|
||||
// #2088 migration: the installer cleans stale gsd-* dirs out of the old
|
||||
// $CODEX_HOME/skills location on a pre-move install.
|
||||
const legacyHelpDir = path.join(codexHome, 'skills', 'gsd-help');
|
||||
assert.equal(fs.existsSync(legacyHelpDir), false, 'migration must remove the stale legacy gsd-help skill dir from $CODEX_HOME/skills');
|
||||
|
||||
// Unrelated user skills are preserved in place — migration only removes
|
||||
// `gsd-*` dirs from the old location; non-gsd-* user dirs are untouched.
|
||||
const userSkill = path.join(codexHome, 'skills', 'custom-user-skill', 'SKILL.md');
|
||||
assert.equal(fs.existsSync(userSkill), true, 'unrelated user skill must survive the #2088 migration');
|
||||
});
|
||||
|
||||
test('stores managed SessionStart update hook in hooks.json and removes inline gsd hook from config.toml', () => {
|
||||
@@ -6239,12 +6347,27 @@ const BUILD_HOOKS_SCRIPT = path.join(__dirname, '..', 'scripts', 'build-hooks.js
|
||||
|
||||
function withCodexHome(codexHome, fn) {
|
||||
const prev = process.env.CODEX_HOME;
|
||||
// #2088 (ADR-1239 upgrade 3): Codex skills now resolve an alternate install
|
||||
// home rooted at the REAL os.homedir() ($HOME/.agents), independent of
|
||||
// CODEX_HOME. Fake $HOME (and $USERPROFILE) too — using the sandbox root
|
||||
// (codexHome's parent, since codexHome is conventionally `<tmpRoot>/.codex`
|
||||
// in this file) — so this in-process install never touches the developer/CI
|
||||
// machine's real home directory. tmpRoot is reclaimed by the caller's afterEach.
|
||||
const prevHome = process.env.HOME;
|
||||
const prevUserProfile = process.env.USERPROFILE;
|
||||
const fakeHome = path.dirname(codexHome);
|
||||
process.env.CODEX_HOME = codexHome;
|
||||
process.env.HOME = fakeHome;
|
||||
process.env.USERPROFILE = fakeHome;
|
||||
try {
|
||||
return fn();
|
||||
} finally {
|
||||
if (prev == null) delete process.env.CODEX_HOME;
|
||||
else process.env.CODEX_HOME = prev;
|
||||
if (prevHome == null) delete process.env.HOME;
|
||||
else process.env.HOME = prevHome;
|
||||
if (prevUserProfile == null) delete process.env.USERPROFILE;
|
||||
else process.env.USERPROFILE = prevUserProfile;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6268,7 +6391,9 @@ describe('#3562 — Codex install produces discoverable $gsd-* skill surface', {
|
||||
test('global install creates skills/gsd-help/SKILL.md', () => {
|
||||
withCodexHome(codexHome, () => install(true, 'codex'));
|
||||
|
||||
const skillPath = path.join(codexHome, 'skills', 'gsd-help', 'SKILL.md');
|
||||
// #2088: skills now install to the canonical $HOME/.agents/skills root.
|
||||
// withCodexHome fakes $HOME to tmpRoot (codexHome's parent) above.
|
||||
const skillPath = path.join(codexSkillsRoot(tmpRoot), 'gsd-help', 'SKILL.md');
|
||||
assert.ok(
|
||||
fs.existsSync(skillPath),
|
||||
`Codex install must create ${skillPath} so $gsd-help is discoverable. ` +
|
||||
@@ -6279,7 +6404,7 @@ describe('#3562 — Codex install produces discoverable $gsd-* skill surface', {
|
||||
test('SKILL.md content has frontmatter expected by Codex skill discovery', () => {
|
||||
withCodexHome(codexHome, () => install(true, 'codex'));
|
||||
|
||||
const skillPath = path.join(codexHome, 'skills', 'gsd-help', 'SKILL.md');
|
||||
const skillPath = path.join(codexSkillsRoot(tmpRoot), 'gsd-help', 'SKILL.md');
|
||||
assert.ok(fs.existsSync(skillPath), 'precondition: SKILL.md exists');
|
||||
|
||||
const content = fs.readFileSync(skillPath, 'utf8');
|
||||
@@ -6290,7 +6415,7 @@ describe('#3562 — Codex install produces discoverable $gsd-* skill surface', {
|
||||
test('multiple core $gsd-* skills are produced (not just gsd-help)', () => {
|
||||
withCodexHome(codexHome, () => install(true, 'codex'));
|
||||
|
||||
const skillsDir = path.join(codexHome, 'skills');
|
||||
const skillsDir = codexSkillsRoot(tmpRoot);
|
||||
assert.ok(fs.existsSync(skillsDir), 'skills/ directory must exist after install');
|
||||
|
||||
const gsdSkills = fs
|
||||
@@ -6384,12 +6509,27 @@ const BUILD_HOOKS_SCRIPT = path.join(__dirname, '..', 'scripts', 'build-hooks.js
|
||||
|
||||
function withCodexHome(codexHome, fn) {
|
||||
const prev = process.env.CODEX_HOME;
|
||||
// #2088 (ADR-1239 upgrade 3): Codex skills now resolve an alternate install
|
||||
// home rooted at the REAL os.homedir() ($HOME/.agents), independent of
|
||||
// CODEX_HOME. Fake $HOME (and $USERPROFILE) too — using the sandbox root
|
||||
// (codexHome's parent, since codexHome is conventionally `<tmpRoot>/.codex`
|
||||
// in this file) — so this in-process install never touches the developer/CI
|
||||
// machine's real home directory. tmpRoot is reclaimed by the caller's afterEach.
|
||||
const prevHome = process.env.HOME;
|
||||
const prevUserProfile = process.env.USERPROFILE;
|
||||
const fakeHome = path.dirname(codexHome);
|
||||
process.env.CODEX_HOME = codexHome;
|
||||
process.env.HOME = fakeHome;
|
||||
process.env.USERPROFILE = fakeHome;
|
||||
try {
|
||||
return fn();
|
||||
} finally {
|
||||
if (prev == null) delete process.env.CODEX_HOME;
|
||||
else process.env.CODEX_HOME = prev;
|
||||
if (prevHome == null) delete process.env.HOME;
|
||||
else process.env.HOME = prevHome;
|
||||
if (prevUserProfile == null) delete process.env.USERPROFILE;
|
||||
else process.env.USERPROFILE = prevUserProfile;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6659,7 +6799,16 @@ function runCodexInstallCaptured() {
|
||||
|
||||
const previousCodexHome = process.env.CODEX_HOME;
|
||||
const previousCwd = process.cwd();
|
||||
// #2088 (ADR-1239 upgrade 3): Codex skills now resolve an alternate install
|
||||
// home rooted at os.homedir() ($HOME/.agents), independent of CODEX_HOME.
|
||||
// Sandbox $HOME (and $USERPROFILE) to codexHome too — otherwise this
|
||||
// in-process install would materialize skills under the developer/CI
|
||||
// machine's REAL home directory instead of the temp dir.
|
||||
const previousHome = process.env.HOME;
|
||||
const previousUserProfile = process.env.USERPROFILE;
|
||||
process.env.CODEX_HOME = codexHome;
|
||||
process.env.HOME = codexHome;
|
||||
process.env.USERPROFILE = codexHome;
|
||||
process.env.GSD_TEST_MODE = '1';
|
||||
try {
|
||||
process.chdir(ROOT);
|
||||
@@ -6679,6 +6828,16 @@ function runCodexInstallCaptured() {
|
||||
} else {
|
||||
process.env.CODEX_HOME = previousCodexHome;
|
||||
}
|
||||
if (previousHome === undefined) {
|
||||
delete process.env.HOME;
|
||||
} else {
|
||||
process.env.HOME = previousHome;
|
||||
}
|
||||
if (previousUserProfile === undefined) {
|
||||
delete process.env.USERPROFILE;
|
||||
} else {
|
||||
process.env.USERPROFILE = previousUserProfile;
|
||||
}
|
||||
if (previousGsdTestMode === undefined) {
|
||||
delete process.env.GSD_TEST_MODE;
|
||||
} else {
|
||||
@@ -6703,7 +6862,7 @@ describe('bug-3582: Codex global install materializes the skill surface', { conc
|
||||
});
|
||||
|
||||
test('writes the exact expected set of gsd-*/SKILL.md skills (deepEqual on name set)', () => {
|
||||
const skillsDir = path.join(installRun.codexHome, 'skills');
|
||||
const skillsDir = codexSkillsRoot(installRun.codexHome);
|
||||
assert.ok(
|
||||
fs.existsSync(skillsDir),
|
||||
`Codex install must create ${skillsDir} (the 1.42.2 regression skipped this entirely)`,
|
||||
@@ -6735,7 +6894,7 @@ describe('bug-3582: Codex global install materializes the skill surface', { conc
|
||||
});
|
||||
|
||||
test('SKILL.md frontmatter declares hyphen-form name matching the directory', () => {
|
||||
const skillsDir = path.join(installRun.codexHome, 'skills');
|
||||
const skillsDir = codexSkillsRoot(installRun.codexHome);
|
||||
const skillDirs = fs.readdirSync(skillsDir, { withFileTypes: true })
|
||||
.filter(e => e.isDirectory() && e.name.startsWith('gsd-'))
|
||||
.map(e => e.name);
|
||||
@@ -6767,7 +6926,7 @@ describe('bug-3582: Codex global install materializes the skill surface', { conc
|
||||
// review); the file on disk must contain its full output verbatim
|
||||
// (open tag, body, closing `</codex_skill_adapter>`). A truncated,
|
||||
// empty, or missing-closing-tag adapter cannot satisfy this assertion.
|
||||
const skillsDir = path.join(installRun.codexHome, 'skills');
|
||||
const skillsDir = codexSkillsRoot(installRun.codexHome);
|
||||
const skillDirs = fs.readdirSync(skillsDir, { withFileTypes: true })
|
||||
.filter(e => e.isDirectory() && e.name.startsWith('gsd-'))
|
||||
.map(e => e.name);
|
||||
@@ -6807,7 +6966,7 @@ describe('bug-3582: Codex global install materializes the skill surface', { conc
|
||||
'gsd-new-project',
|
||||
'gsd-health',
|
||||
];
|
||||
const skillsDir = path.join(installRun.codexHome, 'skills');
|
||||
const skillsDir = codexSkillsRoot(installRun.codexHome);
|
||||
for (const name of representative) {
|
||||
const skillMd = path.join(skillsDir, name, 'SKILL.md');
|
||||
assert.ok(
|
||||
@@ -6818,7 +6977,7 @@ describe('bug-3582: Codex global install materializes the skill surface', { conc
|
||||
});
|
||||
|
||||
test('installed Codex skills do not ask agents to run bare gsd-tools commands', () => {
|
||||
const skillsDir = path.join(installRun.codexHome, 'skills');
|
||||
const skillsDir = codexSkillsRoot(installRun.codexHome);
|
||||
const skillDirs = fs.readdirSync(skillsDir, { withFileTypes: true })
|
||||
.filter(e => e.isDirectory() && e.name.startsWith('gsd-'))
|
||||
.map(e => e.name);
|
||||
@@ -7075,12 +7234,27 @@ const BUILD_HOOKS_SCRIPT = path.join(__dirname, '..', 'scripts', 'build-hooks.js
|
||||
|
||||
function withCodexHome(codexHome, fn) {
|
||||
const prev = process.env.CODEX_HOME;
|
||||
// #2088 (ADR-1239 upgrade 3): Codex skills now resolve an alternate install
|
||||
// home rooted at the REAL os.homedir() ($HOME/.agents), independent of
|
||||
// CODEX_HOME. Fake $HOME (and $USERPROFILE) too — using the sandbox root
|
||||
// (codexHome's parent, since codexHome is conventionally `<tmpRoot>/.codex`
|
||||
// in this file) — so this in-process install never touches the developer/CI
|
||||
// machine's real home directory. tmpRoot is reclaimed by the caller's afterEach.
|
||||
const prevHome = process.env.HOME;
|
||||
const prevUserProfile = process.env.USERPROFILE;
|
||||
const fakeHome = path.dirname(codexHome);
|
||||
process.env.CODEX_HOME = codexHome;
|
||||
process.env.HOME = fakeHome;
|
||||
process.env.USERPROFILE = fakeHome;
|
||||
try {
|
||||
return fn();
|
||||
} finally {
|
||||
if (prev == null) delete process.env.CODEX_HOME;
|
||||
else process.env.CODEX_HOME = prev;
|
||||
if (prevHome == null) delete process.env.HOME;
|
||||
else process.env.HOME = prevHome;
|
||||
if (prevUserProfile == null) delete process.env.USERPROFILE;
|
||||
else process.env.USERPROFILE = prevUserProfile;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
289
tests/codex-declarative-reference.test.cjs
Normal file
289
tests/codex-declarative-reference.test.cjs
Normal file
@@ -0,0 +1,289 @@
|
||||
// allow-test-rule: AC2 requires asserting no `runtime === 'codex'` string-equality and no positive `isCodex` branch remain in bin/install.js/src — the descriptor-migration contract is a property of the source text, so a source-grep is the only faithful check (#2088)
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* codex declarative reference host — ADR-1239 Phase D / #2088 (EoS/codex).
|
||||
*
|
||||
* Proves Codex is driven through the PUBLIC Host-Integration Interface (the
|
||||
* declarative embedding mode), that its negotiated axes classify + negotiate
|
||||
* correctly (including the documented `maxDepth === 1 → flat` dispatch
|
||||
* degradation), that negotiation fails CLOSED on a corrupted descriptor, that
|
||||
* the three Context7-verified UPGRADES land on the user-reachable surface
|
||||
* (skill-root → $HOME/.agents/skills, the 6 new hooks.json lifecycle events, and
|
||||
* explicit `[agents] max_depth` dispatch tuning), and that the migration retired
|
||||
* the hardcoded `runtime === 'codex'` / positive-`isCodex` projection (folded
|
||||
* into descriptor-driven `runtime.hostBehaviors`).
|
||||
*/
|
||||
|
||||
const { test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
|
||||
process.env.GSD_TEST_MODE = '1';
|
||||
|
||||
const { createImperativeAdapter } = require('../gsd-core/bin/lib/adapter-imperative.cjs');
|
||||
const { createDeclarativeAdapter } = require('../gsd-core/bin/lib/adapter-declarative.cjs');
|
||||
const {
|
||||
profileOf,
|
||||
negotiateHostCapabilities,
|
||||
degradationFor,
|
||||
PROFILE_BASELINES,
|
||||
UNDOCUMENTED,
|
||||
} = require('../gsd-core/bin/lib/host-integration.cjs');
|
||||
|
||||
const install = require('../bin/install.js');
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
|
||||
const CODEX_CAP = JSON.parse(
|
||||
fs.readFileSync(path.join(__dirname, '..', 'capabilities', 'codex', 'capability.json'), 'utf8'),
|
||||
);
|
||||
const CODEX_AXES = CODEX_CAP.runtime.hostIntegration;
|
||||
|
||||
// -- AC2: driven through the public interface (declarative embedding mode) ----
|
||||
|
||||
test('codex axes classify as the declarative-cli reference profile', () => {
|
||||
assert.equal(profileOf(CODEX_AXES), 'declarative-cli');
|
||||
});
|
||||
|
||||
test('createDeclarativeAdapter classifies codex as declarative + delegates install to the engine', () => {
|
||||
const adapter = createDeclarativeAdapter({ runtime: 'codex' });
|
||||
assert.equal(adapter.kind, 'declarative');
|
||||
assert.equal(adapter.runtime, 'codex');
|
||||
assert.equal(typeof adapter.install, 'function');
|
||||
assert.equal(typeof adapter.uninstall, 'function');
|
||||
});
|
||||
|
||||
test('the composed-registry install adapter drives codex install/uninstall', () => {
|
||||
const adapter = createImperativeAdapter({ runtime: 'codex' });
|
||||
assert.equal(adapter.kind, 'imperative');
|
||||
assert.equal(adapter.runtime, 'codex');
|
||||
assert.ok(adapter.registry && typeof adapter.registry === 'object');
|
||||
assert.equal(typeof adapter.install, 'function');
|
||||
assert.equal(typeof adapter.uninstall, 'function');
|
||||
});
|
||||
|
||||
// -- AC3: every negotiated axis populated (no undocumented sentinel) ----------
|
||||
|
||||
test('every codex hostIntegration axis is populated (zero undocumented sentinels)', () => {
|
||||
const axisVals = [
|
||||
CODEX_AXES.embeddingMode, CODEX_AXES.commandSurface, CODEX_AXES.modelMode,
|
||||
CODEX_AXES.hookBus, CODEX_AXES.stateIO, CODEX_AXES.transport, CODEX_AXES.runtime,
|
||||
];
|
||||
for (const v of axisVals) {
|
||||
assert.notEqual(v, UNDOCUMENTED, `axis must be documented, got ${v}`);
|
||||
assert.ok(typeof v === 'string' && v.length > 0);
|
||||
}
|
||||
const d = CODEX_AXES.dispatch;
|
||||
for (const k of ['namedDispatch', 'nested', 'maxDepth', 'subagentToolkit', 'background', 'backgroundDispatch']) {
|
||||
assert.notEqual(d[k], UNDOCUMENTED, `dispatch.${k} must be documented`);
|
||||
assert.notEqual(d[k], undefined, `dispatch.${k} must be present`);
|
||||
}
|
||||
assert.equal(d.embeddingMode, undefined); // sanity: dispatch has no stray keys
|
||||
});
|
||||
|
||||
// -- AC5: dispatch degrades to flat because maxDepth === 1 -------------------
|
||||
|
||||
test('dispatch degrades to FLAT for codex — maxDepth===1 even though nested/background are all true', () => {
|
||||
assert.equal(CODEX_AXES.dispatch.nested, true);
|
||||
assert.equal(CODEX_AXES.dispatch.background, true);
|
||||
assert.equal(CODEX_AXES.dispatch.backgroundDispatch, true);
|
||||
assert.equal(CODEX_AXES.dispatch.maxDepth, 1);
|
||||
|
||||
const flat = degradationFor('dispatch', CODEX_AXES);
|
||||
assert.equal(flat.level, 'degraded', 'maxDepth===1 must degrade dispatch, not grant full nesting');
|
||||
assert.match(flat.fallback, /flat dispatch/, 'the documented fallback is flat/inline waves');
|
||||
|
||||
// Prove maxDepth is the cause: at depth 2 the same axes grant full dispatch.
|
||||
const deeper = { ...CODEX_AXES, dispatch: { ...CODEX_AXES.dispatch, maxDepth: 2 } };
|
||||
assert.equal(degradationFor('dispatch', deeper).level, 'full');
|
||||
});
|
||||
|
||||
// -- AC5: negotiation fails CLOSED on a corrupted descriptor ------------------
|
||||
|
||||
test('negotiateHostCapabilities never throws for codex, even fully corrupted', () => {
|
||||
assert.doesNotThrow(() => negotiateHostCapabilities({}));
|
||||
assert.doesNotThrow(() => negotiateHostCapabilities({ ...CODEX_AXES, embeddingMode: UNDOCUMENTED }));
|
||||
assert.doesNotThrow(() => negotiateHostCapabilities({ ...CODEX_AXES, embeddingMode: 'future-unknown' }));
|
||||
});
|
||||
|
||||
test('a partial/empty codex descriptor degrades to the safe floor, not the declarative-cli baseline', () => {
|
||||
const result = negotiateHostCapabilities({});
|
||||
assert.equal(result.effective.embeddingMode, 'declarative', 'omitted embeddingMode degrades closed');
|
||||
assert.equal(result.effective.hookBus, 'none');
|
||||
assert.notDeepEqual(result.effective, PROFILE_BASELINES['declarative-cli']);
|
||||
assert.ok(result.warnings.length > 0);
|
||||
});
|
||||
|
||||
// -- AC2: the hardcoded projection is retired --------------------------------
|
||||
|
||||
test('codex descriptor declares runtime.hostBehaviors (the folded-in behaviors)', () => {
|
||||
const hb = CODEX_CAP.runtime.hostBehaviors;
|
||||
assert.ok(hb && typeof hb === 'object');
|
||||
assert.equal(hb.tomlConfigInstall, true, 'config.toml + agent-toml + hooks.json install runs through the descriptor gate');
|
||||
assert.equal(hb.cleanupSkillSidecars, true);
|
||||
assert.equal(hb.agentTomlFiles, true);
|
||||
assert.equal(hb.frontmatterDialect, 'codex');
|
||||
assert.equal(hb.reapplyCommand, '$gsd-update --reapply');
|
||||
});
|
||||
|
||||
test('no `runtime === "codex"` string-equality and no positive `isCodex` gate remain in the install source (AC2)', () => {
|
||||
const strip = (src) => src
|
||||
.replace(/\/\*[\s\S]*?\*\//g, '')
|
||||
.replace(/\/\/[^\r\n]*/g, '')
|
||||
.replace(/`[^`]*`/g, '');
|
||||
for (const rel of ['bin/install.js', 'src/install-engine.cts', 'src/runtime-artifact-conversion.cts']) {
|
||||
const raw = fs.readFileSync(path.join(__dirname, '..', rel), 'utf8');
|
||||
const src = strip(raw);
|
||||
|
||||
const stringEq = src.match(/runtime\s*[!=]==\s*'codex'/g) || [];
|
||||
assert.deepEqual(stringEq, [], `AC2: no hardcoded runtime==='codex' branch may remain in ${rel}; found: ${stringEq.join(', ')}`);
|
||||
|
||||
// Every `isCodex` reference must be either a runtimeFlags(...) destructure
|
||||
// line or a NEGATED occurrence inside a shared multi-runtime roster
|
||||
// (`!isCodex && !isCopilot && ...`). A positive `isCodex` gate is forbidden.
|
||||
for (const line of src.split(/\r?\n/)) {
|
||||
if (!/\bisCodex\b/.test(line)) continue;
|
||||
if (/runtimeFlags\s*\(/.test(line)) continue; // destructure declaration
|
||||
const positive = line.replace(/!\s*isCodex\b/g, '').match(/\bisCodex\b/);
|
||||
assert.equal(positive, null, `AC2: positive isCodex gate forbidden in ${rel}: ${line.trim()}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// -- AC4 upgrade 3: skill root is the canonical $HOME/.agents/skills ---------
|
||||
|
||||
test('upgrade 3 — codex skills resolve to $HOME/.agents/skills, not the deprecated $CODEX_HOME/skills', () => {
|
||||
const codexHome = path.join(os.homedir(), '.codex');
|
||||
const root = install._resolveSkillsRootDir('codex', codexHome, 'global');
|
||||
assert.equal(root, path.join(os.homedir(), '.agents', 'skills'),
|
||||
'skills must install to the canonical ~/.agents/skills root');
|
||||
assert.ok(!root.startsWith(codexHome), 'skills must NOT land under the deprecated $CODEX_HOME/skills');
|
||||
});
|
||||
|
||||
test('upgrade 3 — the pre-move location is migrated (stale ~/.codex/skills/gsd-* cleaned; user content preserved)', () => {
|
||||
const codexHome = path.join(os.homedir(), '.codex');
|
||||
const oldDir = install._resolveMovedSkillsOldDir('codex', codexHome, 'global');
|
||||
assert.equal(oldDir, path.join(codexHome, 'skills'), 'the pre-move location is $CODEX_HOME/skills');
|
||||
// A runtime with no home override yields null (no false migration).
|
||||
assert.equal(install._resolveMovedSkillsOldDir('kimi', path.join(os.homedir(), '.kimi'), 'global'), null);
|
||||
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'codex-migrate-'));
|
||||
const skillsDir = path.join(tmp, 'skills');
|
||||
fs.mkdirSync(path.join(skillsDir, 'gsd-plan'), { recursive: true });
|
||||
fs.mkdirSync(path.join(skillsDir, 'gsd-dev-preferences'), { recursive: true }); // user-owned, preserved
|
||||
fs.mkdirSync(path.join(skillsDir, 'my-own-skill'), { recursive: true }); // non-gsd, preserved
|
||||
try {
|
||||
const removed = install.cleanupMovedSkillsOldLocation(skillsDir, 'gsd-');
|
||||
assert.equal(removed, 1, 'only the managed gsd-plan dir is removed');
|
||||
assert.ok(!fs.existsSync(path.join(skillsDir, 'gsd-plan')), 'stale managed skill removed');
|
||||
assert.ok(fs.existsSync(path.join(skillsDir, 'gsd-dev-preferences')), 'user-owned gsd-dev-preferences preserved');
|
||||
assert.ok(fs.existsSync(path.join(skillsDir, 'my-own-skill')), 'non-gsd dir preserved');
|
||||
} finally {
|
||||
cleanup(tmp);
|
||||
}
|
||||
});
|
||||
|
||||
// -- AC4 upgrade 1: the 6 new hooks.json lifecycle events are registered ------
|
||||
|
||||
test('upgrade 1 — codex registers all documented hooks.json lifecycle events, incl. the 6 new in #2088', () => {
|
||||
const expected = [
|
||||
'SubagentStart', 'Stop', 'PostToolUse', // #772
|
||||
'PreToolUse', 'PermissionRequest', 'PreCompact', 'PostCompact', 'SubagentStop', 'UserPromptSubmit', // #2088
|
||||
];
|
||||
assert.deepEqual(install.CODEX_EXTENDED_HOOK_EVENTS, expected,
|
||||
'the shared install/uninstall event list must contain the 3 original + 6 new events');
|
||||
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'codex-hooks-'));
|
||||
try {
|
||||
fs.mkdirSync(path.join(tmp, 'hooks'), { recursive: true });
|
||||
fs.writeFileSync(path.join(tmp, 'hooks', 'gsd-context-monitor.js'), '// stub');
|
||||
fs.writeFileSync(path.join(tmp, 'hooks', 'gsd-check-update.js'), '// stub');
|
||||
for (const ev of install.CODEX_EXTENDED_HOOK_EVENTS) {
|
||||
install.ensureCodexHooksJsonEvent(tmp, ev, { absoluteRunner: '/usr/bin/node', platform: 'linux' });
|
||||
}
|
||||
install.ensureCodexHooksJsonSessionStart(tmp, { absoluteRunner: '/usr/bin/node', platform: 'linux' });
|
||||
const hooksJson = JSON.parse(fs.readFileSync(path.join(tmp, 'hooks.json'), 'utf8'));
|
||||
const registered = Object.keys(hooksJson.hooks || hooksJson || {});
|
||||
for (const ev of ['PreToolUse', 'PermissionRequest', 'PreCompact', 'PostCompact', 'SubagentStop', 'UserPromptSubmit']) {
|
||||
assert.ok(registered.includes(ev), `#2088 must register the ${ev} hook in hooks.json`);
|
||||
}
|
||||
assert.ok(registered.includes('SessionStart'), 'the SessionStart baseline stays registered');
|
||||
} finally {
|
||||
cleanup(tmp);
|
||||
}
|
||||
});
|
||||
|
||||
test('upgrade 1 — extendedHookEvents descriptor reconciled to the schema-valid wired subset (no longer [])', () => {
|
||||
assert.deepEqual(CODEX_CAP.runtime.extendedHookEvents, ['SubagentStop', 'Stop', 'PreCompact'],
|
||||
'reconciled from [] to the wired extended-lifecycle events expressible in the cross-runtime vocabulary');
|
||||
});
|
||||
|
||||
// -- AC4 upgrade 2: explicit [agents] max_depth dispatch tuning ---------------
|
||||
|
||||
test('upgrade 2 — the managed config block writes [agents] max_depth = 1', () => {
|
||||
const block = install.generateCodexConfigBlock(
|
||||
[{ name: 'gsd-foo', description: 'Foo' }, { name: 'gsd-bar', description: 'Bar' }],
|
||||
path.join(os.homedir(), '.codex'),
|
||||
);
|
||||
assert.match(block, /\[agents\]\nmax_depth = 1\n/, 'the block pins max_depth = 1 on a bare [agents] table');
|
||||
// The bare [agents] scalar table coexists with the [agents.gsd-*] role tables.
|
||||
assert.match(block, /\[agents\.gsd-foo\]/);
|
||||
});
|
||||
|
||||
test('upgrade 2 — validateCodexConfigSchema accepts the managed [agents] block but still rejects break-forms', () => {
|
||||
const block = install.generateCodexConfigBlock([{ name: 'gsd-foo', description: 'Foo' }], path.join(os.homedir(), '.codex'));
|
||||
assert.equal(install.validateCodexConfigSchema(block).ok, true, 'known-scalar [agents] + role tables must validate');
|
||||
|
||||
// Still rejects the actual #2760 break-forms.
|
||||
assert.equal(install.validateCodexConfigSchema('[[agents]]\nname = "x"\n').ok, false, '[[agents]] sequence still rejected');
|
||||
assert.equal(install.validateCodexConfigSchema('[agents]\ndefault = "x"\n').ok, false, 'bare [agents] with an unknown key still rejected');
|
||||
|
||||
// A user's own AgentsToml scalar table is now accepted (no longer over-rejected).
|
||||
assert.equal(install.validateCodexConfigSchema('[agents]\nmax_threads = 4\n').ok, true, 'user known-scalar [agents] accepted');
|
||||
assert.equal(install.codexBareAgentsHasOnlyKnownScalars('max_depth = 1\n'), true);
|
||||
assert.equal(install.codexBareAgentsHasOnlyKnownScalars('default = "x"\n'), false);
|
||||
});
|
||||
|
||||
test('upgrade 2 — uninstall removes the managed [agents] max_depth block', () => {
|
||||
const block = install.generateCodexConfigBlock([{ name: 'gsd-foo', description: 'Foo' }], path.join(os.homedir(), '.codex'));
|
||||
const stripped = install.stripGsdFromCodexConfig('model = "gpt-5"\n\n' + block);
|
||||
assert.ok(stripped === null || !/\[agents\]/.test(stripped), `uninstall must remove the managed [agents] block; got: ${JSON.stringify(stripped)}`);
|
||||
});
|
||||
|
||||
// Regression (#2088 review): install must NOT silently drop a user's own
|
||||
// AgentsToml scalar tuning when it purges the bare [agents] table to add max_depth.
|
||||
test('upgrade 2 — install preserves the user\'s own AgentsToml scalars (max_threads etc.), GSD-manages max_depth', () => {
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'codex-agents-merge-'));
|
||||
try {
|
||||
const cfgPath = path.join(tmp, 'config.toml');
|
||||
fs.writeFileSync(cfgPath, '[agents]\nmax_threads = 4\nmax_depth = 9\ninterrupt_message = false\n\n[model]\nname = "o3"\n');
|
||||
|
||||
// extract helper: user scalars except the GSD-managed max_depth.
|
||||
const scalars = install.extractCodexUserAgentsScalars(fs.readFileSync(cfgPath, 'utf8'));
|
||||
assert.ok(scalars.includes('max_threads = 4'), 'max_threads is a preserved user scalar');
|
||||
assert.ok(scalars.includes('interrupt_message = false'), 'interrupt_message is a preserved user scalar');
|
||||
assert.ok(!scalars.some((s) => s.startsWith('max_depth')), 'max_depth is GSD-managed, not preserved from the user');
|
||||
|
||||
install.mergeCodexConfig(cfgPath, install.generateCodexConfigBlock([{ name: 'gsd-foo', description: 'Foo' }], tmp));
|
||||
const merged = fs.readFileSync(cfgPath, 'utf8');
|
||||
|
||||
assert.match(merged, /max_threads = 4/, 'user max_threads must survive install');
|
||||
assert.match(merged, /interrupt_message = false/, 'user interrupt_message must survive install');
|
||||
assert.match(merged, /max_depth = 1/, 'GSD pins max_depth = 1');
|
||||
assert.doesNotMatch(merged, /max_depth = 9/, 'user max_depth is overridden by the GSD-managed value');
|
||||
assert.equal((merged.match(/^\[agents\]$/mg) || []).length, 1, 'exactly one managed [agents] table (no duplicate)');
|
||||
assert.equal(install.validateCodexConfigSchema(merged).ok, true, 'the merged config still validates');
|
||||
|
||||
// Symmetric: uninstall restores the user's scalars and drops GSD's max_depth.
|
||||
const uninstalled = install.stripGsdFromCodexConfig(merged);
|
||||
assert.match(uninstalled, /max_threads = 4/, 'uninstall restores the user\'s max_threads');
|
||||
assert.match(uninstalled, /interrupt_message = false/, 'uninstall restores interrupt_message');
|
||||
assert.doesNotMatch(uninstalled, /max_depth/, 'uninstall drops the GSD-managed max_depth');
|
||||
assert.doesNotMatch(uninstalled, /gsd-foo/, 'uninstall removes the gsd role table');
|
||||
assert.equal(install.validateCodexConfigSchema(uninstalled).ok, true, 'the restored config validates');
|
||||
} finally {
|
||||
cleanup(tmp);
|
||||
}
|
||||
});
|
||||
71
tests/cursor-dispatch-upgrade.test.cjs
Normal file
71
tests/cursor-dispatch-upgrade.test.cjs
Normal file
@@ -0,0 +1,71 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* cursor dispatch UPGRADE — ADR-1239 / #2089 AC4b.
|
||||
*
|
||||
* Proves GSD's wave-based execution drives Cursor's native named/background
|
||||
* nested subagent dispatch (background:true, backgroundDispatch:true,
|
||||
* nested:true, maxDepth:2) instead of flattening to inline sequential calls.
|
||||
*
|
||||
* Cite:
|
||||
* https://cursor.com/docs/subagents — named + background dispatch
|
||||
* https://cursor.com/docs/sdk/typescript — nested subagent depth-2 constraint
|
||||
*/
|
||||
|
||||
const { test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const { shouldFlattenDispatch } = require('../gsd-core/bin/lib/host-integration.cjs');
|
||||
|
||||
const CUR_CAP = JSON.parse(
|
||||
fs.readFileSync(path.join(__dirname, '..', 'capabilities', 'cursor', 'capability.json'), 'utf8'),
|
||||
);
|
||||
const CUR_DISPATCH = CUR_CAP.runtime.hostIntegration.dispatch;
|
||||
|
||||
// -- AC4b: cursor dispatch axes ----------------------------------------------
|
||||
|
||||
test('cursor dispatch declares namedDispatch + nested + background + backgroundDispatch', () => {
|
||||
assert.equal(CUR_DISPATCH.namedDispatch, true,
|
||||
'cite https://cursor.com/docs/subagents — named subagent invocation');
|
||||
assert.equal(CUR_DISPATCH.nested, true,
|
||||
'cite https://cursor.com/docs/sdk/typescript — nested subagents');
|
||||
assert.equal(CUR_DISPATCH.background, true,
|
||||
'cite https://cursor.com/docs/subagents — background dispatch');
|
||||
assert.equal(CUR_DISPATCH.backgroundDispatch, true,
|
||||
'cite https://cursor.com/docs/subagents FAQ — subagents can launch child subagents');
|
||||
});
|
||||
|
||||
test('cursor dispatch respects maxDepth: 2 (the documented constraint)', () => {
|
||||
assert.equal(CUR_DISPATCH.maxDepth, 2,
|
||||
'cite https://cursor.com/docs/sdk/typescript — "a subagent launched by another subagent can\'t launch further"');
|
||||
});
|
||||
|
||||
// -- AC4b: shouldFlattenDispatch returns false (NOT force-flattened) ----------
|
||||
|
||||
test('shouldFlattenDispatch(cursor) is false — GSD uses native background dispatch', () => {
|
||||
assert.equal(shouldFlattenDispatch(CUR_DISPATCH), false,
|
||||
'cursor has background:true + backgroundDispatch:true → GSD must NOT force-flatten');
|
||||
});
|
||||
|
||||
test('pre-upgrade cursor axes (background:false) DID force-flatten', () => {
|
||||
const preUpgrade = { ...CUR_DISPATCH, background: false, backgroundDispatch: 'undocumented' };
|
||||
assert.equal(shouldFlattenDispatch(preUpgrade), true,
|
||||
'pre-upgrade cursor (no background dispatch) was force-flattened — the behavioral change #2089 lands');
|
||||
});
|
||||
|
||||
test('shouldFlattenDispatch is true when backgroundDispatch is false (depth-2 but no bg dispatch)', () => {
|
||||
const noBgDispatch = { ...CUR_DISPATCH, backgroundDispatch: false };
|
||||
assert.equal(shouldFlattenDispatch(noBgDispatch), true,
|
||||
'background without backgroundDispatch still flattens (the #853 rule)');
|
||||
});
|
||||
|
||||
// -- AC4b: boundary — maxDepth 2 is the discriminator vs unbounded -----------
|
||||
|
||||
test('maxDepth 2 is the documented constraint (not -1 unbounded)', () => {
|
||||
assert.notEqual(CUR_DISPATCH.maxDepth, -1,
|
||||
'cursor is NOT unbounded — depth-2 is the documented hard limit');
|
||||
assert.ok(CUR_DISPATCH.maxDepth > 0 && CUR_DISPATCH.maxDepth <= 2,
|
||||
'maxDepth must be a positive integer ≤ 2 per cursor docs');
|
||||
});
|
||||
173
tests/cursor-hook-bus-upgrade.test.cjs
Normal file
173
tests/cursor-hook-bus-upgrade.test.cjs
Normal file
@@ -0,0 +1,173 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* cursor hook-bus UPGRADE — ADR-1239 / #2089 AC4a.
|
||||
*
|
||||
* Proves the expanded hook-bus coverage: GSD registers for subagentStart,
|
||||
* subagentStop, preToolUse, and stop IN ADDITION to the baseline sessionStart
|
||||
* and postToolUse. Cite: https://cursor.com/docs/hooks
|
||||
*
|
||||
* Tests the descriptor-driven adapter module (pure) + the reconcile behavior
|
||||
* (all 6 managed events in the generated hooks.json).
|
||||
*/
|
||||
|
||||
const { test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const os = require('node:os');
|
||||
|
||||
const {
|
||||
CURSOR_HOOK_EVENTS,
|
||||
CURSOR_EVENT_SCRIPT_MAP,
|
||||
resolveManagedHookEvents,
|
||||
resolveHookScripts,
|
||||
} = require('../gsd-core/bin/lib/host-integration-adapters/imperative-hook-bus.cjs');
|
||||
|
||||
const {
|
||||
reconcileCursorHooksJson,
|
||||
GSD_CURSOR_HOOK_MARKER,
|
||||
} = require('../bin/install.js');
|
||||
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
|
||||
const CUR_CAP = JSON.parse(
|
||||
fs.readFileSync(path.join(__dirname, '..', 'capabilities', 'cursor', 'capability.json'), 'utf8'),
|
||||
);
|
||||
|
||||
const EXPECTED_EVENTS = [
|
||||
'sessionStart',
|
||||
'postToolUse',
|
||||
'preToolUse',
|
||||
'stop',
|
||||
'subagentStart',
|
||||
'subagentStop',
|
||||
];
|
||||
|
||||
// -- AC4a: the adapter declares all 6 managed events -------------------------
|
||||
|
||||
test('CURSOR_HOOK_EVENTS contains all 6 managed events', () => {
|
||||
for (const ev of EXPECTED_EVENTS) {
|
||||
assert.ok(CURSOR_HOOK_EVENTS.includes(ev),
|
||||
`CURSOR_HOOK_EVENTS must include ${ev}`);
|
||||
}
|
||||
assert.equal(CURSOR_HOOK_EVENTS.length, 6,
|
||||
'exactly 6 managed events (no extras)');
|
||||
});
|
||||
|
||||
test('CURSOR_EVENT_SCRIPT_MAP maps every event to a script', () => {
|
||||
for (const ev of EXPECTED_EVENTS) {
|
||||
const script = CURSOR_EVENT_SCRIPT_MAP[ev];
|
||||
assert.ok(typeof script === 'string' && script.endsWith('.js'),
|
||||
`${ev} must map to a .js script, got: ${script}`);
|
||||
}
|
||||
});
|
||||
|
||||
test('descriptor managedHookEvents matches the adapter event list', () => {
|
||||
const declared = CUR_CAP.runtime.hostBehaviors.managedHookEvents;
|
||||
assert.deepEqual(declared.sort(), [...EXPECTED_EVENTS].sort(),
|
||||
'descriptor managedHookEvents must match the 6-event managed set');
|
||||
});
|
||||
|
||||
// -- AC4a: resolveManagedHookEvents + resolveHookScripts ---------------------
|
||||
|
||||
test('resolveManagedHookEvents returns all 6 from the descriptor list', () => {
|
||||
const resolved = resolveManagedHookEvents(CUR_CAP.runtime.hostBehaviors.managedHookEvents);
|
||||
assert.equal(resolved.length, 6);
|
||||
for (const ev of EXPECTED_EVENTS) {
|
||||
assert.ok(resolved.includes(ev), `resolveManagedHookEvents must include ${ev}`);
|
||||
}
|
||||
});
|
||||
|
||||
test('resolveManagedHookEvents filters unknown events (fail-closed)', () => {
|
||||
const resolved = resolveManagedHookEvents(['sessionStart', 'bogusEvent', 'stop']);
|
||||
assert.deepEqual([...resolved].sort(), ['sessionStart', 'stop']);
|
||||
});
|
||||
|
||||
test('resolveManagedHookEvents falls back to full set when descriptor is empty', () => {
|
||||
const resolved = resolveManagedHookEvents(null);
|
||||
assert.equal(resolved.length, 6);
|
||||
});
|
||||
|
||||
test('resolveHookScripts returns a script for every managed event', () => {
|
||||
const scripts = resolveHookScripts(EXPECTED_EVENTS);
|
||||
assert.equal(scripts.length, 6);
|
||||
for (const s of scripts) {
|
||||
assert.ok(s.startsWith('gsd-cursor-') && s.endsWith('.js'),
|
||||
`script must follow gsd-cursor-*.js convention: ${s}`);
|
||||
}
|
||||
});
|
||||
|
||||
// -- AC4a: hook scripts exist on disk ---------------------------------------
|
||||
|
||||
test('all 6 hook scripts exist under hooks/', () => {
|
||||
for (const ev of EXPECTED_EVENTS) {
|
||||
const script = CURSOR_EVENT_SCRIPT_MAP[ev];
|
||||
const scriptPath = path.join(__dirname, '..', 'hooks', script);
|
||||
assert.ok(fs.existsSync(scriptPath),
|
||||
`hook script must exist: hooks/${script} (event: ${ev})`);
|
||||
}
|
||||
});
|
||||
|
||||
// -- AC4a: reconcile generates hooks.json with all 6 events ------------------
|
||||
|
||||
test('reconcileCursorHooksJson writes all 6 managed events into hooks.json', (t) => {
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-cursor-hook-bus-'));
|
||||
t.after(() => cleanup(tmpDir));
|
||||
const hooksJsonPath = path.join(tmpDir, 'hooks.json');
|
||||
const managedEntries = {};
|
||||
for (const ev of EXPECTED_EVENTS) {
|
||||
managedEntries[ev] = {
|
||||
type: 'command',
|
||||
command: `node /fake/${ev}.js`,
|
||||
[GSD_CURSOR_HOOK_MARKER]: true,
|
||||
};
|
||||
}
|
||||
const result = reconcileCursorHooksJson(hooksJsonPath, managedEntries);
|
||||
assert.ok(result.changed, 'first write must report changed=true');
|
||||
|
||||
const written = JSON.parse(fs.readFileSync(hooksJsonPath, 'utf8'));
|
||||
const hookTable = written.hooks;
|
||||
assert.ok(hookTable && typeof hookTable === 'object');
|
||||
for (const ev of EXPECTED_EVENTS) {
|
||||
assert.ok(Array.isArray(hookTable[ev]),
|
||||
`hooks.json must have a ${ev} array`);
|
||||
assert.equal(hookTable[ev].length, 1,
|
||||
`${ev} must have exactly 1 managed entry`);
|
||||
assert.equal(hookTable[ev][0][GSD_CURSOR_HOOK_MARKER], true,
|
||||
`${ev} entry must carry the GSD managed marker`);
|
||||
}
|
||||
});
|
||||
|
||||
test('reconcileCursorHooksJson preserves user entries across all 6 events', (t) => {
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-cursor-hook-bus-'));
|
||||
t.after(() => cleanup(tmpDir));
|
||||
const hooksJsonPath = path.join(tmpDir, 'hooks.json');
|
||||
// Seed with user-owned entries in two events.
|
||||
const seed = {
|
||||
version: 1,
|
||||
hooks: {
|
||||
sessionStart: [{ type: 'command', command: 'user-start.sh' }],
|
||||
preToolUse: [{ type: 'command', command: 'user-pre.sh' }],
|
||||
},
|
||||
};
|
||||
fs.writeFileSync(hooksJsonPath, JSON.stringify(seed, null, 2) + '\n');
|
||||
|
||||
const managedEntries = {};
|
||||
for (const ev of EXPECTED_EVENTS) {
|
||||
managedEntries[ev] = {
|
||||
type: 'command',
|
||||
command: `node /gsd/${ev}.js`,
|
||||
[GSD_CURSOR_HOOK_MARKER]: true,
|
||||
};
|
||||
}
|
||||
reconcileCursorHooksJson(hooksJsonPath, managedEntries);
|
||||
|
||||
const written = JSON.parse(fs.readFileSync(hooksJsonPath, 'utf8'));
|
||||
// sessionStart: 1 user + 1 managed
|
||||
assert.equal(written.hooks.sessionStart.length, 2);
|
||||
// preToolUse: 1 user + 1 managed
|
||||
assert.equal(written.hooks.preToolUse.length, 2);
|
||||
// postToolUse: 1 managed only
|
||||
assert.equal(written.hooks.postToolUse.length, 1);
|
||||
});
|
||||
126
tests/cursor-imperative-reference.test.cjs
Normal file
126
tests/cursor-imperative-reference.test.cjs
Normal file
@@ -0,0 +1,126 @@
|
||||
// allow-test-rule: AC2 requires asserting no `runtime === 'cursor'` string-equality branch remains in bin/install.js/src — the descriptor-migration contract is a property of the source text, so a source-grep is the only faithful check (#2089)
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* cursor imperative reference host — ADR-1239 Phase D / #2089 (EoS/cursor).
|
||||
*
|
||||
* Proves cursor is driven through the PUBLIC Host-Integration Interface (the
|
||||
* imperative adapter), that its negotiated axes classify + negotiate correctly,
|
||||
* that negotiation fails CLOSED on a corrupted descriptor, that the Context7-
|
||||
* verified dispatch UPGRADE (named/background nested subagents) changes
|
||||
* `shouldFlattenDispatch`, and that the migration retired the hardcoded
|
||||
* `runtime === 'cursor'` / `isCursor` branches (folded into descriptor-driven
|
||||
* `runtime.hostBehaviors`).
|
||||
*/
|
||||
|
||||
const { test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const { createImperativeAdapter } = require('../gsd-core/bin/lib/adapter-imperative.cjs');
|
||||
const {
|
||||
profileOf,
|
||||
negotiateHostCapabilities,
|
||||
shouldFlattenDispatch,
|
||||
PROFILE_BASELINES,
|
||||
UNDOCUMENTED,
|
||||
} = require('../gsd-core/bin/lib/host-integration.cjs');
|
||||
|
||||
const CUR_CAP = JSON.parse(
|
||||
fs.readFileSync(path.join(__dirname, '..', 'capabilities', 'cursor', 'capability.json'), 'utf8'),
|
||||
);
|
||||
const CUR_AXES = CUR_CAP.runtime.hostIntegration;
|
||||
|
||||
// -- AC2: driven through the public interface (imperative adapter) -----------
|
||||
|
||||
test('createImperativeAdapter classifies cursor as imperative + composes the registry', () => {
|
||||
const adapter = createImperativeAdapter({ runtime: 'cursor' });
|
||||
assert.equal(adapter.kind, 'imperative');
|
||||
assert.equal(adapter.runtime, 'cursor');
|
||||
assert.ok(adapter.registry && typeof adapter.registry === 'object');
|
||||
assert.equal(typeof adapter.install, 'function');
|
||||
assert.equal(typeof adapter.uninstall, 'function');
|
||||
});
|
||||
|
||||
test('cursor axes classify as the programmatic-cli reference profile', () => {
|
||||
assert.equal(profileOf(CUR_AXES), 'programmatic-cli');
|
||||
});
|
||||
|
||||
// -- AC3: all axes populated + validated -------------------------------------
|
||||
|
||||
test('cursor descriptor declares all 8 axes + 6 dispatch sub-axes (no undocumented)', () => {
|
||||
assert.equal(CUR_AXES.embeddingMode, 'imperative');
|
||||
assert.equal(CUR_AXES.commandSurface, 'slash-file');
|
||||
assert.equal(CUR_AXES.modelMode, 'passive');
|
||||
assert.equal(CUR_AXES.hookBus, 'host');
|
||||
assert.equal(CUR_AXES.stateIO, 'filesystem');
|
||||
assert.equal(CUR_AXES.transport, 'mcp');
|
||||
assert.equal(CUR_AXES.runtime, 'node');
|
||||
const d = CUR_AXES.dispatch;
|
||||
assert.equal(d.namedDispatch, true);
|
||||
assert.equal(d.nested, true);
|
||||
assert.equal(d.maxDepth, 2);
|
||||
assert.equal(d.background, true);
|
||||
assert.equal(d.subagentToolkit, 'full');
|
||||
assert.equal(d.backgroundDispatch, true);
|
||||
});
|
||||
|
||||
// -- AC4b: the Context7-verified dispatch UPGRADE (named/background nested) ---
|
||||
|
||||
test('cursor descriptor declares background dispatch true/true + nested + maxDepth 2', () => {
|
||||
assert.equal(CUR_AXES.dispatch.background, true);
|
||||
assert.equal(CUR_AXES.dispatch.backgroundDispatch, true);
|
||||
assert.equal(CUR_AXES.dispatch.nested, true);
|
||||
assert.equal(CUR_AXES.dispatch.maxDepth, 2, 'cite https://cursor.com/docs/sdk/typescript');
|
||||
});
|
||||
|
||||
test('dispatch UPGRADE changes shouldFlattenDispatch: false now (may background), true for pre-upgrade axes', () => {
|
||||
assert.equal(shouldFlattenDispatch(CUR_AXES.dispatch), false,
|
||||
'with background:true+backgroundDispatch:true, GSD must NOT force-flatten cursor dispatch');
|
||||
const preUpgrade = { ...CUR_AXES.dispatch, background: false, backgroundDispatch: 'undocumented' };
|
||||
assert.equal(shouldFlattenDispatch(preUpgrade), true,
|
||||
'pre-upgrade (background:false) cursor was force-flattened — this is the behavioral change #2089 lands');
|
||||
});
|
||||
|
||||
// -- AC5: negotiation fails CLOSED on a corrupted descriptor ------------------
|
||||
|
||||
test('negotiateHostCapabilities never throws for cursor, even fully corrupted', () => {
|
||||
assert.doesNotThrow(() => negotiateHostCapabilities({}));
|
||||
assert.doesNotThrow(() => negotiateHostCapabilities({ ...CUR_AXES, embeddingMode: UNDOCUMENTED }));
|
||||
assert.doesNotThrow(() => negotiateHostCapabilities({ ...CUR_AXES, embeddingMode: 'future-unknown' }));
|
||||
});
|
||||
|
||||
test('a partial/empty cursor descriptor degrades to the safe floor, not the programmatic-cli baseline', () => {
|
||||
const result = negotiateHostCapabilities({});
|
||||
assert.equal(result.effective.embeddingMode, 'declarative', 'omitted embeddingMode degrades closed');
|
||||
assert.equal(result.effective.hookBus, 'none');
|
||||
assert.notDeepEqual(result.effective, PROFILE_BASELINES['programmatic-cli']);
|
||||
assert.ok(result.warnings.length > 0);
|
||||
});
|
||||
|
||||
// -- AC2: the hardcoded branches are retired ---------------------------------
|
||||
|
||||
test('cursor descriptor declares runtime.hostBehaviors (the folded-in behaviors)', () => {
|
||||
const hb = CUR_CAP.runtime.hostBehaviors;
|
||||
assert.ok(hb && typeof hb === 'object');
|
||||
assert.equal(hb.reapplyCommand, 'gsd-update --reapply (mention the skill name)');
|
||||
assert.equal(hb.frontmatterDialect, 'cursor');
|
||||
assert.equal(hb.hooksJsonSurface, true);
|
||||
assert.equal(hb.skipSharedHooksInstall, true);
|
||||
assert.equal(hb.reportCommandsDir, true);
|
||||
assert.ok(Array.isArray(hb.managedHookEvents) && hb.managedHookEvents.length >= 6,
|
||||
'managedHookEvents must list at least 6 events (AC4a)');
|
||||
});
|
||||
|
||||
test('no `runtime === "cursor"` string-equality branch remains in the install source (AC2)', () => {
|
||||
const strip = (src) => src
|
||||
.replace(/\/\*[\s\S]*?\*\//g, '')
|
||||
.replace(/\/\/[^\r\n]*/g, '')
|
||||
.replace(/`[^`]*`/g, '');
|
||||
for (const rel of ['bin/install.js', 'src/install-engine.cts', 'src/runtime-artifact-conversion.cts', 'src/runtime-hooks-surface.cts']) {
|
||||
const src = fs.readFileSync(path.join(__dirname, '..', rel), 'utf8');
|
||||
const offenders = strip(src).match(/runtime\s*[!=]==\s*'cursor'/g) || [];
|
||||
assert.deepEqual(offenders, [], `AC2: no hardcoded runtime==='cursor' branch may remain in ${rel}; found: ${offenders.join(', ')}`);
|
||||
}
|
||||
});
|
||||
@@ -315,7 +315,11 @@
|
||||
"hooks/gsd-config-reload.js": "96546e0e8bb47904",
|
||||
"hooks/gsd-context-monitor.js": "6d81d7326e5b2710",
|
||||
"hooks/gsd-cursor-post-tool.js": "9168e0a09de1972a",
|
||||
"hooks/gsd-cursor-pre-tool.js": "873998b25e308c29",
|
||||
"hooks/gsd-cursor-session-start.js": "9b2e6f4f0c405375",
|
||||
"hooks/gsd-cursor-stop.js": "bfaaf60f419e3238",
|
||||
"hooks/gsd-cursor-subagent-start.js": "06d77fde5c1372b6",
|
||||
"hooks/gsd-cursor-subagent-stop.js": "4bbf22917da4d389",
|
||||
"hooks/gsd-ensure-canonical-path.js": "64d092d7e4a01211",
|
||||
"hooks/gsd-graphify-update.sh": "e4c6e14fe6ad64ff",
|
||||
"hooks/gsd-phase-boundary.sh": "32739d5fbe0d0a1c",
|
||||
@@ -330,7 +334,7 @@
|
||||
"hooks/gsd-worktree-path-guard.js": "838498aa91619740",
|
||||
"hooks/lib/git-cmd.js": "268ba15992ca0b23",
|
||||
"hooks/lib/gsd-graphify-rebuild.sh": "66af89601074d2a9",
|
||||
"hooks/managed-hooks-registry.cjs": "45b2431992d3d7d2",
|
||||
"hooks/managed-hooks-registry.cjs": "721d696556b7509f",
|
||||
"package.json": "dbf8353f77358bc1",
|
||||
"scripts/changeset/README.md": "86ff89331dfd94b2",
|
||||
"scripts/changeset/cli.cjs": "68f92a344b199271",
|
||||
|
||||
@@ -386,7 +386,11 @@
|
||||
"hooks/gsd-config-reload.js": "96546e0e8bb47904",
|
||||
"hooks/gsd-context-monitor.js": "44ff1bbf292747af",
|
||||
"hooks/gsd-cursor-post-tool.js": "9168e0a09de1972a",
|
||||
"hooks/gsd-cursor-pre-tool.js": "873998b25e308c29",
|
||||
"hooks/gsd-cursor-session-start.js": "9b2e6f4f0c405375",
|
||||
"hooks/gsd-cursor-stop.js": "bfaaf60f419e3238",
|
||||
"hooks/gsd-cursor-subagent-start.js": "06d77fde5c1372b6",
|
||||
"hooks/gsd-cursor-subagent-stop.js": "4bbf22917da4d389",
|
||||
"hooks/gsd-ensure-canonical-path.js": "d569f5f3578e93e5",
|
||||
"hooks/gsd-graphify-update.sh": "e4c6e14fe6ad64ff",
|
||||
"hooks/gsd-phase-boundary.sh": "32739d5fbe0d0a1c",
|
||||
@@ -401,7 +405,7 @@
|
||||
"hooks/gsd-worktree-path-guard.js": "65b934c3a1709e89",
|
||||
"hooks/lib/git-cmd.js": "268ba15992ca0b23",
|
||||
"hooks/lib/gsd-graphify-rebuild.sh": "66af89601074d2a9",
|
||||
"hooks/managed-hooks-registry.cjs": "f46a329fcfefa465",
|
||||
"hooks/managed-hooks-registry.cjs": "e61da0f7a3037c35",
|
||||
"package.json": "dbf8353f77358bc1",
|
||||
"scripts/changeset/README.md": "86ff89331dfd94b2",
|
||||
"scripts/changeset/cli.cjs": "68f92a344b199271",
|
||||
|
||||
@@ -385,7 +385,11 @@
|
||||
"hooks/gsd-config-reload.js": "96546e0e8bb47904",
|
||||
"hooks/gsd-context-monitor.js": "ecbe9747e4a442e0",
|
||||
"hooks/gsd-cursor-post-tool.js": "8a8a249c0642cc71",
|
||||
"hooks/gsd-cursor-pre-tool.js": "8cb8e8f895edaec9",
|
||||
"hooks/gsd-cursor-session-start.js": "05a14e903c5edafa",
|
||||
"hooks/gsd-cursor-stop.js": "d33be8ac96f4081d",
|
||||
"hooks/gsd-cursor-subagent-start.js": "d773df8caa605de2",
|
||||
"hooks/gsd-cursor-subagent-stop.js": "8ee488d826bf3c37",
|
||||
"hooks/gsd-ensure-canonical-path.js": "b4b3b88a0e493b16",
|
||||
"hooks/gsd-graphify-update.sh": "e4c6e14fe6ad64ff",
|
||||
"hooks/gsd-phase-boundary.sh": "32739d5fbe0d0a1c",
|
||||
@@ -400,7 +404,7 @@
|
||||
"hooks/gsd-worktree-path-guard.js": "02be1bb504b22eb5",
|
||||
"hooks/lib/git-cmd.js": "268ba15992ca0b23",
|
||||
"hooks/lib/gsd-graphify-rebuild.sh": "66af89601074d2a9",
|
||||
"hooks/managed-hooks-registry.cjs": "ea876b1ec185173e",
|
||||
"hooks/managed-hooks-registry.cjs": "f2e325aa9ba31647",
|
||||
"package.json": "dbf8353f77358bc1",
|
||||
"scripts/changeset/README.md": "86ff89331dfd94b2",
|
||||
"scripts/changeset/cli.cjs": "68f92a344b199271",
|
||||
|
||||
@@ -314,7 +314,11 @@
|
||||
"hooks/gsd-config-reload.js": "96546e0e8bb47904",
|
||||
"hooks/gsd-context-monitor.js": "ecbe9747e4a442e0",
|
||||
"hooks/gsd-cursor-post-tool.js": "8a8a249c0642cc71",
|
||||
"hooks/gsd-cursor-pre-tool.js": "8cb8e8f895edaec9",
|
||||
"hooks/gsd-cursor-session-start.js": "05a14e903c5edafa",
|
||||
"hooks/gsd-cursor-stop.js": "d33be8ac96f4081d",
|
||||
"hooks/gsd-cursor-subagent-start.js": "d773df8caa605de2",
|
||||
"hooks/gsd-cursor-subagent-stop.js": "8ee488d826bf3c37",
|
||||
"hooks/gsd-ensure-canonical-path.js": "b4b3b88a0e493b16",
|
||||
"hooks/gsd-graphify-update.sh": "e4c6e14fe6ad64ff",
|
||||
"hooks/gsd-phase-boundary.sh": "32739d5fbe0d0a1c",
|
||||
@@ -329,7 +333,7 @@
|
||||
"hooks/gsd-worktree-path-guard.js": "02be1bb504b22eb5",
|
||||
"hooks/lib/git-cmd.js": "268ba15992ca0b23",
|
||||
"hooks/lib/gsd-graphify-rebuild.sh": "66af89601074d2a9",
|
||||
"hooks/managed-hooks-registry.cjs": "ea876b1ec185173e",
|
||||
"hooks/managed-hooks-registry.cjs": "f2e325aa9ba31647",
|
||||
"package.json": "dbf8353f77358bc1",
|
||||
"scripts/changeset/README.md": "86ff89331dfd94b2",
|
||||
"scripts/changeset/cli.cjs": "68f92a344b199271",
|
||||
|
||||
@@ -386,7 +386,11 @@
|
||||
"hooks/gsd-config-reload.js": "96546e0e8bb47904",
|
||||
"hooks/gsd-context-monitor.js": "f372804867cabe40",
|
||||
"hooks/gsd-cursor-post-tool.js": "9168e0a09de1972a",
|
||||
"hooks/gsd-cursor-pre-tool.js": "873998b25e308c29",
|
||||
"hooks/gsd-cursor-session-start.js": "9b2e6f4f0c405375",
|
||||
"hooks/gsd-cursor-stop.js": "bfaaf60f419e3238",
|
||||
"hooks/gsd-cursor-subagent-start.js": "06d77fde5c1372b6",
|
||||
"hooks/gsd-cursor-subagent-stop.js": "4bbf22917da4d389",
|
||||
"hooks/gsd-ensure-canonical-path.js": "434887487ae63ec5",
|
||||
"hooks/gsd-graphify-update.sh": "e4c6e14fe6ad64ff",
|
||||
"hooks/gsd-phase-boundary.sh": "32739d5fbe0d0a1c",
|
||||
@@ -401,7 +405,7 @@
|
||||
"hooks/gsd-worktree-path-guard.js": "548fc57131a04fa7",
|
||||
"hooks/lib/git-cmd.js": "268ba15992ca0b23",
|
||||
"hooks/lib/gsd-graphify-rebuild.sh": "66af89601074d2a9",
|
||||
"hooks/managed-hooks-registry.cjs": "0368fd4ac7bb3d1d",
|
||||
"hooks/managed-hooks-registry.cjs": "0e7a61bde8688e11",
|
||||
"package.json": "dbf8353f77358bc1",
|
||||
"scripts/changeset/README.md": "86ff89331dfd94b2",
|
||||
"scripts/changeset/cli.cjs": "68f92a344b199271",
|
||||
|
||||
146
tests/fixtures/golden-install-parity/codex.json
vendored
146
tests/fixtures/golden-install-parity/codex.json
vendored
@@ -1,4 +1,75 @@
|
||||
{
|
||||
".agents/skills/gsd-add-tests/SKILL.md": "f3c1594a059de3ee",
|
||||
".agents/skills/gsd-ai-integration-phase/SKILL.md": "ba4b5f406db6de96",
|
||||
".agents/skills/gsd-audit-fix/SKILL.md": "9cea2764b92aa352",
|
||||
".agents/skills/gsd-audit-milestone/SKILL.md": "0528ef8a5834b5df",
|
||||
".agents/skills/gsd-audit-uat/SKILL.md": "6c2aa0c2b62c5233",
|
||||
".agents/skills/gsd-autonomous/SKILL.md": "d56dc692750f4926",
|
||||
".agents/skills/gsd-capture/SKILL.md": "211d601d122b5dd6",
|
||||
".agents/skills/gsd-cleanup/SKILL.md": "18cba17463c6ef97",
|
||||
".agents/skills/gsd-code-review/SKILL.md": "74749a1aff4224d7",
|
||||
".agents/skills/gsd-complete-milestone/SKILL.md": "77bee8741f8cb381",
|
||||
".agents/skills/gsd-config/SKILL.md": "5fde92e30619edcb",
|
||||
".agents/skills/gsd-debug/SKILL.md": "7e56964e14fd0a10",
|
||||
".agents/skills/gsd-discuss-phase/SKILL.md": "837d46c55cc2424e",
|
||||
".agents/skills/gsd-docs-update/SKILL.md": "5028dc5dd44bee7d",
|
||||
".agents/skills/gsd-eval-review/SKILL.md": "a300e78a27ba74d6",
|
||||
".agents/skills/gsd-execute-phase/SKILL.md": "e77bee13068600d3",
|
||||
".agents/skills/gsd-explore/SKILL.md": "d660cde0e6f31ebf",
|
||||
".agents/skills/gsd-extract-learnings/SKILL.md": "e32388999da384e4",
|
||||
".agents/skills/gsd-fast/SKILL.md": "14c407a76a40e115",
|
||||
".agents/skills/gsd-forensics/SKILL.md": "6d496a16f5ca74b3",
|
||||
".agents/skills/gsd-graphify/SKILL.md": "1bbd96129fbacc5c",
|
||||
".agents/skills/gsd-health/SKILL.md": "656c9c63852e3787",
|
||||
".agents/skills/gsd-help/SKILL.md": "28eb8d0b487239f8",
|
||||
".agents/skills/gsd-import/SKILL.md": "4d15ee09e531342f",
|
||||
".agents/skills/gsd-inbox/SKILL.md": "1315033595643485",
|
||||
".agents/skills/gsd-ingest-docs/SKILL.md": "5bd2838bf0b6dc1b",
|
||||
".agents/skills/gsd-manager/SKILL.md": "cb1cf56f5d3f66d6",
|
||||
".agents/skills/gsd-map-codebase/SKILL.md": "cd27dc028718426b",
|
||||
".agents/skills/gsd-mempalace-capture/SKILL.md": "1a7fac4d53f607ab",
|
||||
".agents/skills/gsd-mempalace-recall/SKILL.md": "7205b02250e89f25",
|
||||
".agents/skills/gsd-milestone-summary/SKILL.md": "af84ecb400f23556",
|
||||
".agents/skills/gsd-mvp-phase/SKILL.md": "72b65ae927b280fe",
|
||||
".agents/skills/gsd-new-milestone/SKILL.md": "587574d2d475bb10",
|
||||
".agents/skills/gsd-new-project/SKILL.md": "129ac16e6a1f04ba",
|
||||
".agents/skills/gsd-next/SKILL.md": "7855fd70e387087e",
|
||||
".agents/skills/gsd-ns-context/SKILL.md": "9b496da79789b3f9",
|
||||
".agents/skills/gsd-ns-ideate/SKILL.md": "84ca1cde06110981",
|
||||
".agents/skills/gsd-ns-manage/SKILL.md": "909dafa0cd19ba5a",
|
||||
".agents/skills/gsd-ns-project/SKILL.md": "936b6998d42520ac",
|
||||
".agents/skills/gsd-ns-review/SKILL.md": "022253010db0e072",
|
||||
".agents/skills/gsd-ns-workflow/SKILL.md": "cba075bc819b539e",
|
||||
".agents/skills/gsd-onboard/SKILL.md": "f42fc2edebeda671",
|
||||
".agents/skills/gsd-pause-work/SKILL.md": "b379469eed78a196",
|
||||
".agents/skills/gsd-phase/SKILL.md": "25477edc97a90c91",
|
||||
".agents/skills/gsd-plan-phase/SKILL.md": "19ead1acb151a868",
|
||||
".agents/skills/gsd-plan-review-convergence/SKILL.md": "f654089c11024027",
|
||||
".agents/skills/gsd-pr-branch/SKILL.md": "6901da15e321913e",
|
||||
".agents/skills/gsd-profile-user/SKILL.md": "6259fabfb6afe7be",
|
||||
".agents/skills/gsd-progress/SKILL.md": "85d76286162b9189",
|
||||
".agents/skills/gsd-quick/SKILL.md": "b4f4e711ba664aa0",
|
||||
".agents/skills/gsd-resume-work/SKILL.md": "04f6c2e5b579e8c4",
|
||||
".agents/skills/gsd-review-backlog/SKILL.md": "469696b944c4e7b5",
|
||||
".agents/skills/gsd-review/SKILL.md": "a45ab2fdca06793b",
|
||||
".agents/skills/gsd-secure-phase/SKILL.md": "e64ad269c1e6e319",
|
||||
".agents/skills/gsd-settings/SKILL.md": "fcdda8dd545622ae",
|
||||
".agents/skills/gsd-ship/SKILL.md": "9615cc4c8f6de060",
|
||||
".agents/skills/gsd-sketch/SKILL.md": "90c219b843db7ec7",
|
||||
".agents/skills/gsd-spec-phase/SKILL.md": "56a5cbd606db9cba",
|
||||
".agents/skills/gsd-spike/SKILL.md": "77209fef7a04c11a",
|
||||
".agents/skills/gsd-stats/SKILL.md": "566024444ef71f44",
|
||||
".agents/skills/gsd-surface/SKILL.md": "492cda98187a969b",
|
||||
".agents/skills/gsd-thread/SKILL.md": "85326c97c83a02d1",
|
||||
".agents/skills/gsd-ui-phase/SKILL.md": "a0c8fbcbe5e3c2b9",
|
||||
".agents/skills/gsd-ui-review/SKILL.md": "081a3292a2d357f5",
|
||||
".agents/skills/gsd-ultraplan-phase/SKILL.md": "06fb3d76eb785f94",
|
||||
".agents/skills/gsd-undo/SKILL.md": "007d3b307e027af9",
|
||||
".agents/skills/gsd-update/SKILL.md": "e6e49e117c7c8f35",
|
||||
".agents/skills/gsd-validate-phase/SKILL.md": "373059517a94a194",
|
||||
".agents/skills/gsd-verify-work/SKILL.md": "4272275698e9a3fe",
|
||||
".agents/skills/gsd-workspace/SKILL.md": "06d6400d68318361",
|
||||
".agents/skills/gsd-workstreams/SKILL.md": "2f77bb94db1be1a4",
|
||||
".gsd-profile": "0e716a5fef4e6dc1",
|
||||
".gsd/defaults.json": "560664b045e645cb",
|
||||
"agents/gsd-advisor-researcher.md": "eea6d1604aaf305c",
|
||||
@@ -69,7 +140,7 @@
|
||||
"agents/gsd-user-profiler.toml": "b9c244bb8fbf8140",
|
||||
"agents/gsd-verifier.md": "4ac4b860e2504374",
|
||||
"agents/gsd-verifier.toml": "8ed9fb961409e894",
|
||||
"config.toml": "a34316b9ac61a620",
|
||||
"config.toml": "fa48d84b92174890",
|
||||
"gsd-core/VERSION": "ef0deccd81a6723c",
|
||||
"gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74",
|
||||
"gsd-core/bin/ensure-runtime-build.cjs": "51bc64467ab30f62",
|
||||
@@ -359,76 +430,5 @@
|
||||
"scripts/gen-capability-registry.cjs": "c52201ff4d1c2cd7",
|
||||
"scripts/gen-loop-host-contract.cjs": "c7f15237234811a0",
|
||||
"scripts/lib/allowlist-ratchet.cjs": "ffaceaac3efc2660",
|
||||
"scripts/lib/cli-exit.cjs": "612d0c372c75b7e7",
|
||||
"skills/gsd-add-tests/SKILL.md": "f3c1594a059de3ee",
|
||||
"skills/gsd-ai-integration-phase/SKILL.md": "ba4b5f406db6de96",
|
||||
"skills/gsd-audit-fix/SKILL.md": "9cea2764b92aa352",
|
||||
"skills/gsd-audit-milestone/SKILL.md": "0528ef8a5834b5df",
|
||||
"skills/gsd-audit-uat/SKILL.md": "6c2aa0c2b62c5233",
|
||||
"skills/gsd-autonomous/SKILL.md": "d56dc692750f4926",
|
||||
"skills/gsd-capture/SKILL.md": "211d601d122b5dd6",
|
||||
"skills/gsd-cleanup/SKILL.md": "18cba17463c6ef97",
|
||||
"skills/gsd-code-review/SKILL.md": "74749a1aff4224d7",
|
||||
"skills/gsd-complete-milestone/SKILL.md": "77bee8741f8cb381",
|
||||
"skills/gsd-config/SKILL.md": "5fde92e30619edcb",
|
||||
"skills/gsd-debug/SKILL.md": "7e56964e14fd0a10",
|
||||
"skills/gsd-discuss-phase/SKILL.md": "837d46c55cc2424e",
|
||||
"skills/gsd-docs-update/SKILL.md": "5028dc5dd44bee7d",
|
||||
"skills/gsd-eval-review/SKILL.md": "a300e78a27ba74d6",
|
||||
"skills/gsd-execute-phase/SKILL.md": "e77bee13068600d3",
|
||||
"skills/gsd-explore/SKILL.md": "d660cde0e6f31ebf",
|
||||
"skills/gsd-extract-learnings/SKILL.md": "e32388999da384e4",
|
||||
"skills/gsd-fast/SKILL.md": "14c407a76a40e115",
|
||||
"skills/gsd-forensics/SKILL.md": "6d496a16f5ca74b3",
|
||||
"skills/gsd-graphify/SKILL.md": "1bbd96129fbacc5c",
|
||||
"skills/gsd-health/SKILL.md": "656c9c63852e3787",
|
||||
"skills/gsd-help/SKILL.md": "28eb8d0b487239f8",
|
||||
"skills/gsd-import/SKILL.md": "4d15ee09e531342f",
|
||||
"skills/gsd-inbox/SKILL.md": "1315033595643485",
|
||||
"skills/gsd-ingest-docs/SKILL.md": "5bd2838bf0b6dc1b",
|
||||
"skills/gsd-manager/SKILL.md": "cb1cf56f5d3f66d6",
|
||||
"skills/gsd-map-codebase/SKILL.md": "cd27dc028718426b",
|
||||
"skills/gsd-mempalace-capture/SKILL.md": "1a7fac4d53f607ab",
|
||||
"skills/gsd-mempalace-recall/SKILL.md": "7205b02250e89f25",
|
||||
"skills/gsd-milestone-summary/SKILL.md": "af84ecb400f23556",
|
||||
"skills/gsd-mvp-phase/SKILL.md": "72b65ae927b280fe",
|
||||
"skills/gsd-new-milestone/SKILL.md": "587574d2d475bb10",
|
||||
"skills/gsd-new-project/SKILL.md": "129ac16e6a1f04ba",
|
||||
"skills/gsd-next/SKILL.md": "7855fd70e387087e",
|
||||
"skills/gsd-ns-context/SKILL.md": "9b496da79789b3f9",
|
||||
"skills/gsd-ns-ideate/SKILL.md": "84ca1cde06110981",
|
||||
"skills/gsd-ns-manage/SKILL.md": "909dafa0cd19ba5a",
|
||||
"skills/gsd-ns-project/SKILL.md": "936b6998d42520ac",
|
||||
"skills/gsd-ns-review/SKILL.md": "022253010db0e072",
|
||||
"skills/gsd-ns-workflow/SKILL.md": "cba075bc819b539e",
|
||||
"skills/gsd-onboard/SKILL.md": "f42fc2edebeda671",
|
||||
"skills/gsd-pause-work/SKILL.md": "b379469eed78a196",
|
||||
"skills/gsd-phase/SKILL.md": "25477edc97a90c91",
|
||||
"skills/gsd-plan-phase/SKILL.md": "19ead1acb151a868",
|
||||
"skills/gsd-plan-review-convergence/SKILL.md": "f654089c11024027",
|
||||
"skills/gsd-pr-branch/SKILL.md": "6901da15e321913e",
|
||||
"skills/gsd-profile-user/SKILL.md": "6259fabfb6afe7be",
|
||||
"skills/gsd-progress/SKILL.md": "85d76286162b9189",
|
||||
"skills/gsd-quick/SKILL.md": "b4f4e711ba664aa0",
|
||||
"skills/gsd-resume-work/SKILL.md": "04f6c2e5b579e8c4",
|
||||
"skills/gsd-review-backlog/SKILL.md": "469696b944c4e7b5",
|
||||
"skills/gsd-review/SKILL.md": "a45ab2fdca06793b",
|
||||
"skills/gsd-secure-phase/SKILL.md": "e64ad269c1e6e319",
|
||||
"skills/gsd-settings/SKILL.md": "fcdda8dd545622ae",
|
||||
"skills/gsd-ship/SKILL.md": "9615cc4c8f6de060",
|
||||
"skills/gsd-sketch/SKILL.md": "90c219b843db7ec7",
|
||||
"skills/gsd-spec-phase/SKILL.md": "56a5cbd606db9cba",
|
||||
"skills/gsd-spike/SKILL.md": "77209fef7a04c11a",
|
||||
"skills/gsd-stats/SKILL.md": "566024444ef71f44",
|
||||
"skills/gsd-surface/SKILL.md": "492cda98187a969b",
|
||||
"skills/gsd-thread/SKILL.md": "85326c97c83a02d1",
|
||||
"skills/gsd-ui-phase/SKILL.md": "a0c8fbcbe5e3c2b9",
|
||||
"skills/gsd-ui-review/SKILL.md": "081a3292a2d357f5",
|
||||
"skills/gsd-ultraplan-phase/SKILL.md": "06fb3d76eb785f94",
|
||||
"skills/gsd-undo/SKILL.md": "007d3b307e027af9",
|
||||
"skills/gsd-update/SKILL.md": "e6e49e117c7c8f35",
|
||||
"skills/gsd-validate-phase/SKILL.md": "373059517a94a194",
|
||||
"skills/gsd-verify-work/SKILL.md": "4272275698e9a3fe",
|
||||
"skills/gsd-workspace/SKILL.md": "06d6400d68318361",
|
||||
"skills/gsd-workstreams/SKILL.md": "2f77bb94db1be1a4"
|
||||
"scripts/lib/cli-exit.cjs": "612d0c372c75b7e7"
|
||||
}
|
||||
|
||||
@@ -382,7 +382,11 @@
|
||||
"gsd-core/workflows/verify-phase.md": "e0957e153788a222",
|
||||
"gsd-core/workflows/verify-work.md": "e7e7e900c4874490",
|
||||
"hooks/gsd-cursor-post-tool.js": "019d503aee8b4a3f",
|
||||
"hooks/gsd-cursor-pre-tool.js": "fe274720781fcbb5",
|
||||
"hooks/gsd-cursor-session-start.js": "c6e04ed597ea7020",
|
||||
"hooks/gsd-cursor-stop.js": "902a005c49e7660b",
|
||||
"hooks/gsd-cursor-subagent-start.js": "693d38d298d2252c",
|
||||
"hooks/gsd-cursor-subagent-stop.js": "8da03aad6bb05d3f",
|
||||
"scripts/changeset/README.md": "86ff89331dfd94b2",
|
||||
"scripts/changeset/cli.cjs": "68f92a344b199271",
|
||||
"scripts/changeset/github-release-notes.cjs": "795677f0c009b132",
|
||||
|
||||
@@ -315,7 +315,11 @@
|
||||
"hooks/gsd-config-reload.js": "880b696458e85e9b",
|
||||
"hooks/gsd-context-monitor.js": "41d28e0db7b20968",
|
||||
"hooks/gsd-cursor-post-tool.js": "8a8a249c0642cc71",
|
||||
"hooks/gsd-cursor-pre-tool.js": "8cb8e8f895edaec9",
|
||||
"hooks/gsd-cursor-session-start.js": "05a14e903c5edafa",
|
||||
"hooks/gsd-cursor-stop.js": "d33be8ac96f4081d",
|
||||
"hooks/gsd-cursor-subagent-start.js": "d773df8caa605de2",
|
||||
"hooks/gsd-cursor-subagent-stop.js": "8ee488d826bf3c37",
|
||||
"hooks/gsd-ensure-canonical-path.js": "7d116d7d65c50b4b",
|
||||
"hooks/gsd-graphify-update.sh": "e4c6e14fe6ad64ff",
|
||||
"hooks/gsd-phase-boundary.sh": "32739d5fbe0d0a1c",
|
||||
@@ -330,7 +334,7 @@
|
||||
"hooks/gsd-worktree-path-guard.js": "108ab88ccbafc5d8",
|
||||
"hooks/lib/git-cmd.js": "268ba15992ca0b23",
|
||||
"hooks/lib/gsd-graphify-rebuild.sh": "66af89601074d2a9",
|
||||
"hooks/managed-hooks-registry.cjs": "2218a41c279720c2",
|
||||
"hooks/managed-hooks-registry.cjs": "a494d1a70ed87690",
|
||||
"package.json": "dbf8353f77358bc1",
|
||||
"scripts/changeset/README.md": "86ff89331dfd94b2",
|
||||
"scripts/changeset/cli.cjs": "68f92a344b199271",
|
||||
|
||||
@@ -386,7 +386,11 @@
|
||||
"hooks/gsd-config-reload.js": "96546e0e8bb47904",
|
||||
"hooks/gsd-context-monitor.js": "7a9787868a39b76d",
|
||||
"hooks/gsd-cursor-post-tool.js": "9168e0a09de1972a",
|
||||
"hooks/gsd-cursor-pre-tool.js": "873998b25e308c29",
|
||||
"hooks/gsd-cursor-session-start.js": "9b2e6f4f0c405375",
|
||||
"hooks/gsd-cursor-stop.js": "bfaaf60f419e3238",
|
||||
"hooks/gsd-cursor-subagent-start.js": "06d77fde5c1372b6",
|
||||
"hooks/gsd-cursor-subagent-stop.js": "4bbf22917da4d389",
|
||||
"hooks/gsd-ensure-canonical-path.js": "2801ae3fef9579bf",
|
||||
"hooks/gsd-graphify-update.sh": "e4c6e14fe6ad64ff",
|
||||
"hooks/gsd-phase-boundary.sh": "32739d5fbe0d0a1c",
|
||||
@@ -401,7 +405,7 @@
|
||||
"hooks/gsd-worktree-path-guard.js": "726fb9afefda5d42",
|
||||
"hooks/lib/git-cmd.js": "268ba15992ca0b23",
|
||||
"hooks/lib/gsd-graphify-rebuild.sh": "66af89601074d2a9",
|
||||
"hooks/managed-hooks-registry.cjs": "763730ef31e5fd1c",
|
||||
"hooks/managed-hooks-registry.cjs": "9163e096b74ec4b3",
|
||||
"opencode.json": "2c12c446a88f2f36",
|
||||
"package.json": "dbf8353f77358bc1",
|
||||
"plugins/gsd-core.js": "931ca839dc9eb7f1",
|
||||
|
||||
@@ -315,7 +315,11 @@
|
||||
"hooks/gsd-config-reload.js": "4f52b8a0120bb1b8",
|
||||
"hooks/gsd-context-monitor.js": "437a33e6e3058640",
|
||||
"hooks/gsd-cursor-post-tool.js": "8a8a249c0642cc71",
|
||||
"hooks/gsd-cursor-pre-tool.js": "8cb8e8f895edaec9",
|
||||
"hooks/gsd-cursor-session-start.js": "05a14e903c5edafa",
|
||||
"hooks/gsd-cursor-stop.js": "d33be8ac96f4081d",
|
||||
"hooks/gsd-cursor-subagent-start.js": "d773df8caa605de2",
|
||||
"hooks/gsd-cursor-subagent-stop.js": "8ee488d826bf3c37",
|
||||
"hooks/gsd-ensure-canonical-path.js": "2df5e295b36c3334",
|
||||
"hooks/gsd-graphify-update.sh": "e4c6e14fe6ad64ff",
|
||||
"hooks/gsd-phase-boundary.sh": "32739d5fbe0d0a1c",
|
||||
@@ -330,7 +334,7 @@
|
||||
"hooks/gsd-worktree-path-guard.js": "8389e4c9175b2613",
|
||||
"hooks/lib/git-cmd.js": "268ba15992ca0b23",
|
||||
"hooks/lib/gsd-graphify-rebuild.sh": "66af89601074d2a9",
|
||||
"hooks/managed-hooks-registry.cjs": "a57697c1ae4ac163",
|
||||
"hooks/managed-hooks-registry.cjs": "a5a93d50c4ea7a0c",
|
||||
"package.json": "dbf8353f77358bc1",
|
||||
"scripts/changeset/README.md": "86ff89331dfd94b2",
|
||||
"scripts/changeset/cli.cjs": "68f92a344b199271",
|
||||
|
||||
@@ -12,6 +12,10 @@ const os = require('node:os');
|
||||
const { spawnSync } = require('node:child_process');
|
||||
const assert = require('node:assert/strict');
|
||||
|
||||
const {
|
||||
resolveRuntimeArtifactLayout,
|
||||
} = require('../../gsd-core/bin/lib/runtime-artifact-layout.cjs');
|
||||
|
||||
const INSTALL_SCRIPT = path.join(__dirname, '..', '..', 'bin', 'install.js');
|
||||
const MANIFEST_NAME = 'gsd-file-manifest.json';
|
||||
|
||||
@@ -177,9 +181,27 @@ function manifestAgentCount(manifest) {
|
||||
return Object.keys(manifest.files).filter((k) => k.startsWith('agents/')).length;
|
||||
}
|
||||
|
||||
function collectSkillBasenamesOnDisk(configDir) {
|
||||
/**
|
||||
* Collect gsd-* skill/command basenames actually present on disk under configDir.
|
||||
*
|
||||
* @param {string} configDir
|
||||
* @param {string} [runtime] - when provided, the skills-kind destination is
|
||||
* resolved via resolveRuntimeArtifactLayout so a skills-kind `home` override
|
||||
* (Codex only, ADR-1239 upgrade 3 / #2088: skills -> $HOME/.agents/skills
|
||||
* instead of configDir/skills) is honored. Omitted callers keep the prior
|
||||
* configDir/skills default.
|
||||
* @param {string} [scope='global']
|
||||
*/
|
||||
function collectSkillBasenamesOnDisk(configDir, runtime, scope = 'global') {
|
||||
const out = new Set();
|
||||
const skillsDir = path.join(configDir, 'skills');
|
||||
let skillsDir = path.join(configDir, 'skills');
|
||||
if (runtime) {
|
||||
try {
|
||||
const layout = resolveRuntimeArtifactLayout(runtime, configDir, scope);
|
||||
const skillsKind = layout.kinds.find((k) => k.kind === 'skills');
|
||||
if (skillsKind) skillsDir = path.join(skillsKind.home || configDir, skillsKind.destSubpath);
|
||||
} catch { /* fall back to configDir/skills */ }
|
||||
}
|
||||
if (fs.existsSync(skillsDir)) {
|
||||
for (const entry of fs.readdirSync(skillsDir, { withFileTypes: true })) {
|
||||
if (entry.isDirectory() && entry.name.startsWith('gsd-')) {
|
||||
|
||||
@@ -63,6 +63,31 @@ const {
|
||||
collectSkillBasenamesOnDisk,
|
||||
} = require('./helpers/install-shared.cjs');
|
||||
|
||||
/**
|
||||
* collectSkillBasenamesOnDisk(configDir, runtime, scope) re-resolves the
|
||||
* runtime's skills-kind layout via os.homedir(). runMinimalInstall() already
|
||||
* sandboxes HOME/USERPROFILE to `root` for the spawned install subprocess,
|
||||
* but that sandboxing does not persist into this (parent) process — without
|
||||
* re-sandboxing here, Codex's skills-kind `home: ".agents"` override
|
||||
* (ADR-1239 upgrade 3, #2088) would resolve against the developer's REAL
|
||||
* $HOME/.agents/skills instead of the sandboxed install root. Sandbox
|
||||
* HOME/USERPROFILE to `root` for the synchronous duration of the on-disk scan.
|
||||
*/
|
||||
function collectSkillBasenamesOnDiskSandboxed(configDir, runtime, scope, root) {
|
||||
const savedHome = process.env.HOME;
|
||||
const savedUserProfile = process.env.USERPROFILE;
|
||||
process.env.HOME = root;
|
||||
process.env.USERPROFILE = root;
|
||||
try {
|
||||
return collectSkillBasenamesOnDisk(configDir, runtime, scope);
|
||||
} finally {
|
||||
if (savedHome === undefined) delete process.env.HOME;
|
||||
else process.env.HOME = savedHome;
|
||||
if (savedUserProfile === undefined) delete process.env.USERPROFILE;
|
||||
else process.env.USERPROFILE = savedUserProfile;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Section 9: install-profiles — MINIMAL_SKILL_ALLOWLIST ───────────────────
|
||||
|
||||
describe('install-profiles: MINIMAL_SKILL_ALLOWLIST', () => {
|
||||
@@ -391,7 +416,7 @@ describe('install: on-disk skill files match manifest for --minimal', () => {
|
||||
});
|
||||
try {
|
||||
assert.ok(manifest);
|
||||
const onDisk = collectSkillBasenamesOnDisk(configDir);
|
||||
const onDisk = collectSkillBasenamesOnDiskSandboxed(configDir, runtime, scope, root);
|
||||
const inManifest = manifestSkillSet(manifest);
|
||||
assert.deepStrictEqual([...onDisk].sort(), [...inManifest].sort());
|
||||
// Not the shared listAgentFiles() helper: asserts on the INSTALLED
|
||||
@@ -542,10 +567,15 @@ describe('install: Codex full → minimal downgrade cleans stale agent state', (
|
||||
].join('\n');
|
||||
fs.writeFileSync(path.join(targetDir, 'config.toml'), codexConfig);
|
||||
|
||||
// Sandbox HOME/USERPROFILE to targetDir: Codex's skills-kind `home: ".agents"`
|
||||
// override (ADR-1239 upgrade 3, #2088) resolves via os.homedir(), so an
|
||||
// unsandboxed spawn here would write gsd-* skill dirs into the developer's
|
||||
// real $HOME/.agents/skills. This test only asserts on agents/ and
|
||||
// config.toml (both under targetDir), so the sandbox has no effect on intent.
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
[INSTALL_SCRIPT, '--codex', '--global', '--config-dir', targetDir, '--minimal'],
|
||||
{ encoding: 'utf8', env: installerEnv() },
|
||||
{ encoding: 'utf8', env: installerEnv({ HOME: targetDir, USERPROFILE: targetDir }) },
|
||||
);
|
||||
assert.ok(result.stdout || result.stderr);
|
||||
|
||||
|
||||
@@ -18,6 +18,10 @@ const {
|
||||
installRuntimeArtifacts,
|
||||
} = require('../gsd-core/bin/lib/install-engine.cjs');
|
||||
|
||||
const {
|
||||
resolveRuntimeArtifactLayout,
|
||||
} = require('../gsd-core/bin/lib/runtime-artifact-layout.cjs');
|
||||
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
|
||||
const {
|
||||
@@ -58,16 +62,55 @@ const FLAT = [
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Codex resolves its skills-kind destination via os.homedir() + a 'skills'-kind
|
||||
* 'home: ".agents"' layout override (ADR-1239 EoS upgrade 3, #2088), NOT
|
||||
* tmpDir/skills. Sandbox HOME/USERPROFILE to tmpDir for the duration of the
|
||||
* synchronous callback so codex's resolved skills dir becomes
|
||||
* tmpDir/.agents/skills instead of the developer's real ~/.agents/skills.
|
||||
*/
|
||||
function withSandboxedHome(tmpDir, fn) {
|
||||
const savedHome = process.env.HOME;
|
||||
const savedUserProfile = process.env.USERPROFILE;
|
||||
process.env.HOME = tmpDir;
|
||||
process.env.USERPROFILE = tmpDir;
|
||||
try {
|
||||
return fn();
|
||||
} finally {
|
||||
if (savedHome === undefined) delete process.env.HOME;
|
||||
else process.env.HOME = savedHome;
|
||||
if (savedUserProfile === undefined) delete process.env.USERPROFILE;
|
||||
else process.env.USERPROFILE = savedUserProfile;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a fresh temp dir, run installRuntimeArtifacts into it, and return
|
||||
* the tmpDir path. Caller must cleanup in finally.
|
||||
* the tmpDir path. Caller must cleanup in finally. HOME is sandboxed to
|
||||
* tmpDir for the install call so codex never writes to the real ~/.agents/skills.
|
||||
*/
|
||||
function runInstall(runtime, scope, resolved) {
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), `gsd-nest-test-${runtime}-`));
|
||||
installRuntimeArtifacts(runtime, tmpDir, scope, resolved);
|
||||
withSandboxedHome(tmpDir, () => installRuntimeArtifacts(runtime, tmpDir, scope, resolved));
|
||||
return tmpDir;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the skills-kind destination directory for a runtime, honoring the
|
||||
* skills-kind 'home' override (codex only — resolves under tmpDir/.agents
|
||||
* once HOME is sandboxed). All other runtimes have no 'home' override, so
|
||||
* this falls back to tmpDir/<destSubpath>, matching the static skillsSub
|
||||
* tables above.
|
||||
*/
|
||||
function resolveSkillsDir(runtime, tmpDir, scope) {
|
||||
return withSandboxedHome(tmpDir, () => {
|
||||
const layout = resolveRuntimeArtifactLayout(runtime, tmpDir, scope);
|
||||
const skillsKind = layout.kinds.find((k) => k.kind === 'skills');
|
||||
assert.ok(skillsKind, `${runtime} must have skills kind`);
|
||||
return path.join(skillsKind.home || tmpDir, skillsKind.destSubpath);
|
||||
});
|
||||
}
|
||||
|
||||
// Resolve the full profile once (shared by all installs)
|
||||
const MANIFEST = loadSkillsManifest(COMMANDS_GSD);
|
||||
const RESOLVED_FULL = resolveProfile({ modes: ['full'], manifest: MANIFEST });
|
||||
@@ -267,7 +310,7 @@ describe('claude: total top-level gsd- entries >= 60 (flat layout, #924)', () =>
|
||||
// FLAT runtimes: concrete skills stay top-level, no nesting
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
for (const { runtime, scope, skillsSub } of FLAT) {
|
||||
for (const { runtime, scope } of FLAT) {
|
||||
describe(`${runtime} (flat layout)`, () => {
|
||||
let tmpDir;
|
||||
|
||||
@@ -282,7 +325,7 @@ for (const { runtime, scope, skillsSub } of FLAT) {
|
||||
});
|
||||
|
||||
test(`${runtime}: stays flat — concrete skills remain top-level, no nesting`, () => {
|
||||
const skillsDir = path.join(tmpDir, skillsSub);
|
||||
const skillsDir = resolveSkillsDir(runtime, tmpDir, scope);
|
||||
assert.ok(fs.existsSync(skillsDir), `skillsDir must exist: ${skillsDir}`);
|
||||
|
||||
const topLevel = fs.readdirSync(skillsDir);
|
||||
|
||||
@@ -187,11 +187,31 @@ function readAllSkillMd(dir) {
|
||||
return out.join('\n');
|
||||
}
|
||||
|
||||
// Codex resolves its skills-kind destination via os.homedir() + a 'skills'-kind
|
||||
// 'home: ".agents"' layout override (ADR-1239 EoS upgrade 3, #2088), NOT
|
||||
// configDir/skills. Without sandboxing HOME, an in-process codex install would
|
||||
// write to (and an uninstall would mutate) the developer's REAL ~/.agents/skills.
|
||||
// Sandbox HOME/USERPROFILE to configDir before resolving the layout or invoking
|
||||
// install/uninstall so codex's resolved skills dir is configDir/.agents/skills.
|
||||
function sandboxHome(t, dir) {
|
||||
const savedHome = process.env.HOME;
|
||||
const savedUserProfile = process.env.USERPROFILE;
|
||||
process.env.HOME = dir;
|
||||
process.env.USERPROFILE = dir;
|
||||
t.after(() => {
|
||||
if (savedHome === undefined) delete process.env.HOME;
|
||||
else process.env.HOME = savedHome;
|
||||
if (savedUserProfile === undefined) delete process.env.USERPROFILE;
|
||||
else process.env.USERPROFILE = savedUserProfile;
|
||||
});
|
||||
}
|
||||
|
||||
describe('installRuntimeArtifacts — skills runtimes write gsd-prefixed skill dirs', () => {
|
||||
for (const runtime of SKILLS_RUNTIMES_LAYOUT) {
|
||||
test(`${runtime}: gsd-prefixed skill dirs in skills/`, (t) => {
|
||||
const configDir = createTempDir(`gsd-ial-${runtime}-`);
|
||||
t.after(() => cleanup(configDir));
|
||||
sandboxHome(t, configDir);
|
||||
|
||||
assert.strictEqual(typeof installRuntimeArtifacts, 'function');
|
||||
installRuntimeArtifacts(runtime, configDir, 'global', RESOLVED_CORE);
|
||||
@@ -200,7 +220,7 @@ describe('installRuntimeArtifacts — skills runtimes write gsd-prefixed skill d
|
||||
const skillsKind = layout.kinds.find(k => k.kind === 'skills');
|
||||
assert.ok(skillsKind, `${runtime} must have skills kind`);
|
||||
|
||||
const destDir = path.join(configDir, skillsKind.destSubpath);
|
||||
const destDir = path.join(skillsKind.home || configDir, skillsKind.destSubpath);
|
||||
assert.ok(fs.existsSync(destDir));
|
||||
assert.ok(
|
||||
fs.existsSync(path.join(destDir, `${skillsKind.prefix}help`, 'SKILL.md')),
|
||||
@@ -481,6 +501,7 @@ describe('uninstallRuntimeArtifacts — removes gsd-owned entries, preserves for
|
||||
test(`${runtime}: gsd entries removed, foreign preserved`, (t) => {
|
||||
const configDir = createTempDir(`gsd-ual-${runtime}-`);
|
||||
t.after(() => cleanup(configDir));
|
||||
sandboxHome(t, configDir);
|
||||
|
||||
const { uninstallRuntimeArtifacts } = require('../bin/install.js');
|
||||
assert.strictEqual(typeof uninstallRuntimeArtifacts, 'function');
|
||||
@@ -519,7 +540,7 @@ describe('uninstallRuntimeArtifacts — removes gsd-owned entries, preserves for
|
||||
}
|
||||
|
||||
for (const kind of layout.kinds) {
|
||||
const destDir = path.join(configDir, kind.destSubpath);
|
||||
const destDir = path.join(kind.home || configDir, kind.destSubpath);
|
||||
fs.mkdirSync(destDir, { recursive: true });
|
||||
if (kind.kind === 'skills') {
|
||||
writeSkillEntry(destDir, kind.prefix, 'help');
|
||||
@@ -545,7 +566,7 @@ describe('uninstallRuntimeArtifacts — removes gsd-owned entries, preserves for
|
||||
uninstallRuntimeArtifacts(runtime, configDir, 'global');
|
||||
|
||||
for (const kind of layout.kinds) {
|
||||
const destDir = path.join(configDir, kind.destSubpath);
|
||||
const destDir = path.join(kind.home || configDir, kind.destSubpath);
|
||||
if (kind.kind === 'skills') {
|
||||
assert.ok(!fs.existsSync(path.join(destDir, `${kind.prefix}help`)));
|
||||
assert.ok(!fs.existsSync(path.join(destDir, `${kind.prefix}phase`)));
|
||||
|
||||
@@ -9957,7 +9957,9 @@ function readWorkflow() {
|
||||
describe('install.js --skills-root', () => {
|
||||
const CASES = [
|
||||
{ runtime: 'claude', expected: path.join(os.homedir(), '.claude', 'skills') },
|
||||
{ runtime: 'codex', expected: path.join(os.homedir(), '.codex', 'skills') },
|
||||
// #2088 (ADR-1239 upgrade 3): Codex skills resolve to the canonical
|
||||
// $HOME/.agents/skills root (skills-kind home override), not $CODEX_HOME/skills.
|
||||
{ runtime: 'codex', expected: path.join(os.homedir(), '.agents', 'skills') },
|
||||
{ runtime: 'copilot', expected: path.join(os.homedir(), '.copilot', 'skills') },
|
||||
{ runtime: 'cursor', expected: path.join(os.homedir(), '.cursor', 'skills') },
|
||||
{ runtime: 'trae', expected: path.join(os.homedir(), '.trae', 'skills') },
|
||||
@@ -10182,10 +10184,15 @@ const INSTALL = path.join(__dirname, '..', 'bin', 'install.js');
|
||||
*/
|
||||
function installAndRead(runtime) {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), `gsd-inst-${runtime}-`));
|
||||
// Sandbox HOME/USERPROFILE to `dir`: Codex's skills-kind `home: ".agents"`
|
||||
// override (ADR-1239 upgrade 3, #2088) resolves via os.homedir(), so an
|
||||
// unsandboxed real install here would write a full (non-minimal) gsd-* skill
|
||||
// set into the developer/CI machine's real $HOME/.agents/skills. Harmless
|
||||
// no-op for cursor/claude, which have no skills-kind home override.
|
||||
const res = spawnSync(
|
||||
process.execPath,
|
||||
[INSTALL, `--${runtime}`, '--global', '--config-dir', dir],
|
||||
{ encoding: 'utf8', timeout: 120000 },
|
||||
{ encoding: 'utf8', timeout: 120000, env: { ...process.env, HOME: dir, USERPROFILE: dir } },
|
||||
);
|
||||
assert.strictEqual(res.status, 0, `install --${runtime} failed: ${res.stderr || res.stdout}`);
|
||||
const wf = path.join(dir, 'gsd-core', 'workflows', 'execute-phase.md');
|
||||
|
||||
@@ -81,6 +81,19 @@ function withEnv(key, value, fn) {
|
||||
}
|
||||
}
|
||||
|
||||
// #2088: Codex CLI skills install to `$HOME/.agents/skills` (resolved via
|
||||
// os.homedir()), not `$CODEX_HOME/skills`. In-process codex installs must
|
||||
// sandbox HOME (and USERPROFILE, for Windows os.homedir() resolution) to the
|
||||
// test's codexHome dir, or skills get materialized into the real developer
|
||||
// home directory. withEnv saves/restores a single key, so nesting is safe.
|
||||
function withCodexEnv(codexHome, fn) {
|
||||
return withEnv('CODEX_HOME', codexHome, () =>
|
||||
withEnv('HOME', codexHome, () =>
|
||||
withEnv('USERPROFILE', codexHome, fn)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function captureConsole(fn) {
|
||||
const originalLog = console.log;
|
||||
const originalWarn = console.warn;
|
||||
@@ -216,11 +229,20 @@ function assertFreshInstallContract(runtime, targetDir) {
|
||||
}
|
||||
|
||||
if (contract.surface === 'flat-skills') {
|
||||
// Pre-#3562: codex was special-cased to expect zero gsd-* skill dirs
|
||||
// (assumption: Codex auto-discovers from workflows). That assumption
|
||||
// does not hold for Codex CLI 0.130.0 — fresh installs now materialize
|
||||
// the same flat-skills surface as the other runtimes.
|
||||
assertHasGsdDirectory(targetDir, 'skills');
|
||||
if (runtime === 'codex') {
|
||||
// #2088: Codex CLI skills install to the canonical `$HOME/.agents/skills`
|
||||
// root (resolved via os.homedir()), NOT `<config-dir>/skills`. Here
|
||||
// runInstallerCli sandboxes HOME to <dirname(targetDir)>/home, so assert
|
||||
// the skill dir under that sandboxed home instead of under targetDir.
|
||||
const codexSandboxHome = path.join(path.dirname(targetDir), 'home');
|
||||
assertHasGsdDirectory(path.join(codexSandboxHome, '.agents'), 'skills');
|
||||
} else {
|
||||
// Pre-#3562: codex was special-cased to expect zero gsd-* skill dirs
|
||||
// (assumption: Codex auto-discovers from workflows). That assumption
|
||||
// does not hold for Codex CLI 0.130.0 — fresh installs now materialize
|
||||
// the same flat-skills surface as the other runtimes.
|
||||
assertHasGsdDirectory(targetDir, 'skills');
|
||||
}
|
||||
} else if (contract.surface === 'hermes-skills') {
|
||||
// Hermes layout uses prefix: '' — skill dirs have bare stem names (no gsd- prefix).
|
||||
// Assert that the category dir contains at least one skill dir with SKILL.md.
|
||||
@@ -335,7 +357,7 @@ describe('installer migration install integration', { concurrency: false }, () =
|
||||
});
|
||||
|
||||
const { output } = captureConsole(() =>
|
||||
withEnv('CODEX_HOME', codexHome, () => install(true, 'codex'))
|
||||
withCodexEnv(codexHome, () => install(true, 'codex'))
|
||||
);
|
||||
|
||||
const plainOutput = stripAnsi(output);
|
||||
@@ -353,13 +375,17 @@ describe('installer migration install integration', { concurrency: false }, () =
|
||||
|
||||
assert.throws(
|
||||
() => captureConsole(() =>
|
||||
withEnv('CODEX_HOME', codexHome, () => install(true, 'codex'))
|
||||
withCodexEnv(codexHome, () => install(true, 'codex'))
|
||||
),
|
||||
/installer migration blocked/
|
||||
);
|
||||
|
||||
assert.equal(fs.readFileSync(path.join(codexHome, 'hooks/gsd-retired-hook.txt'), 'utf8'), 'old gsd hook\n');
|
||||
assert.equal(fs.existsSync(path.join(codexHome, 'skills')), false);
|
||||
// #2088: with HOME sandboxed to codexHome via withCodexEnv, Codex's
|
||||
// canonical skill root ($HOME/.agents/skills) resolves under codexHome
|
||||
// too — assert nothing was materialized there when the install is blocked.
|
||||
assert.equal(fs.existsSync(path.join(codexHome, '.agents', 'skills')), false);
|
||||
assert.equal(fs.existsSync(path.join(codexHome, 'gsd-core', 'VERSION')), false);
|
||||
});
|
||||
|
||||
@@ -431,7 +457,7 @@ describe('installer migration install integration', { concurrency: false }, () =
|
||||
assert.throws(
|
||||
() => captureConsole(() =>
|
||||
withEnv('CLAUDE_CONFIG_DIR', claudeHome, () =>
|
||||
withEnv('CODEX_HOME', codexHome, () =>
|
||||
withCodexEnv(codexHome, () =>
|
||||
withWriteFailure(path.join(codexHome, 'gsd-core', 'VERSION'), () =>
|
||||
installModule.installAllRuntimes(['claude', 'codex'], true, false)
|
||||
)
|
||||
|
||||
@@ -1618,12 +1618,24 @@ const BUILD_HOOKS_SCRIPT = path.join(__dirname, '..', 'scripts', 'build-hooks.js
|
||||
|
||||
function withCodexHome(codexHome, fn) {
|
||||
const previousCodexHome = process.env.CODEX_HOME;
|
||||
// #2088 (ADR-1239 upgrade 3): Codex skills now install to $HOME/.agents/skills
|
||||
// (os.homedir()-relative, independent of CODEX_HOME). Sandbox HOME (and
|
||||
// USERPROFILE) to codexHome so in-process installs never write to the
|
||||
// developer/CI machine's real home directory.
|
||||
const previousHome = process.env.HOME;
|
||||
const previousUserProfile = process.env.USERPROFILE;
|
||||
process.env.CODEX_HOME = codexHome;
|
||||
process.env.HOME = codexHome;
|
||||
process.env.USERPROFILE = codexHome;
|
||||
try {
|
||||
return fn();
|
||||
} finally {
|
||||
if (previousCodexHome == null) delete process.env.CODEX_HOME;
|
||||
else process.env.CODEX_HOME = previousCodexHome;
|
||||
if (previousHome == null) delete process.env.HOME;
|
||||
else process.env.HOME = previousHome;
|
||||
if (previousUserProfile == null) delete process.env.USERPROFILE;
|
||||
else process.env.USERPROFILE = previousUserProfile;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ const { test, describe } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
|
||||
const phaseId = require('../gsd-core/bin/lib/phase-id.cjs');
|
||||
const fc = require('fast-check');
|
||||
|
||||
// ─── escapeRegex ─────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -450,3 +451,169 @@ describe('getPhaseDirFromPhaseId', () => {
|
||||
assert.ok(!result.endsWith('-'));
|
||||
});
|
||||
});
|
||||
|
||||
// ─── parsePhaseFromProse (#2121, anchored — fixes #2111) ─────────────────────
|
||||
|
||||
describe('parsePhaseFromProse', () => {
|
||||
test('null / empty input yields null phase and name', () => {
|
||||
assert.deepEqual(phaseId.parsePhaseFromProse(null), { phase: null, name: null });
|
||||
assert.deepEqual(phaseId.parsePhaseFromProse(''), { phase: null, name: null });
|
||||
});
|
||||
|
||||
test('#2111: a milestone-completion string carries no phase', () => {
|
||||
assert.equal(phaseId.parsePhaseFromProse('Milestone v0.5 complete').phase, null);
|
||||
assert.equal(phaseId.parsePhaseFromProse('Milestone v1.0 complete').phase, null);
|
||||
assert.equal(phaseId.parsePhaseFromProse('Milestone v2.10 complete').phase, null);
|
||||
});
|
||||
|
||||
test('#2111: a bare version token or stray numeral is not a phase', () => {
|
||||
assert.equal(phaseId.parsePhaseFromProse('v0.5').phase, null);
|
||||
assert.equal(phaseId.parsePhaseFromProse('v1.0').phase, null);
|
||||
assert.equal(phaseId.parsePhaseFromProse('Fixed 12 bugs in v2.3').phase, null);
|
||||
});
|
||||
|
||||
test('a genuine phase value (starting with the token) is parsed', () => {
|
||||
assert.deepEqual(phaseId.parsePhaseFromProse('3 of 4 (Delta)'), { phase: '3', name: 'Delta' });
|
||||
assert.deepEqual(phaseId.parsePhaseFromProse('3A — Delta'), { phase: '3A', name: 'Delta' });
|
||||
assert.equal(phaseId.parsePhaseFromProse('12.1: Setup').phase, '12.1');
|
||||
assert.equal(phaseId.parsePhaseFromProse('29 of 30').phase, '29');
|
||||
assert.equal(phaseId.parsePhaseFromProse('029').phase, '029');
|
||||
});
|
||||
|
||||
test('a leading project-code prefix is tolerated but not captured (bare token)', () => {
|
||||
assert.equal(phaseId.parsePhaseFromProse('MEM-01 — Foo').phase, '01');
|
||||
assert.equal(phaseId.parsePhaseFromProse('AB-29 of 30').phase, '29');
|
||||
});
|
||||
|
||||
test('an optional leading "Phase" label is tolerated', () => {
|
||||
assert.equal(phaseId.parsePhaseFromProse('Phase 3A — Delta').phase, '3A');
|
||||
});
|
||||
|
||||
test('a status-word parenthetical is filtered from the name (preserved behavior)', () => {
|
||||
// parenName wins over the em-dash tail; "executing" is a status word → name null.
|
||||
assert.deepEqual(phaseId.parsePhaseFromProse('3A — Delta (executing)'), { phase: '3A', name: null });
|
||||
assert.equal(phaseId.parsePhaseFromProse('3 (complete)').name, null);
|
||||
});
|
||||
|
||||
test('#2124 review: name quantifiers are length-bounded (ReDoS guard)', () => {
|
||||
// A parenthetical within the bound extracts; one longer than the bound is
|
||||
// NOT matched — the cap is what prevents O(n^2) backtracking on a crafted
|
||||
// untrusted value. Removing the bound would extract the long name → fail.
|
||||
assert.equal(phaseId.parsePhaseFromProse('3 (Delta)').name, 'Delta');
|
||||
assert.equal(phaseId.parsePhaseFromProse(`3 (${'x'.repeat(201)})`).name, null);
|
||||
// A long unterminated "(" run yields no name and still parses the phase.
|
||||
assert.deepEqual(phaseId.parsePhaseFromProse(`3 ${'('.repeat(5000)}`), { phase: '3', name: null });
|
||||
});
|
||||
|
||||
test('#2124 review: non-string input is coerced, never throws', () => {
|
||||
assert.doesNotThrow(() => phaseId.parsePhaseFromProse(3));
|
||||
assert.equal(phaseId.parsePhaseFromProse(3).phase, '3');
|
||||
assert.deepEqual(phaseId.parsePhaseFromProse(true), { phase: null, name: null });
|
||||
});
|
||||
});
|
||||
|
||||
// ─── stripConfiguredProjectCodePrefix (#2121 / #2104, config-aware) ───────────
|
||||
|
||||
describe('stripConfiguredProjectCodePrefix', () => {
|
||||
test('#2104: a foreign prefix is preserved (not collapsed to a bare phase)', () => {
|
||||
assert.equal(phaseId.stripConfiguredProjectCodePrefix('MEM-01', 'LKML'), 'MEM-01');
|
||||
});
|
||||
|
||||
test('the configured prefix is stripped (case-insensitive)', () => {
|
||||
assert.equal(phaseId.stripConfiguredProjectCodePrefix('CK-01', 'CK'), '01');
|
||||
assert.equal(phaseId.stripConfiguredProjectCodePrefix('LKML-29', 'lkml'), '29');
|
||||
assert.equal(phaseId.stripConfiguredProjectCodePrefix('AB-29', 'AB'), '29');
|
||||
});
|
||||
|
||||
test('a value with no prefix is returned unchanged', () => {
|
||||
assert.equal(phaseId.stripConfiguredProjectCodePrefix('01', 'CK'), '01');
|
||||
assert.equal(phaseId.stripConfiguredProjectCodePrefix('029', 'CK'), '029');
|
||||
});
|
||||
|
||||
test('an absent/empty projectCode preserves the value verbatim', () => {
|
||||
assert.equal(phaseId.stripConfiguredProjectCodePrefix('MEM-01', ''), 'MEM-01');
|
||||
assert.equal(phaseId.stripConfiguredProjectCodePrefix('MEM-01', null), 'MEM-01');
|
||||
assert.equal(phaseId.stripConfiguredProjectCodePrefix('MEM-01', undefined), 'MEM-01');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── isForeignPrefixedPhaseQuery (#2121 / #2056) ─────────────────────────────
|
||||
|
||||
describe('isForeignPrefixedPhaseQuery', () => {
|
||||
test('a prefix that is not the configured code is foreign', () => {
|
||||
assert.equal(phaseId.isForeignPrefixedPhaseQuery('MEM-01', 'LKML'), true);
|
||||
});
|
||||
|
||||
test('the configured prefix is not foreign (case-insensitive)', () => {
|
||||
assert.equal(phaseId.isForeignPrefixedPhaseQuery('CK-01', 'CK'), false);
|
||||
assert.equal(phaseId.isForeignPrefixedPhaseQuery('ck-01', 'CK'), false);
|
||||
});
|
||||
|
||||
test('a value with no prefix is never foreign', () => {
|
||||
assert.equal(phaseId.isForeignPrefixedPhaseQuery('01', 'CK'), false);
|
||||
assert.equal(phaseId.isForeignPrefixedPhaseQuery('29', 'AB'), false);
|
||||
});
|
||||
|
||||
test('a prefixed query with no configured code is foreign; a bare one is not', () => {
|
||||
assert.equal(phaseId.isForeignPrefixedPhaseQuery('MEM-01', ''), true);
|
||||
assert.equal(phaseId.isForeignPrefixedPhaseQuery('MEM-01', null), true);
|
||||
assert.equal(phaseId.isForeignPrefixedPhaseQuery('01', ''), false);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── roadmapPhaseLookupSources (#2121, owned here after the move) ─────────────
|
||||
|
||||
describe('roadmapPhaseLookupSources', () => {
|
||||
const PREFIX_TOLERANT = `${phaseId.OPTIONAL_PROJECT_CODE_PREFIX_SOURCE}0*29`;
|
||||
|
||||
test('a bare numeric query yields the numeric then prefix-tolerant sources', () => {
|
||||
const sources = phaseId.roadmapPhaseLookupSources('29');
|
||||
assert.deepEqual(sources, ['0*29', PREFIX_TOLERANT]);
|
||||
});
|
||||
|
||||
test('the bare numeric source precedes the prefix-tolerant fallback', () => {
|
||||
const sources = phaseId.roadmapPhaseLookupSources('29');
|
||||
assert.ok(sources.indexOf('0*29') < sources.indexOf(PREFIX_TOLERANT));
|
||||
});
|
||||
|
||||
test('a project-code-prefixed query adds the exact source first (3 sources)', () => {
|
||||
const sources = phaseId.roadmapPhaseLookupSources('AB-29');
|
||||
assert.equal(sources.length, 3);
|
||||
assert.equal(sources[0], 'AB-29');
|
||||
assert.ok(sources.includes('0*29'));
|
||||
assert.ok(sources.includes(PREFIX_TOLERANT));
|
||||
});
|
||||
|
||||
test('zero-padding is tolerated: 029 resolves the same sources as 29', () => {
|
||||
assert.deepEqual(phaseId.roadmapPhaseLookupSources('029'), phaseId.roadmapPhaseLookupSources('29'));
|
||||
});
|
||||
|
||||
test('sources are deduplicated', () => {
|
||||
const sources = phaseId.roadmapPhaseLookupSources('29');
|
||||
assert.equal(sources.length, new Set(sources).size);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── #2121 property tests (fast-check) ───────────────────────────────────────
|
||||
|
||||
describe('phase-id canonical surface — properties', () => {
|
||||
test('#2111 invariant: a "Milestone vX.Y complete" string never yields a phase', () => {
|
||||
fc.assert(
|
||||
fc.property(fc.nat(999), fc.nat(999), (major, minor) => {
|
||||
return phaseId.parsePhaseFromProse(`Milestone v${major}.${minor} complete`).phase === null;
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test('parse↔normalize: a "N of M" prose value extracts N, and it normalizes stably', () => {
|
||||
fc.assert(
|
||||
fc.property(fc.integer({ min: 1, max: 9999 }), fc.integer({ min: 1, max: 9999 }), (n, m) => {
|
||||
const parsed = phaseId.parsePhaseFromProse(`${n} of ${m}`);
|
||||
return (
|
||||
parsed.phase === String(n) &&
|
||||
phaseId.normalizePhaseName(parsed.phase) === phaseId.normalizePhaseName(String(n))
|
||||
);
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -47,6 +47,9 @@ const INSTALL_JS_PATH_HELPERS = [
|
||||
'resolveOpencodeConfigPath',
|
||||
'computePathPrefix',
|
||||
'normalizeInstallRelativePath',
|
||||
// #2088 (ADR-1239 upgrade 3): resolves the skills-install dir honoring a
|
||||
// skills-kind `home` override (e.g. Codex → $HOME/.agents/skills).
|
||||
'_resolveSkillsRootDir',
|
||||
];
|
||||
|
||||
describe('portability-vocab drift guard', () => {
|
||||
|
||||
@@ -325,11 +325,14 @@ describe('generate-dev-preferences command', () => {
|
||||
const result = runGsdTools(
|
||||
['generate-dev-preferences', '--analysis', analysisPath, '--raw'],
|
||||
tmpDir,
|
||||
{ CODEX_HOME: codexHome, GSD_RUNTIME: 'codex' }
|
||||
// #2088 (ADR-1239 upgrade 3): Codex skills resolve to $HOME/.agents/skills
|
||||
// (HOME-relative), so sandbox HOME to keep the dev-preferences write inside
|
||||
// the temp dir rather than the developer's real ~/.agents/skills.
|
||||
{ CODEX_HOME: codexHome, GSD_RUNTIME: 'codex', HOME: codexHome, USERPROFILE: codexHome }
|
||||
);
|
||||
assert.ok(result.success, `Failed: ${result.error}`);
|
||||
const out = JSON.parse(result.output);
|
||||
assert.strictEqual(out.command_path, path.join(codexHome, 'skills', 'gsd-dev-preferences', 'SKILL.md'));
|
||||
assert.strictEqual(out.command_path, path.join(codexHome, '.agents', 'skills', 'gsd-dev-preferences', 'SKILL.md'));
|
||||
assert.ok(fs.existsSync(out.command_path), 'runtime-aware output should be written');
|
||||
});
|
||||
|
||||
@@ -347,11 +350,12 @@ describe('generate-dev-preferences command', () => {
|
||||
const result = runGsdTools(
|
||||
['generate-dev-preferences', '--analysis', analysisPath, '--raw'],
|
||||
tmpDir,
|
||||
{ CODEX_HOME: codexHome, GSD_RUNTIME: 'codex-app' }
|
||||
// #2088: codex-app alias canonicalizes to codex → $HOME/.agents/skills.
|
||||
{ CODEX_HOME: codexHome, GSD_RUNTIME: 'codex-app', HOME: codexHome, USERPROFILE: codexHome }
|
||||
);
|
||||
assert.ok(result.success, `Failed: ${result.error}`);
|
||||
const out = JSON.parse(result.output);
|
||||
assert.strictEqual(out.command_path, path.join(codexHome, 'skills', 'gsd-dev-preferences', 'SKILL.md'));
|
||||
assert.strictEqual(out.command_path, path.join(codexHome, '.agents', 'skills', 'gsd-dev-preferences', 'SKILL.md'));
|
||||
});
|
||||
|
||||
test('uses runtime-aware skills dir for cline by default (#782)', () => {
|
||||
|
||||
@@ -12,28 +12,63 @@
|
||||
*
|
||||
* Invoked as a subprocess (the hook reads a JSON payload on stdin and has no
|
||||
* exported surface), so this exercises the real shipped hook end-to-end.
|
||||
*
|
||||
* F.I.R.S.T. design:
|
||||
* Fast — spawnSync is synchronous; scanner exits in <100ms for any input.
|
||||
* Isolated — each invocation is a fresh subprocess; no shared state.
|
||||
* Repeatable — no wall-clock assertion; the 30s safety-net timeout is 6x the
|
||||
* scanner's own internal 5s timer and is never tested against.
|
||||
* Tests assert on the scanner's RESULT (exit code + output shape),
|
||||
* never on timing.
|
||||
* Self-Val — assertions check exit===0 and output is empty or valid JSON.
|
||||
* Timely — written alongside the scanner (#1577); hardened for #2089.
|
||||
*/
|
||||
|
||||
const { describe, test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const { execFileSync } = require('node:child_process');
|
||||
const { spawnSync } = require('node:child_process');
|
||||
const path = require('node:path');
|
||||
const fc = require('./helpers/fast-check-setup.cjs');
|
||||
|
||||
const HOOK_PATH = path.join(__dirname, '..', 'hooks', 'gsd-read-injection-scanner.js');
|
||||
|
||||
/**
|
||||
* Run the scanner hook with a payload and return its result.
|
||||
*
|
||||
* Uses spawnSync (not execFileSync) so non-zero exits return a result object
|
||||
* rather than throwing — cleaner for property tests that assert on exit code.
|
||||
*
|
||||
* Non-serializable payloads (BigInt, circular refs, Symbol, undefined) are
|
||||
* SKIPPED: the scanner receives JSON via stdin, so these values can never
|
||||
* reach it. JSON.stringify throwing is a test-harness artifact (fc.anything()
|
||||
* generates values outside the JSON domain), not a scanner defect.
|
||||
*
|
||||
* The 30s safety-net timeout is NOT a test assertion. The scanner exits in
|
||||
* <100ms for any input; its own internal setTimeout(5000) guarantees exit
|
||||
* even if stdin never closes (impossible here — spawnSync's `input:` pipes
|
||||
* and closes stdin). The ceiling only catches a genuinely hung process (a
|
||||
* real defect) without racing the scanner's internal timer.
|
||||
*/
|
||||
function runHook(payload) {
|
||||
let input;
|
||||
try {
|
||||
const stdout = execFileSync(process.execPath, [HOOK_PATH], {
|
||||
input: JSON.stringify(payload),
|
||||
encoding: 'utf-8',
|
||||
timeout: 5000,
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
});
|
||||
return { exitCode: 0, stdout: stdout.trim() };
|
||||
} catch (err) {
|
||||
return { exitCode: err.status ?? 1, stdout: (err.stdout || '').toString().trim() };
|
||||
input = JSON.stringify(payload);
|
||||
} catch {
|
||||
return { exitCode: 0, stdout: '', skipped: true };
|
||||
}
|
||||
|
||||
const result = spawnSync(process.execPath, [HOOK_PATH], {
|
||||
input,
|
||||
encoding: 'utf-8',
|
||||
timeout: 30000,
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
});
|
||||
|
||||
return {
|
||||
exitCode: result.status ?? 1,
|
||||
stdout: (result.stdout || '').trim(),
|
||||
signal: result.signal,
|
||||
};
|
||||
}
|
||||
|
||||
// Injection-shaped fragments so the regex-matching path is exercised, not just clean text.
|
||||
|
||||
@@ -389,6 +389,41 @@ test('ambient GSD workstream vars are stripped by the runner', () => {
|
||||
`expected final file-count chunking marker in stderr; STDERR:\n${r.stderr}`,
|
||||
);
|
||||
});
|
||||
|
||||
// #2088: install-heavy files (real installs) are weighted so they never all
|
||||
// land in one chunk — otherwise the unsharded targeted lane packs the whole
|
||||
// install surface into a single chunk that blows the 600s per-chunk backstop
|
||||
// on the slow Windows runner.
|
||||
test('install-heavy files carry more weight so they SPREAD across chunks (#2088)', () => {
|
||||
// 4 install-* files at weight 3 = 12 against a 6-weight cap → 2 chunks
|
||||
// (2 heavy each). The whole install load is never in a single chunk.
|
||||
const heavy = Array.from({ length: 4 }, (_, i) => `install-weighttest-${i}.test.cjs`);
|
||||
seed(tmpDir, heavy);
|
||||
const rh = runHarness(tmpDir, [], {
|
||||
RUN_TESTS_MAX_CMDLINE_CHARS: '100000',
|
||||
RUN_TESTS_MAX_FILES_PER_CHUNK: '6',
|
||||
RUN_TESTS_HEAVY_FILE_WEIGHT: '3',
|
||||
});
|
||||
assert.strictEqual(rh.status, 0, `heavy: expected zero exit; STDERR:\n${rh.stderr}`);
|
||||
assert.match(rh.stderr, /run-tests: chunk 1\/2 — 2 files/, `install-heavy files must split into 2 chunks; STDERR:\n${rh.stderr}`);
|
||||
assert.match(rh.stderr, /run-tests: chunk 2\/2 — 2 files/, `STDERR:\n${rh.stderr}`);
|
||||
});
|
||||
|
||||
test('light files of the same count stay in ONE chunk — split is weight-driven, not count-driven (#2088)', () => {
|
||||
// Same file COUNT (4) but LIGHT (weight 1 each): 4 < 6 cap → a single
|
||||
// chunk. Proves the split above is driven by install-heavy WEIGHT.
|
||||
const light = Array.from({ length: 4 }, (_, i) => `lightweighttest-${i}.test.cjs`);
|
||||
seed(tmpDir, light);
|
||||
const rl = runHarness(tmpDir, [], {
|
||||
RUN_TESTS_MAX_CMDLINE_CHARS: '100000',
|
||||
RUN_TESTS_MAX_FILES_PER_CHUNK: '6',
|
||||
RUN_TESTS_HEAVY_FILE_WEIGHT: '3',
|
||||
});
|
||||
assert.strictEqual(rl.status, 0, `light: expected zero exit; STDERR:\n${rl.stderr}`);
|
||||
// A single chunk emits NO `chunk N/M` split marker (it only prints when
|
||||
// chunks.length > 1), so its absence proves the 4 light files stayed together.
|
||||
assert.doesNotMatch(rl.stderr, /run-tests: chunk \d+\/\d+ — /, `4 light files must stay in one chunk (no split marker); STDERR:\n${rl.stderr}`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('shard partitioning CLI (#1212)', () => {
|
||||
|
||||
Reference in New Issue
Block a user