Complete source 6 engineering workflows and preserve release validation evidence
This commit is contained in:
@@ -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()));
|
||||
}
|
||||
Reference in New Issue
Block a user