Checkpoint source 7 workflows and in-progress diagram authoring redesign
Rust checks / check (ubuntu-latest) (push) Canceled after 0s
Rust checks / check (windows-latest) (push) Canceled after 0s
Rust checks / check (ubuntu-latest) (push) Canceled after 0s
Rust checks / check (windows-latest) (push) Canceled after 0s
Preserve current core, source, REST, desktop, recovery, documentation and validation work, including AGENTS.md and the development summary. This is a work-in-progress checkpoint: the new compact palette has outstanding compilation errors and native acceptance; earlier passing gates retain their documented scope.
This commit is contained in:
@@ -0,0 +1,328 @@
|
||||
use super::*;
|
||||
use crate::app::architecture_authoring::{Event, kinds};
|
||||
|
||||
fn setup(layer: Layer) -> App {
|
||||
let mut app = App::with_project(Project::empty());
|
||||
app.layer = layer;
|
||||
app.active_diagram = None;
|
||||
let _ = app.update(Message::AddDiagram);
|
||||
app.zoom = 0.75;
|
||||
app.pan = iced::Vector::new(37., -19.);
|
||||
app
|
||||
}
|
||||
fn send(app: &mut App, event: Event) {
|
||||
let _ = app.update(Message::Architecture(event));
|
||||
}
|
||||
#[test]
|
||||
fn architecture_creation_names_before_commit_preserves_camera_and_undoes_once() {
|
||||
let mut app = setup(Layer::Oa);
|
||||
let before = app.document.bundle().clone();
|
||||
let model = app.project().clone();
|
||||
let camera = (app.zoom, app.pan);
|
||||
send(&mut app, Event::Start(Kind::Entity, None));
|
||||
assert_eq!(app.project(), &model);
|
||||
let context = app.route_context().unwrap();
|
||||
send(
|
||||
&mut app,
|
||||
Event::Click(context, Position { x: 219., y: 117. }),
|
||||
);
|
||||
assert_eq!(app.project(), &model);
|
||||
assert!(app.architecture.as_ref().unwrap().inline());
|
||||
send(&mut app, Event::Name(" Response team 雨 ".into()));
|
||||
send(&mut app, Event::Submit);
|
||||
assert!(app.architecture.is_none());
|
||||
assert_eq!((app.zoom, app.pan), camera);
|
||||
let element = &app.project().elements[&app.selected];
|
||||
assert_eq!(element.name, " Response team 雨 ");
|
||||
assert_eq!(element.parent, None);
|
||||
let node =
|
||||
&app.project().diagrams[&app.active_diagram.unwrap()].nodes[&app.selected_node.unwrap()];
|
||||
assert_eq!(node.position, Position { x: 219., y: 117. });
|
||||
let accepted = app.document.bundle().clone();
|
||||
let _ = app.update(Message::Undo);
|
||||
assert_eq!(app.document.bundle(), &before);
|
||||
let _ = app.update(Message::Redo);
|
||||
assert_eq!(app.document.bundle(), &accepted);
|
||||
}
|
||||
#[test]
|
||||
fn architecture_context_choices_are_small_and_system_is_singleton() {
|
||||
let mut app = setup(Layer::Oa);
|
||||
assert_eq!(kinds(&app), vec![Kind::Entity, Kind::Actor, Kind::Activity]);
|
||||
app = setup(Layer::Sa);
|
||||
assert_eq!(
|
||||
kinds(&app),
|
||||
vec![Kind::System, Kind::SystemActor, Kind::Function]
|
||||
);
|
||||
send(
|
||||
&mut app,
|
||||
Event::Start(Kind::System, Some(Position { x: 60., y: 60. })),
|
||||
);
|
||||
send(&mut app, Event::Name("Pump controller".into()));
|
||||
send(&mut app, Event::Submit);
|
||||
assert_eq!(kinds(&app), vec![Kind::SystemActor, Kind::Function]);
|
||||
}
|
||||
#[test]
|
||||
fn architecture_name_survives_rejected_click_resize_navigation_and_escape_is_explicit() {
|
||||
let mut app = setup(Layer::Oa);
|
||||
let before = app.document.bundle().clone();
|
||||
send(&mut app, Event::Start(Kind::Activity, None));
|
||||
send(&mut app, Event::Name("Diagnose pressure".into()));
|
||||
send(&mut app, Event::Start(Kind::Entity, None));
|
||||
assert!(
|
||||
matches!(&app.architecture.as_ref().unwrap().saved.intent, architecture_authoring::Intent::Create { name, kind: Kind::Activity, .. } if name == "Diagnose pressure")
|
||||
);
|
||||
send(&mut app, Event::Submit);
|
||||
let mut stale = app.route_context().unwrap();
|
||||
stale.revision += 1;
|
||||
send(&mut app, Event::Click(stale, Position { x: 1., y: 2. }));
|
||||
let _ = app.update(Message::Page(Page::Home));
|
||||
let _ = app.update(Message::WindowResized(iced::Size::new(1100., 700.)));
|
||||
assert_eq!(app.page, Page::Diagram);
|
||||
assert!(app.architecture.is_some());
|
||||
assert_eq!(app.document.bundle(), &before);
|
||||
let _ = app.update(Message::Escape);
|
||||
assert!(app.architecture.is_none());
|
||||
assert_eq!(app.document.bundle(), &before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn architecture_existing_inventory_is_contextual_and_placement_never_implies_allocation() {
|
||||
let mut app = setup(Layer::Oa);
|
||||
let entity = app
|
||||
.project()
|
||||
.make_element(Kind::Entity, Layer::Oa, "Response team", None)
|
||||
.unwrap();
|
||||
let entity_id = entity.id;
|
||||
let work = app
|
||||
.project()
|
||||
.make_element(Kind::Activity, Layer::Oa, "Report status", None)
|
||||
.unwrap();
|
||||
let work_id = work.id;
|
||||
let data = app
|
||||
.project()
|
||||
.make_element(Kind::DataType, Layer::Oa, "Pressure", None)
|
||||
.unwrap();
|
||||
let data_id = data.id;
|
||||
assert!(app.execute(Command::Batch(vec![
|
||||
Command::CreateElement(entity),
|
||||
Command::CreateElement(work),
|
||||
Command::CreateElement(data)
|
||||
])));
|
||||
send(&mut app, Event::Existing(Some(Position { x: 80., y: 80. })));
|
||||
let ids: Vec<_> = architecture_authoring::candidates(&app)
|
||||
.iter()
|
||||
.map(|row| row.0)
|
||||
.collect();
|
||||
assert!(ids.contains(&entity_id) && ids.contains(&work_id));
|
||||
assert!(!ids.contains(&data_id));
|
||||
send(&mut app, Event::Pick(entity_id));
|
||||
let semantic = app.project().elements.clone();
|
||||
let before = app.document.bundle().clone();
|
||||
let camera = (app.zoom, app.pan);
|
||||
send(
|
||||
&mut app,
|
||||
Event::Existing(Some(Position { x: 90., y: 100. })),
|
||||
);
|
||||
assert!(
|
||||
!architecture_authoring::candidates(&app)
|
||||
.iter()
|
||||
.any(|row| row.0 == entity_id)
|
||||
);
|
||||
send(&mut app, Event::ShowPlaced(true));
|
||||
assert!(
|
||||
architecture_authoring::candidates(&app)
|
||||
.iter()
|
||||
.any(|row| row.0 == entity_id && row.2)
|
||||
);
|
||||
send(&mut app, Event::Pick(work_id));
|
||||
assert_eq!(app.project().elements, semantic);
|
||||
assert!(app.project().relations.is_empty());
|
||||
assert_eq!((app.zoom, app.pan), camera);
|
||||
let _ = app.update(Message::Undo);
|
||||
assert_eq!(app.document.bundle(), &before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn architecture_checkpoint_retains_exact_name_point_and_fresh_binding_without_commands() {
|
||||
for (name, placing) in [(" Team 雨 ", true), (" ", false)] {
|
||||
let mut app = setup(Layer::Oa);
|
||||
send(&mut app, Event::Start(Kind::Entity, None));
|
||||
send(&mut app, Event::Name(name.into()));
|
||||
if placing {
|
||||
send(&mut app, Event::Submit);
|
||||
}
|
||||
app.architecture.as_mut().unwrap().saved.point = Some(Position { x: 127.5, y: 215. });
|
||||
app.architecture.as_mut().unwrap().saved.placing = placing;
|
||||
let snapshot = crate::recovery::Snapshot::capture(
|
||||
app.canvas_document,
|
||||
&app.document,
|
||||
&app.source_workspace,
|
||||
app.draft.as_ref(),
|
||||
&app.behavior,
|
||||
None,
|
||||
app.selected,
|
||||
)
|
||||
.unwrap()
|
||||
.with_architecture(app.architecture.as_ref());
|
||||
let bytes = serde_json::to_vec(&snapshot).unwrap();
|
||||
let decoded: crate::recovery::Snapshot = serde_json::from_slice(&bytes).unwrap();
|
||||
let accepted = app.document.bundle().clone();
|
||||
let mut restored = setup(Layer::Sa);
|
||||
let _ = restored.update(Message::CheckpointLoaded(Ok(Arc::new(decoded))));
|
||||
assert_eq!(restored.document.bundle(), &accepted);
|
||||
let draft = &restored.architecture.as_ref().unwrap().saved;
|
||||
assert!(
|
||||
matches!(&draft.intent, architecture_authoring::Intent::Create { name: actual, .. } if actual == name)
|
||||
);
|
||||
assert_eq!(draft.placing, placing);
|
||||
assert_eq!(draft.point, Some(Position { x: 127.5, y: 215. }));
|
||||
if placing {
|
||||
let context = restored.route_context().unwrap();
|
||||
send(
|
||||
&mut restored,
|
||||
Event::Click(context, Position { x: 20., y: 30. }),
|
||||
);
|
||||
assert!(restored.architecture.as_ref().unwrap().inline());
|
||||
send(&mut restored, Event::Submit);
|
||||
assert!(restored.architecture.is_none());
|
||||
assert_eq!(restored.project().elements[&restored.selected].name, name);
|
||||
} else {
|
||||
send(&mut restored, Event::Submit);
|
||||
assert!(restored.architecture.is_some());
|
||||
assert_eq!(restored.document.bundle(), &accepted);
|
||||
}
|
||||
let mut value: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
|
||||
value["architecture"]["base"] = json!("stale");
|
||||
assert!(
|
||||
serde_json::from_value::<crate::recovery::Snapshot>(value)
|
||||
.unwrap()
|
||||
.restore()
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn architecture_canvas_left_click_uses_world_coordinate_once_at_zoom_and_pan() {
|
||||
use iced::widget::canvas::Program;
|
||||
let mut app = setup(Layer::Oa);
|
||||
send(&mut app, Event::Start(Kind::Actor, None));
|
||||
send(&mut app, Event::Name("Control centre".into()));
|
||||
send(&mut app, Event::Submit);
|
||||
let bounds = iced::Rectangle {
|
||||
x: 260.,
|
||||
y: 180.,
|
||||
width: 600.,
|
||||
height: 380.,
|
||||
};
|
||||
let local = iced::Point::new(190., 100.);
|
||||
let expected = Position {
|
||||
x: (local.x - app.pan.x) / app.zoom,
|
||||
y: (local.y - app.pan.y) / app.zoom,
|
||||
};
|
||||
let canvas = crate::diagram::DiagramCanvas {
|
||||
project: app.project(),
|
||||
view_index: None,
|
||||
diagram: app.active_diagram.unwrap(),
|
||||
selected: app.selected,
|
||||
zoom: app.zoom,
|
||||
pan: app.pan,
|
||||
context: app.route_context().unwrap(),
|
||||
selected_bend: None,
|
||||
editable: true,
|
||||
placing: true,
|
||||
connection: None,
|
||||
};
|
||||
let mut state = crate::diagram::State::default();
|
||||
let action = canvas
|
||||
.update(
|
||||
&mut state,
|
||||
&iced::widget::canvas::Event::Mouse(iced::mouse::Event::ButtonPressed(
|
||||
iced::mouse::Button::Left,
|
||||
)),
|
||||
bounds,
|
||||
iced::mouse::Cursor::Available(iced::Point::new(
|
||||
bounds.x + local.x,
|
||||
bounds.y + local.y,
|
||||
)),
|
||||
)
|
||||
.unwrap();
|
||||
let message = action
|
||||
.into_inner()
|
||||
.0
|
||||
.expect("named placement emits a command");
|
||||
let _ = app.update(message);
|
||||
assert_eq!(
|
||||
app.architecture.as_ref().unwrap().saved.point,
|
||||
Some(expected)
|
||||
);
|
||||
send(&mut app, Event::Submit);
|
||||
let node =
|
||||
&app.project().diagrams[&app.active_diagram.unwrap()].nodes[&app.selected_node.unwrap()];
|
||||
assert_eq!(node.position, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn architecture_cancelled_gesture_cannot_commit_a_later_named_draft() {
|
||||
let mut app = setup(Layer::Oa);
|
||||
send(&mut app, Event::Start(Kind::Entity, None));
|
||||
send(&mut app, Event::Name("First".into()));
|
||||
send(&mut app, Event::Submit);
|
||||
let stale = app.route_context().unwrap();
|
||||
send(&mut app, Event::Cancel);
|
||||
send(&mut app, Event::Start(Kind::Actor, None));
|
||||
send(&mut app, Event::Name("Second".into()));
|
||||
send(&mut app, Event::Submit);
|
||||
let before = app.document.bundle().clone();
|
||||
send(&mut app, Event::Click(stale, Position { x: 1., y: 1. }));
|
||||
assert_eq!(app.document.bundle(), &before);
|
||||
assert!(app.architecture.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn basic_transition_explicit_overview_focus_wins_over_an_already_populated_architecture() {
|
||||
use crate::workflow::{Event as W, Workflow};
|
||||
let mut p = Project::empty();
|
||||
let work = p
|
||||
.add_element(Kind::Activity, Layer::Oa, "Report status", None)
|
||||
.unwrap();
|
||||
let architecture = p.ensure_diagram(Layer::Sa);
|
||||
p.diagrams.get_mut(&architecture).unwrap().projection = ProjectionKind::Allocation;
|
||||
let mut first = Workflow::transition(&p, work).unwrap();
|
||||
let Workflow::Transition(t) = &first else {
|
||||
unreachable!()
|
||||
};
|
||||
let target = t
|
||||
.plan
|
||||
.changes
|
||||
.iter()
|
||||
.find(|c| c.source == work)
|
||||
.unwrap()
|
||||
.target;
|
||||
let mut p = first.update(&p, W::Apply).unwrap().unwrap();
|
||||
let mut overview = p.diagrams[&architecture].clone();
|
||||
overview.id = Id::new_v4();
|
||||
overview.alias = "chosen_overview".into();
|
||||
overview.name = "Chosen system overview".into();
|
||||
overview.projection = ProjectionKind::Overview;
|
||||
overview.nodes.clear();
|
||||
let chosen = overview.id;
|
||||
p.diagrams.insert(chosen, overview);
|
||||
let placement = p.place(chosen, target, Position { x: 823., y: 417. });
|
||||
let mut app = App::with_project(p);
|
||||
app.selected = work;
|
||||
app.active_diagram = Some(architecture);
|
||||
app.refresh_draft();
|
||||
let _ = app.update(Message::StartTransition);
|
||||
let _ = app.update(Message::Workflow(W::Destination(Choice {
|
||||
id: chosen,
|
||||
label: "Chosen system overview".into(),
|
||||
})));
|
||||
let before = app.project().diagrams.clone();
|
||||
let _ = app.update(Message::Workflow(W::Apply));
|
||||
assert!(app.workflow.is_none(), "{}", app.status);
|
||||
assert_eq!(app.active_diagram, Some(chosen));
|
||||
assert_eq!(app.selected, target);
|
||||
assert_eq!(app.selected_node, Some(placement));
|
||||
assert_eq!(app.project().diagrams, before);
|
||||
}
|
||||
Reference in New Issue
Block a user