Mechanical rename produced by scripts/msd-rename.cjs: gsd/Gsd/GSD -> msd/Msd/MSD across contents and paths, upstream package/repo coordinates -> @golem15/msd-core and golem15com/msd-core. Deep links into upstream history, sibling upstream packages, the GSD-2 import feature, CHANGELOG.md and .changeset/ are kept as-is. Hand edits on top: MSD block-letter banner and logos, LICENSE copyright line, package/plugin identity, regenerated lockfile, install-tree fixtures, derived registries and benchmark baseline; migration checksum baseline re-locked (MSD keeps its own install state, so no install had applied the old sums); sort-order and regex-escaped expectations in tests adjusted.
530 lines
24 KiB
JavaScript
530 lines
24 KiB
JavaScript
'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<N>) 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));
|
|
});
|
|
});
|