From 054a78c111632fae98fdffb723d404948bc763f4 Mon Sep 17 00:00:00 2001 From: Alex Barclay Date: Thu, 13 Aug 2026 21:21:15 -0600 Subject: [PATCH 1/7] ci: gate on rustfmt, clippy, svelte-check and the cockpit crate's tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four gates the workflow was missing: - cargo fmt --all -- --check - cargo clippy --workspace --all-targets -- -D warnings - npm run check (svelte-check + tsc) - cargo test for cockpit/ui/src-tauri The last one closes a real hole. cockpit/ui/src-tauri declares its own empty [workspace] table, so it is a standalone workspace that the root manifest does not list as a member — 'cargo test --workspace' run from the repo root never reached its tests, and that suite (the app-plugin state machine, manifest validation, discovery) had only ever been run by hand. The fmt and clippy gates run once per manifest for the same reason. Compiling the tauri crate needs the WebKitGTK system deps the build job already installs, plus the fleetd sidecar binary, because tauri-build resolves the externalBin resource at compile time and hard-errors without it. Both the lint and cockpit-test jobs therefore repeat that setup. --- .github/workflows/ci.yml | 176 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 175 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c1d1743..83b4004 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,11 +1,16 @@ name: CI # First CI for command-center (Lane C / roadmap C8). -# On every push and PR: run the embargo guard, run the Rust workspace tests, then +# On every push and PR: run the embargo guard, the lint/type gates (rustfmt, +# clippy, svelte-check + tsc), the Rust tests for both cargo workspaces, then # build the frontend + fleetd sidecar and produce Tauri bundles on all three target # OSes. Bundles are uploaded as workflow artifacts so a reviewer can smoke-test a # real build. # +# TWO CARGO WORKSPACES: the root one (crates/fleet-core, crates/fleetd) and the +# standalone cockpit/ui/src-tauri crate. Nothing run from the repo root reaches +# the latter, so the fmt, clippy and test gates each run once per manifest. +# # COVERAGE GAP (intentional): the real-Docker integration tests in # crates/fleetd/tests/ (local_docker_it.rs, preflight_it.rs, swarm_smoke_it.rs) # are marked `#[ignore]` and need a live Docker daemon + the cc-agent:dev image. @@ -74,9 +79,118 @@ jobs: fi node scripts/embargo-guard.mjs --message "$RUNNER_TEMP/msgs.txt" + # --------------------------------------------------------------------------- + # rustfmt + clippy, over BOTH cargo workspaces. + # + # cockpit/ui/src-tauri declares its own empty `[workspace]` table, so it is a + # standalone workspace that the root manifest does not list as a member. That + # makes it invisible to `--workspace` / `--all` run from the repo root, which + # is why every gate here runs twice — once per manifest. Dropping either half + # silently leaves that crate ungated. + # + # rustfmt only needs the sources, so both fmt checks run first and fail fast. + # Clippy has to actually compile, and compiling the tauri crate needs two + # things the root workspace does not: the WebKitGTK system deps (same list the + # build job installs) and the fleetd sidecar binary, because tauri-build + # resolves the `externalBin` resource at compile time and hard-errors when it + # is absent. + # --------------------------------------------------------------------------- + lint: + name: fmt + clippy + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + with: + components: clippy, rustfmt + + - name: Cache cargo registry + target + uses: Swatinem/rust-cache@v2 + with: + workspaces: | + . + cockpit/ui/src-tauri + + - name: Check formatting (root workspace) + run: cargo fmt --all -- --check + + - name: Check formatting (cockpit/ui/src-tauri) + run: cargo fmt --all --manifest-path cockpit/ui/src-tauri/Cargo.toml -- --check + + - name: Clippy (root workspace) + run: cargo clippy --workspace --all-targets -- -D warnings + + # Everything below exists only so the tauri crate can be compiled. + - name: Install Tauri Linux system deps + run: | + sudo apt-get update + sudo apt-get install -y \ + libwebkit2gtk-4.1-dev \ + libgtk-3-dev \ + libayatana-appindicator3-dev \ + librsvg2-dev \ + patchelf \ + build-essential \ + curl \ + wget \ + file \ + libssl-dev + + - name: Install Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + cache-dependency-path: cockpit/ui/package-lock.json + + - name: Install frontend dependencies + run: npm ci + working-directory: cockpit/ui + + # tauri-build fails the compile without this — see the note above. + - name: Build fleetd sidecar + run: npm run sidecar + working-directory: cockpit/ui + + - name: Clippy (cockpit/ui/src-tauri) + run: cargo clippy --all-targets -- -D warnings + working-directory: cockpit/ui/src-tauri + + # --------------------------------------------------------------------------- + # Frontend type gate: `npm run check` is svelte-check over tsconfig.app.json + # followed by `tsc -p tsconfig.node.json`. Pure type-checking — no Rust, no + # system deps, so it stands alone and finishes in well under a minute. + # --------------------------------------------------------------------------- + check: + name: svelte-check + tsc + runs-on: ubuntu-latest + defaults: + run: + working-directory: cockpit/ui + steps: + - uses: actions/checkout@v4 + + - name: Install Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + cache-dependency-path: cockpit/ui/package-lock.json + + - name: Install frontend dependencies + run: npm ci + + - name: Type-check the frontend + run: npm run check + # --------------------------------------------------------------------------- # Rust workspace tests. Fast, OS-independent gate — runs once on Linux. # Does NOT run the `#[ignore]`d real-Docker ITs (see header note above). + # + # ROOT WORKSPACE ONLY. The cockpit crate's tests are a separate job below — + # see the standalone-workspace note on the `lint` job for why they have to be. # --------------------------------------------------------------------------- test: name: cargo test (workspace) @@ -95,6 +209,66 @@ jobs: - name: Run workspace tests (Docker ITs stay --ignored) run: cargo test --workspace + # --------------------------------------------------------------------------- + # The cockpit crate's own tests — the app-plugin state machine, manifest + # validation and discovery. `cargo test --workspace` above runs from the repo + # root and cannot reach them: cockpit/ui/src-tauri is a standalone workspace + # (see the `lint` job note), so until this job existed that whole suite was + # verified only by hand on a developer's machine. + # + # Same compile prerequisites as clippy: WebKitGTK system deps plus the fleetd + # sidecar binary that tauri-build resolves at compile time. + # --------------------------------------------------------------------------- + test-cockpit: + name: cargo test (cockpit) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + + - name: Cache cargo registry + target + uses: Swatinem/rust-cache@v2 + with: + workspaces: | + . + cockpit/ui/src-tauri + + - name: Install Tauri Linux system deps + run: | + sudo apt-get update + sudo apt-get install -y \ + libwebkit2gtk-4.1-dev \ + libgtk-3-dev \ + libayatana-appindicator3-dev \ + librsvg2-dev \ + patchelf \ + build-essential \ + curl \ + wget \ + file \ + libssl-dev + + - name: Install Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + cache-dependency-path: cockpit/ui/package-lock.json + + - name: Install frontend dependencies + run: npm ci + working-directory: cockpit/ui + + - name: Build fleetd sidecar + run: npm run sidecar + working-directory: cockpit/ui + + - name: Run cockpit crate tests + run: cargo test + working-directory: cockpit/ui/src-tauri + # --------------------------------------------------------------------------- # Cross-platform Tauri build matrix. Builds the SvelteKit frontend, then the # fleetd sidecar (MUST precede `tauri build` — externalBin is resolved at From 3e2bd3a723ae8cb30002512299fecefb6b259b71 Mon Sep 17 00:00:00 2001 From: Alex Barclay Date: Thu, 13 Aug 2026 21:22:45 -0600 Subject: [PATCH 2/7] style: apply cargo fmt to both cargo workspaces MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pure rustfmt output, no behavior change — the new 'cargo fmt --all -- --check' gate's baseline. Neither workspace had ever been run through rustfmt, so this is the whole backlog in one pass; there is no rustfmt.toml, so these are the stock defaults. Mostly struct literals and multi-argument calls being split across lines. --- cockpit/ui/src-tauri/build.rs | 2 +- cockpit/ui/src-tauri/src/dashboard.rs | 5 +- cockpit/ui/src-tauri/src/lib.rs | 4 +- cockpit/ui/src-tauri/src/local_projects.rs | 68 +- cockpit/ui/src-tauri/src/main.rs | 2 +- cockpit/ui/src-tauri/src/plugins/discovery.rs | 46 +- cockpit/ui/src-tauri/src/plugins/manifest.rs | 49 +- cockpit/ui/src-tauri/src/plugins/mod.rs | 4 +- cockpit/ui/src-tauri/src/plugins/seams.rs | 56 +- .../ui/src-tauri/src/plugins/seams_impl.rs | 6 +- cockpit/ui/src-tauri/src/plugins/state.rs | 296 ++++-- crates/fleet-core/src/event.rs | 32 +- crates/fleet-core/src/gate.rs | 12 +- crates/fleet-core/src/lib.rs | 4 +- crates/fleet-core/src/phase.rs | 18 +- crates/fleet-core/src/transition.rs | 88 +- crates/fleetd/src/bin/run_once.rs | 53 +- crates/fleetd/src/claude_meter.rs | 11 +- crates/fleetd/src/docsource.rs | 97 +- crates/fleetd/src/driver.rs | 389 +++++-- crates/fleetd/src/fake.rs | 26 +- crates/fleetd/src/gh_forge.rs | 53 +- crates/fleetd/src/local_docker.rs | 87 +- crates/fleetd/src/planner.rs | 83 +- crates/fleetd/src/reconcile.rs | 33 +- crates/fleetd/src/retry.rs | 77 +- crates/fleetd/src/runner.rs | 6 +- crates/fleetd/src/server.rs | 994 ++++++++++++++---- crates/fleetd/src/store.rs | 306 ++++-- crates/fleetd/src/swarm.rs | 70 +- crates/fleetd/tests/demo_mode_it.rs | 58 +- crates/fleetd/tests/local_docker_it.rs | 23 +- crates/fleetd/tests/preflight_it.rs | 62 +- crates/fleetd/tests/swarm_smoke_it.rs | 8 +- 34 files changed, 2486 insertions(+), 642 deletions(-) diff --git a/cockpit/ui/src-tauri/build.rs b/cockpit/ui/src-tauri/build.rs index 795b9b7..d860e1e 100644 --- a/cockpit/ui/src-tauri/build.rs +++ b/cockpit/ui/src-tauri/build.rs @@ -1,3 +1,3 @@ fn main() { - tauri_build::build() + tauri_build::build() } diff --git a/cockpit/ui/src-tauri/src/dashboard.rs b/cockpit/ui/src-tauri/src/dashboard.rs index 554348d..df92984 100644 --- a/cockpit/ui/src-tauri/src/dashboard.rs +++ b/cockpit/ui/src-tauri/src/dashboard.rs @@ -43,7 +43,10 @@ fn run_halyard(subcommand: &str) -> Result { if !out.status.success() { let code = out.status.code().unwrap_or(-1); let stderr = String::from_utf8_lossy(&out.stderr); - return Err(format!("halyard {subcommand} exited {code}: {}", stderr.trim())); + return Err(format!( + "halyard {subcommand} exited {code}: {}", + stderr.trim() + )); } let stdout = String::from_utf8_lossy(&out.stdout); diff --git a/cockpit/ui/src-tauri/src/lib.rs b/cockpit/ui/src-tauri/src/lib.rs index a9412e7..c5c21aa 100644 --- a/cockpit/ui/src-tauri/src/lib.rs +++ b/cockpit/ui/src-tauri/src/lib.rs @@ -63,9 +63,7 @@ pub fn run() { // LANE-B → HOST: stop the fleetd-serve sidecar first so the // supervisor doesn't respawn it as we tear down, and no // orphaned process is left behind. - app_handle - .state::() - .shutdown(); + app_handle.state::().shutdown(); let mgr = app_handle.state::(); mgr.stop_all_owned(30_000); // total budget; kept under the OS force-kill ceiling app_handle.exit(0); diff --git a/cockpit/ui/src-tauri/src/local_projects.rs b/cockpit/ui/src-tauri/src/local_projects.rs index 822acd8..0ff1716 100644 --- a/cockpit/ui/src-tauri/src/local_projects.rs +++ b/cockpit/ui/src-tauri/src/local_projects.rs @@ -20,7 +20,9 @@ pub struct ScanConfig { #[serde(default)] pub excludes: Vec, } -fn default_depth() -> usize { 5 } +fn default_depth() -> usize { + 5 +} #[derive(Serialize, PartialEq, Debug)] #[serde(rename_all = "camelCase")] @@ -40,7 +42,9 @@ fn normalize(p: &Path) -> String { fn is_excluded(path: &str, excludes: &[String]) -> bool { let p = path.to_lowercase(); - excludes.iter().any(|e| p.contains(&e.replace('\\', "/").to_lowercase())) + excludes + .iter() + .any(|e| p.contains(&e.replace('\\', "/").to_lowercase())) } /// Read a project dir's STATUS.md/ROADMAP.md into a doc (raw text; hash over raw bytes). @@ -85,10 +89,14 @@ fn discover(root: &Path, max_depth: usize, excludes: &[String], out: &mut Vec Result, String> { let mut dirs: Vec = Vec::new(); for root in &config.scan_roots { - discover(Path::new(root), config.max_depth, &config.excludes, &mut dirs); + discover( + Path::new(root), + config.max_depth, + &config.excludes, + &mut dirs, + ); } - let discovered: std::collections::HashSet = - dirs.iter().map(|p| normalize(p)).collect(); + let discovered: std::collections::HashSet = dirs.iter().map(|p| normalize(p)).collect(); let mut docs: Vec = dirs.iter().map(|d| read_project(d, false)).collect(); // Pins: included even without a marker; skip a pin already auto-discovered. @@ -126,7 +134,12 @@ mod tests { make_project(&root, "mono/services/api", "---\nstage: Spec\n---\n"); // depth-3 nested fs::create_dir_all(root.join("node_modules/pkg/docs")).unwrap(); fs::write(root.join("node_modules/pkg/docs/STATUS.md"), "x").unwrap(); // must be pruned - let cfg = ScanConfig { scan_roots: vec![root.to_string_lossy().into()], max_depth: 5, pins: vec![], excludes: vec![] }; + let cfg = ScanConfig { + scan_roots: vec![root.to_string_lossy().into()], + max_depth: 5, + pins: vec![], + excludes: vec![], + }; let docs = scan_local_projects(cfg).unwrap(); let dirs: Vec<&str> = docs.iter().map(|d| d.project_dir.as_str()).collect(); assert!(dirs.iter().any(|d| d.ends_with("/alpha"))); @@ -138,10 +151,22 @@ mod tests { fn hashes_roadmap_over_raw_bytes() { let root = tmp(); make_project(&root, "beta", "---\nstage: Build\n---\n"); - fs::write(root.join("beta/ROADMAP.md"), "## X\n\n").unwrap(); - let cfg = ScanConfig { scan_roots: vec![root.to_string_lossy().into()], max_depth: 5, pins: vec![], excludes: vec![] }; + fs::write( + root.join("beta/ROADMAP.md"), + "## X\n\n", + ) + .unwrap(); + let cfg = ScanConfig { + scan_roots: vec![root.to_string_lossy().into()], + max_depth: 5, + pins: vec![], + excludes: vec![], + }; let docs = scan_local_projects(cfg).unwrap(); - let beta = docs.iter().find(|d| d.project_dir.ends_with("/beta")).unwrap(); + let beta = docs + .iter() + .find(|d| d.project_dir.ends_with("/beta")) + .unwrap(); assert!(beta.roadmap_hash.as_ref().unwrap().len() == 64); // hex sha256 } @@ -149,10 +174,22 @@ mod tests { fn roadmap_hash_is_over_raw_bytes_even_when_not_utf8() { let root = tmp(); make_project(&root, "gamma", "---\nstage: Build\n---\n"); - std::fs::write(root.join("gamma/ROADMAP.md"), [0x23, 0x20, 0xff, 0xfe, 0x0a]).unwrap(); - let cfg = ScanConfig { scan_roots: vec![root.to_string_lossy().into()], max_depth: 5, pins: vec![], excludes: vec![] }; + std::fs::write( + root.join("gamma/ROADMAP.md"), + [0x23, 0x20, 0xff, 0xfe, 0x0a], + ) + .unwrap(); + let cfg = ScanConfig { + scan_roots: vec![root.to_string_lossy().into()], + max_depth: 5, + pins: vec![], + excludes: vec![], + }; let docs = scan_local_projects(cfg).unwrap(); - let gamma = docs.iter().find(|d| d.project_dir.ends_with("/gamma")).unwrap(); + let gamma = docs + .iter() + .find(|d| d.project_dir.ends_with("/gamma")) + .unwrap(); assert!(gamma.roadmap_hash.as_ref().unwrap().len() == 64); // hash computed despite invalid UTF-8 assert!(gamma.roadmap_text.is_none()); // decode fails, proving hash path is independent of decode } @@ -162,7 +199,12 @@ mod tests { let root = tmp(); let pin = root.join("pinned-no-marker"); fs::create_dir_all(&pin).unwrap(); - let cfg = ScanConfig { scan_roots: vec![], max_depth: 5, pins: vec![pin.to_string_lossy().into()], excludes: vec![] }; + let cfg = ScanConfig { + scan_roots: vec![], + max_depth: 5, + pins: vec![pin.to_string_lossy().into()], + excludes: vec![], + }; let docs = scan_local_projects(cfg).unwrap(); assert_eq!(docs.len(), 1); assert!(docs[0].is_pinned); diff --git a/cockpit/ui/src-tauri/src/main.rs b/cockpit/ui/src-tauri/src/main.rs index ad5fe83..69c3a72 100644 --- a/cockpit/ui/src-tauri/src/main.rs +++ b/cockpit/ui/src-tauri/src/main.rs @@ -2,5 +2,5 @@ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] fn main() { - app_lib::run(); + app_lib::run(); } diff --git a/cockpit/ui/src-tauri/src/plugins/discovery.rs b/cockpit/ui/src-tauri/src/plugins/discovery.rs index 7d81383..0c16994 100644 --- a/cockpit/ui/src-tauri/src/plugins/discovery.rs +++ b/cockpit/ui/src-tauri/src/plugins/discovery.rs @@ -22,19 +22,37 @@ pub fn discover(roots: &[&Path]) -> Vec { let mani_path = entry.path().join("app-plugin.json"); let text = match std::fs::read_to_string(&mani_path) { Ok(t) => t, - Err(e) => { log::warn!("app-plugins: skipping {} — unreadable: {e}", mani_path.display()); continue } + Err(e) => { + log::warn!( + "app-plugins: skipping {} — unreadable: {e}", + mani_path.display() + ); + continue; + } }; let manifest = match Manifest::from_json(&text) { Ok(m) => m, - Err(e) => { log::warn!("app-plugins: skipping {} — parse error: {e}", mani_path.display()); continue } + Err(e) => { + log::warn!( + "app-plugins: skipping {} — parse error: {e}", + mani_path.display() + ); + continue; + } }; if let Err(e) = manifest.validate() { - log::warn!("app-plugins: skipping {} — validation failed: {e}", mani_path.display()); + log::warn!( + "app-plugins: skipping {} — validation failed: {e}", + mani_path.display() + ); continue; } by_id.insert( manifest.id.clone(), - DiscoveredPlugin { dir: entry.path(), manifest }, + DiscoveredPlugin { + dir: entry.path(), + manifest, + }, ); } } @@ -57,8 +75,16 @@ mod tests { // both define id "audience"; user dir should win let base = r#"{"id":"audience","name":"NAME","apiVersion":1,"url":"http://localhost:3000", "lifecycle":{"start":"x","health":{"url":"h"},"ready":{"url":"r"}}}"#; - fs::write(packaged.join("audience/app-plugin.json"), base.replace("NAME", "Packaged")).unwrap(); - fs::write(user.join("audience/app-plugin.json"), base.replace("NAME", "User")).unwrap(); + fs::write( + packaged.join("audience/app-plugin.json"), + base.replace("NAME", "Packaged"), + ) + .unwrap(); + fs::write( + user.join("audience/app-plugin.json"), + base.replace("NAME", "User"), + ) + .unwrap(); let found = discover(&[packaged.as_path(), user.as_path()]); assert_eq!(found.len(), 1); @@ -78,7 +104,13 @@ mod tests { fs::write(root.join("good/app-plugin.json"), valid).unwrap(); fs::write(root.join("garbage/app-plugin.json"), "{ not json").unwrap(); // parses fine but validate() refuses the unsupported apiVersion → skipped - fs::write(root.join("badversion/app-plugin.json"), valid.replace("\"apiVersion\":1", "\"apiVersion\":99").replace("\"id\":\"good\"", "\"id\":\"bad\"")).unwrap(); + fs::write( + root.join("badversion/app-plugin.json"), + valid + .replace("\"apiVersion\":1", "\"apiVersion\":99") + .replace("\"id\":\"good\"", "\"id\":\"bad\""), + ) + .unwrap(); // "nomanifest" dir has no app-plugin.json at all → skipped let found = discover(&[root.as_path()]); diff --git a/cockpit/ui/src-tauri/src/plugins/manifest.rs b/cockpit/ui/src-tauri/src/plugins/manifest.rs index aaeed59..1f893e4 100644 --- a/cockpit/ui/src-tauri/src/plugins/manifest.rs +++ b/cockpit/ui/src-tauri/src/plugins/manifest.rs @@ -41,7 +41,9 @@ pub struct BuildStep { #[serde(default = "default_build_timeout")] pub timeout: u64, } -fn default_build_timeout() -> u64 { 1_200_000 } +fn default_build_timeout() -> u64 { + 1_200_000 +} #[derive(Debug, Clone, Deserialize)] pub struct Probe { @@ -53,9 +55,15 @@ pub struct Probe { #[serde(default = "default_probe_interval")] pub interval: u64, } -fn default_ok_status() -> Vec { vec![200] } -fn default_probe_timeout() -> u64 { 180_000 } -fn default_probe_interval() -> u64 { 1_000 } +fn default_ok_status() -> Vec { + vec![200] +} +fn default_probe_timeout() -> u64 { + 180_000 +} +fn default_probe_interval() -> u64 { + 1_000 +} #[derive(Debug, Clone, Deserialize, Default)] pub struct WebviewCfg { @@ -69,11 +77,19 @@ pub struct WebviewCfg { #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Default)] #[serde(rename_all = "lowercase")] -pub enum Popups { #[default] Allow, Block } +pub enum Popups { + #[default] + Allow, + Block, +} #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Default)] #[serde(rename_all = "kebab-case")] -pub enum ExternalLinks { #[default] InApp, SystemBrowser } +pub enum ExternalLinks { + #[default] + InApp, + SystemBrowser, +} pub const SUPPORTED_API_VERSIONS: &[u32] = &[1]; @@ -89,7 +105,10 @@ impl Manifest { } /// Effective window title (defaults to `name`). pub fn window_title(&self) -> String { - self.webview.title.clone().unwrap_or_else(|| self.name.clone()) + self.webview + .title + .clone() + .unwrap_or_else(|| self.name.clone()) } pub fn validate(&self) -> Result<(), ManifestError> { if !SUPPORTED_API_VERSIONS.contains(&self.api_version) { @@ -104,7 +123,11 @@ impl Manifest { None => manifest_dir.to_path_buf(), Some(c) => { let p = Path::new(c); - if p.is_absolute() { p.to_path_buf() } else { manifest_dir.join(p) } + if p.is_absolute() { + p.to_path_buf() + } else { + manifest_dir.join(p) + } } } } @@ -152,7 +175,10 @@ mod tests { fn rejects_unknown_api_version() { let json = AUDIENCE_JSON.replace("\"apiVersion\": 1", "\"apiVersion\": 99"); let m = Manifest::from_json(&json).unwrap(); - assert!(matches!(m.validate(), Err(ManifestError::UnsupportedApiVersion(99)))); + assert!(matches!( + m.validate(), + Err(ManifestError::UnsupportedApiVersion(99)) + )); } #[test] @@ -180,6 +206,9 @@ mod tests { fn uses_manifest_dir_when_cwd_is_absent() { let mut m = Manifest::from_json(AUDIENCE_JSON).unwrap(); m.lifecycle.cwd = None; - assert_eq!(m.resolved_cwd(Path::new("/plugins/audience")), Path::new("/plugins/audience")); + assert_eq!( + m.resolved_cwd(Path::new("/plugins/audience")), + Path::new("/plugins/audience") + ); } } diff --git a/cockpit/ui/src-tauri/src/plugins/mod.rs b/cockpit/ui/src-tauri/src/plugins/mod.rs index ada8e7a..1ec1253 100644 --- a/cockpit/ui/src-tauri/src/plugins/mod.rs +++ b/cockpit/ui/src-tauri/src/plugins/mod.rs @@ -1,6 +1,6 @@ -pub mod manifest; pub mod discovery; +pub mod manager; +pub mod manifest; pub mod seams; pub mod seams_impl; pub mod state; -pub mod manager; diff --git a/cockpit/ui/src-tauri/src/plugins/seams.rs b/cockpit/ui/src-tauri/src/plugins/seams.rs index baf5839..a88bb58 100644 --- a/cockpit/ui/src-tauri/src/plugins/seams.rs +++ b/cockpit/ui/src-tauri/src/plugins/seams.rs @@ -13,7 +13,13 @@ pub trait Spawner: Send + Sync { /// `vars` are interpreted by the impl: for `start`/`stop` they are process /// env; for the build step they are the manifest's `build.args` (which the /// real impl must surface as Docker `--build-arg`, not env — see Phase 4). - fn run_to_completion(&self, cmd: &str, cwd: &Path, vars: &BTreeMap, timeout_ms: u64) -> i32; + fn run_to_completion( + &self, + cmd: &str, + cwd: &Path, + vars: &BTreeMap, + timeout_ms: u64, + ) -> i32; /// Spawn a long-running command (start). Returns a child id. fn spawn(&self, cmd: &str, cwd: &Path, env: &BTreeMap) -> u64; /// Has the spawned child exited? (drives crash→error) @@ -39,26 +45,42 @@ pub mod fakes { use std::sync::Mutex; /// Probe that returns a scripted sequence of statuses per URL. - pub struct ScriptedProbe { pub responses: Mutex>>> } + pub struct ScriptedProbe { + pub responses: Mutex>>>, + } impl Probe for ScriptedProbe { fn probe(&self, url: &str) -> Option { let mut map = self.responses.lock().unwrap(); let q = map.get_mut(url).expect("no script for url"); - if q.len() == 1 { q[0] } else { q.remove(0) } + if q.len() == 1 { + q[0] + } else { + q.remove(0) + } } } #[derive(Default)] - pub struct FakeClock { pub t: Mutex } + pub struct FakeClock { + pub t: Mutex, + } impl Clock for FakeClock { - fn now_ms(&self) -> u64 { *self.t.lock().unwrap() } - fn sleep_ms(&self, ms: u64) { *self.t.lock().unwrap() += ms; } // advance, don't block + fn now_ms(&self) -> u64 { + *self.t.lock().unwrap() + } + fn sleep_ms(&self, ms: u64) { + *self.t.lock().unwrap() += ms; + } // advance, don't block } #[derive(Default)] - pub struct RecordingSink { pub states: Mutex> } + pub struct RecordingSink { + pub states: Mutex>, + } impl EventSink for RecordingSink { - fn emit_state(&self, id: &str, s: &str) { self.states.lock().unwrap().push((id.into(), s.into())); } + fn emit_state(&self, id: &str, s: &str) { + self.states.lock().unwrap().push((id.into(), s.into())); + } } pub struct FakeSpawner { @@ -67,9 +89,21 @@ pub mod fakes { pub exited: Mutex, } impl Spawner for FakeSpawner { - fn run_to_completion(&self, _c: &str, _w: &Path, _e: &BTreeMap, _t: u64) -> i32 { self.build_exit } - fn spawn(&self, _c: &str, _w: &Path, _e: &BTreeMap) -> u64 { self.start_child_id } - fn has_exited(&self, _id: u64) -> bool { *self.exited.lock().unwrap() } + fn run_to_completion( + &self, + _c: &str, + _w: &Path, + _e: &BTreeMap, + _t: u64, + ) -> i32 { + self.build_exit + } + fn spawn(&self, _c: &str, _w: &Path, _e: &BTreeMap) -> u64 { + self.start_child_id + } + fn has_exited(&self, _id: u64) -> bool { + *self.exited.lock().unwrap() + } fn kill(&self, _id: u64) {} } } diff --git a/cockpit/ui/src-tauri/src/plugins/seams_impl.rs b/cockpit/ui/src-tauri/src/plugins/seams_impl.rs index 518cc5f..0e3ebd4 100644 --- a/cockpit/ui/src-tauri/src/plugins/seams_impl.rs +++ b/cockpit/ui/src-tauri/src/plugins/seams_impl.rs @@ -11,7 +11,7 @@ impl Probe for HttpProbe { match ureq::get(url).timeout(Duration::from_millis(2000)).call() { Ok(resp) => Some(resp.status()), Err(ureq::Error::Status(code, _)) => Some(code), // 3xx/4xx still a status - Err(_) => None, // connection refused etc. + Err(_) => None, // connection refused etc. } } } @@ -74,9 +74,7 @@ impl Spawner for ShellSpawner { for (k, v) in vars { c.arg("--build-arg").arg(format!("{k}={v}")); } - c.status() - .map(|s| s.code().unwrap_or(-1)) - .unwrap_or(-1) + c.status().map(|s| s.code().unwrap_or(-1)).unwrap_or(-1) } /// Used for the START step: `env` are runtime env vars applied to the process. diff --git a/cockpit/ui/src-tauri/src/plugins/state.rs b/cockpit/ui/src-tauri/src/plugins/state.rs index 84bb1d3..b283e33 100644 --- a/cockpit/ui/src-tauri/src/plugins/state.rs +++ b/cockpit/ui/src-tauri/src/plugins/state.rs @@ -24,25 +24,39 @@ fn poll_until_ok(cfg: &ProbeCfg, probe: &dyn Probe, clock: &dyn Clock) -> bool { let start = clock.now_ms(); loop { if let Some(code) = probe.probe(&cfg.url) { - if cfg.ok_status.contains(&code) { return true; } + if cfg.ok_status.contains(&code) { + return true; + } + } + if clock.now_ms().saturating_sub(start) >= cfg.timeout { + return false; } - if clock.now_ms().saturating_sub(start) >= cfg.timeout { return false; } clock.sleep_ms(cfg.interval); } } fn both_probes_pass(m: &Manifest, probe: &dyn Probe) -> bool { - let h = probe.probe(&m.lifecycle.health.url) - .map(|c| m.lifecycle.health.ok_status.contains(&c)).unwrap_or(false); - if !h { return false; } - probe.probe(&m.lifecycle.ready.url) - .map(|c| m.lifecycle.ready.ok_status.contains(&c)).unwrap_or(false) + let h = probe + .probe(&m.lifecycle.health.url) + .map(|c| m.lifecycle.health.ok_status.contains(&c)) + .unwrap_or(false); + if !h { + return false; + } + probe + .probe(&m.lifecycle.ready.url) + .map(|c| m.lifecycle.ready.ok_status.contains(&c)) + .unwrap_or(false) } #[allow(clippy::too_many_arguments)] pub fn run_start_sequence( - m: &Manifest, manifest_dir: &Path, - probe: &dyn Probe, spawner: &dyn Spawner, clock: &dyn Clock, sink: &dyn EventSink, + m: &Manifest, + manifest_dir: &Path, + probe: &dyn Probe, + spawner: &dyn Spawner, + clock: &dyn Clock, + sink: &dyn EventSink, images_present: bool, ) -> StartOutcome { let cwd = m.resolved_cwd(manifest_dir); @@ -52,14 +66,20 @@ pub fn run_start_sequence( if !images_present { sink.emit_state(&m.id, BUILDING); let code = spawner.run_to_completion(&build.cmd, &cwd, &build.args, build.timeout); - if code != 0 { sink.emit_state(&m.id, ERROR); return StartOutcome::Error(format!("build exited {code}")); } + if code != 0 { + sink.emit_state(&m.id, ERROR); + return StartOutcome::Error(format!("build exited {code}")); + } } } // Step 1: adopt check — both probes already up → adopt (not owned) if both_probes_pass(m, probe) { sink.emit_state(&m.id, HEALTHY); - return StartOutcome::Healthy { owned: false, child_id: None }; + return StartOutcome::Healthy { + owned: false, + child_id: None, + }; } // Step 2: spawn start (owned) @@ -69,21 +89,31 @@ pub fn run_start_sequence( // Step 3: health then ready sink.emit_state(&m.id, HEALTH_PROBING); if !poll_until_ok(&m.lifecycle.health, probe, clock) { - sink.emit_state(&m.id, ERROR); return StartOutcome::Error("health probe timed out".into()); + sink.emit_state(&m.id, ERROR); + return StartOutcome::Error("health probe timed out".into()); } sink.emit_state(&m.id, READY_PROBING); if !poll_until_ok(&m.lifecycle.ready, probe, clock) { - sink.emit_state(&m.id, ERROR); return StartOutcome::Error("ready probe timed out".into()); + sink.emit_state(&m.id, ERROR); + return StartOutcome::Error("ready probe timed out".into()); } // Step 4: healthy sink.emit_state(&m.id, HEALTHY); - StartOutcome::Healthy { owned: true, child_id: Some(child_id) } + StartOutcome::Healthy { + owned: true, + child_id: Some(child_id), + } } /// If the owned child has exited, emit `error` and return true. The caller is /// responsible for destroying the kept-alive webview on a true return (§4). -pub fn check_crash(plugin_id: &str, child_id: u64, spawner: &dyn Spawner, sink: &dyn EventSink) -> bool { +pub fn check_crash( + plugin_id: &str, + child_id: u64, + spawner: &dyn Spawner, + sink: &dyn EventSink, +) -> bool { if spawner.has_exited(child_id) { sink.emit_state(plugin_id, ERROR); true @@ -101,71 +131,165 @@ mod tests { use std::sync::Mutex; fn manifest() -> Manifest { - Manifest::from_json(r#"{"id":"audience","name":"Audience","apiVersion":1, + Manifest::from_json( + r#"{"id":"audience","name":"Audience","apiVersion":1, "url":"http://localhost:3000", "lifecycle":{"cwd":"/x","build":{"cmd":"build","timeout":1000}, "start":"up","stop":"down","env":{}, "health":{"url":"h","okStatus":[200],"timeout":5000,"interval":1000}, - "ready":{"url":"r","okStatus":[200,302],"timeout":5000,"interval":1000}}}"#).unwrap() + "ready":{"url":"r","okStatus":[200,302],"timeout":5000,"interval":1000}}}"#, + ) + .unwrap() } #[test] fn cold_start_walks_building_to_healthy_and_owns_the_stack() { - let probe = ScriptedProbe { responses: Mutex::new(HashMap::from([ - // adopt check: both down at first - ("h".to_string(), vec![None, None, Some(200), Some(200)]), - ("r".to_string(), vec![None, None, Some(302)]), - ])) }; - let spawner = FakeSpawner { start_child_id: 7, build_exit: 0, exited: Mutex::new(false) }; + let probe = ScriptedProbe { + responses: Mutex::new(HashMap::from([ + // adopt check: both down at first + ("h".to_string(), vec![None, None, Some(200), Some(200)]), + ("r".to_string(), vec![None, None, Some(302)]), + ])), + }; + let spawner = FakeSpawner { + start_child_id: 7, + build_exit: 0, + exited: Mutex::new(false), + }; let clock = FakeClock::default(); let sink = RecordingSink::default(); - let outcome = run_start_sequence(&manifest(), std::path::Path::new("/x"), - &probe, &spawner, &clock, &sink, /*images_present=*/false); + let outcome = run_start_sequence( + &manifest(), + std::path::Path::new("/x"), + &probe, + &spawner, + &clock, + &sink, + /*images_present=*/ false, + ); - assert_eq!(outcome, StartOutcome::Healthy { owned: true, child_id: Some(7) }); - let states: Vec = sink.states.lock().unwrap().iter().map(|(_, s)| s.clone()).collect(); - assert_eq!(states, vec!["building","starting","health-probing","ready-probing","healthy"]); + assert_eq!( + outcome, + StartOutcome::Healthy { + owned: true, + child_id: Some(7) + } + ); + let states: Vec = sink + .states + .lock() + .unwrap() + .iter() + .map(|(_, s)| s.clone()) + .collect(); + assert_eq!( + states, + vec![ + "building", + "starting", + "health-probing", + "ready-probing", + "healthy" + ] + ); } #[test] fn adopts_when_both_probes_already_pass_and_marks_not_owned() { - let probe = ScriptedProbe { responses: Mutex::new(HashMap::from([ - ("h".to_string(), vec![Some(200)]), - ("r".to_string(), vec![Some(200)]), - ])) }; - let spawner = FakeSpawner { start_child_id: 7, build_exit: 0, exited: Mutex::new(false) }; - let out = run_start_sequence(&manifest(), std::path::Path::new("/x"), - &probe, &spawner, &FakeClock::default(), &RecordingSink::default(), /*images_present=*/true); - assert_eq!(out, StartOutcome::Healthy { owned: false, child_id: None }); + let probe = ScriptedProbe { + responses: Mutex::new(HashMap::from([ + ("h".to_string(), vec![Some(200)]), + ("r".to_string(), vec![Some(200)]), + ])), + }; + let spawner = FakeSpawner { + start_child_id: 7, + build_exit: 0, + exited: Mutex::new(false), + }; + let out = run_start_sequence( + &manifest(), + std::path::Path::new("/x"), + &probe, + &spawner, + &FakeClock::default(), + &RecordingSink::default(), + /*images_present=*/ true, + ); + assert_eq!( + out, + StartOutcome::Healthy { + owned: false, + child_id: None + } + ); } #[test] fn partial_stack_health_only_falls_through_to_spawn() { // health up, ready down at adopt → must NOT adopt; spawn then both come up - let probe = ScriptedProbe { responses: Mutex::new(HashMap::from([ - ("h".to_string(), vec![Some(200), Some(200)]), // adopt(h) ok, later health-poll ok - ("r".to_string(), vec![None, Some(200)]), // adopt(r) down → fall through; ready-poll ok - ])) }; - let spawner = FakeSpawner { start_child_id: 7, build_exit: 0, exited: Mutex::new(false) }; + let probe = ScriptedProbe { + responses: Mutex::new(HashMap::from([ + ("h".to_string(), vec![Some(200), Some(200)]), // adopt(h) ok, later health-poll ok + ("r".to_string(), vec![None, Some(200)]), // adopt(r) down → fall through; ready-poll ok + ])), + }; + let spawner = FakeSpawner { + start_child_id: 7, + build_exit: 0, + exited: Mutex::new(false), + }; let sink = RecordingSink::default(); - let out = run_start_sequence(&manifest(), std::path::Path::new("/x"), - &probe, &spawner, &FakeClock::default(), &sink, true); - assert_eq!(out, StartOutcome::Healthy { owned: true, child_id: Some(7) }); // spawned → owned - let states: Vec = sink.states.lock().unwrap().iter().map(|(_, s)| s.clone()).collect(); + let out = run_start_sequence( + &manifest(), + std::path::Path::new("/x"), + &probe, + &spawner, + &FakeClock::default(), + &sink, + true, + ); + assert_eq!( + out, + StartOutcome::Healthy { + owned: true, + child_id: Some(7) + } + ); // spawned → owned + let states: Vec = sink + .states + .lock() + .unwrap() + .iter() + .map(|(_, s)| s.clone()) + .collect(); assert!(states.contains(&"starting".to_string())); } #[test] fn health_timeout_yields_error() { - let probe = ScriptedProbe { responses: Mutex::new(HashMap::from([ - ("h".to_string(), vec![None]), // never comes up - ("r".to_string(), vec![None]), - ])) }; - let spawner = FakeSpawner { start_child_id: 7, build_exit: 0, exited: Mutex::new(false) }; + let probe = ScriptedProbe { + responses: Mutex::new(HashMap::from([ + ("h".to_string(), vec![None]), // never comes up + ("r".to_string(), vec![None]), + ])), + }; + let spawner = FakeSpawner { + start_child_id: 7, + build_exit: 0, + exited: Mutex::new(false), + }; let sink = RecordingSink::default(); - let out = run_start_sequence(&manifest(), std::path::Path::new("/x"), - &probe, &spawner, &FakeClock::default(), &sink, true); + let out = run_start_sequence( + &manifest(), + std::path::Path::new("/x"), + &probe, + &spawner, + &FakeClock::default(), + &sink, + true, + ); assert!(matches!(out, StartOutcome::Error(_))); assert_eq!(sink.states.lock().unwrap().last().unwrap().1, "error"); } @@ -173,16 +297,35 @@ mod tests { #[test] fn ready_timeout_yields_error_after_health_passes() { // health comes up, but ready never does → error on the ready path - let probe = ScriptedProbe { responses: Mutex::new(HashMap::from([ - ("h".to_string(), vec![None, Some(200)]), // adopt(h) down → fall through; health-poll up - ("r".to_string(), vec![None]), // ready never ok → times out - ])) }; - let spawner = FakeSpawner { start_child_id: 7, build_exit: 0, exited: Mutex::new(false) }; + let probe = ScriptedProbe { + responses: Mutex::new(HashMap::from([ + ("h".to_string(), vec![None, Some(200)]), // adopt(h) down → fall through; health-poll up + ("r".to_string(), vec![None]), // ready never ok → times out + ])), + }; + let spawner = FakeSpawner { + start_child_id: 7, + build_exit: 0, + exited: Mutex::new(false), + }; let sink = RecordingSink::default(); - let out = run_start_sequence(&manifest(), std::path::Path::new("/x"), - &probe, &spawner, &FakeClock::default(), &sink, true); + let out = run_start_sequence( + &manifest(), + std::path::Path::new("/x"), + &probe, + &spawner, + &FakeClock::default(), + &sink, + true, + ); assert!(matches!(out, StartOutcome::Error(_))); - let states: Vec = sink.states.lock().unwrap().iter().map(|(_, s)| s.clone()).collect(); + let states: Vec = sink + .states + .lock() + .unwrap() + .iter() + .map(|(_, s)| s.clone()) + .collect(); // it must have gotten past health-probing to ready-probing before erroring assert!(states.contains(&"ready-probing".to_string())); assert_eq!(states.last().unwrap(), "error"); @@ -190,18 +333,35 @@ mod tests { #[test] fn build_failure_yields_error_before_spawn() { - let probe = ScriptedProbe { responses: Mutex::new(HashMap::new()) }; - let spawner = FakeSpawner { start_child_id: 7, build_exit: 2, exited: Mutex::new(false) }; + let probe = ScriptedProbe { + responses: Mutex::new(HashMap::new()), + }; + let spawner = FakeSpawner { + start_child_id: 7, + build_exit: 2, + exited: Mutex::new(false), + }; let sink = RecordingSink::default(); - let out = run_start_sequence(&manifest(), std::path::Path::new("/x"), - &probe, &spawner, &FakeClock::default(), &sink, /*images_present=*/false); + let out = run_start_sequence( + &manifest(), + std::path::Path::new("/x"), + &probe, + &spawner, + &FakeClock::default(), + &sink, + /*images_present=*/ false, + ); assert!(matches!(out, StartOutcome::Error(_))); assert_eq!(sink.states.lock().unwrap()[0].1, "building"); } #[test] fn crash_while_healthy_transitions_to_error() { - let spawner = FakeSpawner { start_child_id: 7, build_exit: 0, exited: Mutex::new(true) }; + let spawner = FakeSpawner { + start_child_id: 7, + build_exit: 0, + exited: Mutex::new(true), + }; let sink = RecordingSink::default(); // Given an owned, healthy plugin whose child has exited, the watcher flips to error. let flipped = check_crash(&"audience".to_string(), 7, &spawner, &sink); @@ -211,7 +371,11 @@ mod tests { #[test] fn no_crash_when_child_alive() { - let spawner = FakeSpawner { start_child_id: 7, build_exit: 0, exited: Mutex::new(false) }; + let spawner = FakeSpawner { + start_child_id: 7, + build_exit: 0, + exited: Mutex::new(false), + }; let sink = RecordingSink::default(); let flipped = check_crash(&"audience".to_string(), 7, &spawner, &sink); assert!(!flipped); diff --git a/crates/fleet-core/src/event.rs b/crates/fleet-core/src/event.rs index a58f15d..de6d8fc 100644 --- a/crates/fleet-core/src/event.rs +++ b/crates/fleet-core/src/event.rs @@ -114,18 +114,28 @@ pub enum ErrorScope { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(tag = "command", rename_all = "snake_case")] pub enum Command { - Halt { cmd_id: String }, - Resume { cmd_id: String }, - Abandon { cmd_id: String }, + Halt { + cmd_id: String, + }, + Resume { + cmd_id: String, + }, + Abandon { + cmd_id: String, + }, /// The T3 final gate: human ships the PR from `NeedsHuman`. - Ship { cmd_id: String }, + Ship { + cmd_id: String, + }, /// Approve the frozen test set; may carry an edited set (T2/T3). ApproveOracle { cmd_id: String, #[serde(skip_serializing_if = "Option::is_none")] edited_test_files: Option>, }, - RejectOracle { cmd_id: String }, + RejectOracle { + cmd_id: String, + }, } impl Command { @@ -188,7 +198,12 @@ mod tests { #[test] fn metric_roundtrips() { - let e = Event::Metric { tokens_in: 9457, tokens_out: 4, cost_usd: 0.207, elapsed_ms: 6734 }; + let e = Event::Metric { + tokens_in: 9457, + tokens_out: 4, + cost_usd: 0.207, + elapsed_ms: 6734, + }; let json = serde_json::to_string(&e).unwrap(); let back: Event = serde_json::from_str(&json).unwrap(); assert_eq!(e, back); @@ -203,7 +218,10 @@ mod tests { #[test] fn approve_oracle_optional_edits_omitted_when_none() { - let c = Command::ApproveOracle { cmd_id: "x".into(), edited_test_files: None }; + let c = Command::ApproveOracle { + cmd_id: "x".into(), + edited_test_files: None, + }; let v = serde_json::to_value(&c).unwrap(); assert_eq!(v["command"], "approve_oracle"); assert!(v.get("edited_test_files").is_none()); diff --git a/crates/fleet-core/src/gate.rs b/crates/fleet-core/src/gate.rs index a83be3d..4994e4d 100644 --- a/crates/fleet-core/src/gate.rs +++ b/crates/fleet-core/src/gate.rs @@ -12,7 +12,9 @@ pub struct GateConfig { impl Default for GateConfig { fn default() -> Self { - Self { min_review_rounds: 3 } + Self { + min_review_rounds: 3, + } } } @@ -82,13 +84,17 @@ mod tests { #[test] fn first_round_has_no_prior_to_compare() { // round floor of 1 to isolate the prev=None branch - let cfg = GateConfig { min_review_rounds: 1 }; + let cfg = GateConfig { + min_review_rounds: 1, + }; assert!(gate_met(cfg, snap(1, 0, None, true))); } #[test] fn respects_custom_floor() { - let cfg = GateConfig { min_review_rounds: 1 }; + let cfg = GateConfig { + min_review_rounds: 1, + }; assert!(gate_met(cfg, snap(1, 0, Some(0), true))); } } diff --git a/crates/fleet-core/src/lib.rs b/crates/fleet-core/src/lib.rs index db7c56b..fc61ec0 100644 --- a/crates/fleet-core/src/lib.rs +++ b/crates/fleet-core/src/lib.rs @@ -11,9 +11,7 @@ mod phase; mod tier; mod transition; -pub use event::{ - ArtifactKind, Command, ErrorScope, Event, IterationKind, LogStream, Severity, -}; +pub use event::{ArtifactKind, Command, ErrorScope, Event, IterationKind, LogStream, Severity}; pub use gate::{gate_met, GateConfig, ReviewSnapshot}; pub use phase::{Phase, TERMINAL_PHASE_STRS}; pub use tier::Tier; diff --git a/crates/fleet-core/src/phase.rs b/crates/fleet-core/src/phase.rs index 4f5b250..4a97c11 100644 --- a/crates/fleet-core/src/phase.rs +++ b/crates/fleet-core/src/phase.rs @@ -36,7 +36,10 @@ impl Phase { /// Phases in which the agent/container is actively running, so cost/stall /// caps and oracle-tampering apply. pub fn is_agent_active(self) -> bool { - matches!(self, Phase::Spec | Phase::Building | Phase::Checking | Phase::Reviewing) + matches!( + self, + Phase::Spec | Phase::Building | Phase::Checking | Phase::Reviewing + ) } /// Non-terminal phases can always be halted or fail fatally. @@ -73,9 +76,16 @@ mod tests { Phase::NeedsHuman, Phase::Halted, ] { - let s = serde_json::to_value(p).unwrap().as_str().unwrap().to_string(); - assert_eq!(TERMINAL_PHASE_STRS.contains(&s.as_str()), p.is_terminal(), - "{s} membership must equal is_terminal()"); + let s = serde_json::to_value(p) + .unwrap() + .as_str() + .unwrap() + .to_string(); + assert_eq!( + TERMINAL_PHASE_STRS.contains(&s.as_str()), + p.is_terminal(), + "{s} membership must equal is_terminal()" + ); } } } diff --git a/crates/fleet-core/src/transition.rs b/crates/fleet-core/src/transition.rs index 45dc153..5ca0689 100644 --- a/crates/fleet-core/src/transition.rs +++ b/crates/fleet-core/src/transition.rs @@ -25,7 +25,9 @@ pub enum Trigger { EmptyDiff, /// A review round finished; `gate_met` is the evidence-based gate verdict /// (green + no unresolved blockers + non-increasing + round floor). - ReviewFinished { gate_met: bool }, + ReviewFinished { + gate_met: bool, + }, /// Trial merge into a fresh base succeeded. MergeClean, /// Trial merge hit conflicts. @@ -189,36 +191,55 @@ mod tests { #[test] fn checks_fail_loops_back_to_building() { - assert_eq!(transition(Checking, Tier::T1, Trigger::ChecksFailed), Some(Building)); + assert_eq!( + transition(Checking, Tier::T1, Trigger::ChecksFailed), + Some(Building) + ); } #[test] fn review_not_met_loops_back_to_building() { assert_eq!( - transition(Reviewing, Tier::T1, Trigger::ReviewFinished { gate_met: false }), + transition( + Reviewing, + Tier::T1, + Trigger::ReviewFinished { gate_met: false } + ), Some(Building) ); } #[test] fn empty_diff_is_no_change_not_failure() { - assert_eq!(transition(Checking, Tier::T1, Trigger::EmptyDiff), Some(NoChange)); + assert_eq!( + transition(Checking, Tier::T1, Trigger::EmptyDiff), + Some(NoChange) + ); } #[test] fn merge_conflict_needs_human() { - assert_eq!(transition(MergeCheck, Tier::T1, Trigger::MergeConflict), Some(NeedsHuman)); + assert_eq!( + transition(MergeCheck, Tier::T1, Trigger::MergeConflict), + Some(NeedsHuman) + ); } #[test] fn pr_dirty_after_open_needs_human() { - assert_eq!(transition(PrOpen, Tier::T1, Trigger::PrDirty), Some(NeedsHuman)); + assert_eq!( + transition(PrOpen, Tier::T1, Trigger::PrDirty), + Some(NeedsHuman) + ); } #[test] fn cap_breach_only_interrupts_agent_active_phases() { // Active phase: interrupted. - assert_eq!(transition(Building, Tier::T1, Trigger::CapBreach), Some(NeedsHuman)); + assert_eq!( + transition(Building, Tier::T1, Trigger::CapBreach), + Some(NeedsHuman) + ); // Daemon-only phase: cap breach is not meaningful → invalid. assert_eq!(transition(MergeCheck, Tier::T1, Trigger::CapBreach), None); } @@ -230,7 +251,15 @@ mod tests { #[test] fn fatal_error_from_any_active_phase_fails() { - for p in [Provisioning, Spec, Building, Checking, Reviewing, MergeCheck, PrOpen] { + for p in [ + Provisioning, + Spec, + Building, + Checking, + Reviewing, + MergeCheck, + PrOpen, + ] { assert_eq!(transition(p, Tier::T1, Trigger::FatalError), Some(Failed)); } } @@ -239,25 +268,46 @@ mod tests { fn halt_and_resume_round_trip() { assert_eq!(transition(Building, Tier::T1, Trigger::Halt), Some(Halted)); // Resume re-provisions (reuses the volume) before re-entering the loop. - assert_eq!(transition(Halted, Tier::T1, Trigger::Resume), Some(Provisioning)); + assert_eq!( + transition(Halted, Tier::T1, Trigger::Resume), + Some(Provisioning) + ); } #[test] fn resume_goes_to_provisioning_not_building() { - assert_eq!(transition(NeedsHuman, Tier::T1, Trigger::Resume), Some(Provisioning)); - assert_eq!(transition(Halted, Tier::T1, Trigger::Resume), Some(Provisioning)); + assert_eq!( + transition(NeedsHuman, Tier::T1, Trigger::Resume), + Some(Provisioning) + ); + assert_eq!( + transition(Halted, Tier::T1, Trigger::Resume), + Some(Provisioning) + ); } #[test] fn abandon_from_needs_human_fails() { - assert_eq!(transition(NeedsHuman, Tier::T1, Trigger::Abandon), Some(Failed)); + assert_eq!( + transition(NeedsHuman, Tier::T1, Trigger::Abandon), + Some(Failed) + ); } #[test] fn terminal_phases_reject_everything() { for p in [Done, NoChange, Failed] { - for t in [Trigger::Resume, Trigger::Start, Trigger::Halt, Trigger::FatalError] { - assert_eq!(transition(p, Tier::T1, t), None, "{p:?} should reject {t:?}"); + for t in [ + Trigger::Resume, + Trigger::Start, + Trigger::Halt, + Trigger::FatalError, + ] { + assert_eq!( + transition(p, Tier::T1, t), + None, + "{p:?} should reject {t:?}" + ); } } } @@ -273,9 +323,15 @@ mod tests { #[test] fn retries_exhausted_parks_agent_phases_at_needs_human() { for p in [Spec, Building, Reviewing] { - assert_eq!(transition(p, Tier::T1, Trigger::RetriesExhausted), Some(NeedsHuman)); + assert_eq!( + transition(p, Tier::T1, Trigger::RetriesExhausted), + Some(NeedsHuman) + ); } // Not meaningful from a daemon-only phase → invalid. - assert_eq!(transition(MergeCheck, Tier::T1, Trigger::RetriesExhausted), None); + assert_eq!( + transition(MergeCheck, Tier::T1, Trigger::RetriesExhausted), + None + ); } } diff --git a/crates/fleetd/src/bin/run_once.rs b/crates/fleetd/src/bin/run_once.rs index b2d0648..bf9bd03 100644 --- a/crates/fleetd/src/bin/run_once.rs +++ b/crates/fleetd/src/bin/run_once.rs @@ -39,7 +39,10 @@ async fn main() { .to_string() }); - let millis = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_millis(); + let millis = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_millis(); let unit_id = format!("u{millis}"); let branch = format!("agent/{unit_id}"); @@ -86,7 +89,14 @@ async fn main() { drop(cmd_tx); // T1 run: no interactive commands println!("== run-once: unit {unit_id} on {branch} (cap ${usd_cap}) =="); - let driver = tokio::spawn(run(runner, forge, spec, RunCtx::standalone(), cmd_rx, evt_tx)); + let driver = tokio::spawn(run( + runner, + forge, + spec, + RunCtx::standalone(), + cmd_rx, + evt_tx, + )); while let Some(env) = evt_rx.recv().await { print_event(&env); @@ -100,21 +110,44 @@ async fn main() { fn print_event(env: &EventEnvelope) { match &env.event { - Event::PhaseChanged { from, to, reason, .. } => { - let why = reason.as_deref().map(|r| format!(" ({r})")).unwrap_or_default(); + Event::PhaseChanged { + from, to, reason, .. + } => { + let why = reason + .as_deref() + .map(|r| format!(" ({r})")) + .unwrap_or_default(); println!("[{:>3}] {from:?} -> {to:?}{why}", env.seq); } Event::Iteration { kind, n } => println!("[{:>3}] iteration {kind:?} #{n}", env.seq), - Event::Metric { cost_usd, tokens_in, tokens_out, .. } => { - println!("[{:>3}] $ {cost_usd:.4} (in {tokens_in} / out {tokens_out})", env.seq) + Event::Metric { + cost_usd, + tokens_in, + tokens_out, + .. + } => { + println!( + "[{:>3}] $ {cost_usd:.4} (in {tokens_in} / out {tokens_out})", + env.seq + ) + } + Event::Finding { round, title, .. } => { + println!("[{:>3}] review r{round}: {title}", env.seq) + } + Event::Artifact { kind, reference } => { + println!("[{:>3}] artifact {kind:?}: {reference}", env.seq) } - Event::Finding { round, title, .. } => println!("[{:>3}] review r{round}: {title}", env.seq), - Event::Artifact { kind, reference } => println!("[{:>3}] artifact {kind:?}: {reference}", env.seq), Event::OracleProposed { test_files, .. } => { - println!("[{:>3}] oracle proposed {} test line(s)", env.seq, test_files.len()) + println!( + "[{:>3}] oracle proposed {} test line(s)", + env.seq, + test_files.len() + ) } Event::Blocked { reason, .. } => println!("[{:>3}] BLOCKED: {reason}", env.seq), - Event::Error { scope, detail, .. } => println!("[{:>3}] ERROR {scope:?}: {detail}", env.seq), + Event::Error { scope, detail, .. } => { + println!("[{:>3}] ERROR {scope:?}: {detail}", env.seq) + } Event::Done { result } => println!("[{:>3}] DONE: {result}", env.seq), Event::Log { stream, line } => println!("[{:>3}] {stream:?}| {line}", env.seq), } diff --git a/crates/fleetd/src/claude_meter.rs b/crates/fleetd/src/claude_meter.rs index 4e4679d..11f150a 100644 --- a/crates/fleetd/src/claude_meter.rs +++ b/crates/fleetd/src/claude_meter.rs @@ -27,8 +27,15 @@ pub fn parse_usage(stdout: &[String]) -> Option { .and_then(|u| u.get("output_tokens")) .and_then(|n| n.as_u64()) .unwrap_or(0); - let cost_usd = v.get("total_cost_usd").and_then(|c| c.as_f64()).unwrap_or(0.0); - return Some(Usage { tokens_in, tokens_out, cost_usd }); + let cost_usd = v + .get("total_cost_usd") + .and_then(|c| c.as_f64()) + .unwrap_or(0.0); + return Some(Usage { + tokens_in, + tokens_out, + cost_usd, + }); } None } diff --git a/crates/fleetd/src/docsource.rs b/crates/fleetd/src/docsource.rs index 93d991f..e8ae50f 100644 --- a/crates/fleetd/src/docsource.rs +++ b/crates/fleetd/src/docsource.rs @@ -14,8 +14,12 @@ pub enum DocError { #[async_trait] pub trait DocSource: Send + Sync { - async fn read(&self, repo_url: &str, base_branch: &str, doc_path: &str) - -> Result; + async fn read( + &self, + repo_url: &str, + base_branch: &str, + doc_path: &str, + ) -> Result; } /// Canned content keyed only by `doc_path`; a path of "missing.md" → NotFound. @@ -23,7 +27,11 @@ pub struct FakeDocSource { pub content: String, } impl FakeDocSource { - pub fn new(content: &str) -> Self { Self { content: content.into() } } + pub fn new(content: &str) -> Self { + Self { + content: content.into(), + } + } } #[async_trait] impl DocSource for FakeDocSource { @@ -37,9 +45,15 @@ impl DocSource for FakeDocSource { /// Validate clone inputs before handing them to `git`. Rejects flag-smuggling /// (leading `-`), non-https schemes, and unsafe doc paths (absolute or `..`). -fn validate_clone_inputs(repo_url: &str, base_branch: &str, doc_path: &str) -> Result<(), DocError> { +fn validate_clone_inputs( + repo_url: &str, + base_branch: &str, + doc_path: &str, +) -> Result<(), DocError> { if repo_url.starts_with('-') || base_branch.starts_with('-') { - return Err(DocError::Failed("repo_url/base_branch must not start with '-'".into())); + return Err(DocError::Failed( + "repo_url/base_branch must not start with '-'".into(), + )); } if !repo_url.starts_with("https://") { return Err(DocError::Failed("repo_url must be an https:// URL".into())); @@ -51,9 +65,15 @@ fn validate_clone_inputs(repo_url: &str, base_branch: &str, doc_path: &str) -> R let p = std::path::Path::new(doc_path); if p.is_absolute() || doc_path.is_empty() - || p.components().any(|c| matches!(c, std::path::Component::ParentDir - | std::path::Component::Prefix(_) - | std::path::Component::RootDir)) { + || p.components().any(|c| { + matches!( + c, + std::path::Component::ParentDir + | std::path::Component::Prefix(_) + | std::path::Component::RootDir + ) + }) + { return Err(DocError::NotFound(doc_path.into())); } Ok(()) @@ -62,8 +82,16 @@ fn validate_clone_inputs(repo_url: &str, base_branch: &str, doc_path: &str) -> R /// Shallow-clones the base branch to a temp dir, reads the file, and removes the /// dir on drop regardless of outcome (a failed/empty swarm has no driver to clean up). pub struct GitDocSource; -impl GitDocSource { pub fn new() -> Self { Self } } -impl Default for GitDocSource { fn default() -> Self { Self::new() } } +impl GitDocSource { + pub fn new() -> Self { + Self + } +} +impl Default for GitDocSource { + fn default() -> Self { + Self::new() + } +} /// Per-call sequence so two concurrent real swarms never collide on the same temp /// clone dir (which would make one's drop-guard delete the other's clone). @@ -71,15 +99,25 @@ static CLONE_SEQ: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::n struct TempClone(std::path::PathBuf); impl Drop for TempClone { - fn drop(&mut self) { let _ = std::fs::remove_dir_all(&self.0); } + fn drop(&mut self) { + let _ = std::fs::remove_dir_all(&self.0); + } } #[async_trait] impl DocSource for GitDocSource { - async fn read(&self, repo_url: &str, base_branch: &str, doc_path: &str) -> Result { + async fn read( + &self, + repo_url: &str, + base_branch: &str, + doc_path: &str, + ) -> Result { validate_clone_inputs(repo_url, base_branch, doc_path)?; - let dir = std::env::temp_dir().join(format!("cc-plan-{}-{}", std::process::id(), - CLONE_SEQ.fetch_add(1, std::sync::atomic::Ordering::Relaxed))); + let dir = std::env::temp_dir().join(format!( + "cc-plan-{}-{}", + std::process::id(), + CLONE_SEQ.fetch_add(1, std::sync::atomic::Ordering::Relaxed) + )); let _guard = TempClone(dir.clone()); let ok = tokio::process::Command::new("git") .args(["clone", "--depth", "1", "--branch"]) @@ -87,19 +125,23 @@ impl DocSource for GitDocSource { .arg("--") .arg(repo_url) .arg(&dir) - .output().await + .output() + .await .map_err(|e| DocError::Failed(e.to_string()))?; if !ok.status.success() { return Err(DocError::Failed(String::from_utf8_lossy(&ok.stderr).into())); } - let base = tokio::fs::canonicalize(&dir).await + let base = tokio::fs::canonicalize(&dir) + .await .map_err(|e| DocError::Failed(e.to_string()))?; - let target = tokio::fs::canonicalize(base.join(doc_path)).await + let target = tokio::fs::canonicalize(base.join(doc_path)) + .await .map_err(|_| DocError::NotFound(doc_path.into()))?; if !target.starts_with(&base) { return Err(DocError::NotFound(doc_path.into())); } - tokio::fs::read_to_string(&target).await + tokio::fs::read_to_string(&target) + .await .map_err(|_| DocError::NotFound(doc_path.into())) // _guard drops here → temp dir removed. } @@ -112,18 +154,25 @@ mod tests { #[tokio::test] async fn fake_doc_source_returns_content_or_not_found() { let d = FakeDocSource::new("# spec\n- a\n- b\n"); - assert!(d.read("u", "main", "spec.md").await.unwrap().contains("spec")); - assert!(matches!(d.read("u", "main", "missing.md").await, Err(DocError::NotFound(_)))); + assert!(d + .read("u", "main", "spec.md") + .await + .unwrap() + .contains("spec")); + assert!(matches!( + d.read("u", "main", "missing.md").await, + Err(DocError::NotFound(_)) + )); } #[test] fn validate_clone_inputs_rejects_flag_and_traversal_and_scheme() { assert!(validate_clone_inputs("https://h/r", "main", "spec.md").is_ok()); - assert!(validate_clone_inputs("-x", "main", "spec.md").is_err()); // flag smuggle (url) + assert!(validate_clone_inputs("-x", "main", "spec.md").is_err()); // flag smuggle (url) assert!(validate_clone_inputs("https://h/r", "--upload-pack=x", "s").is_err()); // flag smuggle (branch) - assert!(validate_clone_inputs("git@h:r", "main", "spec.md").is_err()); // non-https + assert!(validate_clone_inputs("git@h:r", "main", "spec.md").is_err()); // non-https assert!(validate_clone_inputs("https://h/r", "main", "../etc/passwd").is_err()); // traversal - assert!(validate_clone_inputs("https://h/r", "main", "/etc/passwd").is_err()); // absolute - assert!(validate_clone_inputs("https://h/r", "main", "").is_err()); // empty + assert!(validate_clone_inputs("https://h/r", "main", "/etc/passwd").is_err()); // absolute + assert!(validate_clone_inputs("https://h/r", "main", "").is_err()); // empty } } diff --git a/crates/fleetd/src/driver.rs b/crates/fleetd/src/driver.rs index 4d60150..cd50363 100644 --- a/crates/fleetd/src/driver.rs +++ b/crates/fleetd/src/driver.rs @@ -3,7 +3,9 @@ //! contract and honoring inbound commands. Fully exercisable against the fakes. use crate::forge::{Forge, MergeResult, Mergeability}; -use crate::retry::{classify, rl_base_secs, rl_cap_secs, rl_max_wait_secs, Backoff, StepOutcome, RL_REASON}; +use crate::retry::{ + classify, rl_base_secs, rl_cap_secs, rl_max_wait_secs, Backoff, StepOutcome, RL_REASON, +}; use crate::runner::{ExecOutput, Handle, Runner, UnitSpec}; use crate::{claude_meter, steps}; use fleet_core::{ @@ -152,7 +154,12 @@ impl Run { match transition(from, self.spec.tier, trigger) { Some(next) => { self.phase = next; - self.emit(Event::PhaseChanged { from, to: next, reason, cmd_id }); + self.emit(Event::PhaseChanged { + from, + to: next, + reason, + cmd_id, + }); } None => { self.emit(Event::Error { @@ -262,7 +269,10 @@ impl Run { } }; for line in &out.stdout { - self.emit(Event::Log { stream, line: line.clone() }); + self.emit(Event::Log { + stream, + line: line.clone(), + }); } if out.usage.is_none() { out.usage = claude_meter::parse_usage(&out.stdout); @@ -316,7 +326,8 @@ impl Run { retryable: false, detail: format!( "command {} not valid in {:?}", - other.cmd_id(), self.phase + other.cmd_id(), + self.phase ), }), // keep waiting on the same `until` // Channel closed (no interactive commands, e.g. run_once): @@ -340,7 +351,10 @@ impl Run { async fn read_oracle_files(&mut self) -> Option> { // Scope the immutable field borrows so they end before the &mut self emit/goto. let result = { - let handle = self.handle.as_ref().expect("handle present in agent-active phase"); + let handle = self + .handle + .as_ref() + .expect("handle present in agent-active phase"); self.runner.read_files(handle, "*.test.js").await }; match result { @@ -351,7 +365,11 @@ impl Run { retryable: false, detail: format!("oracle read failed: {e}"), }); - self.goto(Trigger::OracleTampering, Some("oracle unreadable".into()), None); + self.goto( + Trigger::OracleTampering, + Some("oracle unreadable".into()), + None, + ); None } } @@ -392,8 +410,13 @@ impl Run { cap: None, detail: String::new(), }); - self.permit = - Some(self.permits.clone().acquire_owned().await.expect("semaphore")); + self.permit = Some( + self.permits + .clone() + .acquire_owned() + .await + .expect("semaphore"), + ); } match self.runner.provision(&self.spec).await { Ok(h) => { @@ -416,19 +439,26 @@ impl Run { // On resume the test set is already frozen in the reused volume; // don't re-run/re-charge the oracle or re-trigger approval. if self.resume && self.spec.oracle_frozen { - self.goto(Trigger::OracleFrozen, Some("oracle already frozen".into()), None); + self.goto( + Trigger::OracleFrozen, + Some("oracle already frozen".into()), + None, + ); continue; } if self.check_halt() { continue; } let argv = steps::oracle(&self.spec, self.remaining()); - let Some(out) = - self.agent_exec(IterationKind::Review, 0, LogStream::Agent, true, &argv).await + let Some(out) = self + .agent_exec(IterationKind::Review, 0, LogStream::Agent, true, &argv) + .await else { continue; }; - let Some(frozen) = self.read_oracle_files().await else { continue }; + let Some(frozen) = self.read_oracle_files().await else { + continue; + }; if frozen.is_empty() { self.emit(Event::Error { scope: ErrorScope::System, @@ -464,14 +494,15 @@ impl Run { Command::RejectOracle { .. } => { self.goto(Trigger::OracleRejected, None, Some(cid)) } - Command::Abandon { .. } => { - self.goto(Trigger::Abandon, None, Some(cid)) - } + Command::Abandon { .. } => self.goto(Trigger::Abandon, None, Some(cid)), Command::Halt { .. } => self.goto(Trigger::Halt, None, Some(cid)), other => self.emit(Event::Error { scope: ErrorScope::System, retryable: false, - detail: format!("{} not valid while awaiting oracle", other.cmd_id()), + detail: format!( + "{} not valid while awaiting oracle", + other.cmd_id() + ), }), } } @@ -489,8 +520,9 @@ impl Run { _ => "none".into(), }; let argv = steps::build(&self.spec, &findings, self.remaining()); - let Some(out) = - self.agent_exec(IterationKind::Build, n, LogStream::Agent, true, &argv).await + let Some(out) = self + .agent_exec(IterationKind::Build, n, LogStream::Agent, true, &argv) + .await else { continue; }; @@ -501,7 +533,11 @@ impl Run { // The daemon commits the agent's work (agents edit but don't commit), // so the branch carries the change into the bundle/PR. let handle = self.handle.clone().expect("commit without a handle"); - if let Err(e) = self.runner.commit_all(&handle, &format!("wip: build {n}")).await { + if let Err(e) = self + .runner + .commit_all(&handle, &format!("wip: build {n}")) + .await + { self.emit(Event::Error { scope: ErrorScope::System, retryable: false, @@ -516,22 +552,29 @@ impl Run { continue; } if let Some(frozen_hash) = self.oracle_hash.clone() { - let Some(current) = self.read_oracle_files().await else { continue }; + let Some(current) = self.read_oracle_files().await else { + continue; + }; if hash_oracle(¤t) != frozen_hash { self.emit(Event::Blocked { reason: "frozen oracle tests were modified".into(), cap: None, detail: String::new(), }); - self.goto(Trigger::OracleTampering, Some("oracle tampering".into()), None); + self.goto( + Trigger::OracleTampering, + Some("oracle tampering".into()), + None, + ); continue; } } self.n_check += 1; let n = self.n_check; let argv = steps::check(&self.spec); - let Some(out) = - self.agent_exec(IterationKind::Check, n, LogStream::Check, false, &argv).await + let Some(out) = self + .agent_exec(IterationKind::Check, n, LogStream::Check, false, &argv) + .await else { continue; }; @@ -549,7 +592,9 @@ impl Run { let base = self.spec.base_branch.clone(); let branch = self.spec.branch.clone(); match self.runner.has_diff(&handle, &base, &branch).await { - Ok(false) => self.goto(Trigger::EmptyDiff, Some("no changes vs base".into()), None), + Ok(false) => { + self.goto(Trigger::EmptyDiff, Some("no changes vs base".into()), None) + } Ok(true) => self.goto(Trigger::ChecksPassed, None, None), Err(e) => { // On a diff-check error, proceed rather than stall. @@ -570,8 +615,9 @@ impl Run { self.n_review += 1; let round = self.n_review; let argv = steps::review(self.remaining(), self.spec.wall_clock_secs); - let Some(out) = - self.agent_exec(IterationKind::Review, round, LogStream::Agent, true, &argv).await + let Some(out) = self + .agent_exec(IterationKind::Review, round, LogStream::Agent, true, &argv) + .await else { continue; }; @@ -694,8 +740,14 @@ impl Run { let _ = self.runner.discard(&h).await; } self.permit = None; - let result = if self.phase == Phase::Done { "done" } else { "no_change" }; - self.emit(Event::Done { result: result.into() }); + let result = if self.phase == Phase::Done { + "done" + } else { + "no_change" + }; + self.emit(Event::Done { + result: result.into(), + }); return self.phase; } Phase::Failed => { @@ -704,7 +756,9 @@ impl Run { let _ = self.runner.teardown(&h).await; } self.permit = None; - self.emit(Event::Done { result: "failed".into() }); + self.emit(Event::Done { + result: "failed".into(), + }); return self.phase; } } @@ -738,7 +792,11 @@ impl Run { cap: None, detail: format!("after {MAX_MERGEABLE_POLLS} polls"), }); - self.goto(Trigger::PrDirty, Some("mergeable poll timeout".into()), None); + self.goto( + Trigger::PrDirty, + Some("mergeable poll timeout".into()), + None, + ); } fn fail_closed(&mut self) { @@ -784,7 +842,9 @@ mod tests { task: "do a thing".into(), usd_cap, wall_clock_secs: 0, - gate: GateConfig { min_review_rounds: floor }, + gate: GateConfig { + min_review_rounds: floor, + }, repo_url: "https://github.com/x/y".into(), repo_slug: "x/y".into(), base_branch: "main".into(), @@ -826,8 +886,15 @@ mod tests { let (ctx, crx) = mpsc::unbounded_channel(); let (etx, mut erx) = mpsc::unbounded_channel(); drop(ctx); - let _final_phase = - run(runner, FakeForge::default(), spec, RunCtx::standalone(), crx, etx).await; + let _final_phase = run( + runner, + FakeForge::default(), + spec, + RunCtx::standalone(), + crx, + etx, + ) + .await; drain(&mut erx) } @@ -897,8 +964,16 @@ mod tests { ) .await; assert_eq!(phase, Phase::Done); - assert_eq!(discards.load(std::sync::atomic::Ordering::Relaxed), 1, "Done discards the volume"); - assert_eq!(teardowns.load(std::sync::atomic::Ordering::Relaxed), 0, "Done keeps no volume"); + assert_eq!( + discards.load(std::sync::atomic::Ordering::Relaxed), + 1, + "Done discards the volume" + ); + assert_eq!( + teardowns.load(std::sync::atomic::Ordering::Relaxed), + 0, + "Done keeps no volume" + ); } #[tokio::test] @@ -934,11 +1009,15 @@ mod tests { let evs = drain(&mut erx); // (a) the oracle was NOT re-run on resume assert!( - !evs.iter().any(|e| matches!(e.event, Event::OracleProposed { .. })), + !evs.iter() + .any(|e| matches!(e.event, Event::OracleProposed { .. })), "resume must not re-run the oracle" ); // (b) seq continues from start_seq (first emitted event has seq > 5) - assert!(evs.first().unwrap().seq > 5, "seq must continue from start_seq"); + assert!( + evs.first().unwrap().seq > 5, + "seq must continue from start_seq" + ); // (c) cost continues from start_cost (a metric reflects >= 0.5) let max_cost = evs .iter() @@ -947,9 +1026,16 @@ mod tests { _ => None, }) .fold(0.0, f64::max); - assert!(max_cost >= 0.5, "cost must continue from start_cost, got {max_cost}"); + assert!( + max_cost >= 0.5, + "cost must continue from start_cost, got {max_cost}" + ); // (d) the permit is released once the unit finishes - assert_eq!(permits.available_permits(), 1, "permit released at terminal"); + assert_eq!( + permits.available_permits(), + 1, + "permit released at terminal" + ); } #[tokio::test] @@ -963,8 +1049,8 @@ mod tests { // re-arms on resume without a live Spec phase. let good = vec!["test('x', () => assert(sum(2,3)===5))".to_string()]; let tampered = vec!["test('x', () => assert(true))".to_string()]; // gutted at build time - // Resumed unit: the oracle is already frozen, so the script has NO oracle - // call — just one build/check/review cycle (floor 1 opens the gate). + // Resumed unit: the oracle is already frozen, so the script has NO oracle + // call — just one build/check/review cycle (floor 1 opens the gate). let script = cycle(0); // Only ONE scripted read: the Checking-phase read (Spec is skipped on resume). let runner = FakeRunner::new(script).oracle_contents(vec![tampered]); @@ -993,13 +1079,20 @@ mod tests { let phase = loop { let e = erx.recv().await.expect("stream closed before NeedsHuman"); - if let Event::PhaseChanged { to: Phase::NeedsHuman, .. } = e.event { + if let Event::PhaseChanged { + to: Phase::NeedsHuman, + .. + } = e.event + { break Phase::NeedsHuman; } }; assert_eq!(phase, Phase::NeedsHuman); - ctx.send(Command::Abandon { cmd_id: "cleanup".into() }).unwrap(); + ctx.send(Command::Abandon { + cmd_id: "cleanup".into(), + }) + .unwrap(); let _ = handle.await; } @@ -1035,11 +1128,18 @@ mod tests { // Provisioning (no container/cost) while the only slot is held. loop { let e = erx.recv().await.unwrap(); - if matches!(&e.event, Event::Blocked { reason, .. } if reason == "awaiting concurrency slot") { + if matches!(&e.event, Event::Blocked { reason, .. } if reason == "awaiting concurrency slot") + { break; } assert!( - !matches!(e.event, Event::PhaseChanged { from: Phase::Provisioning, .. }), + !matches!( + e.event, + Event::PhaseChanged { + from: Phase::Provisioning, + .. + } + ), "must not leave Provisioning before acquiring a slot" ); } @@ -1048,7 +1148,11 @@ mod tests { // Free the slot → the driver acquires it and runs to completion. drop(held); assert_eq!(h.await.unwrap(), Phase::Done); - assert_eq!(permits.available_permits(), 1, "permit released at terminal"); + assert_eq!( + permits.available_permits(), + 1, + "permit released at terminal" + ); } #[tokio::test] @@ -1094,14 +1198,27 @@ mod tests { // Wait until the unit parks at AwaitingOracleApproval, accumulating events. let mut all = vec![]; loop { - let e = erx.recv().await.expect("stream closed before approval gate"); - let park = matches!(e.event, Event::PhaseChanged { to: Phase::AwaitingOracleApproval, .. }); + let e = erx + .recv() + .await + .expect("stream closed before approval gate"); + let park = matches!( + e.event, + Event::PhaseChanged { + to: Phase::AwaitingOracleApproval, + .. + } + ); all.push(e); if park { break; } } - ctx.send(Command::ApproveOracle { cmd_id: "c1".into(), edited_test_files: None }).unwrap(); + ctx.send(Command::ApproveOracle { + cmd_id: "c1".into(), + edited_test_files: None, + }) + .unwrap(); let final_phase = handle.await.unwrap(); assert_eq!(final_phase, Phase::Done); @@ -1126,7 +1243,10 @@ mod tests { #[tokio::test] async fn cap_breach_routes_to_needs_human() { // oracle is cheap; the first build blows the $0.5 cap. - let script = vec![FakeRunner::ok(0.1, &["test_a.rs"]), FakeRunner::ok(1.0, &["built"])]; + let script = vec![ + FakeRunner::ok(0.1, &["test_a.rs"]), + FakeRunner::ok(1.0, &["built"]), + ]; let (ctx, crx) = mpsc::unbounded_channel(); let (etx, mut erx) = mpsc::unbounded_channel(); @@ -1143,11 +1263,20 @@ mod tests { // It should park at NeedsHuman; then we abandon to terminate. loop { let e = erx.recv().await.expect("stream closed before cap breach"); - if matches!(e.event, Event::PhaseChanged { to: Phase::NeedsHuman, .. }) { + if matches!( + e.event, + Event::PhaseChanged { + to: Phase::NeedsHuman, + .. + } + ) { break; } } - ctx.send(Command::Abandon { cmd_id: "c2".into() }).unwrap(); + ctx.send(Command::Abandon { + cmd_id: "c2".into(), + }) + .unwrap(); let final_phase = handle.await.unwrap(); assert_eq!(final_phase, Phase::Failed); @@ -1160,7 +1289,10 @@ mod tests { let (etx, mut erx) = mpsc::unbounded_channel(); // Pre-queue a halt; the first agent-active phase (Spec) will pick it up. - ctx.send(Command::Halt { cmd_id: "h1".into() }).unwrap(); + ctx.send(Command::Halt { + cmd_id: "h1".into(), + }) + .unwrap(); let handle = tokio::spawn(run( FakeRunner::new(script), @@ -1173,11 +1305,20 @@ mod tests { loop { let e = erx.recv().await.expect("stream closed before halt"); - if matches!(e.event, Event::PhaseChanged { to: Phase::Halted, .. }) { + if matches!( + e.event, + Event::PhaseChanged { + to: Phase::Halted, + .. + } + ) { break; } } - ctx.send(Command::Abandon { cmd_id: "a1".into() }).unwrap(); + ctx.send(Command::Abandon { + cmd_id: "a1".into(), + }) + .unwrap(); assert_eq!(handle.await.unwrap(), Phase::Failed); } @@ -1190,7 +1331,10 @@ mod tests { #[tokio::test(start_paused = true)] async fn rate_limited_step_retries_then_succeeds() { // Oracle rate-limits once (signal on stderr), then succeeds; floor-1 cycle. - let mut script = vec![FakeRunner::rate_limited(), FakeRunner::ok(0.01, &["test_a.rs"])]; + let mut script = vec![ + FakeRunner::rate_limited(), + FakeRunner::ok(0.01, &["test_a.rs"]), + ]; script.extend(cycle(0)); let (ctx, crx) = mpsc::unbounded_channel(); let (etx, mut erx) = mpsc::unbounded_channel(); @@ -1204,7 +1348,11 @@ mod tests { etx, ) .await; - assert_eq!(final_phase, Phase::Done, "a single rate-limit is retried, not fatal"); + assert_eq!( + final_phase, + Phase::Done, + "a single rate-limit is retried, not fatal" + ); let evs = drain(&mut erx); // The retry surfaced a "rate limited" Blocked. assert!( @@ -1213,9 +1361,22 @@ mod tests { "a rate-limit retry emits a Blocked(\"rate limited\")" ); // The oracle Iteration{Review,0} was emitted exactly once (not per retry). - let oracle_iters = evs.iter().filter(|e| - matches!(e.event, Event::Iteration { kind: IterationKind::Review, n: 0 })).count(); - assert_eq!(oracle_iters, 1, "Iteration emitted once, before the retry loop"); + let oracle_iters = evs + .iter() + .filter(|e| { + matches!( + e.event, + Event::Iteration { + kind: IterationKind::Review, + n: 0 + } + ) + }) + .count(); + assert_eq!( + oracle_iters, 1, + "Iteration emitted once, before the retry loop" + ); } #[tokio::test(start_paused = true)] @@ -1242,12 +1403,22 @@ mod tests { // Drive virtual time forward until it parks at NeedsHuman. loop { let e = erx.recv().await.expect("stream closed before NeedsHuman"); - if matches!(e.event, Event::PhaseChanged { to: Phase::NeedsHuman, .. }) { + if matches!( + e.event, + Event::PhaseChanged { + to: Phase::NeedsHuman, + .. + } + ) { break; } } // Entry cleanup released the permit when parking. - assert_eq!(permits.available_permits(), 1, "permit released at NeedsHuman"); + assert_eq!( + permits.available_permits(), + 1, + "permit released at NeedsHuman" + ); ctx.send(Command::Abandon { cmd_id: "a".into() }).unwrap(); let _ = h.await; } @@ -1260,7 +1431,11 @@ mod tests { exit_code: 1, stdout: vec![], stderr: vec!["API Error: 429 rate limit exceeded".into()], - usage: Some(crate::runner::Usage { tokens_in: 0, tokens_out: 0, cost_usd: 1.0 }), + usage: Some(crate::runner::Usage { + tokens_in: 0, + tokens_out: 0, + cost_usd: 1.0, + }), }; let (ctx, crx) = mpsc::unbounded_channel(); let (etx, mut erx) = mpsc::unbounded_channel(); @@ -1275,15 +1450,28 @@ mod tests { let mut saw_rl_blocked = false; loop { let e = erx.recv().await.expect("stream closed before NeedsHuman"); - if matches!(&e.event, Event::Blocked { reason, .. } if reason == crate::retry::RL_REASON) { + if matches!(&e.event, Event::Blocked { reason, .. } if reason == crate::retry::RL_REASON) + { saw_rl_blocked = true; } - if let Event::PhaseChanged { to: Phase::NeedsHuman, reason, .. } = &e.event { - assert_eq!(reason.as_deref(), Some("usd cap"), "parked via CapBreach, not RetriesExhausted"); + if let Event::PhaseChanged { + to: Phase::NeedsHuman, + reason, + .. + } = &e.event + { + assert_eq!( + reason.as_deref(), + Some("usd cap"), + "parked via CapBreach, not RetriesExhausted" + ); break; } } - assert!(!saw_rl_blocked, "cap breach short-circuits before any backoff Blocked"); + assert!( + !saw_rl_blocked, + "cap breach short-circuits before any backoff Blocked" + ); ctx.send(Command::Abandon { cmd_id: "a".into() }).unwrap(); let _ = h.await; } @@ -1303,14 +1491,21 @@ mod tests { // Wait until it's backing off, then halt mid-wait. loop { let e = erx.recv().await.expect("closed before backoff"); - if matches!(&e.event, Event::Blocked { reason, .. } if reason == crate::retry::RL_REASON) { + if matches!(&e.event, Event::Blocked { reason, .. } if reason == crate::retry::RL_REASON) + { break; } } ctx.send(Command::Halt { cmd_id: "h".into() }).unwrap(); loop { let e = erx.recv().await.expect("closed before Halted"); - if matches!(e.event, Event::PhaseChanged { to: Phase::Halted, .. }) { + if matches!( + e.event, + Event::PhaseChanged { + to: Phase::Halted, + .. + } + ) { break; } } @@ -1333,7 +1528,8 @@ mod tests { // Wait for the first backoff, then send a NON-Halt command into the wait. loop { let e = erx.recv().await.expect("closed before backoff"); - if matches!(&e.event, Event::Blocked { reason, .. } if reason == crate::retry::RL_REASON) { + if matches!(&e.event, Event::Blocked { reason, .. } if reason == crate::retry::RL_REASON) + { break; } } @@ -1345,11 +1541,20 @@ mod tests { if matches!(&e.event, Event::Error { detail, .. } if detail.contains("not valid")) { saw_invalid = true; } - if matches!(e.event, Event::PhaseChanged { to: Phase::NeedsHuman, .. }) { + if matches!( + e.event, + Event::PhaseChanged { + to: Phase::NeedsHuman, + .. + } + ) { break; } } - assert!(saw_invalid, "a non-Halt command during backoff emits a 'not valid' error"); + assert!( + saw_invalid, + "a non-Halt command during backoff emits a 'not valid' error" + ); ctx.send(Command::Abandon { cmd_id: "a".into() }).unwrap(); let _ = h.await; } @@ -1377,7 +1582,7 @@ mod tests { let tampered = vec!["test('x', () => assert(true))".to_string()]; // gutted at build time let mut script = vec![FakeRunner::ok(0.01, &["lru.test.js"])]; // oracle step script.extend(cycle(0)); // one clean build/check/review round (floor 1) - // oracle read #1 (Spec freeze) = good; read #2 (Checking) = tampered + // oracle read #1 (Spec freeze) = good; read #2 (Checking) = tampered let runner = FakeRunner::new(script).oracle_contents(vec![good, tampered]); let (ctx, crx) = mpsc::unbounded_channel(); let (etx, mut erx) = mpsc::unbounded_channel(); @@ -1393,13 +1598,20 @@ mod tests { let phase = loop { let e = erx.recv().await.expect("stream closed before NeedsHuman"); - if let Event::PhaseChanged { to: Phase::NeedsHuman, .. } = e.event { + if let Event::PhaseChanged { + to: Phase::NeedsHuman, + .. + } = e.event + { break Phase::NeedsHuman; } }; assert_eq!(phase, Phase::NeedsHuman); - ctx.send(Command::Abandon { cmd_id: "cleanup".into() }).unwrap(); + ctx.send(Command::Abandon { + cmd_id: "cleanup".into(), + }) + .unwrap(); let _ = handle.await; } @@ -1450,13 +1662,20 @@ mod tests { let phase = loop { let e = erx.recv().await.expect("stream closed before NeedsHuman"); - if let Event::PhaseChanged { to: Phase::NeedsHuman, .. } = e.event { + if let Event::PhaseChanged { + to: Phase::NeedsHuman, + .. + } = e.event + { break Phase::NeedsHuman; } }; assert_eq!(phase, Phase::NeedsHuman); - ctx.send(Command::Abandon { cmd_id: "cleanup".into() }).unwrap(); + ctx.send(Command::Abandon { + cmd_id: "cleanup".into(), + }) + .unwrap(); let _ = handle.await; } @@ -1495,14 +1714,24 @@ mod tests { saw_empty_error = true; } } - if let Event::PhaseChanged { to: Phase::NeedsHuman, .. } = e.event { + if let Event::PhaseChanged { + to: Phase::NeedsHuman, + .. + } = e.event + { break Phase::NeedsHuman; } }; assert_eq!(phase, Phase::NeedsHuman); - assert!(saw_empty_error, "expected an empty-oracle Error event before parking"); + assert!( + saw_empty_error, + "expected an empty-oracle Error event before parking" + ); - ctx.send(Command::Abandon { cmd_id: "cleanup".into() }).unwrap(); + ctx.send(Command::Abandon { + cmd_id: "cleanup".into(), + }) + .unwrap(); let _ = handle.await; } } diff --git a/crates/fleetd/src/fake.rs b/crates/fleetd/src/fake.rs index 4ee5003..dc23038 100644 --- a/crates/fleetd/src/fake.rs +++ b/crates/fleetd/src/fake.rs @@ -82,7 +82,11 @@ impl FakeRunner { exit_code: 0, stdout: stdout.iter().map(|s| s.to_string()).collect(), stderr: vec![], - usage: Some(Usage { tokens_in: 100, tokens_out: 10, cost_usd }), + usage: Some(Usage { + tokens_in: 100, + tokens_out: 10, + cost_usd, + }), } } @@ -92,7 +96,10 @@ impl FakeRunner { exit_code: 1, stdout: vec![], stderr: vec![], - usage: Some(Usage { cost_usd, ..Default::default() }), + usage: Some(Usage { + cost_usd, + ..Default::default() + }), } } @@ -111,7 +118,9 @@ impl FakeRunner { #[async_trait] impl Runner for FakeRunner { async fn provision(&self, _spec: &UnitSpec) -> Result { - Ok(Handle { id: "fake-container".into() }) + Ok(Handle { + id: "fake-container".into(), + }) } async fn exec( @@ -172,7 +181,11 @@ impl Runner for FakeRunner { // Pop the next scripted read; default to a STABLE constant so unscripted tests // see an unchanged oracle and never trip tamper detection. let mut q = self.oracle_reads.lock().unwrap(); - Ok(if q.is_empty() { vec!["".to_string()] } else { q.remove(0) }) + Ok(if q.is_empty() { + vec!["".to_string()] + } else { + q.remove(0) + }) } } @@ -184,7 +197,10 @@ pub struct FakeForge { impl Default for FakeForge { fn default() -> Self { - Self { merge: MergeResult::Clean, mergeable: Mergeability::Mergeable } + Self { + merge: MergeResult::Clean, + mergeable: Mergeability::Mergeable, + } } } diff --git a/crates/fleetd/src/gh_forge.rs b/crates/fleetd/src/gh_forge.rs index 1d72cc6..e90ce24 100644 --- a/crates/fleetd/src/gh_forge.rs +++ b/crates/fleetd/src/gh_forge.rs @@ -37,7 +37,11 @@ impl GhForge { if self.host_clone.join(".git").is_dir() { return Ok(()); } - run("git", &["clone", &self.repo_url, &self.host_clone.to_string_lossy()]).await?; + run( + "git", + &["clone", &self.repo_url, &self.host_clone.to_string_lossy()], + ) + .await?; Ok(()) } @@ -55,11 +59,25 @@ impl Forge for GhForge { let bundle = bundle.to_string_lossy().into_owned(); // Import the agent branch from the bundle and refresh the base. - run("git", &["-C", &dir, "fetch", &bundle, &format!("{branch}:{branch}")]).await?; + run( + "git", + &["-C", &dir, "fetch", &bundle, &format!("{branch}:{branch}")], + ) + .await?; run("git", &["-C", &dir, "fetch", "origin", &self.base_branch]).await?; // Trial-merge the agent branch onto a fresh base; clean exit == mergeable. - run("git", &["-C", &dir, "checkout", "-f", &format!("origin/{}", self.base_branch)]).await?; + run( + "git", + &[ + "-C", + &dir, + "checkout", + "-f", + &format!("origin/{}", self.base_branch), + ], + ) + .await?; let merged = run_status( "git", &["-C", &dir, "merge", "--no-commit", "--no-ff", branch], @@ -68,13 +86,21 @@ impl Forge for GhForge { // Always abort/clean the trial, regardless of outcome. let _ = run_status("git", &["-C", &dir, "merge", "--abort"]).await; - Ok(if merged { MergeResult::Clean } else { MergeResult::Conflict }) + Ok(if merged { + MergeResult::Clean + } else { + MergeResult::Conflict + }) } async fn open_pr(&self, branch: &str) -> Result { guard_branch(branch)?; let dir = self.git_dir(); - run("git", &["-C", &dir, "push", "origin", &format!("{branch}:{branch}")]).await?; + run( + "git", + &["-C", &dir, "push", "origin", &format!("{branch}:{branch}")], + ) + .await?; let url = run( "gh", &[ @@ -97,7 +123,19 @@ impl Forge for GhForge { } async fn poll_mergeable(&self, pr_url: &str) -> Result { - let out = run("gh", &["pr", "view", pr_url, "--json", "mergeable", "-q", ".mergeable"]).await?; + let out = run( + "gh", + &[ + "pr", + "view", + pr_url, + "--json", + "mergeable", + "-q", + ".mergeable", + ], + ) + .await?; Ok(map_mergeable(out.trim())) } } @@ -116,7 +154,8 @@ fn guard_branch(b: &str) -> Result<(), ForgeError> { let ok = !b.is_empty() && !b.starts_with('-') && !b.contains("..") - && b.chars().all(|c| c.is_ascii_alphanumeric() || matches!(c, '_' | '.' | '/' | '-')); + && b.chars() + .all(|c| c.is_ascii_alphanumeric() || matches!(c, '_' | '.' | '/' | '-')); if ok { Ok(()) } else { diff --git a/crates/fleetd/src/local_docker.rs b/crates/fleetd/src/local_docker.rs index 30b5980..0fbb7eb 100644 --- a/crates/fleetd/src/local_docker.rs +++ b/crates/fleetd/src/local_docker.rs @@ -17,7 +17,9 @@ pub struct LocalDockerRunner { impl LocalDockerRunner { pub fn new(image: impl Into) -> Self { - Self { image: image.into() } + Self { + image: image.into(), + } } fn container_name(unit_id: &str) -> String { @@ -31,7 +33,12 @@ impl LocalDockerRunner { /// `docker exec -w `, failing on non-zero exit. async fn exec_in(name: &str, workdir: &str, argv: &[&str]) -> Result<(), RunnerError> { - let mut a = vec!["exec".to_string(), "-w".to_string(), workdir.to_string(), name.to_string()]; + let mut a = vec![ + "exec".to_string(), + "-w".to_string(), + workdir.to_string(), + name.to_string(), + ]; a.extend(argv.iter().map(|s| s.to_string())); docker_ok(a).await.map(|_| ()) } @@ -52,13 +59,20 @@ fn valid_git_branch(b: &str) -> bool { !b.is_empty() && !b.starts_with('-') && !b.contains("..") - && b.chars().all(|c| c.is_ascii_alphanumeric() || matches!(c, '_' | '.' | '/' | '-')) + && b.chars() + .all(|c| c.is_ascii_alphanumeric() || matches!(c, '_' | '.' | '/' | '-')) } /// Keep only docker-name-safe characters. fn sanitize(s: &str) -> String { s.chars() - .map(|c| if c.is_ascii_alphanumeric() || matches!(c, '_' | '.' | '-') { c } else { '_' }) + .map(|c| { + if c.is_ascii_alphanumeric() || matches!(c, '_' | '.' | '-') { + c + } else { + '_' + } + }) .collect() } @@ -80,7 +94,9 @@ async fn docker(args: Vec) -> Result<(i32, String, String), RunnerError> async fn docker_ok(args: Vec) -> Result { let (code, out, err) = docker(args.clone()).await?; if code != 0 { - return Err(RunnerError::Failed(format!("docker {args:?} exited {code}: {err}"))); + return Err(RunnerError::Failed(format!( + "docker {args:?} exited {code}: {err}" + ))); } Ok(out) } @@ -114,14 +130,19 @@ impl Runner for LocalDockerRunner { // Validate inputs that flow into git as args (no shell is used). if !valid_repo_url(&spec.repo_url) { - return Err(RunnerError::Failed(format!("unsafe repo url: {:?}", spec.repo_url))); + return Err(RunnerError::Failed(format!( + "unsafe repo url: {:?}", + spec.repo_url + ))); } if !valid_git_branch(&spec.base_branch) || !valid_git_branch(&spec.branch) { return Err(RunnerError::Failed("unsafe branch name".into())); } // Reuse detection: a persisted volume already has the repo (true resume). - let reused = exec_in(&name, "/work", &["test", "-d", "repo/.git"]).await.is_ok(); + let reused = exec_in(&name, "/work", &["test", "-d", "repo/.git"]) + .await + .is_ok(); if !reused { exec_in(&name, "/work", &["git", "clone", &spec.repo_url, "repo"]).await?; @@ -139,10 +160,20 @@ impl Runner for LocalDockerRunner { // Continue the persisted work: clear a crash-left lock and reset the // branch ref over the existing (possibly dirty) tree. `-b` would fail. let _ = exec_in(&name, "/work/repo", &["rm", "-f", ".git/index.lock"]).await; - exec_in(&name, "/work/repo", &["git", "checkout", "-B", &spec.branch]).await?; + exec_in( + &name, + "/work/repo", + &["git", "checkout", "-B", &spec.branch], + ) + .await?; } else { exec_in(&name, "/work/repo", &["git", "checkout", &spec.base_branch]).await?; - exec_in(&name, "/work/repo", &["git", "checkout", "-b", &spec.branch]).await?; + exec_in( + &name, + "/work/repo", + &["git", "checkout", "-b", &spec.branch], + ) + .await?; } Ok(Handle { id: name }) @@ -154,7 +185,12 @@ impl Runner for LocalDockerRunner { workdir: &str, argv: &[String], ) -> Result { - let mut args = vec!["exec".to_string(), "-w".to_string(), workdir.to_string(), handle.id.clone()]; + let mut args = vec![ + "exec".to_string(), + "-w".to_string(), + workdir.to_string(), + handle.id.clone(), + ]; args.extend(argv.iter().cloned()); let (code, out, err) = docker(args).await?; Ok(ExecOutput { @@ -173,7 +209,11 @@ impl Runner for LocalDockerRunner { handle.id.clone(), ]) .await?; - Ok(if code == 0 && out.trim() == "true" { Liveness::Alive } else { Liveness::Stalled }) + Ok(if code == 0 && out.trim() == "true" { + Liveness::Alive + } else { + Liveness::Stalled + }) } async fn commit_all(&self, handle: &Handle, message: &str) -> Result { @@ -194,7 +234,12 @@ impl Runner for LocalDockerRunner { Ok(code == 0) } - async fn has_diff(&self, handle: &Handle, base: &str, branch: &str) -> Result { + async fn has_diff( + &self, + handle: &Handle, + base: &str, + branch: &str, + ) -> Result { if !valid_git_branch(base) || !valid_git_branch(branch) { return Err(RunnerError::Failed("unsafe ref name".into())); } @@ -218,7 +263,9 @@ impl Runner for LocalDockerRunner { // pass it as its own argv element (no `sh -c` interpolation) to avoid // command/option injection once `branch` becomes unit-derived. if !valid_git_branch(branch) { - return Err(RunnerError::Failed(format!("unsafe branch name: {branch:?}"))); + return Err(RunnerError::Failed(format!( + "unsafe branch name: {branch:?}" + ))); } // Complete, self-contained bundle (Spike 1: no prerequisites). docker_ok(vec![ @@ -257,7 +304,12 @@ impl Runner for LocalDockerRunner { if code != 0 { return Ok(vec![]); } - Ok(out.lines().map(str::trim).filter(|l| !l.is_empty()).map(String::from).collect()) + Ok(out + .lines() + .map(str::trim) + .filter(|l| !l.is_empty()) + .map(String::from) + .collect()) } async fn teardown(&self, handle: &Handle) -> Result<(), RunnerError> { @@ -276,7 +328,12 @@ impl Runner for LocalDockerRunner { async fn reap_unit(&self, unit_id: &str) -> Result<(), RunnerError> { // Reap by unit-id, keeping the volume (resume after restart). - let _ = docker(vec!["rm".into(), "-f".into(), Self::container_name(unit_id)]).await; + let _ = docker(vec![ + "rm".into(), + "-f".into(), + Self::container_name(unit_id), + ]) + .await; Ok(()) } diff --git a/crates/fleetd/src/planner.rs b/crates/fleetd/src/planner.rs index 053ee2c..e904209 100644 --- a/crates/fleetd/src/planner.rs +++ b/crates/fleetd/src/planner.rs @@ -31,9 +31,15 @@ pub struct FakePlanner { } impl FakePlanner { pub fn ok(lanes: Vec, cost_usd: f64) -> Self { - Self { outcome: Ok(PlanOutcome { lanes, cost_usd }) } + Self { + outcome: Ok(PlanOutcome { lanes, cost_usd }), + } + } + pub fn err(msg: &str) -> Self { + Self { + outcome: Err(msg.into()), + } } - pub fn err(msg: &str) -> Self { Self { outcome: Err(msg.into()) } } } #[async_trait] impl Planner for FakePlanner { @@ -51,8 +57,16 @@ impl Planner for FakePlanner { /// Real planner: a read-only Claude call that emits a JSON lane array. Cost is /// parsed from the CLI `result` record via `crate::claude_meter`. pub struct ClaudePlanner; -impl ClaudePlanner { pub fn new() -> Self { Self } } -impl Default for ClaudePlanner { fn default() -> Self { Self::new() } } +impl ClaudePlanner { + pub fn new() -> Self { + Self + } +} +impl Default for ClaudePlanner { + fn default() -> Self { + Self::new() + } +} #[async_trait] impl Planner for ClaudePlanner { @@ -63,20 +77,47 @@ impl Planner for ClaudePlanner { {{\"title\":..,\"task\":..,\"rationale\":..}}. Spec:\n\n{doc}" ); let out = tokio::process::Command::new("claude") - .args(["-p", &prompt, "--output-format", "stream-json", "--max-budget-usd", "1.0"]) - .output().await.map_err(|e| PlanError::Failed(e.to_string()))?; + .args([ + "-p", + &prompt, + "--output-format", + "stream-json", + "--max-budget-usd", + "1.0", + ]) + .output() + .await + .map_err(|e| PlanError::Failed(e.to_string()))?; let stdout = String::from_utf8_lossy(&out.stdout); // Reuse the existing meter: parse_usage(&[String]) -> Option; Usage.cost_usd. let lines: Vec = stdout.lines().map(|l| l.to_string()).collect(); - let cost = crate::claude_meter::parse_usage(&lines).map(|u| u.cost_usd).unwrap_or(0.0); - let json_slice = extract_json_array(&stdout).ok_or_else(|| PlanError::Failed("no JSON array".into()))?; + let cost = crate::claude_meter::parse_usage(&lines) + .map(|u| u.cost_usd) + .unwrap_or(0.0); + let json_slice = + extract_json_array(&stdout).ok_or_else(|| PlanError::Failed("no JSON array".into()))?; #[derive(serde::Deserialize)] - struct RawLane { title: String, task: String, #[serde(default)] rationale: String } + struct RawLane { + title: String, + task: String, + #[serde(default)] + rationale: String, + } let raw: Vec = serde_json::from_str(json_slice) .map_err(|e| PlanError::Failed(format!("bad JSON: {e}")))?; - let lanes = raw.into_iter().take(lane_cap) - .map(|r| Lane { title: r.title, task: r.task, rationale: r.rationale }).collect(); - Ok(PlanOutcome { lanes, cost_usd: cost }) + let lanes = raw + .into_iter() + .take(lane_cap) + .map(|r| Lane { + title: r.title, + task: r.task, + rationale: r.rationale, + }) + .collect(); + Ok(PlanOutcome { + lanes, + cost_usd: cost, + }) } } @@ -84,7 +125,11 @@ impl Planner for ClaudePlanner { fn extract_json_array(s: &str) -> Option<&str> { let start = s.find('[')?; let end = s.rfind(']')?; - if end > start { Some(&s[start..=end]) } else { None } + if end > start { + Some(&s[start..=end]) + } else { + None + } } #[cfg(test)] @@ -94,8 +139,16 @@ mod tests { #[tokio::test] async fn fake_planner_clamps_to_lane_cap_and_can_error() { let lanes = vec![ - Lane { title: "a".into(), task: "ta".into(), rationale: "r".into() }, - Lane { title: "b".into(), task: "tb".into(), rationale: "r".into() }, + Lane { + title: "a".into(), + task: "ta".into(), + rationale: "r".into(), + }, + Lane { + title: "b".into(), + task: "tb".into(), + rationale: "r".into(), + }, ]; let p = FakePlanner::ok(lanes, 0.5); let out = p.plan("doc", 1).await.unwrap(); diff --git a/crates/fleetd/src/reconcile.rs b/crates/fleetd/src/reconcile.rs index fef6407..26270e4 100644 --- a/crates/fleetd/src/reconcile.rs +++ b/crates/fleetd/src/reconcile.rs @@ -102,7 +102,10 @@ mod tests { let actions = reconcile(&["x".into(), "y".into()], &[]); assert_eq!( actions, - vec![Action::HaltNoContainer("x".into()), Action::HaltNoContainer("y".into())] + vec![ + Action::HaltNoContainer("x".into()), + Action::HaltNoContainer("y".into()) + ] ); } @@ -116,9 +119,19 @@ mod tests { &["u1".into()], // live drivers &["u1".into(), "u3".into()], // running containers ); - assert_eq!(actions.len(), 2, "healthy u1 must produce no action: {actions:?}"); - assert!(actions.contains(&Action::HaltNoContainer("u2".into())), "stranded u2 halted"); - assert!(actions.contains(&Action::ReapStray("u3".into())), "stray u3 reaped"); + assert_eq!( + actions.len(), + 2, + "healthy u1 must produce no action: {actions:?}" + ); + assert!( + actions.contains(&Action::HaltNoContainer("u2".into())), + "stranded u2 halted" + ); + assert!( + actions.contains(&Action::ReapStray("u3".into())), + "stray u3 reaped" + ); assert!( !actions.iter().any(|a| matches!(a, Action::HaltWithContainer(x) | Action::HaltNoContainer(x) | Action::ReapStray(x) @@ -137,8 +150,14 @@ mod tests { #[test] fn steady_state_no_drift_is_a_no_op() { // Every non-terminal unit is live and has its container; nothing to do. - let actions = - reconcile_live(&["u1".into(), "u2".into()], &["u1".into(), "u2".into()], &["u1".into(), "u2".into()]); - assert!(actions.is_empty(), "steady state with no drift yields no actions: {actions:?}"); + let actions = reconcile_live( + &["u1".into(), "u2".into()], + &["u1".into(), "u2".into()], + &["u1".into(), "u2".into()], + ); + assert!( + actions.is_empty(), + "steady state with no drift yields no actions: {actions:?}" + ); } } diff --git a/crates/fleetd/src/retry.rs b/crates/fleetd/src/retry.rs index f6a99e4..03489dc 100644 --- a/crates/fleetd/src/retry.rs +++ b/crates/fleetd/src/retry.rs @@ -21,8 +21,14 @@ pub enum StepOutcome { /// Case-insensitive substrings that mark an Anthropic throttle (Task-1 spike may /// extend these). Only consulted on a NON-ZERO exit, so a clean run is never a /// false positive. -const RL_PATTERNS: &[&str] = - &["rate limit", "rate_limit", "overloaded", "429", "529", "usage limit"]; +const RL_PATTERNS: &[&str] = &[ + "rate limit", + "rate_limit", + "overloaded", + "429", + "529", + "usage limit", +]; /// Classify an exec output. Conservative: `Ok` unless a non-zero exit carries a /// recognized rate-limit signal on stdout or stderr. @@ -30,14 +36,10 @@ pub fn classify(out: &ExecOutput) -> StepOutcome { if out.exit_code == 0 { return StepOutcome::Ok; } - let hit = out - .stdout - .iter() - .chain(out.stderr.iter()) - .any(|line| { - let l = line.to_lowercase(); - RL_PATTERNS.iter().any(|p| l.contains(p)) - }); + let hit = out.stdout.iter().chain(out.stderr.iter()).any(|line| { + let l = line.to_lowercase(); + RL_PATTERNS.iter().any(|p| l.contains(p)) + }); if hit { StepOutcome::RateLimited } else { @@ -55,7 +57,11 @@ pub struct Backoff { impl Backoff { pub fn new(base_secs: u64, cap_secs: u64) -> Self { - Self { attempt: 0, base_secs, cap_secs } + Self { + attempt: 0, + base_secs, + cap_secs, + } } pub fn next_delay(&mut self) -> Duration { @@ -73,19 +79,28 @@ pub fn wall_clock_exceeded(elapsed: Duration, rl_elapsed: Duration, cap_secs: u6 } fn env_secs(key: &str, default: u64) -> u64 { - std::env::var(key).ok().and_then(|v| v.parse().ok()).unwrap_or(default) + std::env::var(key) + .ok() + .and_then(|v| v.parse().ok()) + .unwrap_or(default) } /// Backoff/envelope knobs (env-tunable; defaults chosen for SP1). -pub fn rl_base_secs() -> u64 { env_secs("CC_RL_BASE_SECS", 2) } -pub fn rl_cap_secs() -> u64 { env_secs("CC_RL_CAP_SECS", 300) } -pub fn rl_max_wait_secs() -> u64 { env_secs("CC_RL_MAX_WAIT_SECS", 3600) } +pub fn rl_base_secs() -> u64 { + env_secs("CC_RL_BASE_SECS", 2) +} +pub fn rl_cap_secs() -> u64 { + env_secs("CC_RL_CAP_SECS", 300) +} +pub fn rl_max_wait_secs() -> u64 { + env_secs("CC_RL_MAX_WAIT_SECS", 3600) +} #[cfg(test)] mod tests { use super::*; - use std::time::Duration; use crate::runner::Usage; + use std::time::Duration; fn out(code: i32, stdout: &[&str], stderr: &[&str]) -> ExecOutput { ExecOutput { @@ -110,24 +125,42 @@ mod tests { #[test] fn overloaded_on_stdout_is_detected() { - assert_eq!(classify(&out(1, &["Error: Overloaded (529)"], &[])), StepOutcome::RateLimited); + assert_eq!( + classify(&out(1, &["Error: Overloaded (529)"], &[])), + StepOutcome::RateLimited + ); } #[test] fn unrelated_nonzero_exit_is_ok() { // e.g. a timeout-kill (124) or a normal agent failure — preserved as today. - assert_eq!(classify(&out(124, &["compilation failed"], &[])), StepOutcome::Ok); + assert_eq!( + classify(&out(124, &["compilation failed"], &[])), + StepOutcome::Ok + ); } #[test] fn wall_clock_exempts_rate_limit_time() { let cap = 30; // 30s cap - // 100s elapsed but 80s of it was rate-limit waiting => 20s effective <= 30s. - assert!(!wall_clock_exceeded(Duration::from_secs(100), Duration::from_secs(80), cap)); + // 100s elapsed but 80s of it was rate-limit waiting => 20s effective <= 30s. + assert!(!wall_clock_exceeded( + Duration::from_secs(100), + Duration::from_secs(80), + cap + )); // Same elapsed, no exemption => 100s > 30s => exceeded. - assert!(wall_clock_exceeded(Duration::from_secs(100), Duration::ZERO, cap)); + assert!(wall_clock_exceeded( + Duration::from_secs(100), + Duration::ZERO, + cap + )); // cap 0 disables the check entirely. - assert!(!wall_clock_exceeded(Duration::from_secs(9999), Duration::ZERO, 0)); + assert!(!wall_clock_exceeded( + Duration::from_secs(9999), + Duration::ZERO, + 0 + )); } #[test] diff --git a/crates/fleetd/src/runner.rs b/crates/fleetd/src/runner.rs index 7d747ec..1b94897 100644 --- a/crates/fleetd/src/runner.rs +++ b/crates/fleetd/src/runner.rs @@ -96,11 +96,7 @@ pub trait Runner: Send + Sync { branch: &str, ) -> Result; /// `git bundle` the branch and `docker cp` it to a host path (Spike 1). - async fn export_bundle( - &self, - handle: &Handle, - branch: &str, - ) -> Result; + async fn export_bundle(&self, handle: &Handle, branch: &str) -> Result; /// Unit-ids of currently-running containers labeled `cc.unit_id` (for startup /// reconciliation of orphans after a daemon restart). async fn list_unit_containers(&self) -> Result, RunnerError>; diff --git a/crates/fleetd/src/server.rs b/crates/fleetd/src/server.rs index 8c3f4c2..146461a 100644 --- a/crates/fleetd/src/server.rs +++ b/crates/fleetd/src/server.rs @@ -22,8 +22,8 @@ use crate::fake::{FakeForge, FakeRunner}; use crate::planner::Planner; use crate::reconcile::{reconcile, reconcile_live, Action}; use crate::runner::{ExecOutput, Runner, UnitSpec}; -use crate::swarm::{admit_lanes, slug, AdmissionConfig, LaneDecision}; use crate::store::{Store, UnitRow}; +use crate::swarm::{admit_lanes, slug, AdmissionConfig, LaneDecision}; use axum::{ extract::{ ws::{Message, WebSocket, WebSocketUpgrade}, @@ -45,11 +45,18 @@ use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; use tokio::sync::{broadcast, mpsc, Semaphore}; fn now_ms() -> i64 { - SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_millis() as i64 + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_millis() as i64 } fn phase_str(p: &Phase) -> String { - serde_json::to_value(p).unwrap().as_str().unwrap_or("unknown").to_string() + serde_json::to_value(p) + .unwrap() + .as_str() + .unwrap_or("unknown") + .to_string() } /// Per-unit live state held by the server (for broadcast/commands). Event @@ -79,24 +86,44 @@ const DEFAULT_MAX_CONCURRENT: usize = 3; const DEFAULT_GLOBAL_USD_CAP: f64 = 20.0; fn env_usize(key: &str, default: usize) -> usize { - std::env::var(key).ok().and_then(|v| v.parse().ok()).filter(|&n| n > 0).unwrap_or(default) + std::env::var(key) + .ok() + .and_then(|v| v.parse().ok()) + .filter(|&n| n > 0) + .unwrap_or(default) } fn env_f64(key: &str, default: f64) -> f64 { - std::env::var(key).ok().and_then(|v| v.parse().ok()).unwrap_or(default) + std::env::var(key) + .ok() + .and_then(|v| v.parse().ok()) + .unwrap_or(default) } impl AppState { pub fn new(store: Arc>) -> Self { let max_concurrent = env_usize("CC_MAX_CONCURRENT", DEFAULT_MAX_CONCURRENT); - let next = store.lock().unwrap().max_unit_seq().expect("seed next_id from max_unit_seq") + 1; - let next_sw = store.lock().unwrap().max_swarm_seq().expect("seed next_swarm from max_swarm_seq") + 1; + let next = store + .lock() + .unwrap() + .max_unit_seq() + .expect("seed next_id from max_unit_seq") + + 1; + let next_sw = store + .lock() + .unwrap() + .max_swarm_seq() + .expect("seed next_swarm from max_swarm_seq") + + 1; Self { units: Arc::new(Mutex::new(HashMap::new())), next_id: Arc::new(AtomicU64::new(next)), next_swarm: Arc::new(AtomicU64::new(next_sw)), store, // Start "stale" so the first /health does a real probe. - docker: Arc::new(Mutex::new((Instant::now() - Duration::from_secs(60), false))), + docker: Arc::new(Mutex::new(( + Instant::now() - Duration::from_secs(60), + false, + ))), permits: Arc::new(Semaphore::new(max_concurrent)), global_cap: env_f64("CC_GLOBAL_USD_CAP", DEFAULT_GLOBAL_USD_CAP), } @@ -105,7 +132,9 @@ impl AppState { impl Default for AppState { fn default() -> Self { - Self::new(Arc::new(Mutex::new(Store::open_memory().expect("memory store")))) + Self::new(Arc::new(Mutex::new( + Store::open_memory().expect("memory store"), + ))) } } @@ -221,8 +250,12 @@ pub enum SpawnError { /// Register the per-unit handle and spawn its driver. The row must already be /// persisted by the caller (create_mission, or fan-out via /// commit_lane_unit). Returns Err if a driver already exists (no double-spawn). -fn spawn_driver_for(st: &AppState, spec: UnitSpec, mode: &str, unit_id: &str) - -> Result<(), SpawnError> { +fn spawn_driver_for( + st: &AppState, + spec: UnitSpec, + mode: &str, + unit_id: &str, +) -> Result<(), SpawnError> { let (cmd_rx, evt_tx) = match register_unit_if_absent(st, unit_id) { Some(ch) => ch, None => return Err(SpawnError::AlreadyRegistered), @@ -230,15 +263,34 @@ fn spawn_driver_for(st: &AppState, spec: UnitSpec, mode: &str, unit_id: &str) match mode { "demo" => { let runner = FakeRunner::new(demo_script(&spec)); - tokio::spawn(run(runner, FakeForge::default(), spec, fresh_ctx(st), cmd_rx, evt_tx)); + tokio::spawn(run( + runner, + FakeForge::default(), + spec, + fresh_ctx(st), + cmd_rx, + evt_tx, + )); } _ => { use crate::gh_forge::GhForge; use crate::local_docker::LocalDockerRunner; let host_clone = std::env::temp_dir().join(format!("cc-host-{unit_id}")); - let forge = GhForge::new(spec.repo_url.clone(), spec.repo_slug.clone(), - spec.base_branch.clone(), host_clone, format!("command-center SP1: {unit_id}")); - tokio::spawn(run(LocalDockerRunner::new("cc-agent:dev"), forge, spec, fresh_ctx(st), cmd_rx, evt_tx)); + let forge = GhForge::new( + spec.repo_url.clone(), + spec.repo_slug.clone(), + spec.base_branch.clone(), + host_clone, + format!("command-center SP1: {unit_id}"), + ); + tokio::spawn(run( + LocalDockerRunner::new("cc-agent:dev"), + forge, + spec, + fresh_ctx(st), + cmd_rx, + evt_tx, + )); } } Ok(()) @@ -256,7 +308,9 @@ async fn create_mission( task: req.task, usd_cap: 5.0, wall_clock_secs: 1800, - gate: GateConfig { min_review_rounds: req.min_review_rounds.max(1) }, + gate: GateConfig { + min_review_rounds: req.min_review_rounds.max(1), + }, repo_url: "https://github.com/adbarc92/command-center-agent-sandbox".into(), repo_slug: "adbarc92/command-center-agent-sandbox".into(), base_branch: "main".into(), @@ -283,7 +337,10 @@ async fn create_mission( let s = st.store.lock().unwrap(); let since = now_ms() - 24 * 3600 * 1000; if s.committed_spend(since).unwrap_or(0.0) >= st.global_cap { - return Err((StatusCode::TOO_MANY_REQUESTS, "global daily cost cap reached".into())); + return Err(( + StatusCode::TOO_MANY_REQUESTS, + "global daily cost cap reached".into(), + )); } let mut row = row_from_spec(&spec, &runner_mode); row.swarm_id = None; @@ -304,7 +361,10 @@ async fn create_mission( fn register_unit_if_absent( st: &AppState, unit_id: &str, -) -> Option<(mpsc::UnboundedReceiver, mpsc::UnboundedSender)> { +) -> Option<( + mpsc::UnboundedReceiver, + mpsc::UnboundedSender, +)> { let mut units = st.units.lock().unwrap(); if units.contains_key(unit_id) { return None; @@ -375,7 +435,9 @@ fn spec_from_row(r: &UnitRow) -> UnitSpec { task: r.task.clone(), usd_cap: r.usd_cap, wall_clock_secs: r.wall_clock_secs, - gate: GateConfig { min_review_rounds: r.min_review_rounds.max(1) }, + gate: GateConfig { + min_review_rounds: r.min_review_rounds.max(1), + }, repo_url: r.repo_url.clone(), repo_slug: r.repo_slug.clone(), base_branch: r.base_branch.clone(), @@ -419,8 +481,15 @@ fn spawn_forwarder( { let s = store.lock().unwrap(); let _ = s.append_event(&unit_id, env.seq, ts, &json); - let _ = s.update_unit(&unit_id, &cur_phase, cur_cost, env.seq, - terminal_reason.as_deref(), oracle_hash.as_deref(), ts); + let _ = s.update_unit( + &unit_id, + &cur_phase, + cur_cost, + env.seq, + terminal_reason.as_deref(), + oracle_hash.as_deref(), + ts, + ); } let _ = bcast.send(env); } @@ -468,7 +537,11 @@ async fn get_unit( Path(id): Path, ) -> Result, StatusCode> { let s = st.store.lock().unwrap(); - let row = s.get_unit(&id).ok().flatten().ok_or(StatusCode::NOT_FOUND)?; + let row = s + .get_unit(&id) + .ok() + .flatten() + .ok_or(StatusCode::NOT_FOUND)?; let events = s .events_since(&id, 0) .unwrap_or_default() @@ -542,7 +615,12 @@ async fn stream_to_socket(mut socket: WebSocket, id: String, since: u64, st: App let (replay, mut rx) = { let units = st.units.lock().unwrap(); let rx = units.get(&id).map(|h| h.bcast.subscribe()); - let replay = st.store.lock().unwrap().events_since(&id, since).unwrap_or_default(); + let replay = st + .store + .lock() + .unwrap() + .events_since(&id, since) + .unwrap_or_default(); (replay, rx) }; @@ -580,7 +658,10 @@ async fn stream_to_socket(mut socket: WebSocket, id: String, since: u64, st: App } fn seq_of(json: &str) -> Option { - serde_json::from_str::(json).ok()?.get("seq")?.as_u64() + serde_json::from_str::(json) + .ok()? + .get("seq")? + .as_u64() } #[derive(Serialize)] @@ -645,12 +726,29 @@ pub async fn reconcile_on_startup(state: &AppState, runner: &R) { } // Swarm reconcile: planning → failed; fanning_out → resume missing lanes. - let swarms = state.store.lock().unwrap().list_swarms().unwrap_or_default(); + let swarms = state + .store + .lock() + .unwrap() + .list_swarms() + .unwrap_or_default(); for sw in swarms { match sw.status.as_str() { "planning" => { - state.store.lock().unwrap().update_swarm(&sw.swarm_id, "failed", sw.planner_cost, - sw.lanes_launched, sw.lanes_dropped, Some("daemon restarted during planning"), now_ms()).ok(); + state + .store + .lock() + .unwrap() + .update_swarm( + &sw.swarm_id, + "failed", + sw.planner_cost, + sw.lanes_launched, + sw.lanes_dropped, + Some("daemon restarted during planning"), + now_ms(), + ) + .ok(); } "fanning_out" => resume_fan_out(state, &sw), _ => {} @@ -692,23 +790,55 @@ pub async fn reconcile_tick(state: &AppState, runner: &R) { /// A lane counts as launched only if its `unit_id` is set AND a unit row actually /// exists (defensive against a crash between minting the id and committing the row). fn resume_fan_out(st: &AppState, sw: &crate::store::SwarmRow) { - let lanes = st.store.lock().unwrap().lanes_for_swarm(&sw.swarm_id).unwrap_or_default(); + let lanes = st + .store + .lock() + .unwrap() + .lanes_for_swarm(&sw.swarm_id) + .unwrap_or_default(); let mut launched = 0u32; for l in lanes.into_iter().filter(|l| l.decision == "admit") { - let exists = l.unit_id.as_ref() - .and_then(|id| st.store.lock().unwrap().get_unit(id).ok().flatten()).is_some(); - if exists { launched += 1; continue; } + let exists = l + .unit_id + .as_ref() + .and_then(|id| st.store.lock().unwrap().get_unit(id).ok().flatten()) + .is_some(); + if exists { + launched += 1; + continue; + } let n = st.next_id.fetch_add(1, Ordering::Relaxed); let unit_id = format!("u{n}"); - let lane = crate::swarm::Lane { title: l.title.clone(), task: l.task.clone(), rationale: l.rationale.clone() }; + let lane = crate::swarm::Lane { + title: l.title.clone(), + task: l.task.clone(), + rationale: l.rationale.clone(), + }; let spec = lane_spec(sw, &unit_id, l.idx as usize, &lane); let mut row = row_from_spec(&spec, &sw.mode); row.swarm_id = Some(sw.swarm_id.clone()); - st.store.lock().unwrap().commit_lane_unit(&sw.swarm_id, l.idx, &row, now_ms()).ok(); - if spawn_driver_for(st, spec, &sw.mode, &unit_id).is_ok() { launched += 1; } + st.store + .lock() + .unwrap() + .commit_lane_unit(&sw.swarm_id, l.idx, &row, now_ms()) + .ok(); + if spawn_driver_for(st, spec, &sw.mode, &unit_id).is_ok() { + launched += 1; + } } - st.store.lock().unwrap().update_swarm(&sw.swarm_id, "running", sw.planner_cost, - launched, sw.lanes_dropped, None, now_ms()).ok(); + st.store + .lock() + .unwrap() + .update_swarm( + &sw.swarm_id, + "running", + sw.planner_cost, + launched, + sw.lanes_dropped, + None, + now_ms(), + ) + .ok(); } /// Append a synthetic `Halted` event + update the row (one store write, no await). @@ -727,8 +857,21 @@ fn halt_in_store(state: &AppState, id: &str) { }, }; let ts = now_ms(); - let _ = s.append_event(id, seq, ts, &serde_json::to_string(&env).unwrap_or_default()); - let _ = s.update_unit(id, "halted", row.cost, seq, Some("daemon restarted"), None, ts); + let _ = s.append_event( + id, + seq, + ts, + &serde_json::to_string(&env).unwrap_or_default(), + ); + let _ = s.update_unit( + id, + "halted", + row.cost, + seq, + Some("daemon restarted"), + None, + ts, + ); } } @@ -746,7 +889,10 @@ fn demo_script(spec: &UnitSpec) -> Vec { for remaining in (0..floor).rev() { s.push(FakeRunner::ok(0.03, &["implementing the change"])); s.push(FakeRunner::ok(0.0, &["tests: 1 passing"])); - s.push(FakeRunner::ok(0.04, &[&format!("review done\nBLOCKERS={remaining}")])); + s.push(FakeRunner::ok( + 0.04, + &[&format!("review done\nBLOCKERS={remaining}")], + )); } s } @@ -758,22 +904,35 @@ fn demo_script(spec: &UnitSpec) -> Vec { #[derive(Deserialize, Default)] struct CreateSwarmReq { doc_path: String, - #[serde(default)] tier: TierReq, - #[serde(default = "default_mode")] mode: String, - #[serde(default)] max_lanes: Option, - #[serde(default)] usd_budget: Option, - #[serde(default)] per_lane_cap: Option, - #[serde(default = "default_floor")] min_review_rounds: u32, - #[serde(default)] repo_url: Option, - #[serde(default)] repo_slug: Option, - #[serde(default)] base_branch: Option, + #[serde(default)] + tier: TierReq, + #[serde(default = "default_mode")] + mode: String, + #[serde(default)] + max_lanes: Option, + #[serde(default)] + usd_budget: Option, + #[serde(default)] + per_lane_cap: Option, + #[serde(default = "default_floor")] + min_review_rounds: u32, + #[serde(default)] + repo_url: Option, + #[serde(default)] + repo_slug: Option, + #[serde(default)] + base_branch: Option, } #[derive(Serialize)] -struct CreateSwarmResp { swarm_id: String } +struct CreateSwarmResp { + swarm_id: String, +} -async fn create_swarm(State(st): State, Json(req): Json) - -> Result, (StatusCode, String)> { +async fn create_swarm( + State(st): State, + Json(req): Json, +) -> Result, (StatusCode, String)> { // Step 0 — synchronous validation (no 4xx can occur in the spawned task). if req.doc_path.trim().is_empty() { return Err((StatusCode::BAD_REQUEST, "doc_path is required".into())); @@ -785,7 +944,10 @@ async fn create_swarm(State(st): State, Json(req): Json return Err((StatusCode::BAD_REQUEST, format!("unknown mode: {other}"))), @@ -793,25 +955,48 @@ async fn create_swarm(State(st): State, Json(req): Json= st.global_cap { - return Err((StatusCode::TOO_MANY_REQUESTS, "global daily cost cap reached".into())); + return Err(( + StatusCode::TOO_MANY_REQUESTS, + "global daily cost cap reached".into(), + )); } - let usd_budget = req.usd_budget.unwrap_or_else(|| (st.global_cap - committed).clamp(0.0, 15.0)); + let usd_budget = req + .usd_budget + .unwrap_or_else(|| (st.global_cap - committed).clamp(0.0, 15.0)); let row = crate::store::SwarmRow { - swarm_id: swarm_id.clone(), repo_url, repo_slug, base_branch, - doc_path: req.doc_path.clone(), tier: phase_tier(req.tier.into()), mode: req.mode.clone(), - lane_cap, usd_budget, per_lane_cap, status: "planning".into(), planner_cost: 0.0, - lanes_launched: 0, lanes_dropped: 0, min_review_rounds: req.min_review_rounds.max(1), + swarm_id: swarm_id.clone(), + repo_url, + repo_slug, + base_branch, + doc_path: req.doc_path.clone(), + tier: phase_tier(req.tier.into()), + mode: req.mode.clone(), + lane_cap, + usd_budget, + per_lane_cap, + status: "planning".into(), + planner_cost: 0.0, + lanes_launched: 0, + lanes_dropped: 0, + min_review_rounds: req.min_review_rounds.max(1), terminal_reason: None, }; s.upsert_swarm(&row, now_ms()).ok(); @@ -826,10 +1011,24 @@ async fn create_swarm(State(st): State, Json(req): Json { use crate::{docsource::FakeDocSource, planner::FakePlanner, swarm::Lane}; let lanes = vec![ - Lane { title: "Lane One".into(), task: "demo task 1".into(), rationale: "indep".into() }, - Lane { title: "Lane Two".into(), task: "demo task 2".into(), rationale: "indep".into() }, + Lane { + title: "Lane One".into(), + task: "demo task 1".into(), + rationale: "indep".into(), + }, + Lane { + title: "Lane Two".into(), + task: "demo task 2".into(), + rationale: "indep".into(), + }, ]; - run_swarm(st2, id2, FakePlanner::ok(lanes, 0.01), FakeDocSource::new("# demo spec")).await; + run_swarm( + st2, + id2, + FakePlanner::ok(lanes, 0.01), + FakeDocSource::new("# demo spec"), + ) + .await; } _ => { use crate::{docsource::GitDocSource, planner::ClaudePlanner}; @@ -843,50 +1042,93 @@ async fn create_swarm(State(st): State, Json(req): Json) -> Json> { let rows = st.store.lock().unwrap().list_swarms().unwrap_or_default(); - Json(rows.into_iter().map(|r| SwarmSummary { - swarm_id: r.swarm_id, status: r.status, lanes_launched: r.lanes_launched, - lanes_dropped: r.lanes_dropped, planner_cost: r.planner_cost, doc_path: r.doc_path, - }).collect()) + Json( + rows.into_iter() + .map(|r| SwarmSummary { + swarm_id: r.swarm_id, + status: r.status, + lanes_launched: r.lanes_launched, + lanes_dropped: r.lanes_dropped, + planner_cost: r.planner_cost, + doc_path: r.doc_path, + }) + .collect(), + ) } #[derive(Serialize)] struct SwarmDetail { - swarm_id: String, status: String, planner_cost: f64, - lanes_launched: u32, lanes_dropped: u32, awaiting_human: u64, - spent_so_far: f64, lanes: Vec, units: Vec, + swarm_id: String, + status: String, + planner_cost: f64, + lanes_launched: u32, + lanes_dropped: u32, + awaiting_human: u64, + spent_so_far: f64, + lanes: Vec, + units: Vec, } #[derive(Serialize)] -struct LaneView { idx: u32, title: String, decision: String, unit_id: Option } +struct LaneView { + idx: u32, + title: String, + decision: String, + unit_id: Option, +} -async fn get_swarm(State(st): State, Path(id): Path) - -> Result, StatusCode> { +async fn get_swarm( + State(st): State, + Path(id): Path, +) -> Result, StatusCode> { let s = st.store.lock().unwrap(); - let sw = s.get_swarm(&id).ok().flatten().ok_or(StatusCode::NOT_FOUND)?; + let sw = s + .get_swarm(&id) + .ok() + .flatten() + .ok_or(StatusCode::NOT_FOUND)?; let lanes = s.lanes_for_swarm(&id).unwrap_or_default(); let (total, terminal, awaiting) = s.swarm_rollup(&id).unwrap_or((0, 0, 0)); // Computed status: running→done only when every child unit is terminal. let status = if sw.status == "running" && total > 0 && terminal == total { "done".to_string() - } else { sw.status.clone() }; + } else { + sw.status.clone() + }; // "spent so far" = actual child cost + planner cost (NOT reservations). let unit_ids: Vec = lanes.iter().filter_map(|l| l.unit_id.clone()).collect(); let mut spent = sw.planner_cost; for uid in &unit_ids { - if let Ok(Some(u)) = s.get_unit(uid) { spent += u.cost; } + if let Ok(Some(u)) = s.get_unit(uid) { + spent += u.cost; + } } Ok(Json(SwarmDetail { - swarm_id: id, status, planner_cost: sw.planner_cost, - lanes_launched: sw.lanes_launched, lanes_dropped: sw.lanes_dropped, awaiting_human: awaiting, + swarm_id: id, + status, + planner_cost: sw.planner_cost, + lanes_launched: sw.lanes_launched, + lanes_dropped: sw.lanes_dropped, + awaiting_human: awaiting, spent_so_far: spent, - lanes: lanes.iter().map(|l| LaneView { - idx: l.idx, title: l.title.clone(), decision: l.decision.clone(), unit_id: l.unit_id.clone(), - }).collect(), + lanes: lanes + .iter() + .map(|l| LaneView { + idx: l.idx, + title: l.title.clone(), + decision: l.decision.clone(), + unit_id: l.unit_id.clone(), + }) + .collect(), units: unit_ids, })) } @@ -896,8 +1138,15 @@ async fn get_swarm(State(st): State, Path(id): Path) /// sync critical section. Admission here mirrors the mission path: the /// committed-spend check and the unit-row insert happen under ONE store-lock span /// (per-lane re-check + commit_lane_unit), so the cap binds atomically (P2). -pub async fn run_swarm(st: AppState, swarm_id: String, planner: P, doc: D) { - let Some(sw) = st.store.lock().unwrap().get_swarm(&swarm_id).ok().flatten() else { return }; +pub async fn run_swarm( + st: AppState, + swarm_id: String, + planner: P, + doc: D, +) { + let Some(sw) = st.store.lock().unwrap().get_swarm(&swarm_id).ok().flatten() else { + return; + }; // 2. Plan (read doc, then decompose). let doc_text = match doc.read(&sw.repo_url, &sw.base_branch, &sw.doc_path).await { @@ -908,16 +1157,34 @@ pub async fn run_swarm(st: AppState, swarm_id: String, Ok(o) => o, Err(e) => return fail_swarm(&st, &swarm_id, 0.0, &format!("planner: {e}")), }; - st.store.lock().unwrap() - .update_swarm(&swarm_id, "planning", outcome.cost_usd, 0, 0, None, now_ms()).ok(); + st.store + .lock() + .unwrap() + .update_swarm( + &swarm_id, + "planning", + outcome.cost_usd, + 0, + 0, + None, + now_ms(), + ) + .ok(); if outcome.lanes.is_empty() { - return fail_swarm(&st, &swarm_id, outcome.cost_usd, "planner returned zero lanes"); + return fail_swarm( + &st, + &swarm_id, + outcome.cost_usd, + "planner returned zero lanes", + ); } // 3. Admit (pure) + persist every lane's decision. let cfg = AdmissionConfig { - lane_cap: sw.lane_cap as usize, usd_budget: sw.usd_budget, - per_lane_cap: sw.per_lane_cap, planner_cost: outcome.cost_usd, + lane_cap: sw.lane_cap as usize, + usd_budget: sw.usd_budget, + per_lane_cap: sw.per_lane_cap, + planner_cost: outcome.cost_usd, }; let decisions = admit_lanes(&outcome.lanes, &cfg); let mut dropped = 0u32; @@ -928,21 +1195,59 @@ pub async fn run_swarm(st: AppState, swarm_id: String, LaneDecision::DropOverLaneCap => "drop_lane_cap", LaneDecision::DropOverBudget => "drop_budget", }; - if !matches!(d, LaneDecision::Admit) { dropped += 1; } - st.store.lock().unwrap() - .upsert_lane(&swarm_id, *i as u32, &lane.title, &lane.task, &lane.rationale, dstr, None).ok(); + if !matches!(d, LaneDecision::Admit) { + dropped += 1; + } + st.store + .lock() + .unwrap() + .upsert_lane( + &swarm_id, + *i as u32, + &lane.title, + &lane.task, + &lane.rationale, + dstr, + None, + ) + .ok(); } - let admitted_idxs: Vec = decisions.iter() - .filter(|(_, d)| matches!(d, LaneDecision::Admit)).map(|(i, _)| *i).collect(); + let admitted_idxs: Vec = decisions + .iter() + .filter(|(_, d)| matches!(d, LaneDecision::Admit)) + .map(|(i, _)| *i) + .collect(); if admitted_idxs.is_empty() { - st.store.lock().unwrap() - .update_swarm(&swarm_id, "empty", outcome.cost_usd, 0, dropped, Some("no lanes admitted"), now_ms()).ok(); + st.store + .lock() + .unwrap() + .update_swarm( + &swarm_id, + "empty", + outcome.cost_usd, + 0, + dropped, + Some("no lanes admitted"), + now_ms(), + ) + .ok(); return; } // 4. Fan out (idempotent per-lane). - st.store.lock().unwrap() - .update_swarm(&swarm_id, "fanning_out", outcome.cost_usd, 0, dropped, None, now_ms()).ok(); + st.store + .lock() + .unwrap() + .update_swarm( + &swarm_id, + "fanning_out", + outcome.cost_usd, + 0, + dropped, + None, + now_ms(), + ) + .ok(); let since = now_ms() - 24 * 3600 * 1000; let mut launched = 0u32; 'fanout: for (pos, idx) in admitted_idxs.iter().copied().enumerate() { @@ -955,8 +1260,16 @@ pub async fn run_swarm(st: AppState, swarm_id: String, // drop_global_cap so no admit row is left dangling with unit_id = NULL. for rem_idx in admitted_idxs[pos..].iter().copied() { let rem_lane = &outcome.lanes[rem_idx]; - s.upsert_lane(&swarm_id, rem_idx as u32, &rem_lane.title, &rem_lane.task, - &rem_lane.rationale, "drop_global_cap", None).ok(); + s.upsert_lane( + &swarm_id, + rem_idx as u32, + &rem_lane.title, + &rem_lane.task, + &rem_lane.rationale, + "drop_global_cap", + None, + ) + .ok(); dropped += 1; } break 'fanout; @@ -966,7 +1279,8 @@ pub async fn run_swarm(st: AppState, swarm_id: String, let spec = lane_spec(&sw, &unit_id, idx, lane); let mut row = row_from_spec(&spec, &sw.mode); row.swarm_id = Some(swarm_id.clone()); - s.commit_lane_unit(&swarm_id, idx as u32, &row, now_ms()).ok(); + s.commit_lane_unit(&swarm_id, idx as u32, &row, now_ms()) + .ok(); (unit_id, spec) }; // Spawn the driver outside the lock; the row already exists. @@ -974,24 +1288,53 @@ pub async fn run_swarm(st: AppState, swarm_id: String, launched += 1; } } - st.store.lock().unwrap() - .update_swarm(&swarm_id, "running", outcome.cost_usd, launched, dropped, None, now_ms()).ok(); + st.store + .lock() + .unwrap() + .update_swarm( + &swarm_id, + "running", + outcome.cost_usd, + launched, + dropped, + None, + now_ms(), + ) + .ok(); } fn fail_swarm(st: &AppState, swarm_id: &str, planner_cost: f64, reason: &str) { - st.store.lock().unwrap() - .update_swarm(swarm_id, "failed", planner_cost, 0, 0, Some(reason), now_ms()).ok(); + st.store + .lock() + .unwrap() + .update_swarm( + swarm_id, + "failed", + planner_cost, + 0, + 0, + Some(reason), + now_ms(), + ) + .ok(); } /// Build a lane's UnitSpec from the swarm config. -fn lane_spec(sw: &crate::store::SwarmRow, unit_id: &str, idx: usize, lane: &crate::swarm::Lane) -> UnitSpec { +fn lane_spec( + sw: &crate::store::SwarmRow, + unit_id: &str, + idx: usize, + lane: &crate::swarm::Lane, +) -> UnitSpec { UnitSpec { unit_id: unit_id.into(), tier: parse_tier(&sw.tier), task: lane.task.clone(), usd_cap: sw.per_lane_cap, wall_clock_secs: 1800, - gate: GateConfig { min_review_rounds: sw.min_review_rounds.max(1) }, + gate: GateConfig { + min_review_rounds: sw.min_review_rounds.max(1), + }, repo_url: sw.repo_url.clone(), repo_slug: sw.repo_slug.clone(), base_branch: sw.base_branch.clone(), @@ -1060,13 +1403,20 @@ mod tests { let row = store.lock().unwrap().get_unit("u1").unwrap().unwrap(); assert_eq!(row.oracle_hash.as_deref(), Some("h0000000000000abc")); - assert!(row.oracle_frozen, "oracle_frozen flips true once the fold sees OracleProposed"); + assert!( + row.oracle_frozen, + "oracle_frozen flips true once the fold sees OracleProposed" + ); } #[tokio::test] async fn reconcile_halts_stranded_unit_with_coherent_event() { let store = Arc::new(Mutex::new(Store::open_memory().unwrap())); - store.lock().unwrap().upsert_unit(&building_row("u1"), 1).unwrap(); + store + .lock() + .unwrap() + .upsert_unit(&building_row("u1"), 1) + .unwrap(); let state = AppState::new(store.clone()); // FakeRunner reports no running containers → unit is a stranded orphan. let runner = FakeRunner::new(vec![]); @@ -1090,20 +1440,29 @@ mod tests { // genuine orphan container (u2, no unit row, no driver) lingers. One tick // must converge by reaping ONLY the orphan — never the live unit's container. let store = Arc::new(Mutex::new(Store::open_memory().unwrap())); - store.lock().unwrap().upsert_unit(&building_row("u1"), 1).unwrap(); + store + .lock() + .unwrap() + .upsert_unit(&building_row("u1"), 1) + .unwrap(); let state = AppState::new(store.clone()); // u1 has a live in-memory driver → healthy. register_unit_if_absent(&state, "u1"); // The runner sees u1 (healthy) + u2 (stray). - let runner = - FakeRunner::new(vec![]).with_unit_containers(vec!["u1".into(), "u2".into()]); + let runner = FakeRunner::new(vec![]).with_unit_containers(vec!["u1".into(), "u2".into()]); reconcile_tick(&state, &runner).await; // u1 untouched: still non-terminal, driver still live. let row = store.lock().unwrap().get_unit("u1").unwrap().unwrap(); - assert_eq!(row.phase, "building", "healthy live unit must not be halted"); - assert!(state.units.lock().unwrap().contains_key("u1"), "live driver retained"); + assert_eq!( + row.phase, "building", + "healthy live unit must not be halted" + ); + assert!( + state.units.lock().unwrap().contains_key("u1"), + "live driver retained" + ); // Exactly one reap — the stray u2 (`reap_unit` bumps `teardowns`). assert_eq!( runner.teardowns.load(Ordering::Relaxed), @@ -1159,7 +1518,9 @@ mod tests { // A real WebSocket client subscribes from seq 0 and drains the stream. let url = format!("ws://127.0.0.1:{port}/units/{unit_id}/stream?since=0"); - let (mut ws, _) = tokio_tungstenite::connect_async(&url).await.expect("ws connect"); + let (mut ws, _) = tokio_tungstenite::connect_async(&url) + .await + .expect("ws connect"); let mut frames: Vec = Vec::new(); // The server holds the live tail open after replay (the unit's broadcast @@ -1195,7 +1556,11 @@ mod tests { // A non-terminal unit with no live driver and no container is stranded (its // driver died). The steady-state tick converges it to halted. let store = Arc::new(Mutex::new(Store::open_memory().unwrap())); - store.lock().unwrap().upsert_unit(&building_row("u1"), 1).unwrap(); + store + .lock() + .unwrap() + .upsert_unit(&building_row("u1"), 1) + .unwrap(); let state = AppState::new(store.clone()); // No driver registered for u1, no containers running. let runner = FakeRunner::new(vec![]); @@ -1247,7 +1612,11 @@ mod tests { let h1 = tokio::spawn(async move { register_unit_if_absent(&a, "u1").is_some() }); let h2 = tokio::spawn(async move { register_unit_if_absent(&b, "u1").is_some() }); let won = [h1.await.unwrap(), h2.await.unwrap()]; - assert_eq!(won.iter().filter(|&&w| w).count(), 1, "exactly one registration wins"); + assert_eq!( + won.iter().filter(|&&w| w).count(), + 1, + "exactly one registration wins" + ); assert_eq!(state.units.lock().unwrap().len(), 1, "no duplicate handle"); } @@ -1285,7 +1654,10 @@ mod tests { let h = units.get("u1").expect("rehydrated handle present"); (h.bcast.subscribe(), h.cmd_tx.clone()) }; - tx.send(Command::Resume { cmd_id: "r1".into() }).expect("driver alive"); + tx.send(Command::Resume { + cmd_id: "r1".into(), + }) + .expect("driver alive"); let mut saw_oracle = false; let mut done = false; @@ -1336,7 +1708,10 @@ mod tests { let h = units.get("u1").expect("rehydrated handle present"); (h.bcast.subscribe(), h.cmd_tx.clone()) }; - tx.send(Command::Resume { cmd_id: "r1".into() }).expect("driver alive"); + tx.send(Command::Resume { + cmd_id: "r1".into(), + }) + .expect("driver alive"); let mut needs_human = false; for _ in 0..200 { @@ -1348,19 +1723,33 @@ mod tests { .await .expect("timed out waiting for NeedsHuman: tamper gate did not re-arm on resume") .expect("event stream closed before NeedsHuman"); - if let Event::PhaseChanged { to: Phase::NeedsHuman, .. } = env.event { + if let Event::PhaseChanged { + to: Phase::NeedsHuman, + .. + } = env.event + { needs_human = true; break; } } - assert!(needs_human, "a stale reloaded oracle_hash must re-arm the tamper gate on resume"); - tx.send(Command::Abandon { cmd_id: "cleanup".into() }).expect("driver alive"); + assert!( + needs_human, + "a stale reloaded oracle_hash must re-arm the tamper gate on resume" + ); + tx.send(Command::Abandon { + cmd_id: "cleanup".into(), + }) + .expect("driver alive"); } #[tokio::test] async fn next_id_seeds_above_persisted_units() { let store = Arc::new(Mutex::new(Store::open_memory().unwrap())); - store.lock().unwrap().upsert_unit(&building_row("u5"), 1).unwrap(); + store + .lock() + .unwrap() + .upsert_unit(&building_row("u5"), 1) + .unwrap(); let state = AppState::new(store); // Fresh allocation must not collide with u5. let n = state.next_id.fetch_add(1, Ordering::Relaxed); @@ -1369,17 +1758,33 @@ mod tests { fn swarm_row_srv(id: &str, status: &str) -> crate::store::SwarmRow { crate::store::SwarmRow { - swarm_id: id.into(), repo_url: "u".into(), repo_slug: "s".into(), base_branch: "main".into(), - doc_path: "spec.md".into(), tier: "t1".into(), mode: "demo".into(), lane_cap: 8, usd_budget: 15.0, - per_lane_cap: 5.0, status: status.into(), planner_cost: 0.0, lanes_launched: 0, - lanes_dropped: 0, min_review_rounds: 1, terminal_reason: None, + swarm_id: id.into(), + repo_url: "u".into(), + repo_slug: "s".into(), + base_branch: "main".into(), + doc_path: "spec.md".into(), + tier: "t1".into(), + mode: "demo".into(), + lane_cap: 8, + usd_budget: 15.0, + per_lane_cap: 5.0, + status: status.into(), + planner_cost: 0.0, + lanes_launched: 0, + lanes_dropped: 0, + min_review_rounds: 1, + terminal_reason: None, } } #[tokio::test] async fn next_swarm_seeds_above_persisted_swarms() { let store = Arc::new(Mutex::new(Store::open_memory().unwrap())); - store.lock().unwrap().upsert_swarm(&swarm_row_srv("sw5", "running"), 1).unwrap(); + store + .lock() + .unwrap() + .upsert_swarm(&swarm_row_srv("sw5", "running"), 1) + .unwrap(); let state = AppState::new(store); let n = state.next_swarm.fetch_add(1, Ordering::Relaxed); assert_eq!(n, 6, "next swarm mint is sw6, never an existing id"); @@ -1395,13 +1800,20 @@ mod tests { let mut r = building_row("rsv"); r.phase = "building".into(); // non-terminal r.cost = 0.1; - r.usd_cap = 25.0; // reservation alone exceeds the $20 cap + r.usd_cap = 25.0; // reservation alone exceeds the $20 cap s.upsert_unit(&r, now_ms()).unwrap(); } let state = AppState::new(store); - let resp = create_mission(State(state), Json(CreateReq { - task: "t".into(), tier: TierReq::T1, mode: "demo".into(), min_review_rounds: 1, - })).await; + let resp = create_mission( + State(state), + Json(CreateReq { + task: "t".into(), + tier: TierReq::T1, + mode: "demo".into(), + min_review_rounds: 1, + }), + ) + .await; match resp { Err((code, _)) => assert_eq!(code, StatusCode::TOO_MANY_REQUESTS), Ok(_) => panic!("expected 429 — committed reservation breaches the cap"), @@ -1417,20 +1829,49 @@ mod tests { let store = std::sync::Arc::new(std::sync::Mutex::new(Store::open_memory().unwrap())); { let s = store.lock().unwrap(); - let mut r = building_row("seed"); r.phase = "building".into(); r.usd_cap = 16.0; r.cost = 0.0; + let mut r = building_row("seed"); + r.phase = "building".into(); + r.usd_cap = 16.0; + r.cost = 0.0; s.upsert_unit(&r, now_ms()).unwrap(); } let state = AppState::new(store.clone()); let a = state.clone(); let b = state.clone(); let h1 = tokio::spawn(async move { - create_mission(State(a), Json(CreateReq { task: "t".into(), tier: TierReq::T1, mode: "demo".into(), min_review_rounds: 1 })).await.is_ok() + create_mission( + State(a), + Json(CreateReq { + task: "t".into(), + tier: TierReq::T1, + mode: "demo".into(), + min_review_rounds: 1, + }), + ) + .await + .is_ok() }); let h2 = tokio::spawn(async move { - create_mission(State(b), Json(CreateReq { task: "t".into(), tier: TierReq::T1, mode: "demo".into(), min_review_rounds: 1 })).await.is_ok() + create_mission( + State(b), + Json(CreateReq { + task: "t".into(), + tier: TierReq::T1, + mode: "demo".into(), + min_review_rounds: 1, + }), + ) + .await + .is_ok() }); - let wins = [h1.await.unwrap(), h2.await.unwrap()].iter().filter(|&&w| w).count(); - assert_eq!(wins, 1, "exactly one mission admitted; the cap binds atomically"); + let wins = [h1.await.unwrap(), h2.await.unwrap()] + .iter() + .filter(|&&w| w) + .count(); + assert_eq!( + wins, 1, + "exactly one mission admitted; the cap binds atomically" + ); } #[test] @@ -1441,7 +1882,9 @@ mod tests { task: "t".into(), usd_cap: 5.0, wall_clock_secs: 0, - gate: GateConfig { min_review_rounds: 2 }, + gate: GateConfig { + min_review_rounds: 2, + }, repo_url: "https://github.com/x/y".into(), repo_slug: "x/y".into(), base_branch: "main".into(), @@ -1460,17 +1903,41 @@ mod tests { let state = AppState::default(); let sw = crate::store::SwarmRow { - swarm_id: "sw1".into(), repo_url: "https://github.com/x/y".into(), repo_slug: "x/y".into(), - base_branch: "main".into(), doc_path: "spec.md".into(), tier: "t1".into(), mode: "demo".into(), - lane_cap: 8, usd_budget: 100.0, per_lane_cap: 5.0, status: "planning".into(), - planner_cost: 0.0, lanes_launched: 0, lanes_dropped: 0, min_review_rounds: 1, + swarm_id: "sw1".into(), + repo_url: "https://github.com/x/y".into(), + repo_slug: "x/y".into(), + base_branch: "main".into(), + doc_path: "spec.md".into(), + tier: "t1".into(), + mode: "demo".into(), + lane_cap: 8, + usd_budget: 100.0, + per_lane_cap: 5.0, + status: "planning".into(), + planner_cost: 0.0, + lanes_launched: 0, + lanes_dropped: 0, + min_review_rounds: 1, terminal_reason: None, }; - state.store.lock().unwrap().upsert_swarm(&sw, now_ms()).unwrap(); + state + .store + .lock() + .unwrap() + .upsert_swarm(&sw, now_ms()) + .unwrap(); let lanes = vec![ - Lane { title: "Add A".into(), task: "do A".into(), rationale: "indep".into() }, - Lane { title: "Add B".into(), task: "do B".into(), rationale: "indep".into() }, + Lane { + title: "Add A".into(), + task: "do A".into(), + rationale: "indep".into(), + }, + Lane { + title: "Add B".into(), + task: "do B".into(), + rationale: "indep".into(), + }, ]; let planner = FakePlanner::ok(lanes, 0.2); let doc = FakeDocSource::new("# spec"); @@ -1478,21 +1945,39 @@ mod tests { run_swarm(state.clone(), "sw1".into(), planner, doc).await; let units = state.store.lock().unwrap().list_units().unwrap(); - let mine: Vec<_> = units.iter().filter(|u| u.swarm_id.as_deref() == Some("sw1")).cloned().collect(); + let mine: Vec<_> = units + .iter() + .filter(|u| u.swarm_id.as_deref() == Some("sw1")) + .cloned() + .collect(); assert_eq!(mine.len(), 2); assert!(mine.iter().all(|u| (u.usd_cap - 5.0).abs() < 1e-9)); - let branches: std::collections::HashSet<_> = mine.iter().map(|u| u.branch.clone()).collect(); + let branches: std::collections::HashSet<_> = + mine.iter().map(|u| u.branch.clone()).collect(); assert_eq!(branches.len(), 2, "unique branches"); assert!(branches.iter().all(|b| b.starts_with("agent/sw1/"))); let mut done = false; for _ in 0..500 { let (total, term, _) = state.store.lock().unwrap().swarm_rollup("sw1").unwrap(); - if total == 2 && term == 2 { done = true; break; } + if total == 2 && term == 2 { + done = true; + break; + } tokio::time::sleep(std::time::Duration::from_millis(10)).await; } assert!(done, "all lanes reached terminal"); - assert_eq!(state.store.lock().unwrap().get_swarm("sw1").unwrap().unwrap().status, "running"); + assert_eq!( + state + .store + .lock() + .unwrap() + .get_swarm("sw1") + .unwrap() + .unwrap() + .status, + "running" + ); } #[tokio::test] @@ -1502,17 +1987,55 @@ mod tests { use crate::swarm::Lane; let state = AppState::default(); let sw = crate::store::SwarmRow { - swarm_id: "sw2".into(), repo_url: "u".into(), repo_slug: "s".into(), base_branch: "main".into(), - doc_path: "spec.md".into(), tier: "t1".into(), mode: "demo".into(), - lane_cap: 8, usd_budget: 1.0, per_lane_cap: 5.0, status: "planning".into(), - planner_cost: 0.0, lanes_launched: 0, lanes_dropped: 0, min_review_rounds: 1, + swarm_id: "sw2".into(), + repo_url: "u".into(), + repo_slug: "s".into(), + base_branch: "main".into(), + doc_path: "spec.md".into(), + tier: "t1".into(), + mode: "demo".into(), + lane_cap: 8, + usd_budget: 1.0, + per_lane_cap: 5.0, + status: "planning".into(), + planner_cost: 0.0, + lanes_launched: 0, + lanes_dropped: 0, + min_review_rounds: 1, terminal_reason: None, }; - state.store.lock().unwrap().upsert_swarm(&sw, now_ms()).unwrap(); - let planner = FakePlanner::ok(vec![Lane { title: "A".into(), task: "a".into(), rationale: "r".into() }], 0.0); - run_swarm(state.clone(), "sw2".into(), planner, FakeDocSource::new("# spec")).await; - let got = state.store.lock().unwrap().get_swarm("sw2").unwrap().unwrap(); - assert_eq!(got.status, "empty", "lanes produced but none admitted ⇒ empty, never done"); + state + .store + .lock() + .unwrap() + .upsert_swarm(&sw, now_ms()) + .unwrap(); + let planner = FakePlanner::ok( + vec![Lane { + title: "A".into(), + task: "a".into(), + rationale: "r".into(), + }], + 0.0, + ); + run_swarm( + state.clone(), + "sw2".into(), + planner, + FakeDocSource::new("# spec"), + ) + .await; + let got = state + .store + .lock() + .unwrap() + .get_swarm("sw2") + .unwrap() + .unwrap(); + assert_eq!( + got.status, "empty", + "lanes produced but none admitted ⇒ empty, never done" + ); } #[tokio::test] @@ -1521,28 +2044,74 @@ mod tests { use crate::planner::FakePlanner; let state = AppState::default(); let sw = crate::store::SwarmRow { - swarm_id: "sw3".into(), repo_url: "u".into(), repo_slug: "s".into(), base_branch: "main".into(), - doc_path: "spec.md".into(), tier: "t1".into(), mode: "demo".into(), lane_cap: 8, usd_budget: 15.0, - per_lane_cap: 5.0, status: "planning".into(), planner_cost: 0.0, lanes_launched: 0, - lanes_dropped: 0, min_review_rounds: 1, terminal_reason: None, + swarm_id: "sw3".into(), + repo_url: "u".into(), + repo_slug: "s".into(), + base_branch: "main".into(), + doc_path: "spec.md".into(), + tier: "t1".into(), + mode: "demo".into(), + lane_cap: 8, + usd_budget: 15.0, + per_lane_cap: 5.0, + status: "planning".into(), + planner_cost: 0.0, + lanes_launched: 0, + lanes_dropped: 0, + min_review_rounds: 1, + terminal_reason: None, }; - state.store.lock().unwrap().upsert_swarm(&sw, now_ms()).unwrap(); - run_swarm(state.clone(), "sw3".into(), FakePlanner::err("boom"), FakeDocSource::new("x")).await; - assert_eq!(state.store.lock().unwrap().get_swarm("sw3").unwrap().unwrap().status, "failed"); + state + .store + .lock() + .unwrap() + .upsert_swarm(&sw, now_ms()) + .unwrap(); + run_swarm( + state.clone(), + "sw3".into(), + FakePlanner::err("boom"), + FakeDocSource::new("x"), + ) + .await; + assert_eq!( + state + .store + .lock() + .unwrap() + .get_swarm("sw3") + .unwrap() + .unwrap() + .status, + "failed" + ); } #[tokio::test] async fn post_swarms_validates_then_returns_id() { let state = AppState::default(); // unknown mode → error, no row created - let bad = create_swarm(State(state.clone()), Json(CreateSwarmReq { - doc_path: "spec.md".into(), mode: "weird".into(), ..Default::default() - })).await; + let bad = create_swarm( + State(state.clone()), + Json(CreateSwarmReq { + doc_path: "spec.md".into(), + mode: "weird".into(), + ..Default::default() + }), + ) + .await; assert!(bad.is_err()); // demo → ok, returns an id - let ok = create_swarm(State(state.clone()), Json(CreateSwarmReq { - doc_path: "spec.md".into(), mode: "demo".into(), ..Default::default() - })).await.expect("ok"); + let ok = create_swarm( + State(state.clone()), + Json(CreateSwarmReq { + doc_path: "spec.md".into(), + mode: "demo".into(), + ..Default::default() + }), + ) + .await + .expect("ok"); assert!(ok.0.swarm_id.starts_with("sw")); } @@ -1552,14 +2121,21 @@ mod tests { { let s = store.lock().unwrap(); // A swarm stuck mid-planning at crash → must become failed. - s.upsert_swarm(&swarm_row_srv("swP", "planning"), now_ms()).unwrap(); + s.upsert_swarm(&swarm_row_srv("swP", "planning"), now_ms()) + .unwrap(); // A swarm mid-fan-out: lane 0 launched (unit exists), lane 1 not. - let mut f = swarm_row_srv("swF", "fanning_out"); f.mode = "demo".into(); f.min_review_rounds = 1; + let mut f = swarm_row_srv("swF", "fanning_out"); + f.mode = "demo".into(); + f.min_review_rounds = 1; s.upsert_swarm(&f, now_ms()).unwrap(); - s.upsert_lane("swF", 0, "A", "ta", "r", "admit", Some("u1")).unwrap(); - let mut u = building_row("u1"); u.swarm_id = Some("swF".into()); u.phase = "building".into(); + s.upsert_lane("swF", 0, "A", "ta", "r", "admit", Some("u1")) + .unwrap(); + let mut u = building_row("u1"); + u.swarm_id = Some("swF".into()); + u.phase = "building".into(); s.upsert_unit(&u, now_ms()).unwrap(); - s.upsert_lane("swF", 1, "B", "tb", "r", "admit", None).unwrap(); + s.upsert_lane("swF", 1, "B", "tb", "r", "admit", None) + .unwrap(); } let state = AppState::new(store.clone()); let runner = FakeRunner::new(vec![]); @@ -1569,7 +2145,10 @@ mod tests { assert_eq!(s.get_swarm("swP").unwrap().unwrap().status, "failed"); // swF resumed: lane 1 now has a unit_id, status running. let lanes = s.lanes_for_swarm("swF").unwrap(); - assert!(lanes[1].unit_id.is_some(), "missing lane was committed on resume"); + assert!( + lanes[1].unit_id.is_some(), + "missing lane was committed on resume" + ); assert_eq!(s.get_swarm("swF").unwrap().unwrap().status, "running"); } @@ -1590,21 +2169,58 @@ mod tests { s.upsert_unit(&r, now_ms()).unwrap(); } let sw = crate::store::SwarmRow { - swarm_id: "swG".into(), repo_url: "u".into(), repo_slug: "s".into(), base_branch: "main".into(), - doc_path: "spec.md".into(), tier: "t1".into(), mode: "demo".into(), lane_cap: 8, usd_budget: 100.0, - per_lane_cap: 5.0, status: "planning".into(), planner_cost: 0.0, lanes_launched: 0, - lanes_dropped: 0, min_review_rounds: 1, terminal_reason: None, + swarm_id: "swG".into(), + repo_url: "u".into(), + repo_slug: "s".into(), + base_branch: "main".into(), + doc_path: "spec.md".into(), + tier: "t1".into(), + mode: "demo".into(), + lane_cap: 8, + usd_budget: 100.0, + per_lane_cap: 5.0, + status: "planning".into(), + planner_cost: 0.0, + lanes_launched: 0, + lanes_dropped: 0, + min_review_rounds: 1, + terminal_reason: None, }; - state.store.lock().unwrap().upsert_swarm(&sw, now_ms()).unwrap(); + state + .store + .lock() + .unwrap() + .upsert_swarm(&sw, now_ms()) + .unwrap(); let lanes = vec![ - Lane { title: "A".into(), task: "a".into(), rationale: "r".into() }, - Lane { title: "B".into(), task: "b".into(), rationale: "r".into() }, + Lane { + title: "A".into(), + task: "a".into(), + rationale: "r".into(), + }, + Lane { + title: "B".into(), + task: "b".into(), + rationale: "r".into(), + }, ]; - run_swarm(state.clone(), "swG".into(), FakePlanner::ok(lanes, 0.0), FakeDocSource::new("# spec")).await; + run_swarm( + state.clone(), + "swG".into(), + FakePlanner::ok(lanes, 0.0), + FakeDocSource::new("# spec"), + ) + .await; // Both lanes were admitted by admit_lanes (budget 100 allows them) but the global // cap trips at fan-out, so NEITHER launches and BOTH are recorded drop_global_cap. let lanes = state.store.lock().unwrap().lanes_for_swarm("swG").unwrap(); - assert!(lanes.iter().all(|l| l.decision == "drop_global_cap"), "no admit rows left dangling"); - assert!(lanes.iter().all(|l| l.unit_id.is_none()), "nothing launched"); + assert!( + lanes.iter().all(|l| l.decision == "drop_global_cap"), + "no admit rows left dangling" + ); + assert!( + lanes.iter().all(|l| l.unit_id.is_none()), + "nothing launched" + ); } } diff --git a/crates/fleetd/src/store.rs b/crates/fleetd/src/store.rs index bc7f6ad..a8c1f04 100644 --- a/crates/fleetd/src/store.rs +++ b/crates/fleetd/src/store.rs @@ -124,12 +124,26 @@ impl Store { oracle_frozen=(oracle_frozen OR (?6 IS NOT NULL)), updated_ts=?7 WHERE unit_id=?1", - params![unit_id, phase, cost, last_seq, terminal_reason, oracle_hash, now], + params![ + unit_id, + phase, + cost, + last_seq, + terminal_reason, + oracle_hash, + now + ], )?; Ok(()) } - pub fn append_event(&self, unit_id: &str, seq: u64, ts: i64, json: &str) -> rusqlite::Result<()> { + pub fn append_event( + &self, + unit_id: &str, + seq: u64, + ts: i64, + json: &str, + ) -> rusqlite::Result<()> { self.conn.execute( "INSERT OR IGNORE INTO events(unit_id,seq,ts,json) VALUES(?1,?2,?3,?4)", params![unit_id, seq, ts, json], @@ -195,7 +209,10 @@ impl Store { /// count their `planner_cost`. Partitioned by the authoritative terminal list. pub fn committed_spend(&self, since_ts: i64) -> rusqlite::Result { let terminal = fleet_core::TERMINAL_PHASE_STRS - .iter().map(|p| format!("'{p}'")).collect::>().join(","); + .iter() + .map(|p| format!("'{p}'")) + .collect::>() + .join(","); let sql = format!( "SELECT COALESCE((SELECT SUM(cost) FROM units @@ -268,28 +285,63 @@ impl Store { ON CONFLICT(swarm_id) DO UPDATE SET status=?11, planner_cost=?12, lanes_launched=?13, lanes_dropped=?14, terminal_reason=?16, updated_ts=?17", - params![r.swarm_id, r.repo_url, r.repo_slug, r.base_branch, r.doc_path, r.tier, r.mode, - r.lane_cap, r.usd_budget, r.per_lane_cap, r.status, r.planner_cost, - r.lanes_launched, r.lanes_dropped, r.min_review_rounds, r.terminal_reason, now], + params![ + r.swarm_id, + r.repo_url, + r.repo_slug, + r.base_branch, + r.doc_path, + r.tier, + r.mode, + r.lane_cap, + r.usd_budget, + r.per_lane_cap, + r.status, + r.planner_cost, + r.lanes_launched, + r.lanes_dropped, + r.min_review_rounds, + r.terminal_reason, + now + ], )?; Ok(()) } #[allow(clippy::too_many_arguments)] - pub fn update_swarm(&self, id: &str, status: &str, planner_cost: f64, lanes_launched: u32, - lanes_dropped: u32, terminal_reason: Option<&str>, now: i64) -> rusqlite::Result<()> { + pub fn update_swarm( + &self, + id: &str, + status: &str, + planner_cost: f64, + lanes_launched: u32, + lanes_dropped: u32, + terminal_reason: Option<&str>, + now: i64, + ) -> rusqlite::Result<()> { self.conn.execute( "UPDATE swarms SET status=?2, planner_cost=?3, lanes_launched=?4, lanes_dropped=?5, terminal_reason=?6, updated_ts=?7 WHERE swarm_id=?1", - params![id, status, planner_cost, lanes_launched, lanes_dropped, terminal_reason, now], + params![ + id, + status, + planner_cost, + lanes_launched, + lanes_dropped, + terminal_reason, + now + ], )?; Ok(()) } pub fn get_swarm(&self, id: &str) -> rusqlite::Result> { - self.conn.query_row(SELECT_SWARM_WHERE_ID, params![id], Self::map_swarm) - .map(Some).or_else(|e| match e { - rusqlite::Error::QueryReturnedNoRows => Ok(None), other => Err(other), + self.conn + .query_row(SELECT_SWARM_WHERE_ID, params![id], Self::map_swarm) + .map(Some) + .or_else(|e| match e { + rusqlite::Error::QueryReturnedNoRows => Ok(None), + other => Err(other), }) } @@ -303,7 +355,10 @@ impl Store { /// = non-terminal parked phases (`needs_human`/`halted`). pub fn swarm_rollup(&self, swarm_id: &str) -> rusqlite::Result<(u64, u64, u64)> { let terminal = fleet_core::TERMINAL_PHASE_STRS - .iter().map(|p| format!("'{p}'")).collect::>().join(","); + .iter() + .map(|p| format!("'{p}'")) + .collect::>() + .join(","); let sql = format!( "SELECT COUNT(*), @@ -312,18 +367,32 @@ impl Store { FROM units WHERE swarm_id=?1" ); self.conn.query_row(&sql, params![swarm_id], |r| { - Ok((r.get::<_, i64>(0)? as u64, r.get::<_, i64>(1)? as u64, r.get::<_, i64>(2)? as u64)) + Ok(( + r.get::<_, i64>(0)? as u64, + r.get::<_, i64>(1)? as u64, + r.get::<_, i64>(2)? as u64, + )) }) } fn map_swarm(r: &rusqlite::Row) -> rusqlite::Result { Ok(SwarmRow { - swarm_id: r.get(0)?, repo_url: r.get(1)?, repo_slug: r.get(2)?, base_branch: r.get(3)?, - doc_path: r.get(4)?, tier: r.get(5)?, mode: r.get(6)?, - lane_cap: r.get::<_, i64>(7)? as u32, usd_budget: r.get(8)?, per_lane_cap: r.get(9)?, - status: r.get(10)?, planner_cost: r.get(11)?, - lanes_launched: r.get::<_, i64>(12)? as u32, lanes_dropped: r.get::<_, i64>(13)? as u32, - min_review_rounds: r.get::<_, i64>(14)? as u32, terminal_reason: r.get(15)?, + swarm_id: r.get(0)?, + repo_url: r.get(1)?, + repo_slug: r.get(2)?, + base_branch: r.get(3)?, + doc_path: r.get(4)?, + tier: r.get(5)?, + mode: r.get(6)?, + lane_cap: r.get::<_, i64>(7)? as u32, + usd_budget: r.get(8)?, + per_lane_cap: r.get(9)?, + status: r.get(10)?, + planner_cost: r.get(11)?, + lanes_launched: r.get::<_, i64>(12)? as u32, + lanes_dropped: r.get::<_, i64>(13)? as u32, + min_review_rounds: r.get::<_, i64>(14)? as u32, + terminal_reason: r.get(15)?, }) } } @@ -342,8 +411,16 @@ pub struct LaneRow { impl Store { #[allow(clippy::too_many_arguments)] - pub fn upsert_lane(&self, swarm_id: &str, idx: u32, title: &str, task: &str, - rationale: &str, decision: &str, unit_id: Option<&str>) -> rusqlite::Result<()> { + pub fn upsert_lane( + &self, + swarm_id: &str, + idx: u32, + title: &str, + task: &str, + rationale: &str, + decision: &str, + unit_id: Option<&str>, + ) -> rusqlite::Result<()> { self.conn.execute( "INSERT INTO swarm_lanes(swarm_id,idx,title,task,rationale,decision,unit_id) VALUES(?1,?2,?3,?4,?5,?6,?7) @@ -356,18 +433,31 @@ impl Store { pub fn lanes_for_swarm(&self, swarm_id: &str) -> rusqlite::Result> { let mut s = self.conn.prepare( "SELECT swarm_id,idx,title,task,rationale,decision,unit_id - FROM swarm_lanes WHERE swarm_id=?1 ORDER BY idx")?; - let rows = s.query_map(params![swarm_id], |r| Ok(LaneRow { - swarm_id: r.get(0)?, idx: r.get::<_, i64>(1)? as u32, title: r.get(2)?, - task: r.get(3)?, rationale: r.get(4)?, decision: r.get(5)?, unit_id: r.get(6)?, - }))?; + FROM swarm_lanes WHERE swarm_id=?1 ORDER BY idx", + )?; + let rows = s.query_map(params![swarm_id], |r| { + Ok(LaneRow { + swarm_id: r.get(0)?, + idx: r.get::<_, i64>(1)? as u32, + title: r.get(2)?, + task: r.get(3)?, + rationale: r.get(4)?, + decision: r.get(5)?, + unit_id: r.get(6)?, + }) + })?; rows.collect() } /// Insert the lane's unit row AND set `swarm_lanes.unit_id` in ONE transaction, /// so a crash never leaves a dangling back-link or an orphan row (spec R2 #5). - pub fn commit_lane_unit(&self, swarm_id: &str, idx: u32, u: &UnitRow, now: i64) - -> rusqlite::Result<()> { + pub fn commit_lane_unit( + &self, + swarm_id: &str, + idx: u32, + u: &UnitRow, + now: i64, + ) -> rusqlite::Result<()> { self.conn.execute_batch("BEGIN IMMEDIATE")?; let r: rusqlite::Result<()> = (|| { self.upsert_unit(u, now)?; @@ -378,8 +468,14 @@ impl Store { Ok(()) })(); match r { - Ok(()) => { self.conn.execute_batch("COMMIT")?; Ok(()) } - Err(e) => { let _ = self.conn.execute_batch("ROLLBACK"); Err(e) } + Ok(()) => { + self.conn.execute_batch("COMMIT")?; + Ok(()) + } + Err(e) => { + let _ = self.conn.execute_batch("ROLLBACK"); + Err(e) + } } } } @@ -416,8 +512,10 @@ mod tests { fn upsert_append_list_since_spend() { let s = Store::open_memory().unwrap(); s.upsert_unit(&row("u1"), 1000).unwrap(); - s.append_event("u1", 1, 1000, r#"{"type":"phase_changed"}"#).unwrap(); - s.append_event("u1", 2, 1001, r#"{"type":"metric"}"#).unwrap(); + s.append_event("u1", 1, 1000, r#"{"type":"phase_changed"}"#) + .unwrap(); + s.append_event("u1", 2, 1001, r#"{"type":"metric"}"#) + .unwrap(); assert_eq!(s.events_since("u1", 0).unwrap().len(), 2); assert_eq!(s.events_since("u1", 1).unwrap().len(), 1); @@ -448,7 +546,10 @@ mod tests { s.upsert_unit(&upd, 1001).unwrap(); let got = s.get_unit("u1").unwrap().unwrap(); assert_eq!(got.mode, "real", "mode is set-once at create"); - assert_eq!(got.min_review_rounds, 3, "review floor is set-once at create"); + assert_eq!( + got.min_review_rounds, 3, + "review floor is set-once at create" + ); assert_eq!(got.phase, "building", "projection columns still update"); } @@ -461,21 +562,52 @@ mod tests { r.oracle_hash = Some("h0000000000000abc".into()); s.upsert_unit(&r, 1000).unwrap(); let got = s.get_unit("u1").unwrap().unwrap(); - assert_eq!(got.oracle_hash.as_deref(), Some("h0000000000000abc"), "oracle_hash round-trips"); - assert!(!got.oracle_frozen, "upsert_unit alone does not flip oracle_frozen"); + assert_eq!( + got.oracle_hash.as_deref(), + Some("h0000000000000abc"), + "oracle_hash round-trips" + ); + assert!( + !got.oracle_frozen, + "upsert_unit alone does not flip oracle_frozen" + ); - s.update_unit("u1", "checking", got.cost, got.last_seq + 1, None, - Some("h0000000000000abc"), 1001).unwrap(); + s.update_unit( + "u1", + "checking", + got.cost, + got.last_seq + 1, + None, + Some("h0000000000000abc"), + 1001, + ) + .unwrap(); let got2 = s.get_unit("u1").unwrap().unwrap(); assert_eq!(got2.oracle_hash.as_deref(), Some("h0000000000000abc")); - assert!(got2.oracle_frozen, "oracle_frozen flips true once update_unit sees a hash"); + assert!( + got2.oracle_frozen, + "oracle_frozen flips true once update_unit sees a hash" + ); // A later update_unit call with oracle_hash=None (the common case — most // phase transitions don't carry a fresh hash) must NOT clobber the // already-persisted hash/frozen state (COALESCE / OR are no-ops on None). - s.update_unit("u1", "reviewing", got2.cost, got2.last_seq + 1, None, None, 1002).unwrap(); + s.update_unit( + "u1", + "reviewing", + got2.cost, + got2.last_seq + 1, + None, + None, + 1002, + ) + .unwrap(); let got3 = s.get_unit("u1").unwrap().unwrap(); - assert_eq!(got3.oracle_hash.as_deref(), Some("h0000000000000abc"), "None must not wipe a prior hash"); + assert_eq!( + got3.oracle_hash.as_deref(), + Some("h0000000000000abc"), + "None must not wipe a prior hash" + ); assert!(got3.oracle_frozen, "None must not un-freeze"); } @@ -501,7 +633,9 @@ mod tests { [], ).unwrap(); // units.swarm_id column present: - s.conn.execute("UPDATE units SET swarm_id='s1' WHERE unit_id='nope'", []).unwrap(); + s.conn + .execute("UPDATE units SET swarm_id='s1' WHERE unit_id='nope'", []) + .unwrap(); } #[test] @@ -510,7 +644,10 @@ mod tests { let mut r = row("u1"); r.swarm_id = Some("sw1".into()); s.upsert_unit(&r, 1000).unwrap(); - assert_eq!(s.get_unit("u1").unwrap().unwrap().swarm_id.as_deref(), Some("sw1")); + assert_eq!( + s.get_unit("u1").unwrap().unwrap().swarm_id.as_deref(), + Some("sw1") + ); } #[test] @@ -529,20 +666,33 @@ mod tests { s.upsert_unit(&row("u3"), 1).unwrap(); s.upsert_unit(&row("u10"), 1).unwrap(); s.upsert_unit(&row("u2"), 1).unwrap(); - assert_eq!(s.max_unit_seq().unwrap(), 10, "parses the numeric suffix, not lexical max"); + assert_eq!( + s.max_unit_seq().unwrap(), + 10, + "parses the numeric suffix, not lexical max" + ); } #[test] fn committed_spend_counts_reservations_and_overcap_and_planner() { let s = Store::open_memory().unwrap(); // A terminal unit contributes its final cost. - let mut done = row("done"); done.phase = "done".into(); done.cost = 1.0; done.usd_cap = 5.0; + let mut done = row("done"); + done.phase = "done".into(); + done.cost = 1.0; + done.usd_cap = 5.0; s.upsert_unit(&done, 1000).unwrap(); // A non-terminal unit under cap contributes its usd_cap (reservation). - let mut run = row("run"); run.phase = "building".into(); run.cost = 0.5; run.usd_cap = 5.0; + let mut run = row("run"); + run.phase = "building".into(); + run.cost = 0.5; + run.usd_cap = 5.0; s.upsert_unit(&run, 1000).unwrap(); // A non-terminal unit that BILLED PAST its cap contributes cost (MAX), not usd_cap. - let mut over = row("over"); over.phase = "building".into(); over.cost = 9.0; over.usd_cap = 5.0; + let mut over = row("over"); + over.phase = "building".into(); + over.cost = 9.0; + over.usd_cap = 5.0; s.upsert_unit(&over, 1000).unwrap(); // Planner cost of a swarm counts too. s.conn.execute( @@ -556,21 +706,38 @@ mod tests { #[test] fn committed_spend_window_excludes_old() { let s = Store::open_memory().unwrap(); - let mut old = row("old"); old.phase = "building".into(); old.usd_cap = 5.0; + let mut old = row("old"); + old.phase = "building".into(); + old.usd_cap = 5.0; s.upsert_unit(&old, 100).unwrap(); s.conn.execute( "INSERT INTO swarms(swarm_id,status,planner_cost,created_ts,updated_ts) VALUES('oldsw','failed',7.0,100,100)", [], ).unwrap(); - assert_eq!(s.committed_spend(500).unwrap(), 0.0, "created before the window is excluded"); + assert_eq!( + s.committed_spend(500).unwrap(), + 0.0, + "created before the window is excluded" + ); } fn swarm_row(id: &str, status: &str) -> SwarmRow { SwarmRow { - swarm_id: id.into(), repo_url: "u".into(), repo_slug: "s".into(), base_branch: "main".into(), - doc_path: "spec.md".into(), tier: "t1".into(), mode: "demo".into(), - lane_cap: 8, usd_budget: 15.0, per_lane_cap: 5.0, status: status.into(), - planner_cost: 0.0, lanes_launched: 0, lanes_dropped: 0, min_review_rounds: 2, + swarm_id: id.into(), + repo_url: "u".into(), + repo_slug: "s".into(), + base_branch: "main".into(), + doc_path: "spec.md".into(), + tier: "t1".into(), + mode: "demo".into(), + lane_cap: 8, + usd_budget: 15.0, + per_lane_cap: 5.0, + status: status.into(), + planner_cost: 0.0, + lanes_launched: 0, + lanes_dropped: 0, + min_review_rounds: 2, terminal_reason: None, } } @@ -581,7 +748,8 @@ mod tests { let sw = swarm_row("sw1", "planning"); s.upsert_swarm(&sw, 1000).unwrap(); assert_eq!(s.get_swarm("sw1").unwrap().unwrap().status, "planning"); - s.update_swarm("sw1", "running", 0.4, 3, 0, None, 1001).unwrap(); + s.update_swarm("sw1", "running", 0.4, 3, 0, None, 1001) + .unwrap(); let got = s.get_swarm("sw1").unwrap().unwrap(); assert_eq!(got.status, "running"); assert_eq!(got.planner_cost, 0.4); @@ -592,31 +760,45 @@ mod tests { #[test] fn lane_crud_and_commit_is_atomic() { let s = Store::open_memory().unwrap(); - s.upsert_swarm(&swarm_row("sw1", "fanning_out"), 1000).unwrap(); + s.upsert_swarm(&swarm_row("sw1", "fanning_out"), 1000) + .unwrap(); // Persist a lane with no unit yet. - s.upsert_lane("sw1", 0, "Add auth", "do auth", "indep", "admit", None).unwrap(); + s.upsert_lane("sw1", 0, "Add auth", "do auth", "indep", "admit", None) + .unwrap(); let lanes = s.lanes_for_swarm("sw1").unwrap(); assert_eq!(lanes.len(), 1); assert_eq!(lanes[0].decision, "admit"); assert!(lanes[0].unit_id.is_none()); // commit_lane_unit inserts the unit row AND sets the back-link in one txn. - let mut u = row("u1"); u.swarm_id = Some("sw1".into()); + let mut u = row("u1"); + u.swarm_id = Some("sw1".into()); s.commit_lane_unit("sw1", 0, &u, 1001).unwrap(); assert!(s.get_unit("u1").unwrap().is_some(), "unit row inserted"); - assert_eq!(s.lanes_for_swarm("sw1").unwrap()[0].unit_id.as_deref(), Some("u1"), "back-link set"); + assert_eq!( + s.lanes_for_swarm("sw1").unwrap()[0].unit_id.as_deref(), + Some("u1"), + "back-link set" + ); } #[test] fn swarm_rollup_counts_terminal_and_awaiting_human() { let s = Store::open_memory().unwrap(); - for (id, phase) in [("u1","done"), ("u2","failed"), ("u3","building"), ("u4","needs_human")] { - let mut r = row(id); r.phase = phase.into(); r.swarm_id = Some("sw1".into()); + for (id, phase) in [ + ("u1", "done"), + ("u2", "failed"), + ("u3", "building"), + ("u4", "needs_human"), + ] { + let mut r = row(id); + r.phase = phase.into(); + r.swarm_id = Some("sw1".into()); s.upsert_unit(&r, 1000).unwrap(); } let (total, terminal, awaiting) = s.swarm_rollup("sw1").unwrap(); assert_eq!(total, 4); - assert_eq!(terminal, 2); // done + failed - assert_eq!(awaiting, 1); // needs_human (halted would also count) + assert_eq!(terminal, 2); // done + failed + assert_eq!(awaiting, 1); // needs_human (halted would also count) } } diff --git a/crates/fleetd/src/swarm.rs b/crates/fleetd/src/swarm.rs index 7ad9ec1..2d410bd 100644 --- a/crates/fleetd/src/swarm.rs +++ b/crates/fleetd/src/swarm.rs @@ -22,7 +22,11 @@ pub struct AdmissionConfig { /// Per-lane admission verdict. `DropOverGlobalCap` is set later by the fan-out /// loop (a runtime re-check), never by `admit_lanes`. #[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum LaneDecision { Admit, DropOverLaneCap, DropOverBudget } +pub enum LaneDecision { + Admit, + DropOverLaneCap, + DropOverBudget, +} /// Walk lanes in order; admit while BOTH the count cap and the /// (usd_budget − planner_cost) envelope hold. Conservative: each admitted lane @@ -30,17 +34,21 @@ pub enum LaneDecision { Admit, DropOverLaneCap, DropOverBudget } pub fn admit_lanes(lanes: &[Lane], cfg: &AdmissionConfig) -> Vec<(usize, LaneDecision)> { let envelope = (cfg.usd_budget - cfg.planner_cost).max(0.0); let mut admitted = 0usize; - lanes.iter().enumerate().map(|(i, _)| { - let decision = if admitted >= cfg.lane_cap { - LaneDecision::DropOverLaneCap - } else if (admitted as f64 + 1.0) * cfg.per_lane_cap > envelope { - LaneDecision::DropOverBudget - } else { - admitted += 1; - LaneDecision::Admit - }; - (i, decision) - }).collect() + lanes + .iter() + .enumerate() + .map(|(i, _)| { + let decision = if admitted >= cfg.lane_cap { + LaneDecision::DropOverLaneCap + } else if (admitted as f64 + 1.0) * cfg.per_lane_cap > envelope { + LaneDecision::DropOverBudget + } else { + admitted += 1; + LaneDecision::Admit + }; + (i, decision) + }) + .collect() } /// Sanitize a planner-chosen lane title into a git-ref-safe, length-bounded @@ -61,7 +69,11 @@ pub fn slug(title: &str) -> String { } let trimmed: String = out.trim_matches('-').chars().take(32).collect(); let trimmed = trimmed.trim_matches('-').to_string(); - if trimmed.is_empty() { "lane".into() } else { trimmed } + if trimmed.is_empty() { + "lane".into() + } else { + trimmed + } } #[cfg(test)] @@ -72,19 +84,37 @@ mod tests { fn slug_sanitizes_charset_length_and_empty() { assert_eq!(slug("Add Auth!!"), "add-auth"); assert_eq!(slug(" spaced out "), "spaced-out"); - assert_eq!(slug("🚀🚀🚀"), "lane"); // non-ascii → fallback + assert_eq!(slug("🚀🚀🚀"), "lane"); // non-ascii → fallback assert_eq!(slug(""), "lane"); assert_eq!(slug(&"x".repeat(100)).len(), 32); // truncated } fn lanes(n: usize) -> Vec { - (0..n).map(|i| Lane { title: format!("l{i}"), task: "t".into(), rationale: "r".into() }).collect() + (0..n) + .map(|i| Lane { + title: format!("l{i}"), + task: "t".into(), + rationale: "r".into(), + }) + .collect() } - fn cfg(lane_cap: usize, usd_budget: f64, per_lane_cap: f64, planner_cost: f64) -> AdmissionConfig { - AdmissionConfig { lane_cap, usd_budget, per_lane_cap, planner_cost } + fn cfg( + lane_cap: usize, + usd_budget: f64, + per_lane_cap: f64, + planner_cost: f64, + ) -> AdmissionConfig { + AdmissionConfig { + lane_cap, + usd_budget, + per_lane_cap, + planner_cost, + } } fn admits(d: &[(usize, LaneDecision)]) -> usize { - d.iter().filter(|(_, x)| matches!(x, LaneDecision::Admit)).count() + d.iter() + .filter(|(_, x)| matches!(x, LaneDecision::Admit)) + .count() } #[test] @@ -114,7 +144,9 @@ mod tests { fn planner_over_budget_admits_zero() { let d = admit_lanes(&lanes(4), &cfg(8, 4.0, 5.0, 5.0)); assert_eq!(admits(&d), 0); - assert!(d.iter().all(|(_, x)| matches!(x, LaneDecision::DropOverBudget))); + assert!(d + .iter() + .all(|(_, x)| matches!(x, LaneDecision::DropOverBudget))); } #[test] diff --git a/crates/fleetd/tests/demo_mode_it.rs b/crates/fleetd/tests/demo_mode_it.rs index 5eff200..2e08224 100644 --- a/crates/fleetd/tests/demo_mode_it.rs +++ b/crates/fleetd/tests/demo_mode_it.rs @@ -41,7 +41,10 @@ fn demo_script(spec: &UnitSpec) -> Vec { for remaining in (0..floor).rev() { s.push(FakeRunner::ok(0.03, &["implementing the change"])); s.push(FakeRunner::ok(0.0, &["tests: 1 passing"])); - s.push(FakeRunner::ok(0.04, &[&format!("review done\nBLOCKERS={remaining}")])); + s.push(FakeRunner::ok( + 0.04, + &[&format!("review done\nBLOCKERS={remaining}")], + )); } s } @@ -55,7 +58,9 @@ fn demo_spec(floor: u32) -> UnitSpec { task: "add a sum() helper".into(), usd_cap: 5.0, wall_clock_secs: 1800, - gate: GateConfig { min_review_rounds: floor.max(1) }, + gate: GateConfig { + min_review_rounds: floor.max(1), + }, repo_url: "https://github.com/adbarc92/command-center-agent-sandbox".into(), repo_slug: "adbarc92/command-center-agent-sandbox".into(), base_branch: "main".into(), @@ -128,24 +133,37 @@ async fn demo_mission_reaches_terminal_on_fakes_no_docker_no_real_pr() { Phase::PrOpen, Phase::Done, ] { - assert!(seq.contains(&required), "missing phase {required:?} in {seq:?}"); + assert!( + seq.contains(&required), + "missing phase {required:?} in {seq:?}" + ); } // T1 freezes the oracle automatically — never parks for human approval in demo. assert!( !seq.contains(&Phase::AwaitingOracleApproval), "T1 demo auto-freezes the oracle; no human gate" ); - assert!(!seq.contains(&Phase::Failed), "demo happy path never fails: {seq:?}"); + assert!( + !seq.contains(&Phase::Failed), + "demo happy path never fails: {seq:?}" + ); // 3. No REAL PR: the only PR artifact is FakeForge's faked url, never github.com. let pr_refs: Vec<&str> = events .iter() .filter_map(|e| match &e.event { - Event::Artifact { kind: fleet_core::ArtifactKind::Pr, reference } => Some(reference.as_str()), + Event::Artifact { + kind: fleet_core::ArtifactKind::Pr, + reference, + } => Some(reference.as_str()), _ => None, }) .collect(); - assert_eq!(pr_refs, vec![FAKE_PR_URL], "demo opens only a FAKED PR, never a real one"); + assert_eq!( + pr_refs, + vec![FAKE_PR_URL], + "demo opens only a FAKED PR, never a real one" + ); assert!( !pr_refs.iter().any(|r| r.contains("github.com")), "no real github.com PR may be opened in demo: {pr_refs:?}" @@ -162,13 +180,21 @@ async fn demo_mission_reaches_terminal_on_fakes_no_docker_no_real_pr() { _ => None, }) .fold(0.0_f64, f64::max); - assert!((final_cost - 0.16).abs() < 1e-9, "demo metered fake cost is 0.16, got {final_cost}"); - assert!(final_cost < 5.0, "demo never approaches the usd cap — no real spend"); + assert!( + (final_cost - 0.16).abs() < 1e-9, + "demo metered fake cost is 0.16, got {final_cost}" + ); + assert!( + final_cost < 5.0, + "demo never approaches the usd cap — no real spend" + ); // 5. The event stream emitted normally (the cockpit has something to render): // iterations, logs, findings, a terminal Done. assert!( - events.iter().any(|e| matches!(e.event, Event::Iteration { .. })), + events + .iter() + .any(|e| matches!(e.event, Event::Iteration { .. })), "demo emits Iteration events" ); assert!( @@ -176,12 +202,17 @@ async fn demo_mission_reaches_terminal_on_fakes_no_docker_no_real_pr() { "demo emits Log events" ); assert!( - events.iter().any(|e| matches!(&e.event, Event::Done { result } if result == "done")), + events + .iter() + .any(|e| matches!(&e.event, Event::Done { result } if result == "done")), "demo emits a terminal Done(done)" ); // Seqs are strictly increasing — a coherent stream for the WS replay/dedup. let seqs: Vec = events.iter().map(|e| e.seq).collect(); - assert!(seqs.windows(2).all(|w| w[0] < w[1]), "event seqs strictly increase: {seqs:?}"); + assert!( + seqs.windows(2).all(|w| w[0] < w[1]), + "event seqs strictly increase: {seqs:?}" + ); } /// The review gate opens exactly on the floor: floor=3 runs three review rounds @@ -195,7 +226,10 @@ async fn demo_runs_full_review_rounds_to_the_floor() { .iter() .filter(|e| matches!(e.event, Event::Iteration { kind: fleet_core::IterationKind::Review, n } if n >= 1)) .count(); - assert_eq!(review_rounds, 3, "floor=3 demo runs three review rounds before the gate opens"); + assert_eq!( + review_rounds, 3, + "floor=3 demo runs three review rounds before the gate opens" + ); } /// Guard against drift: this test's mirrored `demo_script` must match the daemon's diff --git a/crates/fleetd/tests/local_docker_it.rs b/crates/fleetd/tests/local_docker_it.rs index d08b220..099a929 100644 --- a/crates/fleetd/tests/local_docker_it.rs +++ b/crates/fleetd/tests/local_docker_it.rs @@ -44,7 +44,9 @@ async fn provision_commit_export_roundtrip() { git commit -q -m base; git checkout -q -b agent/it; \ printf 'x\\ny\\n' > f.txt; git add f.txt; git commit -q -m feat; \ git rev-parse agent/it"; - let exec = runner.exec(&handle, "/work", &["sh".into(), "-c".into(), script.into()]).await; + let exec = runner + .exec(&handle, "/work", &["sh".into(), "-c".into(), script.into()]) + .await; let bundle = match &exec { Ok(o) if o.exit_code == 0 => runner.export_bundle(&handle, "agent/it").await.ok(), _ => None, @@ -59,7 +61,12 @@ async fn provision_commit_export_roundtrip() { .status() .ok()?; let o = StdCommand::new("git") - .args(["-C", &dir.to_string_lossy(), "rev-parse", "refs/remotes/origin/agent/it"]) + .args([ + "-C", + &dir.to_string_lossy(), + "rev-parse", + "refs/remotes/origin/agent/it", + ]) .output() .ok()?; Some(String::from_utf8_lossy(&o.stdout).trim().to_string()) @@ -75,6 +82,14 @@ async fn provision_commit_export_roundtrip() { let out = exec.expect("exec git script"); assert_eq!(out.exit_code, 0, "git script failed: {:?}", out.stdout); let container_sha = out.stdout.last().expect("a sha line").trim().to_string(); - assert_eq!(container_sha.len(), 40, "expected a full sha, got {container_sha:?}"); - assert_eq!(host_sha.as_deref(), Some(container_sha.as_str()), "host SHA must match container"); + assert_eq!( + container_sha.len(), + 40, + "expected a full sha, got {container_sha:?}" + ); + assert_eq!( + host_sha.as_deref(), + Some(container_sha.as_str()), + "host SHA must match container" + ); } diff --git a/crates/fleetd/tests/preflight_it.rs b/crates/fleetd/tests/preflight_it.rs index 740b1a1..ba43bef 100644 --- a/crates/fleetd/tests/preflight_it.rs +++ b/crates/fleetd/tests/preflight_it.rs @@ -22,7 +22,10 @@ const URL: &str = "https://github.com/adbarc92/command-center-agent-sandbox"; #[tokio::test] #[ignore = "requires Docker + cc-agent:dev + authed gh; opens a real PR"] async fn full_pipeline_opens_a_real_mergeable_pr() { - let millis = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_millis(); + let millis = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_millis(); let unit_id = format!("preflight-{millis}"); let branch = format!("agent/{unit_id}"); @@ -42,7 +45,10 @@ async fn full_pipeline_opens_a_real_mergeable_pr() { }; let runner = LocalDockerRunner::new("cc-agent:dev"); - let handle = runner.provision(&spec).await.expect("provision (clone sandbox + branch)"); + let handle = runner + .provision(&spec) + .await + .expect("provision (clone sandbox + branch)"); // ── stub the agent: write impl + its test (what oracle+build would do) ── let work = "set -e; cd /work/repo; \ @@ -51,30 +57,60 @@ async fn full_pipeline_opens_a_real_mergeable_pr() { printf 'const test=require(\"node:test\");const assert=require(\"node:assert\");\ const {sum}=require(\"./src/index.js\");\ test(\"sum adds\",()=>assert.strictEqual(sum(2,3),5));\\n' > sum.test.js"; - let w = runner.exec(&handle, "/work/repo", &["sh".into(), "-c".into(), work.into()]).await + let w = runner + .exec( + &handle, + "/work/repo", + &["sh".into(), "-c".into(), work.into()], + ) + .await .expect("write stub files"); assert_eq!(w.exit_code, 0, "stub write failed: {:?}", w.stdout); // ── daemon commits the work ── - let committed = runner.commit_all(&handle, "feat: implement sum").await.expect("commit_all"); + let committed = runner + .commit_all(&handle, "feat: implement sum") + .await + .expect("commit_all"); assert!(committed, "expected a commit to be created"); // ── checks (the objective signal) ── - let check = runner.exec(&handle, "/work/repo", &["node".into(), "--test".into()]).await + let check = runner + .exec(&handle, "/work/repo", &["node".into(), "--test".into()]) + .await .expect("run checks"); assert_eq!(check.exit_code, 0, "checks should pass: {:?}", check.stdout); // ── non-empty diff vs base ── - let diff = runner.has_diff(&handle, "main", &branch).await.expect("has_diff"); + let diff = runner + .has_diff(&handle, "main", &branch) + .await + .expect("has_diff"); assert!(diff, "expected a non-empty diff vs main"); // ── escape the branch + open a real PR via GhForge ── - let bundle = runner.export_bundle(&handle, &branch).await.expect("export bundle"); + let bundle = runner + .export_bundle(&handle, &branch) + .await + .expect("export bundle"); let host_clone = std::env::temp_dir().join(format!("cc-preflight-{millis}")); - let forge = GhForge::new(URL, SLUG, "main", host_clone.clone(), format!("pre-flight: {unit_id}")); + let forge = GhForge::new( + URL, + SLUG, + "main", + host_clone.clone(), + format!("pre-flight: {unit_id}"), + ); - let merge = forge.trial_merge(&bundle, &branch).await.expect("trial merge"); - assert_eq!(merge, MergeResult::Clean, "branch should merge cleanly onto main"); + let merge = forge + .trial_merge(&bundle, &branch) + .await + .expect("trial merge"); + assert_eq!( + merge, + MergeResult::Clean, + "branch should merge cleanly onto main" + ); let pr = forge.open_pr(&branch).await.expect("open PR"); println!("PR: {pr}"); @@ -89,7 +125,11 @@ test(\"sum adds\",()=>assert.strictEqual(sum(2,3),5));\\n' > sum.test.js"; } tokio::time::sleep(Duration::from_secs(2)).await; } - assert_eq!(verdict, Mergeability::Mergeable, "GitHub should report the PR mergeable"); + assert_eq!( + verdict, + Mergeability::Mergeable, + "GitHub should report the PR mergeable" + ); // ── cleanup the container + host clone (leave the PR as proof) ── runner.teardown(&handle).await.expect("teardown"); diff --git a/crates/fleetd/tests/swarm_smoke_it.rs b/crates/fleetd/tests/swarm_smoke_it.rs index 4aa2089..4af624f 100644 --- a/crates/fleetd/tests/swarm_smoke_it.rs +++ b/crates/fleetd/tests/swarm_smoke_it.rs @@ -5,6 +5,12 @@ async fn git_doc_source_clones_reads_and_cleans_up() { use fleetd::docsource::{DocSource, GitDocSource}; let d = GitDocSource::new(); - let out = d.read("https://github.com/adbarc92/command-center-agent-sandbox", "main", "README.md").await; + let out = d + .read( + "https://github.com/adbarc92/command-center-agent-sandbox", + "main", + "README.md", + ) + .await; assert!(out.is_ok(), "reads a known file: {out:?}"); } From e751661c4747a55124fa9300a3965e66c5bee729 Mon Sep 17 00:00:00 2001 From: Alex Barclay Date: Thu, 13 Aug 2026 21:23:31 -0600 Subject: [PATCH 3/7] fix(cockpit): clear clippy debt in the src-tauri crate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The root workspace was already clean under '-D warnings'; the cockpit crate had never been linted and had twelve findings. Three were real and are fixed: a nonminimal_bool in the discovery filter (the pruned test is now a named binding rather than a negated conjunction) and two unnecessary_to_owned in the check_crash tests. The other nine are dead_code on plugin-runtime scaffolding that is deliberately ahead of its caller — the Phase-6 embedding layer that will read the webview manifest surface, the crash watcher that will poll Spawner::has_exited via check_crash, and the STOPPED rest state. Deleting any of it would amputate an in-flight feature, so each site gets a narrowly scoped #[allow(dead_code)] with a comment naming the caller it is waiting on. No blanket allows, and nothing at module or crate level. --- cockpit/ui/src-tauri/src/local_projects.rs | 4 ++-- cockpit/ui/src-tauri/src/plugins/manager.rs | 5 +++++ cockpit/ui/src-tauri/src/plugins/manifest.rs | 11 +++++++++++ cockpit/ui/src-tauri/src/plugins/seams.rs | 6 ++++++ cockpit/ui/src-tauri/src/plugins/state.rs | 10 ++++++++-- 5 files changed, 32 insertions(+), 4 deletions(-) diff --git a/cockpit/ui/src-tauri/src/local_projects.rs b/cockpit/ui/src-tauri/src/local_projects.rs index 0ff1716..9183663 100644 --- a/cockpit/ui/src-tauri/src/local_projects.rs +++ b/cockpit/ui/src-tauri/src/local_projects.rs @@ -75,8 +75,8 @@ fn discover(root: &Path, max_depth: usize, excludes: &[String], out: &mut Vec, pub owned: bool, } @@ -33,6 +36,8 @@ impl PluginManager { } /// The head URL for a discovered plugin (used by the Phase-6 embedding layer). + // That layer is the only caller and has not landed yet. + #[allow(dead_code)] pub fn url_for(&self, id: &str) -> Option { self.discovered .lock() diff --git a/cockpit/ui/src-tauri/src/plugins/manifest.rs b/cockpit/ui/src-tauri/src/plugins/manifest.rs index 1f893e4..50de9d9 100644 --- a/cockpit/ui/src-tauri/src/plugins/manifest.rs +++ b/cockpit/ui/src-tauri/src/plugins/manifest.rs @@ -12,12 +12,18 @@ pub struct Manifest { pub icon: String, pub url: String, pub lifecycle: Lifecycle, + // Parsed and validated here; consumed by the Phase-6 embedding layer that + // creates the webview these settings configure. + #[allow(dead_code)] #[serde(default)] pub webview: WebviewCfg, } #[derive(Debug, Clone, Deserialize)] pub struct Lifecycle { + // Manifest surface that is accepted today but not yet acted on: every plugin + // is treated as managed until adopt-only stacks are wired up. + #[allow(dead_code)] #[serde(default)] pub managed: bool, #[serde(default)] @@ -65,6 +71,9 @@ fn default_probe_interval() -> u64 { 1_000 } +// Every field is manifest surface for the not-yet-landed Phase-6 webview; the +// struct is deserialized and round-tripped in tests, just never read from prod code. +#[allow(dead_code)] #[derive(Debug, Clone, Deserialize, Default)] pub struct WebviewCfg { #[serde(default)] @@ -104,6 +113,8 @@ impl Manifest { serde_json::from_str(s) } /// Effective window title (defaults to `name`). + // Called by the Phase-6 embedding layer when it creates the window. + #[allow(dead_code)] pub fn window_title(&self) -> String { self.webview .title diff --git a/cockpit/ui/src-tauri/src/plugins/seams.rs b/cockpit/ui/src-tauri/src/plugins/seams.rs index a88bb58..7d27fec 100644 --- a/cockpit/ui/src-tauri/src/plugins/seams.rs +++ b/cockpit/ui/src-tauri/src/plugins/seams.rs @@ -23,8 +23,14 @@ pub trait Spawner: Send + Sync { /// Spawn a long-running command (start). Returns a child id. fn spawn(&self, cmd: &str, cwd: &Path, env: &BTreeMap) -> u64; /// Has the spawned child exited? (drives crash→error) + // Only reached through `state::check_crash`, which the Phase-6 crash watcher + // will call; both impls (real + fake) already satisfy it. + #[allow(dead_code)] fn has_exited(&self, child_id: u64) -> bool; /// Force-kill a child (teardown fallback). + // Teardown currently goes through the manifest's `lifecycle.stop` command + // (see manager::stop_one); this stays as the documented force-kill fallback. + #[allow(dead_code)] fn kill(&self, child_id: u64); } diff --git a/cockpit/ui/src-tauri/src/plugins/state.rs b/cockpit/ui/src-tauri/src/plugins/state.rs index b283e33..58b2a0e 100644 --- a/cockpit/ui/src-tauri/src/plugins/state.rs +++ b/cockpit/ui/src-tauri/src/plugins/state.rs @@ -2,6 +2,9 @@ use crate::plugins::manifest::{Manifest, Probe as ProbeCfg}; use crate::plugins::seams::{Clock, EventSink, Probe, Spawner}; use std::path::Path; +// The initial state of the §4 state machine. Nothing emits it yet — the machine +// only reports transitions away from rest — but it belongs with its siblings. +#[allow(dead_code)] pub const STOPPED: &str = "stopped"; pub const BUILDING: &str = "building"; pub const STARTING: &str = "starting"; @@ -108,6 +111,9 @@ pub fn run_start_sequence( /// If the owned child has exited, emit `error` and return true. The caller is /// responsible for destroying the kept-alive webview on a true return (§4). +// Unit-tested but not yet called from production: the crash watcher that polls it +// arrives with the Phase-6 embedding layer, which owns the webview it must destroy. +#[allow(dead_code)] pub fn check_crash( plugin_id: &str, child_id: u64, @@ -364,7 +370,7 @@ mod tests { }; let sink = RecordingSink::default(); // Given an owned, healthy plugin whose child has exited, the watcher flips to error. - let flipped = check_crash(&"audience".to_string(), 7, &spawner, &sink); + let flipped = check_crash("audience", 7, &spawner, &sink); assert!(flipped); assert_eq!(sink.states.lock().unwrap().last().unwrap().1, "error"); } @@ -377,7 +383,7 @@ mod tests { exited: Mutex::new(false), }; let sink = RecordingSink::default(); - let flipped = check_crash(&"audience".to_string(), 7, &spawner, &sink); + let flipped = check_crash("audience", 7, &spawner, &sink); assert!(!flipped); assert!(sink.states.lock().unwrap().is_empty()); } From 9315a45457031df1b14d00293488eb48f3b96900 Mon Sep 17 00:00:00 2001 From: Alex Barclay Date: Thu, 13 Aug 2026 21:23:52 -0600 Subject: [PATCH 4/7] test(cockpit): give each local_projects test its own temp dir MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The tmp() helper keyed its directory on the process id alone, so all four local_projects tests shared one path while running concurrently in the same test binary. One test's remove_dir_all raced another's create_dir_all and the run failed with AlreadyExists or PermissionDenied — 5 times in 20 on Windows. Pre-existing, and invisible until now because these tests were never run by CI. The new 'cargo test (cockpit)' gate would have inherited it as a ~25% flake rate, so: an atomic counter makes each directory unique. 0 failures in 30 runs after. Test-only change, no production code touched. --- cockpit/ui/src-tauri/src/local_projects.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/cockpit/ui/src-tauri/src/local_projects.rs b/cockpit/ui/src-tauri/src/local_projects.rs index 9183663..c1e6a14 100644 --- a/cockpit/ui/src-tauri/src/local_projects.rs +++ b/cockpit/ui/src-tauri/src/local_projects.rs @@ -116,7 +116,12 @@ mod tests { use std::fs; fn tmp() -> PathBuf { - let d = std::env::temp_dir().join(format!("cc-scan-{}", std::process::id())); + // Unique per call. Keying only on the pid gave every test in this binary the + // same directory, and they run concurrently — one test's `remove_dir_all` + // raced another's `create_dir_all` and failed the run about 1 time in 4. + static NEXT: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0); + let n = NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed); + let d = std::env::temp_dir().join(format!("cc-scan-{}-{n}", std::process::id())); let _ = fs::remove_dir_all(&d); fs::create_dir_all(&d).unwrap(); d From 2d83cf1f7b92d8ffd8cab9cb5e10782c53902174 Mon Sep 17 00:00:00 2001 From: Alex Barclay Date: Thu, 13 Aug 2026 21:30:24 -0600 Subject: [PATCH 5/7] test(cockpit): make the absolute-cwd test pass off Windows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit keeps_absolute_cwd_as_is leaned on the AUDIENCE_JSON fixture's 'D:/…' cwd. That is only an absolute path on Windows; everywhere else Path::is_absolute says no, resolved_cwd joins it to the manifest dir, and the assertion fails. Production code is fine — is_absolute is doing exactly its job. Only the test baked in a platform. It now picks an absolute path the host agrees with. Surfaced by the new 'cargo test (cockpit)' gate, which is the first time this suite has ever run on Linux. --- cockpit/ui/src-tauri/src/plugins/manifest.rs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/cockpit/ui/src-tauri/src/plugins/manifest.rs b/cockpit/ui/src-tauri/src/plugins/manifest.rs index 50de9d9..59bf69e 100644 --- a/cockpit/ui/src-tauri/src/plugins/manifest.rs +++ b/cockpit/ui/src-tauri/src/plugins/manifest.rs @@ -208,9 +208,18 @@ mod tests { #[test] fn keeps_absolute_cwd_as_is() { - let m = Manifest::from_json(AUDIENCE_JSON).unwrap(); // cwd is absolute D:/... + // `Path::is_absolute` is platform-specific: the fixture's "D:/…" drive path + // is absolute only on Windows, and elsewhere gets joined to the manifest dir. + // Pick a path the host actually agrees is absolute so the intent holds on both. + let abs = if cfg!(windows) { + "D:/MajorProjects/CURRENT/audience" + } else { + "/srv/audience" + }; + let mut m = Manifest::from_json(AUDIENCE_JSON).unwrap(); + m.lifecycle.cwd = Some(abs.into()); let resolved = m.resolved_cwd(Path::new("/plugins/audience")); - assert_eq!(resolved, Path::new("D:/MajorProjects/CURRENT/audience")); + assert_eq!(resolved, Path::new(abs)); } #[test] From 46aa1675c38c017db5d56846b47818d3caa1c831 Mon Sep 17 00:00:00 2001 From: Alex Barclay Date: Thu, 13 Aug 2026 21:40:50 -0600 Subject: [PATCH 6/7] ci: gate on the frontend vitest suite MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fifth gate: 'npm test' in cockpit/ui, 91 tests across 13 files. Same shape as the svelte-check job — node only, no Rust, no system deps. Closes a half-gated pair rather than just a coverage gap. The regression pin for the plugin_launch main-thread freeze is split across two suites: a Rust test, now covered by 'cargo test (cockpit)', and cockpit/ui/src/App.appPlugin.test.ts, covered here. That file is not on main yet — it lands with the plugin-runtime work — so this job gates 91 tests today and picks up the JS half of the pin the moment it merges. Wiring it now means the pin is never half-enforced. --- .github/workflows/ci.yml | 41 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 83b4004..32dba44 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,10 +2,10 @@ name: CI # First CI for command-center (Lane C / roadmap C8). # On every push and PR: run the embargo guard, the lint/type gates (rustfmt, -# clippy, svelte-check + tsc), the Rust tests for both cargo workspaces, then -# build the frontend + fleetd sidecar and produce Tauri bundles on all three target -# OSes. Bundles are uploaded as workflow artifacts so a reviewer can smoke-test a -# real build. +# clippy, svelte-check + tsc), the Rust tests for both cargo workspaces and the +# frontend's vitest suite, then build the frontend + fleetd sidecar and produce +# Tauri bundles on all three target OSes. Bundles are uploaded as workflow +# artifacts so a reviewer can smoke-test a real build. # # TWO CARGO WORKSPACES: the root one (crates/fleet-core, crates/fleetd) and the # standalone cockpit/ui/src-tauri crate. Nothing run from the repo root reaches @@ -185,6 +185,39 @@ jobs: - name: Type-check the frontend run: npm run check + # --------------------------------------------------------------------------- + # Frontend unit tests (vitest + jsdom). Same shape as the type gate above: no + # Rust, no system deps. + # + # This is half of a pair. The regression pin for the plugin_launch main-thread + # freeze is split across two suites — a Rust test, gated by `cargo test + # (cockpit)` below, and cockpit/ui/src/App.appPlugin.test.ts, gated here. That + # file arrives with the plugin-runtime work and is not on main yet; wiring the + # job now means the JS half is covered the moment it lands, instead of the pin + # being half-enforced and quietly rotting. + # --------------------------------------------------------------------------- + test-ui: + name: vitest (cockpit/ui) + runs-on: ubuntu-latest + defaults: + run: + working-directory: cockpit/ui + steps: + - uses: actions/checkout@v4 + + - name: Install Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + cache-dependency-path: cockpit/ui/package-lock.json + + - name: Install frontend dependencies + run: npm ci + + - name: Run frontend unit tests + run: npm test + # --------------------------------------------------------------------------- # Rust workspace tests. Fast, OS-independent gate — runs once on Linux. # Does NOT run the `#[ignore]`d real-Docker ITs (see header note above). From b659974b89a2c29092ee23d809731f6298c59172 Mon Sep 17 00:00:00 2001 From: Alex Barclay Date: Thu, 13 Aug 2026 21:41:08 -0600 Subject: [PATCH 7/7] docs(cockpit): correct four dead_code allow comments that overstated MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four of the allows I added claimed the code was waiting on the Phase-6 embedding layer. Verified by grep across origin/main and origin/feat/plugin-runtime — that is false for these: state::check_crash only its own two unit tests call it Manifest::webview read only by test assertions WebviewCfg (all fields) read only by test assertions Manifest::window_title one test assertion, no production caller Spawner::has_exited reachable only via check_crash, so dead with it These are unimplemented features with no caller anywhere, not code whose caller is en route. A plugin author setting 'popups: block' today gets no blocking. The allows stay — deleting the code would amputate work in flight on #49 — but the comments now say plainly that nothing calls this on any branch, so the allow cannot quietly become the permanent resting place of two unbuilt features. PluginManager::url_for keeps its original comment: that one is genuinely waiting on a caller, verified at embedding.rs:99 on feat/plugin-runtime. Comments-only change, no code touched. --- cockpit/ui/src-tauri/src/plugins/manifest.rs | 15 ++++++++++----- cockpit/ui/src-tauri/src/plugins/seams.rs | 5 +++-- cockpit/ui/src-tauri/src/plugins/state.rs | 7 +++++-- 3 files changed, 18 insertions(+), 9 deletions(-) diff --git a/cockpit/ui/src-tauri/src/plugins/manifest.rs b/cockpit/ui/src-tauri/src/plugins/manifest.rs index 59bf69e..39c9bf0 100644 --- a/cockpit/ui/src-tauri/src/plugins/manifest.rs +++ b/cockpit/ui/src-tauri/src/plugins/manifest.rs @@ -12,8 +12,9 @@ pub struct Manifest { pub icon: String, pub url: String, pub lifecycle: Lifecycle, - // Parsed and validated here; consumed by the Phase-6 embedding layer that - // creates the webview these settings configure. + // UNWIRED FEATURE — deserialized, then read by nothing but test assertions, on + // main and on every in-flight branch alike. The manifest advertises these keys + // and the app currently ignores them. See WebviewCfg below. #[allow(dead_code)] #[serde(default)] pub webview: WebviewCfg, @@ -71,8 +72,11 @@ fn default_probe_interval() -> u64 { 1_000 } -// Every field is manifest surface for the not-yet-landed Phase-6 webview; the -// struct is deserialized and round-tripped in tests, just never read from prod code. +// UNWIRED FEATURE — `popups`, `external_links` and `title` are parsed from the +// manifest and asserted on in tests, and that is the whole of their use. No +// production code reads any of them on any current branch, so a plugin author who +// sets `popups: block` gets no blocking. The allow keeps the parsed shape around; +// it does not mean someone is about to honor it. #[allow(dead_code)] #[derive(Debug, Clone, Deserialize, Default)] pub struct WebviewCfg { @@ -113,7 +117,8 @@ impl Manifest { serde_json::from_str(s) } /// Effective window title (defaults to `name`). - // Called by the Phase-6 embedding layer when it creates the window. + // UNWIRED FEATURE — the only call is a test assertion; no production caller + // exists on any current branch. Nothing creates a window from this yet. #[allow(dead_code)] pub fn window_title(&self) -> String { self.webview diff --git a/cockpit/ui/src-tauri/src/plugins/seams.rs b/cockpit/ui/src-tauri/src/plugins/seams.rs index 7d27fec..5c8896e 100644 --- a/cockpit/ui/src-tauri/src/plugins/seams.rs +++ b/cockpit/ui/src-tauri/src/plugins/seams.rs @@ -23,8 +23,9 @@ pub trait Spawner: Send + Sync { /// Spawn a long-running command (start). Returns a child id. fn spawn(&self, cmd: &str, cwd: &Path, env: &BTreeMap) -> u64; /// Has the spawned child exited? (drives crash→error) - // Only reached through `state::check_crash`, which the Phase-6 crash watcher - // will call; both impls (real + fake) already satisfy it. + // Reachable only through `state::check_crash`, which itself has no caller on any + // current branch — so this seam is dead for the same reason, and the real impl in + // seams_impl.rs has never run outside a test. See the note on check_crash. #[allow(dead_code)] fn has_exited(&self, child_id: u64) -> bool; /// Force-kill a child (teardown fallback). diff --git a/cockpit/ui/src-tauri/src/plugins/state.rs b/cockpit/ui/src-tauri/src/plugins/state.rs index 58b2a0e..70340aa 100644 --- a/cockpit/ui/src-tauri/src/plugins/state.rs +++ b/cockpit/ui/src-tauri/src/plugins/state.rs @@ -111,8 +111,11 @@ pub fn run_start_sequence( /// If the owned child has exited, emit `error` and return true. The caller is /// responsible for destroying the kept-alive webview on a true return (§4). -// Unit-tested but not yet called from production: the crash watcher that polls it -// arrives with the Phase-6 embedding layer, which owns the webview it must destroy. +// UNWIRED FEATURE — no caller anywhere. Not on main, not on any in-flight branch; +// the two unit tests below are the only things that exercise it. The crash watcher +// described above has not been written, so nothing destroys the webview of a plugin +// whose process dies. Kept because the behavior is still wanted, but this allow is +// hiding missing work, not a caller that is about to arrive. #[allow(dead_code)] pub fn check_crash( plugin_id: &str,