From 91579d9aab2f5d3dcc6af7f0e7b97702fcf030c6 Mon Sep 17 00:00:00 2001 From: Jacob Stephens Date: Sun, 4 Oct 2026 16:05:33 -0400 Subject: [PATCH 1/2] One Spec PR module owns the Spec PR in a Spec run (#367) Co-Authored-By: Claude Opus 5.5 --- src/spec_run.rs | 179 ++++++----------------------------- src/spec_run/spec_pr.rs | 174 ++++++++++++++++++++++++++++++++++ src/spec_run/ticket_board.rs | 16 ++-- 3 files changed, 208 insertions(+), 161 deletions(-) create mode 100644 src/spec_run/spec_pr.rs diff --git a/src/spec_run.rs b/src/spec_run.rs index 86b0e96c7..ee3541447 100644 --- a/src/spec_run.rs +++ b/src/spec_run.rs @@ -4,19 +4,20 @@ //! the Base branch, kept mergeable and green like a Run's PR, and Self-merged //! when the Spec run was asked to merge. +mod spec_pr; mod ticket_board; use std::num::NonZeroUsize; use std::sync::mpsc::{self, Sender}; use std::thread; -use anyhow::{Context, Result, bail}; +use anyhow::{Result, bail}; use crate::base_fix::BaseFixAsk; use crate::child_run::{self, Ended, Handle, Kind}; use crate::delivery::{Delivery, Opening}; use crate::failed_run::FailedRun; -use crate::github::{self, PullRequest, Ticket}; +use crate::github::{self, Ticket}; use crate::interrupt; use crate::issue::IssueUrl; use crate::progress; @@ -24,16 +25,12 @@ use crate::prompt; use crate::run::Reached; use crate::worktree::Worktree; +use spec_pr::SpecPr; use ticket_board::TicketBoard; /// The Spec review session's kind, in its progress lines and log name. const SPEC_REVIEW: &str = "spec-review"; -/// The markers around the Tickets checklist in the Spec PR's body, so it can -/// be replaced without touching the text around it. -const CHECKLIST_START: &str = ""; -const CHECKLIST_END: &str = ""; - /// Whether an issue has Tickets, as a Spec has, and every one is closed: /// `open` says of each of its sub-issues whether it is open. pub fn all_closed(open: impl IntoIterator) -> bool { @@ -67,12 +64,11 @@ pub fn run( parallel: NonZeroUsize, ) -> Result { let (spec, base) = (delivery.issue, delivery.base); - let mut spec_pr = draft_spec_pr(spec, worktree.branch())?; + let mut spec_pr = SpecPr::resume(spec, worktree.branch(), base)?; let (ticket_lines, landed) = land_tickets( spec, tickets, &worktree, - base, parallel, &delivery.base_fix.ask_of_tickets(), &mut spec_pr, @@ -80,7 +76,7 @@ pub fn run( let ended = match landed { Ok(checklist) => review_and_deliver(worktree, spec_pr, &checklist, delivery), Err(error) => Err(FailedRun { - pr_url: spec_pr.map(|pr| pr.url), + pr_url: spec_pr.url().map(str::to_string), ..FailedRun::from(error) }), }; @@ -96,40 +92,34 @@ pub fn run( } } -/// Once every Ticket has landed: open the Spec PR as a draft with -/// `checklist` if `spec_pr` is none, then take it to its goal by `delivery`, +/// Once every Ticket has landed: tell `spec_pr` so, with `checklist`, which +/// opens it if it is not open, then take it to its goal by `delivery`, /// opening with the Spec review, as [`run`] does. fn review_and_deliver( worktree: Worktree, - spec_pr: Option, + mut spec_pr: SpecPr, checklist: &str, delivery: Delivery, ) -> Result { let (spec, base) = (delivery.issue, delivery.base); - let spec_pr = match spec_pr { - Some(pr) => pr, - None => open_spec_pr(spec, worktree.branch(), base, checklist)?, - }; + let spec_pr_url = spec_pr.landed(checklist)?; let opening = Opening { kind: SPEC_REVIEW, - prompt: prompt::spec_review(spec, base, worktree.branch(), &spec_pr.url), + prompt: prompt::spec_review(spec, base, worktree.branch(), spec_pr_url), catch_up_from_origin: true, }; // The Spec review may have rewritten the body without the checklist. - let delivered = delivery.deliver(worktree, opening, || { - write_checklist(spec, &spec_pr, checklist) - }); + let delivered = delivery.deliver(worktree, opening, || spec_pr.put_back(checklist)); if delivered.is_err() { - write_checklist_or_warn(spec, &spec_pr, checklist); + spec_pr.show(checklist); } delivered } /// Push the Spec branch, then keep up to `parallel` Tickets running, each /// time one ends starting ready ones, lowest number first, until none is -/// ready and none is running, keeping the Tickets checklist of `spec_pr` up -/// to date as far as GitHub lets it, and opening it into `base` once a Ticket -/// lands if there is none. Returns a line on each Ticket that landed or is +/// ready and none is running, telling `spec_pr` the Tickets checklist as +/// Tickets start and end, and when a Ticket lands. Returns a line on each Ticket that landed or is /// not done, none if the Spec branch could not be pushed, and the last /// Tickets checklist if every Ticket is done: if not, it fails, after putting /// those lines on stderr unless an interrupt or another error ended it first. @@ -138,16 +128,15 @@ fn land_tickets( spec: &IssueUrl, tickets: Vec, worktree: &Worktree, - base: &str, parallel: NonZeroUsize, base_fix: &BaseFixAsk, - spec_pr: &mut Option, + spec_pr: &mut SpecPr, ) -> (Vec, Result) { if let Err(error) = worktree.push() { return (Vec::new(), Err(error)); } let mut board = TicketBoard::new(tickets, parallel); - let landing = run_ready_tickets(spec, &mut board, worktree.branch(), base, base_fix, spec_pr); + let landing = run_ready_tickets(spec, &mut board, worktree.branch(), base_fix, spec_pr); let lines = board.lines(); let landed = landing.and_then(|()| { let not_done: Vec = board @@ -168,8 +157,8 @@ fn land_tickets( /// Keep the Tickets on `board` running as it offers them, until it offers /// none and none is running, telling it as each starts and ends and as the -/// graph is read again, and keeping the Tickets checklist of `spec_pr`, -/// opened into `base` once a Ticket lands, up to date. An interrupt, or an +/// graph is read again, and telling `spec_pr` the Tickets checklist as they +/// start and end, and when one lands. An interrupt, or an /// error other than a Ticket failing, stops any more from starting, and /// fails this once those running have ended. Each Ticket's Run may start a /// Base fix if `base_fix` allows one. @@ -177,9 +166,8 @@ fn run_ready_tickets( spec: &IssueUrl, board: &mut TicketBoard, spec_branch: &str, - base: &str, base_fix: &BaseFixAsk, - spec_pr: &mut Option, + spec_pr: &mut SpecPr, ) -> Result<()> { let (ended, endings) = mpsc::channel(); let mut error = None; @@ -200,8 +188,8 @@ fn run_ready_tickets( Err(start_error) => error = Some(start_error), } } - if started && let Some(pr) = spec_pr { - write_checklist_or_warn(spec, pr, &board.checklist()); + if started { + spec_pr.show(&board.checklist()); } if !board.any_running() { break; @@ -225,15 +213,12 @@ fn run_ready_tickets( } } let list = board.checklist(); - match spec_pr { - Some(pr) => write_checklist_or_warn(spec, pr, &list), - None if landed => match open_spec_pr(spec, spec_branch, base, &list) { - Ok(pr) => *spec_pr = Some(pr), - Err(open_error) => { - error.get_or_insert(open_error); - } - }, - None => {} + if landed { + if let Err(open_error) = spec_pr.landed(&list) { + error.get_or_insert(open_error); + } + } else { + spec_pr.show(&list); } } if let Some(error) = error { @@ -245,27 +230,6 @@ fn run_ready_tickets( Ok(()) } -/// `body` with its Tickets checklist, the text from its first start marker -/// to the next end marker, replaced by `checklist`, or with `checklist` -/// appended if it has no such pair of markers. -fn with_checklist(body: &str, checklist: &str) -> String { - if let Some(start) = body.find(CHECKLIST_START) - && let Some(end) = body[start..].find(CHECKLIST_END) - { - let end = start + end + CHECKLIST_END.len(); - let end = if body[end..].starts_with('\n') { - end + 1 - } else { - end - }; - return format!("{}{checklist}{}", &body[..start], &body[end..]); - } - if body.is_empty() { - return checklist.to_string(); - } - format!("{}\n\n{checklist}", body.trim_end()) -} - /// Start Ticket `number` of `spec` as a child `thirdshift`, a Merge run into /// `spec_branch` from the same Launch directory that may start a Base fix if /// `base_fix` allows one, and a thread that sends `number` and how it ended @@ -299,90 +263,3 @@ fn finish_ticket(number: u64, child: Handle) -> Result { }); Ok(ended) } - -/// The open Spec PR from `spec_branch`, if there is one, converted back to a -/// draft while the Tickets run. -fn draft_spec_pr(spec: &IssueUrl, spec_branch: &str) -> Result> { - let Some(pr) = github::pull_request_for(spec, spec_branch)?.filter(|pr| pr.is_open()) else { - return Ok(None); - }; - if !pr.is_draft { - github::convert_to_draft(spec, spec_branch)?; - } - Ok(Some(pr)) -} - -/// Open the Spec PR from `spec_branch` into `base` as a draft, titled from -/// the Spec, closing it, with `checklist` as its Tickets checklist. -fn open_spec_pr( - spec: &IssueUrl, - spec_branch: &str, - base: &str, - checklist: &str, -) -> Result { - progress::step(format_args!("opening the Spec PR into {base} as a draft")); - let title = github::issue_title(spec)?; - let body = format!( - "The work on Spec #{number}, gathered from its Tickets on {spec_branch}.\n\n\ - Closes #{number}\n\n\ - {checklist}", - number = spec.number - ); - github::create_draft_pr(spec, spec_branch, base, &title, &body)?; - github::pull_request_for(spec, spec_branch)?.context("the Spec PR just opened is not found") -} - -/// Put `checklist` in the body of the Spec PR `pr`, in place of its Tickets -/// checklist, leaving the rest of the body as it is. -fn write_checklist(spec: &IssueUrl, pr: &PullRequest, checklist: &str) -> Result<()> { - let body = github::pr_body(spec, pr.number)?; - let updated = with_checklist(&body, checklist); - if updated != body { - progress::step("updating the Spec PR's Tickets checklist"); - github::set_pr_body(spec, pr.number, &updated)?; - } - Ok(()) -} - -/// [`write_checklist`], only warning if it fails: the Spec run goes on -/// without it. -fn write_checklist_or_warn(spec: &IssueUrl, pr: &PullRequest, checklist: &str) { - if let Err(error) = write_checklist(spec, pr, checklist) { - progress::step(format_args!( - "could not update the Spec PR's Tickets checklist: {error:#}" - )); - } -} - -#[cfg(test)] -mod tests { - use super::*; - - const LIST: &str = "\nnew\n\n"; - - #[test] - fn the_checklist_replaces_the_one_between_the_markers_and_leaves_the_rest() { - let body = "Intro.\n\n\nold\n\n\nCloses #20\n"; - - assert_eq!( - with_checklist(body, LIST), - format!("Intro.\n\n{LIST}\nCloses #20\n") - ); - } - - #[test] - fn the_checklist_is_appended_when_the_markers_are_gone() { - assert_eq!( - with_checklist("A rewritten body.\n\nCloses #20\n", LIST), - format!("A rewritten body.\n\nCloses #20\n\n{LIST}") - ); - assert_eq!(with_checklist("", LIST), LIST); - } - - #[test] - fn the_checklist_is_appended_when_only_one_marker_is_left() { - let body = "Text\n\n\nmore"; - - assert_eq!(with_checklist(body, LIST), format!("{body}\n\n{LIST}")); - } -} diff --git a/src/spec_run/spec_pr.rs b/src/spec_run/spec_pr.rs new file mode 100644 index 000000000..11299a758 --- /dev/null +++ b/src/spec_run/spec_pr.rs @@ -0,0 +1,174 @@ +//! The Spec PR in a Spec run: resumed as a draft while the Tickets run, +//! opened as a draft once the first Ticket lands, and its Tickets checklist +//! kept up to date between its markers and put back after the Spec review. +//! The Spec run's loop says what happened; this says what that does to the +//! Spec PR. + +use anyhow::{Context, Result}; + +use crate::github::{self, PullRequest}; +use crate::issue::IssueUrl; +use crate::progress; + +/// The markers around the Tickets checklist in the Spec PR's body, so it can +/// be replaced without touching the text around it. +const CHECKLIST_START: &str = ""; +const CHECKLIST_END: &str = ""; + +/// A Spec run's Spec PR, from its Spec branch into its Base branch, if it is +/// open. +pub(super) struct SpecPr<'a> { + spec: &'a IssueUrl, + branch: String, + base: &'a str, + pr: Option, +} + +impl<'a> SpecPr<'a> { + /// The Spec PR of `spec` from `branch` into `base`: the open one, if + /// there is one, converted back to a draft while the Tickets run. + pub(super) fn resume(spec: &'a IssueUrl, branch: &str, base: &'a str) -> Result { + let pr = github::pull_request_for(spec, branch)?.filter(|pr| pr.is_open()); + if let Some(pr) = &pr + && !pr.is_draft + { + github::convert_to_draft(spec, branch)?; + } + Ok(Self { + spec, + branch: branch.to_string(), + base, + pr, + }) + } + + /// Show `checklist` as the Spec PR's Tickets checklist if it is open, + /// only warning if that fails: the Spec run goes on without it. + pub(super) fn show(&self, checklist: &str) { + let Some(pr) = &self.pr else { + return; + }; + if let Err(error) = self.write(pr, checklist) { + progress::step(format_args!( + "could not update the Spec PR's Tickets checklist: {error:#}" + )); + } + } + + /// A Ticket landed, and the Tickets checklist is now `checklist`: open + /// the Spec PR as a draft with it if it is not open, else show it. + /// Returns the Spec PR's URL. + pub(super) fn landed(&mut self, checklist: &str) -> Result<&str> { + if self.pr.is_some() { + self.show(checklist); + } else { + self.pr = Some(self.open(checklist)?); + } + Ok(&self.pr.as_ref().expect("the Spec PR is open").url) + } + + /// Put `checklist` back in the Spec PR's body, as the Spec review may + /// have rewritten it without the checklist. + pub(super) fn put_back(&self, checklist: &str) -> Result<()> { + let pr = self.pr.as_ref().context("the Spec PR is not open")?; + self.write(pr, checklist) + } + + /// The Spec PR's URL, if it is open. + pub(super) fn url(&self) -> Option<&str> { + self.pr.as_ref().map(|pr| pr.url.as_str()) + } + + /// Open the Spec PR as a draft, titled from the Spec, closing it, with + /// `checklist` as its Tickets checklist. + fn open(&self, checklist: &str) -> Result { + let (spec, branch, base) = (self.spec, &self.branch, self.base); + progress::step(format_args!("opening the Spec PR into {base} as a draft")); + let title = github::issue_title(spec)?; + let body = format!( + "The work on Spec #{number}, gathered from its Tickets on {branch}.\n\n\ + Closes #{number}\n\n\ + {checklist}", + number = spec.number, + checklist = between_markers(checklist), + ); + github::create_draft_pr(spec, branch, base, &title, &body)?; + github::pull_request_for(spec, branch)?.context("the Spec PR just opened is not found") + } + + /// Put `checklist` in the body of the Spec PR `pr`, in place of its + /// Tickets checklist, leaving the rest of the body as it is. + fn write(&self, pr: &PullRequest, checklist: &str) -> Result<()> { + let body = github::pr_body(self.spec, pr.number)?; + let updated = with_checklist(&body, &between_markers(checklist)); + if updated != body { + progress::step("updating the Spec PR's Tickets checklist"); + github::set_pr_body(self.spec, pr.number, &updated)?; + } + Ok(()) + } +} + +/// `checklist` wrapped in the Tickets checklist markers. +fn between_markers(checklist: &str) -> String { + format!("{CHECKLIST_START}\n{checklist}{CHECKLIST_END}\n") +} + +/// `body` with its Tickets checklist, the text from its first start marker +/// to the next end marker, replaced by `checklist`, or with `checklist` +/// appended if it has no such pair of markers. +fn with_checklist(body: &str, checklist: &str) -> String { + if let Some(start) = body.find(CHECKLIST_START) + && let Some(end) = body[start..].find(CHECKLIST_END) + { + let end = start + end + CHECKLIST_END.len(); + let end = if body[end..].starts_with('\n') { + end + 1 + } else { + end + }; + return format!("{}{checklist}{}", &body[..start], &body[end..]); + } + if body.is_empty() { + return checklist.to_string(); + } + format!("{}\n\n{checklist}", body.trim_end()) +} + +#[cfg(test)] +mod tests { + use super::*; + + const LIST: &str = "\nnew\n\n"; + + #[test] + fn the_checklist_is_wrapped_in_the_markers() { + assert_eq!(between_markers("new\n"), LIST); + } + + #[test] + fn the_checklist_replaces_the_one_between_the_markers_and_leaves_the_rest() { + let body = "Intro.\n\n\nold\n\n\nCloses #20\n"; + + assert_eq!( + with_checklist(body, LIST), + format!("Intro.\n\n{LIST}\nCloses #20\n") + ); + } + + #[test] + fn the_checklist_is_appended_when_the_markers_are_gone() { + assert_eq!( + with_checklist("A rewritten body.\n\nCloses #20\n", LIST), + format!("A rewritten body.\n\nCloses #20\n\n{LIST}") + ); + assert_eq!(with_checklist("", LIST), LIST); + } + + #[test] + fn the_checklist_is_appended_when_only_one_marker_is_left() { + let body = "Text\n\n\nmore"; + + assert_eq!(with_checklist(body, LIST), format!("{body}\n\n{LIST}")); + } +} diff --git a/src/spec_run/ticket_board.rs b/src/spec_run/ticket_board.rs index 54bd482e5..bb59a1d19 100644 --- a/src/spec_run/ticket_board.rs +++ b/src/spec_run/ticket_board.rs @@ -11,8 +11,6 @@ use std::num::NonZeroUsize; use crate::child_run::Ended; use crate::github::Ticket; -use super::{CHECKLIST_END, CHECKLIST_START}; - /// Where a Ticket this Spec run started stands. enum Standing { /// Its Run has started and not yet ended. @@ -122,15 +120,15 @@ impl TicketBoard { lines } - /// The Tickets checklist, between its markers: a line on each Ticket, - /// lowest number first, ticked if it is done, with where it stands. + /// The Tickets checklist: a line on each Ticket, lowest number first, + /// ticked if it is done, with where it stands. pub(super) fn checklist(&self) -> String { - let mut list = format!("{CHECKLIST_START}\n## Tickets\n\n"); + let mut list = "## Tickets\n\n".to_string(); for ticket in self.by_number() { let tick = if ticket.is_open { ' ' } else { 'x' }; list += &format!("- [{tick}] #{} {}\n", ticket.number, self.standing(ticket)); } - list + CHECKLIST_END + "\n" + list } /// How many Tickets' Runs are running. @@ -443,8 +441,7 @@ mod tests { assert_eq!( board.checklist(), - "\n\ - ## Tickets\n\ + "## Tickets\n\ \n\ - [x] #21 landed with https://x/pull/1\n\ - [x] #22 done\n\ @@ -452,8 +449,7 @@ mod tests { - [ ] #24 blocked by #23\n\ - [ ] #25 running\n\ - [ ] #26 unready: labelled needs-info\n\ - - [ ] #27 not started\n\ - \n" + - [ ] #27 not started\n" ); } } From eadbd914a1863bbb0327bb403d38ff00c49b4722 Mon Sep 17 00:00:00 2001 From: Jacob Stephens Date: Sun, 4 Oct 2026 16:20:19 -0400 Subject: [PATCH 2/2] Review clean-ups for the Spec PR module (#367) Co-Authored-By: Claude Opus 5.5 --- src/spec_run.rs | 17 +++++++++-------- src/spec_run/spec_pr.rs | 24 +++++++++++------------- 2 files changed, 20 insertions(+), 21 deletions(-) diff --git a/src/spec_run.rs b/src/spec_run.rs index ee3541447..0a06b8dcb 100644 --- a/src/spec_run.rs +++ b/src/spec_run.rs @@ -119,10 +119,11 @@ fn review_and_deliver( /// Push the Spec branch, then keep up to `parallel` Tickets running, each /// time one ends starting ready ones, lowest number first, until none is /// ready and none is running, telling `spec_pr` the Tickets checklist as -/// Tickets start and end, and when a Ticket lands. Returns a line on each Ticket that landed or is -/// not done, none if the Spec branch could not be pushed, and the last -/// Tickets checklist if every Ticket is done: if not, it fails, after putting -/// those lines on stderr unless an interrupt or another error ended it first. +/// Tickets start and end, and when a Ticket lands. Returns a line on each +/// Ticket that landed or is not done, none if the Spec branch could not be +/// pushed, and the last Tickets checklist if every Ticket is done: if not, it +/// fails, after putting those lines on stderr unless an interrupt or another +/// error ended it first. /// Each Ticket's Run may start a Base fix if `base_fix` allows one. fn land_tickets( spec: &IssueUrl, @@ -158,10 +159,10 @@ fn land_tickets( /// Keep the Tickets on `board` running as it offers them, until it offers /// none and none is running, telling it as each starts and ends and as the /// graph is read again, and telling `spec_pr` the Tickets checklist as they -/// start and end, and when one lands. An interrupt, or an -/// error other than a Ticket failing, stops any more from starting, and -/// fails this once those running have ended. Each Ticket's Run may start a -/// Base fix if `base_fix` allows one. +/// start and end, and when one lands. An interrupt, or an error other than a +/// Ticket failing, stops any more from starting, and fails this once those +/// running have ended. Each Ticket's Run may start a Base fix if `base_fix` +/// allows one. fn run_ready_tickets( spec: &IssueUrl, board: &mut TicketBoard, diff --git a/src/spec_run/spec_pr.rs b/src/spec_run/spec_pr.rs index 11299a758..fe5095c1d 100644 --- a/src/spec_run/spec_pr.rs +++ b/src/spec_run/spec_pr.rs @@ -19,6 +19,8 @@ const CHECKLIST_END: &str = ""; /// open. pub(super) struct SpecPr<'a> { spec: &'a IssueUrl, + /// Owned: the worktree it is borrowed from moves into the Delivery, + /// while the Spec PR is still needed after it. branch: String, base: &'a str, pr: Option, @@ -100,7 +102,7 @@ impl<'a> SpecPr<'a> { /// Tickets checklist, leaving the rest of the body as it is. fn write(&self, pr: &PullRequest, checklist: &str) -> Result<()> { let body = github::pr_body(self.spec, pr.number)?; - let updated = with_checklist(&body, &between_markers(checklist)); + let updated = with_checklist(&body, checklist); if updated != body { progress::step("updating the Spec PR's Tickets checklist"); github::set_pr_body(self.spec, pr.number, &updated)?; @@ -115,9 +117,10 @@ fn between_markers(checklist: &str) -> String { } /// `body` with its Tickets checklist, the text from its first start marker -/// to the next end marker, replaced by `checklist`, or with `checklist` -/// appended if it has no such pair of markers. +/// to the next end marker, replaced by `checklist` between the markers, or +/// with that appended if it has no such pair of markers. fn with_checklist(body: &str, checklist: &str) -> String { + let checklist = between_markers(checklist); if let Some(start) = body.find(CHECKLIST_START) && let Some(end) = body[start..].find(CHECKLIST_END) { @@ -130,7 +133,7 @@ fn with_checklist(body: &str, checklist: &str) -> String { return format!("{}{checklist}{}", &body[..start], &body[end..]); } if body.is_empty() { - return checklist.to_string(); + return checklist; } format!("{}\n\n{checklist}", body.trim_end()) } @@ -141,17 +144,12 @@ mod tests { const LIST: &str = "\nnew\n\n"; - #[test] - fn the_checklist_is_wrapped_in_the_markers() { - assert_eq!(between_markers("new\n"), LIST); - } - #[test] fn the_checklist_replaces_the_one_between_the_markers_and_leaves_the_rest() { let body = "Intro.\n\n\nold\n\n\nCloses #20\n"; assert_eq!( - with_checklist(body, LIST), + with_checklist(body, "new\n"), format!("Intro.\n\n{LIST}\nCloses #20\n") ); } @@ -159,16 +157,16 @@ mod tests { #[test] fn the_checklist_is_appended_when_the_markers_are_gone() { assert_eq!( - with_checklist("A rewritten body.\n\nCloses #20\n", LIST), + with_checklist("A rewritten body.\n\nCloses #20\n", "new\n"), format!("A rewritten body.\n\nCloses #20\n\n{LIST}") ); - assert_eq!(with_checklist("", LIST), LIST); + assert_eq!(with_checklist("", "new\n"), LIST); } #[test] fn the_checklist_is_appended_when_only_one_marker_is_left() { let body = "Text\n\n\nmore"; - assert_eq!(with_checklist(body, LIST), format!("{body}\n\n{LIST}")); + assert_eq!(with_checklist(body, "new\n"), format!("{body}\n\n{LIST}")); } }