notedeck

One damus client to rule them all
git clone git://jb55.com/notedeck
Log | Files | Refs | README | LICENSE

tests.rs (29795B)


      1 //! Tests for streaming parser behavior.
      2 
      3 use crate::element::Span;
      4 use crate::partial::PartialKind;
      5 use crate::{InlineElement, InlineStyle, MdElement, StreamParser};
      6 
      7 /// Helper to resolve a Span against a parser's buffer.
      8 fn r<'a>(span: &Span, buf: &'a str) -> &'a str {
      9     span.resolve(buf)
     10 }
     11 
     12 #[test]
     13 fn test_heading_complete() {
     14     let mut parser = StreamParser::new();
     15     parser.push("# Hello World\n");
     16 
     17     assert_eq!(parser.parsed().len(), 1);
     18     match &parser.parsed()[0] {
     19         MdElement::Heading { level, content } => {
     20             assert_eq!(*level, 1);
     21             assert_eq!(r(content, parser.buffer()), "Hello World");
     22         }
     23         other => panic!("Expected heading, got {:?}", other),
     24     }
     25 }
     26 
     27 #[test]
     28 fn test_heading_streaming() {
     29     let mut parser = StreamParser::new();
     30 
     31     // Stream in chunks
     32     parser.push("# Hel");
     33     assert_eq!(parser.parsed().len(), 0);
     34     assert!(parser.partial().is_some());
     35 
     36     parser.push("lo Wor");
     37     assert_eq!(parser.parsed().len(), 0);
     38 
     39     parser.push("ld\n");
     40     assert_eq!(parser.parsed().len(), 1);
     41     match &parser.parsed()[0] {
     42         MdElement::Heading { level, content } => {
     43             assert_eq!(*level, 1);
     44             assert_eq!(r(content, parser.buffer()), "Hello World");
     45         }
     46         other => panic!("Expected heading, got {:?}", other),
     47     }
     48 }
     49 
     50 #[test]
     51 fn test_code_block_complete() {
     52     let mut parser = StreamParser::new();
     53     parser.push("```rust\nfn main() {}\n```\n");
     54 
     55     assert_eq!(parser.parsed().len(), 1);
     56     match &parser.parsed()[0] {
     57         MdElement::CodeBlock(cb) => {
     58             assert_eq!(cb.language.map(|s| r(&s, parser.buffer())), Some("rust"));
     59             assert_eq!(r(&cb.content, parser.buffer()), "fn main() {}\n");
     60         }
     61         _ => panic!("Expected code block"),
     62     }
     63 }
     64 
     65 #[test]
     66 fn test_code_block_streaming() {
     67     let mut parser = StreamParser::new();
     68 
     69     parser.push("```py");
     70     // No partial yet — language tag may be incomplete without newline
     71     assert!(parser.partial().is_none());
     72 
     73     parser.push("thon\n");
     74     // Now the full opening fence line is available
     75     assert!(parser.in_code_block());
     76 
     77     parser.push("print('hello')\n");
     78     assert!(parser.in_code_block());
     79     assert_eq!(parser.parsed().len(), 0);
     80 
     81     parser.push("```\n");
     82     assert!(!parser.in_code_block());
     83     assert_eq!(parser.parsed().len(), 1);
     84 }
     85 
     86 #[test]
     87 fn test_multiple_elements() {
     88     let mut parser = StreamParser::new();
     89     parser.push("# Title\n\nSome paragraph text.\n\n## Subtitle\n");
     90 
     91     assert!(parser.parsed().len() >= 2);
     92 }
     93 
     94 #[test]
     95 fn test_thematic_break() {
     96     let mut parser = StreamParser::new();
     97     parser.push("---\n");
     98 
     99     assert_eq!(parser.parsed().len(), 1);
    100     assert_eq!(parser.parsed()[0], MdElement::ThematicBreak);
    101 }
    102 
    103 #[test]
    104 fn test_finalize_incomplete_code() {
    105     let mut parser = StreamParser::new();
    106     parser.push("```\nunclosed code");
    107 
    108     assert_eq!(parser.parsed().len(), 0);
    109 
    110     parser.finalize();
    111 
    112     assert_eq!(parser.parsed().len(), 1);
    113     match &parser.parsed()[0] {
    114         MdElement::CodeBlock(cb) => {
    115             assert!(r(&cb.content, parser.buffer()).contains("unclosed code"));
    116         }
    117         _ => panic!("Expected code block"),
    118     }
    119 }
    120 
    121 #[test]
    122 fn test_realistic_llm_stream() {
    123     let mut parser = StreamParser::new();
    124 
    125     // Simulate realistic LLM token chunks
    126     let chunks = [
    127         "Here's",
    128         " a ",
    129         "simple",
    130         " example:\n\n",
    131         "```",
    132         "rust",
    133         "\n",
    134         "fn ",
    135         "main() {\n",
    136         "    println!(\"Hello\");\n",
    137         "}",
    138         "\n```",
    139         "\n\nThat's",
    140         " it!",
    141     ];
    142 
    143     for chunk in chunks {
    144         parser.push(chunk);
    145     }
    146 
    147     parser.finalize();
    148 
    149     // Should have: paragraph, code block, paragraph
    150     assert!(
    151         parser.parsed().len() >= 2,
    152         "Got {} elements",
    153         parser.parsed().len()
    154     );
    155 }
    156 
    157 #[test]
    158 fn test_heading_levels() {
    159     let mut parser = StreamParser::new();
    160     parser.push("# H1\n## H2\n### H3\n");
    161 
    162     let headings: Vec<_> = parser
    163         .parsed()
    164         .iter()
    165         .filter_map(|e| {
    166             if let MdElement::Heading { level, .. } = e {
    167                 Some(*level)
    168             } else {
    169                 None
    170             }
    171         })
    172         .collect();
    173 
    174     assert!(headings.contains(&1));
    175     assert!(headings.contains(&2));
    176     assert!(headings.contains(&3));
    177 }
    178 
    179 #[test]
    180 fn test_empty_push() {
    181     let mut parser = StreamParser::new();
    182     parser.push("");
    183     parser.push("");
    184     parser.push("# Test\n");
    185 
    186     assert_eq!(parser.parsed().len(), 1);
    187 }
    188 
    189 #[test]
    190 fn test_partial_content_visible() {
    191     let mut parser = StreamParser::new();
    192     parser.push("```\nsome code");
    193 
    194     // Should be able to see partial content for speculative rendering
    195     let partial = parser.partial_content();
    196     assert!(partial.is_some());
    197     assert!(partial.unwrap().contains("some code"));
    198 }
    199 
    200 // Inline formatting tests
    201 
    202 #[test]
    203 fn test_inline_bold() {
    204     let mut parser = StreamParser::new();
    205     parser.push("This has **bold** text.\n\n");
    206 
    207     assert_eq!(parser.parsed().len(), 1);
    208     if let MdElement::Paragraph(inlines) = &parser.parsed()[0] {
    209         let buf = parser.buffer();
    210         assert!(
    211             inlines.iter().any(|e| matches!(
    212                 e,
    213                 InlineElement::Styled { style: InlineStyle::Bold, content } if r(content, buf) == "bold"
    214             )),
    215             "Expected bold element, got: {:?}",
    216             inlines
    217         );
    218     } else {
    219         panic!("Expected paragraph");
    220     }
    221 }
    222 
    223 #[test]
    224 fn test_inline_italic() {
    225     let mut parser = StreamParser::new();
    226     parser.push("This has *italic* text.\n\n");
    227 
    228     assert_eq!(parser.parsed().len(), 1);
    229     if let MdElement::Paragraph(inlines) = &parser.parsed()[0] {
    230         let buf = parser.buffer();
    231         assert!(
    232             inlines.iter().any(|e| matches!(
    233                 e,
    234                 InlineElement::Styled { style: InlineStyle::Italic, content } if r(content, buf) == "italic"
    235             )),
    236             "Expected italic element, got: {:?}",
    237             inlines
    238         );
    239     } else {
    240         panic!("Expected paragraph");
    241     }
    242 }
    243 
    244 #[test]
    245 fn test_inline_code() {
    246     let mut parser = StreamParser::new();
    247     parser.push("Use `code` here.\n\n");
    248 
    249     assert_eq!(parser.parsed().len(), 1);
    250     if let MdElement::Paragraph(inlines) = &parser.parsed()[0] {
    251         let buf = parser.buffer();
    252         assert!(
    253             inlines.iter().any(|e| matches!(
    254                 e,
    255                 InlineElement::Code(s) if r(s, buf) == "code"
    256             )),
    257             "Expected code element, got: {:?}",
    258             inlines
    259         );
    260     } else {
    261         panic!("Expected paragraph");
    262     }
    263 }
    264 
    265 #[test]
    266 fn test_inline_link() {
    267     let mut parser = StreamParser::new();
    268     parser.push("Check [this link](https://example.com) out.\n\n");
    269 
    270     assert_eq!(parser.parsed().len(), 1);
    271     if let MdElement::Paragraph(inlines) = &parser.parsed()[0] {
    272         let buf = parser.buffer();
    273         assert!(inlines.iter().any(|e| matches!(
    274             e,
    275             InlineElement::Link { text, url } if r(text, buf) == "this link" && r(url, buf) == "https://example.com"
    276         )), "Expected link element, got: {:?}", inlines);
    277     } else {
    278         panic!("Expected paragraph");
    279     }
    280 }
    281 
    282 #[test]
    283 fn test_inline_image() {
    284     let mut parser = StreamParser::new();
    285     parser.push("See ![alt text](image.png) here.\n\n");
    286 
    287     assert_eq!(parser.parsed().len(), 1);
    288     if let MdElement::Paragraph(inlines) = &parser.parsed()[0] {
    289         let buf = parser.buffer();
    290         assert!(
    291             inlines.iter().any(|e| matches!(
    292                 e,
    293                 InlineElement::Image { alt, url } if r(alt, buf) == "alt text" && r(url, buf) == "image.png"
    294             )),
    295             "Expected image element, got: {:?}",
    296             inlines
    297         );
    298     } else {
    299         panic!("Expected paragraph");
    300     }
    301 }
    302 
    303 #[test]
    304 fn test_inline_strikethrough() {
    305     let mut parser = StreamParser::new();
    306     parser.push("This is ~~deleted~~ text.\n\n");
    307 
    308     assert_eq!(parser.parsed().len(), 1);
    309     if let MdElement::Paragraph(inlines) = &parser.parsed()[0] {
    310         let buf = parser.buffer();
    311         assert!(inlines.iter().any(|e| matches!(
    312             e,
    313             InlineElement::Styled { style: InlineStyle::Strikethrough, content } if r(content, buf) == "deleted"
    314         )), "Expected strikethrough element, got: {:?}", inlines);
    315     } else {
    316         panic!("Expected paragraph");
    317     }
    318 }
    319 
    320 #[test]
    321 fn test_inline_mixed_formatting() {
    322     let mut parser = StreamParser::new();
    323     parser.push("Some **bold**, *italic*, and `code` mixed.\n\n");
    324 
    325     assert_eq!(parser.parsed().len(), 1);
    326     if let MdElement::Paragraph(inlines) = &parser.parsed()[0] {
    327         let has_bold = inlines.iter().any(|e| {
    328             matches!(
    329                 e,
    330                 InlineElement::Styled {
    331                     style: InlineStyle::Bold,
    332                     ..
    333                 }
    334             )
    335         });
    336         let has_italic = inlines.iter().any(|e| {
    337             matches!(
    338                 e,
    339                 InlineElement::Styled {
    340                     style: InlineStyle::Italic,
    341                     ..
    342                 }
    343             )
    344         });
    345         let has_code = inlines.iter().any(|e| matches!(e, InlineElement::Code(_)));
    346 
    347         assert!(has_bold, "Missing bold");
    348         assert!(has_italic, "Missing italic");
    349         assert!(has_code, "Missing code");
    350     } else {
    351         panic!("Expected paragraph");
    352     }
    353 }
    354 
    355 #[test]
    356 fn test_inline_finalize() {
    357     let mut parser = StreamParser::new();
    358     parser.push("Text with **bold** formatting");
    359 
    360     // Not complete yet (no paragraph break)
    361     assert_eq!(parser.parsed().len(), 0);
    362 
    363     parser.finalize();
    364 
    365     // Now should have parsed with inline formatting
    366     assert_eq!(parser.parsed().len(), 1);
    367     if let MdElement::Paragraph(inlines) = &parser.parsed()[0] {
    368         let buf = parser.buffer();
    369         assert!(inlines.iter().any(|e| matches!(
    370             e,
    371             InlineElement::Styled { style: InlineStyle::Bold, content } if r(content, buf) == "bold"
    372         )));
    373     } else {
    374         panic!("Expected paragraph");
    375     }
    376 }
    377 
    378 // Paragraph partial kind tests
    379 
    380 #[test]
    381 fn test_paragraph_partial_kind() {
    382     let mut parser = StreamParser::new();
    383     parser.push("Some text without");
    384 
    385     // Should have a partial with Paragraph kind, not Heading with level 0
    386     let partial = parser.partial().expect("Should have partial");
    387     assert!(
    388         matches!(partial.kind, PartialKind::Paragraph),
    389         "Expected PartialKind::Paragraph, got {:?}",
    390         partial.kind
    391     );
    392 }
    393 
    394 #[test]
    395 fn test_paragraph_streaming_with_newlines() {
    396     let mut parser = StreamParser::new();
    397 
    398     // Push text with single newline - should continue accumulating
    399     parser.push("First line\n");
    400     assert!(parser.partial().is_some());
    401     assert!(matches!(
    402         parser.partial().unwrap().kind,
    403         PartialKind::Paragraph
    404     ));
    405 
    406     parser.push("Second line");
    407     assert_eq!(parser.parsed().len(), 0); // Not complete yet
    408 
    409     // Finalize should emit the accumulated paragraph
    410     parser.finalize();
    411     assert_eq!(parser.parsed().len(), 1);
    412     assert!(matches!(parser.parsed()[0], MdElement::Paragraph(_)));
    413 }
    414 
    415 #[test]
    416 fn test_paragraph_double_newline_boundary() {
    417     let mut parser = StreamParser::new();
    418 
    419     // Test when double newline arrives all at once
    420     parser.push("Complete paragraph\n\n");
    421     assert_eq!(parser.parsed().len(), 1);
    422     assert!(matches!(parser.parsed()[0], MdElement::Paragraph(_)));
    423 }
    424 
    425 #[test]
    426 fn test_paragraph_finalize_emits_content() {
    427     let mut parser = StreamParser::new();
    428     parser.push("Incomplete paragraph without double newline");
    429 
    430     assert_eq!(parser.parsed().len(), 0);
    431     assert!(matches!(
    432         parser.partial().unwrap().kind,
    433         PartialKind::Paragraph
    434     ));
    435 
    436     parser.finalize();
    437 
    438     assert_eq!(parser.parsed().len(), 1);
    439     if let MdElement::Paragraph(inlines) = &parser.parsed()[0] {
    440         let buf = parser.buffer();
    441         assert!(inlines.iter().any(|e| matches!(
    442             e,
    443             InlineElement::Text(s) if r(s, buf).contains("Incomplete paragraph")
    444         )));
    445     } else {
    446         panic!("Expected paragraph");
    447     }
    448 }
    449 
    450 #[test]
    451 fn test_inline_code_with_angle_brackets() {
    452     // Test parse_inline directly
    453     let input = "Generic Rust: `impl Iterator<Item = &str>` returns a `Result<(), anyhow::Error>`";
    454     let result = crate::parse_inline(input, 0);
    455     eprintln!("parse_inline result: {:#?}", result);
    456 
    457     let code_elements: Vec<_> = result
    458         .iter()
    459         .filter(|e| matches!(e, InlineElement::Code(_)))
    460         .collect();
    461     assert_eq!(
    462         code_elements.len(),
    463         2,
    464         "Expected 2 code spans, got: {:#?}",
    465         result
    466     );
    467 }
    468 
    469 #[test]
    470 fn test_streaming_inline_code_with_angle_brackets() {
    471     // Test streaming parser with token-by-token delivery
    472     let mut parser = StreamParser::new();
    473     let input =
    474         "5. Generic Rust: `impl Iterator<Item = &str>` returns a `Result<(), anyhow::Error>`\n\n";
    475 
    476     // Simulate streaming token by token
    477     for ch in input.chars() {
    478         parser.push(&ch.to_string());
    479     }
    480 
    481     eprintln!("Parsed elements: {:#?}", parser.parsed());
    482     eprintln!("Partial: {:#?}", parser.partial());
    483 
    484     // Should have one paragraph with code spans
    485     assert!(!parser.parsed().is_empty(), "Should have parsed elements");
    486 
    487     if let MdElement::Paragraph(inlines) = &parser.parsed()[0] {
    488         let code_elements: Vec<_> = inlines
    489             .iter()
    490             .filter(|e| matches!(e, InlineElement::Code(_)))
    491             .collect();
    492         assert_eq!(
    493             code_elements.len(),
    494             2,
    495             "Expected 2 code spans, got: {:#?}",
    496             inlines
    497         );
    498     } else {
    499         panic!("Expected paragraph, got: {:?}", parser.parsed()[0]);
    500     }
    501 }
    502 
    503 #[test]
    504 fn test_streaming_multiple_code_spans_with_angle_brackets() {
    505     // From the screenshot: multiple code spans with nested angle brackets
    506     let mut parser = StreamParser::new();
    507     let input =
    508         "use `HashMap<K, V>` or `Vec<String>` or `Option<Box<dyn Error>>` in your types\n\n";
    509 
    510     for ch in input.chars() {
    511         parser.push(&ch.to_string());
    512     }
    513 
    514     assert!(!parser.parsed().is_empty(), "Should have parsed elements");
    515 
    516     if let MdElement::Paragraph(inlines) = &parser.parsed()[0] {
    517         let code_elements: Vec<_> = inlines
    518             .iter()
    519             .filter(|e| matches!(e, InlineElement::Code(_)))
    520             .collect();
    521         assert_eq!(
    522             code_elements.len(),
    523             3,
    524             "Expected 3 code spans, got: {:#?}",
    525             inlines
    526         );
    527     } else {
    528         panic!("Expected paragraph, got: {:?}", parser.parsed()[0]);
    529     }
    530 }
    531 
    532 #[test]
    533 fn test_code_block_after_paragraph_single_newline() {
    534     // Reproduces: paragraph text ending with ":\n" then "```\n" code block
    535     let mut parser = StreamParser::new();
    536     let input = "All events share these common tags:\n```\n[\"d\", \"<session-id>\"]\n```\n";
    537     parser.push(input);
    538 
    539     eprintln!("Before finalize - parsed: {:#?}", parser.parsed());
    540     eprintln!("Before finalize - partial: {:#?}", parser.partial());
    541 
    542     parser.finalize();
    543 
    544     eprintln!("After finalize - parsed: {:#?}", parser.parsed());
    545 
    546     // Should have: paragraph + code block
    547     let has_paragraph = parser
    548         .parsed()
    549         .iter()
    550         .any(|e| matches!(e, MdElement::Paragraph(_)));
    551     let has_code_block = parser
    552         .parsed()
    553         .iter()
    554         .any(|e| matches!(e, MdElement::CodeBlock(_)));
    555 
    556     assert!(has_paragraph, "Missing paragraph element");
    557     assert!(has_code_block, "Missing code block element");
    558 }
    559 
    560 #[test]
    561 fn test_code_block_after_paragraph_single_newline_streaming() {
    562     // Same test but streaming char-by-char (how LLM tokens arrive)
    563     let mut parser = StreamParser::new();
    564     let input = "All events share these common tags:\n```\n[\"d\", \"<session-id>\"]\n```\n";
    565 
    566     for ch in input.chars() {
    567         parser.push(&ch.to_string());
    568     }
    569 
    570     eprintln!("Before finalize - parsed: {:#?}", parser.parsed());
    571     eprintln!("Before finalize - partial: {:#?}", parser.partial());
    572     eprintln!(
    573         "Before finalize - in_code_block: {}",
    574         parser.in_code_block()
    575     );
    576 
    577     parser.finalize();
    578 
    579     eprintln!("After finalize - parsed: {:#?}", parser.parsed());
    580 
    581     let has_paragraph = parser
    582         .parsed()
    583         .iter()
    584         .any(|e| matches!(e, MdElement::Paragraph(_)));
    585     let has_code_block = parser
    586         .parsed()
    587         .iter()
    588         .any(|e| matches!(e, MdElement::CodeBlock(_)));
    589 
    590     assert!(has_paragraph, "Missing paragraph element");
    591     assert!(has_code_block, "Missing code block element");
    592 }
    593 
    594 #[test]
    595 fn test_heading_partial_kind_distinct_from_paragraph() {
    596     let mut parser = StreamParser::new();
    597     parser.push("# Heading without newline");
    598 
    599     let partial = parser.partial().expect("Should have partial");
    600     assert!(
    601         matches!(partial.kind, PartialKind::Heading { level: 1 }),
    602         "Expected PartialKind::Heading {{ level: 1 }}, got {:?}",
    603         partial.kind
    604     );
    605 }
    606 
    607 // Table tests
    608 
    609 #[test]
    610 fn test_table_basic_batch() {
    611     let mut parser = StreamParser::new();
    612     parser.push("| Name | Age |\n|------|-----|\n| Alice | 30 |\n| Bob | 25 |\n\n");
    613 
    614     let tables: Vec<_> = parser
    615         .parsed()
    616         .iter()
    617         .filter(|e| matches!(e, MdElement::Table { .. }))
    618         .collect();
    619     assert_eq!(
    620         tables.len(),
    621         1,
    622         "Expected 1 table, got: {:#?}",
    623         parser.parsed()
    624     );
    625 
    626     if let MdElement::Table { headers, rows } = &tables[0] {
    627         let buf = parser.buffer();
    628         let h: Vec<&str> = headers.iter().map(|s| r(s, buf)).collect();
    629         assert_eq!(h, &["Name", "Age"]);
    630         assert_eq!(rows.len(), 2);
    631         let r0: Vec<&str> = rows[0].iter().map(|s| r(s, buf)).collect();
    632         let r1: Vec<&str> = rows[1].iter().map(|s| r(s, buf)).collect();
    633         assert_eq!(r0, &["Alice", "30"]);
    634         assert_eq!(r1, &["Bob", "25"]);
    635     }
    636 }
    637 
    638 #[test]
    639 fn test_table_streaming_char_by_char() {
    640     let mut parser = StreamParser::new();
    641     let input = "| Name | Age |\n|------|-----|\n| Alice | 30 |\n| Bob | 25 |\n\n";
    642 
    643     for ch in input.chars() {
    644         parser.push(&ch.to_string());
    645     }
    646 
    647     let tables: Vec<_> = parser
    648         .parsed()
    649         .iter()
    650         .filter(|e| matches!(e, MdElement::Table { .. }))
    651         .collect();
    652     assert_eq!(
    653         tables.len(),
    654         1,
    655         "Expected 1 table, got: {:#?}",
    656         parser.parsed()
    657     );
    658 
    659     if let MdElement::Table { headers, rows } = &tables[0] {
    660         let buf = parser.buffer();
    661         let h: Vec<&str> = headers.iter().map(|s| r(s, buf)).collect();
    662         assert_eq!(h, &["Name", "Age"]);
    663         assert_eq!(rows.len(), 2);
    664         let r0: Vec<&str> = rows[0].iter().map(|s| r(s, buf)).collect();
    665         let r1: Vec<&str> = rows[1].iter().map(|s| r(s, buf)).collect();
    666         assert_eq!(r0, &["Alice", "30"]);
    667         assert_eq!(r1, &["Bob", "25"]);
    668     }
    669 }
    670 
    671 #[test]
    672 fn test_table_after_paragraph() {
    673     let mut parser = StreamParser::new();
    674     parser.push("Here is a comparison:\n| A | B |\n|---|---|\n| 1 | 2 |\n\n");
    675 
    676     let has_paragraph = parser
    677         .parsed()
    678         .iter()
    679         .any(|e| matches!(e, MdElement::Paragraph(_)));
    680     let has_table = parser
    681         .parsed()
    682         .iter()
    683         .any(|e| matches!(e, MdElement::Table { .. }));
    684 
    685     assert!(
    686         has_paragraph,
    687         "Missing paragraph, got: {:#?}",
    688         parser.parsed()
    689     );
    690     assert!(has_table, "Missing table, got: {:#?}", parser.parsed());
    691 }
    692 
    693 #[test]
    694 fn test_table_after_paragraph_streaming() {
    695     let mut parser = StreamParser::new();
    696     let input = "Here is a comparison:\n| A | B |\n|---|---|\n| 1 | 2 |\n\n";
    697 
    698     for ch in input.chars() {
    699         parser.push(&ch.to_string());
    700     }
    701 
    702     let has_paragraph = parser
    703         .parsed()
    704         .iter()
    705         .any(|e| matches!(e, MdElement::Paragraph(_)));
    706     let has_table = parser
    707         .parsed()
    708         .iter()
    709         .any(|e| matches!(e, MdElement::Table { .. }));
    710 
    711     assert!(
    712         has_paragraph,
    713         "Missing paragraph, got: {:#?}",
    714         parser.parsed()
    715     );
    716     assert!(has_table, "Missing table, got: {:#?}", parser.parsed());
    717 }
    718 
    719 #[test]
    720 fn test_table_then_paragraph() {
    721     let mut parser = StreamParser::new();
    722     parser.push("| X | Y |\n|---|---|\n| a | b |\n\nSome text after.\n\n");
    723 
    724     let has_table = parser
    725         .parsed()
    726         .iter()
    727         .any(|e| matches!(e, MdElement::Table { .. }));
    728     let has_paragraph = parser
    729         .parsed()
    730         .iter()
    731         .any(|e| matches!(e, MdElement::Paragraph(_)));
    732 
    733     assert!(has_table, "Missing table, got: {:#?}", parser.parsed());
    734     assert!(
    735         has_paragraph,
    736         "Missing paragraph, got: {:#?}",
    737         parser.parsed()
    738     );
    739 }
    740 
    741 #[test]
    742 fn test_table_no_separator_not_a_table() {
    743     let mut parser = StreamParser::new();
    744     // Two pipe rows but no separator — should not be a table
    745     parser.push("| foo | bar |\n| baz | qux |\n\n");
    746 
    747     let has_table = parser
    748         .parsed()
    749         .iter()
    750         .any(|e| matches!(e, MdElement::Table { .. }));
    751     assert!(
    752         !has_table,
    753         "Should NOT be a table without separator row, got: {:#?}",
    754         parser.parsed()
    755     );
    756 }
    757 
    758 #[test]
    759 fn test_table_uneven_columns() {
    760     let mut parser = StreamParser::new();
    761     parser.push("| A | B | C |\n|---|---|---|\n| 1 | 2 |\n| x | y | z |\n\n");
    762 
    763     let tables: Vec<_> = parser
    764         .parsed()
    765         .iter()
    766         .filter(|e| matches!(e, MdElement::Table { .. }))
    767         .collect();
    768     assert_eq!(tables.len(), 1);
    769 
    770     if let MdElement::Table { headers, rows } = &tables[0] {
    771         assert_eq!(headers.len(), 3);
    772         assert_eq!(rows[0].len(), 2); // Fewer cells than headers
    773         assert_eq!(rows[1].len(), 3);
    774     }
    775 }
    776 
    777 #[test]
    778 fn test_table_with_alignment() {
    779     // Separator with alignment colons should still be recognized
    780     let mut parser = StreamParser::new();
    781     parser.push("| Left | Center | Right |\n|:-----|:------:|------:|\n| a | b | c |\n\n");
    782 
    783     let tables: Vec<_> = parser
    784         .parsed()
    785         .iter()
    786         .filter(|e| matches!(e, MdElement::Table { .. }))
    787         .collect();
    788     assert_eq!(
    789         tables.len(),
    790         1,
    791         "Expected table with alignment separators, got: {:#?}",
    792         parser.parsed()
    793     );
    794 
    795     if let MdElement::Table { headers, rows } = &tables[0] {
    796         let buf = parser.buffer();
    797         let h: Vec<&str> = headers.iter().map(|s| r(s, buf)).collect();
    798         assert_eq!(h, &["Left", "Center", "Right"]);
    799         assert_eq!(rows.len(), 1);
    800         let r0: Vec<&str> = rows[0].iter().map(|s| r(s, buf)).collect();
    801         assert_eq!(r0, &["a", "b", "c"]);
    802     }
    803 }
    804 
    805 #[test]
    806 fn test_table_finalize_incomplete() {
    807     // Table without trailing blank line — finalize should emit it
    808     let mut parser = StreamParser::new();
    809     parser.push("| H1 | H2 |\n|---|---|\n| v1 | v2 |");
    810 
    811     assert_eq!(parser.parsed().len(), 0, "Table shouldn't be complete yet");
    812 
    813     parser.finalize();
    814 
    815     let has_table = parser
    816         .parsed()
    817         .iter()
    818         .any(|e| matches!(e, MdElement::Table { .. }));
    819     assert!(
    820         has_table,
    821         "Finalize should emit the table, got: {:#?}",
    822         parser.parsed()
    823     );
    824 }
    825 
    826 #[test]
    827 fn test_table_single_column() {
    828     let mut parser = StreamParser::new();
    829     parser.push("| Item |\n|------|\n| Apple |\n| Banana |\n\n");
    830 
    831     let tables: Vec<_> = parser
    832         .parsed()
    833         .iter()
    834         .filter(|e| matches!(e, MdElement::Table { .. }))
    835         .collect();
    836     assert_eq!(tables.len(), 1);
    837 
    838     if let MdElement::Table { headers, rows } = &tables[0] {
    839         let buf = parser.buffer();
    840         let h: Vec<&str> = headers.iter().map(|s| r(s, buf)).collect();
    841         assert_eq!(h, &["Item"]);
    842         assert_eq!(rows.len(), 2);
    843     }
    844 }
    845 
    846 #[test]
    847 fn test_table_empty_cells() {
    848     let mut parser = StreamParser::new();
    849     parser.push("| A | B |\n|---|---|\n|  | val |\n| val |  |\n\n");
    850 
    851     let tables: Vec<_> = parser
    852         .parsed()
    853         .iter()
    854         .filter(|e| matches!(e, MdElement::Table { .. }))
    855         .collect();
    856     assert_eq!(tables.len(), 1);
    857 
    858     if let MdElement::Table { headers, rows } = &tables[0] {
    859         let buf = parser.buffer();
    860         let h: Vec<&str> = headers.iter().map(|s| r(s, buf)).collect();
    861         assert_eq!(h, &["A", "B"]);
    862         let r0: Vec<&str> = rows[0].iter().map(|s| r(s, buf)).collect();
    863         let r1: Vec<&str> = rows[1].iter().map(|s| r(s, buf)).collect();
    864         assert_eq!(r0, &["", "val"]);
    865         assert_eq!(r1, &["val", ""]);
    866     }
    867 }
    868 
    869 #[test]
    870 fn test_table_streaming_realistic_llm_chunks() {
    871     // Simulate LLM-style token delivery
    872     let mut parser = StreamParser::new();
    873     let chunks = [
    874         "Here's",
    875         " the comparison:\n",
    876         "| Feature",
    877         " | ",
    878         "Rust | ",
    879         "Go |\n",
    880         "|---",
    881         "---|",
    882         "------|------|\n",
    883         "| Speed",
    884         " | Fast",
    885         " | Fast |\n",
    886         "| Safety",
    887         " | Yes | No |\n",
    888         "\nThat's",
    889         " the table.",
    890     ];
    891 
    892     for chunk in chunks {
    893         parser.push(chunk);
    894     }
    895     parser.finalize();
    896 
    897     let has_paragraph = parser
    898         .parsed()
    899         .iter()
    900         .any(|e| matches!(e, MdElement::Paragraph(_)));
    901     let has_table = parser
    902         .parsed()
    903         .iter()
    904         .any(|e| matches!(e, MdElement::Table { .. }));
    905 
    906     assert!(
    907         has_paragraph,
    908         "Missing paragraph, got: {:#?}",
    909         parser.parsed()
    910     );
    911     assert!(has_table, "Missing table, got: {:#?}", parser.parsed());
    912 
    913     if let Some(MdElement::Table { headers, rows }) = parser
    914         .parsed()
    915         .iter()
    916         .find(|e| matches!(e, MdElement::Table { .. }))
    917     {
    918         assert_eq!(headers.len(), 3, "Expected 3 headers, got: {:?}", headers);
    919         assert_eq!(rows.len(), 2, "Expected 2 rows, got: {:?}", rows);
    920     }
    921 }
    922 
    923 #[test]
    924 fn test_table_partial_shows_during_streaming() {
    925     let mut parser = StreamParser::new();
    926     // Push header + separator, then start a data row
    927     parser.push("| A | B |\n|---|---|\n");
    928 
    929     // Should have a table partial with seen_separator=true
    930     let partial = parser.partial().expect("Should have partial");
    931     assert!(
    932         matches!(
    933             &partial.kind,
    934             PartialKind::Table {
    935                 seen_separator: true,
    936                 ..
    937             }
    938         ),
    939         "Expected table partial with seen_separator=true, got: {:?}",
    940         partial.kind
    941     );
    942 }
    943 
    944 #[test]
    945 fn test_code_fence_partial_has_language() {
    946     // While streaming a code block, the partial should expose the language
    947     let mut parser = StreamParser::new();
    948     parser.push("```rust\nfn main() {\n");
    949 
    950     let partial = parser
    951         .partial()
    952         .expect("Should have partial while code block is open");
    953     match &partial.kind {
    954         PartialKind::CodeFence { language, .. } => {
    955             let lang = language.expect("Language should be set during partial");
    956             assert_eq!(lang.resolve(parser.buffer()), "rust");
    957         }
    958         other => panic!("Expected CodeFence partial, got: {:?}", other),
    959     }
    960     // Content should be available too
    961     assert_eq!(partial.content(parser.buffer()), "fn main() {\n");
    962 }
    963 
    964 #[test]
    965 fn test_code_fence_partial_language_streamed_char_by_char() {
    966     // Simulate LLM token-by-token streaming
    967     let mut parser = StreamParser::new();
    968     let input = "```python\ndef hello():\n    print(\"hi\")\n";
    969 
    970     for ch in input.chars() {
    971         parser.push(&ch.to_string());
    972     }
    973 
    974     // Should still be partial (no closing fence)
    975     assert_eq!(
    976         parser.parsed().len(),
    977         0,
    978         "Should not have finalized any elements"
    979     );
    980     let partial = parser.partial().expect("Should have partial");
    981     match &partial.kind {
    982         PartialKind::CodeFence { language, .. } => {
    983             let lang = language.expect("Language should be set");
    984             assert_eq!(lang.resolve(parser.buffer()), "python");
    985         }
    986         other => panic!("Expected CodeFence partial, got: {:?}", other),
    987     }
    988     assert_eq!(
    989         partial.content(parser.buffer()),
    990         "def hello():\n    print(\"hi\")\n"
    991     );
    992 }
    993 
    994 #[test]
    995 fn test_consecutive_code_blocks_preserve_language() {
    996     // Multiple code blocks back-to-back, as an LLM would produce
    997     let mut parser = StreamParser::new();
    998     let input = "```rust\nlet x = 1;\n```\n\n```python\nx = 1\n```\n\n```c\nint x = 1;\n```\n";
    999 
   1000     // Stream in small chunks to simulate LLM output
   1001     let chunks: Vec<&str> = input
   1002         .as_bytes()
   1003         .chunks(5)
   1004         .map(|c| std::str::from_utf8(c).unwrap())
   1005         .collect();
   1006     for chunk in &chunks {
   1007         parser.push(chunk);
   1008     }
   1009 
   1010     let code_blocks: Vec<_> = parser
   1011         .parsed()
   1012         .iter()
   1013         .filter_map(|e| match e {
   1014             MdElement::CodeBlock(cb) => Some(cb),
   1015             _ => None,
   1016         })
   1017         .collect();
   1018 
   1019     assert!(
   1020         code_blocks.len() >= 3,
   1021         "Expected 3 code blocks, got {} (parsed: {:?})",
   1022         code_blocks.len(),
   1023         parser.parsed()
   1024     );
   1025 
   1026     assert_eq!(
   1027         code_blocks[0].language.map(|s| r(&s, parser.buffer())),
   1028         Some("rust")
   1029     );
   1030     assert_eq!(
   1031         code_blocks[1].language.map(|s| r(&s, parser.buffer())),
   1032         Some("python")
   1033     );
   1034     assert_eq!(
   1035         code_blocks[2].language.map(|s| r(&s, parser.buffer())),
   1036         Some("c")
   1037     );
   1038 
   1039     assert_eq!(r(&code_blocks[0].content, parser.buffer()), "let x = 1;\n");
   1040     assert_eq!(r(&code_blocks[1].content, parser.buffer()), "x = 1\n");
   1041     assert_eq!(r(&code_blocks[2].content, parser.buffer()), "int x = 1;\n");
   1042 }