fix(#3699): report why a derived frontmatter key was not written, and repair a missing body source (#3846)
* test(#3699): failing-first coverage for derived-key reporting and the case-D fallback * fix(#3699): report why a derived frontmatter key was not written, and repair a missing body source * fix(#3699): scope session-field writes to ## Session so an archived line cannot absorb the update * fix(#3699): resolve the session writer from body labels only, so a frontmatter key never writes the body * chore(#3699): backfill changeset PR number --------- Co-authored-by: sim <sim@local>
This commit is contained in:
@@ -1278,16 +1278,53 @@ describe('ADR-3408 §8.3(b) Matrix D: patchCore strips frontmatter first (#3469)
|
||||
assert.strictEqual(result.content, input);
|
||||
});
|
||||
|
||||
// D7 (independence, extend): updateCore is unchanged — it already strips
|
||||
// frontmatter first, the correct shape patchCore now matches. A
|
||||
// frontmatter-shaped `field` still cannot reach the YAML block through it.
|
||||
test('D7: updateCore is unchanged — a frontmatter-shaped field still cannot reach the YAML block', () => {
|
||||
const input = ['---', 'current_phase: "3"', '---', '', '# State', '', '**Status:** Planning', ''].join('\n');
|
||||
// D7 (independence, extend): updateCore strips frontmatter first — the correct
|
||||
// shape patchCore now matches — so a frontmatter-shaped `field` cannot reach
|
||||
// the YAML block by text replacement.
|
||||
//
|
||||
// NARROWED BY #3699, deliberately. The original assertion was "a
|
||||
// frontmatter-shaped field can NEVER reach the YAML block", which was a true
|
||||
// characterisation of updateCore when this test was written as an independence
|
||||
// guard for #3469 — but it is broader than the rule ADR-3408 actually states.
|
||||
// §8.3(b)'s invariant is "no transition core calls `stateReplaceField` on
|
||||
// unstripped content" (ADR-3408 line 318), and #3699's repair path honours it:
|
||||
// it strips frontmatter, edits the PARSED object, and re-serialises via
|
||||
// `reconstructFrontmatter` — it never runs the body-field text replacer over
|
||||
// YAML, which is the dangerous shape the rule exists to forbid.
|
||||
//
|
||||
// So the invariant is re-pinned here at the ADR's actual boundary, in both
|
||||
// directions, rather than deleted.
|
||||
test('D7: a frontmatter-shaped field cannot reach the YAML block while a body source exists', () => {
|
||||
// The body carries `Current Phase`, so the body IS the writable route and
|
||||
// the frontmatter key must be refused exactly as before.
|
||||
const input = [
|
||||
'---', 'current_phase: "3"', '---', '',
|
||||
'# State', '', '**Current Phase:** 3', '**Status:** Planning', '',
|
||||
].join('\n');
|
||||
const result = transitionCore(input, { kind: 'update', field: 'current_phase', value: '9' }, deps);
|
||||
assert.strictEqual(result.content, input);
|
||||
assert.strictEqual(result.content, input, 'no write may occur through the frontmatter key');
|
||||
assert.strictEqual(result.data && result.data.updated, false);
|
||||
assert.ok(/^current_phase: "3"$/m.test(result.content));
|
||||
});
|
||||
|
||||
test('D7b: the #3699 repair path is the ONLY way frontmatter is written, and it never text-replaces over YAML', () => {
|
||||
// Body source absent — the case-D repair shape. The write is permitted here,
|
||||
// and `updated` is the field name rather than `false`.
|
||||
const input = ['---', 'current_phase: "3"', '---', '', '# State', '', '**Status:** Planning', ''].join('\n');
|
||||
const result = transitionCore(input, { kind: 'update', field: 'current_phase', value: '9' }, deps);
|
||||
|
||||
assert.strictEqual(result.data && result.data.updated, true);
|
||||
assert.strictEqual(result.data && result.data.wroteFrontmatter, true, 'the repair path must announce itself');
|
||||
assert.ok(/^current_phase: 9$/m.test(result.content), 'the frontmatter key carries the new value');
|
||||
|
||||
// ADR-3408 §8.3(b) still holds: the body is untouched and the frontmatter
|
||||
// block was REBUILT from the parsed object, not text-patched in place. A
|
||||
// `stateReplaceField` pass over unstripped content would have left the rest
|
||||
// of the document's frontmatter formatting alone; re-serialisation is what
|
||||
// proves the parsed-object route was taken.
|
||||
assert.ok(result.content.includes('**Status:** Planning'), 'the body must be untouched');
|
||||
assert.ok(!/current_phase: "9"/.test(result.content), 'the value went through the YAML serialiser, not a text splice');
|
||||
});
|
||||
});
|
||||
|
||||
// ADR-1769 Phase 7: update, prune, sync
|
||||
|
||||
@@ -29,6 +29,8 @@ const fc = require('fast-check');
|
||||
const stateLib = require('../gsd-core/bin/lib/state.cjs');
|
||||
const stateTransitionMod = require('../gsd-core/bin/lib/state-transition.cjs');
|
||||
const stateDocument = require('../gsd-core/bin/lib/state-document.cjs');
|
||||
// #3699: the repo's one metacharacter-escape helper (local/no-adhoc-regex-escape).
|
||||
const { escapeRegex } = require('../gsd-core/bin/lib/pattern.cjs');
|
||||
const frontmatterLib = require('../gsd-core/bin/lib/frontmatter.cjs');
|
||||
const { SCOPE } = require('../gsd-core/bin/lib/planning-scope.cjs');
|
||||
const workstreamInventory = require('../gsd-core/bin/lib/workstream-inventory.cjs');
|
||||
@@ -16490,3 +16492,406 @@ describe('#3468 B8: a drifted / malformed / unparseable STATE.md never reaches t
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// #3699 — `state update` told the truth about failure.
|
||||
//
|
||||
// A frontmatter key like `stopped_at` is a PROJECTION of a body field, and the
|
||||
// body is the source of truth. Asking to update the key used to return
|
||||
// `Field "stopped_at" not found in STATE.md` — byte-identical to what a
|
||||
// genuinely absent field returns, and pointing away from the route that works.
|
||||
//
|
||||
// Case D is the one real capability gap: frontmatter carries the key, the body
|
||||
// has no source line, and neither route can write. `updateCore` now falls back
|
||||
// to writing the frontmatter key directly there (and only there).
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
describe('#3699 state update — derived frontmatter keys explain themselves', () => {
|
||||
let tmpDir;
|
||||
|
||||
beforeEach(() => {
|
||||
tmpDir = createFixture();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup(tmpDir);
|
||||
});
|
||||
|
||||
const FM = [
|
||||
'---',
|
||||
'gsd_state_version: 1.0',
|
||||
'current_phase: 1',
|
||||
'current_phase_name: alpha',
|
||||
'status: executing',
|
||||
'stopped_at: "original value"',
|
||||
'---',
|
||||
'',
|
||||
];
|
||||
const BODY = ['# Project State', '', '## Current Position', '', 'Phase: 1 (alpha)', 'Status: Executing', ''];
|
||||
const SESSION = ['## Session Continuity', '', 'Stopped at: original value', ''];
|
||||
|
||||
function writeState(lines, opts = {}) {
|
||||
const eol = opts.crlf ? '\r\n' : '\n';
|
||||
fs.writeFileSync(path.join(tmpDir, '.planning', 'STATE.md'), lines.join(eol));
|
||||
}
|
||||
function update(field, value) {
|
||||
const result = runGsdTools(['state', 'update', field, value], tmpDir);
|
||||
return { result, output: JSON.parse(result.output) };
|
||||
}
|
||||
function stateText() {
|
||||
return fs.readFileSync(path.join(tmpDir, '.planning', 'STATE.md'), 'utf-8');
|
||||
}
|
||||
function frontmatterStoppedAt() {
|
||||
const m = stateText().match(/^stopped_at:.*$/m);
|
||||
return m ? m[0] : null;
|
||||
}
|
||||
|
||||
// ── the headline defect: present-but-derived vs genuinely absent ───────────
|
||||
|
||||
test('a body-derived frontmatter key is reported as derived, and names its body source', () => {
|
||||
writeState([...FM, ...BODY, ...SESSION]);
|
||||
|
||||
const { output } = update('stopped_at', 'NEW VALUE');
|
||||
assert.strictEqual(output.updated, false);
|
||||
assert.match(output.reason, /not directly writable/i);
|
||||
assert.match(output.reason, /Stopped At/i, 'the reason must name the body source that DOES work');
|
||||
assert.doesNotMatch(output.reason, /not found in STATE\.md/i, 'the key is present — reporting absence is the bug');
|
||||
assert.match(frontmatterStoppedAt(), /original value/, 'a refused update must not write');
|
||||
});
|
||||
|
||||
test('a genuinely absent field still reports absence', () => {
|
||||
// The control that keeps the fix honest: if EVERY failure now says
|
||||
// "derived", the defect has been inverted, not closed.
|
||||
writeState([...FM, ...BODY, ...SESSION]);
|
||||
|
||||
const { output } = update('definitely_not_a_field', 'NEW VALUE');
|
||||
assert.strictEqual(output.updated, false);
|
||||
assert.strictEqual(output.reason, 'Field "definitely_not_a_field" not found in STATE.md');
|
||||
});
|
||||
|
||||
test('a present-but-derived key and a genuinely absent field no longer produce the same message', () => {
|
||||
// #3699 stated as a test: the two were byte-identical apart from the name.
|
||||
writeState([...FM, ...BODY, ...SESSION]);
|
||||
const derived = update('stopped_at', 'NEW VALUE').output.reason;
|
||||
|
||||
writeState([...FM, ...BODY, ...SESSION]);
|
||||
const absent = update('definitely_not_a_field', 'NEW VALUE').output.reason;
|
||||
|
||||
assert.notStrictEqual(
|
||||
derived.replace(/"stopped_at"/g, 'X'),
|
||||
absent.replace(/"definitely_not_a_field"/g, 'X'),
|
||||
'the two failures must be distinguishable by more than the field name',
|
||||
);
|
||||
});
|
||||
|
||||
test('the body source route still works and still syncs to frontmatter', () => {
|
||||
writeState([...FM, ...BODY, ...SESSION]);
|
||||
|
||||
const { output } = update('Stopped at', 'NEW VALUE');
|
||||
assert.strictEqual(output.updated, true);
|
||||
assert.match(frontmatterStoppedAt(), /NEW VALUE/);
|
||||
});
|
||||
|
||||
// ── case D: the capability gap ────────────────────────────────────────────
|
||||
|
||||
test('case D: with no body source, the frontmatter key becomes directly writable', () => {
|
||||
// Frontmatter carries stopped_at; the body has no `Stopped at:` line and no
|
||||
// `## Session` section. Before this, BOTH routes failed and the stale value
|
||||
// survived — the document was unrepairable through `state update`.
|
||||
writeState([...FM, ...BODY]);
|
||||
|
||||
const { output } = update('stopped_at', 'NEW VALUE');
|
||||
assert.strictEqual(output.updated, true);
|
||||
assert.strictEqual(output.wrote, 'frontmatter');
|
||||
assert.match(
|
||||
frontmatterStoppedAt(),
|
||||
/NEW VALUE/,
|
||||
'the value must survive syncStateFrontmatter + applyStatePreservation, not just be written by the transition',
|
||||
);
|
||||
});
|
||||
|
||||
test('case D: the fallback is idempotent across repeated writes', () => {
|
||||
writeState([...FM, ...BODY]);
|
||||
|
||||
update('stopped_at', 'FIRST');
|
||||
assert.match(frontmatterStoppedAt(), /FIRST/);
|
||||
const { output } = update('stopped_at', 'SECOND');
|
||||
assert.strictEqual(output.updated, true);
|
||||
assert.match(frontmatterStoppedAt(), /SECOND/);
|
||||
});
|
||||
|
||||
test('case D: preserved does not claim a restore that authoritativeFm overrode', () => {
|
||||
// Preservation DOES restore stopped_at's snapshot here (its body source is
|
||||
// unchanged — absent), and authoritativeFm then overrides it. Listing the
|
||||
// field in `preserved` would report a restore that did not survive: the
|
||||
// same unfalsifiable-success shape this issue is about, one field over.
|
||||
writeState([...FM, ...BODY]);
|
||||
|
||||
const { output } = update('stopped_at', 'NEW VALUE');
|
||||
const claimed = (output.preserved || []).map((p) => String(p).toLowerCase());
|
||||
assert.ok(
|
||||
!claimed.includes('stopped at') && !claimed.includes('stopped_at'),
|
||||
`preserved must not claim this field; got: ${JSON.stringify(output.preserved)}`,
|
||||
);
|
||||
});
|
||||
|
||||
test('case D via the body field name names the frontmatter key that still holds a value', () => {
|
||||
writeState([...FM, ...BODY]);
|
||||
|
||||
const { output } = update('Stopped at', 'NEW VALUE');
|
||||
assert.strictEqual(output.updated, false);
|
||||
assert.match(output.reason, /stopped_at/, 'the reason must name the frontmatter key carrying the value');
|
||||
});
|
||||
|
||||
// ── negative space: where the fallback must NOT fire ──────────────────────
|
||||
|
||||
test('the fallback does not fire when frontmatter does not carry the key', () => {
|
||||
// Nothing to repair — inventing a key here would be fabricating state.
|
||||
writeState([...FM.filter((l) => !l.startsWith('stopped_at:')), ...BODY]);
|
||||
|
||||
const { output } = update('stopped_at', 'NEW VALUE');
|
||||
assert.strictEqual(output.updated, false);
|
||||
assert.strictEqual(frontmatterStoppedAt(), null, 'no frontmatter key may be invented');
|
||||
});
|
||||
|
||||
test('a stale body-source line OUTSIDE ## Session is not treated as the source', () => {
|
||||
// Reversed from this change's first cut, on evidence. That cut suppressed the
|
||||
// repair whenever ANY body line existed, reasoning "prefer a line the user can
|
||||
// edit". But `buildStateFrontmatter` harvests Stopped At from `## Session`
|
||||
// ONLY, so an archive line is not a source — suppressing on it left the
|
||||
// document unrepairable AND pointed the user at a command that rewrote the
|
||||
// wrong line. Read scope, write scope and probe scope now all agree.
|
||||
writeState([
|
||||
...FM, ...BODY,
|
||||
'## Session', '', 'Notes: none', '',
|
||||
'## Session Continuity Archive', '', 'Stopped At: 2025-01-01 (old session)', '',
|
||||
]);
|
||||
|
||||
const { output } = update('stopped_at', '2026-08-24');
|
||||
assert.strictEqual(output.updated, true, 'an archive line must not block the repair');
|
||||
assert.strictEqual(output.wrote, 'frontmatter');
|
||||
assert.match(
|
||||
stateText(),
|
||||
/Stopped At: 2025-01-01 \(old session\)/,
|
||||
'the archived line is a historical record and must be left alone',
|
||||
);
|
||||
});
|
||||
|
||||
test('updating a session field never rewrites a line outside ## Session', () => {
|
||||
// The defect this guards: `stateReplaceField` matches the FIRST occurrence
|
||||
// anywhere in the body, so with no `Stopped At:` in `## Session` and a stale
|
||||
// one in the archive, the update reported success while silently rewriting
|
||||
// the archived record and leaving the real field untouched. #3374 established
|
||||
// the scoped writer for exactly this; `updateCore` had not adopted it.
|
||||
writeState([
|
||||
...FM, ...BODY,
|
||||
'## Session', '', 'Notes: none', '',
|
||||
'## Session Continuity Archive', '', 'Stopped At: 2025-01-01 (old session)', '',
|
||||
]);
|
||||
|
||||
const { output } = update('Stopped At', '2026-08-24');
|
||||
assert.strictEqual(output.updated, false, 'there is no Stopped At line in ## Session to write');
|
||||
assert.match(
|
||||
stateText(),
|
||||
/Stopped At: 2025-01-01 \(old session\)/,
|
||||
'the archived line must be byte-identical after a refused update',
|
||||
);
|
||||
assert.doesNotMatch(stateText(), /Stopped At: 2026-08-24/, 'nothing may have been written anywhere');
|
||||
});
|
||||
|
||||
test('a session field inside ## Session is still writable and still syncs', () => {
|
||||
// The complement: scoping must not break the normal route.
|
||||
writeState([...FM, ...BODY, '## Session', '', 'Stopped at: original value', '']);
|
||||
|
||||
const { output } = update('Stopped at', 'NEW VALUE');
|
||||
assert.strictEqual(output.updated, true);
|
||||
assert.match(stateText(), /^Stopped at: NEW VALUE$/m, 'the session line is the one that moved');
|
||||
assert.match(frontmatterStoppedAt(), /NEW VALUE/, 'and it synced to frontmatter');
|
||||
});
|
||||
|
||||
test('case D behaves identically on a CRLF document', () => {
|
||||
writeState([...FM, ...BODY], { crlf: true });
|
||||
|
||||
const { output } = update('stopped_at', 'NEW VALUE');
|
||||
assert.strictEqual(output.updated, true);
|
||||
assert.match(frontmatterStoppedAt(), /NEW VALUE/);
|
||||
});
|
||||
|
||||
// ── keys with no body source must not be given one ───────────────────────
|
||||
|
||||
test('keys derived from the clock, ROADMAP.md, or a disk scan say so instead of naming a body field', () => {
|
||||
const cases = [
|
||||
['last_updated', /recomputed on every write/i],
|
||||
['state_head', /recomputed on every write/i],
|
||||
['gsd_state_version', /recomputed on every write/i],
|
||||
['milestone', /ROADMAP\.md/i],
|
||||
['milestone_name', /ROADMAP\.md/i],
|
||||
['progress.percent', /scan of \.planning\/phases/i],
|
||||
['progress.total_plans', /scan of \.planning\/phases/i],
|
||||
];
|
||||
for (const [field, expected] of cases) {
|
||||
writeState([...FM, ...BODY, ...SESSION]);
|
||||
const { output } = update(field, 'X');
|
||||
assert.strictEqual(output.updated, false, `${field} must not be writable`);
|
||||
assert.match(output.reason, expected, `${field}: wrong derivation named`);
|
||||
assert.doesNotMatch(output.reason, /Update its body source/i, `${field} has no body source to name`);
|
||||
}
|
||||
});
|
||||
|
||||
// ── the map cannot silently drift from the builder ───────────────────────
|
||||
|
||||
test('every FRONTMATTER_BODY_SOURCE entry actually round-trips from its body field', () => {
|
||||
// Real parity, per key. An earlier cut asserted only SET MEMBERSHIP against
|
||||
// the emitted frontmatter — near-vacuous, because buildStateFrontmatter emits
|
||||
// the whole schema key set regardless of body derivation, so a wrong mapping
|
||||
// would still pass.
|
||||
//
|
||||
// This drives each mapped key's own BODY LABEL to a distinct value and
|
||||
// asserts that value arrives in that frontmatter key. A mapping naming the
|
||||
// wrong body field cannot survive it.
|
||||
//
|
||||
// Two fixtures, because `paused_at` is not independent: normalizeStateStatus
|
||||
// forces `status: paused` whenever Paused At is set, so a single fixture
|
||||
// could not assert both `status` and `paused_at`.
|
||||
const expected = {
|
||||
current_phase: '7',
|
||||
current_phase_name: 'sentinel-name',
|
||||
current_plan: '3',
|
||||
status: 'executing', // normalized from the update below
|
||||
stopped_at: 'sentinel-stopped',
|
||||
last_activity: '2026-08-19',
|
||||
last_activity_desc: 'sentinel-desc',
|
||||
};
|
||||
|
||||
writeState([
|
||||
'---', 'gsd_state_version: 1.0', '---', '',
|
||||
'# Project State', '',
|
||||
'## Current Position', '',
|
||||
'Current Phase: 7',
|
||||
'Current Phase Name: sentinel-name',
|
||||
'Current Plan: 3',
|
||||
'Status: Planning', // deliberately != the update below, or the #948 no-op guard skips the sync
|
||||
'Last Activity: 2026-08-19',
|
||||
'Last Activity Description: sentinel-desc',
|
||||
'',
|
||||
'## Session', '',
|
||||
'Stopped at: sentinel-stopped',
|
||||
'',
|
||||
]);
|
||||
update('Status', 'Executing');
|
||||
|
||||
let fm = stateText().split('---')[1];
|
||||
assert.match(fm, /^last_updated:/m, 'precondition: the update must have actually synced frontmatter');
|
||||
|
||||
for (const [key, want] of Object.entries(expected)) {
|
||||
const hit = new RegExp(`^${key}:\\s*(.+)$`, 'm').exec(fm);
|
||||
assert.ok(hit, `${key} was not emitted from its mapped body field — the mapping is wrong`);
|
||||
assert.match(
|
||||
hit[1],
|
||||
new RegExp(escapeRegex(want)),
|
||||
`${key} did not carry the value written to its mapped body field`,
|
||||
);
|
||||
}
|
||||
|
||||
// paused_at, in its own fixture for the reason above.
|
||||
writeState([
|
||||
'---', 'gsd_state_version: 1.0', '---', '',
|
||||
'# Project State', '',
|
||||
'## Current Position', '',
|
||||
'Current Phase: 7',
|
||||
'Status: Planning',
|
||||
'',
|
||||
'## Session', '',
|
||||
'Paused At: sentinel-paused',
|
||||
'',
|
||||
]);
|
||||
update('Status', 'Executing');
|
||||
|
||||
fm = stateText().split('---')[1];
|
||||
const paused = /^paused_at:\s*(.+)$/m.exec(fm);
|
||||
assert.ok(paused, 'paused_at was not emitted from its mapped body field');
|
||||
assert.match(paused[1], /sentinel-paused/);
|
||||
|
||||
// And the fixtures above must have covered the whole map — otherwise a key
|
||||
// added to FRONTMATTER_BODY_SOURCE could go untested here forever.
|
||||
const covered = new Set([...Object.keys(expected), 'paused_at']);
|
||||
for (const key of Object.keys(stateTransitionMod.FRONTMATTER_BODY_SOURCE)) {
|
||||
assert.ok(covered.has(key), `FRONTMATTER_BODY_SOURCE maps "${key}" but this round-trip test does not exercise it`);
|
||||
}
|
||||
});
|
||||
|
||||
test('no body-derived frontmatter key escapes FRONTMATTER_BODY_SOURCE', () => {
|
||||
// The reverse direction. Every key buildStateFrontmatter emits must be either
|
||||
// mapped, or a declared non-body-derived key. A NEW body-derived key added to
|
||||
// the builder without a map entry fails here.
|
||||
//
|
||||
// Known limit, stated rather than hidden: someone could add a key to the
|
||||
// exclusion set below instead of the map. That is a smaller and far more
|
||||
// visible edit than silently forgetting the map, which is what this guards.
|
||||
const NOT_BODY_DERIVED = new Set([
|
||||
'gsd_state_version', // schema constant
|
||||
'last_updated', 'state_head', // recomputed every write
|
||||
'milestone', 'milestone_name', // ROADMAP.md
|
||||
'progress', // disk scan
|
||||
]);
|
||||
|
||||
writeState([
|
||||
'---', 'gsd_state_version: 1.0', '---', '',
|
||||
'# Project State', '',
|
||||
'## Current Position', '',
|
||||
'Current Phase: 2', 'Current Phase Name: beta', 'Current Plan: 1',
|
||||
'Status: Planning',
|
||||
'Last Activity: 2026-08-19 — did a thing',
|
||||
'',
|
||||
'## Session', '', 'Stopped at: somewhere', 'Paused At: elsewhere', '',
|
||||
]);
|
||||
update('Status', 'Executing');
|
||||
|
||||
const fm = stateText().split('---')[1];
|
||||
assert.match(fm, /^last_updated:/m, 'precondition: the write must have synced frontmatter');
|
||||
|
||||
const emitted = fm.split('\n')
|
||||
.filter((l) => /^[a-z_]+:/.test(l))
|
||||
.map((l) => l.split(':')[0].trim());
|
||||
const mapped = new Set(Object.keys(stateTransitionMod.FRONTMATTER_BODY_SOURCE));
|
||||
|
||||
for (const key of emitted) {
|
||||
assert.ok(
|
||||
mapped.has(key) || NOT_BODY_DERIVED.has(key),
|
||||
`buildStateFrontmatter emits "${key}", which is neither mapped in FRONTMATTER_BODY_SOURCE nor declared non-body-derived — `
|
||||
+ 'if it is body-derived, `state update` cannot name its body source',
|
||||
);
|
||||
}
|
||||
// And the map may not carry a key the builder never emits.
|
||||
for (const key of mapped) {
|
||||
assert.ok(emitted.includes(key), `FRONTMATTER_BODY_SOURCE maps "${key}", which the builder did not emit — the map has drifted`);
|
||||
}
|
||||
});
|
||||
|
||||
test('property: every body label round-trips back to its frontmatter key', () => {
|
||||
const entries = Object.entries(stateTransitionMod.FRONTMATTER_BODY_SOURCE);
|
||||
fc.assert(
|
||||
fc.property(fc.integer({ min: 0, max: entries.length - 1 }), fc.boolean(), (i, upper) => {
|
||||
const [key, labels] = entries[i];
|
||||
for (const label of labels) {
|
||||
const probe = upper ? label.toUpperCase() : label.toLowerCase();
|
||||
assert.strictEqual(
|
||||
stateTransitionMod.frontmatterKeyForBodyField(probe),
|
||||
key,
|
||||
`"${probe}" must resolve back to "${key}"`,
|
||||
);
|
||||
}
|
||||
}),
|
||||
{ numRuns: 25 },
|
||||
);
|
||||
});
|
||||
|
||||
test('inherited prototype members are not treated as fields', () => {
|
||||
// Both lookups are own-property only; a prototype member must not produce a
|
||||
// bogus "is a derived key" reason.
|
||||
for (const probe of ['toString', 'constructor', 'valueOf', '__proto__', 'hasOwnProperty']) {
|
||||
assert.strictEqual(stateTransitionMod.getFrontmatterBodySource(probe), null, `${probe} is not a frontmatter key`);
|
||||
assert.strictEqual(stateTransitionMod.frontmatterKeyForBodyField(probe), null, `${probe} is not a body field`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user