Files
msd-core/tests/codex-inherit-smoke.test.cjs
Tom Boucher 57437071e7 test(#3244): codex smoke test across the emit, validate and repair seam (#3298)
* test(#3244): end-to-end codex smoke test across the emit and validate seam

Test-only. The only place in this epic where the emitter (Phase 1) and
the validator (Phase 2) meet the same bytes.

Every phase so far tested its own half against fixtures it authored, and
per #2371 a fixture written by the gate's own author can only confirm
what that author already believed. Rows 7 and 10 feed a REAL emitted
tree to Phase 2's checkCodexModelPosture: if the emitter and the
validator disagree about what "clean" means, nothing else in the suite
can see it.

Scope corrected from the epic, which scoped this to model_profile:
inherit. That profile is the one this epic does NOT change —
readGsdRuntimeProfileResolver returns null for it, so a Codex install
under inherit omitted the model before Phase 1 too, and a test scoped
only to it would pass identically before and after the change it exists
to prove. Row 1 uses `balanced`, the default and the path that actually
lost its pin; inherit is row 4, the unchanged control.

Honest classification, because this is a smoke test and claiming
otherwise would be false: NO row here is red-first. Phases 1 and 2 are
merged and working, so every row passes today. Rows 1-6 and 9 are
regression guards; rows 7 and 10 are cross-phase integration. Their
value is that nothing else can catch the drift they cover, not that they
are red now. The RED checkpoint is therefore skipped deliberately for
this phase rather than spent proving a tautology.

Rows 8 and 11 — the same cross-checks against Phase 3's repairer — are
deliberately ABSENT, not forgotten. Phase 3 (#3243) is still in CI and
its module does not exist on next yet. They land once it merges; writing
them against a surface that does not exist would have meant inventing
the assertion.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(#3244): close the emit-validate-repair loop with rows 8 and 11

These were deliberately absent from the previous commit because Phase 3
had not merged; writing them against a module that did not exist would
have meant inventing the assertion. It has merged, so they land now.

Row 8: a freshly-installed tree reports ZERO changes from the sync's dry
run. If a brand-new install needs repair, the emitter and the repairer
disagree about what a correct file looks like.

Row 11 is the sharpest row in the phase: after installing with an
explicit real-Codex model_overrides pin, the sync reports that agent
skipped rather than synced, and the file is byte-identical afterwards.
An over-eager stripper that removes any `model` line passes every Phase
3 unit test and fails only here.

Byte-identity is asserted by comparing file CONTENTS, not mtime — a
rewrite with identical bytes still moves mtime, so an mtime check would
pass exactly the implementation this row exists to catch.

With these, all four cross-phase rows are in place, and this suite is
the only place in the epic where the emitter, the validator and the
repairer meet the same bytes. Every phase before this tested its own
half against fixtures it authored, and per #2371 those can only confirm
what their author already believed.

Both rows are cross-phase integration and pass today, like the rest of
this suite. No row here is red-first and none is described as such.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(#3244): extract withAgentsDir instead of try/finally in test bodies

Review finding. Rows 7 and 10 put try/finally directly inside the test
callback to save and restore GSD_AGENTS_DIR. CONTRIBUTING prohibits that
in test bodies — it masks failures — and permits it only inside
standalone helpers. The phase's own test matrix restated the rule and it
was violated anyway.

lint:ci does not catch this; it is a prose standard, which is precisely
why it survived to review.

It was also inconsistent with this file's own conventions:
runCodexInstall, runEffortSyncDryRun and captureStderr already factor
save/restore into helpers. withAgentsDir now follows the same shape and
both sites use it.

No assertion changed — both rows still drive the real
checkCodexModelPosture against the real installed tree, which is the
whole point of them. Swept the rest of the file: the only other try
blocks are inside the two pre-existing helpers and were already
compliant.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: sim <sim@local>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 02:11:57 -04:00

421 lines
20 KiB
JavaScript

// allow-test-rule: source-text-is-the-product (see #3244)
// Reads the installed .toml agent files whose deployed text IS what Codex
// loads at session start — testing that text tests the deployed contract.
// (Row 7/10 additionally assert on Phase 2's parsed IR — see below — the raw
// text reads here are for the fields that IR does not expose per-agent.)
/**
* Phase 4 smoke test — Codex passive model posture, end-to-end (#3244).
*
* Test-only; no production change. Drives a REAL install through
* `install()` (the same entry point every user hits) and then feeds the
* emitted tree to Phase 2's validator (`checkCodexModelPosture`,
* gsd-core/bin/lib/agent-install-check.cjs). This is the only place in the
* epic where the emitter (#3241) and the validator (#3242) are exercised
* against the same bytes — every phase before this tested its own half
* against fixtures it authored (#2371).
*
* Row-to-classification map (see .gsd/phase/test-3244-codex-inherit-smoke/
* 50-test-matrix.md for the full rationale):
* Row 1, 2, 3 — regression guard (passes today; would have failed pre-#3241)
* Row 4 — regression guard — the unchanged `inherit` control
* Row 5, 6 — regression guard — negative proof (posture removes a pin,
* not an agent; #774 service_tier/model_verbosity decoupled)
* Row 7 — cross-phase integration (emitter -> validator)
* Row 8 — cross-phase integration (emitter -> repairer, dry-run,
* posture-clean tree needs zero changes)
* Row 9 — regression guard — explicit pin survives
* Row 10 — cross-phase integration (emitter -> validator, legal pin)
* Row 11 — cross-phase integration (emitter -> repairer, dry-run,
* legal pin reported skipped and left byte-identical)
*
* Rows 8 and 11 exercise Phase 3's repair sync (#3243: "sync installed codex
* .toml model/effort to the passive posture") against a REAL emitted tree —
* `cmdEffortSync` (src/commands.cts) is driven the same way Phase 3's own
* `tests/commands.test.cjs` (describe('#3243 (ADR-2313 D7): Codex .toml
* effort sync')) drives it: `require('../gsd-core/bin/lib/commands.cjs')`,
* called as `cmdEffortSync(cwd, raw=false, {dryRun, configDir, runtime:
* 'codex'})`, with its `output()` JSON captured off fd 1 (see
* `captureOutput` below — same technique as commands.test.cjs's own helper).
* Both rows are expected to PASS today (Phase 3 is merged) — cross-phase
* integration, not red-first; see 50-test-matrix.md's "Red-before-green"
* section.
*/
'use strict';
process.env.GSD_TEST_MODE = '1';
const { test, describe, beforeEach, afterEach } = 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 { install } = require('../bin/install.js');
const { checkCodexModelPosture } = require('../gsd-core/bin/lib/agent-install-check.cjs');
const { cmdEffortSync } = require('../gsd-core/bin/lib/commands.cjs');
const { parseCodexAgentToml } = require('../gsd-core/bin/lib/codex-agent-toml.cjs');
const { cleanup } = require('./helpers.cjs');
const REPO_ROOT = path.resolve(__dirname, '..');
function makeTmpDir(prefix) {
return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
}
/**
* Run a global Codex install, redirecting CODEX_HOME to codexHome. Mirrors
* the `runGlobalInstall` helper in tests/install-runtime-artifacts.test.cjs
* (reused pattern, not reused code — that helper is not exported). HOME is
* isolated so install.js code that calls os.homedir() never touches the
* real $HOME, and GSD_SKIP_STALE_SDK_CHECK=1 suppresses the `npm ls -g`
* subprocess (irrelevant to posture assertions, and a subprocess this test
* does not want to be timeout-bound on).
*/
function runCodexInstall(codexHome) {
const isolatedHome = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-3244-home-'));
const prevCodexHome = process.env.CODEX_HOME;
const prevCwd = process.cwd();
const prevHome = process.env.HOME;
const prevUserProfile = process.env.USERPROFILE;
const prevSkipStale = process.env.GSD_SKIP_STALE_SDK_CHECK;
process.env.CODEX_HOME = codexHome;
process.env.HOME = isolatedHome;
process.env.USERPROFILE = isolatedHome;
process.env.GSD_SKIP_STALE_SDK_CHECK = '1';
process.chdir(REPO_ROOT);
try {
install(true, 'codex');
} finally {
process.chdir(prevCwd);
if (prevCodexHome === undefined) delete process.env.CODEX_HOME;
else process.env.CODEX_HOME = prevCodexHome;
if (prevHome === undefined) delete process.env.HOME;
else process.env.HOME = prevHome;
if (prevUserProfile === undefined) delete process.env.USERPROFILE;
else process.env.USERPROFILE = prevUserProfile;
if (prevSkipStale === undefined) delete process.env.GSD_SKIP_STALE_SDK_CHECK;
else process.env.GSD_SKIP_STALE_SDK_CHECK = prevSkipStale;
cleanup(isolatedHome);
}
}
function writeProjectConfig(projectDir, config) {
fs.mkdirSync(path.join(projectDir, '.planning'), { recursive: true });
fs.writeFileSync(
path.join(projectDir, '.planning', 'config.json'),
JSON.stringify(config, null, 2),
);
}
function readToml(codexHome, agentName) {
return fs.readFileSync(path.join(codexHome, 'agents', `${agentName}.toml`), 'utf8');
}
/**
* Lines before the `developer_instructions = '''` marker — the header
* region generateCodexAgentToml always emits model/effort/service_tier/etc.
* into (bin/install.js's `lines` array construction). Scoping to this
* region, rather than scanning the whole file, is required: GSD agent
* prompts discuss "model" constantly, so a bare substring/whole-file scan
* would false-match prose inside the developer_instructions block. Same
* convention already established in tests/install-runtime-artifacts.test.cjs
* (describe('#3241 ...')).
*/
function headerLines(tomlContent) {
const lines = tomlContent.split(/\r?\n/);
const bodyStart = lines.findIndex((line) => line.startsWith("developer_instructions = '''"));
assert.notStrictEqual(bodyStart, -1,
`expected a developer_instructions = ''' marker to scope the header scan against\nActual:\n${tomlContent.slice(0, 500)}`);
return lines.slice(0, bodyStart);
}
function findKeyLine(header, key) {
const re = new RegExp(`^${key}\\s*=`);
return header.find((line) => re.test(line));
}
/**
* Runs Phase 3's `cmdEffortSync` codex branch against `codexHome` and returns
* the parsed structured result. Mirrors `syncCodex` in tests/commands.test.cjs
* (describe('#3243 (ADR-2313 D7): Codex .toml effort sync')) — `configDir` is
* the agents dir's PARENT (cmdEffortSync joins 'agents' itself), `raw` is
* false so `output()` emits JSON, and `output()`'s `fs.writeSync(1, ...)` is
* intercepted rather than parsed from stdout text, so this asserts on
* structure, never prose.
*/
function runEffortSyncDryRun(codexHome) {
const origWriteSync = fs.writeSync;
let captured = '';
fs.writeSync = (fd, data) => {
if (fd === 1) { captured += data; return data.length; }
return origWriteSync(fd, data);
};
try {
cmdEffortSync(codexHome, false, { dryRun: true, configDir: codexHome, runtime: 'codex' });
} finally {
fs.writeSync = origWriteSync;
}
return JSON.parse(captured);
}
/**
* Runs `fn()` with `GSD_AGENTS_DIR` set to `dir`, restoring the prior value
* (or deleting it, if previously unset) in a `finally` inside this helper —
* never inside a test body, per CONTRIBUTING.md's try/finally-in-tests ban.
* Returns `fn`'s result.
*/
function withAgentsDir(dir, fn) {
const prevAgentsDir = process.env.GSD_AGENTS_DIR;
process.env.GSD_AGENTS_DIR = dir;
try {
return fn();
} finally {
if (prevAgentsDir === undefined) delete process.env.GSD_AGENTS_DIR;
else process.env.GSD_AGENTS_DIR = prevAgentsDir;
}
}
function captureStderr() {
const chunks = [];
const original = process.stderr.write;
process.stderr.write = (chunk) => { chunks.push(String(chunk)); return true; };
return {
restore() { process.stderr.write = original; },
text() { return chunks.join(''); },
noticeLines() {
return chunks.join('').split(/\r?\n/).filter((line) => line.startsWith('gsd: notice — '));
},
};
}
// ─── Rows 1, 2, 3, 5a, 6, 7: model_profile "balanced" (the default) ─────────
// This is the path that changed. `readGsdRuntimeProfileResolver` resolves a
// per-tier Codex model for "balanced" (bin/install.js:1666-1682) — under
// Phase 1 (#3241) that resolved model is no longer embedded, only warned
// about via the one-time notice. `model_profile: "inherit"` (row 4, below)
// short-circuits the resolver to null (bin/install.js:1667) and would have
// omitted the model even before #3241 — asserting it alone would be vacuous.
describe('Row 1/2/3/5a/6/7 — codex install, model_profile: balanced (default), no model_overrides', () => {
let tmpDir;
let codexHome;
let stderr;
beforeEach(() => {
tmpDir = makeTmpDir('gsd-3244-balanced-');
const projectDir = path.join(tmpDir, 'project');
codexHome = path.join(projectDir, '.codex');
fs.mkdirSync(codexHome, { recursive: true });
// runtime:"codex" with no model_profile key resolves to "balanced"
// (bin/install.js:1657) and no model_overrides — the default shipping
// shape.
writeProjectConfig(projectDir, { runtime: 'codex' });
stderr = captureStderr();
runCodexInstall(codexHome);
});
afterEach(() => {
stderr.restore();
cleanup(tmpDir);
});
test('row 1: researcher/planner/checker .toml carry no model', () => {
for (const agent of ['gsd-phase-researcher', 'gsd-planner', 'gsd-plan-checker']) {
const header = headerLines(readToml(codexHome, agent));
assert.strictEqual(findKeyLine(header, 'model'), undefined,
`${agent}.toml must not pin a model under the default balanced profile\nheader:\n${header.join('\n')}`);
}
});
test('row 2: researcher/planner/checker .toml carry no model_reasoning_effort (#838 coupling)', () => {
for (const agent of ['gsd-phase-researcher', 'gsd-planner', 'gsd-plan-checker']) {
const header = headerLines(readToml(codexHome, agent));
assert.strictEqual(findKeyLine(header, 'model_reasoning_effort'), undefined,
`${agent}.toml must not emit model_reasoning_effort with no model pinned\nheader:\n${header.join('\n')}`);
}
});
test('row 3: exactly one "gsd: notice — " line on stderr', () => {
const notices = stderr.noticeLines();
assert.strictEqual(notices.length, 1,
`expected exactly one notice line across the whole install, got ${notices.length}\nstderr:\n${stderr.text()}`);
});
test('row 5a: gsd-planner.toml keeps name/description/sandbox_mode/developer_instructions intact', () => {
const content = readToml(codexHome, 'gsd-planner');
const header = headerLines(content);
const nameLine = findKeyLine(header, 'name');
const descLine = findKeyLine(header, 'description');
const sandboxLine = findKeyLine(header, 'sandbox_mode');
assert.ok(nameLine && /=\s*".+"/.test(nameLine), `name must be present and non-empty\nActual:\n${nameLine}`);
assert.ok(descLine && /=\s*".+"/.test(descLine), `description must be present and non-empty\nActual:\n${descLine}`);
assert.ok(sandboxLine && /=\s*".+"/.test(sandboxLine), `sandbox_mode must be present and non-empty\nActual:\n${sandboxLine}`);
assert.match(content, /developer_instructions = '''[\s\S]+'''/,
'developer_instructions block must be present and non-empty');
});
test('row 6: gsd-plan-checker.toml (light tier) still emits service_tier/model_verbosity (#774, decoupled)', () => {
const header = headerLines(readToml(codexHome, 'gsd-plan-checker'));
const serviceTierLine = findKeyLine(header, 'service_tier');
const verbosityLine = findKeyLine(header, 'model_verbosity');
assert.match(serviceTierLine || '', /=\s*"flex"$/,
`gsd-plan-checker.toml must still emit service_tier = "flex"\nheader:\n${header.join('\n')}`);
assert.match(verbosityLine || '', /=\s*"low"$/,
`gsd-plan-checker.toml must still emit model_verbosity = "low"\nheader:\n${header.join('\n')}`);
});
test('row 7: checkCodexModelPosture reports the freshly-installed tree clean (emit -> validate)', () => {
const result = withAgentsDir(path.join(codexHome, 'agents'), () => checkCodexModelPosture('codex'));
assert.strictEqual(result.ok, true,
`Phase 2's validator must report the tree Phase 1's emitter just produced as clean\nviolations:\n${JSON.stringify(result.violations, null, 2)}`);
assert.deepStrictEqual(result.violations, []);
assert.ok(result.checked.includes('gsd-planner'), 'sanity: gsd-planner.toml must have been checked');
});
test('row 8: effort sync (dry-run) reports zero changes on the freshly-installed tree (emit -> repair)', () => {
const result = runEffortSyncDryRun(codexHome);
assert.strictEqual(result.synced, 0,
`Phase 3's repairer must find nothing to fix in the tree Phase 1's emitter just produced\nchanges:\n${JSON.stringify(result.changes, null, 2)}`);
assert.deepStrictEqual(result.changes, []);
assert.deepStrictEqual(result.refused, []);
assert.deepStrictEqual(result.write_failures, []);
assert.ok(result.skipped > 0, 'sanity: at least one agent must have been examined and reported skipped (clean)');
});
});
// ─── Row 4/5b — model_profile "inherit": the unchanged control ─────────────
// readGsdRuntimeProfileResolver returns null for "inherit" (bin/install.js:
// 1667) — a Codex install under inherit omitted the model before #3241 too.
// This row alone would be vacuous (would pass identically pre/post-epic);
// it exists as the negative control alongside row 1's positive case.
describe('Row 4/5b — codex install, model_profile: inherit (unchanged control)', () => {
let tmpDir;
let codexHome;
let stderr;
beforeEach(() => {
tmpDir = makeTmpDir('gsd-3244-inherit-');
const projectDir = path.join(tmpDir, 'project');
codexHome = path.join(projectDir, '.codex');
fs.mkdirSync(codexHome, { recursive: true });
writeProjectConfig(projectDir, { runtime: 'codex', model_profile: 'inherit' });
stderr = captureStderr();
runCodexInstall(codexHome);
});
afterEach(() => {
stderr.restore();
cleanup(tmpDir);
});
test('row 4: no model, no effort, and no notice — nothing was lost', () => {
const header = headerLines(readToml(codexHome, 'gsd-planner'));
assert.strictEqual(findKeyLine(header, 'model'), undefined,
'gsd-planner.toml must not pin a model under model_profile: inherit');
assert.strictEqual(findKeyLine(header, 'model_reasoning_effort'), undefined,
'gsd-planner.toml must not emit model_reasoning_effort under model_profile: inherit');
assert.strictEqual(stderr.noticeLines().length, 0,
`inherit must never fire the #3241 notice — nothing was lost\nstderr:\n${stderr.text()}`);
});
test('row 5b: gsd-planner.toml keeps name/description/sandbox_mode/developer_instructions intact', () => {
const content = readToml(codexHome, 'gsd-planner');
const header = headerLines(content);
const nameLine = findKeyLine(header, 'name');
const descLine = findKeyLine(header, 'description');
const sandboxLine = findKeyLine(header, 'sandbox_mode');
assert.ok(nameLine && /=\s*".+"/.test(nameLine), `name must be present and non-empty\nActual:\n${nameLine}`);
assert.ok(descLine && /=\s*".+"/.test(descLine), `description must be present and non-empty\nActual:\n${descLine}`);
assert.ok(sandboxLine && /=\s*".+"/.test(sandboxLine), `sandbox_mode must be present and non-empty\nActual:\n${sandboxLine}`);
assert.match(content, /developer_instructions = '''[\s\S]+'''/,
'developer_instructions block must be present and non-empty');
});
});
// ─── Row 9/10 — an explicit real-Codex model_overrides pin survives ────────
// model_profile: inherit (nullifying the resolver, per row 4) plus an
// explicit model_overrides pin for gsd-planner only: model_overrides is
// checked before, and independently of, the runtime resolver
// (bin/install.js:4220-4237), so the pin embeds regardless of profile, while
// inherit guarantees no OTHER agent's notice fires. This isolates "does an
// explicit pin survive" from "does the resolver's notice fire" — the two
// things row 9 asserts.
describe('Row 9/10 — codex install with an explicit real-Codex model_overrides pin', () => {
let tmpDir;
let codexHome;
let stderr;
const PINNED_MODEL = 'gpt-5.6-sol';
beforeEach(() => {
tmpDir = makeTmpDir('gsd-3244-pinned-');
const projectDir = path.join(tmpDir, 'project');
codexHome = path.join(projectDir, '.codex');
fs.mkdirSync(codexHome, { recursive: true });
writeProjectConfig(projectDir, {
runtime: 'codex',
model_profile: 'inherit',
model_overrides: { 'gsd-planner': PINNED_MODEL },
});
stderr = captureStderr();
runCodexInstall(codexHome);
});
afterEach(() => {
stderr.restore();
cleanup(tmpDir);
});
test('row 9: the pin is present, its coupled effort is present, and the notice does not fire', () => {
const header = headerLines(readToml(codexHome, 'gsd-planner'));
const modelLine = findKeyLine(header, 'model');
const effortLine = findKeyLine(header, 'model_reasoning_effort');
assert.match(modelLine || '', new RegExp(`=\\s*"${PINNED_MODEL}"$`),
`gsd-planner.toml must pin the explicit real-Codex model\nheader:\n${header.join('\n')}`);
assert.ok(effortLine, `gsd-planner.toml must emit model_reasoning_effort alongside an explicit model pin (#838 coupling)\nheader:\n${header.join('\n')}`);
assert.strictEqual(stderr.noticeLines().length, 0,
`an explicit pin must never fire the #3241 "resolver model omitted" notice\nstderr:\n${stderr.text()}`);
});
test('row 10: checkCodexModelPosture reports the pinned tree clean — a legal pin is not a violation', () => {
const result = withAgentsDir(path.join(codexHome, 'agents'), () => checkCodexModelPosture('codex'));
assert.strictEqual(result.ok, true,
`Phase 2's validator must not flag a legal explicit model_overrides pin\nviolations:\n${JSON.stringify(result.violations, null, 2)}`);
assert.deepStrictEqual(result.violations, []);
});
test('row 11: effort sync (dry-run) reports the pinned agent skipped, not synced, and the file is byte-identical', () => {
const filePath = path.join(codexHome, 'agents', 'gsd-planner.toml');
const before = fs.readFileSync(filePath, 'utf8');
const result = runEffortSyncDryRun(codexHome);
const plannerChange = result.changes.find((c) => c.agent === 'gsd-planner');
assert.strictEqual(plannerChange, undefined,
`a legal pin must never be reported as a change\nchanges:\n${JSON.stringify(result.changes, null, 2)}`);
assert.strictEqual(result.synced, 0,
'an over-eager stripper would report this agent synced; a legal pin must be skipped instead');
assert.ok(result.skipped > 0);
const after = fs.readFileSync(filePath, 'utf8');
assert.strictEqual(after, before,
'file contents must be byte-identical after a dry-run — comparing bytes, not mtime, per the phase brief');
// Confirm via Phase 3's own IR (parseCodexAgentToml), not just the raw
// text compare above: the pin and its coupled effort must still be
// present in the parsed structure, not merely absent from `changes`.
const parsed = parseCodexAgentToml(after);
assert.strictEqual(parsed.ok, true);
assert.strictEqual(parsed.doc.model, PINNED_MODEL,
'the legal pin must still be present in the parsed IR after a dry-run sync');
assert.strictEqual(parsed.doc.reasoningEffort !== null, true,
'the coupled effort must still be present in the parsed IR after a dry-run sync');
});
});