* Harden Tauri backend preflight and startup
Require managed Studio root IDs to match before attaching to existing backends, close the concurrent backend-start window, and tighten frontend Tauri detection to Tauri-specific signals.
* Add Tauri backend manageability guards
Gate desktop backend compatibility on explicit manageability fields, add external-conflict handling for unsafe backend states, and protect update/repair paths from mutating active non-owned Studio backends. Track Tauri-owned backends with local owner metadata for verified orphan cleanup only.
* Split Tauri preflight probes into modules
Move preflight types, version checks, managed install probing, and backend probing into focused submodules while preserving behavior and keeping implementation files under the release-readiness size target.
* Use desktop-specific Tauri updater channel
Point the desktop updater at a same-repo desktop-latest manifest and publish that channel from non-draft desktop releases after validating the Tauri-generated latest.json.
* Add Linux desktop update policy
* Add owned backend lifecycle guards
* Adopt verified desktop-owned backends
* Validate desktop backend readiness
* Trim Tauri release hardening code
* Require desktop backend 2026.5.3
* Handle desktop backend edge cases
* Fail stalled desktop backend startup
* Fix desktop update edge cases
* Avoid secret-gating adopted watchdog
* Fix desktop update comparison guards
* Automate desktop release versioning
* Serialize desktop release workflow
* tests: follow preflight.rs split into preflight/{backend,managed,types,version}.rs
PR #5341 splits studio/src-tauri/src/preflight.rs into a directory of
submodules. The cmd.env_remove("UNSLOTH_STUDIO_HOME") + STUDIO_HOME
calls now live in preflight/managed.rs instead of preflight.rs, so
test_tauri_preflight_scrubs_studio_home_env counted zero matches in
the old single-file location and failed with "assert 0 >= 2".
Read whichever shape is on disk: preflight.rs at the old path plus
every *.rs under preflight/ (current PR has 2 occurrences in
preflight/managed.rs). The guard intent is unchanged: at least 2
env_remove calls covering run_cli_probe and probe_cli_capability,
plus the single commands.rs scrub in check_install_status. Verified
locally: pytest tests/test_studio_install_workspace_guard.py::test_tauri_preflight_scrubs_studio_home_env passes.
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* Avoid browser Tauri hostname detection
* Restore shutdown flag after failed stop
---------
Co-authored-by: Daniel Han <danielhanchen@gmail.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
769 lines
26 KiB
Rust
769 lines
26 KiB
Rust
mod backend;
|
|
mod managed;
|
|
mod types;
|
|
mod version;
|
|
|
|
use crate::desktop_backend_owner::{
|
|
OwnedBackendProbe, OwnedBackendReadiness, VerifiedOwnedBackend,
|
|
};
|
|
use backend::probe_existing_backends;
|
|
use log::warn;
|
|
pub use managed::managed_install_ready;
|
|
use managed::probe_managed_install;
|
|
use std::path::PathBuf;
|
|
use types::{BackendProbe, ManagedProbe};
|
|
pub use types::{DesktopPreflightDisposition, DesktopPreflightResult, ExternalBackendConflict};
|
|
pub(crate) use version::{
|
|
backend_version_stale_reason, DESKTOP_MANAGEABILITY_VERSION, DESKTOP_PROTOCOL_VERSION,
|
|
};
|
|
|
|
#[cfg(test)]
|
|
use backend::{backend_desktop_auth_status, backend_health};
|
|
#[cfg(test)]
|
|
use managed::probe_managed_bin;
|
|
#[cfg(test)]
|
|
use version::{backend_version_compatible, MIN_DESKTOP_BACKEND_VERSION};
|
|
|
|
fn release_auto_repair() -> bool {
|
|
!cfg!(debug_assertions)
|
|
}
|
|
|
|
fn managed_bin_for_result(managed: &ManagedProbe) -> Option<PathBuf> {
|
|
match managed {
|
|
ManagedProbe::Ready { bin } | ManagedProbe::Stale { bin, .. } => Some(bin.clone()),
|
|
ManagedProbe::Missing => None,
|
|
}
|
|
}
|
|
|
|
fn choose_preflight(managed: ManagedProbe, backend: BackendProbe) -> DesktopPreflightResult {
|
|
match (backend, managed) {
|
|
(BackendProbe::ExternalConflict { port, reason }, managed) => DesktopPreflightResult {
|
|
disposition: DesktopPreflightDisposition::ExternalConflict,
|
|
reason: Some(reason),
|
|
port: Some(port),
|
|
can_auto_repair: false,
|
|
managed_bin: managed_bin_for_result(&managed),
|
|
},
|
|
(BackendProbe::Ready { port }, managed) => DesktopPreflightResult {
|
|
disposition: DesktopPreflightDisposition::AttachedReady,
|
|
reason: None,
|
|
port: Some(port),
|
|
can_auto_repair: false,
|
|
managed_bin: managed_bin_for_result(&managed),
|
|
},
|
|
(_, managed) => match managed {
|
|
ManagedProbe::Ready { bin } => DesktopPreflightResult {
|
|
disposition: DesktopPreflightDisposition::ManagedReady,
|
|
reason: None,
|
|
port: None,
|
|
can_auto_repair: false,
|
|
managed_bin: Some(bin),
|
|
},
|
|
ManagedProbe::Stale { bin, reason } => DesktopPreflightResult {
|
|
disposition: DesktopPreflightDisposition::ManagedStale,
|
|
reason: Some(reason),
|
|
port: None,
|
|
can_auto_repair: release_auto_repair(),
|
|
managed_bin: Some(bin),
|
|
},
|
|
ManagedProbe::Missing => DesktopPreflightResult {
|
|
disposition: DesktopPreflightDisposition::NotInstalled,
|
|
reason: None,
|
|
port: None,
|
|
can_auto_repair: false,
|
|
managed_bin: None,
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
fn owned_unmanageable_reason(reason: &str) -> String {
|
|
format!("desktop_owned_backend_unmanageable:{reason}")
|
|
}
|
|
|
|
fn choose_owned_preflight(
|
|
managed: &ManagedProbe,
|
|
owned: &VerifiedOwnedBackend,
|
|
) -> DesktopPreflightResult {
|
|
match &owned.readiness {
|
|
OwnedBackendReadiness::Ready => DesktopPreflightResult {
|
|
disposition: DesktopPreflightDisposition::OwnedReady,
|
|
reason: None,
|
|
port: Some(owned.port),
|
|
can_auto_repair: false,
|
|
managed_bin: managed_bin_for_result(managed),
|
|
},
|
|
OwnedBackendReadiness::Stale { reason } => DesktopPreflightResult {
|
|
disposition: DesktopPreflightDisposition::OwnedStale,
|
|
reason: Some(reason.clone()),
|
|
port: Some(owned.port),
|
|
can_auto_repair: release_auto_repair(),
|
|
managed_bin: managed_bin_for_result(managed),
|
|
},
|
|
}
|
|
}
|
|
|
|
fn choose_unmanageable_owned_preflight(
|
|
managed: &ManagedProbe,
|
|
port: u16,
|
|
reason: String,
|
|
) -> DesktopPreflightResult {
|
|
DesktopPreflightResult {
|
|
disposition: DesktopPreflightDisposition::ExternalConflict,
|
|
reason: Some(owned_unmanageable_reason(&reason)),
|
|
port: Some(port),
|
|
can_auto_repair: false,
|
|
managed_bin: managed_bin_for_result(managed),
|
|
}
|
|
}
|
|
|
|
fn choose_owned_transitional_preflight(
|
|
managed: &ManagedProbe,
|
|
port: Option<u16>,
|
|
) -> DesktopPreflightResult {
|
|
DesktopPreflightResult {
|
|
disposition: DesktopPreflightDisposition::ExternalConflict,
|
|
reason: Some("desktop_owned_backend_starting".to_string()),
|
|
port,
|
|
can_auto_repair: false,
|
|
managed_bin: managed_bin_for_result(managed),
|
|
}
|
|
}
|
|
|
|
fn choose_ownerless_spawned_preflight(
|
|
managed: &ManagedProbe,
|
|
backend: &BackendProbe,
|
|
port: Option<u16>,
|
|
) -> DesktopPreflightResult {
|
|
match (port, backend) {
|
|
(Some(owned_port), BackendProbe::Ready { port }) if owned_port == *port => {
|
|
DesktopPreflightResult {
|
|
disposition: DesktopPreflightDisposition::OwnedReady,
|
|
reason: None,
|
|
port: Some(*port),
|
|
can_auto_repair: false,
|
|
managed_bin: managed_bin_for_result(managed),
|
|
}
|
|
}
|
|
(Some(owned_port), BackendProbe::Old { port, reason }) if owned_port == *port => {
|
|
DesktopPreflightResult {
|
|
disposition: DesktopPreflightDisposition::OwnedStale,
|
|
reason: Some(reason.clone()),
|
|
port: Some(*port),
|
|
can_auto_repair: release_auto_repair(),
|
|
managed_bin: managed_bin_for_result(managed),
|
|
}
|
|
}
|
|
_ => choose_owned_transitional_preflight(managed, port),
|
|
}
|
|
}
|
|
|
|
fn mutation_blocker_from_probe(probe: BackendProbe) -> Option<ExternalBackendConflict> {
|
|
match probe {
|
|
BackendProbe::ExternalConflict { port, reason } => {
|
|
Some(ExternalBackendConflict { port, reason })
|
|
}
|
|
BackendProbe::Ready { port } => Some(ExternalBackendConflict {
|
|
port,
|
|
reason: "same_root_external_backend_active".to_string(),
|
|
}),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
pub async fn mutation_blocking_backend_ignoring(
|
|
ignored_ports: &[u16],
|
|
) -> Option<ExternalBackendConflict> {
|
|
mutation_blocker_from_probe(probe_existing_backends(ignored_ports).await)
|
|
}
|
|
|
|
pub async fn desktop_preflight_result() -> DesktopPreflightResult {
|
|
let (managed, backend) = tokio::join!(probe_managed_install(), probe_existing_backends(&[]));
|
|
choose_preflight(managed, backend)
|
|
}
|
|
|
|
pub async fn desktop_preflight_result_with_state(
|
|
state: &crate::process::BackendState,
|
|
) -> Result<(DesktopPreflightResult, Option<(u64, bool)>), String> {
|
|
let (managed, backend, owned) = tokio::join!(
|
|
probe_managed_install(),
|
|
probe_existing_backends(&[]),
|
|
crate::desktop_backend_owner::probe_verified_owned_backend()
|
|
);
|
|
|
|
if let Some(snapshot) = crate::process::owned_backend_snapshot(state)? {
|
|
let Some(owner) = snapshot.owner.clone() else {
|
|
let probe = match snapshot.port {
|
|
Some(port) => backend::probe_ownerless_spawned_backend(port).await,
|
|
None => backend,
|
|
};
|
|
return Ok((
|
|
choose_ownerless_spawned_preflight(&managed, &probe, snapshot.port),
|
|
None,
|
|
));
|
|
};
|
|
match crate::desktop_backend_owner::probe_owned_backend_state(
|
|
owner,
|
|
snapshot.port,
|
|
snapshot.is_adopted,
|
|
)
|
|
.await
|
|
{
|
|
OwnedBackendProbe::Verified(verified) => {
|
|
if snapshot.port.is_none() {
|
|
crate::process::record_owned_backend_port_if_current(
|
|
state,
|
|
snapshot.generation,
|
|
verified.port,
|
|
);
|
|
}
|
|
let result = choose_owned_preflight(&managed, &verified);
|
|
let watchdog_generation = if snapshot.is_adopted
|
|
&& result.disposition == DesktopPreflightDisposition::OwnedReady
|
|
{
|
|
Some((snapshot.generation, false))
|
|
} else {
|
|
None
|
|
};
|
|
return Ok((result, watchdog_generation));
|
|
}
|
|
OwnedBackendProbe::Unmanageable { port, reason } => {
|
|
return Ok((
|
|
choose_unmanageable_owned_preflight(&managed, port, reason),
|
|
None,
|
|
));
|
|
}
|
|
OwnedBackendProbe::NoMetadata
|
|
| OwnedBackendProbe::RemovedMalformed
|
|
| OwnedBackendProbe::NotVerified { .. } => {
|
|
if snapshot.is_adopted {
|
|
crate::process::clear_adopted_backend_if_current(
|
|
state,
|
|
snapshot.generation,
|
|
snapshot.port,
|
|
"state owner probe no longer verifies",
|
|
);
|
|
return Ok((choose_preflight(managed, backend), None));
|
|
}
|
|
return Ok((
|
|
choose_owned_transitional_preflight(&managed, snapshot.port),
|
|
None,
|
|
));
|
|
}
|
|
}
|
|
}
|
|
|
|
let owned = match owned {
|
|
Ok(owned) => owned,
|
|
Err(error) => {
|
|
warn!(
|
|
"Desktop-owned backend probe failed; continuing without adoption: {}",
|
|
error
|
|
);
|
|
return Ok((choose_preflight(managed, backend), None));
|
|
}
|
|
};
|
|
|
|
match owned {
|
|
OwnedBackendProbe::Verified(verified) => {
|
|
let result = choose_owned_preflight(&managed, &verified);
|
|
let adopted = crate::process::adopt_verified_backend(state, verified)?;
|
|
let watchdog_generation =
|
|
if result.disposition == DesktopPreflightDisposition::OwnedReady {
|
|
Some((adopted.generation, adopted.newly_adopted))
|
|
} else {
|
|
None
|
|
};
|
|
Ok((result, watchdog_generation))
|
|
}
|
|
OwnedBackendProbe::Unmanageable { port, reason } => Ok((
|
|
choose_unmanageable_owned_preflight(&managed, port, reason),
|
|
None,
|
|
)),
|
|
OwnedBackendProbe::NoMetadata
|
|
| OwnedBackendProbe::RemovedMalformed
|
|
| OwnedBackendProbe::NotVerified { .. } => Ok((choose_preflight(managed, backend), None)),
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
|
use tokio::net::TcpListener;
|
|
|
|
#[test]
|
|
fn choose_preflight_classifies_core_cases() {
|
|
let bin = || PathBuf::from("/managed/unsloth");
|
|
let ready = || ManagedProbe::Ready { bin: bin() };
|
|
let stale = || ManagedProbe::Stale {
|
|
bin: bin(),
|
|
reason: "old cli".to_string(),
|
|
};
|
|
let old_backend = || BackendProbe::Old {
|
|
port: 8001,
|
|
reason: "missing endpoint".to_string(),
|
|
};
|
|
let cases = [
|
|
(
|
|
stale(),
|
|
BackendProbe::Ready { port: 8000 },
|
|
DesktopPreflightDisposition::AttachedReady,
|
|
Some(8000),
|
|
None,
|
|
false,
|
|
Some(bin()),
|
|
),
|
|
(
|
|
ready(),
|
|
BackendProbe::Ready { port: 8000 },
|
|
DesktopPreflightDisposition::AttachedReady,
|
|
Some(8000),
|
|
None,
|
|
false,
|
|
Some(bin()),
|
|
),
|
|
(
|
|
ManagedProbe::Missing,
|
|
BackendProbe::Ready { port: 8000 },
|
|
DesktopPreflightDisposition::AttachedReady,
|
|
Some(8000),
|
|
None,
|
|
false,
|
|
None,
|
|
),
|
|
(
|
|
ready(),
|
|
old_backend(),
|
|
DesktopPreflightDisposition::ManagedReady,
|
|
None,
|
|
None,
|
|
false,
|
|
Some(bin()),
|
|
),
|
|
(
|
|
stale(),
|
|
old_backend(),
|
|
DesktopPreflightDisposition::ManagedStale,
|
|
None,
|
|
Some("old cli"),
|
|
release_auto_repair(),
|
|
Some(bin()),
|
|
),
|
|
(
|
|
ready(),
|
|
BackendProbe::Missing,
|
|
DesktopPreflightDisposition::ManagedReady,
|
|
None,
|
|
None,
|
|
false,
|
|
Some(bin()),
|
|
),
|
|
(
|
|
stale(),
|
|
BackendProbe::Missing,
|
|
DesktopPreflightDisposition::ManagedStale,
|
|
None,
|
|
Some("old cli"),
|
|
release_auto_repair(),
|
|
Some(bin()),
|
|
),
|
|
(
|
|
ManagedProbe::Missing,
|
|
BackendProbe::Missing,
|
|
DesktopPreflightDisposition::NotInstalled,
|
|
None,
|
|
None,
|
|
false,
|
|
None,
|
|
),
|
|
];
|
|
|
|
for (managed, backend, disposition, port, reason, can_auto_repair, managed_bin) in cases {
|
|
let result = choose_preflight(managed, backend);
|
|
assert_eq!(result.disposition, disposition);
|
|
assert_eq!(result.port, port);
|
|
assert_eq!(result.reason.as_deref(), reason);
|
|
assert_eq!(result.can_auto_repair, can_auto_repair);
|
|
assert_eq!(result.managed_bin, managed_bin);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn external_conflict_blocks_managed_flow() {
|
|
let result = choose_preflight(
|
|
ManagedProbe::Ready {
|
|
bin: PathBuf::from("/managed/unsloth"),
|
|
},
|
|
BackendProbe::ExternalConflict {
|
|
port: 8888,
|
|
reason: "same_root_external_backend_active".to_string(),
|
|
},
|
|
);
|
|
|
|
assert_eq!(
|
|
result.disposition,
|
|
DesktopPreflightDisposition::ExternalConflict
|
|
);
|
|
assert_eq!(result.port, Some(8888));
|
|
assert_eq!(
|
|
result.reason,
|
|
Some("same_root_external_backend_active".to_string())
|
|
);
|
|
assert!(!result.can_auto_repair);
|
|
assert_eq!(result.managed_bin, Some(PathBuf::from("/managed/unsloth")));
|
|
}
|
|
|
|
#[test]
|
|
fn mutation_blocker_blocks_ready_external_backends() {
|
|
assert_eq!(
|
|
mutation_blocker_from_probe(BackendProbe::Ready { port: 8890 }),
|
|
Some(ExternalBackendConflict {
|
|
port: 8890,
|
|
reason: "same_root_external_backend_active".to_string(),
|
|
})
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn backend_version_gate_classifies_core_cases() {
|
|
for version in [
|
|
MIN_DESKTOP_BACKEND_VERSION,
|
|
"2026.5.4",
|
|
"2027.1.0",
|
|
"2026.5.3.post1",
|
|
"2026.5.3+local",
|
|
"2026.5.3.post1",
|
|
] {
|
|
assert!(backend_version_compatible(Some(version)), "{version}");
|
|
}
|
|
for (version, reason) in [
|
|
(None, "desktop_backend_version_missing"),
|
|
(Some("not-a-version"), "desktop_backend_version_invalid"),
|
|
(Some("2026.5.3.1"), "desktop_backend_version_invalid"),
|
|
(Some("2026.5.3foo"), "desktop_backend_version_invalid"),
|
|
(Some("2026.5.3.devx"), "desktop_backend_version_invalid"),
|
|
(Some("2026.5.2"), "desktop_backend_version_too_old"),
|
|
(Some("2026.5.3rc1"), "desktop_backend_version_too_old"),
|
|
(Some("2026.5.3.dev1"), "desktop_backend_version_too_old"),
|
|
] {
|
|
assert_eq!(
|
|
backend_version_stale_reason(version).as_deref(),
|
|
Some(reason)
|
|
);
|
|
}
|
|
assert_eq!(
|
|
backend_version_compatible(Some("dev")),
|
|
cfg!(debug_assertions)
|
|
);
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
struct FakeCli {
|
|
bin: PathBuf,
|
|
dir: PathBuf,
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
impl Drop for FakeCli {
|
|
fn drop(&mut self) {
|
|
let _ = std::fs::remove_dir_all(&self.dir);
|
|
}
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
fn fake_cli(test_name: &str, script: &str) -> FakeCli {
|
|
use std::fs;
|
|
use std::os::unix::fs::PermissionsExt;
|
|
use std::time::{SystemTime, UNIX_EPOCH};
|
|
|
|
let nanos = SystemTime::now()
|
|
.duration_since(UNIX_EPOCH)
|
|
.unwrap()
|
|
.as_nanos();
|
|
let dir = std::env::temp_dir().join(format!(
|
|
"unsloth-preflight-{test_name}-{}-{nanos}",
|
|
std::process::id()
|
|
));
|
|
fs::create_dir_all(&dir).unwrap();
|
|
let bin = dir.join("unsloth");
|
|
fs::write(&bin, script).unwrap();
|
|
let mut perms = fs::metadata(&bin).unwrap().permissions();
|
|
perms.set_mode(0o755);
|
|
fs::set_permissions(&bin, perms).unwrap();
|
|
FakeCli { bin, dir }
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
#[tokio::test]
|
|
async fn managed_cli_capability_probe_classifies_core_cases() {
|
|
for (name, script, stale_reason) in [
|
|
(
|
|
"cap-missing",
|
|
r#"#!/bin/sh
|
|
if [ "$1" = "-h" ]; then exit 0; fi
|
|
if [ "$1" = "studio" ] && [ "$2" = "provision-desktop-auth" ] && [ "$3" = "--help" ]; then exit 0; fi
|
|
exit 1
|
|
"#,
|
|
Some("desktop_capability_probe_failed"),
|
|
),
|
|
(
|
|
"cap-true-helper-missing",
|
|
r#"#!/bin/sh
|
|
if [ "$1" = "-h" ]; then exit 0; fi
|
|
if [ "$1" = "studio" ] && [ "$2" = "desktop-capabilities" ] && [ "$3" = "--json" ]; then
|
|
printf '{"desktop_protocol_version":1,"desktop_manageability_version":1,"supports_api_only":true,"supports_provision_desktop_auth":true,"supports_desktop_backend_ownership":true,"version":"2026.5.3"}'
|
|
exit 0
|
|
fi
|
|
exit 1
|
|
"#,
|
|
None,
|
|
),
|
|
(
|
|
"cap-false-helper-ready",
|
|
r#"#!/bin/sh
|
|
if [ "$1" = "-h" ]; then exit 0; fi
|
|
if [ "$1" = "studio" ] && [ "$2" = "desktop-capabilities" ] && [ "$3" = "--json" ]; then
|
|
printf '{"desktop_protocol_version":1,"desktop_manageability_version":1,"supports_api_only":true,"supports_provision_desktop_auth":false,"supports_desktop_backend_ownership":true,"desktop_auth_stale_reason":"cap_false","version":"2026.5.3"}'
|
|
exit 0
|
|
fi
|
|
if [ "$1" = "studio" ] && [ "$2" = "provision-desktop-auth" ] && [ "$3" = "--help" ]; then exit 0; fi
|
|
exit 1
|
|
"#,
|
|
Some("cap_false"),
|
|
),
|
|
] {
|
|
let fake = fake_cli(name, script);
|
|
let bin = fake.bin.clone();
|
|
match (probe_managed_bin(bin.clone()).await, stale_reason) {
|
|
(ManagedProbe::Ready { bin: actual }, None) => assert_eq!(actual, bin),
|
|
(
|
|
ManagedProbe::Stale {
|
|
bin: actual,
|
|
reason,
|
|
},
|
|
Some(expected),
|
|
) => {
|
|
assert_eq!((actual, reason.as_str()), (bin, expected));
|
|
}
|
|
(probe, expected) => panic!("unexpected probe {probe:?}, expected {expected:?}"),
|
|
}
|
|
}
|
|
}
|
|
|
|
const EXPECTED_ROOT_ID: &str =
|
|
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
|
|
const OTHER_ROOT_ID: &str = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
|
|
const OWNER_TOKEN: &str = "desktop-owner-token";
|
|
|
|
fn install_test_owner() {
|
|
crate::desktop_backend_owner::install_test_owner(EXPECTED_ROOT_ID, OWNER_TOKEN);
|
|
}
|
|
|
|
fn desktop_ready_health(root_id: &str) -> String {
|
|
desktop_ready_health_with_owner(root_id, true)
|
|
}
|
|
|
|
fn desktop_owner_json(include_owner: bool) -> String {
|
|
if include_owner {
|
|
format!(
|
|
r#", "desktop_owner":{{"kind":"tauri","token_sha256":"{}"}}"#,
|
|
crate::desktop_backend_owner::token_sha256(OWNER_TOKEN)
|
|
)
|
|
} else {
|
|
String::new()
|
|
}
|
|
}
|
|
|
|
fn desktop_ready_health_with_owner(root_id: &str, include_owner: bool) -> String {
|
|
let owner = desktop_owner_json(include_owner);
|
|
format!(
|
|
r#"{{"status":"healthy","service":"Unsloth UI Backend","version":"2026.5.3","desktop_protocol_version":1,"desktop_manageability_version":1,"supports_desktop_auth":true,"supports_desktop_backend_ownership":true,"studio_root_id":"{root_id}"{owner}}}"#
|
|
)
|
|
}
|
|
|
|
async fn backend_server(health_body: impl Into<String>, route_status: &'static str) -> u16 {
|
|
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
|
let port = listener.local_addr().unwrap().port();
|
|
let health_body = health_body.into();
|
|
|
|
tokio::spawn(async move {
|
|
for _ in 0..2 {
|
|
let (mut stream, _) = listener.accept().await.unwrap();
|
|
let mut buffer = [0; 2048];
|
|
let n = stream.read(&mut buffer).await.unwrap();
|
|
let request = String::from_utf8_lossy(&buffer[..n]);
|
|
let (status, body) = if request.starts_with("GET /api/health ") {
|
|
("200 OK", health_body.as_str())
|
|
} else if request.starts_with("POST /api/auth/desktop-login ") {
|
|
(route_status, "")
|
|
} else {
|
|
("404 Not Found", "")
|
|
};
|
|
let response = format!(
|
|
"HTTP/1.1 {status}\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}",
|
|
body.len()
|
|
);
|
|
stream.write_all(response.as_bytes()).await.unwrap();
|
|
}
|
|
});
|
|
|
|
port
|
|
}
|
|
|
|
async fn probe_test_backend(
|
|
health_body: impl Into<String>,
|
|
route_status: &'static str,
|
|
) -> BackendProbe {
|
|
install_test_owner();
|
|
let port = backend_server(health_body, route_status).await;
|
|
let client = reqwest::Client::new();
|
|
let health = backend_health(&client, port).await.unwrap();
|
|
backend_desktop_auth_status(&client, port, &health, Some(EXPECTED_ROOT_ID)).await
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn backend_health_without_desktop_capability_fields_is_still_candidate() {
|
|
let port = backend_server(
|
|
r#"{"status":"healthy","service":"Unsloth UI Backend"}"#,
|
|
"401 Unauthorized",
|
|
)
|
|
.await;
|
|
let client = reqwest::Client::new();
|
|
|
|
assert!(backend_health(&client, port).await.is_some());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn backend_with_auth_support_but_missing_protocol_is_old() {
|
|
let probe = probe_test_backend(
|
|
format!(
|
|
r#"{{"status":"healthy","service":"Unsloth UI Backend","version":"2026.5.3","desktop_manageability_version":1,"supports_desktop_auth":true,"supports_desktop_backend_ownership":true,"studio_root_id":"{EXPECTED_ROOT_ID}"{}}}"#,
|
|
desktop_owner_json(true)
|
|
),
|
|
"401 Unauthorized",
|
|
)
|
|
.await;
|
|
|
|
assert!(matches!(probe, BackendProbe::Old { .. }));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn backend_health_with_desktop_capability_fields_and_401_is_ready() {
|
|
let probe =
|
|
probe_test_backend(desktop_ready_health(EXPECTED_ROOT_ID), "401 Unauthorized").await;
|
|
|
|
assert!(matches!(probe, BackendProbe::Ready { .. }));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn compatible_same_root_without_desktop_owner_is_ready() {
|
|
let probe = probe_test_backend(
|
|
desktop_ready_health_with_owner(EXPECTED_ROOT_ID, false),
|
|
"401 Unauthorized",
|
|
)
|
|
.await;
|
|
|
|
assert!(matches!(probe, BackendProbe::Ready { .. }));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn stale_same_root_without_desktop_owner_is_external_conflict() {
|
|
let probe = probe_test_backend(
|
|
format!(
|
|
r#"{{"status":"healthy","service":"Unsloth UI Backend","version":"2026.5.1","desktop_protocol_version":1,"desktop_manageability_version":1,"supports_desktop_auth":true,"supports_desktop_backend_ownership":true,"studio_root_id":"{EXPECTED_ROOT_ID}"}}"#,
|
|
),
|
|
"401 Unauthorized",
|
|
)
|
|
.await;
|
|
|
|
assert!(matches!(
|
|
probe,
|
|
BackendProbe::ExternalConflict {
|
|
reason,
|
|
..
|
|
} if reason == "desktop_backend_version_too_old"
|
|
));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn backend_root_id_mismatch_is_old_before_auth_probe() {
|
|
let probe =
|
|
probe_test_backend(desktop_ready_health(OTHER_ROOT_ID), "401 Unauthorized").await;
|
|
|
|
assert!(matches!(
|
|
probe,
|
|
BackendProbe::Old {
|
|
reason,
|
|
..
|
|
} if reason == "studio_root_id_mismatch"
|
|
));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn backend_missing_root_id_is_external_conflict_before_auth_probe() {
|
|
let probe = probe_test_backend(
|
|
r#"{"status":"healthy","service":"Unsloth UI Backend","desktop_protocol_version":1,"supports_desktop_auth":true}"#,
|
|
"401 Unauthorized",
|
|
)
|
|
.await;
|
|
|
|
assert!(matches!(
|
|
probe,
|
|
BackendProbe::ExternalConflict {
|
|
reason,
|
|
..
|
|
} if reason == "ambiguous_root_external_backend_active"
|
|
));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn backend_expected_root_id_missing_is_external_conflict_before_auth_probe() {
|
|
install_test_owner();
|
|
let port = backend_server(desktop_ready_health(EXPECTED_ROOT_ID), "401 Unauthorized").await;
|
|
let client = reqwest::Client::new();
|
|
let health = backend_health(&client, port).await.unwrap();
|
|
|
|
assert!(matches!(
|
|
backend_desktop_auth_status(&client, port, &health, None).await,
|
|
BackendProbe::ExternalConflict {
|
|
reason,
|
|
..
|
|
} if reason == "ambiguous_root_external_backend_active"
|
|
));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn backend_route_404_is_old() {
|
|
let probe =
|
|
probe_test_backend(desktop_ready_health(EXPECTED_ROOT_ID), "404 Not Found").await;
|
|
|
|
assert!(matches!(
|
|
probe,
|
|
BackendProbe::Old {
|
|
reason,
|
|
..
|
|
} if reason == "desktop_login_not_found"
|
|
));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn backend_capability_false_is_old_even_when_route_401() {
|
|
let probe = probe_test_backend(
|
|
format!(
|
|
r#"{{"status":"healthy","service":"Unsloth UI Backend","version":"2026.5.3","desktop_protocol_version":1,"desktop_manageability_version":1,"supports_desktop_auth":false,"supports_desktop_backend_ownership":true,"desktop_auth_stale_reason":"cap_false","studio_root_id":"{EXPECTED_ROOT_ID}"{}}}"#,
|
|
desktop_owner_json(true)
|
|
),
|
|
"401 Unauthorized",
|
|
)
|
|
.await;
|
|
|
|
assert!(matches!(
|
|
probe,
|
|
BackendProbe::Old {
|
|
reason,
|
|
..
|
|
} if reason == "cap_false"
|
|
));
|
|
}
|
|
}
|