Complete source 6 engineering workflows and preserve release validation evidence

This commit is contained in:
Codex
2026-09-06 21:49:04 -07:00
parent 2b2d2bc18b
commit 4b6b5fc197
290 changed files with 48271 additions and 1791 deletions
+29 -1
View File
@@ -16,6 +16,7 @@ impl App {
home::Event::New => self.handle(Message::Request(Pending::New)),
home::Event::Open => self.handle(Message::Request(Pending::Open)),
home::Event::Example => self.handle(Message::Request(Pending::Demo)),
home::Event::Walkthrough => self.handle(Message::Request(Pending::Walkthrough)),
home::Event::Guide => self.update(Message::Page(Page::Guide)),
home::Event::Resume => self.update(Message::Page(self.last_workspace_page)),
home::Event::OpenRecent(path) => {
@@ -151,6 +152,7 @@ impl App {
self.reset(restored.document.project().clone(), None);
self.hosted.leave_document();
self.document = restored.document;
self.migration = self.document.compiled().decoded.migration.clone();
self.selected = if self.project().layer_of(restored.selected).is_some()
|| restored.selected == self.project().id
{
@@ -184,7 +186,11 @@ impl App {
} else {
Page::Diagram
};
self.status="Recovered an unsaved local copy. Choose Save as to save it; the original file and hosted model are unchanged.".into();
self.status = if self.migration.is_some() {
"Recovered source 5 as source 6 in an unsaved local copy. Draft text is preserved; workflow previews were rebuilt and semantic impact approvals need review. Choose Save as to save it."
} else {
"Recovered an unsaved local copy. Choose Save as to save it; the original file and hosted model are unchanged."
}.into();
self.request_lint()
}
}
@@ -279,6 +285,28 @@ mod tests {
assert!(Arc::ptr_eq(&accepted, &app.document.snapshot()));
}
#[test]
fn walkthrough_request_keeps_unfinished_source_behind_the_normal_pending_guard() {
let mut app = App::with_project(Project::demo());
let accepted = app.document.snapshot();
let raw = format!(
"{}\nunfinished walkthrough draft",
app.source_workspace.text()
);
app.source_workspace
.edit(raw.clone(), app.source_editor.cursor());
let task = app.update(Message::Home(home::Event::Walkthrough));
assert!(matches!(app.pending, Some(Pending::Walkthrough)));
assert!(!app.busy);
assert!(iced_runtime::task::into_stream(task).is_none());
assert_eq!(app.source_workspace.text(), raw);
assert!(Arc::ptr_eq(&accepted, &app.document.snapshot()));
let _ = app.update(Message::Escape);
assert!(app.pending.is_none());
assert_eq!(app.source_workspace.text(), raw);
assert!(Arc::ptr_eq(&accepted, &app.document.snapshot()));
}
fn retained_snapshot(app: &App) -> recovery::Snapshot {
recovery::Snapshot::capture(
app.canvas_document,
@@ -0,0 +1,150 @@
//! A view picker follows a view inspector without stealing semantic selection.
use super::*;
fn fixture() -> (App, Id, Id, Id) {
let project = language::parse(
r#"arcadia 6; project views "View switching" {
oa_activity work "Inspect work" {}
diagram first "Original concern" {}
diagram second "Commands review" {}
node original_work { diagram @first; element @work; }
node copied_work { diagram @second; element @work; }
}"#,
)
.unwrap();
let id = |alias| {
project
.identities()
.into_iter()
.find(|(_, value)| *value == alias)
.unwrap()
.0
};
let (first, second, work) = (id("first"), id("second"), id("work"));
let mut app = App::with_project(project);
app.page = Page::Diagram;
app.active_diagram = Some(second);
app.selected = second;
app.refresh_draft();
(app, first, second, work)
}
fn choose(app: &mut App, id: Id) {
let _ = app.update(Message::Diagram(Choice {
id,
label: "Captured choice".into(),
}));
}
fn field(app: &mut App, path: &str, value: &str) {
let _ = app.update(Message::Field(path.into(), value.into()));
}
#[test]
fn changing_visible_view_follows_its_clean_inspector_without_a_model_edit() {
let (mut app, original, copy, _) = fixture();
let before = app.document.snapshot();
let revision = app.document.revision();
choose(&mut app, original);
assert_eq!(app.active_diagram, Some(original));
assert_eq!(app.selected, original);
assert_eq!(app.draft.as_ref().unwrap().original, original);
assert_eq!(
app.draft.as_ref().unwrap().inputs["/name"].raw,
"Original concern"
);
assert_eq!(app.project(), &before.decoded.project);
assert_eq!(app.document.bundle(), &before.bundle);
assert_eq!(app.document.revision(), revision);
assert!(!app.document.can_undo());
field(&mut app, "/name", "Edited visible original");
assert!(app.apply());
assert_eq!(
app.project().diagrams[&original].name,
"Edited visible original"
);
assert_eq!(app.project().diagrams[&copy].name, "Commands review");
}
#[test]
fn invalid_view_draft_blocks_the_switch_and_retains_raw_input() {
let (mut app, original, copy, _) = fixture();
field(&mut app, "/id", "-");
let raw = serde_json::to_value(app.draft.as_ref().unwrap().recovery_snapshot()).unwrap();
let before = app.document.snapshot();
choose(&mut app, original);
assert_eq!(app.active_diagram, Some(copy));
assert_eq!(app.selected, copy);
assert_eq!(
serde_json::to_value(app.draft.as_ref().unwrap().recovery_snapshot()).unwrap(),
raw
);
assert_eq!(app.project(), &before.decoded.project);
assert_eq!(app.document.bundle(), &before.bundle);
assert!(!app.document.can_undo());
}
#[test]
fn valid_pending_view_edit_applies_to_old_view_then_inspects_the_new_view() {
let (mut app, original, copy, _) = fixture();
let before = app.document.snapshot();
field(&mut app, "/name", "Reviewed copy");
choose(&mut app, original);
assert_eq!(app.project().diagrams[&copy].name, "Reviewed copy");
assert_eq!(
app.project().diagrams[&original],
before.decoded.project.diagrams[&original]
);
assert_eq!(app.selected, original);
assert_eq!(app.draft.as_ref().unwrap().original, original);
let _ = app.update(Message::Undo);
assert_eq!(app.project(), &before.decoded.project);
assert_eq!(app.document.bundle(), &before.bundle);
assert!(!app.document.can_undo());
}
#[test]
fn diagram_uuid_edit_follows_the_same_view_or_a_different_picker_target() {
for same_view in [false, true] {
let (mut app, original, copy, _) = fixture();
let changed = Id::from_u128(0xabcd123440008000000000000001);
field(&mut app, "/id", &changed.to_string());
choose(&mut app, if same_view { copy } else { original });
let expected = if same_view { changed } else { original };
assert!(!app.project().diagrams.contains_key(&copy));
assert!(app.project().diagrams.contains_key(&changed));
assert_eq!(app.active_diagram, Some(expected));
assert_eq!(app.selected, expected);
assert_eq!(app.draft.as_ref().unwrap().original, expected);
assert!(!app.draft_dirty());
}
}
#[test]
fn diagram_switch_preserves_an_independent_semantic_inspection() {
let (mut app, original, _, work) = fixture();
let _ = app.update(Message::Select(work));
let before = app.document.snapshot();
choose(&mut app, original);
assert_eq!(app.active_diagram, Some(original));
assert_eq!(app.selected, work);
assert_eq!(app.draft.as_ref().unwrap().original, work);
assert_eq!(app.project(), &before.decoded.project);
assert_eq!(app.document.bundle(), &before.bundle);
}
#[test]
fn stale_diagram_choice_never_applies_an_unrelated_valid_draft() {
let (mut app, _, copy, _) = fixture();
field(&mut app, "/name", "Pending copy name");
let before = app.document.snapshot();
choose(&mut app, Id::new_v4());
assert_eq!(app.active_diagram, Some(copy));
assert_eq!(app.selected, copy);
assert_eq!(
app.draft.as_ref().unwrap().inputs["/name"].raw,
"Pending copy name"
);
assert_eq!(app.project(), &before.decoded.project);
assert_eq!(app.document.bundle(), &before.bundle);
assert!(!app.document.can_undo());
assert!(app.status.contains("no longer exists"));
}
@@ -0,0 +1,184 @@
//! Accepted semantic review keeps selection, layer and visible diagram coherent.
use super::*;
use crate::{
semantic_review::{RowEvent, State},
workflow::{Event, Workflow},
};
fn send(app: &mut App, message: Message) {
let _ = app.update(message);
}
fn state(app: &App) -> &State {
let Some(Workflow::Semantic(state)) = &app.workflow else {
panic!("{}", app.status)
};
state
}
fn row_event(app: &mut App, event: RowEvent) {
let event = state(app).address_event(event);
send(app, Message::Workflow(Event::Semantic(event)));
}
fn fixture(reverse: bool, destination_view: bool) -> (App, Id, Id, Id, Option<Id>) {
let mut p = Project::empty();
let oa = p
.add_element(Kind::Activity, Layer::Oa, "Confirm stable pressure", None)
.unwrap();
let sa = p
.add_element(Kind::Function, Layer::Sa, "Confirm safe pressure", None)
.unwrap();
let trace = Id::new_v4();
p.traces.insert(
trace,
Trace {
id: trace,
alias: "confirm_trace".into(),
name: "Confirmation refinement".into(),
description: String::new(),
oa,
sa,
kind: TraceKind::Refinement,
baseline: None,
},
);
let (source, target, source_layer, target_layer) = if reverse {
(sa, oa, Layer::Sa, Layer::Oa)
} else {
(oa, sa, Layer::Oa, Layer::Sa)
};
let from = p.ensure_diagram(source_layer);
p.place(from, source, Position { x: 91., y: 143. });
let to = destination_view.then(|| {
let view = p.ensure_diagram(target_layer);
p.place(view, target, Position { x: 487., y: 213. });
view
});
let mut app = App::with_project(p);
app.page = Page::Diagram;
app.layer = source_layer;
app.active_diagram = Some(from);
app.selected = source;
app.refresh_draft();
(app, source, target, from, to)
}
fn name_take(app: &mut App, source: Id) -> Project {
send(app, Message::StartSemanticReview(source));
let facet = FacetSelector::Header(HeaderFacet::Name);
row_event(app, RowEvent::Select(facet.clone()));
row_event(
app,
RowEvent::Decide(facet, Some(SemanticDecision::TakeSource)),
);
assert!(state(app).can_apply(), "{:?}", state(app).error);
let Some(Workflow::Semantic(state)) = &mut app.workflow else {
unreachable!()
};
let command = state.command(app.document.project()).unwrap();
execute_batch(app.document.project(), &[command]).unwrap()
}
#[test]
fn semantic_name_take_selects_the_counterpart_view_in_both_directions_with_one_exact_history_entry()
{
for reverse in [false, true] {
let (mut app, source, target, from, to) = fixture(reverse, true);
app.search = format!("@{}", app.project().elements[&source].alias);
let query = app.search.clone();
let before = app.document.snapshot();
let expected = name_take(&mut app, source);
app.selected_node = app.project().diagrams[&from].nodes.keys().next().copied();
send(&mut app, Message::Workflow(Event::Apply));
assert_eq!(app.project(), &expected);
assert_eq!(app.project().diagrams, before.decoded.project.diagrams);
assert_eq!(app.selected, target);
assert_eq!(app.layer, app.project().elements[&target].layer());
assert_eq!(app.active_diagram, to);
assert_eq!(
app.project().diagrams[&app.active_diagram.unwrap()].layer,
app.layer
);
// refresh_draft deliberately selects the target's visible placement.
// The invariant is that the old-layer placement cannot survive.
let placement = app.selected_node.expect("visible counterpart placement");
assert_eq!(
app.project().diagrams[&to.unwrap()].nodes[&placement].element,
target
);
assert!(!app.project().diagrams[&from].nodes.contains_key(&placement));
assert_eq!(app.page, Page::Diagram);
assert_eq!(app.search, query);
assert!(app.workflow.is_none());
assert_eq!(app.document.revision(), 1);
let accepted = app.document.snapshot();
assert_eq!(
source_project::compile_bundle(&accepted.bundle, LIMITS)
.unwrap()
.decoded
.project,
expected
);
send(&mut app, Message::Undo);
assert_eq!(app.project(), &before.decoded.project);
assert_eq!(app.document.bundle(), &before.bundle);
assert!(!app.document.can_undo());
send(&mut app, Message::Redo);
assert_eq!(app.project(), &accepted.decoded.project);
assert_eq!(app.document.bundle(), &accepted.bundle);
}
}
#[test]
fn semantic_review_without_a_destination_view_clears_the_old_layer_canvas_without_creating_a_view()
{
let (mut app, source, target, _, _) = fixture(false, false);
let expected = name_take(&mut app, source);
send(&mut app, Message::Workflow(Event::Apply));
assert_eq!(app.project(), &expected);
assert_eq!(app.selected, target);
assert_eq!(app.layer, Layer::Sa);
assert_eq!(app.active_diagram, None);
assert_eq!(app.project().diagrams.len(), 1);
}
#[test]
fn review_already_showing_the_target_keeps_its_camera_and_query() {
let (mut app, source, target, _, to) = fixture(false, true);
app.layer = Layer::Sa;
app.active_diagram = to;
app.selected = target;
app.zoom = 1.31;
app.pan = iced::Vector::new(-219., 88.);
app.search = "pressure".into();
app.refresh_draft();
let expected = name_take(&mut app, source);
send(&mut app, Message::Workflow(Event::Apply));
assert_eq!(app.project(), &expected);
assert_eq!(app.active_diagram, to);
assert_eq!(app.zoom, 1.31);
assert_eq!(app.pan, iced::Vector::new(-219., 88.));
assert_eq!(app.search, "pressure");
}
#[test]
fn rejected_semantic_source_writeback_retains_review_camera_and_original_navigation() {
let (mut app, source, _, from, _) = fixture(false, true);
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::<usize>() + 1;
app.document = Document::from_bundle(bundle, limits).unwrap();
app.zoom = 1.21;
app.pan = iced::Vector::new(72., -44.);
app.search = "confirmation".into();
let before = app.document.snapshot();
let _valid_model = name_take(&mut app, source);
send(&mut app, Message::Workflow(Event::Apply));
assert!(Arc::ptr_eq(&before, &app.document.snapshot()));
assert_eq!(app.selected, source);
assert_eq!(app.layer, Layer::Oa);
assert_eq!(app.active_diagram, Some(from));
assert_eq!(app.page, Page::Diagram);
assert_eq!(app.zoom, 1.21);
assert_eq!(app.pan, iced::Vector::new(72., -44.));
assert_eq!(app.search, "confirmation");
assert!(state(&app).error.is_some());
assert!(!app.document.can_undo());
}
@@ -0,0 +1,298 @@
//! Transition navigation must not add presentation edits to a behavior review.
use super::*;
use crate::workflow::{Event, Workflow};
use oa_core::transitions::*;
fn send(app: &mut App, message: Message) {
let _ = app.update(message);
}
fn element(
p: &mut Project,
number: u128,
kind: Kind,
layer: Layer,
name: &str,
parent: Option<Id>,
) -> Id {
let mut e = p.make_element(kind, layer, name, parent).unwrap();
e.id = Id::from_u128(number);
let id = e.id;
p.elements.insert(id, e);
id
}
fn fixture(layer: Layer, kind: Kind) -> (App, Id, Id, Id) {
let mut p = Project::empty();
let other = if layer == Layer::Oa {
Layer::Sa
} else {
Layer::Oa
};
let work_kind = |l| {
if l == Layer::Oa {
Kind::Activity
} else {
Kind::Function
}
};
let work = element(&mut p, 1, work_kind(layer), layer, "Assess pressure", None);
let reused = element(&mut p, 10, work_kind(other), other, "Assess pressure", None);
let root = element(&mut p, 100, kind, layer, "Restore service", None);
if kind == Kind::Scenario {
let line = element(&mut p, 2, Kind::Lifeline, layer, "Assessment", Some(root));
*p.elements.get_mut(&line).unwrap().shared_mut().unwrap() = SharedBody::Lifeline {
represented: Some(work),
};
}
let view = p.ensure_diagram(other);
p.diagrams.get_mut(&view).unwrap().name = "Unrelated architecture".into();
p.place(view, reused, Position { x: 873., y: 421. });
let mut app = App::with_project(p);
app.layer = layer;
app.page = Page::Behavior;
app.selected = root;
app.behavior.active_owner = Some(root);
app.refresh_draft();
(app, root, work, reused)
}
fn review(
app: &mut App,
selected: Id,
mapping: Option<(Id, Id)>,
destination: Option<Id>,
) -> (Id, Project) {
app.selected = selected;
app.refresh_draft();
send(app, Message::StartTransition);
let Some(Workflow::Transition(t)) = &mut app.workflow else {
panic!("{}", app.status)
};
t.request.scope = DependencyScope::Subtree;
t.request.destination_diagram = destination;
if let Some((source, target)) = mapping {
t.request.mappings.push(TransitionMapping {
source,
target: Some(target),
target_kind: None,
});
}
t.plan = plan_transition(app.document.project(), &t.request).unwrap();
assert!(t.plan.conflicts.is_empty(), "{:?}", t.plan.conflicts);
let target = t
.plan
.changes
.iter()
.find(|c| c.source == selected)
.unwrap()
.target;
let expected = apply_transition(app.document.project(), &t.plan).unwrap();
(target, expected)
}
#[test]
fn scenario_transition_focuses_requested_counterpart_and_preserves_unrelated_views_in_both_directions()
{
for layer in [Layer::Oa, Layer::Sa] {
let (mut app, root, work, reused) = fixture(layer, Kind::Scenario);
let stale_selection = crate::behavior::scenario::CanvasEvent::Select(
app.scenario_context().unwrap(),
Id::from_u128(2),
);
let before = app.document.snapshot();
let (target, expected) = review(&mut app, root, Some((work, reused)), None);
let Some(Workflow::Transition(t)) = &app.workflow else {
unreachable!()
};
assert_ne!(
t.plan.changes.first().unwrap().source,
root,
"Fixture must expose a dependency before the selected root"
);
app.selected_node = Some(Id::from_u128(999));
send(&mut app, Message::Workflow(Event::Apply));
assert_eq!(
app.project(),
&expected,
"Desktop must preserve the core candidate exactly"
);
assert_eq!(app.project().diagrams, before.decoded.project.diagrams);
assert_eq!(app.page, Page::Behavior);
assert_eq!(app.selected, target);
assert_eq!(app.behavior.active_owner, Some(target));
assert_eq!(app.scenario_context().unwrap().scenario, target);
assert_eq!(app.selected_node, None);
assert!(app.workflow.is_none());
assert_eq!(app.document.revision(), 1);
let accepted = app.document.snapshot();
send(&mut app, Message::ScenarioCanvas(stale_selection.clone()));
assert_eq!(app.selected, target);
assert!(Arc::ptr_eq(&accepted, &app.document.snapshot()));
assert_eq!(
source_project::compile_bundle(&accepted.bundle, LIMITS)
.unwrap()
.decoded
.project,
expected
);
send(&mut app, Message::Undo);
assert_eq!(app.project(), &before.decoded.project);
assert_eq!(app.document.bundle(), &before.bundle);
assert!(!app.document.can_undo());
let undone_selection = app.selected;
send(&mut app, Message::ScenarioCanvas(stale_selection));
assert_eq!(app.selected, undone_selection);
assert_eq!(app.project(), &before.decoded.project);
send(&mut app, Message::Redo);
assert_eq!(app.project(), &accepted.decoded.project);
assert_eq!(app.document.bundle(), &accepted.bundle);
}
}
#[test]
fn explicit_behavior_internal_and_process_roots_keep_their_workspace_without_creating_a_view() {
for (layer, kind) in [
(Layer::Oa, Kind::Scenario),
(Layer::Sa, Kind::Scenario),
(Layer::Oa, Kind::Process),
(Layer::Sa, Kind::FunctionalChain),
] {
let (mut app, root, work, reused) = fixture(layer, kind);
let mut p = app.project().clone();
p.diagrams.clear();
app = App::with_project(p);
app.page = Page::Behavior;
let selected = if kind == Kind::Scenario {
Id::from_u128(2)
} else {
root
};
let (target, expected) = review(&mut app, selected, Some((work, reused)), None);
let owner = crate::behavior::selected_behavior(&expected, target).unwrap();
send(&mut app, Message::Workflow(Event::Apply));
assert_eq!(app.project(), &expected);
assert!(app.project().diagrams.is_empty());
assert_eq!(app.selected, target);
assert_eq!(app.behavior.active_owner, Some(owner));
assert_eq!(app.page, Page::Behavior);
}
}
#[test]
fn explicit_destination_is_honored_and_ordinary_work_transition_still_places_in_architecture() {
let (mut app, root, work, reused) = fixture(Layer::Oa, Kind::Scenario);
let destination = *app.project().diagrams.keys().next().unwrap();
let (target, expected) = review(&mut app, root, Some((work, reused)), Some(destination));
assert!(
expected.diagrams[&destination]
.nodes
.values()
.any(|n| n.element == target)
);
send(&mut app, Message::Workflow(Event::Apply));
assert_eq!(app.project(), &expected);
assert_eq!(app.page, Page::Behavior);
let (mut app, _, work, _) = fixture(Layer::Oa, Kind::Scenario);
let (target, _) = review(&mut app, work, None, None);
send(&mut app, Message::Workflow(Event::Apply));
assert_eq!(app.page, Page::Diagram);
assert_eq!(app.selected, target);
assert!(
app.project().diagrams[&app.active_diagram.unwrap()]
.nodes
.values()
.any(|n| n.element == target)
);
}
#[test]
fn machine_and_typed_action_transitions_resolve_the_owner_and_reusing_that_context_retains_camera()
{
for selected_action in [false, true] {
let mut p = Project::empty();
let owner = element(&mut p, 1, Kind::Entity, Layer::Oa, "Control room", None);
let counterpart = element(&mut p, 10, Kind::System, Layer::Sa, "Control room", None);
let machine = element(
&mut p,
100,
Kind::StateMachine,
Layer::Oa,
"Pressure modes",
Some(owner),
);
let region = element(&mut p, 101, Kind::Region, Layer::Oa, "Main", Some(machine));
let state = element(&mut p, 102, Kind::State, Layer::Oa, "Waiting", Some(region));
let work = element(&mut p, 2, Kind::Activity, Layer::Oa, "Assess alarm", None);
let action = Id::from_u128(103);
p.relations.insert(
action,
Relation {
id: action,
alias: "assess_entry".into(),
name: "Assess on entry".into(),
description: String::new(),
body: RelationBody::StateAction {
source: state,
target: work,
phase: StateActionKind::Entry,
},
},
);
let mut app = App::with_project(p);
app.page = Page::Behavior;
app.behavior.active_owner = Some(machine);
let selected = if selected_action { action } else { machine };
let (target, expected) = review(&mut app, selected, Some((owner, counterpart)), None);
let target_machine = crate::behavior::selected_behavior(&expected, target).unwrap();
send(&mut app, Message::Workflow(Event::Apply));
assert_eq!(app.project(), &expected);
assert_eq!(app.page, Page::Behavior);
assert_eq!(app.selected, target);
assert_eq!(app.behavior.active_owner, Some(target_machine));
assert!(
app.project().diagrams.is_empty(),
"The state workspace must offer explicit view creation"
);
app.behavior.diagram.zoom = 1.17;
app.behavior.diagram.pan = iced::Vector::new(-73., 146.);
let epoch = app.canvas_epoch;
let (same_target, _) = review(&mut app, selected, Some((owner, counterpart)), None);
assert_eq!(same_target, target);
send(&mut app, Message::Workflow(Event::Apply));
assert_eq!(app.behavior.diagram.zoom, 1.17);
assert_eq!(app.behavior.diagram.pan, iced::Vector::new(-73., 146.));
assert_eq!(app.canvas_epoch, epoch);
}
}
#[test]
fn rejected_source_writeback_keeps_review_and_navigation_and_invalid_draft_still_blocks_start() {
let (mut app, root, work, reused) = fixture(Layer::Oa, Kind::Scenario);
let bundle = app.document.bundle().clone();
let mut limits = oa_core::source_project::ProjectLimits::default();
limits.model.max_source_bytes = bundle.files.values().map(String::len).sum::<usize>() + 1;
app.document = Document::from_bundle(bundle.clone(), limits).unwrap();
let (_, _) = review(&mut app, root, Some((work, reused)), None);
let before = app.document.snapshot();
send(&mut app, Message::Workflow(Event::Apply));
assert!(Arc::ptr_eq(&before, &app.document.snapshot()));
assert!(app.workflow.is_some());
assert_eq!(app.page, Page::Behavior);
assert_eq!(app.selected, root);
assert_eq!(app.behavior.active_owner, Some(root));
assert!(!app.document.can_undo());
app.workflow = None;
send(
&mut app,
Message::Field("/alias".into(), "not an alias!".into()),
);
let draft = serde_json::to_value(app.draft.as_ref().unwrap().recovery_snapshot()).unwrap();
send(&mut app, Message::StartTransition);
assert!(app.workflow.is_none());
assert_eq!(
serde_json::to_value(app.draft.as_ref().unwrap().recovery_snapshot()).unwrap(),
draft
);
assert!(Arc::ptr_eq(&before, &app.document.snapshot()));
}
@@ -0,0 +1,222 @@
//! App-level copy behavior, with separate state/architecture view identities.
use super::*;
struct Fixture {
app: App,
architecture: Id,
states: Id,
machine: Id,
region: Id,
state: Id,
}
fn fixture() -> Fixture {
let project = language::parse(
r#"arcadia 3; project copied_views "Copied views" {
oa_entity crew "Crew" {}
oa_state_machine modes "Modes" { parent @crew; }
oa_region main "Main" { parent @modes; }
oa_state ready "Ready" { parent @main; }
diagram architecture "Architecture" {}
diagram state_view "Modes view" { projection { StateMachine { machine @modes; }; }; }
node crew_view { diagram @architecture; element @crew; }
node main_view { diagram @state_view; element @main; }
node ready_view { diagram @state_view; element @ready; }
}"#,
)
.unwrap();
let identity = |alias| {
project
.identities()
.into_iter()
.find(|(_, candidate)| *candidate == alias)
.unwrap()
.0
};
let architecture = identity("architecture");
let states = identity("state_view");
let machine = identity("modes");
let region = identity("main");
let state = identity("ready");
let mut app = App::with_project(project);
// Initial view UUID order must not determine the test's workspace.
app.page = Page::Behavior;
app.active_diagram = Some(architecture);
app.behavior.active_owner = Some(machine);
app.behavior.diagram.diagram = Some(states);
app.selected = state;
app.refresh_draft();
Fixture {
app,
architecture,
states,
machine,
region,
state,
}
}
fn send(app: &mut App, message: Message) {
let _ = app.update(message);
}
fn raw_property(app: &App) -> serde_json::Value {
serde_json::to_value(app.draft.as_ref().unwrap().recovery_snapshot()).unwrap()
}
#[test]
fn copying_the_current_state_view_uses_its_own_identity_and_is_one_source_undo() {
let mut f = fixture();
assert_eq!(f.app.state_context().unwrap().diagram, f.states);
assert_eq!(f.app.active_diagram, Some(f.architecture));
let before = f.app.document.snapshot();
let revision = f.app.document.revision();
send(&mut f.app, Message::DuplicateDiagram(f.states));
assert_eq!(f.app.page, Page::Behavior);
assert_eq!(f.app.behavior.active_owner, Some(f.machine));
let copied = f.app.state_context().unwrap().diagram;
assert_ne!(copied, f.states);
assert_ne!(copied, f.architecture);
assert_eq!(f.app.active_diagram, Some(copied));
assert_eq!(f.app.selected, copied);
assert_eq!(
f.app.project().diagrams[&copied].projection.state_machine(),
Some(f.machine)
);
assert_eq!(
f.app.project().diagrams[&copied]
.nodes
.values()
.map(|node| node.element)
.collect::<BTreeSet<_>>(),
BTreeSet::from([f.region, f.state])
);
assert_eq!(f.app.document.revision(), revision + 1);
let accepted = f.app.document.snapshot();
assert_eq!(
source_project::compile_bundle(f.app.document.bundle(), LIMITS)
.unwrap()
.decoded
.project,
*f.app.project()
);
let mut only_original = f.app.project().clone();
only_original.diagrams.remove(&copied);
assert_eq!(only_original, before.decoded.project);
send(&mut f.app, Message::Undo);
assert_eq!(f.app.project(), &before.decoded.project);
assert_eq!(f.app.document.bundle(), &before.bundle);
assert!(!f.app.document.can_undo());
send(&mut f.app, Message::Redo);
assert_eq!(f.app.project(), &accepted.decoded.project);
assert_eq!(f.app.document.bundle(), &accepted.bundle);
}
#[test]
fn copying_architecture_keeps_architecture_even_with_a_retained_state_view() {
let mut f = fixture();
f.app.page = Page::Diagram;
f.app.active_diagram = Some(f.architecture);
f.app.selected = f.architecture;
f.app.refresh_draft();
send(&mut f.app, Message::DuplicateDiagram(f.architecture));
let copied = f.app.active_diagram.unwrap();
assert_ne!(copied, f.architecture);
assert_eq!(f.app.page, Page::Diagram);
assert!(f.app.state_context().is_none());
assert_eq!(f.app.project().diagrams[&copied].name, "Architecture copy");
assert_eq!(
f.app.project().diagrams[&copied].projection,
f.app.project().diagrams[&f.architecture].projection
);
assert_eq!(f.app.project().diagrams[&copied].nodes.len(), 1);
assert_eq!(f.app.selected, copied);
}
#[test]
fn stale_copy_does_not_apply_an_unrelated_valid_property_draft() {
let mut f = fixture();
send(
&mut f.app,
Message::Field("/name".into(), "Pending state rename".into()),
);
let raw = raw_property(&f.app);
let before = f.app.document.snapshot();
let revision = f.app.document.revision();
send(&mut f.app, Message::DuplicateDiagram(Id::new_v4()));
assert_eq!(f.app.project(), &before.decoded.project);
assert_eq!(f.app.document.bundle(), &before.bundle);
assert_eq!(f.app.document.revision(), revision);
assert_eq!(raw_property(&f.app), raw);
assert_eq!(f.app.selected, f.state);
assert!(!f.app.document.can_undo());
assert!(f.app.status.contains("no longer exists"));
}
#[test]
fn duplicate_rejection_preserves_invalid_property_and_pending_behavior_drafts() {
let mut f = fixture();
send(
&mut f.app,
Message::Field("/alias".into(), "invalid alias!".into()),
);
let before = f.app.document.snapshot();
let raw = raw_property(&f.app);
send(&mut f.app, Message::DuplicateDiagram(f.states));
assert_eq!(f.app.project(), &before.decoded.project);
assert_eq!(f.app.document.bundle(), &before.bundle);
assert_eq!(raw_property(&f.app), raw);
assert!(!f.app.document.can_undo());
let mut f = fixture();
send(
&mut f.app,
Message::Behavior(crate::behavior::Event::States(
crate::behavior::states::Event::Start(
f.machine,
crate::behavior::states::Tool::Vertex,
f.region,
),
)),
);
send(
&mut f.app,
Message::Behavior(crate::behavior::Event::States(
crate::behavior::states::Event::Name("Unfinished nested state".into()),
)),
);
assert!(f.app.behavior.has_pending());
let raw = f.app.behavior.recovery_snapshot().unwrap();
let before = f.app.document.snapshot();
send(&mut f.app, Message::DuplicateDiagram(f.states));
assert_eq!(f.app.project(), &before.decoded.project);
assert_eq!(f.app.document.bundle(), &before.bundle);
assert_eq!(f.app.behavior.recovery_snapshot().unwrap(), raw);
assert_eq!(f.app.state_context().unwrap().diagram, f.states);
assert!(!f.app.document.can_undo());
}
#[test]
fn duplicate_rejection_preserves_an_incomplete_source_buffer() {
let mut f = fixture();
let raw = "arcadia 6; project unfinished \"Unfinished source\" {".to_owned();
send(
&mut f.app,
Message::SourceEdit(text_editor::Action::SelectAll),
);
send(
&mut f.app,
Message::SourceEdit(text_editor::Action::Edit(text_editor::Edit::Paste(
Arc::new(raw.clone()),
))),
);
assert!(f.app.source_dirty());
let before = f.app.document.snapshot();
let source = f.app.source_workspace.bundle();
send(&mut f.app, Message::DuplicateDiagram(f.states));
assert_eq!(f.app.project(), &before.decoded.project);
assert_eq!(f.app.document.bundle(), &before.bundle);
assert_eq!(f.app.source_workspace.bundle(), source);
assert_eq!(f.app.source_workspace.text(), raw);
assert!(!f.app.document.can_undo());
}