test(harness): TOCTOU-safe shared port/spawn helpers across all 33 server-spawning suites#284
Conversation
…conversion (task #18 WIP)
…pers Task #18. Every integration suite spawning a real moon process carried its own free_port() that binds :0 and DROPS the listener before the server spawns. Two failure modes kept hitting CI (latest: spsc_two_db on PR #283, which cost two full macOS rerolls): 1. Port TOCTOU — between probe drop and moon's bind, another test's probe or a concurrent outbound connection's ephemeral source port takes the port; moon exits with EADDRINUSE. 2. Dead-server blind poll — harnesses polled connect() for up to 30s without checking child liveness, so a lost bind race surfaced as "server never accepted: Connection refused" half a minute later with the real error unread in the server's stderr log. Fix: new shared tests/common/mod.rs with - reserve_port(): process-wide dedup set over kernel-chosen probe ports (kills intra-process reuse); - spawn_listening(spawn): polls TCP accept WHILE watching child.try_wait(), respawning on a fresh port the moment the child dies (external steals cannot be prevented, only recovered from; 3 attempts, then a loud panic pointing at the server's stderr log). All 33 suites converted. Conversion rules held throughout: - protocol-level readiness (PING/AUTH) stays with each suite — spawn_listening only guarantees "listening"; - kill-9/SIGTERM/restart tests keep their deliberate same-port+same-dir restart legs (only the FIRST spawn of a lifecycle goes through spawn_listening): coordinator_local_leg_durability (7 legs), sharded_multi_exec_durability, sharded_multi_exec_routing, spsc_wake_floor_red, vector_db_isolation; - expected-startup-failure tests (db_maxmemory_quota CLI validation, admin_auth hard02) keep direct spawns on reserve_port() ports; - txn_kv_wiring's in-process async server routes port choice through reserve_port() and keeps its await_server_ready poll (no Child to watch); its real-subprocess crash-recovery path uses spawn_listening; - no test assertions, timeouts, or CLI flags changed. Found en route (filed separately, not fixed here): admin_auth_cors_ratelimit is cfg(feature = "console")-gated and has 13 pre-existing compile errors from ureq API drift — invisible because the console CI job is skipped on PRs (task #37). Verified: cargo test --no-run clean on default AND tokio matrices; all 33 suites green (--no-fail-fast); 10-rep stress running 14 port-hungry suites CONCURRENTLY (the CI contention pattern) green; fmt clean. Refs: task #18, follow-up to PR #283 CI rerolls author: Tin Dang <tindang.ht97@gmail.com>
Qodo reviews are paused for this user.Troubleshooting steps vary by plan Learn more → On a Teams plan? Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center? |
📝 WalkthroughWalkthroughChangesIntegration test port-flake sweep
Estimated code review effort: 3 (Moderate) | ~25 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (2)
tests/common/mod.rs (1)
29-49: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winSwitch
HANDED_OUTtoparking_lot::Mutexand drop theunwrap()
A panic while holding this process-wide lock can poison laterreserve_port()calls and cascade failures across unrelated tests.parking_lotis already available here.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/common/mod.rs` around lines 29 - 49, The process-wide lock in reserve_port should use parking_lot::Mutex to avoid poisoning after a panic. Update the Mutex import and initialize HANDED_OUT with the parking_lot type, then remove unwrap() from HANDED_OUT.lock() while preserving the existing insert and retry behavior.Source: Coding guidelines
tests/db_maxmemory_quota.rs (1)
64-91: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueConsider consolidating
spawn_moon_db_quotaandspawn_moon_db_quota_no_spill.The two functions are nearly identical, differing only in the
--disk-offload disableflag. A single function with adisable_spill: boolparameter would eliminate the duplication.♻️ Optional consolidation
fn spawn_moon_db_quota(dir: &std::path::Path, db_entries: &[&str]) -> (ServerGuard, u16) { - let (child, port) = common::spawn_listening(|port| { - let mut cmd = Command::new(find_moon_binary()); - cmd.args([ - "--port", &port.to_string(), - "--dir", &dir.to_string_lossy(), - "--shards", "1", - "--appendonly", "no", - "--maxmemory", "0", - "--maxmemory-policy", "noeviction", - "--databases", "16", - ]); - for entry in db_entries { - cmd.args(["--db-maxmemory", entry]); - } - cmd.stdout(std::fs::File::create(dir.join("moon.stdout.log")).expect("stdout log")) - .stderr(std::fs::File::create(dir.join("moon.stderr.log")).expect("stderr log")) - .spawn() - .expect("spawn moon") - }); - (ServerGuard(child), port) -} - -fn spawn_moon_db_quota_no_spill(dir: &std::path::Path, db_entries: &[&str]) -> (ServerGuard, u16) { + spawn_moon_db_quota_inner(dir, db_entries, false) +} + +fn spawn_moon_db_quota_no_spill(dir: &std::path::Path, db_entries: &[&str]) -> (ServerGuard, u16) { + spawn_moon_db_quota_inner(dir, db_entries, true) +} + +fn spawn_moon_db_quota_inner( + dir: &std::path::Path, + db_entries: &[&str], + disable_spill: bool, +) -> (ServerGuard, u16) { let (child, port) = common::spawn_listening(|port| { let mut cmd = Command::new(find_moon_binary()); cmd.args([ "--port", &port.to_string(), "--dir", &dir.to_string_lossy(), "--shards", "1", "--appendonly", "no", "--maxmemory", "0", "--maxmemory-policy", "noeviction", "--databases", "16", + ]); + if disable_spill { + cmd.args(["--disk-offload", "disable"]); + } + cmd.args([ "--disk-offload", "disable", ]); for entry in db_entries { cmd.args(["--db-maxmemory", entry]); } cmd.stdout(std::fs::File::create(dir.join("moon.stdout.log")).expect("stdout log")) .stderr(std::fs::File::create(dir.join("moon.stderr.log")).expect("stderr log")) .spawn() .expect("spawn moon") }); (ServerGuard(child), port) }Also applies to: 107-136
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/db_maxmemory_quota.rs` around lines 64 - 91, Consolidate spawn_moon_db_quota and spawn_moon_db_quota_no_spill into one helper that accepts a disable_spill: bool parameter. Keep the shared command construction in the unified function, and add --disk-offload disable only when disable_spill is true; update all callers to pass the appropriate value.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@tests/common/mod.rs`:
- Around line 84-101: Update the error path around child.try_wait and the
ACCEPT_DEADLINE assertion in spawn_listening to explicitly kill the
still-running child before panicking. Preserve the existing diagnostic messages
and normal respawn behavior for an exited child, ensuring both panic paths clean
up the process and its resources first.
In `@tests/memory_prometheus_kinds.rs`:
- Around line 62-85: Move the common::reserve_port() call into the closure
passed to common::spawn_listening, and use that per-attempt value when building
the --admin-port argument. Preserve the returned admin_port value after
spawn_listening so it represents the winning attempt’s port.
In `@tests/vector_db_isolation.rs`:
- Around line 118-119: Update spawn_moon_first to redirect the spawned server’s
stdout and stderr to log files in the test temporary directory, matching the
existing setup in vector_del_unindex.rs and wire_reachability_red.rs; remove
both Stdio::null() destinations while preserving the current log filenames and
panic-diagnostic behavior expected by spawn_listening.
---
Nitpick comments:
In `@tests/common/mod.rs`:
- Around line 29-49: The process-wide lock in reserve_port should use
parking_lot::Mutex to avoid poisoning after a panic. Update the Mutex import and
initialize HANDED_OUT with the parking_lot type, then remove unwrap() from
HANDED_OUT.lock() while preserving the existing insert and retry behavior.
In `@tests/db_maxmemory_quota.rs`:
- Around line 64-91: Consolidate spawn_moon_db_quota and
spawn_moon_db_quota_no_spill into one helper that accepts a disable_spill: bool
parameter. Keep the shared command construction in the unified function, and add
--disk-offload disable only when disable_spill is true; update all callers to
pass the appropriate value.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 13eb24b9-972c-4fc1-9424-e94b4a27fa7e
📒 Files selected for processing (35)
CHANGELOG.mdtests/admin_auth_cors_ratelimit.rstests/busy_poll_idle.rstests/client_tracking_invalidation.rstests/cmd_flush_dbsize_debug_memory.rstests/common/mod.rstests/container_growth_memory_accounting.rstests/coordinator_local_leg_durability.rstests/cross_shard_consistency_red.rstests/db_maxmemory_quota.rstests/flush_cross_shard_scatter.rstests/ft_search_yield_red.rstests/ft_yield_chunk_ab.rstests/mem_watchdog.rstests/memory_doctor_response.rstests/memory_prometheus_kinds.rstests/msetnx_cross_shard_reject.rstests/multishard_serve_smoke.rstests/oom_bypass_closure.rstests/pubsub_burst_delivery.rstests/pubsub_kv_ordering.rstests/pubsub_multi_channel_acl.rstests/resp3_hello.rstests/shard_panic_abort.rstests/sharded_multi_exec_durability.rstests/sharded_multi_exec_locality.rstests/sharded_multi_exec_routing.rstests/shardslice_live.rstests/sigterm_shutdown.rstests/spsc_two_db.rstests/spsc_wake_floor_red.rstests/txn_kv_wiring.rstests/vector_db_isolation.rstests/vector_del_unindex.rstests/wire_reachability_red.rs
| match child.try_wait() { | ||
| Ok(Some(status)) => { | ||
| // Lost the bind race (or crashed at startup): give the | ||
| // next attempt a fresh port instead of polling a corpse. | ||
| eprintln!( | ||
| "spawn_listening: child exited {status} before accepting on \ | ||
| port {port} (attempt {attempt}/{ATTEMPTS}) — respawning" | ||
| ); | ||
| break; | ||
| } | ||
| Ok(None) => {} | ||
| Err(e) => panic!("spawn_listening: try_wait failed: {e}"), | ||
| } | ||
| assert!( | ||
| start.elapsed() < ACCEPT_DEADLINE, | ||
| "spawn_listening: live child never accepted on port {port} within \ | ||
| {ACCEPT_DEADLINE:?} — check the server log in the test's --dir" | ||
| ); |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Kill the child before panicking on both panic paths.
When try_wait returns Err (line 95) or the ACCEPT_DEADLINE assert fires (lines 97-101), the child is still alive but is dropped without kill(). std::process::Child::drop does NOT terminate the process — the server becomes an orphan, holding its port and resources, which can interfere with subsequent tests in the same binary.
🐛 Proposed fix
match child.try_wait() {
Ok(Some(status)) => {
// Lost the bind race (or crashed at startup): give the
// next attempt a fresh port instead of polling a corpse.
eprintln!(
"spawn_listening: child exited {status} before accepting on \
port {port} (attempt {attempt}/{ATTEMPTS}) — respawning"
);
break;
}
Ok(None) => {}
- Err(e) => panic!("spawn_listening: try_wait failed: {e}"),
+ Err(e) => {
+ let _ = child.kill();
+ let _ = child.wait();
+ panic!("spawn_listening: try_wait failed: {e}");
+ }
}
- assert!(
- start.elapsed() < ACCEPT_DEADLINE,
- "spawn_listening: live child never accepted on port {port} within \
- {ACCEPT_DEADLINE:?} — check the server log in the test's --dir"
- );
+ if start.elapsed() >= ACCEPT_DEADLINE {
+ let _ = child.kill();
+ let _ = child.wait();
+ panic!(
+ "spawn_listening: live child never accepted on port {port} within \
+ {ACCEPT_DEADLINE:?} — check the server log in the test's --dir"
+ );
+ }
std::thread::sleep(Duration::from_millis(50));📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| match child.try_wait() { | |
| Ok(Some(status)) => { | |
| // Lost the bind race (or crashed at startup): give the | |
| // next attempt a fresh port instead of polling a corpse. | |
| eprintln!( | |
| "spawn_listening: child exited {status} before accepting on \ | |
| port {port} (attempt {attempt}/{ATTEMPTS}) — respawning" | |
| ); | |
| break; | |
| } | |
| Ok(None) => {} | |
| Err(e) => panic!("spawn_listening: try_wait failed: {e}"), | |
| } | |
| assert!( | |
| start.elapsed() < ACCEPT_DEADLINE, | |
| "spawn_listening: live child never accepted on port {port} within \ | |
| {ACCEPT_DEADLINE:?} — check the server log in the test's --dir" | |
| ); | |
| match child.try_wait() { | |
| Ok(Some(status)) => { | |
| // Lost the bind race (or crashed at startup): give the | |
| // next attempt a fresh port instead of polling a corpse. | |
| eprintln!( | |
| "spawn_listening: child exited {status} before accepting on \ | |
| port {port} (attempt {attempt}/{ATTEMPTS}) — respawning" | |
| ); | |
| break; | |
| } | |
| Ok(None) => {} | |
| Err(e) => { | |
| let _ = child.kill(); | |
| let _ = child.wait(); | |
| panic!("spawn_listening: try_wait failed: {e}"); | |
| } | |
| } | |
| if start.elapsed() >= ACCEPT_DEADLINE { | |
| let _ = child.kill(); | |
| let _ = child.wait(); | |
| panic!( | |
| "spawn_listening: live child never accepted on port {port} within \ | |
| {ACCEPT_DEADLINE:?} — check the server log in the test's --dir" | |
| ); | |
| } | |
| std::thread::sleep(Duration::from_millis(50)); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tests/common/mod.rs` around lines 84 - 101, Update the error path around
child.try_wait and the ACCEPT_DEADLINE assertion in spawn_listening to
explicitly kill the still-running child before panicking. Preserve the existing
diagnostic messages and normal respawn behavior for an exited child, ensuring
both panic paths clean up the process and its resources first.
| let admin_port = common::reserve_port(); | ||
| let tmp_dir = std::env::temp_dir().join(format!("moon-test-prom-{}", std::process::id())); | ||
| let _ = std::fs::create_dir_all(&tmp_dir); | ||
| let child = Command::new(&bin) | ||
| .args([ | ||
| "--port", | ||
| &port.to_string(), | ||
| "--shards", | ||
| "1", | ||
| "--admin-port", | ||
| &admin_port.to_string(), | ||
| "--appendonly", | ||
| "no", | ||
| "--dir", | ||
| tmp_dir.to_str().unwrap(), | ||
| "--disk-offload", | ||
| "disable", | ||
| ]) | ||
| .stdout(Stdio::null()) | ||
| .stderr(Stdio::null()) | ||
| .spawn() | ||
| .ok()?; | ||
| let (child, port) = common::spawn_listening(|port| { | ||
| Command::new(&bin) | ||
| .args([ | ||
| "--port", | ||
| &port.to_string(), | ||
| "--shards", | ||
| "1", | ||
| "--admin-port", | ||
| &admin_port.to_string(), | ||
| "--appendonly", | ||
| "no", | ||
| "--dir", | ||
| tmp_dir.to_str().unwrap(), | ||
| "--disk-offload", | ||
| "disable", | ||
| ]) | ||
| .stdout(Stdio::null()) | ||
| .stderr(Stdio::null()) | ||
| .spawn() | ||
| .expect("spawn moon") | ||
| }); |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
Admin port TOCTOU not covered by spawn_listening retry logic.
admin_port is reserved once outside the closure (line 62) and captured by reference, so all three retry attempts inside spawn_listening reuse the same admin port. If an external process steals that port between reserve_port() dropping its probe and the server binding it, every retry fails identically and spawn_listening panics — the retry logic can't help because only the main port changes.
Move reserve_port() inside the closure so each attempt gets a fresh admin port:
🛡️ Proposed fix
- let admin_port = common::reserve_port();
let tmp_dir = std::env::temp_dir().join(format!("moon-test-prom-{}", std::process::id()));
let _ = std::fs::create_dir_all(&tmp_dir);
+ let mut admin_port = 0u16;
let (child, port) = common::spawn_listening(|port| {
+ admin_port = common::reserve_port();
Command::new(&bin)
.args([
"--port",
&port.to_string(),
"--shards",
"1",
"--admin-port",
- &admin_port.to_string(),
+ &admin_port.to_string(),
"--appendonly",
"no",
"--dir",
tmp_dir.to_str().unwrap(),
"--disk-offload",
"disable",
])
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.expect("spawn moon")
});After spawn_listening returns, admin_port holds the winning attempt's value.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| let admin_port = common::reserve_port(); | |
| let tmp_dir = std::env::temp_dir().join(format!("moon-test-prom-{}", std::process::id())); | |
| let _ = std::fs::create_dir_all(&tmp_dir); | |
| let child = Command::new(&bin) | |
| .args([ | |
| "--port", | |
| &port.to_string(), | |
| "--shards", | |
| "1", | |
| "--admin-port", | |
| &admin_port.to_string(), | |
| "--appendonly", | |
| "no", | |
| "--dir", | |
| tmp_dir.to_str().unwrap(), | |
| "--disk-offload", | |
| "disable", | |
| ]) | |
| .stdout(Stdio::null()) | |
| .stderr(Stdio::null()) | |
| .spawn() | |
| .ok()?; | |
| let (child, port) = common::spawn_listening(|port| { | |
| Command::new(&bin) | |
| .args([ | |
| "--port", | |
| &port.to_string(), | |
| "--shards", | |
| "1", | |
| "--admin-port", | |
| &admin_port.to_string(), | |
| "--appendonly", | |
| "no", | |
| "--dir", | |
| tmp_dir.to_str().unwrap(), | |
| "--disk-offload", | |
| "disable", | |
| ]) | |
| .stdout(Stdio::null()) | |
| .stderr(Stdio::null()) | |
| .spawn() | |
| .expect("spawn moon") | |
| }); | |
| let tmp_dir = std::env::temp_dir().join(format!("moon-test-prom-{}", std::process::id())); | |
| let _ = std::fs::create_dir_all(&tmp_dir); | |
| let mut admin_port = 0u16; | |
| let (child, port) = common::spawn_listening(|port| { | |
| admin_port = common::reserve_port(); | |
| Command::new(&bin) | |
| .args([ | |
| "--port", | |
| &port.to_string(), | |
| "--shards", | |
| "1", | |
| "--admin-port", | |
| &admin_port.to_string(), | |
| "--appendonly", | |
| "no", | |
| "--dir", | |
| tmp_dir.to_str().unwrap(), | |
| "--disk-offload", | |
| "disable", | |
| ]) | |
| .stdout(Stdio::null()) | |
| .stderr(Stdio::null()) | |
| .spawn() | |
| .expect("spawn moon") | |
| }); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tests/memory_prometheus_kinds.rs` around lines 62 - 85, Move the
common::reserve_port() call into the closure passed to common::spawn_listening,
and use that per-attempt value when building the --admin-port argument. Preserve
the returned admin_port value after spawn_listening so it represents the winning
attempt’s port.
| .stdout(Stdio::null()) | ||
| .stderr(Stdio::null()) |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Redirect server stdout/stderr to log files instead of Stdio::null().
spawn_listening panics with messages like "check the server log in the test's --dir" and "read the server stderr log in the test's --dir", but spawn_moon_first discards both streams via Stdio::null(). When startup fails there are no logs to inspect, making the panic messages misleading. The other two files in this cohort (vector_del_unindex.rs and wire_reachability_red.rs) redirect to log files in the temp dir.
🛠️ Proposed fix
- .stdout(Stdio::null())
- .stderr(Stdio::null())
+ .stdout(std::fs::File::create(tmp_dir.join("moon.stdout.log")).expect("stdout log"))
+ .stderr(std::fs::File::create(tmp_dir.join("moon.stderr.log")).expect("stderr log"))📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| .stdout(Stdio::null()) | |
| .stderr(Stdio::null()) | |
| .stdout(std::fs::File::create(tmp_dir.join("moon.stdout.log")).expect("stdout log")) | |
| .stderr(std::fs::File::create(tmp_dir.join("moon.stderr.log")).expect("stderr log")) |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tests/vector_db_isolation.rs` around lines 118 - 119, Update spawn_moon_first
to redirect the spawned server’s stdout and stderr to log files in the test
temporary directory, matching the existing setup in vector_del_unindex.rs and
wire_reachability_red.rs; remove both Stdio::null() destinations while
preserving the current log filenames and panic-diagnostic behavior expected by
spawn_listening.
Summary
Task #18: sweep all 33 server-spawning integration suites onto a shared TOCTOU-safe port/spawn harness. This is the flake class that cost PR #283 two full macOS CI rerolls this week (
spsc_two_db: "server never accepted on port 50081: Connection refused" after 30s), and previously hittxn_kv_wiring(#16),sigterm/bgsave(#11), and others.The defect class
Every suite carried a copy-pasted
free_port()that binds:0, reads the port, and drops the listener before the server spawns:connect()for up to 30s without ever checking child liveness, so a lost bind race surfaced as "Connection refused" half a minute later, with the real error unread in the server's stderr log.The fix —
tests/common/mod.rsreserve_port()— process-wide dedup set over kernel-chosen probe ports (kills intra-process reuse).spawn_listening(spawn)— reserves a port, runs the caller's spawn closure, polls TCP accept while watchingchild.try_wait(), and respawns on a fresh port the moment a child dies (external steals can't be prevented, only recovered from; 3 attempts, then a loud panic pointing at the stderr log).Protocol-level readiness (PING/AUTH) stays with each suite —
spawn_listeningonly guarantees "listening".Conversion rules held across all 33 files
spawn_listening(coordinator_local_leg_durability×7,sharded_multi_exec_durability,sharded_multi_exec_routing,spsc_wake_floor_red,vector_db_isolation).db_maxmemory_quota,admin_authhard02) keep direct spawns onreserve_port()ports —spawn_listeningwould mask the assertion.txn_kv_wiring's in-process async server routes its port throughreserve_port()and keeps itsawait_server_readypoll (noChildto watch); its real-subprocess crash-recovery path usesspawn_listening.Verification
cargo test --no-runclean on default AND tokio matrices; fmt + clippy (--tests, both matrices) clean for every touched file.--no-fail-fast).Found en route (pre-existing, filed as task #37, not fixed here)
tests/admin_auth_cors_ratelimit.rsis#![cfg(feature = "console")]-gated and has 13 pre-existing compile errors from ureq API drift — invisible because the console-feature CI job is skipped on PRs (verified pre-existing viagit stash). Two other untouched test files (graph_restart_id_aliasing,crash_recovery_disk_offload_no_aof) carry pre-existing clippy---testsdebt that CI's Lint (which doesn't lint tests) never sees.Closes task #18.
Summary by CodeRabbit
Bug Fixes
Tests