1use bcp_types::block::{Block, BlockContent};
2use bcp_types::block_type::BlockType;
3use bcp_types::content_store::ContentStore;
4use bcp_types::summary::Summary;
5use bcp_wire::block_frame::BlockFrame;
6use bcp_wire::header::{HEADER_SIZE, BcpHeader};
7
8use crate::decompression::{self, MAX_BLOCK_DECOMPRESSED_SIZE, MAX_PAYLOAD_DECOMPRESSED_SIZE};
9use crate::error::DecodeError;
10
11pub struct DecodedPayload {
25 pub header: BcpHeader,
27
28 pub blocks: Vec<Block>,
34}
35
36pub struct BcpDecoder;
81
82impl BcpDecoder {
83 pub fn decode(payload: &[u8]) -> Result<DecodedPayload, DecodeError> {
109 Self::decode_inner(payload, None)
110 }
111
112 pub fn decode_with_store(
125 payload: &[u8],
126 store: &dyn ContentStore,
127 ) -> Result<DecodedPayload, DecodeError> {
128 Self::decode_inner(payload, Some(store))
129 }
130
131 fn decode_inner(
133 payload: &[u8],
134 store: Option<&dyn ContentStore>,
135 ) -> Result<DecodedPayload, DecodeError> {
136 let header = BcpHeader::read_from(payload).map_err(DecodeError::InvalidHeader)?;
138
139 let block_data: std::borrow::Cow<'_, [u8]> = if header.flags.is_compressed() {
141 let compressed = &payload[HEADER_SIZE..];
142 let decompressed =
143 decompression::decompress(compressed, MAX_PAYLOAD_DECOMPRESSED_SIZE)?;
144 std::borrow::Cow::Owned(decompressed)
145 } else {
146 std::borrow::Cow::Borrowed(&payload[HEADER_SIZE..])
147 };
148
149 let mut cursor = 0;
150 let mut blocks = Vec::new();
151 let mut found_end = false;
152
153 while cursor < block_data.len() {
155 let remaining = &block_data[cursor..];
156
157 if let Some((frame, consumed)) = BlockFrame::read_from(remaining)? {
158 let block = Self::decode_block_frame(&frame, store)?;
159 blocks.push(block);
160 cursor += consumed;
161 } else {
162 found_end = true;
168 cursor += Self::end_sentinel_size(remaining)?;
169 break;
170 }
171 }
172
173 if !found_end {
175 return Err(DecodeError::MissingEndSentinel);
176 }
177
178 if cursor < block_data.len() {
179 return Err(DecodeError::TrailingData {
180 extra_bytes: block_data.len() - cursor,
181 });
182 }
183
184 Ok(DecodedPayload { header, blocks })
185 }
186
187 fn decode_block_frame(
195 frame: &BlockFrame,
196 store: Option<&dyn ContentStore>,
197 ) -> Result<Block, DecodeError> {
198 let block_type = BlockType::from_wire_id(frame.block_type);
199
200 let resolved_body = if frame.flags.is_reference() {
202 let store = store.ok_or(DecodeError::MissingContentStore)?;
203 if frame.body.len() != 32 {
204 return Err(DecodeError::Wire(bcp_wire::WireError::UnexpectedEof {
205 offset: frame.body.len(),
206 }));
207 }
208 let hash: [u8; 32] = frame.body[..32].try_into().unwrap();
210 store
211 .get(&hash)
212 .ok_or(DecodeError::UnresolvedReference { hash })?
213 } else {
214 frame.body.clone()
215 };
216
217 let decompressed_body = if frame.flags.is_compressed() {
219 decompression::decompress(&resolved_body, MAX_BLOCK_DECOMPRESSED_SIZE)?
220 } else {
221 resolved_body
222 };
223
224 let mut body = decompressed_body.as_slice();
226 let mut summary = None;
227
228 if frame.flags.has_summary() {
229 let (sum, consumed) = Summary::decode(body)?;
230 summary = Some(sum);
231 body = &body[consumed..];
232 }
233
234 let content = BlockContent::decode_body(&block_type, body)?;
235
236 Ok(Block {
237 block_type,
238 flags: frame.flags,
239 summary,
240 content,
241 })
242 }
243
244 fn end_sentinel_size(buf: &[u8]) -> Result<usize, DecodeError> {
255 let (_, type_len) = bcp_wire::varint::decode_varint(buf)?;
257 let mut size = type_len;
258
259 if size >= buf.len() {
261 return Err(DecodeError::Wire(bcp_wire::WireError::UnexpectedEof {
262 offset: size,
263 }));
264 }
265 size += 1;
266
267 let rest = buf.get(size..).ok_or(DecodeError::Wire(
269 bcp_wire::WireError::UnexpectedEof { offset: size },
270 ))?;
271 if rest.is_empty() {
272 return Err(DecodeError::Wire(bcp_wire::WireError::UnexpectedEof {
273 offset: size,
274 }));
275 }
276 let (_, len_size) = bcp_wire::varint::decode_varint(rest)?;
277 size += len_size;
278
279 Ok(size)
280 }
281}
282
283#[cfg(test)]
284mod tests {
285 use super::*;
286 use bcp_encoder::BcpEncoder;
287 use bcp_types::diff::DiffHunk;
288 use bcp_types::enums::{
289 AnnotationKind, DataFormat, FormatHint, Lang, MediaType, Priority, Role, Status,
290 };
291 use bcp_types::file_tree::{FileEntry, FileEntryKind};
292 use bcp_wire::block_frame::{BlockFlags, BlockFrame};
293
294 fn roundtrip(encoder: &BcpEncoder) -> DecodedPayload {
298 let payload = encoder.encode().unwrap();
299 BcpDecoder::decode(&payload).unwrap()
300 }
301
302 #[test]
305 fn decode_parses_encoder_output() {
306 let payload = BcpEncoder::new()
307 .add_code(Lang::Rust, "main.rs", b"fn main() {}")
308 .encode()
309 .unwrap();
310
311 let decoded = BcpDecoder::decode(&payload).unwrap();
312 assert_eq!(decoded.blocks.len(), 1);
313 assert_eq!(decoded.header.version_major, 1);
314 assert_eq!(decoded.header.version_minor, 0);
315 }
316
317 #[test]
318 fn roundtrip_single_code_block() {
319 let decoded =
320 roundtrip(BcpEncoder::new().add_code(Lang::Rust, "lib.rs", b"pub fn hello() {}"));
321
322 assert_eq!(decoded.blocks.len(), 1);
323 let block = &decoded.blocks[0];
324 assert_eq!(block.block_type, BlockType::Code);
325 assert!(block.summary.is_none());
326
327 match &block.content {
328 BlockContent::Code(code) => {
329 assert_eq!(code.lang, Lang::Rust);
330 assert_eq!(code.path, "lib.rs");
331 assert_eq!(code.content, b"pub fn hello() {}");
332 assert!(code.line_range.is_none());
333 }
334 other => panic!("expected Code, got {other:?}"),
335 }
336 }
337
338 #[test]
339 fn roundtrip_multiple_block_types() {
340 let decoded = roundtrip(
341 BcpEncoder::new()
342 .add_code(Lang::Python, "app.py", b"print('hi')")
343 .add_conversation(Role::User, b"What is this?")
344 .add_conversation(Role::Assistant, b"A greeting script.")
345 .add_tool_result("pytest", Status::Ok, b"1 passed")
346 .add_document("README", b"# Hello", FormatHint::Markdown),
347 );
348
349 assert_eq!(decoded.blocks.len(), 5);
350
351 let types: Vec<_> = decoded
353 .blocks
354 .iter()
355 .map(|b| b.block_type.clone())
356 .collect();
357 assert_eq!(
358 types,
359 vec![
360 BlockType::Code,
361 BlockType::Conversation,
362 BlockType::Conversation,
363 BlockType::ToolResult,
364 BlockType::Document,
365 ]
366 );
367 }
368
369 #[test]
370 fn roundtrip_with_summary() {
371 let decoded = roundtrip(
372 BcpEncoder::new()
373 .add_code(Lang::Rust, "main.rs", b"fn main() {}")
374 .with_summary("Application entry point.").unwrap(),
375 );
376
377 assert_eq!(decoded.blocks.len(), 1);
378 let block = &decoded.blocks[0];
379 assert!(block.flags.has_summary());
380 assert_eq!(
381 block.summary.as_ref().unwrap().text,
382 "Application entry point."
383 );
384
385 match &block.content {
387 BlockContent::Code(code) => {
388 assert_eq!(code.path, "main.rs");
389 }
390 other => panic!("expected Code, got {other:?}"),
391 }
392 }
393
394 #[test]
395 fn roundtrip_with_priority_annotation() {
396 let decoded = roundtrip(
397 BcpEncoder::new()
398 .add_code(Lang::Rust, "lib.rs", b"// code")
399 .with_priority(Priority::High).unwrap(),
400 );
401
402 assert_eq!(decoded.blocks.len(), 2);
404 assert_eq!(decoded.blocks[0].block_type, BlockType::Code);
405 assert_eq!(decoded.blocks[1].block_type, BlockType::Annotation);
406
407 match &decoded.blocks[1].content {
408 BlockContent::Annotation(ann) => {
409 assert_eq!(ann.target_block_id, 0);
410 assert_eq!(ann.kind, AnnotationKind::Priority);
411 assert_eq!(ann.value, vec![Priority::High.to_wire_byte()]);
412 }
413 other => panic!("expected Annotation, got {other:?}"),
414 }
415 }
416
417 #[test]
418 fn roundtrip_all_block_types() {
419 let decoded = roundtrip(
420 BcpEncoder::new()
421 .add_code(Lang::Rust, "main.rs", b"fn main() {}")
422 .add_conversation(Role::User, b"hello")
423 .add_file_tree(
424 "/project",
425 vec![FileEntry {
426 name: "lib.rs".to_string(),
427 kind: FileEntryKind::File,
428 size: 100,
429 children: vec![],
430 }],
431 )
432 .add_tool_result("rg", Status::Ok, b"3 matches")
433 .add_document("README", b"# Title", FormatHint::Markdown)
434 .add_structured_data(DataFormat::Json, b"{\"key\": \"val\"}")
435 .add_diff(
436 "src/lib.rs",
437 vec![DiffHunk {
438 old_start: 1,
439 new_start: 1,
440 lines: b"+new line\n".to_vec(),
441 }],
442 )
443 .add_annotation(0, AnnotationKind::Tag, b"important")
444 .add_image(MediaType::Png, "screenshot", b"\x89PNG")
445 .add_extension("myco", "custom", b"data"),
446 );
447
448 assert_eq!(decoded.blocks.len(), 10);
449 let types: Vec<_> = decoded
450 .blocks
451 .iter()
452 .map(|b| b.block_type.clone())
453 .collect();
454 assert_eq!(
455 types,
456 vec![
457 BlockType::Code,
458 BlockType::Conversation,
459 BlockType::FileTree,
460 BlockType::ToolResult,
461 BlockType::Document,
462 BlockType::StructuredData,
463 BlockType::Diff,
464 BlockType::Annotation,
465 BlockType::Image,
466 BlockType::Extension,
467 ]
468 );
469 }
470
471 #[test]
472 fn roundtrip_code_with_line_range() {
473 let decoded = roundtrip(BcpEncoder::new().add_code_range(
474 Lang::Rust,
475 "lib.rs",
476 b"fn foo() {}",
477 10,
478 20,
479 ));
480
481 match &decoded.blocks[0].content {
482 BlockContent::Code(code) => {
483 assert_eq!(code.line_range, Some((10, 20)));
484 }
485 other => panic!("expected Code, got {other:?}"),
486 }
487 }
488
489 #[test]
490 fn roundtrip_conversation_with_tool_call_id() {
491 let decoded =
492 roundtrip(BcpEncoder::new().add_conversation_tool(Role::Tool, b"result", "call_abc"));
493
494 match &decoded.blocks[0].content {
495 BlockContent::Conversation(conv) => {
496 assert_eq!(conv.tool_call_id.as_deref(), Some("call_abc"));
497 }
498 other => panic!("expected Conversation, got {other:?}"),
499 }
500 }
501
502 #[test]
503 fn roundtrip_preserves_all_field_values() {
504 let decoded = roundtrip(
506 BcpEncoder::new()
507 .add_file_tree(
508 "/project/src",
509 vec![
510 FileEntry {
511 name: "main.rs".to_string(),
512 kind: FileEntryKind::File,
513 size: 512,
514 children: vec![],
515 },
516 FileEntry {
517 name: "lib".to_string(),
518 kind: FileEntryKind::Directory,
519 size: 0,
520 children: vec![FileEntry {
521 name: "utils.rs".to_string(),
522 kind: FileEntryKind::File,
523 size: 128,
524 children: vec![],
525 }],
526 },
527 ],
528 )
529 .add_diff(
530 "Cargo.toml",
531 vec![
532 DiffHunk {
533 old_start: 5,
534 new_start: 5,
535 lines: b"+tokio = \"1\"\n".to_vec(),
536 },
537 DiffHunk {
538 old_start: 20,
539 new_start: 21,
540 lines: b"-old_dep = \"0.1\"\n+new_dep = \"0.2\"\n".to_vec(),
541 },
542 ],
543 ),
544 );
545
546 assert_eq!(decoded.blocks.len(), 2);
547
548 match &decoded.blocks[0].content {
550 BlockContent::FileTree(tree) => {
551 assert_eq!(tree.root_path, "/project/src");
552 assert_eq!(tree.entries.len(), 2);
553 assert_eq!(tree.entries[0].name, "main.rs");
554 assert_eq!(tree.entries[0].size, 512);
555 assert_eq!(tree.entries[1].name, "lib");
556 assert_eq!(tree.entries[1].children.len(), 1);
557 assert_eq!(tree.entries[1].children[0].name, "utils.rs");
558 }
559 other => panic!("expected FileTree, got {other:?}"),
560 }
561
562 match &decoded.blocks[1].content {
564 BlockContent::Diff(diff) => {
565 assert_eq!(diff.path, "Cargo.toml");
566 assert_eq!(diff.hunks.len(), 2);
567 assert_eq!(diff.hunks[0].old_start, 5);
568 assert_eq!(diff.hunks[1].old_start, 20);
569 assert_eq!(diff.hunks[1].new_start, 21);
570 }
571 other => panic!("expected Diff, got {other:?}"),
572 }
573 }
574
575 #[test]
578 fn rejects_bad_magic() {
579 let mut payload = BcpEncoder::new()
580 .add_conversation(Role::User, b"hi")
581 .encode()
582 .unwrap();
583
584 payload[0] = b'X';
586 let result = BcpDecoder::decode(&payload);
587 assert!(matches!(result, Err(DecodeError::InvalidHeader(_))));
588 }
589
590 #[test]
591 fn rejects_truncated_header() {
592 let result = BcpDecoder::decode(&[0x4C, 0x43, 0x50, 0x00]);
593 assert!(matches!(result, Err(DecodeError::InvalidHeader(_))));
594 }
595
596 #[test]
597 fn rejects_missing_end_sentinel() {
598 let payload = BcpEncoder::new()
599 .add_conversation(Role::User, b"hi")
600 .encode()
601 .unwrap();
602
603 let truncated = &payload[..payload.len() - 4];
605 let result = BcpDecoder::decode(truncated);
606 assert!(matches!(result, Err(DecodeError::MissingEndSentinel)));
607 }
608
609 #[test]
610 fn detects_trailing_data() {
611 let mut payload = BcpEncoder::new()
612 .add_conversation(Role::User, b"hi")
613 .encode()
614 .unwrap();
615
616 payload.extend_from_slice(b"trailing garbage");
618 let result = BcpDecoder::decode(&payload);
619 assert!(matches!(
620 result,
621 Err(DecodeError::TrailingData { extra_bytes: 16 })
622 ));
623 }
624
625 #[test]
626 fn unknown_block_type_captured_not_rejected() {
627 use bcp_wire::header::HeaderFlags;
630
631 let mut payload = vec![0u8; HEADER_SIZE];
632 let header = BcpHeader::new(HeaderFlags::NONE);
633 header.write_to(&mut payload).unwrap();
634
635 let frame = BlockFrame {
637 block_type: 0x42,
638 flags: BlockFlags::NONE,
639 body: b"hello".to_vec(),
640 };
641 frame.write_to(&mut payload).unwrap();
642
643 let end = BlockFrame {
645 block_type: 0xFF,
646 flags: BlockFlags::NONE,
647 body: Vec::new(),
648 };
649 end.write_to(&mut payload).unwrap();
650
651 let decoded = BcpDecoder::decode(&payload).unwrap();
652 assert_eq!(decoded.blocks.len(), 1);
653 assert_eq!(decoded.blocks[0].block_type, BlockType::Unknown(0x42));
654
655 match &decoded.blocks[0].content {
656 BlockContent::Unknown { type_id, body } => {
657 assert_eq!(*type_id, 0x42);
658 assert_eq!(body, b"hello");
659 }
660 other => panic!("expected Unknown, got {other:?}"),
661 }
662 }
663
664 #[test]
665 fn optional_fields_absent_result_in_none() {
666 let decoded = roundtrip(
667 BcpEncoder::new()
668 .add_code(Lang::Rust, "x.rs", b"let x = 1;")
669 .add_conversation(Role::User, b"msg"),
670 );
671
672 match &decoded.blocks[0].content {
674 BlockContent::Code(code) => assert!(code.line_range.is_none()),
675 other => panic!("expected Code, got {other:?}"),
676 }
677
678 match &decoded.blocks[1].content {
680 BlockContent::Conversation(conv) => assert!(conv.tool_call_id.is_none()),
681 other => panic!("expected Conversation, got {other:?}"),
682 }
683 }
684
685 #[test]
686 fn summary_extraction_with_body() {
687 let decoded = roundtrip(
688 BcpEncoder::new()
689 .add_document(
690 "Guide",
691 b"# Getting Started\n\nWelcome!",
692 FormatHint::Markdown,
693 )
694 .with_summary("Onboarding guide for new contributors.").unwrap(),
695 );
696
697 let block = &decoded.blocks[0];
698 assert!(block.flags.has_summary());
699 assert_eq!(
700 block.summary.as_ref().unwrap().text,
701 "Onboarding guide for new contributors."
702 );
703
704 match &block.content {
705 BlockContent::Document(doc) => {
706 assert_eq!(doc.title, "Guide");
707 assert_eq!(doc.content, b"# Getting Started\n\nWelcome!");
708 assert_eq!(doc.format_hint, FormatHint::Markdown);
709 }
710 other => panic!("expected Document, got {other:?}"),
711 }
712 }
713
714 #[test]
715 fn rfc_example_roundtrip() {
716 let decoded = roundtrip(
717 BcpEncoder::new()
718 .add_code(Lang::Rust, "src/main.rs", b"fn main() { todo!() }")
719 .with_summary("Entry point: CLI setup and server startup.").unwrap()
720 .with_priority(Priority::High).unwrap()
721 .add_conversation(Role::User, b"Fix the timeout bug.")
722 .add_conversation(Role::Assistant, b"I'll examine the pool config...")
723 .add_tool_result("ripgrep", Status::Ok, b"3 matches found."),
724 );
725
726 assert_eq!(decoded.blocks.len(), 5);
727
728 assert_eq!(decoded.blocks[0].block_type, BlockType::Code);
730 assert_eq!(
731 decoded.blocks[0].summary.as_ref().unwrap().text,
732 "Entry point: CLI setup and server startup."
733 );
734
735 assert_eq!(decoded.blocks[1].block_type, BlockType::Annotation);
737
738 assert_eq!(decoded.blocks[2].block_type, BlockType::Conversation);
740 assert_eq!(decoded.blocks[3].block_type, BlockType::Conversation);
741
742 assert_eq!(decoded.blocks[4].block_type, BlockType::ToolResult);
744 }
745
746 #[test]
747 fn empty_body_blocks() {
748 let decoded = roundtrip(BcpEncoder::new().add_extension("ns", "type", b""));
750
751 match &decoded.blocks[0].content {
752 BlockContent::Extension(ext) => {
753 assert_eq!(ext.namespace, "ns");
754 assert_eq!(ext.type_name, "type");
755 assert!(ext.content.is_empty());
756 }
757 other => panic!("expected Extension, got {other:?}"),
758 }
759 }
760
761 #[test]
764 fn roundtrip_per_block_compression() {
765 let big_content = "fn main() { println!(\"hello world\"); }\n".repeat(50);
766 let payload = BcpEncoder::new()
767 .add_code(Lang::Rust, "main.rs", big_content.as_bytes())
768 .with_compression().unwrap()
769 .encode()
770 .unwrap();
771
772 let frame_buf = &payload[HEADER_SIZE..];
774 let (frame, _) = BlockFrame::read_from(frame_buf).unwrap().unwrap();
775 assert!(frame.flags.is_compressed());
776
777 let decoded = BcpDecoder::decode(&payload).unwrap();
779 assert_eq!(decoded.blocks.len(), 1);
780 match &decoded.blocks[0].content {
781 BlockContent::Code(code) => {
782 assert_eq!(code.path, "main.rs");
783 assert_eq!(code.content, big_content.as_bytes());
784 }
785 other => panic!("expected Code, got {other:?}"),
786 }
787 }
788
789 #[test]
790 fn roundtrip_per_block_compression_with_summary() {
791 let big_content = "pub fn process() -> Result<(), Error> { Ok(()) }\n".repeat(50);
792 let payload = BcpEncoder::new()
793 .add_code(Lang::Rust, "lib.rs", big_content.as_bytes())
794 .with_summary("Main processing function.").unwrap()
795 .with_compression().unwrap()
796 .encode()
797 .unwrap();
798
799 let decoded = BcpDecoder::decode(&payload).unwrap();
800 let block = &decoded.blocks[0];
801 assert!(block.flags.has_summary());
802 assert!(block.flags.is_compressed());
803 assert_eq!(
804 block.summary.as_ref().unwrap().text,
805 "Main processing function."
806 );
807 match &block.content {
808 BlockContent::Code(code) => assert_eq!(code.content, big_content.as_bytes()),
809 other => panic!("expected Code, got {other:?}"),
810 }
811 }
812
813 #[test]
816 fn roundtrip_whole_payload_compression() {
817 let big_content = "use std::io;\n".repeat(100);
818 let payload = BcpEncoder::new()
819 .add_code(Lang::Rust, "a.rs", big_content.as_bytes())
820 .add_code(Lang::Rust, "b.rs", big_content.as_bytes())
821 .compress_payload()
822 .encode()
823 .unwrap();
824
825 let decoded = BcpDecoder::decode(&payload).unwrap();
826 assert_eq!(decoded.blocks.len(), 2);
827 assert!(decoded.header.flags.is_compressed());
828
829 for block in &decoded.blocks {
830 match &block.content {
831 BlockContent::Code(code) => {
832 assert_eq!(code.content, big_content.as_bytes());
833 }
834 other => panic!("expected Code, got {other:?}"),
835 }
836 }
837 }
838
839 #[test]
842 fn roundtrip_content_addressing() {
843 use bcp_encoder::MemoryContentStore;
844 use std::sync::Arc;
845
846 let store = Arc::new(MemoryContentStore::new());
847 let payload = BcpEncoder::new()
848 .set_content_store(store.clone())
849 .add_code(Lang::Rust, "main.rs", b"fn main() {}")
850 .with_content_addressing().unwrap()
851 .encode()
852 .unwrap();
853
854 let frame_buf = &payload[HEADER_SIZE..];
856 let (frame, _) = BlockFrame::read_from(frame_buf).unwrap().unwrap();
857 assert!(frame.flags.is_reference());
858 assert_eq!(frame.body.len(), 32);
859
860 let result = BcpDecoder::decode(&payload);
862 assert!(matches!(result, Err(DecodeError::MissingContentStore)));
863
864 let decoded = BcpDecoder::decode_with_store(&payload, store.as_ref()).unwrap();
866 assert_eq!(decoded.blocks.len(), 1);
867 match &decoded.blocks[0].content {
868 BlockContent::Code(code) => {
869 assert_eq!(code.path, "main.rs");
870 assert_eq!(code.content, b"fn main() {}");
871 }
872 other => panic!("expected Code, got {other:?}"),
873 }
874 }
875
876 #[test]
877 fn roundtrip_auto_dedup() {
878 use bcp_encoder::MemoryContentStore;
879 use std::sync::Arc;
880
881 let store = Arc::new(MemoryContentStore::new());
882 let payload = BcpEncoder::new()
883 .set_content_store(store.clone())
884 .auto_dedup()
885 .add_code(Lang::Rust, "main.rs", b"fn main() {}")
886 .add_code(Lang::Rust, "main.rs", b"fn main() {}") .encode()
888 .unwrap();
889
890 let decoded = BcpDecoder::decode_with_store(&payload, store.as_ref()).unwrap();
891 assert_eq!(decoded.blocks.len(), 2);
892
893 for block in &decoded.blocks {
895 match &block.content {
896 BlockContent::Code(code) => {
897 assert_eq!(code.content, b"fn main() {}");
898 }
899 other => panic!("expected Code, got {other:?}"),
900 }
901 }
902 }
903
904 #[test]
905 fn unresolved_reference_errors() {
906 use bcp_encoder::MemoryContentStore;
907 use std::sync::Arc;
908
909 let encode_store = Arc::new(MemoryContentStore::new());
910 let payload = BcpEncoder::new()
911 .set_content_store(encode_store)
912 .add_code(Lang::Rust, "main.rs", b"fn main() {}")
913 .with_content_addressing().unwrap()
914 .encode()
915 .unwrap();
916
917 let decode_store = MemoryContentStore::new();
919 let result = BcpDecoder::decode_with_store(&payload, &decode_store);
920 assert!(matches!(
921 result,
922 Err(DecodeError::UnresolvedReference { .. })
923 ));
924 }
925
926 #[test]
929 fn roundtrip_refs_with_whole_payload_compression() {
930 use bcp_encoder::MemoryContentStore;
931 use std::sync::Arc;
932
933 let store = Arc::new(MemoryContentStore::new());
934 let big_content = "fn process() -> bool { true }\n".repeat(50);
935 let payload = BcpEncoder::new()
936 .set_content_store(store.clone())
937 .compress_payload()
938 .add_code(Lang::Rust, "main.rs", big_content.as_bytes())
939 .with_content_addressing().unwrap()
940 .add_conversation(Role::User, b"Review this code")
941 .encode()
942 .unwrap();
943
944 let decoded = BcpDecoder::decode_with_store(&payload, store.as_ref()).unwrap();
945 assert_eq!(decoded.blocks.len(), 2);
946
947 match &decoded.blocks[0].content {
948 BlockContent::Code(code) => {
949 assert_eq!(code.content, big_content.as_bytes());
950 }
951 other => panic!("expected Code, got {other:?}"),
952 }
953 match &decoded.blocks[1].content {
954 BlockContent::Conversation(conv) => {
955 assert_eq!(conv.content, b"Review this code");
956 }
957 other => panic!("expected Conversation, got {other:?}"),
958 }
959 }
960}