Integrate release recovery, semantic review, coverage and Windows packaging

This commit is contained in:
Codex
2026-09-06 16:31:55 -07:00
parent c091f80e6e
commit 2b2d2bc18b
174 changed files with 27635 additions and 127 deletions
+479
View File
@@ -0,0 +1,479 @@
//! Home navigation and asynchronous private checkpoints share the normal guards.
use super::*;
use crate::{home, recovery};
impl App {
pub(super) fn remember_local(&mut self) {
if let Some(path) = self.path.clone() {
let name = self.project().name.clone();
if let Err(error) = self.home.remember(&path, &name) {
self.home.notice = Some(error);
}
}
}
pub(super) fn home_event(&mut self, event: home::Event) -> Task<Message> {
match event {
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::Guide => self.update(Message::Page(Page::Guide)),
home::Event::Resume => self.update(Message::Page(self.last_workspace_page)),
home::Event::OpenRecent(path) => {
self.recent_target = Some(path);
self.handle(Message::Request(Pending::Recent))
}
home::Event::Recover(id) => self.handle(Message::Request(Pending::Recover(id))),
home::Event::Forget(path) => {
if let Err(error) = self.home.forget(&path) {
self.home.notice = Some(error);
}
Task::none()
}
home::Event::Refresh => {
self.home.refresh();
self.recovery.refresh();
self.recovery.entries.retain(|entry| {
entry.id != self.canvas_document && Some(entry.id) != self.recovery.restored
});
Task::none()
}
}
}
pub(super) fn checkpoint_tick(&mut self) -> Task<Message> {
if self.recovery.writing || self.busy {
return Task::none();
}
if !self.dirty() {
self.retire_checkpoint();
return Task::none();
}
let Some(directory) = self.recovery.directory.clone() else {
return Task::none();
};
let id = self.canvas_document;
let snapshot = match recovery::Snapshot::capture(
id,
&self.document,
&self.source_workspace,
self.draft.as_ref(),
&self.behavior,
self.path.clone(),
self.selected,
)
.and_then(|snapshot| snapshot.with_workflow(self.workflow.as_ref()))
{
Ok(snapshot) => snapshot,
Err(error) => {
self.recovery.warning = Some(error);
return Task::none();
}
};
self.recovery.writing = true;
Task::perform(
async move {
tokio::task::spawn_blocking(move || recovery::write(&directory, &snapshot))
.await
.map_err(|e| e.to_string())?
},
move |result| Message::CheckpointWritten(id, result),
)
}
pub(super) fn checkpoint_written(&mut self, id: Id, result: Result<(), String>) {
self.recovery.writing = false;
match result {
Ok(()) if id == self.canvas_document && self.dirty() => {
self.recovery.active = Some(id);
}
Ok(()) => {
if let Some(directory) = &self.recovery.directory
&& let Err(error) = recovery::remove(directory, id)
{
self.recovery.warning = Some(error);
}
}
Err(error) => {
self.recovery.warning = Some(format!(
"Recovery checkpoint failed: {error}. Save your work explicitly."
))
}
}
}
pub(super) fn retire_checkpoint(&mut self) {
let Some(directory) = &self.recovery.directory else {
return;
};
for id in [self.recovery.active.take(), self.recovery.restored.take()]
.into_iter()
.flatten()
{
if let Err(error) = recovery::remove(directory, id) {
self.recovery.warning = Some(error);
} else {
self.recovery.entries.retain(|e| e.id != id);
}
}
}
pub(super) fn load_checkpoint(&mut self, id: Id) -> Task<Message> {
let Some(directory) = self.recovery.directory.clone() else {
return Task::none();
};
self.busy = true;
Task::perform(
async move {
tokio::task::spawn_blocking(move || recovery::read(&directory, id).map(Arc::new))
.await
.map_err(|e| e.to_string())?
},
Message::CheckpointLoaded,
)
}
pub(super) fn checkpoint_loaded(
&mut self,
result: Result<Arc<recovery::Snapshot>, String>,
) -> Task<Message> {
self.busy = false;
let snapshot = match result {
Ok(s) => s,
Err(error) => {
self.status = error;
return Task::none();
}
};
let id = snapshot.id;
let restored = match (*snapshot).clone().restore() {
Ok(r) => r,
Err(error) => {
self.status = error;
return Task::none();
}
};
// Fully prepare recovery before touching the currently accepted document.
self.reset(restored.document.project().clone(), None);
self.hosted.leave_document();
self.document = restored.document;
self.selected = if self.project().layer_of(restored.selected).is_some()
|| restored.selected == self.project().id
{
restored.selected
} else {
self.project().id
};
// Installing the raw workspace must follow all accepted-source synchronization.
self.source_workspace = restored.workspace;
self.show_source_file(false);
self.refresh_draft();
if let Some(draft) = restored.property {
self.selected = draft.original;
self.exact_strings = draft.exact_strings;
self.draft = Some(draft);
}
self.behavior = restored.behavior;
self.workflow = restored.workflow;
let focus = self.behavior.pending_owner().unwrap_or(self.selected);
if let Some(layer) = self.project().layer_of(focus) {
self.layer = layer;
self.explorer_index = crate::explorer::Index::new(self.project(), layer);
self.active_diagram = self.diagram_for(layer, focus);
}
self.recovery.restored = Some(id);
self.recovery.entries.retain(|entry| entry.id != id);
self.page = if self.behavior.has_pending() {
Page::Behavior
} else if self.source_dirty() {
Page::Text
} 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.request_lint()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn home_resume_returns_to_each_actual_workspace_without_model_edits() {
let mut app = App::with_project(Project::demo());
let accepted = app.document.snapshot();
for page in [
Page::Diagram,
Page::Catalog,
Page::Behavior,
Page::Data,
Page::Traces,
Page::Text,
Page::Validation,
Page::Guide,
] {
let _ = app.update(Message::Page(page));
assert_eq!(app.page, page);
let selected = app.selected;
let _ = app.update(Message::Page(Page::Home));
assert_eq!(app.last_workspace_page, page);
// Repeated Home events must not replace the remembered workspace.
let _ = app.update(Message::Page(Page::Home));
let _ = app.update(Message::Home(home::Event::Resume));
assert_eq!(app.page, page);
assert_eq!(app.selected, selected);
assert!(Arc::ptr_eq(&accepted, &app.document.snapshot()));
}
}
#[test]
fn home_resume_keeps_state_view_while_explicit_architecture_remains_reachable() {
let mut project = Project::empty();
let owner = project
.add_element(Kind::System, Layer::Sa, "System", None)
.unwrap();
let machine = project
.add_element(Kind::StateMachine, Layer::Sa, "Modes", Some(owner))
.unwrap();
let plan = crate::behavior::state_edit::create_view(&project, machine).unwrap();
project = prepare_command(&project, &plan.command).unwrap().project;
let mut app = App::with_project(project);
assert_eq!(app.page, Page::Behavior);
let accepted = app.document.snapshot();
let _ = app.update(Message::Page(Page::Home));
let _ = app.update(Message::Home(home::Event::Resume));
assert_eq!(app.page, Page::Behavior);
assert_eq!(app.state_context().unwrap().diagram, plan.diagram);
assert_eq!(app.behavior.active_owner, Some(machine));
let _ = app.update(Message::Page(Page::Diagram));
assert_eq!(app.page, Page::Diagram);
assert!(app.active_diagram.is_none());
let _ = app.update(Message::Page(Page::Home));
let _ = app.update(Message::Home(home::Event::Resume));
assert_eq!(app.page, Page::Diagram);
assert!(Arc::ptr_eq(&accepted, &app.document.snapshot()));
}
#[test]
fn home_resume_retains_invalid_source_and_property_drafts() {
let mut app = App::with_project(Project::demo());
let accepted = app.document.snapshot();
let _ = app.update(Message::Page(Page::Text));
let raw = format!("{}\nunfinished draft", app.source_workspace.text());
app.source_workspace
.edit(raw.clone(), app.source_editor.cursor());
let _ = app.update(Message::Page(Page::Home));
let _ = app.update(Message::Home(home::Event::Resume));
assert_eq!(app.page, Page::Text);
assert_eq!(app.source_workspace.text(), raw);
assert!(Arc::ptr_eq(&accepted, &app.document.snapshot()));
let mut app = App::with_project(Project::demo());
let accepted = app.document.snapshot();
let _ = app.update(Message::Page(Page::Data));
let _ = app.update(Message::Field("/id".into(), "-".into()));
let raw = serde_json::to_value(app.draft.as_ref().unwrap().recovery_snapshot()).unwrap();
assert!(app.draft_dirty());
let _ = app.update(Message::Page(Page::Home));
let _ = app.update(Message::Home(home::Event::Resume));
assert_eq!(app.page, Page::Data);
assert_eq!(
serde_json::to_value(app.draft.as_ref().unwrap().recovery_snapshot()).unwrap(),
raw
);
assert!(Arc::ptr_eq(&accepted, &app.document.snapshot()));
}
fn retained_snapshot(app: &App) -> recovery::Snapshot {
recovery::Snapshot::capture(
app.canvas_document,
&app.document,
&app.source_workspace,
app.draft.as_ref(),
&app.behavior,
app.path.clone(),
app.selected,
)
.unwrap()
}
#[test]
fn saving_an_older_snapshot_keeps_recovery_for_newer_unsaved_work() {
let directory = tempfile::tempdir().unwrap();
let mut app = App::with_project(Project::demo());
app.recovery.directory = Some(directory.path().join("recovery"));
let id = app.project().id;
app.execute(Command::Rename(id, "Saved milestone".into()));
let path = directory.path().join(&app.document.bundle().entry);
let receipt = write_snapshot(
app.canvas_document,
app.document.revision(),
app.document.snapshot(),
path,
None,
)
.unwrap();
app.execute(Command::Rename(id, "Later unsaved engineering".into()));
let snapshot = retained_snapshot(&app);
let recovery_directory = app.recovery.directory.clone().unwrap();
recovery::write(&recovery_directory, &snapshot).unwrap();
app.recovery.active = Some(app.canvas_document);
let _ = app.update(Message::Saved(Ok(Some(receipt))));
assert!(app.dirty());
assert_eq!(app.project().name, "Later unsaved engineering");
assert_eq!(app.recovery.active, Some(app.canvas_document));
let restored = recovery::read(&recovery_directory, app.canvas_document)
.unwrap()
.restore()
.unwrap();
assert_eq!(
restored.document.project().name,
"Later unsaved engineering"
);
}
#[test]
fn checkpoint_completion_releases_the_writer_while_a_context_menu_is_open() {
let directory = tempfile::tempdir().unwrap();
let mut app = App::with_project(Project::demo());
app.recovery.directory = Some(directory.path().into());
let id = app.project().id;
app.execute(Command::Rename(id, "Unsaved engineering".into()));
let snapshot = retained_snapshot(&app);
recovery::write(directory.path(), &snapshot).unwrap();
app.recovery.writing = true;
let _ = app.update(Message::Menu(crate::model_menu::Event::Open(
crate::model_menu::Request::object(id, iced::Point::ORIGIN),
)));
assert!(app.model_menu.is_some());
let _ = app.update(Message::CheckpointWritten(app.canvas_document, Ok(())));
assert!(
!app.recovery.writing,
"A passive completion cannot be swallowed by an open menu"
);
assert_eq!(app.recovery.active, Some(app.canvas_document));
assert!(
app.model_menu.is_some(),
"Saving a checkpoint does not dismiss the menu"
);
assert_eq!(app.project().name, "Unsaved engineering");
}
#[test]
fn discard_and_close_waits_for_the_late_checkpoint_then_retires_it() {
let directory = tempfile::tempdir().unwrap();
let mut app = App::with_project(Project::demo());
app.recovery.directory = Some(directory.path().into());
app.execute(Command::Rename(
app.project().id,
"Discarded on close".into(),
));
let snapshot = retained_snapshot(&app);
let document = app.canvas_document;
app.recovery.writing = true;
let window = window::Id::unique();
let _ = app.update(Message::Request(Pending::Close(window)));
assert!(matches!(app.pending, Some(Pending::Close(id)) if id == window));
let closing = app.update(Message::Discard);
assert!(
iced_runtime::task::into_stream(closing).is_none(),
"The window must remain until its outstanding writer finishes"
);
assert_eq!(app.checkpoint_close, Some(window));
assert!(app.busy);
recovery::write(directory.path(), &snapshot).unwrap();
assert!(recovery::read(directory.path(), document).is_ok());
let close = app.update(Message::CheckpointWritten(document, Ok(())));
assert!(
iced_runtime::task::into_stream(close).is_some(),
"Close is emitted after checkpoint cleanup"
);
assert!(app.checkpoint_close.is_none());
assert!(recovery::read(directory.path(), document).is_err());
assert!(app.recovery.active.is_none());
}
#[test]
fn recent_and_recovery_navigation_respect_unsaved_decision() {
let mut app = App::with_project(Project::demo());
let id = app.project().id;
app.execute(Command::Rename(id, "Retained work".into()));
let _ = app.update(Message::Home(home::Event::OpenRecent(PathBuf::from(
"missing.arcadia",
))));
assert!(matches!(app.pending, Some(Pending::Recent)));
assert!(!app.busy);
assert_eq!(app.project().name, "Retained work");
let _ = app.update(Message::Cancel);
let recovery_id = Id::new_v4();
let _ = app.update(Message::Home(home::Event::Recover(recovery_id)));
assert!(matches!(app.pending,Some(Pending::Recover(id)) if id==recovery_id));
assert!(!app.busy);
}
#[test]
fn restored_copy_keeps_raw_property_and_requires_save_without_local_or_hosted_binding() {
let mut app = App::with_project(Project::demo());
let selected = app.project().id;
app.draft
.as_mut()
.unwrap()
.input("/name", "Not yet applied".into());
let snapshot = recovery::Snapshot::capture(
Id::new_v4(),
&app.document,
&app.source_workspace,
app.draft.as_ref(),
&app.behavior,
Some(PathBuf::from("original.arcadia")),
selected,
)
.unwrap();
let checkpoint_id = snapshot.id;
app.recovery.entries.push(recovery::Entry {
id: checkpoint_id,
name: "Retained work".into(),
origin: None,
modified: snapshot.modified,
});
let _ = app.checkpoint_loaded(Ok(Arc::new(snapshot)));
assert!(app.path.is_none());
assert!(app.disk_project.is_none());
assert!(app.hosted.session.is_none());
assert!(app.document.dirty());
assert!(app.draft_dirty());
assert_eq!(app.recovery.restored, Some(checkpoint_id));
assert!(
app.recovery.entries.is_empty(),
"Home must not offer the copy currently being recovered again"
);
assert_eq!(
app.draft.as_ref().unwrap().inputs["/name"].raw,
"Not yet applied"
);
assert_ne!(app.project().name, "Not yet applied");
}
#[test]
fn stale_write_completion_cannot_recover_a_discarded_document() {
let directory = tempfile::tempdir().unwrap();
let mut app = App::with_project(Project::demo());
app.recovery.directory = Some(directory.path().into());
let old = app.canvas_document;
let s = recovery::Snapshot::capture(
old,
&app.document,
&app.source_workspace,
None,
&app.behavior,
None,
app.selected,
)
.unwrap();
app.reset(Project::empty(), None);
recovery::write(directory.path(), &s).unwrap();
app.checkpoint_written(old, Ok(()));
assert!(recovery::read(directory.path(), old).is_err());
assert!(app.recovery.active.is_none());
}
#[test]
fn malformed_checkpoint_does_not_replace_the_current_model() {
let mut app = App::with_project(Project::demo());
let original = app.document.bundle().clone();
let _ = app.checkpoint_loaded(Err("Damaged checkpoint".into()));
assert_eq!(app.document.bundle(), &original);
assert!(app.status.contains("Damaged"));
}
}
+114
View File
@@ -0,0 +1,114 @@
use super::*;
fn complete_save(app: &mut App, path: PathBuf) {
let receipt = write_snapshot(
app.canvas_document,
app.document.revision(),
app.document.snapshot(),
path,
app.disk_project.clone(),
)
.unwrap();
let _ = app.update(Message::Saved(Ok(Some(receipt))));
}
#[test]
fn save_as_receipt_then_one_undo_restores_meaning_and_saving_preserves_redo() {
let directory = tempfile::tempdir().unwrap();
let path = directory.path().join("semantic-accepted.arcadia");
let mut app = App::with_project(Project::empty());
app.document.mark_recovered();
let original = app.document.bundle().files[&app.document.bundle().entry].clone();
app.execute(Command::Rename(
app.project().id,
"Accepted semantic change".into(),
));
let after = app.document.bundle().files[&app.document.bundle().entry].clone();
complete_save(&mut app, path.clone());
assert!(!app.dirty());
assert_eq!(app.path.as_ref(), Some(&path));
assert_eq!(std::fs::read_to_string(&path).unwrap(), after);
let _ = app.update(Message::Undo);
assert_eq!(app.document.bundle().entry, "semantic-accepted.arcadia");
assert_eq!(
app.document.bundle().files[&app.document.bundle().entry],
original
);
assert!(
!app.document.can_undo(),
"Save as did not insert a history step"
);
assert!(app.document.can_redo());
complete_save(&mut app, path.clone());
assert_eq!(std::fs::read_to_string(&path).unwrap(), original);
assert!(
app.document.can_redo(),
"Saving an undone state preserves the future"
);
let _ = app.update(Message::Redo);
assert_eq!(
app.document.bundle().files[&app.document.bundle().entry],
after
);
assert!(app.dirty());
assert_eq!(
source_project::compile_bundle(app.document.bundle(), LIMITS)
.unwrap()
.decoded
.project,
*app.project()
);
}
#[test]
fn save_as_preflight_rejects_historical_collision_before_starting_io() {
let directory = tempfile::tempdir().unwrap();
let path = directory.path().join("taken.arcadia");
let mut app = App::with_project(Project::empty());
let base = app.document.bundle().clone();
let mut included = base.clone();
let text = included.files.get_mut(&included.entry).unwrap();
text.insert_str(text.rfind('}').unwrap(), "\n include \"taken.arcadia\";\n");
included
.files
.insert("taken.arcadia".into(), "// retained\n".into());
app.document.apply_bundle(included).unwrap();
app.document.apply_bundle(base).unwrap();
app.sync_source();
let before = app.document.snapshot();
let task = app.save_to(path.clone());
assert!(iced_runtime::task::into_stream(task).is_none());
assert!(!path.exists());
assert!(!app.busy);
assert!(app.status.contains("Undo/Redo"));
assert!(Arc::ptr_eq(&before, &app.document.snapshot()));
let json = app.save_to(directory.path().join("taken.json"));
assert!(
iced_runtime::task::into_stream(json).is_some(),
"JSON exports do not relocate source entries"
);
}
#[test]
fn save_as_late_receipt_preserves_newer_raw_source_draft_and_destination() {
let directory = tempfile::tempdir().unwrap();
let mut app = App::with_project(Project::empty());
let entry = app.document.bundle().entry.clone();
let receipt = write_snapshot(
app.canvas_document,
app.document.revision(),
app.document.snapshot(),
directory.path().join("accepted.arcadia"),
None,
)
.unwrap();
let raw = format!("{}\nunfinished {{", app.source_workspace.text());
let cursor = app.source_workspace.files[&entry].cursor;
app.source_workspace.edit(raw.clone(), cursor);
let _ = app.update(Message::Saved(Ok(Some(receipt))));
assert_eq!(app.source_workspace.text(), raw);
assert_eq!(app.document.bundle().entry, entry);
assert!(app.path.is_none());
assert!(app.dirty());
assert!(app.status.contains("Newer edits"));
}
@@ -6,6 +6,41 @@ use crate::behavior::{
};
impl App {
/// A saved state view has one renderer and tool set: the behavior workspace.
/// Run after navigation/adoption, never as a model or property edit. Keeping
/// selection and its Draft intact also preserves unrelated raw properties.
pub(super) fn route_state_diagram_workspace(&mut self) {
if self.page != Page::Diagram {
return;
}
let Some((diagram, machine, layer)) = self.active_diagram.and_then(|id| {
let view = self.project().diagrams.get(&id)?;
Some((id, view.projection.state_machine()?, view.layer))
}) else {
return;
};
// Existing navigation guards own pending behavior forms. Never replace
// their owner/camera if an exceptional caller leaves one on another page.
if self.behavior.has_pending() {
return;
}
self.layer = layer;
self.page = Page::Behavior;
self.behavior.active_owner = Some(machine);
self.behavior.diagram = behavior::state_view::State {
diagram: Some(diagram),
placement: self.project().diagrams[&diagram]
.nodes
.values()
.find(|node| node.id == self.selected || node.element == self.selected)
.map(|node| node.id),
..Default::default()
};
self.selected_node = None;
self.canvas_epoch = self.canvas_epoch.wrapping_add(1);
self.fit_state_diagram();
}
/// The behavior view resolved independently of the current page. Capture
/// this before selection/layer changes, while it still identifies the view
/// whose camera and tool are retained during navigation.
@@ -89,6 +89,153 @@ fn assert_source_model_agree(app: &App) {
assert!(!app.source_dirty());
}
#[test]
fn state_diagram_routes_constructor_and_source_open_to_the_existing_workspace_without_edits() {
let mut f = fixture();
let diagram = create_view(&mut f).diagram;
let project = f.app.project().clone();
let app = App::with_project(project.clone());
assert_eq!(app.page, Page::Behavior);
assert_eq!(app.state_context().unwrap().diagram, diagram);
assert_eq!(behavior::selected_owner(&app), Some(f.machine));
assert_eq!(app.behavior.diagram.tool, Tool::Select);
assert_eq!(app.selected, project.id);
assert_eq!(app.project(), &project);
assert!(!app.document.can_undo());
let directory = tempfile::tempdir().unwrap();
let path = directory.path().join("state-view.arcadia");
let text = format!(
"// Exact authored source must remain unchanged.\n{}",
language::format(&project)
);
std::fs::write(&path, &text).unwrap();
let mut reopened = App::with_project(Project::empty());
send(
&mut reopened,
Message::Loaded(Ok(Some(read_snapshot(path.clone()).unwrap()))),
);
assert_eq!(reopened.page, Page::Behavior);
assert_eq!(reopened.state_context().unwrap().diagram, diagram);
assert_eq!(reopened.project(), &project);
assert_eq!(
reopened.document.bundle().files[&reopened.document.bundle().entry],
text
);
assert_eq!(std::fs::read_to_string(path).unwrap(), text);
assert!(!reopened.document.can_undo());
assert_source_model_agree(&reopened);
}
#[test]
fn state_diagram_routes_picker_and_reveal_but_explicit_architecture_does_not_bounce() {
let mut f = fixture();
let diagram = create_view(&mut f).diagram;
let mut project = f.app.project().clone();
let second = state_edit::create_view(&project, f.machine).unwrap();
project = prepare_command(&project, &second.command).unwrap().project;
let transition = project.relate(RelationKind::StateTransition, "Switch", f.a, f.b);
let mut app = App::with_project(project.clone());
let before = app.document.snapshot();
send(&mut app, Message::Page(Page::Diagram));
assert_eq!(app.page, Page::Diagram);
assert_eq!(app.active_diagram, None);
send(
&mut app,
Message::Diagram(Choice {
id: second.diagram,
label: "Second state view".into(),
}),
);
assert_eq!(app.page, Page::Behavior);
assert_eq!(app.state_context().unwrap().diagram, second.diagram);
send(&mut app, Message::RevealInDiagram(diagram, transition));
assert_eq!(app.page, Page::Behavior);
assert_eq!(app.state_context().unwrap().diagram, diagram);
assert_eq!(app.selected, transition);
assert_eq!(app.draft.as_ref().unwrap().original, transition);
assert!(Arc::ptr_eq(&before, &app.document.snapshot()));
assert_source_model_agree(&app);
let architecture = project.ensure_diagram(Layer::Sa);
let mut app = App::with_project(project);
send(
&mut app,
Message::Diagram(Choice {
id: diagram,
label: "State view".into(),
}),
);
assert_eq!(app.page, Page::Behavior);
send(&mut app, Message::Page(Page::Diagram));
assert_eq!(app.page, Page::Diagram);
assert_eq!(app.active_diagram, Some(architecture));
assert!(app.state_context().is_none());
assert!(!app.document.can_undo());
}
#[test]
fn state_diagram_routes_preserve_selected_raw_properties_and_pending_behavior_guards() {
let mut f = fixture();
let diagram = create_view(&mut f).diagram;
let mut app = App::with_project(f.app.project().clone());
send(&mut app, Message::Select(f.a));
send(
&mut app,
Message::Field("/alias".into(), "invalid alias!".into()),
);
let raw = serde_json::to_value(app.draft.as_ref().unwrap().recovery_snapshot()).unwrap();
assert!(!app.apply());
let before = app.document.snapshot();
// Exercise normalization itself with an existing raw draft, including callers
// that have just adopted a view without a successful property Apply.
app.page = Page::Diagram;
app.route_state_diagram_workspace();
assert_eq!(app.page, Page::Behavior);
assert_eq!(app.selected, f.a);
assert_eq!(
serde_json::to_value(app.draft.as_ref().unwrap().recovery_snapshot()).unwrap(),
raw
);
assert!(Arc::ptr_eq(&before, &app.document.snapshot()));
send(
&mut app,
Message::Diagram(Choice {
id: diagram,
label: "State view".into(),
}),
);
assert_eq!(
serde_json::to_value(app.draft.as_ref().unwrap().recovery_snapshot()).unwrap(),
raw
);
assert!(Arc::ptr_eq(&before, &app.document.snapshot()));
let mut app = App::with_project(f.app.project().clone());
form(
&mut app,
states::Event::Start(f.machine, states::Tool::Transition, f.a),
);
form(
&mut app,
states::Event::Name("Pending behavior stays".into()),
);
let pending = app.behavior.recovery_snapshot().unwrap();
let context = app.state_context().unwrap();
send(&mut app, Message::Page(Page::Diagram));
send(
&mut app,
Message::Diagram(Choice {
id: diagram,
label: "State view".into(),
}),
);
assert_eq!(app.page, Page::Behavior);
assert_eq!(app.state_context().unwrap().diagram, context.diagram);
assert_eq!(app.behavior.recovery_snapshot().unwrap(), pending);
assert!(!app.document.can_undo());
}
struct NavigationFixture {
app: App,
machine_a: Id,
@@ -789,12 +936,15 @@ fn fallback_owner_first_selection_keeps_the_captured_context_for_its_first_move(
let f = fixture();
let mut project = f.app.project().clone();
// One eligible machine makes the fallback deterministic. Start from a loaded
// project with no selected behavior element and no explicitly chosen owner.
// architecture view with no selected behavior element or chosen owner. A
// state-only project now opens its saved view with an explicit owner.
project
.elements
.retain(|id, element| f.members.contains(id) || element.kind() == Kind::System);
let plan = state_edit::create_view(&project, f.machine).unwrap();
let project = prepare_command(&project, &plan.command).unwrap().project;
let mut project = prepare_command(&project, &plan.command).unwrap().project;
let architecture = project.ensure_diagram(Layer::Sa);
project.diagrams.get_mut(&architecture).unwrap().projection = ProjectionKind::Allocation;
let mut app = App::with_project(project);
assert_eq!(app.selected, app.project().id);
assert!(app.behavior.active_owner.is_none());
@@ -917,7 +1067,11 @@ fn shared_semantic_states_keep_architecture_and_behavior_placements_independent(
id: architecture,
label: app.project().named(architecture).into(),
};
// The initial UUID-ordered view may be a state view, which correctly opens
// Behavior. Enter Architecture explicitly before testing its selection.
send(&mut app, Message::Page(Page::Diagram));
send(&mut app, Message::Diagram(architecture_choice));
assert_eq!(app.page, Page::Diagram);
send(&mut app, Message::Select(f.b));
assert_eq!(app.active_diagram, Some(architecture));
assert_eq!(app.selected_node, Some(arch_b));