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
+206
View File
@@ -556,7 +556,72 @@ pub struct State {
states: states::State,
}
const PENDING_DRAFT: &str = "Apply or cancel the current behavior form before starting another.";
#[derive(serde::Serialize, serde::Deserialize)]
#[serde(deny_unknown_fields)]
struct RecoveryDraft {
version: u32,
kind: RecoveryKind,
draft: serde_json::Value,
}
#[derive(serde::Serialize, serde::Deserialize)]
enum RecoveryKind {
State,
Scenario,
Process,
}
impl State {
/// Capture only the pending authored form. Pointer gestures, cached choices,
/// viewport geometry and document/revision tokens never cross a restart.
pub fn recovery_snapshot(&self) -> Result<Option<serde_json::Value>, String> {
let drafts = [
(RecoveryKind::State, self.states.recovery_snapshot()?),
(RecoveryKind::Scenario, self.scenario.recovery_snapshot()?),
(RecoveryKind::Process, self.topology.recovery_snapshot()?),
];
let mut drafts = drafts.into_iter().filter_map(|(kind, draft)| {
draft.map(|draft| RecoveryDraft {
version: 1,
kind,
draft,
})
});
let first = drafts.next();
if drafts.next().is_some() {
return Err(
"Several behavior forms are pending; recovery cannot choose one to discard.".into(),
);
}
first
.map(serde_json::to_value)
.transpose()
.map_err(|e| e.to_string())
}
/// Restore raw fields without planning/executing them. Rejected values and
/// incomplete endpoints must remain available for repair after a crash.
pub fn restore_recovery(&mut self, value: serde_json::Value) -> Result<(), String> {
let saved: RecoveryDraft = serde_json::from_value(value).map_err(|e| e.to_string())?;
if saved.version != 1 {
return Err(format!(
"Unsupported behavior recovery version {}",
saved.version
));
}
let mut restored = Self::default();
match saved.kind {
RecoveryKind::State => restored.states = states::State::from_recovery(saved.draft)?,
RecoveryKind::Scenario => {
restored.scenario = scenario::State::from_recovery(saved.draft)?
}
RecoveryKind::Process => {
restored.topology = topology::State::from_recovery(saved.draft)?
}
}
restored.active_owner = restored.pending_owner();
*self = restored;
Ok(())
}
pub fn has_pending(&self) -> bool {
self.pending_owner().is_some()
}
@@ -2352,6 +2417,147 @@ mod direct_canvas_tests {
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn recovery_retains_each_incomplete_behavior_form_without_executing_it() {
let p = Project::demo();
let before = p.clone();
let owner = |kind| p.elements.values().find(|e| e.kind() == kind).unwrap().id;
let machine = owner(Kind::StateMachine);
let scenario = owner(Kind::Scenario);
let process = owner(Kind::Process);
let cases = [
(
machine,
vec![
Event::States(states::Event::Start(
machine,
states::Tool::Transition,
machine,
)),
Event::States(states::Event::Name("Unfinished timeout".into())),
Event::States(states::Event::Timed(true)),
Event::States(states::Event::Expression("-".into())),
Event::States(states::Event::Guard("temperature >".into())),
],
),
(
scenario,
vec![
Event::Scenario(scenario::Event::Start(scenario, scenario::Tool::Message)),
Event::Scenario(scenario::Event::Name("Unfinished request".into())),
Event::Scenario(scenario::Event::Expression("-".into())),
],
),
(
process,
vec![
Event::Topology(topology::Event::Start(
process,
topology::Tool::Connect,
None,
)),
Event::Topology(topology::Event::Name("Unfinished path".into())),
Event::Topology(topology::Event::Guard("available &&".into())),
],
),
];
for (owner, events) in cases {
let mut original = State::default();
for event in events {
original.update(&p, event).unwrap();
}
assert!(original.prepare_pending(&p).is_err());
let snapshot = original.recovery_snapshot().unwrap().unwrap();
let bytes = serde_json::to_vec(&snapshot).unwrap();
let mut recovered = State::default();
recovered
.restore_recovery(serde_json::from_slice(&bytes).unwrap())
.unwrap();
assert_eq!(recovered.pending_owner(), Some(owner));
assert_eq!(recovered.active_owner, Some(owner));
assert!(
recovered.prepare_pending(&p).is_err(),
"An incomplete recovered form is not applied or silently repaired"
);
assert_eq!(recovered.recovery_snapshot().unwrap(), Some(snapshot));
assert_eq!(p, before);
recovered.acknowledge_commit();
assert!(recovered.recovery_snapshot().unwrap().is_none());
}
}
#[test]
fn recovery_preserves_distinct_qualified_value_paths_and_rejects_invalid_payload_atomically() {
let p = Project::demo();
let scenario = p
.elements
.values()
.find(|e| e.kind() == Kind::Scenario)
.unwrap()
.id;
let field = Id::new_v4();
let paths = [Id::new_v4(), Id::new_v4()];
let mut state = State::default();
state
.update(
&p,
Event::Scenario(scenario::Event::Start(scenario, scenario::Tool::Message)),
)
.unwrap();
for (path, value) in paths.into_iter().zip(["-", "3.4e"]) {
state
.update(
&p,
Event::Scenario(scenario::Event::Value(
crate::data_values::FieldPathKey {
path: vec![path],
field,
},
value.into(),
)),
)
.unwrap();
}
let original = state.recovery_snapshot().unwrap().unwrap();
let mut recovered = State::default();
recovered.restore_recovery(original.clone()).unwrap();
assert_eq!(
recovered.recovery_snapshot().unwrap(),
Some(original.clone())
);
let values = original["draft"]["values"].as_array().unwrap();
assert_eq!(values.len(), 2);
assert_ne!(values[0][0], values[1][0]);
assert_eq!(values[0][1], values[1][1]);
for bad in [
{
let mut value = original.clone();
value["version"] = 99.into();
value
},
{
let mut value = original.clone();
value["draft"]["unknown"] = true.into();
value
},
{
let mut value = original.clone();
value["draft"]["values"]
.as_array_mut()
.unwrap()
.push(values[0].clone());
value
},
] {
assert!(recovered.restore_recovery(bad).is_err());
assert_eq!(
recovered.recovery_snapshot().unwrap(),
Some(original.clone()),
"Malformed recovery cannot discard the currently retained form"
);
}
}
use crate::document::{Document, DocumentError};
use oa_core::source_project::{self, ProjectLimits};