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.
884 lines
40 KiB
JavaScript
884 lines
40 KiB
JavaScript
// ADR-3473 §8.1 (#3881) — consequence and boundary coverage for the js-yaml migration.
|
||
// See .msd/phase/feat-3881-one-yaml-parser/50-test-matrix.md sections A and F. Each row
|
||
// pins a consequence of swapping the hand-rolled line scanner for the vendored js-yaml
|
||
// (§40-design.md §0.2) that is otherwise invisible to the existing suite.
|
||
'use strict';
|
||
|
||
const { describe, test } = require('node:test');
|
||
const assert = require('node:assert/strict');
|
||
const fs = require('node:fs');
|
||
const path = require('node:path');
|
||
|
||
const {
|
||
extractFrontmatter,
|
||
reconstructFrontmatter,
|
||
spliceFrontmatter,
|
||
UNTERMINATED_KEY_THRESHOLD,
|
||
FRONTMATTER_UNPARSEABLE,
|
||
} = require('../msd-core/bin/lib/frontmatter.cjs');
|
||
const { transitionCore } = require('../msd-core/bin/lib/state-transition.cjs');
|
||
const {
|
||
_resetUnusableInputWarningsForTests,
|
||
_unusableInputEmissionCountForTests,
|
||
} = require('../msd-core/bin/lib/unusable-input.cjs');
|
||
const { createTempDir, cleanup, runMsdTools, createTempProject } = require('./helpers.cjs');
|
||
const { runNode } = require('./helpers/process-seam.cjs');
|
||
const { throwIfFailed } = require('./helpers/git-fixture.cjs');
|
||
const { PROBE_TIMEOUT_MS } = require('./helpers/timeouts.cjs');
|
||
const TOOLS_PATH = path.join(__dirname, '..', 'msd-core', 'bin', 'msd-tools.cjs');
|
||
|
||
const fixedClock = Object.freeze({
|
||
today: () => '2026-06-27',
|
||
localToday: () => '2026-06-27',
|
||
nowIso: () => '2026-06-27T12:00:00.000Z',
|
||
});
|
||
|
||
function withTempDir(fn) {
|
||
const dir = createTempDir('feat-3881-consequences-');
|
||
try {
|
||
return fn(dir);
|
||
} finally {
|
||
cleanup(dir);
|
||
}
|
||
}
|
||
|
||
// ─── A. Consequences ────────────────────────────────────────────────────────
|
||
|
||
describe('A1 emptyValuedKeySurvivesAWrite', () => {
|
||
test('a key with no value round-trips through parse -> reconstruct -> re-parse with the key still present', () => {
|
||
const doc = '---\nphase: 3\nprogress:\n---\n\nbody\n';
|
||
|
||
const parsed = extractFrontmatter(doc);
|
||
assert.ok(
|
||
Object.prototype.hasOwnProperty.call(parsed, 'progress'),
|
||
'an empty-valued key must survive the initial parse'
|
||
);
|
||
|
||
// reconstructFrontmatter omits null-valued keys (frontmatter.cjs: `if (value === null ...) continue`),
|
||
// so the empty-value contract only survives a write if extractFrontmatter never hands one back —
|
||
// this is what pins that guarantee rather than reconstructFrontmatter's own omission logic.
|
||
const reconstructed = reconstructFrontmatter(parsed);
|
||
const rewritten = `---\n${reconstructed}\n---\n\nbody\n`;
|
||
const reparsed = extractFrontmatter(rewritten);
|
||
|
||
assert.ok(
|
||
Object.prototype.hasOwnProperty.call(reparsed, 'progress'),
|
||
`progress must survive a write; reconstructed frontmatter was ${JSON.stringify(reconstructed)}`
|
||
);
|
||
});
|
||
});
|
||
|
||
describe('A2 unparseableDocumentKeepsItsFrontmatterBlock', () => {
|
||
test('a STATE.md with a git merge-conflict marker in its frontmatter keeps the block through beginPhase', () => {
|
||
const fmBlock = [
|
||
'---',
|
||
'<<<<<<< HEAD',
|
||
'status: foo',
|
||
'=======',
|
||
'status: bar',
|
||
'>>>>>>> feature',
|
||
'---',
|
||
'',
|
||
].join('\n');
|
||
const body = [
|
||
'# Project State',
|
||
'',
|
||
'**Status:** Planning',
|
||
'',
|
||
'## Current Position',
|
||
'',
|
||
'Phase: 2 — DONE',
|
||
'Plan: —',
|
||
'Status: Planning',
|
||
'',
|
||
].join('\n');
|
||
const content = fmBlock + body;
|
||
|
||
// Verify reachability first: the conflicted region parses to zero keys with the
|
||
// unparseable marker set, exercising the exact branch beginPhaseCore relies on.
|
||
const fm = extractFrontmatter(content);
|
||
assert.equal(Object.keys(fm).length, 0);
|
||
assert.equal(fm[FRONTMATTER_UNPARSEABLE], true);
|
||
|
||
const result = transitionCore(
|
||
content,
|
||
{ kind: 'beginPhase', phaseNumber: 3, phaseName: 'Test Phase', planCount: 5 },
|
||
{ clock: fixedClock }
|
||
);
|
||
|
||
assert.ok(
|
||
result.content.includes('<<<<<<< HEAD') &&
|
||
result.content.includes('=======') &&
|
||
result.content.includes('>>>>>>> feature'),
|
||
`frontmatter conflict markers must survive the write; got ${JSON.stringify(result.content)}`
|
||
);
|
||
});
|
||
|
||
// Post-#3881-review, finding 7: this describe block exercised only ONE of the 8 call sites
|
||
// that route through `beginFrontmatterReassembly` (frontmatter.cts's docblock names all 8:
|
||
// 7 `*Core` functions in state-transition.cts, dispatched by `transitionCore`, plus 1 more
|
||
// hand-verified separately in `state.cts`'s `cmdStateCompletePhase`). Table-driven over the
|
||
// remaining 6 `transitionCore` kinds that share the same preservation contract.
|
||
const OTHER_TRANSITION_KINDS = [
|
||
['advancePlan', { kind: 'advancePlan' }],
|
||
['completePhase', { kind: 'completePhase', phaseNum: '2', nextPhaseNum: '3', nextPhaseName: 'Next Phase', isLastPhase: false, planCount: 1, summaryCount: 1 }],
|
||
['plannedPhase', { kind: 'plannedPhase', phaseNumber: 3, phaseName: 'Test Phase', planCount: 5 }],
|
||
['milestoneComplete', { kind: 'milestoneComplete', version: 'v1.0', nextMilestoneCommand: '/msd:new-milestone' }],
|
||
['patch', { kind: 'patch', patches: { Status: 'Paused' } }],
|
||
['update', { kind: 'update', field: 'Status', value: 'Paused' }],
|
||
];
|
||
|
||
const fmBlock = [
|
||
'---',
|
||
'<<<<<<< HEAD',
|
||
'status: foo',
|
||
'=======',
|
||
'status: bar',
|
||
'>>>>>>> feature',
|
||
'---',
|
||
'',
|
||
].join('\n');
|
||
const body = [
|
||
'# Project State',
|
||
'',
|
||
'**Status:** Planning',
|
||
'',
|
||
'## Current Position',
|
||
'',
|
||
'Phase: 2 — DONE',
|
||
'Plan: —',
|
||
'Status: Planning',
|
||
'',
|
||
].join('\n');
|
||
const content = fmBlock + body;
|
||
|
||
for (const [label, intent] of OTHER_TRANSITION_KINDS) {
|
||
test(`a git merge-conflict marker in the frontmatter keeps the block through ${label}`, () => {
|
||
const result = transitionCore(content, intent, { clock: fixedClock, roadmapProvider: () => null });
|
||
assert.ok(
|
||
result.content.includes('<<<<<<< HEAD')
|
||
&& result.content.includes('=======')
|
||
&& result.content.includes('>>>>>>> feature'),
|
||
`${label}: frontmatter conflict markers must survive the write; got ${JSON.stringify(result.content)}`
|
||
);
|
||
});
|
||
}
|
||
|
||
});
|
||
|
||
// ─── A2b. The 8th reassemble site — the real CLI path, not just the pure transform ─────────
|
||
//
|
||
// Post-#3881-review, second round: the 7 `transitionCore` kinds above preserve an unparseable
|
||
// block at the PURE-TRANSFORM layer, but `state.cts`'s CLI adapters wrap every transform in
|
||
// `readModifyWriteStateMd` -> `syncAndPreserveStateMd`, which reruns `extractFrontmatter` on
|
||
// the (already-preserved) result and — before this fix — unconditionally re-derived a FRESH
|
||
// frontmatter block, discarding the raw one a second time. Confirmed by execution: BEFORE the
|
||
// fix, `state complete-phase` on a conflict-marker STATE.md returned success with the markers
|
||
// GONE, replaced by a freshly-derived well-formed block (re-derivation, not fence deletion —
|
||
// case (b), not (a)). Table-driven over the CLI verbs found to share the same
|
||
// `readModifyWriteStateMd` path, plus a control proving the ADR-3408 §8.3 CLOSED-list "body
|
||
// wins" contract (`state sync`) is untouched.
|
||
describe('A2b unparseableFrontmatterSurvivesTheRealCliPath', () => {
|
||
const CONFLICT_FM_BLOCK = [
|
||
'---',
|
||
'<<<<<<< HEAD',
|
||
'status: foo',
|
||
'=======',
|
||
'status: bar',
|
||
'>>>>>>> feature',
|
||
'---',
|
||
'',
|
||
].join('\n');
|
||
const CONFLICT_BODY = [
|
||
'# Project State',
|
||
'',
|
||
'## Current Position',
|
||
'',
|
||
'Phase: 1 — Foundation',
|
||
'Plan: 1 of 1',
|
||
'Status: Executing Phase 1',
|
||
'Last activity: 2026-07-01 — mid-flight',
|
||
'',
|
||
].join('\n');
|
||
|
||
function writeConflictFixture(tmpDir) {
|
||
const planningDir = path.join(tmpDir, '.planning');
|
||
fs.mkdirSync(path.join(planningDir, 'phases', '01-foundation'), { recursive: true });
|
||
fs.writeFileSync(
|
||
path.join(planningDir, 'ROADMAP.md'),
|
||
['# Roadmap', '', '### Phase 1: Foundation', '**Goal:** Setup', ''].join('\n'),
|
||
);
|
||
const statePath = path.join(planningDir, 'STATE.md');
|
||
fs.writeFileSync(statePath, CONFLICT_FM_BLOCK + CONFLICT_BODY);
|
||
return statePath;
|
||
}
|
||
|
||
const NON_SANCTIONED_VERBS = [
|
||
['state complete-phase', ['state', 'complete-phase']],
|
||
['state update', ['state', 'update', 'Last Activity', '2026-08-26']],
|
||
['query state.patch', ['query', 'state.patch', JSON.stringify({ Status: 'Paused for review' })]],
|
||
['state begin-phase', ['state', 'begin-phase', '--phase', '2', '--name', 'Next Phase']],
|
||
];
|
||
|
||
for (const [label, args] of NON_SANCTIONED_VERBS) {
|
||
test(`${label}: a git merge-conflict-marked frontmatter block survives the real CLI write`, () => {
|
||
const tmpDir = createTempProject();
|
||
try {
|
||
const statePath = writeConflictFixture(tmpDir);
|
||
const result = runMsdTools(args, tmpDir);
|
||
assert.ok(result.success, `${label} failed: ${result.error}`);
|
||
const after = fs.readFileSync(statePath, 'utf-8');
|
||
assert.ok(
|
||
after.includes('<<<<<<< HEAD') && after.includes('=======') && after.includes('>>>>>>> feature'),
|
||
`${label}: conflict markers must survive; got:\n${after}`,
|
||
);
|
||
} finally {
|
||
cleanup(tmpDir);
|
||
}
|
||
});
|
||
}
|
||
|
||
test('control: state sync (ADR-3408 §8.3 CLOSED list — body wins) still overwrites unparseable frontmatter, unchanged', () => {
|
||
// The one command that MUST keep clobbering it — a regression here would mean the fix
|
||
// widened the closed list, which the review explicitly forbids.
|
||
const tmpDir = createTempProject();
|
||
try {
|
||
const statePath = writeConflictFixture(tmpDir);
|
||
const result = runMsdTools(['state', 'sync'], tmpDir);
|
||
assert.ok(result.success, `state sync failed: ${result.error}`);
|
||
const after = fs.readFileSync(statePath, 'utf-8');
|
||
assert.ok(
|
||
!after.includes('<<<<<<< HEAD'),
|
||
'state sync must still re-derive frontmatter from the body (its documented contract) — conflict markers must NOT survive',
|
||
);
|
||
assert.ok(/^---\r?\n/.test(after), 'state sync must still produce a well-formed frontmatter block');
|
||
} finally {
|
||
cleanup(tmpDir);
|
||
}
|
||
});
|
||
});
|
||
|
||
// #3881 ADR-3473 §8.5: `state sync`'s "body wins" regeneration over an unparseable
|
||
// frontmatter block (control test above, A2b) is correct and must not change — but it was
|
||
// SILENT: `synced: true`, exit 0, no signal that the existing block (including any
|
||
// merge-conflict markers) was unreadable and destroyed. §8.5: "a derived conclusion may not
|
||
// be reported as authoritative when the derivation dropped input it could not resolve."
|
||
// Table-driven per the dispatch brief's instruction to check sibling verbs on the same
|
||
// ADR-3408 §8.3 sanctioned-regenerate list: `REGENERATE_STATE` (`/msd-health --repair`) is
|
||
// on that list too, but is DESTRUCTIVE-risk and unconditionally REFUSED by `applyRepairs`'s
|
||
// dispatcher (src/health-diagnostic.cts) before `runRepairAction` is ever invoked — so
|
||
// `state sync` is the only LIVE verb on the sanctioned path today. No table needed; a single
|
||
// verb, driven through the real CLI, is the whole live surface.
|
||
describe('A2c stateSyncWarnsOnUnparseableFrontmatterRegeneration', () => {
|
||
const CONFLICT_FM_BLOCK = [
|
||
'---',
|
||
'<<<<<<< HEAD',
|
||
'status: foo',
|
||
'=======',
|
||
'status: bar',
|
||
'>>>>>>> feature',
|
||
'---',
|
||
'',
|
||
].join('\n');
|
||
const CONFLICT_BODY = [
|
||
'# Project State',
|
||
'',
|
||
'## Current Position',
|
||
'',
|
||
'Phase: 1 — Foundation',
|
||
'Plan: 1 of 1',
|
||
'Status: Executing Phase 1',
|
||
'Last activity: 2026-07-01 — mid-flight',
|
||
'',
|
||
].join('\n');
|
||
|
||
function seedPhaseDirs(tmpDir) {
|
||
const planningDir = path.join(tmpDir, '.planning');
|
||
fs.mkdirSync(path.join(planningDir, 'phases', '01-foundation'), { recursive: true });
|
||
fs.mkdirSync(path.join(planningDir, 'phases', '02-next-phase'), { recursive: true });
|
||
fs.writeFileSync(
|
||
path.join(planningDir, 'ROADMAP.md'),
|
||
['# Roadmap', '', '### Phase 1: Foundation', '**Goal:** Setup', '', '### Phase 2: Next', '**Goal:** More', ''].join('\n'),
|
||
);
|
||
}
|
||
|
||
function writeConflictState(tmpDir) {
|
||
const statePath = path.join(tmpDir, '.planning', 'STATE.md');
|
||
fs.writeFileSync(statePath, CONFLICT_FM_BLOCK + CONFLICT_BODY);
|
||
return statePath;
|
||
}
|
||
|
||
function writeValidState(tmpDir) {
|
||
const statePath = path.join(tmpDir, '.planning', 'STATE.md');
|
||
const validFm = [
|
||
'---',
|
||
'msd_state_version: \'1.0\'',
|
||
'status: executing',
|
||
'current_phase: 1',
|
||
'---',
|
||
'',
|
||
].join('\n');
|
||
fs.writeFileSync(statePath, validFm + CONFLICT_BODY);
|
||
return statePath;
|
||
}
|
||
|
||
test('RED (pre-fix) proof: unparseable frontmatter — stderr carries the msd: warning line and the JSON result surfaces it in `changes`', () => {
|
||
const tmpDir = createTempProject();
|
||
try {
|
||
seedPhaseDirs(tmpDir);
|
||
const statePath = writeConflictState(tmpDir);
|
||
const r = runNode([TOOLS_PATH, 'state', 'sync', '--raw'], { cwd: tmpDir, timeoutMs: PROBE_TIMEOUT_MS });
|
||
throwIfFailed(r, 'msd-tools state sync --raw');
|
||
|
||
// Regeneration still happened (unchanged contract — the control test above pins this
|
||
// for the general case; re-asserted here on the same fixture this warning covers).
|
||
const after = fs.readFileSync(statePath, 'utf-8');
|
||
assert.ok(!after.includes('<<<<<<< HEAD'), 'state sync must still regenerate over the unparseable block');
|
||
|
||
// Human channel: matches the existing `msd: warning — ... (#NNNN)` precedent (#3573).
|
||
assert.match(
|
||
r.stderr,
|
||
/msd: warning — .*frontmatter.*could not be parsed.*regenerated.*\(#3881\)/s,
|
||
`expected a msd: warning on stderr naming the unparseable frontmatter; got stderr:\n${r.stderr}\nstdout:\n${r.stdout}`,
|
||
);
|
||
|
||
// Machine channel: the JSON result's existing `changes` array (the mechanism this
|
||
// codebase already uses to surface sync-time signals — see the "Progress: skipped —
|
||
// ..." entries in src/state.cts) must carry the same disclosure.
|
||
const parsed = JSON.parse(r.stdout);
|
||
assert.ok(Array.isArray(parsed.changes), `expected a changes array in JSON result; got ${r.stdout}`);
|
||
assert.ok(
|
||
parsed.changes.some((c) => typeof c === 'string' && c.includes('could not be parsed') && c.includes('#3881')),
|
||
`expected 'changes' to include the unparseable-frontmatter warning; got ${JSON.stringify(parsed.changes)}`,
|
||
);
|
||
assert.strictEqual(parsed.synced, true, 'exit-0/synced:true stays correct — sync did what its contract says');
|
||
} finally {
|
||
cleanup(tmpDir);
|
||
}
|
||
});
|
||
|
||
test('control (cannot pass vacuously): valid, parseable frontmatter emits NO such warning', () => {
|
||
const tmpDir = createTempProject();
|
||
try {
|
||
seedPhaseDirs(tmpDir);
|
||
const statePath = writeValidState(tmpDir);
|
||
const r = runNode([TOOLS_PATH, 'state', 'sync', '--raw'], { cwd: tmpDir, timeoutMs: PROBE_TIMEOUT_MS });
|
||
throwIfFailed(r, 'msd-tools state sync --raw');
|
||
|
||
assert.doesNotMatch(
|
||
r.stderr,
|
||
/#3881/,
|
||
`valid frontmatter must not trigger the unparseable-frontmatter warning; got stderr:\n${r.stderr}`,
|
||
);
|
||
const parsed = JSON.parse(r.stdout);
|
||
assert.ok(
|
||
!parsed.changes.some((c) => typeof c === 'string' && c.includes('#3881')),
|
||
`expected no #3881 warning in changes for valid frontmatter; got ${JSON.stringify(parsed.changes)}`,
|
||
);
|
||
void statePath;
|
||
} finally {
|
||
cleanup(tmpDir);
|
||
}
|
||
});
|
||
});
|
||
|
||
describe('A3 unparseableIsDistinguishableFromEmpty', () => {
|
||
test('both an empty and an unparseable block yield zero keys, but only the unparseable one carries the marker', () => {
|
||
const empty = extractFrontmatter('---\n---\n\nbody\n');
|
||
const unparseable = extractFrontmatter('---\nfoo: [unclosed\n---\n\nbody\n');
|
||
|
||
assert.equal(Object.keys(empty).length, 0);
|
||
assert.equal(Object.keys(unparseable).length, 0);
|
||
|
||
assert.notEqual(
|
||
empty[FRONTMATTER_UNPARSEABLE],
|
||
true,
|
||
'a genuinely empty frontmatter block must not carry the unparseable marker'
|
||
);
|
||
assert.equal(
|
||
unparseable[FRONTMATTER_UNPARSEABLE],
|
||
true,
|
||
'a malformed frontmatter block must carry the unparseable marker'
|
||
);
|
||
});
|
||
});
|
||
|
||
describe('A4 nonScalarValuesCanonicalize', () => {
|
||
test('the four spellings of an object-list scalar canonicalize to one value', () => {
|
||
const spellings = [
|
||
'- test: "a b"',
|
||
'- test: a b',
|
||
"- test: 'a b'",
|
||
'- {test: a b}',
|
||
];
|
||
const CANONICAL = ['test: a b'];
|
||
|
||
for (const spelling of spellings) {
|
||
const doc = `---\nkey:\n${spelling}\n---\n\nbody\n`;
|
||
const parsed = extractFrontmatter(doc);
|
||
assert.deepEqual(
|
||
parsed.key,
|
||
CANONICAL,
|
||
`spelling ${JSON.stringify(spelling)} must canonicalize to ${JSON.stringify(CANONICAL)}; got ${JSON.stringify(parsed.key)}`
|
||
);
|
||
}
|
||
});
|
||
});
|
||
|
||
describe('A5 truncationProbeStillFiresOnAnOpenFence', () => {
|
||
test('fires on the dominant real truncation shape: opening fence, well-formed keys, then nothing', () => {
|
||
_resetUnusableInputWarningsForTests();
|
||
const truncated = '---\nphase: 3\nplan: 2\n';
|
||
extractFrontmatter(truncated);
|
||
assert.equal(
|
||
_unusableInputEmissionCountForTests(),
|
||
1,
|
||
'the #1882 probe must fire on a well-formed-but-unterminated frontmatter region'
|
||
);
|
||
});
|
||
|
||
test('does NOT fire on the documented false-positive shape: a rule followed by ordinary prose', () => {
|
||
_resetUnusableInputWarningsForTests();
|
||
const rule = '---\nNote: this is a paragraph.\n\nJust ordinary prose after a thematic break.\n';
|
||
extractFrontmatter(rule);
|
||
assert.equal(
|
||
_unusableInputEmissionCountForTests(),
|
||
0,
|
||
'a document that merely opens with a thematic break above prose must not be flagged as truncated'
|
||
);
|
||
});
|
||
|
||
// Post-#3881-review, finding 5: the trivially-parseable dominant shape above was the ONLY
|
||
// shape this row exercised — vacuous for the risk it names, since it never touched
|
||
// `countKeysBeforeTruncation`'s failure/recovery path at all (that whole-region text is
|
||
// valid YAML; the probe fires purely from a successful parse). Table-driven over every real
|
||
// truncation shape confirmed regressed by execution during review: an unquoted colon inside
|
||
// a value, an open (unterminated) flow collection, a mis-indented sibling key, and an
|
||
// anchor/alias whose refusal throws a mark-less exception. Each must still fire the #1882
|
||
// diagnostic exactly once.
|
||
const REGRESSED_TRUNCATION_SHAPES = [
|
||
['unquoted colon in a value', '---\nphase: 3\ntitle: a: b\n'],
|
||
['open (unterminated) flow collection', '---\nphase: 3\nlist: [a, b\n'],
|
||
['mis-indented sibling key', '---\nphase: 3\n plan: 2\n'],
|
||
['anchor/alias — refusal throws a mark-less exception', '---\nphase: 3\nfoo: &a bar\n'],
|
||
];
|
||
|
||
for (const [label, doc] of REGRESSED_TRUNCATION_SHAPES) {
|
||
test(`fires on a real truncation shape the mark-based recovery regressed on: ${label}`, () => {
|
||
_resetUnusableInputWarningsForTests();
|
||
extractFrontmatter(doc);
|
||
assert.equal(
|
||
_unusableInputEmissionCountForTests(),
|
||
1,
|
||
`the #1882 probe must fire on an unterminated region shaped like: ${label}; doc=${JSON.stringify(doc)}`
|
||
);
|
||
});
|
||
}
|
||
});
|
||
|
||
describe('A6 commentsStayOnTheirOwnKey', () => {
|
||
test('a column-0 comment above a Unicode key attaches to that key and survives a round-trip', () => {
|
||
const doc = '---\nfoo: bar\n# note\n相: baz\n---\n\nbody\n';
|
||
|
||
const parsed = extractFrontmatter(doc);
|
||
assert.deepEqual(Object.keys(parsed), ['foo', '相']);
|
||
assert.equal(parsed['相'], 'baz');
|
||
|
||
const reconstructed = reconstructFrontmatter(parsed);
|
||
const commentLine = reconstructed.split('\n').find((l) => l.startsWith('#'));
|
||
const keyLine = reconstructed.split('\n').find((l) => l.startsWith('相:'));
|
||
assert.ok(commentLine, `reconstructed frontmatter must carry the comment; got ${JSON.stringify(reconstructed)}`);
|
||
const commentIdx = reconstructed.split('\n').indexOf(commentLine);
|
||
const keyIdx = reconstructed.split('\n').indexOf(keyLine);
|
||
assert.equal(keyIdx, commentIdx + 1, 'the comment must sit immediately above the 相 key, not the following one');
|
||
|
||
// Round-trip: reparsing the reconstructed block and reconstructing again is byte-identical.
|
||
const rewritten = `---\n${reconstructed}\n---\n\nbody\n`;
|
||
const reparsed = extractFrontmatter(rewritten);
|
||
assert.equal(reconstructFrontmatter(reparsed), reconstructed);
|
||
});
|
||
});
|
||
|
||
describe('A7 anchorsAndAliasesAreRefused', () => {
|
||
// #3881 review, finding 1: the original refusal was a raw-line regex matching only the
|
||
// bare-key spelling (`key: &x`). A quoted key, a flow mapping and a flow sequence all
|
||
// define/use the SAME anchor mechanics while never matching that line shape — table-driven
|
||
// over every spelling that was confirmed bypassable, plus the original passing case, so a
|
||
// future regression in any one spelling fails loudly rather than hiding behind the others.
|
||
const SPELLINGS = [
|
||
['plain', '---\nfoo: &a bar\nbaz: *a\n---\n\nbody\n'],
|
||
['quoted key', '---\n"foo": &a bar\n"baz": *a\n---\n\nbody\n'],
|
||
['flow mapping', '---\na: {b: &a 1, c: *a}\n---\n\nbody\n'],
|
||
['flow sequence', '---\na: [&a "q", *a]\n---\n\nbody\n'],
|
||
['merge key (<<:) with an alias', '---\nbase: &b\n x: "1"\nfoo:\n <<: *b\n y: "2"\n---\n\nbody\n'],
|
||
];
|
||
|
||
for (const [label, doc] of SPELLINGS) {
|
||
test(`${label}: refused rather than expanded`, () => {
|
||
const parsed = extractFrontmatter(doc);
|
||
assert.equal(Object.keys(parsed).length, 0, `${label} must parse to zero keys`);
|
||
assert.equal(parsed[FRONTMATTER_UNPARSEABLE], true, `${label} must carry the unparseable marker`);
|
||
});
|
||
}
|
||
|
||
test('a bare merge key with NO alias is not itself refused (no anchor, no expansion risk)', () => {
|
||
// Under FAILSAFE_SCHEMA (no !!merge type resolution) this never actually merges — it
|
||
// parses as an ordinary, non-expanding literal "<<" string key. Documented behavior
|
||
// change from the pre-review regex (which refused every `<<:`-shaped line regardless of
|
||
// whether an alias was present) — see frontmatter.cts refuseAnchorsAndAliases docblock.
|
||
const doc = '---\na:\n <<: {b: 1}\n c: 2\n---\n\nbody\n';
|
||
const parsed = extractFrontmatter(doc);
|
||
assert.notEqual(parsed[FRONTMATTER_UNPARSEABLE], true);
|
||
assert.deepEqual(parsed.a, { '<<': { b: '1' }, c: '2' });
|
||
});
|
||
});
|
||
|
||
describe('A8 aliasExpansionCannotExhaustMemory', () => {
|
||
test('a billion-laughs frontmatter is refused, bounded on the RESULT, never on elapsed time', () => {
|
||
const bomb = [
|
||
'a: &a ["lol","lol","lol","lol","lol","lol","lol","lol","lol"]',
|
||
'b: &b [*a,*a,*a,*a,*a,*a,*a,*a,*a]',
|
||
'c: &c [*b,*b,*b,*b,*b,*b,*b,*b,*b]',
|
||
'd: &d [*c,*c,*c,*c,*c,*c,*c,*c,*c]',
|
||
'e: &e [*d,*d,*d,*d,*d,*d,*d,*d,*d]',
|
||
'f: &f [*e,*e,*e,*e,*e,*e,*e,*e,*e]',
|
||
'g: [*f,*f,*f,*f,*f,*f,*f,*f,*f]',
|
||
].join('\n');
|
||
const doc = `---\n${bomb}\n---\n\nbody\n`;
|
||
|
||
const parsed = extractFrontmatter(doc);
|
||
|
||
// Assertions are on the RESULT SHAPE (zero keys, bounded serialized size), never on
|
||
// wall-clock elapsed time — this repo forbids elapsed-time assertions in tests.
|
||
assert.equal(Object.keys(parsed).length, 0);
|
||
assert.equal(parsed[FRONTMATTER_UNPARSEABLE], true);
|
||
const serializedSize = Buffer.byteLength(JSON.stringify(parsed), 'utf8');
|
||
assert.ok(
|
||
serializedSize < 1024,
|
||
`a refused parse must stay tiny (would be ~22.8MB if expanded); got ${serializedSize} bytes`
|
||
);
|
||
});
|
||
|
||
test('the same billion-laughs bomb, quoted-key-spelled, is ALSO refused (#3881 review, finding 1)', () => {
|
||
// The exact bypass the review found: the pre-fix raw-text regex matched only bare
|
||
// (unquoted) keys, so this 303-byte quoted-key spelling of the identical bomb went
|
||
// straight through unrefused and expanded to ~35.8MB. Pinned here on the RESULT shape.
|
||
const bomb = [
|
||
'"a": &a ["lol","lol","lol","lol","lol","lol","lol","lol","lol"]',
|
||
'"b": &b [*a,*a,*a,*a,*a,*a,*a,*a,*a]',
|
||
'"c": &c [*b,*b,*b,*b,*b,*b,*b,*b,*b]',
|
||
'"d": &d [*c,*c,*c,*c,*c,*c,*c,*c,*c]',
|
||
'"e": &e [*d,*d,*d,*d,*d,*d,*d,*d,*d]',
|
||
'"f": &f [*e,*e,*e,*e,*e,*e,*e,*e,*e]',
|
||
'"g": [*f,*f,*f,*f,*f,*f,*f,*f,*f]',
|
||
].join('\n');
|
||
const doc = `---\n${bomb}\n---\n\nbody\n`;
|
||
|
||
const parsed = extractFrontmatter(doc);
|
||
|
||
assert.equal(Object.keys(parsed).length, 0);
|
||
assert.equal(parsed[FRONTMATTER_UNPARSEABLE], true);
|
||
const serializedSize = Buffer.byteLength(JSON.stringify(parsed), 'utf8');
|
||
assert.ok(
|
||
serializedSize < 1024,
|
||
`a refused parse must stay tiny (would be ~35.8MB if expanded); got ${serializedSize} bytes`
|
||
);
|
||
});
|
||
});
|
||
|
||
// A9: fix #3881/#3881-followup-2 (regression pinned by tests/smart-entry.unit.test.cjs:867,
|
||
// tests/smart-entry.property.test.cjs). `repairAmbiguousColonValues`'s only real dependent is
|
||
// hand-edited STATE.md content that never lives in this repo's own tracked `*.md` files — a
|
||
// tracked-document sweep will always show zero dependents for this function, which is exactly
|
||
// the wrong signal to delete it on (see `loadWithAmbiguousColonRepair`'s docblock in
|
||
// src/frontmatter.cts). This row pins the dependency at the frontmatter layer itself, so the
|
||
// next document sweep sees it here too, not only three modules away in smart-entry.
|
||
describe('A9 ambiguousColonRepairSurvivesHandEditedStateMd (#2571/#2570)', () => {
|
||
test('a colon-separated date+description value parses to the full string after the first colon', () => {
|
||
const doc = '---\nlast_activity: 2026-06-08: reviewed the PR queue\n---\n\nbody\n';
|
||
|
||
const parsed = extractFrontmatter(doc);
|
||
|
||
assert.equal(
|
||
parsed.last_activity,
|
||
'2026-06-08: reviewed the PR queue',
|
||
'the ambiguous colon must be repaired rather than the whole region going unparseable'
|
||
);
|
||
});
|
||
});
|
||
|
||
describe('finding 3: null-byte sentinel round-trip is injective', () => {
|
||
const E000 = String.fromCharCode(0xE000);
|
||
|
||
test('a real NUL is preserved exactly when no pre-existing U+E000 is present', () => {
|
||
const doc = '---\nfoo: "has |