Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 36 additions & 1 deletion apps/desktop/loopx-control-plane/src-tauri/src/maintenance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -603,6 +603,16 @@ fn pairing_details(bundled: &Value, installed: Option<&Value>, app_version: &str
// Shared App/runtime pairing gate for both release startup entrances: the
// journal-absent path and the start that just discarded a stale journal may
// connect only when the installed runtime pairs with the bundled snapshot.
// A runtime state that already published its own phase must not be relabelled
// by the supervisor's generic error publication: the boot surface renders the
// repair guidance and the operator decision by their own rules.
fn runtime_state_publishes_own_phase(error: &str) -> bool {
matches!(
error,
"runtime_setup_required" | "runtime_pairing_required"
)
}

fn require_paired_runtime(state: &Maintenance, app: &AppHandle) -> Result<(), String> {
let bundled = bundled_runtime::identity(app)?;
let installed =
Expand Down Expand Up @@ -728,7 +738,12 @@ pub fn start_services(app: &AppHandle) -> Result<Option<crate::services::Service
.get_or_init(Instant::now);
app.state::<Maintenance>().reconcile_services(|| {
if let Err(error) = resume_runtime(app) {
if error != "runtime_setup_required" {
// Runtime states publish the phase that explains them before they
// return: a missing runtime is the repair guidance, and a different
// installed runtime is the operator decision. Relabelling either as
// a generic error would replace the surface that offers the next
// step with a failure notice.
if !runtime_state_publishes_own_phase(&error) {
app.state::<Maintenance>()
.publish("error", json!({"code":error}));
}
Expand Down Expand Up @@ -1194,6 +1209,26 @@ mod tests {
);
}

#[test]
fn supervisor_keeps_the_phase_that_explains_a_runtime_state() {
// Both runtime states publish their own phase before resume_runtime
// returns. A generic error publication here would replace the repair
// guidance or the operator decision with a failure notice -- the
// decision would stop rendering its two choices entirely.
for owned in ["runtime_setup_required", "runtime_pairing_required"] {
assert!(runtime_state_publishes_own_phase(owned), "{owned}");
}
for relabelled in [
"runtime_identity_mismatch",
"runtime_install_exit_1",
"runtime_install_timeout",
"update_state_invalid",
"service_start_failed",
] {
assert!(!runtime_state_publishes_own_phase(relabelled), "{relabelled}");
}
}

#[test]
fn stale_journal_start_connects_only_through_the_pairing_gate() {
// Stale journal + paired App/runtime: the start may connect.
Expand Down
Loading