* fix(#3457): split deferred items on leaf headings, not bullets * fix(#3457): backfill changeset pr with 3488 --------- Co-authored-by: sim <sim@local>
This commit is contained in:
@@ -1974,6 +1974,205 @@ describe('#2766 parseDeferredItems: GFM table shape', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// ─── #3457: heading-delimited deferred entries ────────────────────────────────
|
||||
|
||||
describe('#3457 parseDeferredItems: heading-delimited entries', () => {
|
||||
const items = (md) => parseDeferredItems(md);
|
||||
const names = (md) => items(md).map(i => i.name);
|
||||
|
||||
test('issue minimal repro: heading + sibling field bullets = ONE item', () => {
|
||||
const got = items([
|
||||
'# Deferred Items',
|
||||
'',
|
||||
'## Deferred Items',
|
||||
'',
|
||||
'### Widget layout suite — 3 failing assertions',
|
||||
'',
|
||||
'- **What:** three assertions fail on widget alignment.',
|
||||
'- **Cause:** a pre-existing uncommitted edit in the working tree.',
|
||||
'- **Scope:** out of this plan\'s scope.',
|
||||
'- **Disposition:** NOT fixed here; left for a follow-up plan.',
|
||||
].join('\n'));
|
||||
|
||||
assert.strictEqual(got.length, 1, JSON.stringify(got.map(i => i.name)));
|
||||
assert.match(got[0].name, /Widget layout suite — 3 failing assertions/);
|
||||
assert.match(got[0].name, /three assertions fail/);
|
||||
assert.strictEqual(got[0].result, 'unresolved');
|
||||
assert.strictEqual(got[0].category, 'deferred');
|
||||
});
|
||||
|
||||
test('flat shape: `#` title + `##` entries — title is not an item', () => {
|
||||
const got = names([
|
||||
'# Deferred Items',
|
||||
'',
|
||||
'## DEF-01 renderer fix',
|
||||
'',
|
||||
'- **What:** a.',
|
||||
'',
|
||||
'## DEF-02 seed drift',
|
||||
'',
|
||||
'- **What:** b.',
|
||||
].join('\n'));
|
||||
|
||||
assert.strictEqual(got.length, 2, JSON.stringify(got));
|
||||
assert.match(got[0], /^DEF-01 renderer fix/);
|
||||
assert.match(got[1], /^DEF-02 seed drift/);
|
||||
});
|
||||
|
||||
test('container shape: `##` group label + `###` entries — group is not an item, entries not collapsed', () => {
|
||||
// The shape both shallow-boundary rules get wrong: "count all headings"
|
||||
// counts the group; "shallowest level" collapses both entries into one.
|
||||
const got = names([
|
||||
'# Deferred Items',
|
||||
'',
|
||||
'## Plan 28-02 provenance',
|
||||
'',
|
||||
'### Entry A — flaky seed',
|
||||
'',
|
||||
'- **What:** a.',
|
||||
'',
|
||||
'### Entry B — slow build',
|
||||
'',
|
||||
'- **What:** b.',
|
||||
].join('\n'));
|
||||
|
||||
assert.strictEqual(got.length, 2, JSON.stringify(got));
|
||||
assert.match(got[0], /^Entry A — flaky seed/);
|
||||
assert.match(got[1], /^Entry B — slow build/);
|
||||
// A following entry's heading must not be swallowed into the previous
|
||||
// entry's name (the pre-fix bullet-split folded it in).
|
||||
assert.ok(!got[0].includes('Entry B'), got[0]);
|
||||
});
|
||||
|
||||
test('mixed shape: loose preamble bullets before a later heading group stay one-per-bullet', () => {
|
||||
const got = names([
|
||||
'# Deferred Items',
|
||||
'',
|
||||
'- loose preamble item one',
|
||||
'- loose preamble item two',
|
||||
'',
|
||||
'## Group under here',
|
||||
'',
|
||||
'### Entry C',
|
||||
'- **What:** c.',
|
||||
].join('\n'));
|
||||
|
||||
assert.deepStrictEqual(
|
||||
got.map(n => n.replace(/\s+- \*\*What:\*\*.*$/, '')),
|
||||
['loose preamble item one', 'loose preamble item two', 'Entry C'],
|
||||
JSON.stringify(got),
|
||||
);
|
||||
});
|
||||
|
||||
test('mixed depths: childless `##` entry alongside a `##` group with `###` children — all counted', () => {
|
||||
// The case "deepest heading level present" rules miss: the childless ##
|
||||
// is shallower than the deepest level in the file but is still an entry.
|
||||
const got = names([
|
||||
'# Deferred Items',
|
||||
'',
|
||||
'## Group with children',
|
||||
'',
|
||||
'### Entry A',
|
||||
'- **What:** a.',
|
||||
'',
|
||||
'### Entry B',
|
||||
'- **What:** b.',
|
||||
'',
|
||||
'## Standalone entry',
|
||||
'',
|
||||
'- **What:** standalone.',
|
||||
].join('\n'));
|
||||
|
||||
assert.strictEqual(got.length, 3, JSON.stringify(got));
|
||||
assert.ok(got.some(n => /^Standalone entry/.test(n)), JSON.stringify(got));
|
||||
});
|
||||
|
||||
test('no headings at all → one-bullet-per-item, unchanged names (no regression)', () => {
|
||||
assert.deepStrictEqual(
|
||||
names('## Deferred Items\n\n- entry one\n- entry two\n'),
|
||||
['entry one', 'entry two'],
|
||||
);
|
||||
});
|
||||
|
||||
test('bolded `- **Status:** resolved` under a leaf heading resolves the entry', () => {
|
||||
const got = names([
|
||||
'## Deferred Items',
|
||||
'',
|
||||
'### Item resolved inline',
|
||||
'',
|
||||
'- **What:** x.',
|
||||
'- **Status:** resolved',
|
||||
].join('\n'));
|
||||
|
||||
assert.deepStrictEqual(got, [], JSON.stringify(got));
|
||||
});
|
||||
|
||||
test('bolded `- **Status:** resolved` with no headings: resolves itself, never surfaces as its own item', () => {
|
||||
// The issue's negative control: previously count = 2 with a literal
|
||||
// `**Status:** resolved` pseudo-entry; must match the bare form's count = 1.
|
||||
const got = names('## Deferred Items\n\n- **What:** one deferred item.\n- **Status:** resolved\n');
|
||||
|
||||
assert.strictEqual(got.length, 1, JSON.stringify(got));
|
||||
assert.match(got[0], /\*\*What:\*\* one deferred item\./);
|
||||
assert.ok(!got.some(n => /Status/.test(n)), JSON.stringify(got));
|
||||
});
|
||||
|
||||
test('bare `status: resolved` controls keep working (no regression on #2287)', () => {
|
||||
// Headless continuation form.
|
||||
assert.strictEqual(names('## Deferred Items\n\n- a\n status: resolved\n- b\n').length, 1);
|
||||
// Bare status as a sibling bullet under a leaf heading.
|
||||
assert.strictEqual(names([
|
||||
'## Deferred Items',
|
||||
'',
|
||||
'### Item resolved bare',
|
||||
'',
|
||||
'- **What:** x.',
|
||||
' status: resolved',
|
||||
].join('\n')).length, 0);
|
||||
});
|
||||
|
||||
test('leaf heading over a table-only body → table rows only, no double-count', () => {
|
||||
// parseDeferredTableItems owns the rows; the heading must not add an item.
|
||||
const got = names([
|
||||
'## Discovered during 01-03',
|
||||
'',
|
||||
'| Test | Failing seeds |',
|
||||
'|------|---------------|',
|
||||
'| test_a | 0, 1 |',
|
||||
].join('\n'));
|
||||
|
||||
assert.deepStrictEqual(got, ['test_a — 0, 1'], JSON.stringify(got));
|
||||
});
|
||||
|
||||
test('prose-only or bare headings contribute no items', () => {
|
||||
// "Prose is not an item" is this parser's pre-existing contract (#2766
|
||||
// `# Notes` case) — heading mode must not start counting prose sections.
|
||||
assert.deepStrictEqual(names('## Deferred Items\n\n### Musings\n\njust prose here.\n'), []);
|
||||
assert.deepStrictEqual(names('## Deferred Items\n\n### A bare heading with no body\n'), []);
|
||||
});
|
||||
|
||||
test('CRLF files: heading entries still split and resolve', () => {
|
||||
const got = names('## Deferred Items\r\n\r\n### Entry\r\n\r\n- **What:** x.\r\n- **Status:** resolved\r\n');
|
||||
|
||||
assert.deepStrictEqual(got, [], JSON.stringify(got));
|
||||
});
|
||||
|
||||
test('mid-line `status: resolved` decoy under a heading must not resolve the entry', () => {
|
||||
// The #2287 decoy invariant, ported to the heading shape: a status-shaped
|
||||
// phrase inside entry prose is never a field.
|
||||
const got = items([
|
||||
'## Deferred Items',
|
||||
'',
|
||||
'### Entry with decoy prose',
|
||||
'',
|
||||
'- note: saw a status: resolved message in the log',
|
||||
].join('\n'));
|
||||
|
||||
assert.strictEqual(got.length, 1, JSON.stringify(got.map(i => i.name)));
|
||||
assert.strictEqual(got[0].result, 'unresolved');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Bug 3: table-shaped ## Gaps section ──────────────────────────────────────
|
||||
|
||||
describe('#2766 parseGapsItems: GFM table shape', () => {
|
||||
|
||||
Reference in New Issue
Block a user