From 027075d35290bc84f6d22937125c111f95ebc10b Mon Sep 17 00:00:00 2001 From: aiongg Date: Sat, 24 Oct 2026 09:16:08 +0000 Subject: [PATCH] Integration: a rejected positional revision stays rejected after a slide is inserted or deleted in the open deck --- crates/engine/src/compare.rs | 43 +++++++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/crates/engine/src/compare.rs b/crates/engine/src/compare.rs index b24d9649..328ec558 100644 --- a/crates/engine/src/compare.rs +++ b/crates/engine/src/compare.rs @@ -44,10 +44,17 @@ impl ChangeKey { /// The revisions still to review: [`diff`] without the rejected ones. pub fn pending(doc: &Presentation, c: &Compare) -> Vec { let mut out = diff(doc, &c.revised, c.by_position); - out.retain(|ch| !c.rejected.contains(&ChangeKey::of(doc, ch))); + out.retain(|ch| !is_rejected(doc, c, ch)); out } +/// Whether `ch` was rejected. Positional matching re-pairs slides by index when the open deck gains +/// or loses a slide, which changes the open half of the key; the revised index stays put, so any +/// rejected key for the same revised slide counts. +fn is_rejected(doc: &Presentation, c: &Compare, ch: &Change) -> bool { + c.rejected.contains(&ChangeKey::of(doc, ch)) || (c.by_position && ch.revised.is_some() && c.rejected.iter().any(|k| k.revised == ch.revised)) +} + /// Whether `cur` and `rev` share no slide id (then slides match by position). pub fn unrelated(cur: &Presentation, rev: &Presentation) -> bool { let ids: HashSet = rev.slides.iter().map(|s| s.id).collect(); @@ -524,4 +531,38 @@ mod tests { assert!(s.execute("review.compare", &json!({})).is_err()); assert!(s.execute("review.accept", &json!({"index": 999})).is_err()); } + + fn slide_texts(s: &Session) -> Vec { + s.doc().unwrap().doc.slides.iter().map(|sl| sl.shapes.iter().filter_map(|x| x.text.as_ref()).map(|t| t.text()).collect::()).collect() + } + + /// By position, a rejected revision stays rejected when a slide is inserted into the open deck + /// and the same revised slide now pairs with a different open slide. + #[test] + fn a_rejected_positional_revision_stays_rejected_after_a_slide_is_inserted() { + let mut s = Session::with_new(); + s.execute("slide.new", &json!({"layout": "title", "title": "A"})).unwrap(); + let mut other = Session::with_new(); + other.execute("slide.new", &json!({"layout": "title", "title": "B"})).unwrap(); + let data = other.execute("file.saveBytes", &json!({"format": "pptx"})).unwrap()["data"].as_str().unwrap().to_string(); + let v = s.execute("review.compare", &json!({"name": "o.pptx", "data": data})).unwrap(); + assert_eq!(v["byPosition"], true, "{v}"); + let changed = |v: &Value| -> Vec { + v["changes"].as_array().unwrap().iter().filter(|c| c["kind"] == "changed").map(|c| c["revised"].as_u64().unwrap()).collect() + }; + let revised = changed(&v); + let last = *revised.last().expect("a changed slide"); + let at = v["changes"].as_array().unwrap().iter().position(|c| c["revised"] == last).unwrap(); + s.execute("review.reject", &json!({"index": at})).unwrap(); + // A new slide at the front shifts every open slide down by one. + s.execute("slide.new", &json!({"layout": "title", "title": "N"})).unwrap(); + let n_at = slide_texts(&s).iter().position(|t| t.contains('N')).unwrap(); + s.execute("slide.move", &json!({"from": n_at, "to": 0})).unwrap(); + assert_eq!(slide_texts(&s).iter().position(|t| t.contains('N')), Some(0)); + let now = s.execute("review.revisions", &json!({})).unwrap(); + assert!(!changed(&now).contains(&last), "{now}"); + s.execute("review.accept", &json!({"all": true})).unwrap(); + let texts = slide_texts(&s); + assert!(!texts.iter().any(|t| t.contains('B')), "{texts:?}"); + } }