Exercise real source-budget refusal in Scenario group removal
This commit is contained in:
@@ -195,6 +195,236 @@ fn scenario_group_removal_is_one_exact_model_source_transaction_in_both_represen
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scenario_group_removal_source_write_failure_preserves_snapshot_selection_camera_and_redo() {
|
||||
use oa_core::source_project::{self, ProjectLimits};
|
||||
|
||||
let mut project = Project::empty();
|
||||
let owner = project
|
||||
.add_element(Kind::Scenario, Layer::Oa, "Conversation", None)
|
||||
.unwrap();
|
||||
let lanes = ["a", "b"].map(|alias| {
|
||||
let id = project
|
||||
.add_element(Kind::Lifeline, Layer::Oa, alias, Some(owner))
|
||||
.unwrap();
|
||||
project.elements.get_mut(&id).unwrap().alias = alias.into();
|
||||
id
|
||||
});
|
||||
let mut messages = Vec::new();
|
||||
for index in 0..256 {
|
||||
let mut message = project
|
||||
.make_element(
|
||||
Kind::Occurrence,
|
||||
Layer::Oa,
|
||||
&format!("Message {index}"),
|
||||
Some(owner),
|
||||
)
|
||||
.unwrap();
|
||||
let SharedBody::Occurrence { body, .. } = message.shared_mut().unwrap() else {
|
||||
panic!("message occurrence");
|
||||
};
|
||||
*body = OccurrenceBody::Exchange {
|
||||
exchange: None,
|
||||
sender: Some(lanes[0]),
|
||||
receiver: Some(lanes[1]),
|
||||
message_kind: oa_core::MessageKind::Asynchronous,
|
||||
reply_to: None,
|
||||
values: vec![],
|
||||
operation: None,
|
||||
};
|
||||
messages.push(message.id);
|
||||
project.elements.insert(message.id, message);
|
||||
}
|
||||
*project
|
||||
.elements
|
||||
.get_mut(&owner)
|
||||
.unwrap()
|
||||
.steps_mut()
|
||||
.unwrap() = messages.clone();
|
||||
let diagram = Id::new_v4();
|
||||
project.diagrams.insert(
|
||||
diagram,
|
||||
Diagram {
|
||||
id: diagram,
|
||||
alias: "removal_view".into(),
|
||||
name: "Removal view".into(),
|
||||
description: String::new(),
|
||||
layer: Layer::Oa,
|
||||
projection: ProjectionKind::Scenario { scenario: owner },
|
||||
view_options: Default::default(),
|
||||
nodes: Default::default(),
|
||||
routes: vec![],
|
||||
layout: Some(DiagramLayout::Scenario(ScenarioLayout::default())),
|
||||
},
|
||||
);
|
||||
oa_core::validate_candidate(&project).unwrap();
|
||||
let bundle = source_project::format_bundle(&project);
|
||||
let bytes = |bundle: &source_project::SourceBundle| {
|
||||
bundle.files.values().map(String::len).sum::<usize>()
|
||||
};
|
||||
let targets = lanes.map(Target::ScenarioObject);
|
||||
let plan = group_removal::prepare(&project, owner, &targets).unwrap();
|
||||
assert_eq!(plan.removed, lanes.into_iter().collect());
|
||||
let mut expected = project.clone();
|
||||
for lane in lanes {
|
||||
expected.elements.remove(&lane);
|
||||
}
|
||||
for id in &messages {
|
||||
let SharedBody::Occurrence {
|
||||
body: OccurrenceBody::Exchange {
|
||||
sender, receiver, ..
|
||||
},
|
||||
..
|
||||
} = expected.elements.get_mut(id).unwrap().shared_mut().unwrap()
|
||||
else {
|
||||
panic!("surviving message");
|
||||
};
|
||||
*sender = None;
|
||||
*receiver = None;
|
||||
}
|
||||
oa_core::validate_candidate(&expected).unwrap();
|
||||
let mut unrestricted = Document::from_bundle(bundle.clone(), Default::default()).unwrap();
|
||||
unrestricted.execute(plan.command.clone()).unwrap();
|
||||
assert_eq!(unrestricted.project(), &expected);
|
||||
// Removal can grow losslessly edited source: each authored @a/@b becomes
|
||||
// explicit none in a surviving message. Prove net growth after deletion,
|
||||
// rather than assuming a budget failure or changing private Document limits.
|
||||
assert!(bytes(unrestricted.bundle()) > bytes(&bundle));
|
||||
let mut limits = ProjectLimits::default();
|
||||
limits.model.max_source_bytes = bytes(&bundle);
|
||||
let mut bounded = Document::from_bundle(bundle.clone(), limits).unwrap();
|
||||
let error = bounded.execute(plan.command).unwrap_err();
|
||||
assert!(
|
||||
error
|
||||
.diagnostics()
|
||||
.iter()
|
||||
.any(|diagnostic| diagnostic.diagnostic.code == "source_limit")
|
||||
);
|
||||
let refusal = format!("Nothing removed: {error}");
|
||||
|
||||
for saved in [false, true] {
|
||||
for with_redo in [false, true] {
|
||||
let mut app = App::with_project(project.clone());
|
||||
app.document = Document::from_bundle(bundle.clone(), limits).unwrap();
|
||||
let redo = if with_redo {
|
||||
// A genuine earlier rename fits the same budget. Undo leaves
|
||||
// its exact model/source snapshot available before this review.
|
||||
app.document
|
||||
.execute(Command::Rename(owner, "Short".into()))
|
||||
.unwrap();
|
||||
let future = app.document.snapshot();
|
||||
assert!(app.document.undo());
|
||||
Some(future)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
app.sync();
|
||||
let _ = app.update(Message::Select(owner));
|
||||
let _ = app.update(if saved {
|
||||
Message::Diagram(Choice {
|
||||
id: diagram,
|
||||
label: "Saved".into(),
|
||||
})
|
||||
} else {
|
||||
Message::Page(Page::Behavior)
|
||||
});
|
||||
for (id, choice) in [
|
||||
(lanes[1], multiselect::Choice::Replace),
|
||||
(lanes[0], multiselect::Choice::Add),
|
||||
] {
|
||||
let event = selection::Event {
|
||||
context: app.scenario_context().unwrap(),
|
||||
snapshot: app.multiselect_snapshot().unwrap(),
|
||||
intent: selection::Intent::Choose(Target::ScenarioObject(id), choice),
|
||||
};
|
||||
let _ = app.update(Message::ScenarioCanvas(CanvasEvent::Selection(Box::new(
|
||||
event,
|
||||
))));
|
||||
}
|
||||
app.window_size = iced::Size::new(1600., 1000.);
|
||||
let context = app.scenario_context().unwrap();
|
||||
let _ = app.update(Message::ScenarioCanvas(CanvasEvent::Viewport(
|
||||
context,
|
||||
iced::Size::new(900., 600.),
|
||||
)));
|
||||
assert!(app.scenario_removal_enabled());
|
||||
let baseline = app.document.snapshot();
|
||||
let revision = app.document.revision();
|
||||
let selected = app.multiselect_snapshot().unwrap();
|
||||
assert_eq!(
|
||||
selected.targets(),
|
||||
&[targets[1].clone(), targets[0].clone()]
|
||||
);
|
||||
let context = app.scenario_context().unwrap();
|
||||
let camera = app.behavior.scenario.camera_boundary();
|
||||
let navigation = (
|
||||
app.page,
|
||||
app.active_diagram,
|
||||
app.selected,
|
||||
app.selected_node,
|
||||
app.behavior.active_owner,
|
||||
);
|
||||
let editor = app.source_editor.text();
|
||||
assert!(!app.document.dirty() && !app.document.can_undo());
|
||||
assert_eq!(app.document.can_redo(), with_redo);
|
||||
let token = open(&mut app);
|
||||
let review = app.delete_target.as_ref().unwrap();
|
||||
assert!(app.scenario_removal_review_valid(review));
|
||||
assert_eq!(review.scenario.as_ref().unwrap().plan.removed, plan.removed);
|
||||
let epoch = app.canvas_epoch;
|
||||
let _ = app.update(Message::ConfirmDelete(token));
|
||||
assert_eq!(app.status, refusal);
|
||||
assert!(!app.delete_pending && app.delete_target.is_none());
|
||||
assert_ne!(app.canvas_epoch, epoch, "failed confirmation retires input");
|
||||
assert!(std::sync::Arc::ptr_eq(&baseline, &app.document.snapshot()));
|
||||
assert_eq!(app.project(), &baseline.decoded.project);
|
||||
assert_eq!(app.document.bundle(), &baseline.bundle);
|
||||
assert_eq!(app.document.revision(), revision);
|
||||
assert!(!app.document.dirty() && !app.document.can_undo());
|
||||
assert_eq!(app.document.can_redo(), with_redo);
|
||||
let retained = app.multiselect_snapshot().unwrap();
|
||||
assert_eq!(retained.targets(), selected.targets());
|
||||
assert_eq!(retained.primary(), selected.primary());
|
||||
assert_eq!(retained.generation(), selected.generation());
|
||||
let after = app.scenario_context().unwrap();
|
||||
assert_eq!(after.document, context.document);
|
||||
assert_eq!(after.revision, context.revision);
|
||||
assert_eq!(after.representation, context.representation);
|
||||
assert_eq!(after.visit, context.visit);
|
||||
assert!(app.behavior.scenario.camera_boundary() == camera);
|
||||
assert_eq!(
|
||||
(
|
||||
app.page,
|
||||
app.active_diagram,
|
||||
app.selected,
|
||||
app.selected_node,
|
||||
app.behavior.active_owner,
|
||||
),
|
||||
navigation
|
||||
);
|
||||
assert_eq!(app.source_editor.text(), editor);
|
||||
assert_eq!(app.source_workspace.bundle(), baseline.bundle);
|
||||
assert!(!app.source_dirty() && !app.draft_dirty());
|
||||
let epoch = app.canvas_epoch;
|
||||
let _ = app.update(Message::ConfirmDelete(token));
|
||||
assert_eq!(app.canvas_epoch, epoch);
|
||||
assert_eq!(app.multiselect_snapshot().unwrap(), retained);
|
||||
assert_eq!(app.status, refusal);
|
||||
assert!(std::sync::Arc::ptr_eq(&baseline, &app.document.snapshot()));
|
||||
if let Some(future) = redo {
|
||||
let _ = app.update(Message::Redo);
|
||||
assert_eq!(app.project(), &future.decoded.project);
|
||||
assert_eq!(app.document.bundle(), &future.bundle);
|
||||
assert!(!app.document.can_redo());
|
||||
let _ = app.update(Message::Undo);
|
||||
assert_eq!(app.project(), &baseline.decoded.project);
|
||||
assert_eq!(app.document.bundle(), &baseline.bundle);
|
||||
assert!(!app.document.can_undo() && !app.document.dirty());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scenario_group_removal_old_modal_packets_do_not_touch_the_new_review() {
|
||||
let mut f = setup(true);
|
||||
|
||||
Reference in New Issue
Block a user