* fix(#4306): forward real bytes through io.test.cjs's fault-injection mocks The bug #1008 fault-injection tests mock fs.writeSync scoped only by file descriptor. On their "success" arms (the retry-after-EAGAIN/EINTR call, and the short-write simulation) they fabricated a return byte count without ever calling the real writeSync -- the bytes went into a local array and nowhere else. node:test's process-isolation runner (default on Node >= 22) reads each test file's own stdout to parse its child-to-parent result protocol. If the runner's own reporter write for an adjacent test lands on fd 1 while one of these mocks is installed, that write was silently swallowed instead of reaching the real pipe -- observed in CI as "Unable to deserialize cloned data" (a corrupted/truncated byte stream on the parent's read side), not a thrown exception. Every "success" arm now forwards the real bytes to orig()/restore() instead of fabricating a return value, so anything else sharing the fd during the mocked window still gets its bytes delivered for real. writeAllSync (the only production caller reaching this mock) always passes a Buffer, so the forwarded calls use the buffer-form fs.writeSync overload unambiguously. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(#4306): extend fault-injection fd-swallow fix across the whole suite The originally-fixed instance (tests/io.test.cjs) was one occurrence of a copy-pasted defect: mocked fs.writeSync arms fabricated a return byte count without ever forwarding the call to the real fs.writeSync, silently discarding bytes. Under node:test's process-isolated runner, the parent reads the child's real stdout to parse v8-serialized report frames interleaved with plain output (confirmed against node's own lib/internal/test_runner/runner.js and a matching upstream issue, nodejs/node#64061) — a swallowed write on that fd corrupts the parent's parse ("Unable to deserialize cloned data"). Adds a shared, safe capture helper to tests/helpers.cjs, captureFdSync(fd, fn): it always forwards every write to the real fs.writeSync first, then records only the observed fd's bytes, sliced by the real return count (not the requested length), decoded once via Buffer.concat so a short write can't split a multi-byte codepoint across two decodes. 17 test files migrate their local copy of the unsafe mock to this shared helper. tests/worktree-base-ref.test.cjs keeps a narrower in-place fix instead (it needs to record every fd a write touched, which the shared helper doesn't expose). tests/io.test.cjs gets two follow-up correctness fixes on top of the already-committed forwarding fix: the EAGAIN/EINTR/short-write arms now derive their recorded chunk from the real return count everywhere (including the string-form overload), and the short-write test no longer forces a Buffer-shaped truncation call onto a string-form write that could land on the same fd. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --------- Co-authored-by: sim <sim@local> Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
413 lines
20 KiB
JavaScript
413 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, captureFdSync } = 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 captured = captureFdSync(1, () =>
|
|
cmdEffortSync(codexHome, false, { dryRun: true, configDir: codexHome, runtime: 'codex' })
|
|
);
|
|
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');
|
|
});
|
|
});
|