Final convergence review found the shared <tag> seam introduced 3 behavior regressions; fixed all + locked with tests: - #557 REGRESSION: stripTaggedBlocks's attribute-tolerance stripped `<details open>` (the ACTIVE-milestone marker) that the old `<details>`-only regex preserved. The seam now takes `allowAttributes` (default false) — details/decisions strip is attr-INTOLERANT (preserves `<details open>`); only `<task type="…">` opts in. Regression test added to roadmap-parser + markdown-sectionizer suites. - verify.cts actionZones (negative-grep-echo security scan): reverted to a bounded to-first-close scan `<action>([\s\S]{0,20000}?)</action>` so a grep-echo trick can't hide behind an unterminated inner <action> (the seam's stop-at-next-open would drop it). ReDoS-safe via the cap. - check-command-router HTML-comment strip: `(?:-->|$)` fallback wiped to EOF (fail-closed spurious gate block) — replaced with stop-at-next-open so an unclosed `<!--` leaves downstream tags intact. - Updated the extractTaggedBlocks nested-tag tests to the new (stop-at-next-open) behavior: `<x><x>inner</x></x>` -> ['inner']. All vectors still linear; every fix verified in-process. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -33,6 +33,7 @@ const {
|
||||
collectSection,
|
||||
iterateBullets,
|
||||
extractTaggedBlocks,
|
||||
stripTaggedBlocks,
|
||||
replaceSection,
|
||||
} = require('../gsd-core/bin/lib/markdown-sectionizer.cjs');
|
||||
|
||||
@@ -1015,15 +1016,14 @@ describe('stripFencedCode and tokenizeHeadings: backtick info string with backti
|
||||
|
||||
// ─── FIX 6: extractTaggedBlocks — nested tag behavior ─────────────────────────
|
||||
|
||||
describe('extractTaggedBlocks: nested same-name tag behavior (non-greedy limitation)', () => {
|
||||
test('nested <x><x>…</x></x> closes at first </x> (non-greedy; nested tags not supported)', () => {
|
||||
// Non-greedy match: <x>([\s\S]*?)</x> closes at the FIRST </x>.
|
||||
// So <x><x>inner</x></x> → first block captures "<x>inner", second </x> is unmatched.
|
||||
describe('extractTaggedBlocks: nested same-name tag behavior (#2128 stop-at-next-open)', () => {
|
||||
test('nested <x><x>inner</x></x> extracts the well-formed inner block', () => {
|
||||
// #2128: the ReDoS-safe body scan terminates at the NEXT opening <x>, so the
|
||||
// unterminated outer <x> is skipped and the inner block is extracted.
|
||||
const content = '<x><x>inner</x></x>';
|
||||
const result = extractTaggedBlocks(content, 'x');
|
||||
// The first match closes at the first </x>, capturing "<x>inner"
|
||||
assert.equal(result.length, 1, 'non-greedy match produces exactly one result from nested input');
|
||||
assert.equal(result[0], '<x>inner', 'inner capture is the content up to the first closing tag');
|
||||
assert.equal(result.length, 1, 'exactly one result from nested input');
|
||||
assert.equal(result[0], 'inner', 'the well-formed inner block is extracted; the unterminated outer is skipped');
|
||||
});
|
||||
|
||||
test('back-to-back blocks (not nested) are both extracted', () => {
|
||||
@@ -1033,6 +1033,24 @@ describe('extractTaggedBlocks: nested same-name tag behavior (non-greedy limitat
|
||||
assert.equal(result[0], 'first');
|
||||
assert.equal(result[1], 'second');
|
||||
});
|
||||
|
||||
test('#2128: a document full of unclosed <x> openings stays linear and yields no match', () => {
|
||||
const content = '<x>a\n'.repeat(50) + 'no closing tag';
|
||||
assert.deepEqual(extractTaggedBlocks(content, 'x'), [], 'no </x> anywhere -> no blocks');
|
||||
});
|
||||
|
||||
test('#557 / #2128: attr-intolerant by default preserves <details open>; opt-in matches <task type=…>', () => {
|
||||
// stripTaggedBlocks(details) must PRESERVE <details open> (the active-milestone
|
||||
// marker) and strip only bare <details>; extractTaggedBlocks(task, true) must
|
||||
// match attributed tasks, and must NOT when allowAttributes is left false.
|
||||
assert.equal(
|
||||
stripTaggedBlocks('X<details>shipped</details>Y<details open>active</details>Z', 'details'),
|
||||
'XY<details open>active</details>Z',
|
||||
'#557: <details open> preserved; bare <details> stripped',
|
||||
);
|
||||
assert.deepEqual(extractTaggedBlocks('<task type="auto">body</task>', 'task', true), ['body'], 'attributed task matched with allowAttributes=true');
|
||||
assert.deepEqual(extractTaggedBlocks('<task type="auto">body</task>', 'task'), [], 'attributed task NOT matched with allowAttributes=false');
|
||||
});
|
||||
});
|
||||
|
||||
// Parity guard removed in T5 (ADR-1372): uat-predicate now imports stripFencedCode
|
||||
|
||||
Reference in New Issue
Block a user