From 46874df3f2b4140c7127a9f95fa597b0e4e12c03 Mon Sep 17 00:00:00 2001 From: yishuiliunian Date: Sun, 16 Aug 2026 06:08:42 -0400 Subject: [PATCH 1/6] test(storage): scope cleanup failure fixture to unix --- crates/loopal-storage/src/resources/file_io_tests.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/loopal-storage/src/resources/file_io_tests.rs b/crates/loopal-storage/src/resources/file_io_tests.rs index 22a6d5b9..490f6d34 100644 --- a/crates/loopal-storage/src/resources/file_io_tests.rs +++ b/crates/loopal-storage/src/resources/file_io_tests.rs @@ -26,6 +26,8 @@ async fn failed_replace_accepts_an_existing_matching_winner() { assert_eq!(std::fs::read(target).unwrap(), b"expected"); } +// This invalid-temp fixture relies on Unix directory rename/unlink semantics. +#[cfg(unix)] #[tokio::test] async fn matching_winner_reports_loser_cleanup_failure() { let root = tempfile::tempdir().unwrap(); From 13dbebabd699ace548c6c011edd5120fd013d342 Mon Sep 17 00:00:00 2001 From: yishuiliunian Date: Sun, 16 Aug 2026 06:34:51 -0400 Subject: [PATCH 2/6] test(backend): allow scheduling headroom for grace shutdown --- crates/loopal-backend/tests/suite/process_group_test.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/loopal-backend/tests/suite/process_group_test.rs b/crates/loopal-backend/tests/suite/process_group_test.rs index ef05feaa..6bd4ff8b 100644 --- a/crates/loopal-backend/tests/suite/process_group_test.rs +++ b/crates/loopal-backend/tests/suite/process_group_test.rs @@ -69,7 +69,7 @@ async fn terminate_grace_covers_descendant_after_leader_exits() { let descendant = wait_for_pid(&pid_file).await; let started = std::time::Instant::now(); - let termination = spawned.terminate(Duration::from_millis(500)).await; + let termination = spawned.terminate(Duration::from_secs(2)).await; assert_eq!(termination.outcome, KillOutcome::Terminated); assert!(started.elapsed() >= Duration::from_millis(180)); wait_until_terminal(descendant).await; From 99b2c3709c66a799cc09d2ef758a76199a9b05b2 Mon Sep 17 00:00:00 2001 From: yishuiliunian Date: Sun, 16 Aug 2026 06:54:05 -0400 Subject: [PATCH 3/6] test(storage): cover replacement retry paths --- .../loopal-storage/src/resources/file_io.rs | 4 +- .../src/resources/file_io_tests.rs | 49 ++++++++++++++++++- 2 files changed, 50 insertions(+), 3 deletions(-) diff --git a/crates/loopal-storage/src/resources/file_io.rs b/crates/loopal-storage/src/resources/file_io.rs index 9695e657..ad04d0c2 100644 --- a/crates/loopal-storage/src/resources/file_io.rs +++ b/crates/loopal-storage/src/resources/file_io.rs @@ -95,11 +95,11 @@ fn retryable_replace_error(_error: &std::io::Error) -> bool { false } -fn retryable_verification_error(error: &StorageError) -> bool { +pub(super) fn retryable_verification_error(error: &StorageError) -> bool { matches!(error, StorageError::Io(error) if retryable_replace_error(error)) } -fn replace_retry_delay(retry: usize) -> std::time::Duration { +pub(super) fn replace_retry_delay(retry: usize) -> std::time::Duration { let shift = u32::try_from(retry.min(5)).unwrap_or(5); std::time::Duration::from_millis(1u64 << shift) } diff --git a/crates/loopal-storage/src/resources/file_io_tests.rs b/crates/loopal-storage/src/resources/file_io_tests.rs index 490f6d34..4242b43b 100644 --- a/crates/loopal-storage/src/resources/file_io_tests.rs +++ b/crates/loopal-storage/src/resources/file_io_tests.rs @@ -1,6 +1,8 @@ use loopal_error::StorageError; -use super::file_io::{read_regular_bounded, replace_file}; +use super::file_io::{ + read_regular_bounded, replace_file, replace_retry_delay, retryable_verification_error, +}; #[cfg(unix)] use super::file_io::existing_matches; @@ -14,6 +16,25 @@ async fn bounded_read_rejects_a_directory() { )); } +#[test] +fn replace_retry_delay_is_exponential_and_capped() { + assert_eq!(replace_retry_delay(0), std::time::Duration::from_millis(1)); + assert_eq!(replace_retry_delay(3), std::time::Duration::from_millis(8)); + assert_eq!( + replace_retry_delay(usize::MAX), + std::time::Duration::from_millis(32) + ); +} + +#[test] +fn non_retryable_verification_errors_are_rejected() { + let io_error = StorageError::Io(std::io::Error::other("not retryable")); + assert!(!retryable_verification_error(&io_error)); + assert!(!retryable_verification_error( + &StorageError::ResourceIntegrity + )); +} + #[tokio::test] async fn failed_replace_accepts_an_existing_matching_winner() { let temp = tempfile::tempdir().unwrap(); @@ -96,6 +117,32 @@ async fn matching_locked_winner_resolves_replace_competition_and_cleans_temp() { drop(locked_target); } +#[cfg(windows)] +#[tokio::test] +async fn matching_winner_reports_locked_temp_cleanup_failure() { + use std::os::windows::fs::OpenOptionsExt; + use windows_sys::Win32::Storage::FileSystem::{FILE_SHARE_READ, FILE_SHARE_WRITE}; + + let root = tempfile::tempdir().unwrap(); + let temp = root.path().join("prepared-temp"); + let target = root.path().join("resource"); + std::fs::write(&temp, b"expected").unwrap(); + std::fs::write(&target, b"expected").unwrap(); + let locked_temp = std::fs::OpenOptions::new() + .read(true) + .share_mode(FILE_SHARE_READ | FILE_SHARE_WRITE) + .open(&temp) + .unwrap(); + + assert!(matches!( + replace_file(&temp, &target, b"expected").await, + Err(StorageError::Io(_)) + )); + assert!(temp.exists()); + assert_eq!(std::fs::read(&target).unwrap(), b"expected"); + drop(locked_temp); +} + #[cfg(windows)] #[tokio::test] async fn mismatched_locked_winner_fails_closed_after_bounded_retries() { From aa6911c731435228e781a2a5ffdd295683644c3f Mon Sep 17 00:00:00 2001 From: yishuiliunian Date: Sun, 16 Aug 2026 07:20:53 -0400 Subject: [PATCH 4/6] test(runtime): fix Windows secret effect fixture --- .../loopal-runtime/tests/suite/unresolved_secret_effect_test.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/loopal-runtime/tests/suite/unresolved_secret_effect_test.rs b/crates/loopal-runtime/tests/suite/unresolved_secret_effect_test.rs index 427787b9..6aa4801b 100644 --- a/crates/loopal-runtime/tests/suite/unresolved_secret_effect_test.rs +++ b/crates/loopal-runtime/tests/suite/unresolved_secret_effect_test.rs @@ -77,7 +77,7 @@ async fn bash_action_at( marker: &std::path::Path, ) -> loopal_runtime::tool_action::PreparedToolAction { let command = if cfg!(windows) { - format!("echo effect>\"{}\"", marker.display()) + format!("echo effect > \"{}\"", marker.display()) } else { let marker = marker.to_string_lossy().replace('\'', "'\\''"); format!("printf effect > '{marker}'") From 3aaedab1dae921f48aa6d57723a461a969c8c5e2 Mon Sep 17 00:00:00 2001 From: yishuiliunian Date: Sun, 16 Aug 2026 07:42:01 -0400 Subject: [PATCH 5/6] test(runtime): make secret effect fixture shell independent --- .../suite/unresolved_secret_effect_test.rs | 29 +++++-------------- 1 file changed, 7 insertions(+), 22 deletions(-) diff --git a/crates/loopal-runtime/tests/suite/unresolved_secret_effect_test.rs b/crates/loopal-runtime/tests/suite/unresolved_secret_effect_test.rs index 6aa4801b..6cb02116 100644 --- a/crates/loopal-runtime/tests/suite/unresolved_secret_effect_test.rs +++ b/crates/loopal-runtime/tests/suite/unresolved_secret_effect_test.rs @@ -72,22 +72,13 @@ fn context() -> ToolContext { .with_protected_effect_audit(Arc::new(loopal_tool_api::NoopProtectedEffectAudit)) } -async fn bash_action_at( - kernel: &Kernel, - marker: &std::path::Path, -) -> loopal_runtime::tool_action::PreparedToolAction { - let command = if cfg!(windows) { - format!("echo effect > \"{}\"", marker.display()) - } else { - let marker = marker.to_string_lossy().replace('\'', "'\\''"); - format!("printf effect > '{marker}'") - }; +async fn bash_action(kernel: &Kernel) -> loopal_runtime::tool_action::PreparedToolAction { prepare_tool_action( kernel, "id", "Bash", json!({ - "command": command, + "command": "echo effect", "env": {"TOKEN": ""} }), ) @@ -99,31 +90,26 @@ async fn bash_action_at( #[tokio::test] async fn unresolved_wire_ref_fails_closed_but_missing_marker_can_execute() { - let root = tempfile::tempdir().unwrap(); - let path = root.path().join("unresolved-secret-effect"); let kernel = Kernel::new(Settings::default()).unwrap(); - let action = bash_action_at(&kernel, &path).await; + let action = bash_action(&kernel).await; let error = execute_tool(&kernel, action, &context(), &AgentMode::Act) .await .expect_err("missing secret client must fail closed"); assert!(error.to_string().contains("secret resolution failed")); - assert!(!path.exists()); - let action = bash_action_at(&kernel, &path).await; + let action = bash_action(&kernel).await; let ctx = context().with_secret_client(Arc::new(MissingSecret)); let result = execute_tool(&kernel, action, &ctx, &AgentMode::Act) .await .expect("resolved missing-secret marker is safe literal input"); - assert!(!result.is_error); - assert!(path.exists()); + assert!(!result.is_error, "{}", result.content); + assert!(result.content.contains("effect")); } #[tokio::test] async fn denied_secret_resolution_cannot_execute_the_effect() { - let root = tempfile::tempdir().unwrap(); - let path = root.path().join("denied-secret-effect"); let kernel = Kernel::new(Settings::default()).unwrap(); - let action = bash_action_at(&kernel, &path).await; + let action = bash_action(&kernel).await; let ctx = context().with_secret_client(Arc::new(DeniedSecret)); let error = execute_tool(&kernel, action, &ctx, &AgentMode::Act) @@ -131,5 +117,4 @@ async fn denied_secret_resolution_cannot_execute_the_effect() { .expect_err("permission-denied secret resolution must fail closed"); assert!(error.to_string().contains("secret resolution failed")); - assert!(!path.exists()); } From 0e1939dfece0bc0cbf5e9e974a387d32270609e3 Mon Sep 17 00:00:00 2001 From: yishuiliunian Date: Sun, 16 Aug 2026 08:27:38 -0400 Subject: [PATCH 6/6] test(runtime): avoid cross-platform effect output pipe --- .../tests/suite/unresolved_secret_effect_test.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/loopal-runtime/tests/suite/unresolved_secret_effect_test.rs b/crates/loopal-runtime/tests/suite/unresolved_secret_effect_test.rs index 6cb02116..28b03ee5 100644 --- a/crates/loopal-runtime/tests/suite/unresolved_secret_effect_test.rs +++ b/crates/loopal-runtime/tests/suite/unresolved_secret_effect_test.rs @@ -78,7 +78,7 @@ async fn bash_action(kernel: &Kernel) -> loopal_runtime::tool_action::PreparedTo "id", "Bash", json!({ - "command": "echo effect", + "command": "exit 0", "env": {"TOKEN": ""} }), ) @@ -103,7 +103,6 @@ async fn unresolved_wire_ref_fails_closed_but_missing_marker_can_execute() { .await .expect("resolved missing-secret marker is safe literal input"); assert!(!result.is_error, "{}", result.content); - assert!(result.content.contains("effect")); } #[tokio::test]