'use strict'; /** * Behavioral tests for codex-agent-toml.cjs (#3243, ADR-2313 Phase 3). * * Module: msd-core/bin/lib/codex-agent-toml.cjs * Exports: PARSE_REASON, parseCodexAgentToml, renderCodexAgentToml, * stripModel, stripReasoningEffort * * Spec: .msd/phase/feat-3243-codex-toml-sync/{40-design,50-test-matrix}.md * Row numbers below (# A) map 1:1 to 50-test-matrix.md's "A — the IR" table. * * Every fixture is hand-authored against the real `generateCodexAgentToml` * shape (header fields + `developer_instructions = '''...'''`), never produced * by calling that emitter (CONTRIBUTING's fixture-provenance rule, #2371) — a * fixture generated by the writer under test could only confirm what the * writer already believes about its own output. */ const { test, describe } = require('node:test'); const assert = require('node:assert/strict'); const { PARSE_REASON, parseCodexAgentToml, renderCodexAgentToml, stripModel, stripReasoningEffort, } = require('../msd-core/bin/lib/codex-agent-toml.cjs'); // Row 18a's CRLF fixture (and A12 here) is derived at test runtime, never read // from a committed file: `.gitattributes` (`* text=auto eol=lf`, repo-wide) would // normalize any committed `\r\n` fixture back to LF on checkout, silently // defeating the CRLF assertion. Authoring LF content and converting it here // keeps the CRLF-ness under the test's control instead of git's. function toCrlf(lfContent) { return lfContent.replace(/\n/g, '\r\n'); } // ─── A1-A13 fixtures (hand-authored, #2371) ─────────────────────────────────── const A1_FULL = 'name = "msd-planner"\n' + 'description = "Plans phases for MSD milestones"\n' + 'model = "gpt-5.6-sol"\n' + 'model_reasoning_effort = "high"\n' + "developer_instructions = '''\n" + 'Plan the next phase.\n' + "'''\n"; const A2_NO_HEADER_FIELDS = 'name = "msd-plain"\n' + 'description = "No model, no effort"\n' + "developer_instructions = '''\n" + 'Just work.\n' + "'''\n"; const A3_DOUBLE_QUOTED_KEY = 'name = "msd-quoted-double"\n' + '"model" = "sonnet"\n' + "developer_instructions = '''\n" + 'Work.\n' + "'''\n"; const A3_SINGLE_QUOTED_KEY = "name = \"msd-quoted-single\"\n" + "'model' = \"sonnet\"\n" + "developer_instructions = '''\n" + 'Work.\n' + "'''\n"; const A4_VERBOSITY_ONLY = 'name = "msd-light"\n' + 'model_verbosity = "low"\n' + "developer_instructions = '''\n" + 'Work fast.\n' + "'''\n"; const A5_MODEL_INSIDE_BLOCK = 'name = "msd-planner"\n' + "developer_instructions = '''\n" + 'When picking a plan, remember: model = "sonnet" is just prose here.\n' + "'''\n"; const A6_MODEL_AFTER_BLOCK = 'name = "msd-reordered"\n' + "developer_instructions = '''\n" + 'Work.\n' + "'''\n" + 'model = "sonnet"\n'; const A7_DECOY_MARKER_IN_DESCRIPTION = 'name = "msd-decoy-marker"\n' + 'description = "mentions developer_instructions = \'\'\' as an example string"\n' + 'model = "sonnet"\n' + "developer_instructions = '''\n" + 'Work.\n' + "'''\n"; const A8_SAME_LINE_BLOCK = 'name = "msd-terse"\n' + "developer_instructions = '''one line block'''\n"; const A9_UNTERMINATED = 'name = "msd-broken"\n' + "developer_instructions = '''\n" + 'This block never closes.\n'; const A10_NO_BLOCK_AT_ALL = 'name = "msd-noblock"\n' + 'description = "no prompt block on this agent"\n' + '"model" = "sonnet"\n'; const A11_COMMENTED_PIN = 'name = "msd-reviewer"\n' + '# model = "sonnet"\n' + "developer_instructions = '''\n" + 'Review.\n' + "'''\n"; const A12_CRLF_SOURCE = toCrlf( 'name = "msd-scribe"\n' + 'model = "sonnet"\n' + "developer_instructions = '''\n" + 'Write a changelog entry.\n' + "'''\n", ); const A13_BOM_SOURCE = String.fromCharCode(0xfeff) + 'name = "msd-archivist"\n' + 'model = "sonnet"\n' + "developer_instructions = '''\n" + 'Archive.\n' + "'''\n"; // ─── A1-A13: parse behavior ──────────────────────────────────────────────── describe('parseCodexAgentToml: happy / boundary / hostile', () => { test('A1: real emitted .toml (all header fields + block) — every field recovered', () => { const result = parseCodexAgentToml(A1_FULL); assert.equal(result.ok, true); assert.equal(result.doc.model, 'gpt-5.6-sol'); assert.equal(result.doc.reasoningEffort, 'high'); assert.equal(result.doc.hadBOM, false); assert.equal(result.doc.eol, '\n'); assert.equal(result.doc.trailingNewline, true); assert.notEqual(result.doc.blockRange.start, -1); }); test('A2: header with no model / no effort — both null', () => { const result = parseCodexAgentToml(A2_NO_HEADER_FIELDS); assert.equal(result.ok, true); assert.equal(result.doc.model, null); assert.equal(result.doc.reasoningEffort, null); }); test('A3: quoted keys ("model" = / \'model\' =) recovered as key model', () => { const doubleQuoted = parseCodexAgentToml(A3_DOUBLE_QUOTED_KEY); assert.equal(doubleQuoted.ok, true); assert.equal(doubleQuoted.doc.model, 'sonnet'); const singleQuoted = parseCodexAgentToml(A3_SINGLE_QUOTED_KEY); assert.equal(singleQuoted.ok, true); assert.equal(singleQuoted.doc.model, 'sonnet'); }); test('A4: model_verbosity present, model absent — model: null (full-key anchoring)', () => { const result = parseCodexAgentToml(A4_VERBOSITY_ONLY); assert.equal(result.ok, true); assert.equal(result.doc.model, null); }); test('A5 (hostile, data-loss case): model = inside the block — model: null', () => { const result = parseCodexAgentToml(A5_MODEL_INSIDE_BLOCK); assert.equal(result.ok, true); assert.equal(result.doc.model, null); }); test('A6: model line after the block (hand-reordered) — recovered', () => { const result = parseCodexAgentToml(A6_MODEL_AFTER_BLOCK); assert.equal(result.ok, true); assert.equal(result.doc.model, 'sonnet'); }); test('A7 (hostile): description value quoting the block marker does not hide the real model pin', () => { const result = parseCodexAgentToml(A7_DECOY_MARKER_IN_DESCRIPTION); assert.equal(result.ok, true); assert.equal(result.doc.model, 'sonnet'); }); test('A8: same-line block — block range is one line', () => { const result = parseCodexAgentToml(A8_SAME_LINE_BLOCK); assert.equal(result.ok, true); assert.equal(result.doc.blockRange.start, result.doc.blockRange.end); assert.notEqual(result.doc.blockRange.start, -1); }); test('A9: unterminated block — {ok:false, reason:UNTERMINATED_BLOCK}', () => { const result = parseCodexAgentToml(A9_UNTERMINATED); assert.equal(result.ok, false); assert.equal(result.reason, PARSE_REASON.UNTERMINATED_BLOCK); }); test('A10: no developer_instructions at all — {ok:true}, whole file scanned', () => { const result = parseCodexAgentToml(A10_NO_BLOCK_AT_ALL); assert.equal(result.ok, true); assert.equal(result.doc.blockRange.start, -1); assert.equal(result.doc.model, 'sonnet'); }); test('A11 (hostile): commented # model = "x" — model: null', () => { const result = parseCodexAgentToml(A11_COMMENTED_PIN); assert.equal(result.ok, true); assert.equal(result.doc.model, null); }); test('A12 (cross-platform): CRLF input parses identically; eol recorded as \\r\\n', () => { const result = parseCodexAgentToml(A12_CRLF_SOURCE); assert.equal(result.ok, true); assert.equal(result.doc.model, 'sonnet'); assert.equal(result.doc.eol, '\r\n'); }); test('A13 (cross-platform): BOM input parses; BOM recorded', () => { const result = parseCodexAgentToml(A13_BOM_SOURCE); assert.equal(result.ok, true); assert.equal(result.doc.model, 'sonnet'); assert.equal(result.doc.hadBOM, true); }); }); // ─── A14 (LOAD-BEARING): round-trip byte-identity ───────────────────────────── // // Asserts on raw BYTES, never a re-parsed structure — a renderer that dropped // every comment or normalized whitespace would still pass a structural // comparison. A parse/render pair that fails this row silently reformats a // user's file on every sync. describe('A14 (load-bearing): render(parse(x)) === x, byte-identical', () => { const roundTripFixtures = [ ['A1_FULL', A1_FULL], ['A2_NO_HEADER_FIELDS', A2_NO_HEADER_FIELDS], ['A3_DOUBLE_QUOTED_KEY', A3_DOUBLE_QUOTED_KEY], ['A3_SINGLE_QUOTED_KEY', A3_SINGLE_QUOTED_KEY], ['A4_VERBOSITY_ONLY', A4_VERBOSITY_ONLY], ['A5_MODEL_INSIDE_BLOCK', A5_MODEL_INSIDE_BLOCK], ['A6_MODEL_AFTER_BLOCK', A6_MODEL_AFTER_BLOCK], ['A7_DECOY_MARKER_IN_DESCRIPTION', A7_DECOY_MARKER_IN_DESCRIPTION], ['A8_SAME_LINE_BLOCK', A8_SAME_LINE_BLOCK], ['A10_NO_BLOCK_AT_ALL', A10_NO_BLOCK_AT_ALL], ['A11_COMMENTED_PIN', A11_COMMENTED_PIN], ['A12_CRLF_SOURCE', A12_CRLF_SOURCE], ['A13_BOM_SOURCE', A13_BOM_SOURCE], ]; for (const [label, source] of roundTripFixtures) { test(`round-trips ${label} byte-identically, unmodified`, () => { const result = parseCodexAgentToml(source); assert.equal(result.ok, true, `${label} must parse ok`); const rendered = renderCodexAgentToml(result.doc); assert.equal(rendered, source, `${label} must round-trip byte-identically`); }); } }); // ─── A15/A16: round-trip after a targeted strip ─────────────────────────────── describe('A15/A16: round-trip after stripModel / stripReasoningEffort', () => { test('A15: after stripModel(), only the model line is gone — every other byte identical', () => { const result = parseCodexAgentToml(A1_FULL); assert.equal(result.ok, true); const stripped = stripModel(result.doc); const rendered = renderCodexAgentToml(stripped); const expected = 'name = "msd-planner"\n' + 'description = "Plans phases for MSD milestones"\n' + 'model_reasoning_effort = "high"\n' + "developer_instructions = '''\n" + 'Plan the next phase.\n' + "'''\n"; assert.equal(rendered, expected); assert.equal(stripped.model, null); assert.equal(stripped.reasoningEffort, 'high', 'the effort line must survive untouched'); }); test('A16: after stripReasoningEffort(), only that line is gone — every other byte identical', () => { const result = parseCodexAgentToml(A1_FULL); assert.equal(result.ok, true); const stripped = stripReasoningEffort(result.doc); const rendered = renderCodexAgentToml(stripped); const expected = 'name = "msd-planner"\n' + 'description = "Plans phases for MSD milestones"\n' + 'model = "gpt-5.6-sol"\n' + "developer_instructions = '''\n" + 'Plan the next phase.\n' + "'''\n"; assert.equal(rendered, expected); assert.equal(stripped.reasoningEffort, null); assert.equal(stripped.model, 'gpt-5.6-sol', 'the model line must survive untouched'); }); test('stripModel() on a doc with no model line is a no-op copy', () => { const result = parseCodexAgentToml(A2_NO_HEADER_FIELDS); assert.equal(result.ok, true); const stripped = stripModel(result.doc); assert.equal(renderCodexAgentToml(stripped), A2_NO_HEADER_FIELDS); }); test('stripReasoningEffort() on a doc with no effort line is a no-op copy', () => { const result = parseCodexAgentToml(A2_NO_HEADER_FIELDS); assert.equal(result.ok, true); const stripped = stripReasoningEffort(result.doc); assert.equal(renderCodexAgentToml(stripped), A2_NO_HEADER_FIELDS); }); }); // ─── A18-A24: mixed/edge line-ending round-trip (BLOCKER fix, per-line terminators) ── // // Regression coverage for the defect a review caught: `eol` used to be a // single whole-file flag and `split(/\r?\n/)` discarded every line's own // terminator, so `renderCodexAgentToml` re-joined with ONE style and // normalized every line — even when zero strips were performed. None of // A1-A17 exercised a MIXED-ending source (A12 is pure CRLF), so this gap // shipped untested. These fixtures are hand-authored (never emitted by // `generateCodexAgentToml`), per CONTRIBUTING's fixture-provenance rule. const A18_MIXED_EOL = 'name = "msd-mixed"\r\n' + 'model = "sonnet"\n' + 'description = "mixed line endings"\r\n' + "developer_instructions = '''\n" + 'Work.\r\n' + "'''\n"; const A20_LONE_CR = 'name = "msd-oldmac"\r' + 'model = "sonnet"\n' + "developer_instructions = '''\n" + 'Work.\n' + "'''\n"; const A21_NO_TRAILING_AFTER_BLOCK = 'name = "msd-terse2"\n' + "developer_instructions = '''\n" + 'Work.\n' + "'''"; // no trailing newline at all — last line is the block's closing ''' const A22_MULTI_TRAILING_NEWLINES = 'name = "msd-multi"\n' + 'model = "sonnet"\n' + "developer_instructions = '''\n" + 'Work.\n' + "'''\n\n\n"; // three trailing newlines after the closing ''' const A23_BOM_ONLY = String.fromCharCode(0xfeff); // a file that is ONLY a BOM const A24_EMPTY = ''; // a genuinely empty file describe('A18-A24 (BLOCKER regression): mixed/edge line-ending round-trip', () => { test('A18: mixed \\r\\n and \\n in one file, unmodified — round-trips byte-identically', () => { const result = parseCodexAgentToml(A18_MIXED_EOL); assert.equal(result.ok, true); assert.equal(result.doc.model, 'sonnet'); // Each line's own terminator is captured, never collapsed to one style. // No phantom trailing '' entry: the file has exactly 6 content lines, and // `terminators[i]` is the terminator that follows `lines[i]`, so the // arrays are the same length as the file's real line count. assert.deepEqual(result.doc.terminators, ['\r\n', '\n', '\r\n', '\n', '\r\n', '\n']); const rendered = renderCodexAgentToml(result.doc); assert.equal(rendered, A18_MIXED_EOL, 'mixed-EOL source must round-trip byte-identically, unmodified'); }); test('A19: mixed endings + a stale pin stripped — the pin\'s line (and only its own terminator) goes, every other line keeps its original terminator', () => { const result = parseCodexAgentToml(A18_MIXED_EOL); assert.equal(result.ok, true); const stripped = stripModel(result.doc); const rendered = renderCodexAgentToml(stripped); const expected = 'name = "msd-mixed"\r\n' + 'description = "mixed line endings"\r\n' + "developer_instructions = '''\n" + 'Work.\r\n' + "'''\n"; assert.equal(rendered, expected, 'the model line (and its own \\n) must be gone; every surviving line keeps its own original terminator'); assert.equal(stripped.model, null); }); test('A20: a lone \\r (old-Mac style) somewhere in the file — preserved, not upgraded to \\r\\n or collapsed to \\n', () => { const result = parseCodexAgentToml(A20_LONE_CR); assert.equal(result.ok, true); assert.equal(result.doc.model, 'sonnet'); assert.equal(result.doc.terminators[0], '\r', 'the lone CR must be recorded exactly, not merged with the following line'); const rendered = renderCodexAgentToml(result.doc); assert.equal(rendered, A20_LONE_CR, 'must round-trip byte-identically, unmodified'); }); test('A21: last line is the block\'s closing \'\'\' with no trailing newline — round-trips byte-identically', () => { const result = parseCodexAgentToml(A21_NO_TRAILING_AFTER_BLOCK); assert.equal(result.ok, true); assert.equal(result.doc.trailingNewline, false); const rendered = renderCodexAgentToml(result.doc); assert.equal(rendered, A21_NO_TRAILING_AFTER_BLOCK); }); test('A22: multiple trailing newlines — every one preserved, round-trips byte-identically', () => { const result = parseCodexAgentToml(A22_MULTI_TRAILING_NEWLINES); assert.equal(result.ok, true); assert.equal(result.doc.model, 'sonnet'); const rendered = renderCodexAgentToml(result.doc); assert.equal(rendered, A22_MULTI_TRAILING_NEWLINES); }); test('A23: a file that is only a BOM — parses, round-trips to just the BOM', () => { const result = parseCodexAgentToml(A23_BOM_ONLY); assert.equal(result.ok, true); assert.equal(result.doc.hadBOM, true); assert.equal(result.doc.model, null); const rendered = renderCodexAgentToml(result.doc); assert.equal(rendered, A23_BOM_ONLY); }); test('A24: an empty file — parses, round-trips to an empty string', () => { const result = parseCodexAgentToml(A24_EMPTY); assert.equal(result.ok, true); assert.equal(result.doc.hadBOM, false); assert.equal(result.doc.model, null); const rendered = renderCodexAgentToml(result.doc); assert.equal(rendered, A24_EMPTY); }); }); // ─── A25-A28 (regression, B17): removeLine trailing-newline preservation ────── // // A stale/orphaned strip that lands on the file's LAST line must preserve // (not invent, not drop) the file's trailing-newline status. The defect: a // last-line removal used to keep the PREVIOUS line's own terminator, which // only happens to be correct when every terminator in the file is identical // (the common case, which is why A1-A24 never caught it) — it silently // invents a trailing newline whenever the removed line's own terminator // differs from the survivor's, e.g. no trailing newline at all (B17, // commands.test.cjs), or a mixed-EOL source (A26 below). describe('A25-A28 (regression, B17): removeLine preserves trailing-newline status', () => { test('A25 (red pre-fix): strip a model line that is the file\'s last line, no trailing newline — still no trailing newline', () => { const source = 'name = "msd-bare"\nmodel = "sonnet"'; // no trailing \n; model is the last line const result = parseCodexAgentToml(source); assert.equal(result.ok, true); assert.equal(result.doc.trailingNewline, false); const rendered = renderCodexAgentToml(stripModel(result.doc)); // Pre-fix: removeLine kept the PREVIOUS line's terminator ('\n') instead // of the removed line's own ('') — rendered came back as // 'name = "msd-bare"\n', a trailing newline the source never had. assert.equal(rendered, 'name = "msd-bare"', 'the file must still have no trailing newline'); }); test('A26 (red pre-fix): strip a model line that is the file\'s last line, WITH a trailing newline — still exactly one trailing newline', () => { // Mixed EOL is deliberate: it is the only way to distinguish the two // candidate fixes. removeLine must inherit the removed line's // EMPTINESS (trailing-newline-or-not), never its STYLE — the survivor's // OWN terminator is already the right style for the survivor; copying // the removed line's terminator onto it would silently change the // survivor's own ending, which is exactly the class of defect the // mixed-EOL round-trip guarantee (A14) exists to prevent. const source = 'name = "msd-bare"\r\nmodel = "sonnet"\n'; // name: CRLF: model (last): LF const result = parseCodexAgentToml(source); assert.equal(result.ok, true); assert.equal(result.doc.trailingNewline, true); const rendered = renderCodexAgentToml(stripModel(result.doc)); // Pre-fix (style-inheriting variant): rendered came back as // 'name = "msd-bare"\n' — the removed line's own LF terminator, which // silently changed the survivor's ending from CRLF to LF. Correct: the // survivor keeps its OWN CRLF terminator unchanged; the file still ends // with exactly one trailing newline, in the survivor's original style. assert.equal(rendered, 'name = "msd-bare"\r\n', 'exactly one trailing newline must survive, on the new last line, in ITS OWN style'); }); test('A27: strip the only line in a file — empty result', () => { // Already correct pre-fix (JS Array#slice(1) on a length-1 array is [], // so the old generic slice degenerated to the right answer here) — // included as a regression guard for the new length===1 branch, not // because it was red before the fix. const source = 'model = "sonnet"'; // one line, no trailing newline const result = parseCodexAgentToml(source); assert.equal(result.ok, true); assert.equal(result.doc.lines.length, 1); const stripped = stripModel(result.doc); assert.deepEqual(stripped.lines, []); assert.deepEqual(stripped.terminators, []); assert.equal(renderCodexAgentToml(stripped), ''); }); test('A28 (red pre-fix, row B7 shape): stale model plus its orphaned effort, effort is the last line, no trailing newline — both counts correct', () => { const source = 'name = "msd-stale"\nmodel = "opus"\nmodel_reasoning_effort = "medium"'; // no trailing \n const result = parseCodexAgentToml(source); assert.equal(result.ok, true); assert.equal(result.doc.trailingNewline, false); // Sequential removal, same order commands.test.cjs's syncCodex applies: // model first (a middle line — unaffected by the fix), then the now-last // orphaned effort line (the fix's compounding case). const afterModel = stripModel(result.doc); const afterBoth = stripReasoningEffort(afterModel); // Pre-fix: the second removal (now-last-line) kept the intermediate // survivor's terminator instead of the removed effort line's own, // rendering 'name = "msd-stale"\n' — a trailing newline the source // never had. assert.equal(renderCodexAgentToml(afterBoth), 'name = "msd-stale"', 'both lines gone AND no invented trailing newline'); assert.equal(afterBoth.model, null); assert.equal(afterBoth.reasoningEffort, null); }); test('A29 (regression, B7 shape, mixed EOL): stale model plus its orphaned effort, effort is the last line, mixed line endings — survivor keeps its OWN terminator, not the removed effort\'s', () => { // Exercises the two fixes' interaction: stripModel is a middle-line // removal (untouched by the last-line fix), then stripReasoningEffort // lands on the now-last line. Deliberately non-uniform EOL either side // of the second removal (survivor 'name' is CRLF-terminated, the // removed effort line is LF-terminated) so a style-inheriting bug and // the correct emptiness-inheriting behavior render DIFFERENT bytes. const source = 'name = "msd-stale"\r\nmodel = "opus"\nmodel_reasoning_effort = "medium"\n'; const result = parseCodexAgentToml(source); assert.equal(result.ok, true); assert.equal(result.doc.trailingNewline, true); assert.deepEqual(result.doc.terminators, ['\r\n', '\n', '\n']); const afterModel = stripModel(result.doc); // Middle-line removal: splice only, no terminator fiddling — survivor // keeps its own '\r\n', the effort line keeps its own '\n'. assert.deepEqual(afterModel.terminators, ['\r\n', '\n']); const afterBoth = stripReasoningEffort(afterModel); // If the bug were still present (inheriting the removed effort line's // OWN '\n'), this would render 'name = "msd-stale"\n' — silently // downgrading the survivor's CRLF to LF. Correct: the survivor's own // '\r\n' is unchanged; the file still ends with exactly one trailing // newline. assert.equal(renderCodexAgentToml(afterBoth), 'name = "msd-stale"\r\n', 'survivor keeps its OWN terminator; no invented/altered line ending'); assert.equal(afterBoth.model, null); assert.equal(afterBoth.reasoningEffort, null); }); }); // ─── A17: enum lock ──────────────────────────────────────────────────────── describe('PARSE_REASON enum', () => { test('A17: Object.keys(PARSE_REASON).sort() is locked and frozen', () => { assert.deepEqual(Object.keys(PARSE_REASON).sort(), ['UNTERMINATED_BLOCK']); assert.equal(PARSE_REASON.UNTERMINATED_BLOCK, 'unterminated_block'); assert.ok(Object.isFrozen(PARSE_REASON)); }); });