From 7e5a673a6eadb17703a8953c52495e7b5048b3e0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 21 Oct 2026 03:05:55 +0000 Subject: [PATCH 1/5] chore(deps): update dependency bazel to v9.3.1 --- .bazelversion | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.bazelversion b/.bazelversion index b13d146a7b..3b74042132 100644 --- a/.bazelversion +++ b/.bazelversion @@ -1 +1 @@ -9.3.0 +9.3.1 From dc2f502fa49ac603b31f57500156900608d68afd Mon Sep 17 00:00:00 2001 From: Zack Jackson <25274700+ScriptedAlchemy@users.noreply.github.com> Date: Wed, 21 Oct 2026 00:54:59 -0700 Subject: [PATCH 2/5] fix(tests): repair daemon readiness and child cleanup --- .../sweep_outcomes.rs | 91 +++++++++++++++++-- .../tests/memory_suite/memory_eval_test.rs | 46 ++-------- 2 files changed, 88 insertions(+), 49 deletions(-) diff --git a/crates/tracedecay-cli/tests/host_journeys_suite/host_lifecycle_cli_acceptance/sweep_outcomes.rs b/crates/tracedecay-cli/tests/host_journeys_suite/host_lifecycle_cli_acceptance/sweep_outcomes.rs index e447a37183..54e7188487 100644 --- a/crates/tracedecay-cli/tests/host_journeys_suite/host_lifecycle_cli_acceptance/sweep_outcomes.rs +++ b/crates/tracedecay-cli/tests/host_journeys_suite/host_lifecycle_cli_acceptance/sweep_outcomes.rs @@ -3,10 +3,16 @@ use std::fs; use std::path::Path; +#[cfg(unix)] +use std::path::PathBuf; use std::process::Output; +#[cfg(unix)] +use std::process::{Child, Stdio}; use tracedecay_agent_hosts::agents::host_bundle::{HostComponentV1, HostKindV1}; #[cfg(unix)] +use tracedecay_daemon_control::{SOCKET_ENV, default_socket_path}; +#[cfg(unix)] use tracedecay_runtime_core::test_executable::write_executable_script; #[cfg(unix)] @@ -376,24 +382,61 @@ fn a_signed_out_kiro_cli_never_blocks_the_kiro_lifecycle() { /// canonical report from the sole daemon owner and counts its absence as an /// issue. #[cfg(unix)] -pub(super) struct ProfileDaemon(std::process::Child); +pub(super) struct ProfileDaemon { + child: Option, + owned_socket_directory: Option, +} #[cfg(unix)] impl ProfileDaemon { pub(super) fn start(cli: &IsolatedCli) -> Self { + // The daemon relocates sockets that exceed the platform path limit. + // Readiness must use that same authority, including on remote workers + // whose TMPDIR makes an otherwise ordinary profile path too long. + let socket = default_socket_path(&cli.profile).unwrap(); + let mut daemon = Self { + child: None, + owned_socket_directory: None, + }; + let socket_override = std::env::var_os(SOCKET_ENV).filter(|path| !path.is_empty()); + if socket_override.is_none() && socket != cli.profile.join("daemon.sock") { + let parent = socket.parent().unwrap().to_path_buf(); + // Claim only a freshly created directory. Earlier fixture work + // may already own a private parent, which this guard must retain. + match tracedecay_private_fs::create_private_directory(&parent) { + Ok(()) => daemon.owned_socket_directory = Some(parent), + Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => { + tracedecay_private_fs::validate_private_directory(&parent).unwrap(); + } + Err(error) => panic!("could not create fixture socket directory: {error}"), + } + } + let log_path = cli.home.path().join("profile-daemon.stderr"); + let stderr_log = fs::File::create(&log_path).unwrap(); let mut command = cli.command(&["daemon", "run"]); command .env("TRACEDECAY_TEST_ALLOW_INCOMPLETE_HOLDER_SCAN", "1") - .stdout(std::process::Stdio::null()) - .stderr(std::process::Stdio::null()); - let daemon = Self(command.spawn().unwrap()); - let socket = cli.profile.join("daemon.sock"); + .stdout(Stdio::null()) + .stderr(stderr_log); + daemon.child = Some(command.spawn().unwrap()); + let stderr = || { + fs::read_to_string(&log_path) + .unwrap_or_else(|error| format!("could not read daemon stderr: {error}")) + }; let deadline = std::time::Instant::now() + std::time::Duration::from_secs(30); - while std::os::unix::net::UnixStream::connect(&socket).is_err() { + while let Err(error) = std::os::unix::net::UnixStream::connect(&socket) { + if let Some(status) = daemon.child.as_mut().unwrap().try_wait().unwrap() { + panic!( + "profile daemon exited before listening at {}: {status}; stderr:\n{}", + socket.display(), + stderr() + ); + } assert!( std::time::Instant::now() < deadline, - "the profile daemon never listened at {}", - socket.display() + "the profile daemon never listened at {}: {error}; stderr:\n{}", + socket.display(), + stderr() ); std::thread::sleep(std::time::Duration::from_millis(25)); } @@ -404,11 +447,39 @@ impl ProfileDaemon { #[cfg(unix)] impl Drop for ProfileDaemon { fn drop(&mut self) { - let _ = self.0.kill(); - let _ = self.0.wait(); + if let Some(child) = self.child.as_mut() { + let _ = child.kill(); + if child.wait().is_err() { + return; + } + } + if let Some(directory) = self.owned_socket_directory.take() { + let _ = fs::remove_dir_all(directory); + } } } +#[cfg(unix)] +#[test] +fn doctor_reaches_the_canonical_daemon_socket_for_a_long_profile_path() { + let mut cli = IsolatedCli::new(); + cli.profile = cli.home.path().join("p".repeat(120)).join("profile"); + install_cline(&cli); + let socket = default_socket_path(&cli.profile).unwrap(); + assert_ne!(socket, cli.profile.join("daemon.sock")); + + { + let _daemon = ProfileDaemon::start(&cli); + assert_success("profile", "doctor", cli.run(&["doctor"])); + } + + assert!( + !socket.parent().unwrap().exists(), + "the stopped fixture must remove its owned fallback socket directory" + ); + assert!(cli.profile.join("config.toml").exists()); +} + /// Doctor classifies hosts the way `update-plugin` does: Kimi's pending /// `/plugins install` exits 75, a tracked host without its CLI is skipped as /// not installed, and the run converges to 0 once the operator installs diff --git a/crates/tracedecay/tests/memory_suite/memory_eval_test.rs b/crates/tracedecay/tests/memory_suite/memory_eval_test.rs index a94d7cf031..1abeee3f25 100644 --- a/crates/tracedecay/tests/memory_suite/memory_eval_test.rs +++ b/crates/tracedecay/tests/memory_suite/memory_eval_test.rs @@ -3,7 +3,6 @@ use std::collections::BTreeMap; use std::path::{Path, PathBuf}; use std::process::{Command, Output, Stdio}; -use std::thread::JoinHandle; use std::time::{Duration, Instant}; use serde::Deserialize; @@ -133,47 +132,16 @@ fn load_scenario(id: &str) -> Scenario { scenario } -/// Drains one child pipe on its own thread so a child blocked on a full pipe -/// cannot be mistaken for a hang (see the same harness in the CLI suite). -fn drain_pipe(mut pipe: R) -> JoinHandle> { - std::thread::spawn(move || { - let mut buf = Vec::new(); - pipe.read_to_end(&mut buf) - .unwrap_or_else(|error| panic!("failed to drain child pipe: {error}")); - buf - }) -} - fn run_with_timeout(mut command: Command, timeout: Duration) -> Output { - let mut child = command + let child = command .spawn() .unwrap_or_else(|error| panic!("failed to spawn tracedecay: {error}")); - let stdout = child.stdout.take().map(drain_pipe); - let stderr = child.stderr.take().map(drain_pipe); - let started = Instant::now(); - loop { - if let Some(status) = child - .try_wait() - .unwrap_or_else(|error| panic!("failed to poll tracedecay: {error}")) - { - let join = |handle: Option>>| { - handle.map_or_default(|handle| { - handle.join().expect("child pipe drain thread panicked") - }) - }; - return Output { - status, - stdout: join(stdout), - stderr: join(stderr), - }; - } - assert!( - started.elapsed() < timeout, - "tracedecay hung after {:?}", - started.elapsed() - ); - std::thread::sleep(Duration::from_millis(5)); - } + // Arm the canonical owner before starting either pipe reader. A thread + // admission panic or wait failure must reap the command before its + // fixture is removed. + common::TestChildProcess::new(child) + .wait_with_output(timeout) + .unwrap_or_else(|error| panic!("failed to collect tracedecay output: {error}")) } fn run_ok(fixture: &Fixture, args: &[&str]) -> Output { From 5af26193f36531ec59fa09f46219e7f161a7b9e9 Mon Sep 17 00:00:00 2001 From: Zack Jackson <25274700+ScriptedAlchemy@users.noreply.github.com> Date: Wed, 21 Oct 2026 01:01:24 -0700 Subject: [PATCH 3/5] test(memory): stop completed fixtures before the next scenario --- crates/tracedecay/tests/memory_suite/memory_eval_test.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/tracedecay/tests/memory_suite/memory_eval_test.rs b/crates/tracedecay/tests/memory_suite/memory_eval_test.rs index 1abeee3f25..9c8fa9a360 100644 --- a/crates/tracedecay/tests/memory_suite/memory_eval_test.rs +++ b/crates/tracedecay/tests/memory_suite/memory_eval_test.rs @@ -742,6 +742,9 @@ fn run_scenario(id: &str) { "[{id}] well-behaved phase failed:\n{}", format_outcomes(&outcomes) ); + // The violation uses an independent store; retire this daemon before + // starting the next one instead of retaining both until the test ends. + drop(fixture); let Some(violation) = &scenario.deterministic.violation else { return; From 4a35ee83e5e721a2d4ba7f9879afe657ff1720e9 Mon Sep 17 00:00:00 2001 From: Zack Jackson Date: Wed, 21 Oct 2026 05:24:06 -0700 Subject: [PATCH 4/5] test(sessions): hold rekey workers while checking transferred requests --- .../registry_tests.rs | 45 ++++++++++++++++--- 1 file changed, 38 insertions(+), 7 deletions(-) diff --git a/crates/tracedecay-session-runtime/src/session_temporal_refresh_scheduler/registry_tests.rs b/crates/tracedecay-session-runtime/src/session_temporal_refresh_scheduler/registry_tests.rs index ad4495ee87..7c5c23aa22 100644 --- a/crates/tracedecay-session-runtime/src/session_temporal_refresh_scheduler/registry_tests.rs +++ b/crates/tracedecay-session-runtime/src/session_temporal_refresh_scheduler/registry_tests.rs @@ -540,10 +540,11 @@ async fn a_rekey_after_a_readmission_lands_every_route_and_request() { .project_state(&a_owner) .await .expect("project entry"); - a_state.requeue_request(tracedecay_store::SessionRefreshBeginOrJoinRequestV1::new( + let parked_request = tracedecay_store::SessionRefreshBeginOrJoinRequestV1::new( tracedecay_domain::SessionId::new("session.readmitted-a").unwrap(), tracedecay_store::SessionRefreshFrontierV1::new(0, 0).unwrap(), - )); + ); + a_state.requeue_request(parked_request.clone()); { let mut rekey = std::pin::pin!(registry.rekey_project( @@ -601,18 +602,35 @@ async fn a_rekey_after_a_readmission_lands_every_route_and_request() { !a_wake.wake(), "the parked handoff stays owed to its own destination" ); + // Keep both live workers in history while inspecting transferred work. + // Otherwise either owner can consume its queue before the handoff assertion. + let readmitted_history = + mount_held_project(®istry, &harness, &a_owner, HeldHistory::default()).await; + let destination_history = + mount_held_project(®istry, &harness, &d_owner, HeldHistory::default()).await; let readmitted = registry .project_state(&a_owner) .await .expect("readmitted project entry"); - readmitted.requeue_request(tracedecay_store::SessionRefreshBeginOrJoinRequestV1::new( + let live_request = tracedecay_store::SessionRefreshBeginOrJoinRequestV1::new( tracedecay_domain::SessionId::new("session.readmitted-live").unwrap(), tracedecay_store::SessionRefreshFrontierV1::new(0, 0).unwrap(), - )); + ); + readmitted.requeue_request(live_request.clone()); - registry - .rekey_project(&a_owner, d_owner.clone(), harness.registered.clone()) - .await; + { + let mut rekey = std::pin::pin!(registry.rekey_project( + &a_owner, + d_owner.clone(), + harness.registered.clone(), + )); + assert!( + rekey.as_mut().now_or_never().is_none(), + "the readmitted worker must be cancelled before releasing its history pass" + ); + readmitted_history.release.notify_one(); + rekey.await; + } assert!(registry.project_state(&a_owner).await.is_none()); let landed = registry @@ -624,6 +642,18 @@ async fn a_rekey_after_a_readmission_lands_every_route_and_request() { 2, "the parked request and the readmitted entry's request must both reach the final owner" ); + let transferred = landed.take_requests(2); + for expected in [&parked_request, &live_request] { + assert!( + transferred + .iter() + .any(|request| request.is_equivalent_to(expected)), + "the handoff must retain both exact requests and frontiers" + ); + } + for request in transferred { + landed.requeue_request(request); + } assert!( a_wake.wake(), "the parked route must reach the new owner once the rekey completes" @@ -636,6 +666,7 @@ async fn a_rekey_after_a_readmission_lands_every_route_and_request() { // The destination retains the complete route set it absorbed: a later // D-to-E rekey must still land both handles, or older callers die at // the next move. + destination_history.release.notify_one(); registry .rekey_project(&d_owner, e_owner.clone(), harness.registered.clone()) .await; From 2bd938a73a658ce213a81e67f8826ff0c11272a7 Mon Sep 17 00:00:00 2001 From: Zack Jackson <25274700+ScriptedAlchemy@users.noreply.github.com> Date: Thu, 22 Oct 2026 03:07:03 -0700 Subject: [PATCH 5/5] test(cli): isolate long-profile daemon journey --- .../sweep_outcomes.rs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/crates/tracedecay-cli/tests/host_journeys_suite/host_lifecycle_cli_acceptance/sweep_outcomes.rs b/crates/tracedecay-cli/tests/host_journeys_suite/host_lifecycle_cli_acceptance/sweep_outcomes.rs index cdab30a00b..0f5df1e761 100644 --- a/crates/tracedecay-cli/tests/host_journeys_suite/host_lifecycle_cli_acceptance/sweep_outcomes.rs +++ b/crates/tracedecay-cli/tests/host_journeys_suite/host_lifecycle_cli_acceptance/sweep_outcomes.rs @@ -7,7 +7,7 @@ use std::path::Path; use std::path::PathBuf; use std::process::Output; #[cfg(unix)] -use std::process::{Child, Stdio}; +use std::process::{Child, Command, Stdio}; use tracedecay_agent_hosts::agents::host_bundle::{HostComponentV1, HostKindV1}; #[cfg(unix)] @@ -476,6 +476,19 @@ impl Drop for ProfileDaemon { #[cfg(unix)] #[test] fn doctor_reaches_the_canonical_daemon_socket_for_a_long_profile_path() { + if std::env::var_os(SOCKET_ENV).is_some() { + let status = Command::new(std::env::current_exe().unwrap()) + .args([ + "--exact", + "host_lifecycle_cli_acceptance::sweep_outcomes::doctor_reaches_the_canonical_daemon_socket_for_a_long_profile_path", + ]) + .env_remove(SOCKET_ENV) + .status() + .unwrap(); + assert!(status.success(), "the isolated Doctor journey failed"); + return; + } + let mut cli = IsolatedCli::new(); cli.profile = cli.home.path().join("p".repeat(120)).join("profile"); install_cline(&cli);