use super::{tests::*, *}; use crate::{app::App, document::Document}; fn errors(s: &State) -> String { s.error .iter() .chain(s.plan.iter().flat_map(|p| &p.conflicts)) .cloned() .collect::>() .join("; ") } fn put(p: &mut Project, owner: Id, facet: &FacetSelector, present: bool) { let layer = p.layer_of(owner).unwrap(); let mut value = Draft::from_project(p, owner).unwrap().value; keyed::project_presence(&mut value, facet, layer, present).unwrap(); if let Some(element) = p.elements.get_mut(&owner) { *element = serde_json::from_value(value).unwrap(); } else { p.relations .insert(owner, serde_json::from_value(value).unwrap()); } } fn contains(p: &Project, owner: Id, facet: &FacetSelector, oa: Id, sa: Id) -> bool { scalar::value( &capture_facet(p, oa, sa, facet).unwrap(), p.layer_of(owner).unwrap(), ) .unwrap() .as_bool() .unwrap() } fn history(p: &mut Project, trace: Id, selectors: &[FacetSelector]) { let t = &p.traces[&trace]; let baseline = TraceBaseline { oa: p.properties(t.oa).unwrap(), sa: p.properties(t.sa).unwrap(), semantic: Some(SemanticBaseline { facets: selectors .iter() .map(|s| capture_facet(p, t.oa, t.sa, s).unwrap()) .collect(), }), }; p.traces.get_mut(&trace).unwrap().baseline = Some(baseline); } fn literal(name: &str) -> FacetSelector { FacetSelector::Literal(KeyPair { oa: Some(name.into()), sa: Some(name.into()), }) } fn enum_pair() -> (Project, Id, Id, Id) { let mut p = Project::empty(); let mut ids = vec![]; for layer in [Layer::Oa, Layer::Sa] { let id = p.add_element(Kind::DataType, layer, "Enum", None).unwrap(); *p.elements.get_mut(&id).unwrap().shared_mut().unwrap() = SharedBody::DataType { definition: DataTypeDefinition::Enumeration { literals: vec!["retained".into()], }, default: Some(DataValue::Enumeration("retained".into())), unit: String::new(), }; ids.push(id); } let trace = add_trace(&mut p, ids[0], ids[1], "Literal review"); (p, ids[0], ids[1], trace) } fn member_pair(collection: ReferenceCollection) -> (Project, Id, Id, Id, FacetSelector, [Id; 2]) { let mut p = Project::empty(); let mut owners = vec![]; let mut keys = vec![]; let mut locals = vec![]; for layer in [Layer::Oa, Layer::Sa] { let work = if layer == Layer::Oa { Kind::Activity } else { Kind::Function }; let kind = match collection { ReferenceCollection::TransitionTriggers => Kind::Event, ReferenceCollection::TransitionEffects | ReferenceCollection::Constrained => work, _ => Kind::ExchangeItem, }; let key = p.add_element(kind, layer, "Mapped member", None).unwrap(); let local = p.add_element(kind, layer, "Local member", None).unwrap(); let owner = match collection { ReferenceCollection::SignalItems => { let id = p.add_element(Kind::Event, layer, "Signal", None).unwrap(); *p.elements.get_mut(&id).unwrap().shared_mut().unwrap() = SharedBody::Event { body: EventBody::Signal { items: vec![] }, }; id } ReferenceCollection::Constrained => p .add_element(Kind::Constraint, layer, "Constraint", None) .unwrap(), ReferenceCollection::ExchangeItems => { let a = p.add_element(work, layer, "From", None).unwrap(); let b = p.add_element(work, layer, "To", None).unwrap(); let id = p.relate( if layer == Layer::Oa { RelationKind::Interaction } else { RelationKind::FunctionalExchange }, "Exchange", a, b, ); if let RelationBody::FunctionalExchange { source_port, target_port, .. } = p.relations[&id].body { for port in [source_port, target_port] { if let Some(SharedBody::Port { contract, .. }) = p.elements.get_mut(&port).unwrap().shared_mut() { match contract { PortContract::FunctionInput { incoming_items } => { *incoming_items = vec![key, local] } PortContract::FunctionOutput { outgoing_items } => { *outgoing_items = vec![key, local] } _ => unreachable!(), } } } } id } ReferenceCollection::TransitionTriggers | ReferenceCollection::TransitionEffects => { let machine = p .add_element(Kind::StateMachine, layer, "Machine", None) .unwrap(); let region = p .add_element(Kind::Region, layer, "Region", Some(machine)) .unwrap(); let state = p .add_element(Kind::State, layer, "State", Some(region)) .unwrap(); p.relate(RelationKind::StateTransition, "Transition", state, state) } ReferenceCollection::FunctionInputItems | ReferenceCollection::FunctionOutputItems => { let parent = p.add_element(work, layer, "Owner", None).unwrap(); let id = Id::new_v4(); let contract = if layer == Layer::Oa { PortContract::Legacy } else if collection == ReferenceCollection::FunctionInputItems { PortContract::FunctionInput { incoming_items: vec![], } } else { PortContract::FunctionOutput { outgoing_items: vec![], } }; let mut port = ports::make_port(&p, id, parent, "Port", contract).unwrap(); if layer == Layer::Oa && let Some(SharedBody::Port { legacy, .. }) = port.shared_mut() { *legacy = Some(LegacyPortContract { direction: PortDirection::InOut, interface: None, }); } p.elements.insert(id, port); id } _ => unreachable!(), }; owners.push(owner); keys.push(key); locals.push(local); } let trace = add_trace(&mut p, owners[0], owners[1], "Membership review"); let facet = FacetSelector::Member { collection, keys: KeyPair { oa: Some(keys[0]), sa: Some(keys[1]), }, }; ( p, owners[0], owners[1], trace, facet, [locals[0], locals[1]], ) } fn stage(app: &mut App, facet: &FacetSelector) { send(app, RowEvent::EditPair(facet.clone())); send(app, RowEvent::StageMember); assert_eq!( state(app).selected.as_ref(), Some(facet), "{}", errors(state(app)) ); } #[test] fn unordered_members_keep_take_candidate_preserve_other_rows_both_directions() { for collection in [ ReferenceCollection::SignalItems, ReferenceCollection::ExchangeItems, ReferenceCollection::TransitionTriggers, ReferenceCollection::TransitionEffects, ReferenceCollection::Constrained, ] { for reverse in [false, true] { for decision in [ SemanticDecision::KeepTarget, SemanticDecision::TakeSource, SemanticDecision::Candidate, ] { let (mut p, oa, sa, trace, facet, locals) = member_pair(collection); let (source, target, local) = if reverse { (sa, oa, locals[0]) } else { (oa, sa, locals[1]) }; put(&mut p, source, &facet, true); let local_facet = FacetSelector::Member { collection, keys: KeyPair { oa: reverse.then_some(local), sa: (!reverse).then_some(local), }, }; put(&mut p, target, &local_facet, true); let mut app = App::with_project(p); let before = app.document.snapshot(); start(&mut app, source); stage(&mut app, &facet); decide(&mut app, facet.clone(), decision); if decision == SemanticDecision::Candidate { send(&mut app, RowEvent::Presence(facet.clone(), true)); } // A target-only Take is an explicit acknowledgment, never implicit removal. decide(&mut app, local_facet.clone(), SemanticDecision::TakeSource); assert!( state(&app).can_apply(), "{collection:?} reverse={reverse} {decision:?}: {}", errors(state(&app)) ); let proposed = state(&app).proposed.clone().unwrap(); assert_eq!(app.document.bundle(), &before.bundle); apply(&mut app); assert!(app.workflow.is_none(), "{}", app.status); assert_eq!(app.project(), &proposed); assert_eq!( contains(app.project(), target, &facet, oa, sa), decision != SemanticDecision::KeepTarget ); assert!(contains(app.project(), target, &local_facet, oa, sa)); assert!(app.project().elements.contains_key(&local)); assert_eq!( app.project().traces[&trace] .baseline .as_ref() .unwrap() .semantic .as_ref() .unwrap() .facets .len(), 2 ); let _ = app.update(Message::Undo); assert_eq!(app.document.bundle(), &before.bundle); } } } } #[test] fn function_port_members_use_existing_typed_choices_and_preserve_oa_legacy() { for collection in [ ReferenceCollection::FunctionInputItems, ReferenceCollection::FunctionOutputItems, ] { let (p, oa, sa, _, facet, _) = member_pair(collection); let FacetSelector::Member { keys, .. } = &facet else { unreachable!() }; let mut app = App::with_project(p); start(&mut app, oa); assert_eq!( state(&app).member_families(app.project()), vec![keyed::Family::Member(collection)] ); send(&mut app, RowEvent::EditPair(facet.clone())); assert!( state(&app) .member_choices(app.project(), Layer::Sa) .iter() .any(|c| Some(c.id) == keys.sa) ); send(&mut app, RowEvent::StageMember); decide(&mut app, facet.clone(), SemanticDecision::Candidate); send(&mut app, RowEvent::Presence(facet.clone(), true)); assert!(state(&app).can_apply(), "{}", errors(state(&app))); apply(&mut app); assert!(contains(app.project(), sa, &facet, oa, sa)); assert!(matches!( app.project().elements[&oa].shared(), Some(SharedBody::Port { contract: PortContract::Legacy, .. }) )); start(&mut app, sa); stage(&mut app, &facet); decide(&mut app, facet.clone(), SemanticDecision::KeepTarget); assert!(state(&app).can_apply(), "{}", errors(state(&app))); decide(&mut app, facet.clone(), SemanticDecision::Candidate); send(&mut app, RowEvent::Presence(facet, true)); assert!( !state(&app).can_apply(), "OA typed membership must remain inapplicable" ); } } #[test] fn exact_literal_new_rows_and_removals_commit_both_directions_without_default_loss() { for reverse in [false, true] { let (mut p, oa, sa, _) = enum_pair(); let (source, target) = if reverse { (sa, oa) } else { (oa, sa) }; let incoming = literal(" New value "); put(&mut p, source, &incoming, true); let removed = literal("old"); put(&mut p, target, &removed, true); let created = literal("new value"); let mut app = App::with_project(p); start(&mut app, source); for (facet, decision) in [ (&incoming, SemanticDecision::TakeSource), (&removed, SemanticDecision::Candidate), (&created, SemanticDecision::Candidate), ] { stage(&mut app, facet); decide(&mut app, facet.clone(), decision); if decision == SemanticDecision::Candidate { send( &mut app, RowEvent::Presence(facet.clone(), facet == &created), ); } } assert!(state(&app).can_apply(), "{}", errors(state(&app))); apply(&mut app); assert!(contains(app.project(), target, &incoming, oa, sa)); assert!(contains(app.project(), target, &created, oa, sa)); assert!(!contains(app.project(), target, &removed, oa, sa)); assert!( matches!(app.project().elements[&target].shared(),Some(SharedBody::DataType { default: Some(DataValue::Enumeration(value)), .. }) if value=="retained") ); let reopened = Document::from_bundle( app.document.bundle().clone(), source_project::ProjectLimits::default(), ) .unwrap(); assert_eq!(reopened.project(), app.project()); } } #[test] fn exact_reset_only_is_one_history_source_transaction_and_undo_redo_both_directions() { for reverse in [false, true] { let (mut p, oa, sa, trace) = enum_pair(); let reset = literal("retained"); let kept = FacetSelector::Header(HeaderFacet::Name); history(&mut p, trace, &[reset.clone(), kept.clone()]); let previous = p.traces[&trace].baseline.clone().unwrap(); let mut app = App::with_project(p); let mut bundle = app.document.bundle().clone(); bundle .files .get_mut(&bundle.entry) .unwrap() .insert_str(0, "// Exact review rationale survives reset.\n"); app.document.apply_bundle(bundle).unwrap(); let before = app.document.snapshot(); let revision = app.document.revision(); start(&mut app, if reverse { sa } else { oa }); send(&mut app, RowEvent::Reset(reset.clone(), true)); assert!(state(&app).decisions.is_empty()); assert!(state(&app).can_apply(), "{}", errors(state(&app))); assert_eq!( state(&app) .facts .iter() .find(|f| f.selector == reset) .unwrap() .drift, Drift::Unreviewed ); send(&mut app, RowEvent::Reset(reset.clone(), false)); assert!(!state(&app).can_apply()); assert_eq!( state(&app) .facts .iter() .find(|f| f.selector == reset) .unwrap() .drift, Drift::Unchanged ); send(&mut app, RowEvent::Reset(reset, true)); apply(&mut app); assert_eq!(app.document.revision(), revision + 1); assert_eq!(app.project().elements, before.decoded.project.elements); let after = &app.project().traces[&trace].baseline.as_ref().unwrap(); assert_eq!(after.oa, previous.oa); assert_eq!(after.sa, previous.sa); assert_eq!( after .semantic .as_ref() .unwrap() .facets .iter() .map(FacetReview::selector) .collect::>(), vec![kept] ); assert!( app.document.bundle().files[&app.document.bundle().entry] .contains("// Exact review rationale survives reset.") ); let accepted = app.document.snapshot(); let _ = app.update(Message::Undo); assert_eq!(app.document.bundle(), &before.bundle); let _ = app.update(Message::Redo); assert_eq!(app.document.bundle(), &accepted.bundle); } } #[test] fn historical_one_sided_pair_migration_requires_exact_reset_and_explicit_overlap_resolution() { for reverse in [false, true] { let (mut p, oa, sa, trace, paired, _) = member_pair(ReferenceCollection::SignalItems); let FacetSelector::Member { collection, keys } = &paired else { unreachable!() }; let (source, target) = if reverse { (sa, oa) } else { (oa, sa) }; let previous = FacetSelector::Member { collection: *collection, keys: KeyPair { oa: if reverse { None } else { keys.oa }, sa: if reverse { keys.sa } else { None }, }, }; put(&mut p, source, &paired, true); history(&mut p, trace, std::slice::from_ref(&previous)); let mut app = App::with_project(p); start(&mut app, source); decide(&mut app, previous.clone(), SemanticDecision::KeepTarget); stage(&mut app, &paired); decide(&mut app, paired.clone(), SemanticDecision::Candidate); send(&mut app, RowEvent::Presence(paired.clone(), true)); assert!(!state(&app).can_apply()); assert_eq!(state(&app).decisions.len(), 2); send(&mut app, RowEvent::Decide(previous.clone(), None)); assert!( !state(&app).can_apply(), "Historic pair cannot silently migrate" ); send(&mut app, RowEvent::Reset(previous.clone(), true)); assert!(state(&app).can_apply(), "{}", errors(state(&app))); send(&mut app, RowEvent::Reset(previous, false)); assert!(!state(&app).can_apply()); assert!(state(&app).presence[&paired]); let old = state(&app).resets.clone(); assert!(old.is_empty()); let historic = app.project().traces[&trace] .baseline .as_ref() .unwrap() .semantic .as_ref() .unwrap() .facets[0] .selector(); send(&mut app, RowEvent::Reset(historic, true)); apply(&mut app); assert!(contains(app.project(), target, &paired, oa, sa)); assert_eq!( app.project().traces[&trace] .baseline .as_ref() .unwrap() .semantic .as_ref() .unwrap() .facets[0] .selector(), paired ); } } #[test] fn reset_precedes_equal_constructor_candidate_and_preserves_independent_literal_draft() { for reverse in [false, true] { let (mut p, oa, sa, trace) = enum_pair(); let ctor = FacetSelector::Data(DataFacet::Constructor); history(&mut p, trace, std::slice::from_ref(&ctor)); let mut app = App::with_project(p); start(&mut app, if reverse { sa } else { oa }); let created = literal("separate"); stage(&mut app, &created); decide(&mut app, created.clone(), SemanticDecision::Candidate); send(&mut app, RowEvent::Presence(created.clone(), true)); send(&mut app, RowEvent::Reset(ctor.clone(), true)); decide(&mut app, ctor.clone(), SemanticDecision::Candidate); assert!(state(&app).can_acknowledge_constructor(app.project(), &ctor)); assert!(state(&app).can_apply(), "{}", errors(state(&app))); let reviewed = &state(&app).plan.as_ref().unwrap().reviews[0]; assert_eq!( reviewed .acknowledged .iter() .cloned() .collect::>(), BTreeSet::from([ctor.clone(), created.clone()]) ); assert!(reviewed.retired.is_empty()); send(&mut app, RowEvent::Reset(ctor.clone(), false)); assert!(!state(&app).can_apply()); assert!(state(&app).presence[&created]); send(&mut app, RowEvent::Reset(ctor, true)); assert!(state(&app).can_apply(), "{}", errors(state(&app))); } } #[test] fn invalid_literal_removal_and_unselected_scalar_buffers_are_retained_and_repairable() { let (p, oa, _, _) = enum_pair(); let mut app = App::with_project(p); start(&mut app, oa); let unit = FacetSelector::Data(DataFacet::Unit); decide(&mut app, unit.clone(), SemanticDecision::Candidate); send( &mut app, RowEvent::Input( "/body/Sa/Shared/DataType/unit".into(), " untouched unit ".into(), ), ); decide(&mut app, unit.clone(), SemanticDecision::KeepTarget); let retained = literal("retained"); decide(&mut app, retained.clone(), SemanticDecision::Candidate); send(&mut app, RowEvent::Presence(retained.clone(), false)); assert!( !state(&app).can_apply(), "Existing default must prevent removal" ); assert!(!state(&app).presence[&retained]); send(&mut app, RowEvent::Presence(retained.clone(), true)); assert!(state(&app).can_apply(), "{}", errors(state(&app))); assert_eq!( state(&app).draft.as_ref().unwrap().inputs["/body/Sa/Shared/DataType/unit"].raw, " untouched unit " ); decide(&mut app, unit, SemanticDecision::Candidate); assert!(state(&app).can_apply(), "{}", errors(state(&app))); } #[test] fn unsupported_independent_families_and_missing_identities_do_not_gain_edit_authority() { let (p, oa, _, _) = enum_pair(); let mut app = App::with_project(p); start(&mut app, oa); let unsupported = FacetSelector::Member { collection: ReferenceCollection::Steps, keys: KeyPair { oa: Some(oa), sa: None, }, }; decide(&mut app, unsupported.clone(), SemanticDecision::Candidate); assert!(!state(&app).decisions.contains_key(&unsupported)); send(&mut app, RowEvent::MemberFamily(keyed::Family::Literal)); send(&mut app, RowEvent::Literal("unpaired".into())); send(&mut app, RowEvent::LiteralSide(Layer::Sa, false)); send(&mut app, RowEvent::StageMember); let facet = state(&app).selected.clone().unwrap(); decide(&mut app, facet.clone(), SemanticDecision::Candidate); assert!(!state(&app).can_apply()); send(&mut app, RowEvent::LiteralSide(Layer::Oa, false)); send(&mut app, RowEvent::StageMember); assert!(state(&app).error.is_some()); assert_eq!( state(&app).decisions.get(&facet), Some(&SemanticDecision::Candidate) ); } #[test] fn deleted_member_source_observation_removes_only_mapped_membership_both_directions() { for reverse in [false, true] { let (mut p, oa, sa, trace, facet, locals) = member_pair(ReferenceCollection::SignalItems); let FacetSelector::Member { keys, .. } = &facet else { unreachable!() }; let (source, target, deleted, local) = if reverse { (sa, oa, keys.sa.unwrap(), locals[0]) } else { (oa, sa, keys.oa.unwrap(), locals[1]) }; let target_only = FacetSelector::Member { collection: ReferenceCollection::SignalItems, keys: KeyPair { oa: reverse.then_some(local), sa: (!reverse).then_some(local), }, }; put(&mut p, source, &facet, true); put(&mut p, target, &facet, true); put(&mut p, target, &target_only, true); history(&mut p, trace, std::slice::from_ref(&facet)); p = prepare_command(&p, &Command::Delete(deleted)) .unwrap() .project; let mut app = App::with_project(p); start(&mut app, source); stage(&mut app, &facet); decide(&mut app, facet.clone(), SemanticDecision::TakeSource); // Force mixed final-record composition, as well as the core Take projection. let name = FacetSelector::Header(HeaderFacet::Name); decide(&mut app, name, SemanticDecision::Candidate); assert!(state(&app).can_apply(), "{}", errors(state(&app))); apply(&mut app); assert!(!contains(app.project(), target, &facet, oa, sa)); assert!(contains(app.project(), target, &target_only, oa, sa)); let retained = if reverse { keys.oa.unwrap() } else { keys.sa.unwrap() }; assert!(app.project().elements.contains_key(&retained)); } } #[test] fn stale_or_source_failed_reset_and_membership_apply_retain_exact_choices() { let (mut p, oa, _, trace) = enum_pair(); let reset = FacetSelector::Header(HeaderFacet::Name); history(&mut p, trace, std::slice::from_ref(&reset)); let mut app = App::with_project(p.clone()); start(&mut app, oa); send(&mut app, RowEvent::Reset(reset.clone(), true)); app.document .execute(Command::Rename(oa, "Changed after preview".into())) .unwrap(); let before = app.document.snapshot(); apply(&mut app); assert!(state(&app).error.as_ref().unwrap().contains("stale")); assert!(state(&app).resets.contains(&reset)); assert_eq!(app.document.bundle(), &before.bundle); let mut app = App::with_project(p); let bundle = app.document.bundle().clone(); let mut limits = source_project::ProjectLimits::default(); limits.model.max_source_bytes = bundle.files.values().map(String::len).sum::() + 10; app.document = Document::from_bundle(bundle, limits).unwrap(); start(&mut app, oa); send(&mut app, RowEvent::Reset(reset.clone(), true)); let added = literal(&" exact long literal ".repeat(100)); stage(&mut app, &added); decide(&mut app, added.clone(), SemanticDecision::Candidate); send(&mut app, RowEvent::Presence(added.clone(), true)); assert!(state(&app).can_apply(), "{}", errors(state(&app))); let before = app.document.snapshot(); apply(&mut app); assert!(state(&app).error.is_some()); assert!(state(&app).resets.contains(&reset)); assert_eq!( state(&app).decisions.get(&added), Some(&SemanticDecision::Candidate) ); assert!(state(&app).presence[&added]); assert_eq!(app.document.bundle(), &before.bundle); assert!(!app.document.can_undo()); } #[test] fn member_identity_labels_and_new_tabs_preserve_exact_names_and_deleted_keys() { let (mut p, oa, _, trace, facet, _) = member_pair(ReferenceCollection::SignalItems); let FacetSelector::Member { collection, keys } = &facet else { unreachable!() }; p.elements.get_mut(&keys.oa.unwrap()).unwrap().name = "Absent intentionally in name".into(); let name = view::title(&p, &facet); assert!(name.contains("Absent intentionally in name")); let deleted1 = Id::new_v4(); let deleted2 = Id::new_v4(); let old1 = FacetSelector::Member { collection: *collection, keys: KeyPair { oa: Some(deleted1), sa: None, }, }; let old2 = FacetSelector::Member { collection: *collection, keys: KeyPair { oa: Some(deleted2), sa: None, }, }; history(&mut p, trace, &[old1.clone(), old2.clone()]); assert!(view::title(&p, &old1).contains(&deleted1.to_string())); assert_ne!(view::title(&p, &old1), view::title(&p, &old2)); assert_ne!( view::title(&p, &literal("Absent")), view::title( &p, &FacetSelector::Literal(KeyPair { oa: None, sa: Some("Absent".into()) }) ) ); let mut app = App::with_project(p); start(&mut app, oa); for tab in [Tab::History, Tab::Members, Tab::Facts] { send(&mut app, RowEvent::Tab(tab)); let _ = state(&app).view(app.project()); } send(&mut app, RowEvent::Reset(old1.clone(), true)); assert!(state(&app).can_apply(), "{}", errors(state(&app))); assert!(!state(&app).facts.iter().any(|f| f.selector == old1)); assert!(state(&app).facts.iter().any(|f| f.selector == old2)); } #[test] fn staging_removal_and_obsolete_reset_recovery_keep_other_decisions_and_raw_presence() { let (mut p, oa, _, trace) = enum_pair(); let reset = FacetSelector::Header(HeaderFacet::Name); history(&mut p, trace, std::slice::from_ref(&reset)); let mut app = App::with_project(p.clone()); start(&mut app, oa); send(&mut app, RowEvent::Reset(reset.clone(), true)); let added = literal("staged draft"); stage(&mut app, &added); decide(&mut app, added.clone(), SemanticDecision::Candidate); send(&mut app, RowEvent::Presence(added.clone(), true)); send(&mut app, RowEvent::DropStaged(added.clone())); assert!(state(&app).decisions.is_empty()); assert!(!state(&app).staged.contains(&added)); assert!(state(&app).presence[&added]); assert!(state(&app).can_apply()); stage(&mut app, &added); decide(&mut app, added.clone(), SemanticDecision::Candidate); assert!(state(&app).presence[&added]); p.traces .get_mut(&trace) .unwrap() .baseline .as_mut() .unwrap() .semantic .as_mut() .unwrap() .facets .clear(); let crate::workflow::Workflow::Semantic(s) = app.workflow.as_mut().unwrap() else { unreachable!() }; s.refresh(&p); assert!(!s.can_apply()); s.tab = Tab::History; let _ = s.view(&p); s.update(&p, RowEvent::Reset(reset.clone(), false)); // The old single-row test implicitly adopted externally changed history on // refresh. A batch has one frozen accepted base: withdrawing raw reset intent // survives, while application requires an explicitly reopened review. assert!(!s.resets.contains(&reset)); assert!(!s.can_apply()); assert!(s.error.as_ref().unwrap().contains("stale")); assert!(s.plan.is_none() && s.proposed.is_none()); assert_eq!(s.decisions.get(&added), Some(&SemanticDecision::Candidate)); assert!(s.presence[&added]); }