* test(#2940): prove gsd-update discards codex config after the GSD marker Failing-first regression for #2940. mergeCodexConfig Case 2 (marker present) preserves content before the marker but discards everything from the marker to EOF, so user/Codex-CLI settings ([model], [mcp_servers.*], [profiles.*]) added after a fresh install are wiped on every update. Rows 1-2 assert trailing user content survives; rows 3-7 cover idempotency, the #2406 leaked-section non-regression, the [agents] namespace, both regions, and the zero-trailing boundary. * fix(#2940): preserve codex config.toml content after the GSD marker on update mergeCodexConfig Case 2 (marker present) preserved content before the marker but discarded everything from the marker to EOF, so user/Codex-CLI settings added after a fresh install ([model], [mcp_servers.*], [profiles.*]) were wiped on every gsd-update. Route the trailing region through the existing stripLeakedGsdCodexSections, which removes GSD's own managed/leaked sections (the bare [agents] table GSD regenerates, legacy [agents.gsd-*], [[agents]]) while preserving genuine user TOML. The marker comment line (and the optional codex_hooks ownership line) is stripped from the trailing region first so it is not duplicated alongside the regenerated block. #2406's de-dup still holds (leaked sections after the marker are still stripped), and re-merge is idempotent. * fix(#2940): use CRLF-safe regex in regression test (lint:ci no-crlf-fragile-split) The Row 7 blank-line assertion used a bare \n which trips local/no-crlf-fragile-split under Windows git-autocrlf. Use \r?\n. * fix(#2940): correct Row 5 to valid single-[agents] TOML shape The isolated adversarial review found Row 5 (bareAgentsAfterMarkerHandled) constructed TWO [agents] tables (the GSD-managed one + a user trailing one), which is invalid TOML — a duplicate table definition. extractCodexUserAgentsScalars reads only the first [agents] and stripLeakedGsdCodexSections removes ALL bare [agents] tables, so the user's max_threads would be silently dropped. The valid, realistic shape is the user folding max_threads INTO the managed [agents] block (which the existing spliceCodexAgentsScalars path preserves) plus a separate trailing [model]. Reword Row 5 to that shape. A duplicate-[agents] input is invalid TOML Codex itself rejects, so it is out of scope for this fix. * chore(#2940): add changeset fragment * chore(#2940): backfill changeset PR number 3067 --------- Co-authored-by: sim <sim@local>
This commit is contained in:
@@ -6322,17 +6322,40 @@ function mergeCodexConfig(configPath, gsdBlock) {
|
||||
const normalizedGsdBlock = mergedGsdBlock.replace(/\r?\n/g, eol);
|
||||
const markerIndex = existing.indexOf(GSD_CODEX_MARKER);
|
||||
|
||||
// Case 2: Has GSD marker — truncate and re-append
|
||||
// Case 2: Has GSD marker — preserve user content on BOTH sides, regenerate the GSD block.
|
||||
//
|
||||
// #2940: the marker delimits where GSD's OWN block begins, NOT where every post-marker byte
|
||||
// is GSD-owned. A fresh install writes the GSD block as the file's entire content, so any
|
||||
// settings the user or Codex CLI later adds ([model], [mcp_servers.*], [profiles.*]) land
|
||||
// AFTER the block. The previous truncate-to-marker logic discarded that trailing region on
|
||||
// every update, destroying user config. The fix routes the trailing region through the
|
||||
// existing AST-based `stripLeakedGsdCodexSections`, which removes GSD's own managed/leaked
|
||||
// sections (the bare [agents] table GSD regenerates, legacy [agents.gsd-*], [[agents]])
|
||||
// while preserving genuine user TOML — so #2406's de-dup still holds AND user content survives.
|
||||
if (markerIndex !== -1) {
|
||||
let before = existing.substring(0, markerIndex).trimEnd();
|
||||
if (before) {
|
||||
// Strip any GSD-managed sections that leaked above the marker from previous installs
|
||||
before = stripLeakedGsdCodexSections(before).trimEnd();
|
||||
|
||||
atomicWriteFileSync(configPath, before + eol + eol + normalizedGsdBlock + eol);
|
||||
} else {
|
||||
atomicWriteFileSync(configPath, normalizedGsdBlock + eol);
|
||||
}
|
||||
// Capture and preserve genuine user content AFTER the GSD-managed region. The whole
|
||||
// post-marker region is passed through stripLeakedGsdCodexSections: GSD's own previously-
|
||||
// emitted [agents] table (regenerated above as normalizedGsdBlock) and any leaked sections
|
||||
// are removed, while user tables ([model], [mcp_servers.*], [profiles.*]) are kept. The
|
||||
// marker comment line itself (and the optional codex_hooks ownership line right under it)
|
||||
// is GSD-owned and is stripped from the trailing region so it is not duplicated alongside
|
||||
// the freshly regenerated block.
|
||||
const rawAfter = existing.substring(markerIndex);
|
||||
const markerStripped = rawAfter
|
||||
.replace(GSD_CODEX_MARKER, '')
|
||||
.replace(/^\r?\n# GSD codex_hooks ownership: (?:section|root_dotted)\r?\n/, '');
|
||||
const afterUser = stripLeakedGsdCodexSections(markerStripped).trim();
|
||||
|
||||
const parts = [];
|
||||
if (before) parts.push(before);
|
||||
parts.push(normalizedGsdBlock);
|
||||
if (afterUser) parts.push(afterUser);
|
||||
atomicWriteFileSync(configPath, parts.join(eol + eol) + eol);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user