From 435f50f8c141d63e6bd85812ab8b3e6061c2de45 Mon Sep 17 00:00:00 2001 From: ejc3 Date: Mon, 26 Oct 2026 03:45:52 +0000 Subject: [PATCH] Publish every GPU host inventory observation Remove the watcher-state gate so the core receives the final running observation after the fleet is already awake. The core retains ownership of the wake-history window. The focused regression first observes an awake fleet with a pending host, then fails without this reduction when the running host never publishes its booted phase. --- src/fleet/coord/hosts.rs | 15 ++++++------ tests/test_fleet_coord.rs | 51 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 8 deletions(-) diff --git a/src/fleet/coord/hosts.rs b/src/fleet/coord/hosts.rs index 5cef79c0b..07023d96b 100644 --- a/src/fleet/coord/hosts.rs +++ b/src/fleet/coord/hosts.rs @@ -407,14 +407,13 @@ pub async fn run(cfg: HostConfig, mut power: watch::Receiver, handle: Ha busy = true; } } - if state == "waking" || watch.iter().any(|w| w.coming_up) { - handle - .send(Event::ScaleInstances { - at: 0, - instances: report, - }) - .await; - } + // The core decides whether these observations belong to an open wake. + handle + .send(Event::ScaleInstances { + at: 0, + instances: report, + }) + .await; if !errors.is_empty() { reported = true; degraded(&handle, &errors.join("; ")).await; diff --git a/tests/test_fleet_coord.rs b/tests/test_fleet_coord.rs index c743b3899..9307799dd 100644 --- a/tests/test_fleet_coord.rs +++ b/tests/test_fleet_coord.rs @@ -2576,6 +2576,57 @@ fn launches(fake: &fcvm::fleet::coord::hosts::FakeHosts) -> Vec { .collect() } +/// The EC2 half can make the fleet awake before the GPU host finishes booting. The +/// snapshot must still publish that host's final running observation. +#[tokio::test] +async fn a_host_that_runs_after_the_fleet_is_awake_publishes_its_booted_phase() { + use fcvm::fleet::coord::hosts::FakeHosts; + let now = now_ms(); + let mut state = State::new( + Config { + power_managed: true, + ..Config::default() + }, + now, + ); + state.fleet.power.state = "awake".into(); + state.fleet.power.woke_at = Some(now); + let fake = Arc::new(FakeHosts::default()); + let (handle, _actor) = start( + state, + None, + None, + None, + Some(host_config(&fake, &["us-east-1"], 1, 8)), + None, + ); + let mut viewer = page_viewer(&handle).await; + let pending = viewer + .until(|s| { + s["fleet"]["power"]["hosts"] + .as_array() + .is_some_and(|h| !h.is_empty()) + }) + .await; + assert_eq!(pending["fleet"]["power"]["state"], "awake"); + assert_eq!( + pending["fleet"]["power"]["hosts"][0]["phases"][0]["phase"], + "instance_pending" + ); + fake.set_state("us-east-1", "pending", "running"); + let booted = tokio::time::timeout( + Duration::from_secs(2), + viewer.until(|s| { + s["fleet"]["power"]["hosts"][0]["phases"] + .as_array() + .is_some_and(|p| p.iter().any(|p| p["phase"] == "booted")) + }), + ) + .await + .expect("a running host's booted phase is published after the fleet is awake"); + assert_eq!(booted["fleet"]["power"]["state"], "awake"); +} + /// The coordinator launches the GPU hosts itself, spot first: awake at start it launches /// the awake capacity, with no active viewer it terminates every host, and a viewer's /// visible page launches them again. While it wakes, the hosts show in the snapshot.