From a56a928b0ccaee75c1549255d555324b21305ec3 Mon Sep 17 00:00:00 2001 From: iris <2+iris@noreply.localhost> Date: Sun, 6 Sep 2026 18:28:57 -0400 Subject: [PATCH] client-core: the transcript's own markdown shapes, and the streaming property as a property split_blocks was tested on the shapes it was written against. These are the ones a real reply contains -- a fence with blank lines in it, a `---` inside a fence, a nested list, a fence directly under a heading, a table, a quote -- plus the property RowBlocks::apply_delta actually depends on, checked at every character boundary of a message that has all of them: growing a message may rewrite its last block and never an earlier one, or common_prefix must say so. No defect found; the split already held. Co-Authored-By: Claude Fable 5.1 --- client-core/src/markdown_blocks.rs | 91 ++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/client-core/src/markdown_blocks.rs b/client-core/src/markdown_blocks.rs index 9eb4a93..9df6596 100644 --- a/client-core/src/markdown_blocks.rs +++ b/client-core/src/markdown_blocks.rs @@ -231,4 +231,95 @@ mod tests { vec![BlockKind::Paragraph, BlockKind::Other, BlockKind::Paragraph] ); } + + /// The shapes a real transcript actually contains, each checked for + /// the one property the streaming fast path needs: the *number* of + /// blocks and every earlier block's source stay put while the message + /// grows. A fence's own blank lines, a `---` inside one, a nested + /// list and a table are all places where a naive line-based split + /// would break the message into more pieces than there are blocks. + #[test] + fn the_transcripts_own_block_shapes_survive_a_split() { + let fence_with_blanks = "Intro.\n\n```rust\nfn a() {}\n\nfn b() {}\n```\n\nAfter."; + assert_eq!( + kinds(fence_with_blanks), + vec![BlockKind::Paragraph, BlockKind::Code, BlockKind::Paragraph], + "a blank line inside a fence is not a block boundary" + ); + assert_eq!( + kinds("```\n---\n```"), + vec![BlockKind::Code], + "a thematic break inside a fence is code, not a break" + ); + assert_eq!( + kinds("- a\n - a1\n - a2\n- b"), + vec![BlockKind::List], + "a nested list is one top-level block" + ); + assert_eq!( + kinds("## Heading\n```sh\nls\n```"), + vec![BlockKind::Heading, BlockKind::Code], + "a fence directly under a heading, with no blank line" + ); + assert_eq!( + kinds("| a | b |\n|---|---|\n| 1 | 2 |"), + vec![BlockKind::Table] + ); + assert_eq!( + kinds("> quoted\n> more\n\nplain"), + vec![BlockKind::Quote, BlockKind::Paragraph] + ); + } + + /// `apply_delta`'s precondition, stated as the property rather than + /// the arithmetic: for every prefix of a realistic streamed message, + /// the blocks before the last one must be exactly the blocks the + /// previous prefix had. Where markdown breaks that (the `---` case + /// above), `common_prefix` has to *say* so -- which is what the + /// `>= len - 1` assertion below checks: the split may rewrite the + /// last block, never an earlier one, or `RowBlocks::apply_delta` + /// would keep a widget whose text is no longer what it holds. + #[test] + fn every_prefix_of_a_streamed_message_keeps_all_but_its_last_block() { + let full = "# Report\n\nFirst finding, at some length.\n\n```rust\nfn main() {\n\n println!(\"hi\");\n}\n```\n\n- one\n - nested\n- two\n\n| a | b |\n |---|---|\n| 1 | 2 |\n\n> and a closing quote."; + // Every character boundary, so a delta landing mid-word and one + // landing exactly on a fence's closing backtick are both covered. + let mut prev = Vec::new(); + for end in full.char_indices().map(|(i, _)| i).chain([full.len()]) { + let now = split_blocks(&full[..end]); + let common = common_prefix(&prev, &now); + assert!( + prev.is_empty() || common + 1 >= prev.len(), + "at {end} bytes the split rewrote block {common} of {}, not just the last one:\n before={prev:#?}\nafter={now:#?}", + prev.len() + ); + prev = now; + } + } + + /// The half a growing message cannot show: a fence that never closes. + /// The stream ends there and the block must still be the code block + /// it has been all along, not re-split into paragraphs. + #[test] + fn a_stream_that_ends_inside_a_fence_still_ends_with_one_code_block() { + let src = "Here is the patch:\n\n```diff\n- old line\n+ new line"; + let blocks = split_blocks(src); + assert_eq!( + blocks.iter().map(|b| b.kind).collect::>(), + vec![BlockKind::Paragraph, BlockKind::Code] + ); + assert_eq!(blocks[1].source, "```diff\n- old line\n+ new line"); + } + + /// A delta that closes a fence changes the *last* block only, so the + /// fast path takes it -- the case the module doc says is the reason + /// `common_prefix` is a comparison. + #[test] + fn the_delta_that_closes_a_fence_changes_only_the_last_block() { + let before = split_blocks("Text.\n\n```\ncode\n"); + let after = split_blocks("Text.\n\n```\ncode\n```"); + assert_eq!(before.len(), after.len()); + assert_eq!(common_prefix(&before, &after), 1); + assert_ne!(before[1], after[1]); + } }