@@ -246,17 +246,20 @@ describe('parseSpecialTags with <question>', () => {
246246 } )
247247
248248 it ( 'does not rescan the interior of a body that carried no markers' , ( ) => {
249- // Pins WHY the two literal reasons resume at different offsets. A
250- // never-a-payload body resumes past the CLOSE; resuming past the opener
251- // instead would rescan the interior, and since the marker scan runs on the
252- // blanked body, a tag quoted inside a JSON string is invisible to it and
253- // would be re-parsed as a real tag on the second pass — then dropped,
254- // deleting the very text this parser exists to preserve.
249+ // Pins WHY a settled span resumes past the CLOSE, never past the opener.
250+ // Resuming past the opener would rescan the interior, and since the marker
251+ // scan runs on the blanked body, a tag quoted inside a JSON string is
252+ // invisible to it and would be re-parsed as a REAL tag on the second pass —
253+ // painting the quoted JSON verbatim as raw text (its escaped quotes cannot
254+ // re-parse as a card), the exact failure `discard` exists to prevent. The
255+ // span itself opens `{"` and will not parse (trailing junk), so it is an
256+ // attempted payload and is discarded whole; what must never happen is a
257+ // partial re-parse of its quoted interior.
255258 const raw =
256259 'A <question>{"a":"<options>{\\"k\\":{\\"title\\":\\"x\\",\\"description\\":\\"y\\"}}</options>"} junk</question> B'
257260 const { segments } = parseSpecialTags ( raw , false )
258261 expect ( segments . every ( ( segment ) => segment . type === 'text' ) ) . toBe ( true )
259- expect ( renderedText ( segments ) ) . toBe ( raw )
262+ expect ( renderedText ( segments ) ) . toBe ( 'A B' )
260263 } )
261264
262265 it ( 'keeps prose a tag wrapped instead of a payload' , ( ) => {
@@ -320,6 +323,88 @@ describe('parseSpecialTags with <question>', () => {
320323 expect ( segments . every ( ( segment ) => segment . type === 'text' ) ) . toBe ( true )
321324 } )
322325
326+ it ( 'drops a payload one typo away from valid instead of showing raw JSON' , ( ) => {
327+ // The first three are verbatim from production screenshots (2026-07-31): an
328+ // extra ` }` before the array close, a missing opening quote on a key, and a
329+ // stray `] }` after the map closes; the fourth adds a trailing comma. Each
330+ // fails JSON.parse, so the old was-it-ever-JSON test called them prose and
331+ // rendered the whole payload verbatim — the markdown layer then swallowed
332+ // the tag markers and the reader saw a wall of raw JSON. They open `{"` or
333+ // `[{` and carry key-value colons, which marks them as attempted payloads:
334+ // droppable, like any other broken emission.
335+ const cases = [
336+ 'Prose before. <question>{"type": "single_select", "prompt": "How should I proceed?", "options": [{"id": "a", "label": "Confirm the id"}}]}</question>' ,
337+ 'Prose before. <question>{"type":"multi_select","prompt":"What should I build now?",options": [{"id":"lib","label":"Pattern library"}]}</question>' ,
338+ 'Prose before. <options>{"1": {"title": "Define the criteria", "description": "Populate"}}]}</options>' ,
339+ 'Prose before. <options>[{"title":"Ship it","description":"Open the PR"},]</options>' ,
340+ ]
341+ for ( const raw of cases ) {
342+ const { segments, hasPendingTag } = parseSpecialTags ( raw , false )
343+ expect ( hasPendingTag , raw ) . toBe ( false )
344+ expect ( renderedText ( segments ) , raw ) . toBe ( 'Prose before. ' )
345+ expect (
346+ segments . every ( ( segment ) => segment . type === 'text' ) ,
347+ raw
348+ ) . toBe ( true )
349+ }
350+ } )
351+
352+ it ( 'drops a broken inline payload rather than dumping it mid-sentence' , ( ) => {
353+ // Same treatment for the inline tag: a `{"`-opening body with a syntax
354+ // error reads as an attempted chip, and the sentence survives around the
355+ // hole exactly as it does for a wrong-shape payload today.
356+ const raw =
357+ 'I saved <workspace_resource>{"type":"file",path:"a.md"}</workspace_resource> for you.'
358+ expect ( renderedText ( parseSpecialTags ( raw , false ) . segments ) ) . toBe ( 'I saved for you.' )
359+ } )
360+
361+ it ( 'renders nothing for a message that is only an unparsable payload' , ( ) => {
362+ // The discardedTag guard must cover the new class too: with every segment
363+ // discarded, the raw-content fallback would otherwise resurrect the exact
364+ // JSON the discard removed.
365+ const { segments } = parseSpecialTags (
366+ '<options>{"1": {"title": "a", "description": "b"}}]}</options>' ,
367+ false
368+ )
369+ expect ( segments ) . toHaveLength ( 0 )
370+ } )
371+
372+ it ( 'still shows an unparsable body that never opened like a payload' , ( ) => {
373+ // The other side of the attempted-payload line: a bare scalar opens with
374+ // its own first character, not `{"`/`[{`, so it reads as prose in quotes
375+ // and must render — same as the brace-wrapped prose cases above.
376+ const raw = 'see <options>"just a phrase"</options> end'
377+ expect ( renderedText ( parseSpecialTags ( raw , false ) . segments ) ) . toBe ( raw )
378+ } )
379+
380+ it ( 'renders brace-wrapped quoted prose — an opener alone is not an attempt' , ( ) => {
381+ // The attempted-payload call takes BOTH kinds of evidence: the `{"` opener
382+ // and a key-value colon outside string literals. `{"the Q4 report"}` has
383+ // the opener but no colon — prose in costume, so it renders; a colon
384+ // inside the quotes changes nothing. The array twin parses as JSON, so it
385+ // was dropped as `wrong-shape` before this heuristic existed and still is —
386+ // that verdict comes from a real parse, not from the opener.
387+ const braceWrapped = 'see <options>{"the Q4 report"}</options> end'
388+ expect ( renderedText ( parseSpecialTags ( braceWrapped , false ) . segments ) ) . toBe ( braceWrapped )
389+ const quotedColon = 'see <options>{"ratio: 4:5"}</options> end'
390+ expect ( renderedText ( parseSpecialTags ( quotedColon , false ) . segments ) ) . toBe ( quotedColon )
391+ const arrayWrapped = 'see <options>["some list item"]</options> end'
392+ expect ( renderedText ( parseSpecialTags ( arrayWrapped , false ) . segments ) ) . toBe ( 'see end' )
393+ } )
394+
395+ it ( 'discards a broken payload whose strings legitimately mention tag syntax' , ( ) => {
396+ // The prompt quotes a tag name, so a raw scan sees a marker — but the
397+ // body's quotes are balanced, so the blanked scan already proved the marker
398+ // sits inside a string. Treating it as a nested tag would render the broken
399+ // payload as raw JSON, the exact failure `discard` exists to prevent. The
400+ // raw rescan is reserved for mispaired quotes, where blanked offsets lie.
401+ const raw =
402+ 'Prose before. <question>{"type": "single_select", "prompt": "Use the <options> tag", "options": [{"id": "a", "label": "x"}}]}</question>'
403+ const { segments } = parseSpecialTags ( raw , false )
404+ expect ( renderedText ( segments ) ) . toBe ( 'Prose before. ' )
405+ expect ( segments . every ( ( segment ) => segment . type === 'text' ) ) . toBe ( true )
406+ } )
407+
323408 it ( 'does not flash the payload while the closing tag is still arriving' , ( ) => {
324409 // Each frame below is a real mid-stream state: the JSON value has closed, so
325410 // without tolerating an arriving close the trailing `</opt` reads as stray
@@ -334,6 +419,23 @@ describe('parseSpecialTags with <question>', () => {
334419 }
335420 } )
336421
422+ it ( 'never flashes a broken payload at any streamed frame' , ( ) => {
423+ // A body that goes non-viable mid-stream (the stray `] }` lands before the
424+ // close does) used to release as literal text at that frame, then vanish
425+ // when the close arrived and classified it not-parsable — raw JSON painted
426+ // on screen only for the close to retract it. Suppression must hold at
427+ // EVERY frame from the completed opener on, and the settled parse must
428+ // agree with what the frames showed.
429+ const raw =
430+ 'Prose before. <options>{"1": {"title": "Define the criteria", "description": "Populate"}}]}</options> after.'
431+ const bodyStart = raw . indexOf ( '<options>' ) + '<options>' . length
432+ for ( let end = bodyStart ; end <= raw . length ; end ++ ) {
433+ const { segments } = parseSpecialTags ( raw . slice ( 0 , end ) , true )
434+ expect ( renderedText ( segments ) , `frame ${ end } ` ) . not . toContain ( '{' )
435+ }
436+ expect ( renderedText ( parseSpecialTags ( raw , false ) . segments ) ) . toBe ( 'Prose before. after.' )
437+ } )
438+
337439 it ( 'still rejects a close whose name is wrong rather than merely unfinished' , ( ) => {
338440 // The counterpart to the case above: `</workflow_resource>` can never grow
339441 // into `</workspace_resource>`, so it settles immediately instead of hiding
@@ -363,7 +465,7 @@ describe('parseSpecialTags with <question>', () => {
363465 it ( 'finds a nested tag an unbalanced quote hid from the blanked scan' , ( ) => {
364466 // One stray `"` is enough to make blankJsonStringLiterals treat the REST of
365467 // the body as a string literal, hiding the real `<options>` marker from the
366- // scan. The verdict then degrades from `foreign-markers` to `never-a-payload `
468+ // scan. The verdict then degrades from `foreign-markers` to `not-viable-json `
367469 // and resumes past the close, flattening both nested tags into one literal
368470 // span — so a card already on screen un-renders when the close arrives.
369471 //
@@ -901,8 +1003,13 @@ describe('parser properties', () => {
9011003 /**
9021004 * Fragments that must survive verbatim. Every one is a shape the parser has to
9031005 * reject: prose mentions, malformed closes, bodies that never were payloads.
904- * None is a valid tag and none is a well-formed payload, so nothing here is
905- * eligible for `discard` — which makes "output equals input" a legal assertion.
1006+ * Nothing here is eligible for `discard` — which makes "output equals input" a
1007+ * legal assertion. That takes two properties, not one: no fragment is a
1008+ * well-formed payload (`wrong-shape`), and the `{"`-opening bodies never land
1009+ * in a marker-free matched pair (`not-parsable`) — their own close is
1010+ * misspelled, truncated, or absent, so any close they borrow from a later
1011+ * fragment drags that fragment's own opener into the body, and the
1012+ * nested-marker rule settles the span before the attempted-payload test runs.
9061013 */
9071014 const LOSSLESS_FRAGMENTS = [
9081015 'Plain prose with no markup at all. ' ,
0 commit comments