use super::*;
use super::artifact_patterns::{
expected_output_glob_list, get_artifact_patterns, get_custom_target_artifact_patterns,
get_project_artifact_patterns, kind_produces_transferable_artifacts,
sync_back_verified_zero_build_outputs, sync_back_verified_zero_package_archives,
};
use super::cargo_target_dir::{
extract_cargo_target_dir_from_command_tokens, feature_set_for_command,
parse_stale_target_reap_idle_hours, remote_cargo_pooled_target_dir_name,
remote_cargo_target_dir_name, resolve_forwarded_cargo_target_dir_with_lookup,
strip_cargo_target_dir_assignments_from_command_tokens,
strip_cargo_target_dir_flags_from_command_tokens, target_reuse_disabled_from_value,
target_triple_for_command,
};
use super::command_parsing::{
cargo_custom_profile_output_dir, has_exact_flag, has_ignored_only_flag,
is_filtered_test_command, parse_jobs_flag, parse_test_threads,
};
use super::daemon_ipc::{
DEFAULT_DAEMON_RESPONSE_TIMEOUT_SECS, DEFAULT_DAEMON_WAIT_RESPONSE_TIMEOUT_SECS,
daemon_response_timeout_for, queue_when_busy_enabled_from, urlencoding_encode,
};
use super::dependency_closure::{
DEPENDENCY_PREFLIGHT_CODE_MISSING, DEPENDENCY_PREFLIGHT_CODE_STALE,
DEPENDENCY_PREFLIGHT_REMEDIATION_MISSING, DEPENDENCY_PREFLIGHT_REMEDIATION_STALE,
DependencyPreflightCheck, SyncClosureMode, SyncClosurePlanEntry, SyncRootOutcome,
build_dependency_preflight_report, build_remote_dependency_preflight_command,
build_sync_closure_manifest, build_sync_closure_plan, canonicalize_sync_root_for_plan,
cargo_package_source_entrypoints, cargo_workspace_member_source_entrypoints,
dependency_preflight_checks_for_entry, dependency_preflight_timeout, is_within_sync_topology,
parse_dependency_preflight_probe_output, synced_dependency_preflight_checks,
verify_remote_dependency_manifests,
};
use super::fallback_selection::WorkerCapacitySnapshot;
use super::progress_reporting::BuildHeartbeatLoop;
use super::repo_updater::{
auto_tune_repo_updater_contract, build_repo_sync_idempotency_key_for_command,
collect_repo_updater_roots_and_specs, hydrate_repo_updater_auth_context_defaults,
infer_repo_updater_auth_context_with_env_lookup, repo_updater_command_name,
};
use super::ssh::offload_remote_command_transport;
use super::timing_history::{
MAX_TIMING_SAMPLES, ProjectTimingData, TimingEstimate, TimingHistory, TimingRecord,
estimate_timing_for_build, record_build_timing, timing_cache,
};
use super::transfer_orchestration::{
apply_source_sync_integrity_policy, source_sync_terminal_summary, wrap_command_with_telemetry,
};
use proptest::prelude::*;
use rch_common::mock::{
self, MockConfig, MockRsyncConfig, clear_mock_overrides, set_mock_enabled_override,
set_mock_rsync_config_override, set_mock_ssh_config_override,
};
use rch_common::test_guard;
use rch_common::{SelectionReason, TierDecision, ToolInput, classify_command_detailed};
use serial_test::serial;
use std::sync::OnceLock;
use tokio::io::BufReader as TokioBufReader;
use tokio::net::UnixListener;
use tokio::sync::Mutex;
#[tokio::test]
async fn queued_cancellation_replaces_hook_command_with_terminal_exit() {
let reporter = HookReporter::new(OutputVisibility::None);
let response = SelectionResponse {
worker: None,
reason: SelectionReason::SelectionError("job_cancelled_before_start".into()),
build_id: None,
diagnostics: None,
};
assert!(selection_cancelled_before_start(&response));
let output = handle_selection_response(
response,
"cargo build",
&rch_common::RchConfig::default(),
&reporter,
None,
Some(CompilationKind::CargoBuild),
"queued",
2,
)
.await;
assert_eq!(delegated_command(&output), "exit 130");
let busy = SelectionResponse {
worker: None,
reason: SelectionReason::AllWorkersBusy,
build_id: None,
diagnostics: None,
};
assert!(!selection_cancelled_before_start(&busy));
}
fn delegated_command(output: &HookOutput) -> &str {
if let HookOutput::AllowWithModifiedCommand(modified) = output {
&modified.hook_specific_output.updated_input.command
} else {
assert!(
matches!(output, HookOutput::AllowWithModifiedCommand(_)),
"expected AllowWithModifiedCommand"
);
""
}
}
fn init_clean_overlay_test_repo() -> tempfile::TempDir {
let temp = tempfile::tempdir().expect("create clean-overlay test repo");
let root = temp.path();
for args in [
vec!["init", "-q"],
vec!["config", "user.email", "rch-test@example.invalid"],
vec!["config", "user.name", "RCH Test"],
] {
let status = std::process::Command::new("git")
.current_dir(root)
.args(args)
.status()
.expect("run git setup command");
assert!(status.success());
}
std::fs::create_dir_all(root.join("src")).expect("create src");
std::fs::write(
root.join("Cargo.toml"),
"[package]\nname='overlay-fixture'\nversion='0.1.0'\n",
)
.expect("write manifest");
std::fs::write(root.join("src/lib.rs"), "pub fn baseline() {}\n").expect("write source");
let add = std::process::Command::new("git")
.current_dir(root)
.args(["add", "Cargo.toml", "src/lib.rs"])
.status()
.expect("git add fixture");
assert!(add.success());
let commit = std::process::Command::new("git")
.current_dir(root)
.args(["commit", "-q", "--no-gpg-sign", "-m", "fixture"])
.status()
.expect("git commit fixture");
assert!(commit.success());
temp
}
#[tokio::test]
async fn clean_overlay_cargo_preflight_reads_selected_base_not_ambient_manifest() {
let _guard = test_guard!();
let root = init_clean_overlay_test_repo().keep();
let good = git_output(&root, &["rev-parse", "HEAD"]).await.unwrap();
let bad = "[package]\nname='overlay-fixture'\nversion='0.1.0'\n[dependencies]\nstale={path='../stale-sibling'}\n";
std::fs::write(root.join("Cargo.toml"), bad).unwrap();
git_output(&root, &["add", "Cargo.toml"]).await.unwrap();
git_output(
&root,
&["commit", "-q", "--no-gpg-sign", "-m", "external dependency"],
)
.await
.unwrap();
std::fs::write(
root.join("Cargo.toml"),
"[package]\nname='ambient'\nversion='0.1.0'\n",
)
.unwrap();
let bad_spec = prepare_clean_overlay_spec(&root, Some("HEAD".into()), true, vec![], true)
.await
.unwrap()
.unwrap();
let error = validate_clean_overlay_cargo_sources(&root, &bad_spec, "cargo test")
.await
.unwrap_err();
assert!(format!("{error:#}").contains("stale-sibling"));
let good_spec = prepare_clean_overlay_spec(&root, Some(good), true, vec![], true)
.await
.unwrap()
.unwrap();
validate_clean_overlay_cargo_sources(&root, &good_spec, "cargo test")
.await
.unwrap();
}
async fn retained_clean_overlay_sibling_fixture() -> (PathBuf, PathBuf, PathBuf) {
let parent = tempfile::tempdir().unwrap().keep();
let app = parent.join("app");
let dep = parent.join("dep");
for (root, manifest) in [
(
&app,
"[package]\nname='app'\nversion='0.1.0'\n[workspace]\n[dependencies]\ndep={path='../dep'}\n",
),
(
&dep,
"[package]\nname='dep'\nversion='0.1.0'\n[workspace]\n",
),
] {
std::fs::create_dir_all(root.join("src")).unwrap();
std::fs::write(root.join("Cargo.toml"), manifest).unwrap();
std::fs::write(root.join("src/lib.rs"), "pub fn value() -> u32 { 1 }\n").unwrap();
git_output(root, &["init", "-q", "-b", "main"])
.await
.unwrap();
git_output(root, &["config", "user.email", "rch-test@example.invalid"])
.await
.unwrap();
git_output(root, &["config", "user.name", "RCH Test"])
.await
.unwrap();
git_output(root, &["config", "core.hooksPath", "/dev/null"])
.await
.unwrap();
git_output(root, &["add", "Cargo.toml", "src/lib.rs"])
.await
.unwrap();
git_output(
root,
&[
"commit",
"-q",
"--no-gpg-sign",
"-m",
"selected sibling fixture",
],
)
.await
.unwrap();
}
(parent, app, dep)
}
#[tokio::test]
async fn clean_overlay_dependency_binding_captures_committed_tree_and_ignores_peer_dirt() {
let _guard = test_guard!();
let (_retained_parent, app, dep) = retained_clean_overlay_sibling_fixture().await;
let commit = git_output(&dep, &["rev-parse", "HEAD"]).await.unwrap();
let tree = git_output(&dep, &["rev-parse", "HEAD^{tree}"])
.await
.unwrap();
std::fs::write(
dep.join("Cargo.toml"),
"[dependencies]\nambient={path='../unselected'}\n",
)
.unwrap();
std::fs::write(dep.join("src/lib.rs"), "pub fn value() -> u32 { 2 }\n").unwrap();
let mut spec = prepare_clean_overlay_spec(&app, Some("HEAD".into()), true, vec![], true)
.await
.unwrap()
.unwrap();
assert!(
validate_clean_overlay_cargo_sources(&app, &spec, "cargo test")
.await
.is_err()
);
bind_clean_overlay_dependencies(&app, &mut spec, &["../dep=HEAD".into()])
.await
.unwrap();
assert_eq!(spec.primary_directory.as_deref(), Some(Path::new("app")));
assert_eq!(spec.dependencies.len(), 1);
assert_eq!(spec.dependencies[0].1.base_commit, commit);
assert_eq!(spec.dependencies[0].1.tree_object, tree);
validate_clean_overlay_cargo_sources(&app, &spec, "cargo test")
.await
.unwrap();
git_output(&dep, &["add", "Cargo.toml", "src/lib.rs"])
.await
.unwrap();
git_output(
&dep,
&[
"commit",
"-q",
"--no-gpg-sign",
"-m",
"different later HEAD",
],
)
.await
.unwrap();
assert_ne!(
git_output(&dep, &["rev-parse", "HEAD"]).await.unwrap(),
commit
);
validate_clean_overlay_cargo_sources(&app, &spec, "cargo test")
.await
.unwrap();
let selected = selected_clean_overlay_cargo_tree(&dep, &spec.dependencies[0].1)
.await
.unwrap();
assert!(
matches!(selected.get(Path::new("Cargo.toml")), Some(rch_common::cargo_path_deps::SelectedCargoEntry::File(Some(text))) if !text.contains("unselected"))
);
let receipt = spec.execution_receipt();
assert!(
receipt.contains(&format!("commit={commit} tree={tree}")),
"{receipt}"
);
assert!(
receipt.contains(&format!(
"dependency-root={}",
dep.canonicalize().unwrap().display()
)),
"{receipt}"
);
}
#[tokio::test]
async fn clean_overlay_dependency_bindings_refuse_duplicate_non_sibling_and_missing_revisions() {
let _guard = test_guard!();
let (parent, app, _dep) = retained_clean_overlay_sibling_fixture().await;
std::fs::create_dir_all(app.join("nested")).unwrap();
std::fs::create_dir_all(parent.join("other/deep")).unwrap();
for bindings in [
vec!["../dep=HEAD", "../dep=HEAD"],
vec![".=HEAD"],
vec!["nested=HEAD"],
vec!["../other/deep=HEAD"],
vec!["../dep=missing-ref"],
vec!["../dep"],
vec!["../dep="],
] {
let mut spec = prepare_clean_overlay_spec(&app, Some("HEAD".into()), true, vec![], true)
.await
.unwrap()
.unwrap();
let arguments = bindings
.iter()
.map(|value| (*value).to_owned())
.collect::<Vec<_>>();
assert!(
bind_clean_overlay_dependencies(&app, &mut spec, &arguments)
.await
.is_err(),
"{bindings:?}"
);
}
}
#[tokio::test]
async fn clean_overlay_dependency_binding_still_rejects_unbound_transitive_root() {
let _guard = test_guard!();
let (_retained_parent, app, dep) = retained_clean_overlay_sibling_fixture().await;
std::fs::write(dep.join("Cargo.toml"), "[package]\nname='dep'\nversion='0.1.0'\n[workspace]\n[dependencies]\nunbound={path='../third'}\n").unwrap();
git_output(&dep, &["add", "Cargo.toml"]).await.unwrap();
git_output(
&dep,
&[
"commit",
"-q",
"--no-gpg-sign",
"-m",
"unbound transitive sibling",
],
)
.await
.unwrap();
let mut spec = prepare_clean_overlay_spec(&app, Some("HEAD".into()), true, vec![], true)
.await
.unwrap()
.unwrap();
bind_clean_overlay_dependencies(&app, &mut spec, &["../dep=HEAD".into()])
.await
.unwrap();
let error = validate_clean_overlay_cargo_sources(&app, &spec, "cargo test")
.await
.unwrap_err();
assert!(format!("{error:#}").contains("third"), "{error:#}");
}
#[tokio::test]
async fn clean_overlay_cargo_preflight_honors_selected_manifest_overlay() {
let _guard = test_guard!();
let root = init_clean_overlay_test_repo().keep();
std::fs::write(root.join("Cargo.toml"), "[package]\nname='overlay-fixture'\nversion='0.1.0'\n[dependencies]\nstale={path='../stale-sibling'}\n").unwrap();
let base = prepare_clean_overlay_spec(&root, Some("HEAD".into()), true, vec![], true)
.await
.unwrap()
.unwrap();
validate_clean_overlay_cargo_sources(&root, &base, "cargo check")
.await
.unwrap();
let overlay = prepare_clean_overlay_spec(
&root,
Some("HEAD".into()),
true,
vec![PathBuf::from("Cargo.toml")],
false,
)
.await
.unwrap()
.unwrap();
let error = validate_clean_overlay_cargo_sources(&root, &overlay, "cargo check")
.await
.unwrap_err();
assert!(format!("{error:#}").contains("stale-sibling"));
}
#[tokio::test]
async fn clean_overlay_cargo_preflight_checks_cli_paths_and_inline_config() {
let _guard = test_guard!();
let root = init_clean_overlay_test_repo().keep();
std::fs::create_dir(root.join(".cargo")).unwrap();
std::fs::write(
root.join(".cargo/config.toml"),
"[package]\nname='wrong-kind'\n[dependencies]\nstale={path='../outside'}\n",
)
.unwrap();
let spec = prepare_clean_overlay_spec(
&root,
Some("HEAD".into()),
true,
vec![PathBuf::from(".cargo/config.toml")],
false,
)
.await
.unwrap()
.unwrap();
for command in [
"cargo test --manifest-path Cargo.toml",
"cargo test --config 'build.build-dir=\"/managed later\"'",
"cargo test -- --manifest-path ../ignored-test-argument",
] {
validate_clean_overlay_cargo_sources(&root, &spec, command)
.await
.unwrap();
}
for command in [
"cargo test --manifest-path ../outside/Cargo.toml",
"cargo test --manifest-path .cargo/config.toml",
"cargo test --config 'paths=[\"../outside\"]'",
"cargo test --config ../outside/config.toml",
"cargo -C ../outside test",
"env --chdir=../outside cargo test",
] {
assert!(
validate_clean_overlay_cargo_sources(&root, &spec, command)
.await
.is_err(),
"{command}"
);
}
}
#[tokio::test]
async fn clean_overlay_spec_accepts_base_only_and_resolves_commit() {
let _guard = test_guard!();
let repo = init_clean_overlay_test_repo();
let spec =
prepare_clean_overlay_spec(repo.path(), Some("HEAD".to_string()), true, vec![], true)
.await
.expect("prepare base-only clean overlay")
.expect("clean-overlay spec");
assert!(spec.is_base_only());
assert!(matches!(spec.base_commit().len(), 40 | 64));
assert!(
spec.base_commit()
.chars()
.all(|character| character.is_ascii_hexdigit())
);
}
#[tokio::test]
async fn clean_overlay_execution_receipt_names_the_resolved_base_and_content_fingerprint() {
let _guard = test_guard!();
let repo = init_clean_overlay_test_repo();
let first = prepare_clean_overlay_spec(
repo.path(),
Some("HEAD".to_string()),
true,
vec![PathBuf::from("src/lib.rs")],
false,
)
.await
.expect("prepare first clean-overlay spec")
.expect("first clean-overlay spec");
let first_receipt = first.execution_receipt();
assert!(first_receipt.contains(&format!("base={}", first.base_commit())));
assert!(first_receipt.contains(&format!(
"overlay-fingerprint={}",
first.overlay_fingerprint()
)));
std::fs::write(repo.path().join("src/lib.rs"), "pub fn changed() {}\n")
.expect("change selected overlay");
let second = prepare_clean_overlay_spec(
repo.path(),
Some("HEAD".to_string()),
true,
vec![PathBuf::from("src/lib.rs")],
false,
)
.await
.expect("prepare second clean-overlay spec")
.expect("second clean-overlay spec");
assert_ne!(first.execution_receipt(), second.execution_receipt());
}
#[test]
fn clean_overlay_selection_identity_is_source_bound_and_per_job() {
let _guard = test_guard!();
let spec = CleanOverlaySpec {
base_commit: "0123456789abcdef0123456789abcdef01234567".to_string(),
tree_object: "0123456789abcdef0123456789abcdef01234567".to_string(),
overlay_paths: vec![PathBuf::from("src/lib.rs")],
overlay_fingerprint: "first-overlay-fingerprint".to_string(),
dependencies: Vec::new(),
primary_directory: None,
};
let plain = selection_project_for_execution("fixture-project", None, uuid::Uuid::from_u128(1));
assert_eq!(plain, "fixture-project");
let first =
selection_project_for_execution("fixture-project", Some(&spec), uuid::Uuid::from_u128(2));
let second_job =
selection_project_for_execution("fixture-project", Some(&spec), uuid::Uuid::from_u128(3));
let different_base = CleanOverlaySpec {
base_commit: "fedcba9876543210fedcba9876543210fedcba98".to_string(),
..spec.clone()
};
let different_overlay = CleanOverlaySpec {
overlay_fingerprint: "second-overlay-fingerprint".to_string(),
..spec.clone()
};
assert_ne!(
first, second_job,
"each clean-overlay job needs a new identity"
);
assert_ne!(
first,
selection_project_for_execution(
"fixture-project",
Some(&different_base),
uuid::Uuid::from_u128(2),
),
"the immutable base must bind the selection identity"
);
assert_ne!(
first,
selection_project_for_execution(
"fixture-project",
Some(&different_overlay),
uuid::Uuid::from_u128(2),
),
"the dirty overlay fingerprint must bind the selection identity"
);
}
#[tokio::test]
async fn clean_overlay_spec_requires_exactly_one_overlay_scope() {
let _guard = test_guard!();
let repo = init_clean_overlay_test_repo();
let missing =
prepare_clean_overlay_spec(repo.path(), Some("HEAD".to_string()), true, vec![], false)
.await
.expect_err("missing overlay scope must fail");
assert!(missing.to_string().contains("exactly one"));
let conflicting = prepare_clean_overlay_spec(
repo.path(),
Some("HEAD".to_string()),
true,
vec![PathBuf::from("src/lib.rs")],
true,
)
.await
.expect_err("conflicting overlay scope must fail");
assert!(conflicting.to_string().contains("exactly one"));
}
#[tokio::test]
async fn clean_overlay_spec_rejects_traversal_and_detects_source_churn() {
let _guard = test_guard!();
assert!(normalize_clean_overlay_path(Path::new("../peer.rs")).is_err());
assert!(normalize_clean_overlay_path(Path::new("/absolute.rs")).is_err());
assert!(normalize_clean_overlay_path(Path::new(r"src\backslash.rs")).is_err());
let non_ascii = normalize_clean_overlay_path(Path::new("src/caf\u{e9}.rs"))
.expect_err("non-ASCII overlay paths must fail closed");
assert!(non_ascii.to_string().contains("only ASCII"));
let repo = init_clean_overlay_test_repo();
std::fs::write(repo.path().join("src/lib.rs"), "pub fn overlay() {}\n")
.expect("write overlay source");
let spec = prepare_clean_overlay_spec(
repo.path(),
Some("HEAD".to_string()),
true,
vec![PathBuf::from("src/lib.rs")],
false,
)
.await
.expect("prepare clean overlay")
.expect("clean-overlay spec");
spec.verify_overlay_unchanged(repo.path())
.expect("unchanged overlay");
std::fs::write(
repo.path().join("src/lib.rs"),
"pub fn changed_again() {}\n",
)
.expect("mutate overlay source");
let error = spec
.verify_overlay_unchanged(repo.path())
.expect_err("source churn must fail closed");
assert!(error.to_string().contains("changed after admission"));
}
#[tokio::test]
async fn clean_overlay_post_admission_spelling_drift_fails_closed() {
let _guard = test_guard!();
let repo = init_clean_overlay_test_repo();
let spec = prepare_clean_overlay_spec(
repo.path(),
Some("HEAD".to_string()),
true,
vec![PathBuf::from("src/lib.rs")],
false,
)
.await
.expect("prepare clean overlay")
.expect("clean-overlay spec");
std::fs::rename(
repo.path().join("src/lib.rs"),
repo.path().join("src/LIB.rs"),
)
.expect("change selected path spelling");
let error = spec
.verify_overlay_unchanged(repo.path())
.expect_err("post-admission spelling drift must fail closed");
assert!(
error
.to_string()
.contains("spelling does not exactly match")
);
}
#[tokio::test]
async fn clean_overlay_directory_rejects_nested_non_ascii_path() {
let _guard = test_guard!();
let repo = init_clean_overlay_test_repo();
std::fs::write(repo.path().join("src/caf\u{e9}.rs"), "pub fn coffee() {}\n")
.expect("write non-ASCII nested path");
let error = prepare_clean_overlay_spec(
repo.path(),
Some("HEAD".to_string()),
true,
vec![PathBuf::from("src")],
false,
)
.await
.expect_err("nested non-ASCII overlay paths must fail closed");
assert!(error.to_string().contains("only ASCII"));
}
#[tokio::test]
async fn clean_overlay_directory_rejects_nested_backslash_or_control_path() {
let _guard = test_guard!();
for relative in ["src/back\\slash.rs", "src/line\nbreak.rs"] {
let repo = init_clean_overlay_test_repo();
std::fs::write(repo.path().join(relative), "pub fn hazard() {}\n")
.expect("write nested portability hazard");
let error = prepare_clean_overlay_spec(
repo.path(),
Some("HEAD".to_string()),
true,
vec![PathBuf::from("src")],
false,
)
.await
.expect_err("nested backslash/control overlay paths must fail closed");
assert!(
error
.to_string()
.contains("backslashes or control characters")
);
}
}
#[cfg(unix)]
#[tokio::test]
async fn clean_overlay_spec_rejects_nested_symlink() {
let _guard = test_guard!();
let repo = init_clean_overlay_test_repo();
std::os::unix::fs::symlink("/dev/null", repo.path().join("src/escape"))
.expect("create escaping fixture symlink");
let error = prepare_clean_overlay_spec(
repo.path(),
Some("HEAD".to_string()),
true,
vec![PathBuf::from("src")],
false,
)
.await
.expect_err("nested symlink escape must fail closed");
assert!(error.to_string().contains("do not support symlinks"));
}
#[tokio::test]
async fn clean_overlay_spec_rejects_nested_git_metadata() {
let _guard = test_guard!();
let repo = init_clean_overlay_test_repo();
std::fs::create_dir_all(repo.path().join("src/vendor/.GIT"))
.expect("create nested Git metadata fixture");
let error = prepare_clean_overlay_spec(
repo.path(),
Some("HEAD".to_string()),
true,
vec![PathBuf::from("src")],
false,
)
.await
.expect_err("nested Git metadata must fail closed");
assert!(error.to_string().contains("Git metadata"));
}
#[test]
fn clean_overlay_exact_spelling_check_rejects_case_drift() {
let _guard = test_guard!();
let repo = init_clean_overlay_test_repo();
assert!(
!clean_overlay_path_has_exact_spelling(repo.path(), Path::new("SRC/lib.rs"))
.expect("check path spelling")
);
}
#[tokio::test]
async fn clean_overlay_directory_rejects_base_path_rename() {
let _guard = test_guard!();
let repo = init_clean_overlay_test_repo();
std::fs::rename(
repo.path().join("src/lib.rs"),
repo.path().join("src/renamed.rs"),
)
.expect("rename tracked fixture path");
let error = prepare_clean_overlay_spec(
repo.path(),
Some("HEAD".to_string()),
true,
vec![PathBuf::from("src")],
false,
)
.await
.expect_err("directory overlay with base rename must fail closed");
assert!(
error
.to_string()
.contains("selected deletions are unsupported")
);
}
#[tokio::test]
async fn clean_overlay_rejects_case_only_base_directory_alias() {
let _guard = test_guard!();
let repo = init_clean_overlay_test_repo();
std::fs::rename(repo.path().join("src"), repo.path().join("SRC"))
.expect("case-rename tracked fixture directory");
let error = prepare_clean_overlay_spec(
repo.path(),
Some("HEAD".to_string()),
true,
vec![PathBuf::from("SRC")],
false,
)
.await
.expect_err("case-only base alias must fail closed");
assert!(error.to_string().contains("case-only overlays"));
}
#[tokio::test]
async fn clean_overlay_rejects_new_file_under_case_aliased_base_directory() {
let _guard = test_guard!();
let repo = init_clean_overlay_test_repo();
std::fs::rename(repo.path().join("src"), repo.path().join("SRC"))
.expect("case-rename tracked fixture directory");
std::fs::write(repo.path().join("SRC/new.rs"), "pub fn new_file() {}\n")
.expect("write new file under case-aliased directory");
let error = prepare_clean_overlay_spec(
repo.path(),
Some("HEAD".to_string()),
true,
vec![PathBuf::from("SRC/new.rs")],
false,
)
.await
.expect_err("new file under case-aliased base directory must fail closed");
assert!(error.to_string().contains("case-only overlays"));
}
#[tokio::test]
async fn clean_overlay_spec_rejects_empty_overlay_directory() {
let _guard = test_guard!();
let repo = init_clean_overlay_test_repo();
std::fs::create_dir_all(repo.path().join("src/empty")).expect("create empty overlay fixture");
let error = prepare_clean_overlay_spec(
repo.path(),
Some("HEAD".to_string()),
true,
vec![PathBuf::from("src/empty")],
false,
)
.await
.expect_err("empty overlay directory must fail closed");
assert!(error.to_string().contains("empty directories"));
}
#[tokio::test]
async fn clean_overlay_spec_rejects_gitlinks() {
let _guard = test_guard!();
let repo = init_clean_overlay_test_repo();
let head = std::process::Command::new("git")
.current_dir(repo.path())
.args(["rev-parse", "HEAD"])
.output()
.expect("resolve fixture HEAD");
assert!(head.status.success());
let head = String::from_utf8(head.stdout)
.expect("fixture HEAD is UTF-8")
.trim()
.to_string();
let cache_info = format!("160000,{head},vendor/submodule");
let update = std::process::Command::new("git")
.current_dir(repo.path())
.args(["update-index", "--add", "--cacheinfo", &cache_info])
.status()
.expect("add fixture gitlink");
assert!(update.success());
let commit = std::process::Command::new("git")
.current_dir(repo.path())
.args(["commit", "-q", "--no-gpg-sign", "-m", "add gitlink"])
.status()
.expect("commit fixture gitlink");
assert!(commit.success());
let error =
prepare_clean_overlay_spec(repo.path(), Some("HEAD".to_string()), true, vec![], true)
.await
.expect_err("gitlink must fail closed");
assert!(
error
.to_string()
.contains("does not support Git submodules")
);
}
#[tokio::test]
async fn clean_overlay_spec_rejects_git_archive_transform_attributes() {
let _guard = test_guard!();
let repo = init_clean_overlay_test_repo();
std::fs::write(
repo.path().join(".gitattributes"),
"Cargo.toml export-ignore\nsrc/lib.rs export-subst\n",
)
.expect("write archive-transform attributes");
let add = std::process::Command::new("git")
.current_dir(repo.path())
.args(["add", ".gitattributes"])
.status()
.expect("add attributes fixture");
assert!(add.success());
let commit = std::process::Command::new("git")
.current_dir(repo.path())
.args(["commit", "-q", "--no-gpg-sign", "-m", "add attributes"])
.status()
.expect("commit attributes fixture");
assert!(commit.success());
let error =
prepare_clean_overlay_spec(repo.path(), Some("HEAD".to_string()), true, vec![], true)
.await
.expect_err("archive-transform attributes must fail closed");
assert!(error.to_string().contains("non-checkout-equivalent"));
}
#[tokio::test]
async fn clean_overlay_spec_rejects_info_archive_transform_attributes() {
let _guard = test_guard!();
let repo = init_clean_overlay_test_repo();
std::fs::write(
repo.path().join(".git/info/attributes"),
"Cargo.toml export-ignore\n",
)
.expect("write info attributes fixture");
let error =
prepare_clean_overlay_spec(repo.path(), Some("HEAD".to_string()), true, vec![], true)
.await
.expect_err("info archive-transform attributes must fail closed");
assert!(error.to_string().contains("info/attributes"));
}
#[tokio::test]
async fn clean_overlay_rechecks_info_attributes_after_admission() {
let _guard = test_guard!();
let repo = init_clean_overlay_test_repo();
let spec =
prepare_clean_overlay_spec(repo.path(), Some("HEAD".to_string()), true, vec![], true)
.await
.expect("prepare clean overlay")
.expect("clean-overlay spec");
std::fs::write(
repo.path().join(".git/info/attributes"),
"Cargo.toml export-ignore\n",
)
.expect("mutate info attributes after admission");
let error = spec
.verify_archive_attributes(repo.path())
.await
.expect_err("post-admission info attributes must fail closed");
assert!(error.to_string().contains("info/attributes"));
}
#[tokio::test]
async fn clean_overlay_git_reads_ignore_replace_refs() {
let _guard = test_guard!();
let repo = init_clean_overlay_test_repo();
let original = std::process::Command::new("git")
.current_dir(repo.path())
.args(["rev-parse", "HEAD"])
.output()
.expect("resolve original fixture commit");
assert!(original.status.success());
let original = String::from_utf8(original.stdout)
.expect("original fixture commit is UTF-8")
.trim()
.to_string();
std::fs::write(repo.path().join("src/lib.rs"), "pub fn replacement() {}\n")
.expect("write replacement source");
let replacement_commit = std::process::Command::new("git")
.current_dir(repo.path())
.args(["commit", "-qam", "replacement", "--no-gpg-sign"])
.status()
.expect("commit replacement fixture");
assert!(replacement_commit.success());
let replacement = std::process::Command::new("git")
.current_dir(repo.path())
.args(["rev-parse", "HEAD"])
.output()
.expect("resolve replacement fixture commit");
assert!(replacement.status.success());
let replacement = String::from_utf8(replacement.stdout)
.expect("replacement fixture commit is UTF-8")
.trim()
.to_string();
let replace = std::process::Command::new("git")
.current_dir(repo.path())
.args(["replace", &original, &replacement])
.status()
.expect("install fixture replace ref");
assert!(replace.success());
let source = git_show_optional(repo.path(), &original, "src/lib.rs")
.await
.expect("read original source without replacement")
.expect("original source exists");
assert_eq!(source, "pub fn baseline() {}\n");
let spec = prepare_clean_overlay_spec(repo.path(), Some(original.clone()), true, vec![], true)
.await
.expect("prepare original commit with replace ref present")
.expect("clean-overlay spec");
assert_eq!(spec.base_commit(), original);
}
#[test]
fn join_exec_command_plain_args_unchanged() {
let _guard = test_guard!();
let parts = vec![
"cargo".to_string(),
"build".to_string(),
"--release".to_string(),
];
let joined = join_exec_command(&parts);
let round_trip = shell_words::split(&joined).expect("valid shell words");
assert_eq!(round_trip, parts);
}
#[cfg(unix)]
#[test]
fn join_exec_command_keeps_leading_assignments_as_assignments() {
let _guard = test_guard!();
let parts = vec![
"RCH_T_A=one".to_string(),
"RCH_T_B=two words".to_string(),
"sh".to_string(),
"-c".to_string(),
"printf '%s|%s' \"$RCH_T_A\" \"$RCH_T_B\"".to_string(),
];
let joined = join_exec_command(&parts);
let output = std::process::Command::new("sh")
.arg("-c")
.arg(&joined)
.output()
.expect("run sh");
assert!(output.status.success(), "{joined}: {output:?}");
assert_eq!(String::from_utf8_lossy(&output.stdout), "one|two words");
let single = join_exec_command(&["RCH_T_A=1 cargo test".to_string()]);
assert_eq!(
shell_words::split(&single).expect("valid shell words"),
["env", "RCH_T_A=1", "cargo", "test"]
);
assert_eq!(
join_exec_command(&["=x".to_string(), "y".to_string()]),
shell_words::join(["=x", "y"])
);
}
#[test]
fn clean_overlay_fmt_check_allowlist_is_exact_and_read_only() {
let _guard = test_guard!();
assert!(is_clean_overlay_cargo_fmt_check(&[
"cargo".into(),
"fmt".into(),
"--check".into(),
]));
assert!(is_clean_overlay_cargo_fmt_check(&[
"/usr/bin/cargo".into(),
"+nightly".into(),
"fmt".into(),
"--all".into(),
"--check".into(),
]));
assert!(!is_clean_overlay_cargo_fmt_check(&[
"cargo".into(),
"fmt".into(),
]));
assert!(!is_clean_overlay_cargo_fmt_check(&[
"cargo".into(),
"metadata".into(),
"--check".into(),
]));
}
#[test]
fn join_exec_command_preserves_space_bearing_arg() {
let _guard = test_guard!();
let parts = vec![
"cargo".to_string(),
"build".to_string(),
"--features=foo bar".to_string(),
];
let joined = join_exec_command(&parts);
let round_trip = shell_words::split(&joined).expect("valid shell words");
assert_eq!(
round_trip, parts,
"space must survive round-trip through sh"
);
}
#[test]
fn join_exec_command_preserves_quote_metachars() {
let _guard = test_guard!();
let parts = vec![
"cargo".to_string(),
"run".to_string(),
"--".to_string(),
"he said \"hi\"".to_string(),
"$PATH".to_string(),
"a;b".to_string(),
];
let joined = join_exec_command(&parts);
let round_trip = shell_words::split(&joined).expect("valid shell words");
assert_eq!(round_trip, parts);
}
#[test]
fn join_exec_command_splits_single_shell_command_arg() {
let _guard = test_guard!();
let parts =
vec!["env RUSTFLAGS=\"-C linker=cc\" cargo build --bin generate_react_goldens".to_string()];
let joined = join_exec_command(&parts);
let round_trip = shell_words::split(&joined).expect("valid shell words");
assert_eq!(
round_trip,
vec![
"env".to_string(),
"RUSTFLAGS=-C linker=cc".to_string(),
"cargo".to_string(),
"build".to_string(),
"--bin".to_string(),
"generate_react_goldens".to_string(),
]
);
assert!(
!joined.starts_with("'env "),
"env wrapper must remain the executable, not part of one quoted command: {joined}"
);
}
#[test]
fn join_exec_command_preserves_already_split_env_prefix() {
let _guard = test_guard!();
let parts = vec![
"env".to_string(),
"RUSTFLAGS=-C linker=cc".to_string(),
"cargo".to_string(),
"build".to_string(),
];
let joined = join_exec_command(&parts);
let round_trip = shell_words::split(&joined).expect("valid shell words");
assert_eq!(round_trip, parts);
}
#[test]
fn join_exec_command_empty_input() {
let _guard = test_guard!();
let parts: Vec<String> = Vec::new();
assert_eq!(join_exec_command(&parts), "");
}
#[test]
fn local_fallback_command_bypasses_cargo_wrapper() {
let _guard = test_guard!();
let command = local_fallback_command("cargo test -p rch");
let has_bypass = command.get_envs().any(|(key, value)| {
key == std::ffi::OsStr::new(RCH_CARGO_WRAPPER_BYPASS_ENV)
&& value == Some(std::ffi::OsStr::new("1"))
});
assert!(
has_bypass,
"local fallback must bypass the PATH cargo wrapper to avoid recursive rch exec"
);
let args = command.get_args().collect::<Vec<_>>();
assert_eq!(
args,
vec![
std::ffi::OsStr::new("-c"),
std::ffi::OsStr::new("cargo test -p rch")
]
);
}
#[test]
fn shell_wrapped_cargo_command_is_detected_before_exec_fallback() {
let _guard = test_guard!();
assert!(shell_wrapped_cargo_command(&[
"bash".to_string(),
"-lc".to_string(),
"cargo build --release".to_string(),
]));
assert!(shell_wrapped_cargo_command(&[
"sh".to_string(),
"-c".to_string(),
"cargo clippy --workspace --all-targets && cargo build --release --example probe"
.to_string(),
]));
assert!(!shell_wrapped_cargo_command(&[
"sh".to_string(),
"-c".to_string(),
"echo cargo build".to_string(),
]));
assert!(!shell_wrapped_cargo_command(&[
"cargo".to_string(),
"build".to_string(),
]));
}
#[test]
fn remote_required_fallback_refuses_before_building_local_shell_command() {
let _guard = test_guard!();
let command = "bash -lc 'cargo test --lib focused_case -- --nocapture'";
assert!(
matches!(
local_fallback_command_for_policy(command, true),
Err(LocalFallbackRefusal::RemoteRequired)
),
"remote-required policy must refuse before constructing a local shell fallback"
);
assert!(
local_fallback_command_for_policy(command, false).is_ok(),
"ordinary local fallback behavior remains available when remote is not required"
);
}
#[test]
fn remote_required_non_compilation_shell_wrapped_cargo_has_stable_refusal_code() {
let _guard = test_guard!();
let parts = vec![
"bash".to_string(),
"-lc".to_string(),
"cargo test --lib focused_case -- --nocapture".to_string(),
];
let command = join_exec_command(&parts);
let classification = classify_command(&command);
assert!(
!classification.is_compilation,
"global classification should keep arbitrary shell wrappers out of hook-mode offload"
);
assert!(
matches!(
local_fallback_command_for_policy(&command, true),
Err(LocalFallbackRefusal::RemoteRequired)
),
"RCH_REQUIRE_REMOTE must prevent the shell from running locally even when classification rejects it"
);
assert!(remote_required_refusal_summary("non-compilation command").contains("RCH-E301"));
assert!(
!remote_required_refusal_summary("dependency preflight failed").contains("RCH-E301"),
"dependency-topology refusals should remain distinguishable from command-classification refusals"
);
}
#[test]
fn env_flag_enabled_accepts_common_truthy_values() {
let _guard = test_guard!();
for value in ["1", "true", "TRUE", "yes", "on"] {
assert!(env_flag_enabled(value), "{value} should be truthy");
}
for value in ["", "0", "false", "no", "off", "remote"] {
assert!(!env_flag_enabled(value), "{value} should not be truthy");
}
}
#[test]
fn hook_panic_fail_open_can_be_enabled_without_env_var() {
let _guard = test_guard!();
let previous = HOOK_MODE_PANIC_FAIL_OPEN.swap(false, Ordering::AcqRel);
enable_hook_mode_panic_fail_open();
assert!(
hook_mode_panic_fail_open_enabled(),
"installing the hook panic handler must mark no-subcommand hook mode as fail-open even when RCH_HOOK_MODE is unset"
);
HOOK_MODE_PANIC_FAIL_OPEN.store(previous, Ordering::Release);
}
fn test_lock() -> &'static Mutex<()> {
static ENV_MUTEX: OnceLock<Mutex<()>> = OnceLock::new();
ENV_MUTEX.get_or_init(|| Mutex::new(()))
}
struct TestOverridesGuard;
impl TestOverridesGuard {
fn set(socket_path: &str, ssh_config: MockConfig, rsync_config: MockRsyncConfig) -> Self {
let mut config = rch_common::RchConfig::default();
config.general.socket_path = socket_path.to_string();
crate::config::set_test_config_override(Some(config));
set_mock_enabled_override(Some(true));
set_mock_ssh_config_override(Some(ssh_config));
set_mock_rsync_config_override(Some(rsync_config));
Self
}
}
impl Drop for TestOverridesGuard {
fn drop(&mut self) {
crate::config::set_test_config_override(None);
clear_mock_overrides();
}
}
struct ConfigOverrideGuard;
impl ConfigOverrideGuard {
fn set(config: rch_common::RchConfig) -> Self {
crate::config::set_test_config_override(Some(config));
Self
}
}
impl Drop for ConfigOverrideGuard {
fn drop(&mut self) {
crate::config::set_test_config_override(None);
}
}
struct CanonicalTempDir {
_dir: tempfile::TempDir,
path: PathBuf,
}
impl CanonicalTempDir {
fn path(&self) -> &Path {
&self.path
}
}
fn topology_tempdir() -> (CanonicalTempDir, PathTopologyPolicy) {
let raw = tempfile::tempdir().expect("create tempdir");
let canonical = std::fs::canonicalize(raw.path()).expect("canonicalize tempdir");
let alias_root = canonical
.parent()
.map(|parent| {
let leaf = canonical
.file_name()
.and_then(|s| s.to_str())
.unwrap_or("tmp");
parent.join(format!("{leaf}__rch_alias_sentinel"))
})
.unwrap_or_else(|| canonical.clone());
let policy = PathTopologyPolicy::new(canonical.clone(), alias_root);
(
CanonicalTempDir {
_dir: raw,
path: canonical,
},
policy,
)
}
async fn spawn_mock_daemon(socket_path: &str, response: SelectionResponse) {
let _ = std::fs::remove_file(socket_path);
let listener = UnixListener::bind(socket_path).expect("Failed to bind mock socket");
tokio::spawn(async move {
let (stream, _) = listener.accept().await.expect("Accept failed");
let (reader, mut writer) = stream.into_split();
let mut buf_reader = TokioBufReader::new(reader);
let mut request_line = String::new();
buf_reader
.read_line(&mut request_line)
.await
.expect("Failed to read request");
let body = serde_json::to_string(&response).expect("Serialize response");
let http = format!("HTTP/1.1 200 OK\r\n\r\n{}", body);
writer
.write_all(http.as_bytes())
.await
.expect("Failed to write response");
writer.flush().await.expect("Failed to flush response");
});
}
#[tokio::test]
async fn test_non_bash_allowed() {
let input = HookInput {
tool_name: "Read".to_string(),
tool_input: ToolInput {
command: "anything".to_string(),
description: None,
},
session_id: None,
};
let output = process_hook(input).await;
assert!(output.is_allow());
}
#[tokio::test]
async fn test_non_compilation_allowed() {
let input = HookInput {
tool_name: "Bash".to_string(),
tool_input: ToolInput {
command: "ls -la".to_string(),
description: None,
},
session_id: None,
};
let output = process_hook(input).await;
assert!(output.is_allow());
}
#[tokio::test]
async fn test_process_hook_allows_beads_comment_with_embedded_build_text() {
let input = HookInput {
tool_name: "Bash".to_string(),
tool_input: ToolInput {
command:
r#"br comments add ft-4tp7g.1 "remote proof blocked: cargo test -p rchd --lib""#
.to_string(),
description: None,
},
session_id: None,
};
let output = process_hook(input).await;
assert!(
matches!(output, HookOutput::Allow(_)),
"embedded build text in a Beads comment must not delegate to rch exec: {output:?}"
);
}
#[tokio::test]
async fn test_process_hook_allows_env_prefixed_beads_comment_with_embedded_build_text() {
let input = HookInput {
tool_name: "Bash".to_string(),
tool_input: ToolInput {
command: r#"AGENT_NAME=Codex br comments add ft-4tp7g.4 "proof lane: cargo clippy --workspace""#
.to_string(),
description: None,
},
session_id: None,
};
let output = process_hook(input).await;
assert!(
matches!(output, HookOutput::Allow(_)),
"env-prefixed Beads comments with build text must not delegate to rch exec: {output:?}"
);
}
#[tokio::test]
async fn test_process_hook_bypasses_classification_cache_without_env_flag() {
let unique_cmd = "echo rch-hook-cache-bypass-without-env-marker";
let input = HookInput {
tool_name: "Bash".to_string(),
tool_input: ToolInput {
command: unique_cmd.to_string(),
description: None,
},
session_id: None,
};
let output = process_hook(input).await;
assert!(output.is_allow());
assert!(
crate::cache::global_cache().get(unique_cmd).is_none(),
"process_hook must bypass the cache even when RCH_HOOK_MODE is unset"
);
}
#[tokio::test]
async fn test_compilation_detected() {
let _lock = test_lock().lock().await;
mock::set_mock_enabled_override(Some(false));
let input = HookInput {
tool_name: "Bash".to_string(),
tool_input: ToolInput {
command: "cargo build --release".to_string(),
description: None,
},
session_id: None,
};
let output = process_hook(input).await;
assert!(
output.is_allow(),
"Expected allow when daemon unavailable (fail-open)"
);
mock::set_mock_enabled_override(None);
}
#[test]
fn test_timing_estimate_struct() {
let _guard = test_guard!();
let estimate = TimingEstimate {
predicted_local_ms: 5000,
predicted_speedup: Some(2.5),
};
assert_eq!(estimate.predicted_local_ms, 5000);
assert_eq!(estimate.predicted_speedup, Some(2.5));
}
#[test]
fn test_timing_estimate_no_speedup() {
let _guard = test_guard!();
let estimate = TimingEstimate {
predicted_local_ms: 3000,
predicted_speedup: None,
};
assert_eq!(estimate.predicted_local_ms, 3000);
assert!(estimate.predicted_speedup.is_none());
}
#[test]
fn test_estimate_timing_returns_none_without_history() {
let _guard = test_guard!();
let config = rch_common::RchConfig::default();
let estimate =
estimate_timing_for_build("test-project", Some(CompilationKind::CargoBuild), &config);
assert!(estimate.is_none());
}
#[test]
fn test_timing_gating_thresholds_default() {
let _guard = test_guard!();
let config = rch_common::CompilationConfig::default();
assert_eq!(config.min_local_time_ms, 2000);
assert!((config.remote_speedup_threshold - 1.2).abs() < 0.001);
}
#[test]
fn test_urlencoding_encode_basic() {
let _guard = test_guard!();
assert_eq!(urlencoding_encode("hello world"), "hello%20world");
assert_eq!(urlencoding_encode("path/to/file"), "path%2Fto%2Ffile");
assert_eq!(urlencoding_encode("foo:bar"), "foo%3Abar");
}
#[test]
fn test_urlencoding_encode_special_chars() {
let _guard = test_guard!();
assert_eq!(urlencoding_encode("a&b=c"), "a%26b%3Dc");
assert_eq!(urlencoding_encode("100%"), "100%25");
assert_eq!(urlencoding_encode("hello+world"), "hello%2Bworld");
}
#[test]
fn test_urlencoding_encode_no_encoding_needed() {
let _guard = test_guard!();
assert_eq!(urlencoding_encode("simple"), "simple");
assert_eq!(
urlencoding_encode("with-dash_underscore.dot~tilde"),
"with-dash_underscore.dot~tilde"
);
assert_eq!(urlencoding_encode("ABC123"), "ABC123");
}
#[test]
fn test_urlencoding_encode_unicode() {
let _guard = test_guard!();
let encoded = urlencoding_encode("café");
assert!(encoded.contains("%")); assert!(encoded.starts_with("caf")); }
#[test]
fn test_parse_jobs_flag_variants() {
let _guard = test_guard!();
assert_eq!(parse_jobs_flag("cargo build -j 8"), Some(8));
assert_eq!(parse_jobs_flag("cargo build -j8"), Some(8));
assert_eq!(parse_jobs_flag("cargo build --jobs 4"), Some(4));
assert_eq!(parse_jobs_flag("cargo build --jobs=12"), Some(12));
assert_eq!(parse_jobs_flag("cargo build -j=16"), Some(16));
assert_eq!(parse_jobs_flag("cargo build --jobs=12"), Some(12));
assert_eq!(parse_jobs_flag("cargo build -j"), None);
assert_eq!(parse_jobs_flag("cargo build --jobs"), None);
}
#[test]
fn test_parse_test_threads_variants() {
let _guard = test_guard!();
assert_eq!(
parse_test_threads("cargo test -- --test-threads=4"),
Some(4)
);
assert_eq!(
parse_test_threads("cargo test -- --test-threads 2"),
Some(2)
);
assert_eq!(parse_test_threads("cargo test"), None);
}
#[test]
fn test_go_classification_drives_reservations_and_test_kind() {
let _guard = test_guard!();
let config = rch_common::CompilationConfig {
build_slots: 12,
test_slots: 8,
check_slots: 4,
..Default::default()
};
for (verb, kind, default, is_test) in [
("build -o bin/app", CompilationKind::GoBuild, 12, false),
("test", CompilationKind::GoTest, 8, true),
("vet", CompilationKind::GoVet, 4, false),
] {
if kind == CompilationKind::GoBuild && !cfg!(unix) {
continue;
}
for (command, expected) in [
(format!("go {verb} ./..."), default),
(format!("go {verb} -p 2 ./..."), 2),
] {
let classification = classify_command(&command);
assert!(classification.is_compilation, "{command}");
assert_eq!(classification.kind, Some(kind), "{command}");
assert_eq!(is_test_kind(classification.kind), is_test, "{command}");
assert_eq!(
estimate_cores_for_command(classification.kind, &command, &config),
expected,
"{command}"
);
}
}
}
#[test]
fn test_estimate_cores_for_command() {
let _guard = test_guard!();
let config = rch_common::CompilationConfig {
build_slots: 6,
test_slots: 10,
check_slots: 3,
..Default::default()
};
let build =
estimate_cores_for_command(Some(CompilationKind::CargoBuild), "cargo build", &config);
assert_eq!(build, 6);
let build_jobs = estimate_cores_for_command(
Some(CompilationKind::CargoBuild),
"cargo build -j 12",
&config,
);
assert_eq!(build_jobs, 12);
let test_default =
estimate_cores_for_command(Some(CompilationKind::CargoTest), "cargo test", &config);
assert_eq!(test_default, 10);
let test_threads = estimate_cores_for_command(
Some(CompilationKind::CargoTest),
"cargo test -- --test-threads=4",
&config,
);
assert_eq!(test_threads, 4);
let test_jobs = estimate_cores_for_command(
Some(CompilationKind::CargoTest),
"cargo test -j 1 -p rchd --lib",
&config,
);
assert_eq!(test_jobs, 1);
let test_long_jobs = estimate_cores_for_command(
Some(CompilationKind::CargoTest),
"cargo test --jobs=3 -p rchd --lib",
&config,
);
assert_eq!(test_long_jobs, 3);
let test_jobs_override_threads = estimate_cores_for_command(
Some(CompilationKind::CargoTest),
"cargo test -j 1 -- --test-threads=8",
&config,
);
assert_eq!(test_jobs_override_threads, 1);
let test_build_jobs_env = estimate_cores_for_command(
Some(CompilationKind::CargoTest),
"CARGO_BUILD_JOBS=2 cargo test -p rchd --lib",
&config,
);
assert_eq!(test_build_jobs_env, 2);
let test_env = estimate_cores_for_command(
Some(CompilationKind::CargoTest),
"RUST_TEST_THREADS=3 cargo test",
&config,
);
assert_eq!(test_env, 3);
let check_default =
estimate_cores_for_command(Some(CompilationKind::CargoCheck), "cargo check", &config);
assert_eq!(check_default, 3);
}
#[test]
fn test_cargo_xwin_quoted_options_preserve_classification_and_admission() {
use rch_common::admit_preflight::{preflight, required_os_for_command};
use rch_common::patterns::cargo_xwin_compilation_kind;
let _guard = test_guard!();
for (subcommand, kind, family) in [
("build", CompilationKind::CargoBuild, "cargo_build"),
("check", CompilationKind::CargoCheck, "cargo_check"),
("clippy", CompilationKind::CargoClippy, "cargo_clippy"),
] {
for (prefix, toolchain) in [
(vec!["cargo", "--config=build.jobs=2", "xwin"], None),
(
vec![
"cargo",
"+nightly-2026-08-31",
"--config=build.jobs=2",
"xwin",
],
Some("nightly-2026-08-31"),
),
(vec!["cargo-xwin"], None),
(vec!["cargo-xwin", "xwin"], None),
(
vec![
"env",
"RCH_XWIN_NOTE=placeholder --target=aarch64-apple-darwin",
"cargo",
"--config=build.jobs=2",
"xwin",
],
None,
),
] {
for (target, expected_os) in [
("x86_64-pc-windows-msvc", None),
("aarch64-pc-windows-msvc", None),
("aarch64-apple-darwin", Some("darwin")),
] {
let target_argument = format!("--target={target}");
let mut argv = prefix.clone();
argv.extend([
subcommand,
"--config=build.jobs=2",
"--locked",
"--all-targets",
target_argument.as_str(),
]);
if subcommand == "clippy" {
argv.extend(["--", "-D", "warnings"]);
}
let command = shell_words::join(&argv);
assert!(command.contains("'--config=build.jobs=2'"), "{command}");
assert!(
command.contains(&format!("'{target_argument}'")),
"{command}"
);
assert_eq!(
cargo_xwin_compilation_kind(&command),
Some(kind),
"{command}"
);
let classification = classify_command(&command);
assert!(classification.is_compilation, "{command}");
assert_eq!(classification.kind, Some(kind), "{command}");
let result = preflight(&command, true);
let expected_toolchains: Vec<String> = toolchain
.into_iter()
.map(|value| value.to_string())
.collect();
assert!(result.is_compilation, "{command}");
assert_eq!(result.family.as_deref(), Some(family), "{command}");
assert!(result.required.needs_cargo, "{command}");
assert_eq!(
result.required.needs_targets,
vec![target.to_string()],
"{command}"
);
assert_eq!(
result.required.needs_toolchains, expected_toolchains,
"{command}"
);
assert_eq!(
result.required.needs_os.as_deref(),
expected_os,
"{command}"
);
assert_eq!(
required_os_for_command(&command).as_deref(),
expected_os,
"{command}"
);
let requirement = result.required.to_requirement(1);
assert!(requirement.needs_cargo, "{command}");
assert_eq!(
requirement.needs_targets, result.required.needs_targets,
"{command}"
);
assert_eq!(
requirement.needs_toolchains, result.required.needs_toolchains,
"{command}"
);
assert_eq!(requirement.needs_os.as_deref(), expected_os, "{command}");
}
}
}
}
#[test]
fn test_cargo_xwin_quoted_mentions_and_nonliteral_options_do_not_authorize_offload() {
use rch_common::admit_preflight::{preflight, required_os_for_command};
use rch_common::patterns::cargo_xwin_compilation_kind;
let _guard = test_guard!();
for (subcommand, kind) in [
("build", CompilationKind::CargoBuild),
("check", CompilationKind::CargoCheck),
("clippy", CompilationKind::CargoClippy),
] {
for argv in [
vec![
"cargo",
"--config=xwin",
subcommand,
"--target",
"x86_64-pc-windows-msvc",
],
vec![
"cargo",
"--config",
"xwin",
subcommand,
"--target",
"x86_64-pc-windows-msvc",
],
vec![
"cargo",
subcommand,
"--manifest-path",
"cargo xwin check",
"--target",
"x86_64-pc-windows-msvc",
],
] {
let command = shell_words::join(&argv);
assert_eq!(cargo_xwin_compilation_kind(&command), None, "{command}");
assert_eq!(classify_command(&command).kind, Some(kind), "{command}");
assert_eq!(
preflight(&command, true).required.needs_os.as_deref(),
Some("windows"),
"{command}"
);
assert_eq!(
required_os_for_command(&command).as_deref(),
Some("windows"),
"{command}"
);
}
}
for command in [
"cargo 'xwin' check '--target=x86_64-pc-windows-msvc'",
"cargo xwin 'check' '--target=x86_64-pc-windows-msvc'",
"cargo-xwin 'xwin' clippy '--target=x86_64-pc-windows-msvc'",
"cargo-xwin 'clippy' '--target=x86_64-pc-windows-msvc'",
"'cargo-xwin' check '--target=x86_64-pc-windows-msvc'",
"cargo xwin check '--help'",
"cargo xwin check --config='build.jobs=2'",
] {
assert_eq!(cargo_xwin_compilation_kind(command), None, "{command}");
assert!(!classify_command(command).is_compilation, "{command}");
}
for value in [
"two words",
"a\"b",
"a'b",
r"a\b",
"$value",
"`value`",
"a;b",
"a|b",
"a&b",
"<value>",
"(value)",
"{a,b}",
"*",
"[value]",
"#value",
"~value",
] {
let option = format!("--config={value}");
let command = shell_words::join(["cargo", "xwin", "check", option.as_str()]);
assert_eq!(cargo_xwin_compilation_kind(&command), None, "{command}");
assert!(!classify_command(&command).is_compilation, "{command}");
}
}
#[test]
fn test_classification_confidence_levels() {
let _guard = test_guard!();
let result = classify_command("cargo build");
assert!(result.is_compilation);
assert!(result.confidence >= 0.90);
let result = classify_command("cargo test --release");
assert!(result.is_compilation);
assert!(result.confidence >= 0.85);
let result = classify_command("cargo fmt");
assert!(!result.is_compilation);
}
#[test]
fn test_classification_bun_commands() {
let _guard = test_guard!();
let result = classify_command("bun test");
assert!(result.is_compilation);
let result = classify_command("bun typecheck");
assert!(result.is_compilation);
let result = classify_command("bun test --watch");
assert!(!result.is_compilation);
let result = classify_command("bun typecheck --watch");
assert!(!result.is_compilation);
let result = classify_command("bun install");
assert!(!result.is_compilation);
let result = classify_command("bun add react");
assert!(!result.is_compilation);
let result = classify_command("bun remove react");
assert!(!result.is_compilation);
let result = classify_command("bun link");
assert!(!result.is_compilation);
let result = classify_command("bun run build");
assert!(!result.is_compilation);
let result = classify_command("bun build");
assert!(!result.is_compilation);
let result = classify_command("bun dev");
assert!(!result.is_compilation);
let result = classify_command("bun repl");
assert!(!result.is_compilation);
let result = classify_command("bun x vite build");
assert!(!result.is_compilation);
let result = classify_command("bunx vite build");
assert!(!result.is_compilation);
}
#[test]
fn test_classification_c_compilers_and_build_systems() {
let _guard = test_guard!();
let result = classify_command("gcc -O2 -o hello hello.c");
assert!(result.is_compilation);
let result = classify_command("g++ -std=c++20 -o hello hello.cpp");
assert!(result.is_compilation);
let result = classify_command("clang -o hello hello.c");
assert!(result.is_compilation);
let result = classify_command("clang++ -o hello hello.cpp");
assert!(result.is_compilation);
let result = classify_command("make");
assert!(result.is_compilation);
let result = classify_command("ninja -C build");
assert!(result.is_compilation);
let result = classify_command("cmake --build build");
assert!(result.is_compilation);
}
#[test]
fn test_classification_env_wrapped_commands() {
let _guard = test_guard!();
let result = classify_command("RUST_BACKTRACE=1 cargo test");
assert!(result.is_compilation);
let result = classify_command("RUST_TEST_THREADS=4 cargo test");
assert!(result.is_compilation);
}
#[test]
fn test_classification_rejects_shell_metachars() {
let _guard = test_guard!();
let result = classify_command("cargo build | tee log.txt");
assert!(result.is_compilation, "got: {:?}", result.reason);
assert_eq!(
result.extracted_command.as_deref(),
Some("cargo build | tee log.txt")
);
let result = classify_command("cargo build &");
assert!(result.is_compilation, "got: {:?}", result.reason);
assert_eq!(result.extracted_command.as_deref(), Some("cargo build &"));
let result = classify_command("cargo build > output.log");
assert!(result.is_compilation, "got: {:?}", result.reason);
assert_eq!(
result.extracted_command.as_deref(),
Some("cargo build > output.log")
);
let result = classify_command("result=$(cargo build)");
assert!(!result.is_compilation);
assert!(result.reason.contains("subshell"));
let result = classify_command("cargo build < input.txt");
assert!(!result.is_compilation);
assert!(result.reason.contains("input"));
let result = classify_command("cargo build | xargs rm");
assert!(!result.is_compilation);
assert!(result.reason.contains("pipe"));
}
#[test]
fn test_extract_project_name() {
let _guard = test_guard!();
let project = command_parsing::extract_project_name();
assert!(!project.is_empty());
}
#[test]
fn test_extract_project_name_honors_custom_policy() {
let _guard = test_guard!();
use std::fs;
let tmp = std::env::temp_dir().join(format!(
"rch_extract_custom_{}_{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0)
));
fs::create_dir_all(&tmp).expect("create canonical root");
let project_dir = tmp.join("sample_project");
fs::create_dir_all(&project_dir).expect("create project dir");
let canonical_tmp = fs::canonicalize(&tmp).expect("canonicalize tmp");
let canonical_project = fs::canonicalize(&project_dir).expect("canonicalize project");
let policy = PathTopologyPolicy::new(canonical_tmp.clone(), canonical_tmp.clone());
let prev_cwd = std::env::current_dir().ok();
std::env::set_current_dir(&canonical_project).expect("cd into project dir");
let project = extract_project_name_with_policy(&policy);
if let Some(prev) = prev_cwd {
let _ = std::env::set_current_dir(prev);
}
let _ = fs::remove_dir_all(&tmp);
assert!(
project.starts_with("sample_project-"),
"expected project name to start with sample_project-, got {:?} \
(cwd was {:?})",
project,
canonical_project
);
}
#[test]
fn test_hook_output_allow_is_empty() {
let _guard = test_guard!();
let output = HookOutput::allow();
assert!(output.is_allow());
}
#[test]
fn test_hook_output_deny_serializes() {
let _guard = test_guard!();
let output = HookOutput::deny("Test denial reason".to_string());
let json = serde_json::to_string(&output).expect("Should serialize");
assert!(json.contains("deny"));
assert!(json.contains("Test denial reason"));
}
#[test]
fn test_selected_worker_to_config() {
let _guard = test_guard!();
let worker = SelectedWorker {
id: rch_common::WorkerId::new("test-worker"),
host: "192.168.1.100".to_string(),
user: "ubuntu".to_string(),
identity_file: "~/.ssh/id_rsa".to_string(),
slots_available: 8,
speed_score: 75.5,
declared_os: None,
};
let config = selected_worker_to_config(&worker);
assert_eq!(config.id.as_str(), "test-worker");
assert_eq!(config.host, "192.168.1.100");
assert_eq!(config.user, "ubuntu");
assert_eq!(config.total_slots, 8);
}
#[test]
fn test_parse_preferred_workers_dedupes_ordered_values() {
let _guard = test_guard!();
let workers = command_parsing::dedupe_worker_ids(command_parsing::parse_preferred_workers(
" ts2, vmi1,,ts2 , vmi2 ",
));
let ids: Vec<&str> = workers.iter().map(|worker| worker.as_str()).collect();
assert_eq!(ids, vec!["ts2", "vmi1", "vmi2"]);
}
#[test]
fn test_parse_preferred_workers_from_toml_routing_section() {
let _guard = test_guard!();
let toml = r#"
[general]
enabled = true
[routing]
preferred_workers = ["hz2", " vmi1264463 ", ""]
"#;
let ids: Vec<String> = command_parsing::parse_preferred_workers_from_toml(toml)
.iter()
.map(|w| w.as_str().to_string())
.collect();
assert_eq!(ids, vec!["hz2".to_string(), "vmi1264463".to_string()]);
}
#[test]
fn test_parse_preferred_workers_from_toml_absent_or_malformed_is_empty() {
let _guard = test_guard!();
assert!(
command_parsing::parse_preferred_workers_from_toml("[general]\nenabled = true\n")
.is_empty()
);
assert!(
command_parsing::parse_preferred_workers_from_toml("[routing]\nother = 1\n").is_empty()
);
assert!(
command_parsing::parse_preferred_workers_from_toml(
"[routing]\npreferred_workers = \"hz2\"\n"
)
.is_empty()
);
assert!(command_parsing::parse_preferred_workers_from_toml("this is not toml {{{").is_empty());
assert!(command_parsing::parse_preferred_workers_from_toml("").is_empty());
}
#[test]
fn test_preferred_workers_from_config_path_missing_file_is_empty() {
let _guard = test_guard!();
let missing = std::path::Path::new("/tmp/definitely-not-a-real-rch-config-xyz.toml");
assert!(command_parsing::preferred_workers_from_config_path(missing).is_empty());
}
#[test]
fn test_remote_required_refusal_retryable_classification() {
let _guard = test_guard!();
for retryable in [
"daemon unavailable",
"no worker assigned",
"transfer skipped",
"remote execution failed",
] {
assert!(
remote_required_refusal_is_retryable(retryable),
"{retryable} should be retryable"
);
assert!(
remote_required_refusal_summary(retryable).contains("retryable"),
"{retryable} summary should carry the typed retryable marker"
);
}
for permanent in ["non-compilation command", "config unavailable"] {
assert!(
!remote_required_refusal_is_retryable(permanent),
"{permanent} should NOT be retryable"
);
assert!(
!remote_required_refusal_summary(permanent).contains("retryable"),
"{permanent} summary must not claim retryable"
);
}
}
#[test]
fn test_requested_worker_refusal_summary_is_stable_and_actionable() {
let _guard = test_guard!();
let response = SelectionResponse {
worker: None,
reason: SelectionReason::NoMatchingWorkers,
build_id: None,
diagnostics: None,
};
let summary = requested_worker_refusal_summary(&[WorkerId::new("missing")], &response)
.expect("explicit request should produce a refusal summary");
assert!(summary.starts_with("[RCH-I001]"));
assert!(summary.contains("requested worker set [missing]"));
assert!(summary.contains("run `rch diagnose -- <command>`"));
}
#[test]
fn test_selected_worker_must_belong_to_requested_set() {
let _guard = test_guard!();
let requested = [WorkerId::new("worker-a"), WorkerId::new("worker-b")];
assert!(selected_worker_is_requested(
&WorkerId::new("worker-b"),
&requested
));
assert!(!selected_worker_is_requested(
&WorkerId::new("worker-c"),
&requested
));
assert!(selected_worker_is_requested(
&WorkerId::new("automatic"),
&[]
));
}
#[test]
fn exact_exec_worker_overrides_preferences_and_retries_cannot_escape() {
let _guard = test_guard!();
let inherited = vec![
WorkerId::new("project-worker"),
WorkerId::new("environment-worker"),
];
let mut inherited_reads = 0;
let pinned = exec_worker_preferences(Some(WorkerId::new("native-worker")), || {
inherited_reads += 1;
inherited.clone()
});
assert_eq!(
inherited_reads, 0,
"an explicit pin must not read merged preferences"
);
assert_eq!(pinned, vec![WorkerId::new("native-worker")]);
assert!(selected_worker_is_requested(
&WorkerId::new("native-worker"),
&pinned
));
assert!(!selected_worker_is_requested(
&WorkerId::new("project-worker"),
&pinned
));
assert!(!selected_worker_is_requested(
&WorkerId::new("environment-worker"),
&pinned
));
assert_eq!(
exec_worker_preferences(None, || inherited.clone()),
inherited
);
let snapshots = vec![
WorkerCapacitySnapshot {
id: WorkerId::new("project-worker"),
healthy: true,
pressure_critical: false,
free_slots: 64,
total_slots: 64,
speed_score: 100.0,
memory_pressure: None,
},
WorkerCapacitySnapshot {
id: WorkerId::new("native-worker"),
healthy: true,
pressure_critical: false,
free_slots: 4,
total_slots: 4,
speed_score: 50.0,
memory_pressure: None,
},
];
assert_eq!(
pick_bigger_worker(&snapshots, &[], &pinned),
Some(WorkerId::new("native-worker"))
);
assert!(pick_bigger_worker(&snapshots, &[WorkerId::new("native-worker")], &pinned).is_none());
}
#[tokio::test]
async fn test_daemon_query_missing_socket() {
let result = query_daemon(
"/tmp/nonexistent_rch_test.sock",
"testproj",
4,
0,
"cargo build",
None,
RequiredRuntime::None,
CommandPriority::Normal,
100, None,
None,
false,
&[],
false,
&[],
)
.await;
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(err_msg.contains("not found") || err_msg.contains("No such file"));
}
fn queued_selection_test_lease(path: PathBuf) -> DurableLeaseWriter {
let writer = DurableLeaseWriter {
path,
#[cfg(unix)]
recovery_ownership: None,
lease: Arc::new(std::sync::Mutex::new(DurableJobLease::new(
JobIdentity::new_local(),
std::process::id(),
None,
None,
0,
false,
true,
"test-fingerprint".into(),
))),
};
writer.persist().unwrap();
writer
}
#[test]
fn compiler_admission_reference_binds_one_durable_attempt_without_rounding() {
let _guard = test_guard!();
let retained = tempfile::tempdir().unwrap().keep();
let journal = retained.join("lease.json");
let writer = queued_selection_test_lease(journal.clone());
let wrapper = writer.wrapper_id();
let worker = WorkerId::new("attempt-worker-fixture");
let build = 9_007_199_254_740_993_u64;
let before = std::fs::read(&journal).unwrap();
for invalid_build in [0, 1_u64 << 63, u64::MAX] {
assert!(writer.admit(invalid_build, &worker).is_err());
assert!(
writer
.admission_reference(&wrapper, invalid_build, &worker)
.is_err()
);
}
for invalid_worker in ["", " \t"] {
assert!(writer.admit(build, &WorkerId::new(invalid_worker)).is_err());
}
assert_eq!(std::fs::read(&journal).unwrap(), before);
assert_eq!(
absolute_lease_path(PathBuf::from("relative-journal.json")).unwrap(),
std::env::current_dir()
.unwrap()
.join("relative-journal.json")
);
assert_eq!(absolute_lease_path(journal.clone()).unwrap(), journal);
assert!(
writer
.admission_reference(&wrapper, build, &worker)
.is_err()
);
writer.admit(build, &worker).unwrap();
let reference = writer
.admission_reference(&wrapper, build, &worker)
.unwrap();
let value = serde_json::to_value(&reference).unwrap();
assert_eq!(value["remote_build_id"], "9007199254740993");
assert_eq!(value["local_wrapper_id"], wrapper);
assert_eq!(value["worker_id"], worker.as_str());
assert_eq!(value["journal_path"], journal.to_str().unwrap());
assert_eq!(value["terminal_acknowledged"], false);
let persisted: DurableJobLease =
serde_json::from_slice(&std::fs::read(&journal).unwrap()).unwrap();
assert_eq!(persisted.identity.remote_build_id, Some(build));
assert_eq!(persisted.worker_id.as_deref(), Some(worker.as_str()));
assert!(
writer
.admission_reference("other-wrapper", build, &worker)
.is_err()
);
assert!(
writer
.admission_reference(&wrapper, build + 1, &worker)
.is_err()
);
assert!(
writer
.admission_reference(&wrapper, build, &WorkerId::new("other-worker"))
.is_err()
);
writer.record_exit(0).unwrap();
let intent = writer.begin_retry_selection(true).unwrap();
assert!(
writer
.admission_reference(&wrapper, build, &worker)
.is_err()
);
writer.restore_after_unadmitted_retry(intent).unwrap();
assert!(
!writer
.admission_reference(&wrapper, build, &worker)
.unwrap()
.terminal_acknowledged
);
assert!(
writer
.admission_reference(&wrapper, build + 1, &worker)
.is_err()
);
writer.acknowledge_terminal().unwrap();
assert!(
writer
.admission_reference(&wrapper, build, &worker)
.unwrap()
.terminal_acknowledged
);
#[cfg(unix)]
{
use std::os::unix::ffi::OsStringExt;
let mut invalid_path = writer.clone();
invalid_path.path = retained.join(std::ffi::OsString::from_vec(vec![0xff]));
assert!(
invalid_path
.admission_reference(&wrapper, build, &worker)
.is_err()
);
}
assert!(journal.is_file());
}
#[cfg(unix)]
#[test]
#[cfg_attr(target_vendor = "apple", allow(unused_imports, unused_variables))]
fn recovery_journal_reader_keeps_large_receipts_and_rejects_redirected_entries() {
use rustix::fs::{Mode, OFlags};
use std::os::fd::AsFd;
use std::os::unix::fs::FileTypeExt;
let _guard = test_guard!();
let retained = tempfile::tempdir().unwrap().keep();
let journal = retained.join("valid.json");
let writer = queued_selection_test_lease(journal.clone());
writer
.set_recovery(serde_json::json!({"retained_fixture": "x".repeat(2 * 1024 * 1024)}))
.unwrap();
let original = std::fs::read(&journal).unwrap();
let wrapper = writer.wrapper_id();
let read = DurableLeaseWriter::read_journal(&journal, &wrapper).unwrap();
assert_eq!(read, writer.snapshot());
assert!(DurableLeaseWriter::read_journal(&journal, "different-wrapper").is_err());
let link = retained.join("linked.json");
std::os::unix::fs::symlink(&journal, &link).unwrap();
assert!(DurableLeaseWriter::read_journal(&link, &wrapper).is_err());
let linked_directory = retained.join("linked-directory");
std::os::unix::fs::symlink(&retained, &linked_directory).unwrap();
assert!(
DurableLeaseWriter::read_journal(&linked_directory.join("valid.json"), &wrapper).is_err()
);
let wrong_schema = retained.join("wrong-schema.json");
let mut changed = read;
changed.schema_version = 2;
std::fs::write(&wrong_schema, serde_json::to_vec(&changed).unwrap()).unwrap();
assert!(DurableLeaseWriter::read_journal(&wrong_schema, &wrapper).is_err());
let directory =
rustix::fs::open(&retained, OFlags::RDONLY | OFlags::DIRECTORY, Mode::empty()).unwrap();
#[cfg(not(target_vendor = "apple"))]
rustix::fs::mkfifoat(directory.as_fd(), c"fifo.json", Mode::RUSR | Mode::WUSR).unwrap();
let fifo = retained.join("fifo.json");
#[cfg(target_vendor = "apple")]
assert!(
std::process::Command::new("/usr/bin/mkfifo")
.args(["-m", "600"])
.arg(&fifo)
.status()
.unwrap()
.success()
);
let (sender, receiver) = std::sync::mpsc::channel();
let fifo_read = fifo.clone();
let reader = std::thread::spawn(move || {
sender
.send(DurableLeaseWriter::read_journal(&fifo_read, &wrapper))
.unwrap();
});
let error = receiver
.recv_timeout(std::time::Duration::from_secs(2))
.expect("recovery journal read blocked on a FIFO with no writer")
.unwrap_err();
reader.join().unwrap();
assert!(format!("{error:#}").contains("journal is not a regular file"));
assert!(
std::fs::symlink_metadata(fifo)
.unwrap()
.file_type()
.is_fifo()
);
assert_eq!(std::fs::read(&journal).unwrap(), original);
assert_eq!(std::fs::read_link(link).unwrap(), journal);
}
#[test]
fn terminal_job_report_uses_one_current_admission_and_observed_exit() {
let _guard = test_guard!();
let retained = tempfile::tempdir().unwrap().keep();
let writer = queued_selection_test_lease(retained.join("job.json"));
let wrapper = writer.wrapper_id();
assert!(
writer
.terminal_job_report(&wrapper, "completed", None)
.is_err()
);
let build = 9_007_199_254_740_993_u64;
let worker = WorkerId::new("recovery-worker-fixture");
writer.admit(build, &worker).unwrap();
writer.record_exit(102).unwrap();
assert!(
writer
.terminal_job_report(&wrapper, "recovered", Some(102))
.is_err()
);
writer.acknowledge_terminal().unwrap();
let report = writer
.terminal_job_report(&wrapper, "recovered", Some(102))
.unwrap();
assert_eq!(report["identity"]["remote_build_id"], build);
assert_eq!(report["admission"]["remote_build_id"], "9007199254740993");
assert_eq!(report["admission"]["worker_id"], worker.as_str());
assert_eq!(report["admission"]["terminal_acknowledged"], true);
assert_eq!(report["exit_code"], 102);
assert_eq!(
report["admission"]["local_wrapper_id"],
report["identity"]["local_wrapper_id"]
);
assert!(
writer
.terminal_job_report(&wrapper, "recovered", Some(0))
.is_err()
);
assert!(
writer
.terminal_job_report("other-wrapper", "completed", None)
.is_err()
);
assert!(
writer
.terminal_job_report(&wrapper, "active", None)
.is_err()
);
let unadmitted = queued_selection_test_lease(retained.join("unadmitted.json"));
let unadmitted_wrapper = unadmitted.wrapper_id();
unadmitted.record_exit(EXIT_BUILD_ERROR).unwrap();
unadmitted.acknowledge_terminal().unwrap();
let no_admission = unadmitted
.terminal_job_report(&unadmitted_wrapper, "completed", Some(EXIT_BUILD_ERROR))
.unwrap();
assert!(no_admission["admission"].is_null());
assert!(no_admission["identity"]["remote_build_id"].is_null());
let missing_exit = queued_selection_test_lease(retained.join("missing-exit.json"));
let missing_bytes = std::fs::read(&missing_exit.path).unwrap();
assert!(missing_exit.acknowledge_terminal().is_err());
assert_eq!(std::fs::read(&missing_exit.path).unwrap(), missing_bytes);
let mut legacy = missing_exit.snapshot();
legacy.acknowledge_terminal(1);
let legacy_path = retained.join("legacy-unknown-exit.json");
std::fs::write(&legacy_path, serde_json::to_vec(&legacy).unwrap()).unwrap();
let legacy_writer = DurableLeaseWriter {
lease: Arc::new(std::sync::Mutex::new(
DurableLeaseWriter::read_journal(&legacy_path, &legacy.identity.local_wrapper_id)
.unwrap(),
)),
path: legacy_path,
#[cfg(unix)]
recovery_ownership: None,
};
assert!(
legacy_writer
.terminal_job_report(&missing_exit.wrapper_id(), "completed", None)
.is_err()
);
for admitted in [true, false] {
let mut incomplete = writer.snapshot();
if admitted {
incomplete.worker_id = None;
} else {
incomplete.identity.remote_build_id = None;
}
let invalid = DurableLeaseWriter {
path: retained.join("incomplete-admission-fixture.json"),
#[cfg(unix)]
recovery_ownership: None,
lease: Arc::new(std::sync::Mutex::new(incomplete)),
};
assert!(
invalid
.terminal_job_report(&wrapper, "completed", None)
.is_err()
);
}
let original = std::fs::read(&writer.path).unwrap();
assert_eq!(
writer
.terminal_job_report(&wrapper, "recovered", Some(102))
.unwrap(),
report
);
assert_eq!(std::fs::read(&writer.path).unwrap(), original);
}
fn retry_selection_test_lease(path: PathBuf) -> DurableLeaseWriter {
let writer = queued_selection_test_lease(path);
writer.admit(41, &WorkerId::new("previous-worker")).unwrap();
writer
.set_recovery(serde_json::json!({
"retired": true, "returned": 137,
"wrapper_id": writer.wrapper_id(), "build_id": 41,
"worker": {"id": "previous-worker"},
}))
.unwrap();
writer.record_exit(137).unwrap();
writer
}
#[test]
fn terminal_acknowledgement_requires_an_observed_result_and_preserves_its_final_value() {
let _guard = test_guard!();
let retained = tempfile::tempdir().unwrap().keep();
for exit in [0, EXIT_BUILD_ERROR, 102, 130] {
let writer = queued_selection_test_lease(retained.join(format!("exit-{exit}.json")));
let pending = writer.snapshot();
let bytes = std::fs::read(&writer.path).unwrap();
assert!(writer.acknowledge_terminal().is_err());
assert_eq!(writer.snapshot(), pending);
assert_eq!(std::fs::read(&writer.path).unwrap(), bytes);
writer.record_exit(exit).unwrap();
writer.acknowledge_terminal().unwrap();
let terminal = writer.snapshot();
let bytes = std::fs::read(&writer.path).unwrap();
assert!(terminal.terminal_acknowledged);
assert_eq!(terminal.exit_code, Some(exit));
writer.record_exit(exit).unwrap();
assert_eq!(std::fs::read(&writer.path).unwrap(), bytes);
assert!(writer.record_exit(exit + 1).is_err());
assert_eq!(writer.snapshot(), terminal);
assert_eq!(std::fs::read(&writer.path).unwrap(), bytes);
}
}
#[test]
fn daemon_completion_cannot_replace_known_command_or_delivery_results() {
let _guard = test_guard!();
let retained = tempfile::tempdir().unwrap().keep();
let writer = queued_selection_test_lease(retained.join("known-command.json"));
writer.admit(42, &WorkerId::new("worker-fixture")).unwrap();
writer.record_exit(102).unwrap();
let observed = writer.snapshot();
let bytes = std::fs::read(&writer.path).unwrap();
assert!(
writer
.acknowledge_observed_completion(&observed, 0)
.is_err()
);
assert_eq!(writer.snapshot(), observed);
assert_eq!(std::fs::read(&writer.path).unwrap(), bytes);
writer
.acknowledge_observed_completion(&observed, 102)
.unwrap();
assert_eq!(writer.snapshot().exit_code, Some(102));
assert!(writer.snapshot().terminal_acknowledged);
let delivery = queued_selection_test_lease(retained.join("known-delivery.json"));
delivery
.admit(43, &WorkerId::new("worker-fixture"))
.unwrap();
delivery
.set_recovery(serde_json::json!({
"returned": 102, "retired": true, "daemon_exit_code": 0,
}))
.unwrap();
let observed = delivery.snapshot();
let bytes = std::fs::read(&delivery.path).unwrap();
assert!(
delivery
.acknowledge_observed_completion(&observed, 0)
.is_err()
);
assert_eq!(delivery.snapshot(), observed);
assert_eq!(std::fs::read(&delivery.path).unwrap(), bytes);
delivery.record_exit(0).unwrap();
let wrong_result = delivery.snapshot();
let wrong_bytes = std::fs::read(&delivery.path).unwrap();
assert!(delivery.acknowledge_terminal().is_err());
assert_eq!(delivery.snapshot(), wrong_result);
assert_eq!(std::fs::read(&delivery.path).unwrap(), wrong_bytes);
delivery.record_exit(102).unwrap();
delivery.acknowledge_terminal().unwrap();
let terminal = delivery.snapshot();
assert_eq!(terminal.exit_code, Some(102));
assert!(terminal.terminal_acknowledged);
assert_eq!(terminal.recovery.unwrap()["daemon_exit_code"], 0);
let receipt = queued_selection_test_lease(retained.join("retired-receipt.json"));
receipt.admit(44, &WorkerId::new("worker-fixture")).unwrap();
receipt
.set_recovery(serde_json::json!({
"returned": 130, "retired": true, "daemon_exit_code": 0,
}))
.unwrap();
assert!(receipt.snapshot().exit_code.is_none());
receipt.acknowledge_terminal().unwrap();
let terminal = receipt.snapshot();
let disk: DurableJobLease =
serde_json::from_slice(&std::fs::read(&receipt.path).unwrap()).unwrap();
assert_eq!(disk, terminal);
assert_eq!(terminal.exit_code, Some(130));
assert!(terminal.terminal_acknowledged);
assert_eq!(terminal.recovery.unwrap()["daemon_exit_code"], 0);
let unfinished = queued_selection_test_lease(retained.join("unfinished-receipt.json"));
for recipe in [
serde_json::json!({"returned": 130, "retired": false}),
serde_json::json!({"retired": true}),
serde_json::json!({"returned": i64::from(i32::MAX) + 1, "retired": true}),
] {
unfinished.set_recovery(recipe).unwrap();
let before = unfinished.snapshot();
let bytes = std::fs::read(&unfinished.path).unwrap();
assert!(unfinished.acknowledge_terminal().is_err());
assert_eq!(unfinished.snapshot(), before);
assert_eq!(std::fs::read(&unfinished.path).unwrap(), bytes);
}
}
fn read_retry_lease(writer: &DurableLeaseWriter) -> DurableJobLease {
serde_json::from_slice(&std::fs::read(&writer.path).unwrap()).unwrap()
}
fn retry_unassigned_response() -> SelectionResponse {
SelectionResponse {
worker: None,
build_id: None,
reason: SelectionReason::AllWorkersBusy,
diagnostics: None,
}
}
fn assert_retry_pending(writer: &DurableLeaseWriter, previous: &DurableJobLease) {
let disk = read_retry_lease(writer);
assert_eq!(disk, writer.snapshot());
assert_eq!(disk.phase, "selection_pending");
assert_eq!(
disk.state,
rch_common::job_identity::JobLifecycleState::Queued
);
assert_eq!(
disk.identity.local_wrapper_id,
previous.identity.local_wrapper_id
);
assert_eq!(disk.wrapper_pid, previous.wrapper_pid);
assert_eq!(disk.process_birth, previous.process_birth);
assert_eq!(disk.process_start_ticks, previous.process_start_ticks);
assert_eq!(disk.boot_id, previous.boot_id);
assert_eq!(disk.command_fingerprint, previous.command_fingerprint);
assert_eq!(disk.strict_remote, previous.strict_remote);
assert_eq!(disk.self_healing_enabled, previous.self_healing_enabled);
assert!(disk.identity.remote_build_id.is_none());
assert!(disk.worker_id.is_none());
assert!(disk.recovery.is_none());
assert!(disk.exit_code.is_none());
assert!(!disk.terminal_acknowledged);
}
#[tokio::test]
async fn retry_selection_unconfirmed_retains_new_intent_and_never_exhausts() {
let directory = tempfile::tempdir().unwrap();
let writer = retry_selection_test_lease(directory.path().join("lease.json"));
let previous = writer.snapshot();
let mut exhausted = false;
let result: anyhow::Result<()> = async {
dispatch_retry_selection(&writer, true, async || {
assert_retry_pending(&writer, &previous);
anyhow::bail!(
anyhow::anyhow!("reply lost after admission")
.context(daemon_ipc::SelectionOutcomeUnconfirmed)
.context("outer transport context")
);
})
.await?;
exhausted = true;
writer.acknowledge_terminal()?;
Ok(())
}
.await;
assert!(
result
.unwrap_err()
.downcast_ref::<daemon_ipc::SelectionOutcomeUnconfirmed>()
.is_some()
);
assert!(
!exhausted,
"uncertain selection must not reach fault.on_exhaust"
);
let disk = read_retry_lease(&writer);
assert_eq!(disk, writer.snapshot());
assert_eq!(disk.phase, "selection_unconfirmed");
assert!(disk.identity.remote_build_id.is_none());
assert!(disk.worker_id.is_none());
assert!(disk.recovery.is_none());
assert!(disk.exit_code.is_none());
assert!(!disk.terminal_acknowledged);
assert!(writer.acknowledge_terminal().is_err());
assert!(writer.record_exit(137).is_err());
assert!(writer.heartbeat("finalize").is_err());
assert!(writer.set_recovery(previous.recovery.unwrap()).is_err());
assert_eq!(read_retry_lease(&writer), disk);
assert!(
dispatch_retry_selection(&writer, true, async || {
panic!("an uncertain selection cannot dispatch again");
})
.await
.is_err()
);
}
#[tokio::test]
async fn retry_selection_definitive_refusal_restores_previous_settled_evidence() {
for preconnect_error in [false, true] {
let directory = tempfile::tempdir().unwrap();
let writer = retry_selection_test_lease(directory.path().join("lease.json"));
let previous = writer.snapshot();
let bytes = std::fs::read(&writer.path).unwrap();
let response = dispatch_retry_selection(&writer, true, async || {
assert_retry_pending(&writer, &previous);
if preconnect_error {
anyhow::bail!(std::io::Error::new(
std::io::ErrorKind::NotFound,
"socket absent before dispatch"
));
}
Ok(retry_unassigned_response())
})
.await
.unwrap();
assert_eq!(response.is_none(), preconnect_error);
assert_eq!(writer.snapshot(), previous);
assert_eq!(std::fs::read(&writer.path).unwrap(), bytes);
writer.acknowledge_terminal().unwrap();
assert_eq!(read_retry_lease(&writer).exit_code, Some(137));
}
}
#[tokio::test]
async fn retry_selection_selected_response_stays_pending_until_new_admission() {
let directory = tempfile::tempdir().unwrap();
let writer = retry_selection_test_lease(directory.path().join("lease.json"));
let previous = writer.snapshot();
let response = dispatch_retry_selection(&writer, true, async || {
assert_retry_pending(&writer, &previous);
Ok(SelectionResponse {
worker: Some(rch_common::SelectedWorker {
id: WorkerId::new("next-worker"),
host: "not-contacted.invalid".into(),
user: "test".into(),
identity_file: "/unused/key".into(),
slots_available: 4,
speed_score: 90.0,
declared_os: None,
}),
build_id: Some(42),
reason: SelectionReason::Success,
diagnostics: None,
})
})
.await
.unwrap()
.unwrap();
assert_retry_pending(&writer, &previous);
assert!(writer.acknowledge_terminal().is_err());
writer
.admit(response.build_id.unwrap(), &response.worker.unwrap().id)
.unwrap();
let disk = read_retry_lease(&writer);
assert_eq!(
disk.identity.local_wrapper_id,
previous.identity.local_wrapper_id
);
assert_eq!(disk.identity.remote_build_id, Some(42));
assert_eq!(disk.worker_id.as_deref(), Some("next-worker"));
assert_eq!(disk.phase, "sync_up");
assert!(disk.recovery.is_none() && disk.exit_code.is_none());
assert!(!disk.terminal_acknowledged);
assert!(writer.confirm_selection_cancelled().is_err());
}
#[tokio::test]
async fn retry_selection_cancelled_response_requires_explicit_terminal_receipt() {
let directory = tempfile::tempdir().unwrap();
let writer = retry_selection_test_lease(directory.path().join("lease.json"));
let previous = writer.snapshot();
let response = dispatch_retry_selection(&writer, true, async || {
assert_retry_pending(&writer, &previous);
Ok(SelectionResponse {
reason: SelectionReason::SelectionError("job_cancelled_before_start".into()),
..retry_unassigned_response()
})
})
.await
.unwrap()
.unwrap();
assert!(selection_cancelled_before_start(&response));
assert_retry_pending(&writer, &previous);
assert!(writer.acknowledge_terminal().is_err());
writer.confirm_selection_cancelled().unwrap();
let disk = read_retry_lease(&writer);
assert_eq!(disk.exit_code, Some(130));
assert_eq!(disk.phase, "finished");
assert!(disk.terminal_acknowledged);
assert!(disk.identity.remote_build_id.is_none() && disk.worker_id.is_none());
assert!(writer.heartbeat("selection_unconfirmed").is_err());
assert_eq!(read_retry_lease(&writer), disk);
}
#[test]
fn a_selection_refusal_retires_only_an_unadmitted_lease() {
use rch_common::job_identity::JobLifecycleState;
let directory = tempfile::tempdir().unwrap();
let writer = queued_selection_test_lease(directory.path().join("lease.json"));
writer.confirm_failed_before_admission().unwrap();
let disk = read_retry_lease(&writer);
assert_eq!(disk.state, JobLifecycleState::FailedBeforeAdmission);
assert!(disk.state.is_terminal() && disk.terminal_acknowledged);
assert_eq!(disk.phase, "failed_before_admission");
assert!(disk.exit_code.is_none(), "no remote command ran");
assert!(disk.identity.remote_build_id.is_none() && disk.worker_id.is_none());
assert!(writer.heartbeat("admission").is_err());
assert!(writer.confirm_failed_before_admission().is_err());
assert!(writer.confirm_selection_cancelled().is_err());
assert_eq!(read_retry_lease(&writer), disk);
let admitted = queued_selection_test_lease(directory.path().join("admitted.json"));
admitted
.admit(42, &WorkerId::new("admitted-worker"))
.unwrap();
let before = read_retry_lease(&admitted);
assert!(admitted.confirm_failed_before_admission().is_err());
assert_eq!(read_retry_lease(&admitted), before);
}
#[tokio::test]
async fn a_refused_retry_selection_keeps_the_previous_admission() {
let directory = tempfile::tempdir().unwrap();
let writer = retry_selection_test_lease(directory.path().join("lease.json"));
let previous = writer.snapshot();
let response = dispatch_retry_selection(&writer, true, async || {
Ok(SelectionResponse {
reason: SelectionReason::SelectionError("queue_timeout".into()),
..retry_unassigned_response()
})
})
.await
.unwrap()
.unwrap();
assert!(response.worker.is_none());
assert!(writer.confirm_failed_before_admission().is_err());
assert_eq!(read_retry_lease(&writer), previous);
}
#[test]
fn retry_selection_old_observed_completion_cannot_acknowledge_new_admission() {
let directory = tempfile::tempdir().unwrap();
let writer = retry_selection_test_lease(directory.path().join("lease.json"));
let observed = writer.snapshot();
writer.begin_retry_selection(true).unwrap();
let pending = std::fs::read(&writer.path).unwrap();
assert!(
writer
.acknowledge_observed_completion(&observed, 137)
.is_err()
);
assert_eq!(std::fs::read(&writer.path).unwrap(), pending);
writer.admit(42, &WorkerId::new("next-worker")).unwrap();
writer
.set_recovery(serde_json::json!({"retired": true, "returned": 0}))
.unwrap();
let next = writer.snapshot();
let bytes = std::fs::read(&writer.path).unwrap();
assert!(
writer
.acknowledge_observed_completion(&observed, 137)
.is_err()
);
assert_eq!(writer.snapshot(), next);
assert_eq!(std::fs::read(&writer.path).unwrap(), bytes);
assert_eq!(next.identity.remote_build_id, Some(42));
assert!(next.exit_code.is_none() && !next.terminal_acknowledged);
writer.acknowledge_observed_completion(&next, 0).unwrap();
let finished = read_retry_lease(&writer);
assert_eq!(finished.identity, next.identity);
assert_eq!(finished.worker_id, next.worker_id);
assert_eq!(finished.exit_code, Some(0));
assert!(finished.terminal_acknowledged);
}
#[test]
fn retry_selection_observed_completion_requires_worker_and_delivery_evidence() {
let directory = tempfile::tempdir().unwrap();
let writer = retry_selection_test_lease(directory.path().join("lease.json"));
let observed = writer.snapshot();
let mut wrong_worker = observed.clone();
wrong_worker.worker_id = Some("other-worker".into());
assert!(
writer
.acknowledge_observed_completion(&wrong_worker, 137)
.is_err()
);
assert_eq!(read_retry_lease(&writer), observed);
for recipe in [
serde_json::json!({"retired": false, "returned": 137}),
serde_json::json!({"retired": true}),
] {
writer.set_recovery(recipe).unwrap();
let before = writer.snapshot();
assert!(
writer
.acknowledge_observed_completion(&observed, 137)
.is_err()
);
assert_eq!(writer.snapshot(), before);
assert_eq!(read_retry_lease(&writer), before);
}
writer
.set_recovery(observed.recovery.clone().unwrap())
.unwrap();
writer
.acknowledge_observed_completion(&observed, 137)
.unwrap();
let bytes = std::fs::read(&writer.path).unwrap();
writer
.acknowledge_observed_completion(&observed, 137)
.unwrap();
assert_eq!(std::fs::read(&writer.path).unwrap(), bytes);
assert!(
writer
.acknowledge_observed_completion(&observed, 130)
.is_err()
);
assert_eq!(std::fs::read(&writer.path).unwrap(), bytes);
}
#[tokio::test]
async fn retry_selection_requires_both_release_and_source_retirement_before_dispatch() {
for (release_acknowledged, source_retired) in [(false, true), (true, false)] {
let directory = tempfile::tempdir().unwrap();
let writer = retry_selection_test_lease(directory.path().join("lease.json"));
if !source_retired {
let mut recipe = writer.snapshot().recovery.unwrap();
recipe["retired"] = serde_json::Value::Bool(false);
writer.set_recovery(recipe).unwrap();
}
let previous = writer.snapshot();
let bytes = std::fs::read(&writer.path).unwrap();
let result = dispatch_retry_selection(&writer, release_acknowledged, async || {
panic!("unreleased prior ownership must prevent query dispatch");
})
.await;
assert!(result.is_err());
assert_eq!(writer.snapshot(), previous);
assert_eq!(std::fs::read(&writer.path).unwrap(), bytes);
}
}
#[tokio::test]
async fn retry_selection_failed_intent_publication_does_not_dispatch_or_change_memory() {
let directory = tempfile::tempdir().unwrap();
let writer = retry_selection_test_lease(directory.path().join("lease.json"));
let previous = writer.snapshot();
let bytes = std::fs::read(&writer.path).unwrap();
let blocked = DurableLeaseWriter {
path: directory.path().to_path_buf(),
#[cfg(unix)]
recovery_ownership: None,
lease: writer.lease.clone(),
};
let result = dispatch_retry_selection(&blocked, true, async || {
panic!("failed durable intent must prevent network dispatch");
})
.await;
assert!(result.is_err());
assert_eq!(writer.snapshot(), previous);
assert_eq!(std::fs::read(&writer.path).unwrap(), bytes);
}
#[tokio::test]
async fn retry_selection_late_refusal_cannot_restore_over_confirmed_cancellation() {
let directory = tempfile::tempdir().unwrap();
let writer = retry_selection_test_lease(directory.path().join("lease.json"));
let result = dispatch_retry_selection(&writer, true, async || {
writer.confirm_selection_cancelled().unwrap();
Ok(retry_unassigned_response())
})
.await;
assert!(result.is_err());
let disk = read_retry_lease(&writer);
assert_eq!(disk, writer.snapshot());
assert!(disk.terminal_acknowledged);
assert_eq!(disk.exit_code, Some(130));
assert!(disk.identity.remote_build_id.is_none());
assert!(disk.recovery.is_none());
}
#[tokio::test]
async fn retry_selection_dropped_query_leaves_durable_pending_without_old_receipt() {
let directory = tempfile::tempdir().unwrap();
let writer = retry_selection_test_lease(directory.path().join("lease.json"));
let previous = writer.snapshot();
let result = tokio::time::timeout(
Duration::from_millis(10),
dispatch_retry_selection(&writer, true, async || {
assert_retry_pending(&writer, &previous);
std::future::pending().await
}),
)
.await;
assert!(result.is_err());
assert_retry_pending(&writer, &previous);
assert!(writer.heartbeat("finalize").is_err());
assert!(writer.record_exit(137).is_err());
assert!(
writer
.set_recovery(previous.recovery.clone().unwrap())
.is_err()
);
assert!(writer.acknowledge_terminal().is_err());
}
#[cfg(unix)]
#[tokio::test]
async fn retry_failover_socket_preserves_endpoint_and_admission_constraints() {
let _guard = test_guard!();
let directory = tempfile::Builder::new()
.prefix("rch-retry-")
.tempdir()
.unwrap()
.keep();
let socket = directory.join("selected.sock");
let listener = UnixListener::bind(&socket).unwrap();
let mut config = rch_common::RchConfig::default();
config.general.socket_path = directory.join("wrong.sock").display().to_string();
let _config_guard = ConfigOverrideGuard::set(config);
let writer = retry_selection_test_lease(directory.join("lease.json"));
let previous = writer.snapshot();
let server_writer = writer.clone();
let toolchain = ToolchainInfo::new(
"nightly",
Some("2026-10-07".into()),
"fixture toolchain identity",
);
let server_toolchain = toolchain.clone();
let status = serde_json::json!({
"daemon": {"pid": 9, "uptime_secs": 1, "version": "retry-endpoint", "socket_path": socket,
"started_at": "2026-10-07T00:00:00Z", "workers_total": 1, "workers_healthy": 1,
"slots_total": 16, "slots_available": 16},
"workers": [{"id": "next-worker", "host": "unused.invalid", "user": "test",
"status": "healthy", "circuit_state": "closed", "used_slots": 0,
"total_slots": 16, "speed_score": 90.0, "last_error": null}],
"active_builds": [], "recent_builds": [], "issues": [],
"stats": {"total_builds": 0, "success_count": 0, "failure_count": 0,
"remote_count": 0, "local_count": 0, "avg_duration_ms": 0}
});
let server = async move {
for attempt in 0..2 {
let (stream, _) = listener.accept().await.unwrap();
let (reader, mut output) = stream.into_split();
let mut reader = TokioBufReader::new(reader);
let mut request = String::new();
reader.read_line(&mut request).await.unwrap();
let body = if attempt == 0 {
assert_eq!(request, "GET /status\n");
status.to_string()
} else {
assert_retry_pending(&server_writer, &previous);
assert!(request.starts_with("GET /select-verified-tools/disk-budget?"));
for parameter in [
"&cores=3",
"&disk_headroom_gib=12",
"&runtime=rust",
"&job_mode=1",
"&require_tool=git",
"&require_tool=cmake",
"&worker=next-worker",
"&preferred_workers=next-worker",
] {
assert!(
request.contains(parameter),
"missing {parameter}: {request}"
);
}
assert!(
request.contains(&format!("&local_wrapper_id={}", server_writer.wrapper_id()))
);
let encoded_toolchain =
urlencoding_encode(&serde_json::to_string(&server_toolchain).unwrap());
assert!(request.contains(&format!("&toolchain={encoded_toolchain}")));
assert!(!request.contains("&wait=1"));
serde_json::to_string(&SelectionResponse {
reason: SelectionReason::SelectionError("job_cancelled_before_start".into()),
..retry_unassigned_response()
})
.unwrap()
};
output
.write_all(format!("HTTP/1.0 200 OK\r\n\r\n{body}").as_bytes())
.await
.unwrap();
output.shutdown().await.unwrap();
}
};
let client = async {
try_retry_on_bigger_worker(
socket.to_str().unwrap(),
"job-project",
3,
12,
"git status",
Some(&toolchain),
RequiredRuntime::Rust,
CommandPriority::Normal,
&[WorkerId::new("previous-worker")],
&[WorkerId::new("next-worker")],
true,
&["git".into(), "cmake".into()],
&writer,
true,
&HookReporter::new(OutputVisibility::None),
)
.await
};
let (response, ()) = timeout(Duration::from_secs(3), async {
tokio::join!(client, server)
})
.await
.unwrap();
let response = response.unwrap().unwrap();
assert!(selection_cancelled_before_start(&response.0));
assert_eq!(response.1, WorkerId::new("next-worker"));
assert_eq!(read_retry_lease(&writer).phase, "selection_pending");
}
#[tokio::test]
async fn queued_selection_lost_or_malformed_response_never_reaches_recovery() {
let _guard = test_guard!();
for wire_response in [
"", "HTTP/1.0 200 OK\r\n\r\nnot-json",
"HTTP/1.0 200 OK\r\n\r\n{}",
"HTTP/1.0 503 Unavailable\r\n\r\n{}",
"HTTP/1.0 200 OK\r\n", ] {
let retained = tempfile::tempdir().unwrap().keep();
let socket = retained.join("daemon.sock");
let listener = UnixListener::bind(&socket).unwrap();
let resumes_lost_reply = wire_response.is_empty();
let server = async move {
let mut requests = Vec::new();
for reply in std::iter::once(wire_response)
.chain(resumes_lost_reply.then_some("HTTP/1.0 404 Not Found\r\n\r\n{}"))
{
let (stream, _) = listener.accept().await.unwrap();
let (reader, mut writer) = stream.into_split();
let mut reader = TokioBufReader::new(reader);
let mut request = String::new();
reader.read_line(&mut request).await.unwrap();
writer.write_all(reply.as_bytes()).await.unwrap();
writer.shutdown().await.unwrap();
requests.push(request);
}
requests
};
let lease = queued_selection_test_lease(retained.join("lease.json"));
let wrapper = lease.wrapper_id();
let client = async {
query_daemon(
socket.to_str().unwrap(),
"queued",
2,
0,
"cargo build",
None,
RequiredRuntime::None,
CommandPriority::Normal,
0,
Some(std::process::id()),
Some(&wrapper),
true,
&[],
false,
&[],
)
.await
};
let (result, requests) = timeout(Duration::from_secs(2), async {
tokio::join!(client, server)
})
.await
.unwrap();
let error = result.unwrap_err();
assert_eq!(requests.len(), if resumes_lost_reply { 2 } else { 1 });
let request = &requests[0];
assert!(request.contains("&wait=1"));
assert!(request.contains(&format!("local_wrapper_id={wrapper}")));
if resumes_lost_reply {
let query = request.strip_prefix("GET /select-worker?").unwrap();
assert_eq!(
requests[1],
format!("GET /select-worker/resume-queued?{query}"),
"resume changes only the endpoint, never identity or constraints"
);
}
assert!(
error
.downcast_ref::<daemon_ipc::SelectionOutcomeUnconfirmed>()
.is_some()
);
let error = selection_error_for_recovery(error.context("outer caller context"), &lease)
.unwrap_err();
assert!(
error
.downcast_ref::<daemon_ipc::SelectionOutcomeUnconfirmed>()
.is_some()
);
let persisted: DurableJobLease =
serde_json::from_slice(&std::fs::read(&lease.path).unwrap()).unwrap();
assert_eq!(persisted.phase, "selection_unconfirmed");
assert_eq!(persisted.identity.local_wrapper_id, wrapper);
assert!(!persisted.terminal_acknowledged);
assert!(persisted.exit_code.is_none());
assert!(persisted.identity.remote_build_id.is_none());
}
}
#[tokio::test]
async fn queued_selection_preconnect_failure_preserves_recovery() {
let _guard = test_guard!();
let tmp = tempfile::TempDir::new().unwrap();
let absent = tmp.path().join("missing.sock");
let refused = tmp.path().join("refused.sock");
drop(UnixListener::bind(&refused).unwrap());
for socket in [absent, refused] {
let lease = queued_selection_test_lease(tmp.path().join("lease.json"));
let wrapper = lease.wrapper_id();
let error = query_daemon(
socket.to_str().unwrap(),
"queued",
2,
0,
"cargo build",
None,
RequiredRuntime::None,
CommandPriority::Normal,
0,
Some(std::process::id()),
Some(&wrapper),
true,
&[],
false,
&[],
)
.await
.unwrap_err();
assert!(
error
.downcast_ref::<daemon_ipc::SelectionOutcomeUnconfirmed>()
.is_none()
);
let _ = selection_error_for_recovery(error, &lease).unwrap();
assert_eq!(lease.snapshot().phase, "admission");
}
}
#[tokio::test]
async fn test_daemon_query_protocol() {
let socket_path = format!("/tmp/rch_test_daemon_{}.sock", std::process::id());
let _ = std::fs::remove_file(&socket_path);
let listener = UnixListener::bind(&socket_path).expect("Failed to create test socket");
let socket_path_clone = socket_path.clone();
let daemon_handle = tokio::spawn(async move {
let (stream, _) = listener
.accept()
.await
.expect("Failed to accept connection");
let (reader, mut writer) = stream.into_split();
let mut buf_reader = TokioBufReader::new(reader);
let mut request_line = String::new();
buf_reader
.read_line(&mut request_line)
.await
.expect("Failed to read request");
assert!(request_line.starts_with("GET /select-worker"));
assert!(request_line.contains("project="));
assert!(request_line.contains("cores="));
assert!(request_line.contains("command=cargo%20build"));
assert!(request_line.contains("priority=normal"));
let response = SelectionResponse {
worker: Some(SelectedWorker {
id: rch_common::WorkerId::new("mock-worker"),
host: "mock.host.local".to_string(),
user: "mockuser".to_string(),
identity_file: "~/.ssh/mock_key".to_string(),
slots_available: 16,
speed_score: 95.0,
declared_os: None,
}),
reason: SelectionReason::Success,
build_id: None,
diagnostics: None,
};
let body = serde_json::to_string(&response).unwrap();
let http_response = format!(
"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\n\r\n{}",
body.len(),
body
);
writer
.write_all(http_response.as_bytes())
.await
.expect("Failed to write response");
writer.flush().await.expect("Failed to flush response");
});
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
let result = query_daemon(
&socket_path,
"test-project",
4,
0,
"cargo build",
None,
RequiredRuntime::None,
CommandPriority::Normal,
100,
None,
None,
false,
&[],
false,
&[],
)
.await;
daemon_handle.await.expect("Daemon task panicked");
let _ = std::fs::remove_file(&socket_path_clone);
let response = result.expect("Query should succeed");
let worker = response.worker.expect("Should have worker");
assert_eq!(worker.id.as_str(), "mock-worker");
assert_eq!(worker.host, "mock.host.local");
assert_eq!(worker.slots_available, 16);
}
#[tokio::test]
async fn test_daemon_query_sends_preferred_workers() {
let socket_path = format!("/tmp/rch_test_daemon_preferred_{}.sock", std::process::id());
let _ = std::fs::remove_file(&socket_path);
let listener = UnixListener::bind(&socket_path).expect("Failed to create test socket");
let socket_path_clone = socket_path.clone();
let daemon_handle = tokio::spawn(async move {
let (stream, _) = listener
.accept()
.await
.expect("Failed to accept connection");
let (reader, mut writer) = stream.into_split();
let mut buf_reader = TokioBufReader::new(reader);
let mut request_line = String::new();
buf_reader
.read_line(&mut request_line)
.await
.expect("Failed to read request");
assert!(request_line.contains("worker=ts2"));
assert!(request_line.contains("worker=vmi1264463"));
assert!(request_line.contains("preferred_workers=ts2%2Cvmi1264463"));
let response = SelectionResponse {
worker: Some(SelectedWorker {
id: rch_common::WorkerId::new("ts2"),
host: "mock.host.local".to_string(),
user: "mockuser".to_string(),
identity_file: "~/.ssh/mock_key".to_string(),
slots_available: 16,
speed_score: 95.0,
declared_os: None,
}),
reason: SelectionReason::Success,
build_id: None,
diagnostics: None,
};
let body = serde_json::to_string(&response).unwrap();
let http_response = format!(
"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\n\r\n{}",
body.len(),
body
);
writer
.write_all(http_response.as_bytes())
.await
.expect("Failed to write response");
writer.flush().await.expect("Failed to flush response");
});
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
let preferred = vec![
rch_common::WorkerId::new("ts2"),
rch_common::WorkerId::new("vmi1264463"),
];
let result = query_daemon(
&socket_path,
"test-project",
4,
0,
"cargo build",
None,
RequiredRuntime::None,
CommandPriority::Normal,
100,
None,
None,
false,
&preferred,
false,
&[],
)
.await;
daemon_handle.await.expect("Daemon task panicked");
let _ = std::fs::remove_file(&socket_path_clone);
let response = result.expect("Query should succeed");
let worker = response.worker.expect("Should have worker");
assert_eq!(worker.id.as_str(), "ts2");
}
#[tokio::test]
async fn test_daemon_query_releases_worker_outside_requested_set() {
let socket_path = format!(
"/tmp/rch_test_daemon_unrequested_{}_{}.sock",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
);
let _ = std::fs::remove_file(&socket_path);
let listener = UnixListener::bind(&socket_path).expect("bind mock daemon socket");
let socket_path_clone = socket_path.clone();
let daemon_handle = tokio::spawn(async move {
{
let (stream, _) = listener.accept().await.expect("accept selection request");
let (reader, mut writer) = stream.into_split();
let mut reader = TokioBufReader::new(reader);
let mut request_line = String::new();
reader
.read_line(&mut request_line)
.await
.expect("read selection request");
assert!(request_line.contains("worker=requested"));
let response = SelectionResponse {
worker: Some(SelectedWorker {
id: WorkerId::new("unrequested"),
host: "mock.host.local".to_string(),
user: "mockuser".to_string(),
identity_file: "~/.ssh/mock_key".to_string(),
slots_available: 6,
speed_score: 90.0,
declared_os: None,
}),
reason: SelectionReason::Success,
build_id: Some(42),
diagnostics: None,
};
let body = serde_json::to_string(&response).unwrap();
let http_response = format!(
"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\n\r\n{}",
body.len(),
body
);
writer
.write_all(http_response.as_bytes())
.await
.expect("write selection response");
writer.shutdown().await.expect("close selection response");
}
let (stream, _) =
tokio::time::timeout(tokio::time::Duration::from_secs(2), listener.accept())
.await
.expect("client did not release unrequested worker")
.expect("accept release request");
let (reader, mut writer) = stream.into_split();
let mut reader = TokioBufReader::new(reader);
let mut release_line = String::new();
reader
.read_line(&mut release_line)
.await
.expect("read release request");
assert!(release_line.starts_with("POST /release-worker?"));
assert!(release_line.contains("worker=unrequested"));
assert!(release_line.contains("slots=2"));
assert!(release_line.contains("build_id=42"));
assert!(release_line.contains("exit_code=1"));
writer
.write_all(b"HTTP/1.0 200 OK\r\n\r\n{}\n")
.await
.expect("ack release");
writer.flush().await.expect("flush release ack");
});
let response = query_daemon(
&socket_path,
"test-project",
2,
0,
"cargo build",
None,
RequiredRuntime::None,
CommandPriority::Normal,
0,
Some(1234),
None,
false,
&[WorkerId::new("requested")],
false,
&[],
)
.await
.expect("query should return a structured refusal");
daemon_handle.await.expect("mock daemon task panicked");
let _ = std::fs::remove_file(&socket_path_clone);
assert!(response.worker.is_none());
assert_eq!(response.reason, SelectionReason::NoMatchingWorkers);
assert_eq!(response.build_id, None);
}
#[tokio::test]
async fn test_daemon_query_surfaces_unacknowledged_unrequested_worker_release() {
let socket_path = format!(
"/tmp/rch_test_daemon_unrequested_release_failure_{}_{}.sock",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
);
let _ = std::fs::remove_file(&socket_path);
let listener = UnixListener::bind(&socket_path).expect("bind mock daemon socket");
let socket_path_clone = socket_path.clone();
let daemon_handle = tokio::spawn(async move {
{
let (stream, _) = listener.accept().await.expect("accept selection request");
let (reader, mut writer) = stream.into_split();
let mut reader = TokioBufReader::new(reader);
let mut request_line = String::new();
reader
.read_line(&mut request_line)
.await
.expect("read selection request");
let response = SelectionResponse {
worker: Some(SelectedWorker {
id: WorkerId::new("unrequested"),
host: "mock.host.local".to_string(),
user: "mockuser".to_string(),
identity_file: "~/.ssh/mock_key".to_string(),
slots_available: 6,
speed_score: 90.0,
declared_os: None,
}),
reason: SelectionReason::Success,
build_id: Some(43),
diagnostics: None,
};
let body = serde_json::to_string(&response).unwrap();
let http_response = format!(
"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\n\r\n{}",
body.len(),
body
);
writer
.write_all(http_response.as_bytes())
.await
.expect("write selection response");
writer.shutdown().await.expect("close selection response");
}
let (stream, _) =
tokio::time::timeout(tokio::time::Duration::from_secs(2), listener.accept())
.await
.expect("client did not attempt release")
.expect("accept release request");
let (reader, mut writer) = stream.into_split();
let mut reader = TokioBufReader::new(reader);
let mut release_line = String::new();
reader
.read_line(&mut release_line)
.await
.expect("read release request");
assert!(release_line.contains("worker=unrequested"));
assert!(release_line.contains("build_id=43"));
writer
.write_all(b"HTTP/1.0 500 Internal Server Error\r\n\r\n{}\n")
.await
.expect("reject release");
writer.flush().await.expect("flush release rejection");
});
let error = query_daemon(
&socket_path,
"test-project",
2,
0,
"cargo build",
None,
RequiredRuntime::None,
CommandPriority::Normal,
0,
Some(1234),
None,
false,
&[WorkerId::new("requested")],
false,
&[],
)
.await
.expect_err("an unacknowledged release must not look like a safe ordinary refusal (9ec9b32f)");
daemon_handle.await.expect("mock daemon task panicked");
let _ = std::fs::remove_file(&socket_path_clone);
assert!(
error
.downcast_ref::<daemon_ipc::SelectionOutcomeUnconfirmed>()
.is_some()
);
let message = format!("{error:#}");
assert!(
message.contains("release was not acknowledged"),
"{message}"
);
assert!(message.contains("build 43"), "{message}");
}
#[tokio::test]
async fn test_daemon_query_wait_parameters() {
let socket_path = format!("/tmp/rch_test_wait_{}.sock", std::process::id());
let _ = std::fs::remove_file(&socket_path);
let listener = UnixListener::bind(&socket_path).expect("Failed to create test socket");
let expected_wait_timeout_secs = daemon_response_timeout_for(true, None, None)
.as_secs()
.saturating_sub(1)
.max(1);
let socket_path_clone = socket_path.clone();
let daemon_handle = tokio::spawn(async move {
let (stream, _) = listener
.accept()
.await
.expect("Failed to accept connection");
let (reader, mut writer) = stream.into_split();
let mut buf_reader = TokioBufReader::new(reader);
let mut request_line = String::new();
buf_reader
.read_line(&mut request_line)
.await
.expect("Failed to read request");
assert!(request_line.starts_with("GET /select-worker"));
assert!(request_line.contains("wait=1"));
assert!(request_line.contains(&format!("wait_timeout_secs={expected_wait_timeout_secs}")));
let response = SelectionResponse {
worker: Some(SelectedWorker {
id: rch_common::WorkerId::new("mock-worker"),
host: "mock.host.local".to_string(),
user: "mockuser".to_string(),
identity_file: "~/.ssh/mock_key".to_string(),
slots_available: 16,
speed_score: 95.0,
declared_os: None,
}),
reason: SelectionReason::Success,
build_id: None,
diagnostics: None,
};
let body = serde_json::to_string(&response).unwrap();
let http_response = format!(
"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\n\r\n{}",
body.len(),
body
);
writer
.write_all(http_response.as_bytes())
.await
.expect("Failed to write response");
writer.flush().await.expect("Failed to flush response");
});
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
let result = query_daemon(
&socket_path,
"test-project",
4,
0,
"cargo build",
None,
RequiredRuntime::None,
CommandPriority::Normal,
100,
None,
None,
true,
&[],
false,
&[],
)
.await;
daemon_handle.await.expect("Daemon task panicked");
let _ = std::fs::remove_file(&socket_path_clone);
let response = result.expect("Query should succeed");
let worker = response.worker.expect("Should have worker");
assert_eq!(worker.id.as_str(), "mock-worker");
}
#[tokio::test]
async fn test_daemon_query_url_encoding() {
let socket_path = format!("/tmp/rch_test_url_{}.sock", std::process::id());
let _ = std::fs::remove_file(&socket_path);
let listener = UnixListener::bind(&socket_path).expect("Failed to create test socket");
let socket_path_clone = socket_path.clone();
let daemon_handle = tokio::spawn(async move {
let (stream, _) = listener
.accept()
.await
.expect("Failed to accept connection");
let (reader, mut writer) = stream.into_split();
let mut buf_reader = TokioBufReader::new(reader);
let mut request_line = String::new();
buf_reader.read_line(&mut request_line).await.expect("Read");
assert!(request_line.contains("my%20project%2Ftest"));
let response = SelectionResponse {
worker: Some(SelectedWorker {
id: rch_common::WorkerId::new("w1"),
host: "h".to_string(),
user: "u".to_string(),
identity_file: "i".to_string(),
slots_available: 1,
speed_score: 1.0,
declared_os: None,
}),
reason: SelectionReason::Success,
build_id: None,
diagnostics: None,
};
let body = serde_json::to_string(&response).unwrap();
let http = format!("HTTP/1.1 200 OK\r\n\r\n{}", body);
writer.write_all(http.as_bytes()).await.expect("Write");
});
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
let result = query_daemon(
&socket_path,
"my project/test",
2,
0,
"cargo build --release",
None,
RequiredRuntime::None,
CommandPriority::Normal,
150, None,
None,
false,
&[],
false,
&[],
)
.await;
daemon_handle.await.expect("Daemon task");
let _ = std::fs::remove_file(&socket_path_clone);
assert!(result.is_ok());
}
#[tokio::test]
async fn test_fail_open_on_invalid_json() {
let _lock = test_lock().lock().await;
mock::set_mock_enabled_override(Some(false));
let mut config = rch_common::RchConfig::default();
config.general.socket_path = "/tmp/rch-test-no-daemon.sock".to_string();
let _ = std::fs::remove_file(&config.general.socket_path);
let _config_guard = ConfigOverrideGuard::set(config);
let input = HookInput {
tool_name: "Bash".to_string(),
tool_input: ToolInput {
command: "cargo build".to_string(),
description: None,
},
session_id: None,
};
let output = process_hook(input).await;
mock::clear_mock_overrides();
assert!(output.is_allow());
}
#[tokio::test]
async fn test_fail_open_on_config_error() {
let _lock = test_lock().lock().await;
mock::set_mock_enabled_override(Some(false));
let input = HookInput {
tool_name: "Bash".to_string(),
tool_input: ToolInput {
command: "cargo build --release".to_string(),
description: None,
},
session_id: None,
};
let output = process_hook(input).await;
mock::clear_mock_overrides();
assert!(output.is_allow());
}
#[tokio::test]
#[serial(mock_global)]
async fn test_process_hook_remote_success_mocked() {
let _lock = test_lock().lock().await;
let socket_path = format!(
"/tmp/rch_test_hook_success_{}_{}.sock",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
);
let _overrides = TestOverridesGuard::set(
&socket_path,
MockConfig::default(),
MockRsyncConfig::success(),
);
mock::clear_global_invocations();
let response = SelectionResponse {
worker: Some(SelectedWorker {
id: rch_common::WorkerId::new("mock-worker"),
host: "mock.host.local".to_string(),
user: "mockuser".to_string(),
identity_file: "~/.ssh/mock_key".to_string(),
slots_available: 8,
speed_score: 90.0,
declared_os: None,
}),
reason: SelectionReason::Success,
build_id: None,
diagnostics: None,
};
spawn_mock_daemon(&socket_path, response).await;
tokio::time::sleep(tokio::time::Duration::from_millis(25)).await;
let input = HookInput {
tool_name: "Bash".to_string(),
tool_input: ToolInput {
command: "cargo build".to_string(),
description: None,
},
session_id: None,
};
let output: HookOutput = process_hook(input).await;
let _ = std::fs::remove_file(&socket_path);
assert!(output.is_allow());
let cmd = delegated_command(&output);
assert!(
cmd.starts_with("rch exec -- "),
"Modified command should delegate to rch exec: {}",
cmd
);
assert!(
cmd.contains("cargo build"),
"Modified command should contain original command: {}",
cmd
);
let rsync_logs = mock::global_rsync_invocations_snapshot();
let ssh_logs = mock::global_ssh_invocations_snapshot();
assert!(
rsync_logs.is_empty(),
"Hook should not invoke rsync directly"
);
assert!(ssh_logs.is_empty(), "Hook should not invoke SSH directly");
}
#[tokio::test]
#[serial(mock_global)]
async fn test_force_local_allows_even_when_remote_available() {
let _lock = test_lock().lock().await;
let socket_path = format!(
"/tmp/rch_test_hook_force_local_{}_{}.sock",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
);
let _overrides = TestOverridesGuard::set(
&socket_path,
MockConfig::default(),
MockRsyncConfig::success(),
);
mock::clear_global_invocations();
let mut config = rch_common::RchConfig::default();
config.general.socket_path = socket_path.to_string();
config.general.force_local = true;
crate::config::set_test_config_override(Some(config));
let response = SelectionResponse {
worker: Some(SelectedWorker {
id: rch_common::WorkerId::new("mock-worker"),
host: "mock.host.local".to_string(),
user: "mockuser".to_string(),
identity_file: "~/.ssh/mock_key".to_string(),
slots_available: 8,
speed_score: 90.0,
declared_os: None,
}),
reason: SelectionReason::Success,
build_id: None,
diagnostics: None,
};
spawn_mock_daemon(&socket_path, response).await;
tokio::time::sleep(tokio::time::Duration::from_millis(25)).await;
let input = HookInput {
tool_name: "Bash".to_string(),
tool_input: ToolInput {
command: "cargo build".to_string(),
description: None,
},
session_id: None,
};
let output = process_hook(input).await;
let _ = std::fs::remove_file(&socket_path);
assert!(output.is_allow());
let rsync_logs = mock::global_rsync_invocations_snapshot();
let ssh_logs = mock::global_ssh_invocations_snapshot();
assert!(rsync_logs.is_empty());
assert!(ssh_logs.is_empty());
}
#[tokio::test]
#[serial(mock_global)]
async fn test_force_remote_bypasses_confidence_threshold() {
let _lock = test_lock().lock().await;
let socket_path = format!(
"/tmp/rch_test_hook_force_remote_threshold_{}_{}.sock",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
);
let _overrides = TestOverridesGuard::set(
&socket_path,
MockConfig::default(),
MockRsyncConfig::success(),
);
mock::clear_global_invocations();
let classification = classify_command("cargo build");
assert!(classification.is_compilation);
let high_threshold = (classification.confidence + 0.01).min(1.0);
let mut config = rch_common::RchConfig::default();
config.general.socket_path = socket_path.to_string();
config.general.force_remote = true;
config.compilation.confidence_threshold = high_threshold;
crate::config::set_test_config_override(Some(config));
let response = SelectionResponse {
worker: Some(SelectedWorker {
id: rch_common::WorkerId::new("mock-worker"),
host: "mock.host.local".to_string(),
user: "mockuser".to_string(),
identity_file: "~/.ssh/mock_key".to_string(),
slots_available: 8,
speed_score: 90.0,
declared_os: None,
}),
reason: SelectionReason::Success,
build_id: None,
diagnostics: None,
};
spawn_mock_daemon(&socket_path, response).await;
tokio::time::sleep(tokio::time::Duration::from_millis(25)).await;
let input = HookInput {
tool_name: "Bash".to_string(),
tool_input: ToolInput {
command: "cargo build".to_string(),
description: None,
},
session_id: None,
};
let output = process_hook(input).await;
let _ = std::fs::remove_file(&socket_path);
assert!(output.is_allow());
let cmd = delegated_command(&output);
assert!(
cmd.starts_with("rch exec -- "),
"Should delegate to rch exec: {}",
cmd
);
let rsync_logs = mock::global_rsync_invocations_snapshot();
let ssh_logs = mock::global_ssh_invocations_snapshot();
assert!(
rsync_logs.is_empty(),
"Hook should not invoke rsync directly"
);
assert!(ssh_logs.is_empty(), "Hook should not invoke SSH directly");
}
#[tokio::test]
#[serial(mock_global)]
async fn test_process_hook_delegates_to_rch_exec() {
let _lock = test_lock().lock().await;
let socket_path = format!(
"/tmp/rch_test_hook_delegate_{}_{}.sock",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
);
let _overrides = TestOverridesGuard::set(
&socket_path,
MockConfig::default(),
MockRsyncConfig::sync_failure(),
);
mock::clear_global_invocations();
let input = HookInput {
tool_name: "Bash".to_string(),
tool_input: ToolInput {
command: "cargo build".to_string(),
description: None,
},
session_id: None,
};
let output = process_hook(input).await;
let _ = std::fs::remove_file(&socket_path);
assert!(output.is_allow());
let cmd = delegated_command(&output);
assert!(
cmd.starts_with("rch exec -- "),
"Should delegate to rch exec: {}",
cmd
);
let rsync_logs = mock::global_rsync_invocations_snapshot();
let ssh_logs = mock::global_ssh_invocations_snapshot();
assert!(
rsync_logs.is_empty(),
"Hook should not invoke rsync directly"
);
assert!(ssh_logs.is_empty(), "Hook should not invoke SSH directly");
}
#[tokio::test]
#[serial(mock_global)]
async fn test_process_hook_delegates_env_prefixed_cargo_command() {
let _lock = test_lock().lock().await;
let socket_path = format!(
"/tmp/rch_test_hook_delegate_env_{}_{}.sock",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
);
let _overrides = TestOverridesGuard::set(
&socket_path,
MockConfig::default(),
MockRsyncConfig::sync_failure(),
);
mock::clear_global_invocations();
let input = HookInput {
tool_name: "Bash".to_string(),
tool_input: ToolInput {
command: "env RUSTFLAGS=\"-C linker=cc\" cargo build --bin frankenctl".to_string(),
description: None,
},
session_id: None,
};
let output = process_hook(input).await;
let _ = std::fs::remove_file(&socket_path);
let cmd = delegated_command(&output);
assert!(
cmd.starts_with("rch exec -- env "),
"env wrapper must remain an argv prefix in delegated command: {cmd}"
);
let tokens = shell_words::split(
cmd.strip_prefix("rch exec -- ")
.expect("delegated command prefix"),
)
.expect("delegated command should parse as shell words");
assert_eq!(
tokens,
vec![
"env".to_string(),
"RUSTFLAGS=-C linker=cc".to_string(),
"cargo".to_string(),
"build".to_string(),
"--bin".to_string(),
"frankenctl".to_string(),
]
);
assert!(mock::global_rsync_invocations_snapshot().is_empty());
assert!(mock::global_ssh_invocations_snapshot().is_empty());
}
#[tokio::test]
#[serial(mock_global)]
async fn test_process_hook_remote_nonzero_exit_uses_transparent_interception() {
let _lock = test_lock().lock().await;
let socket_path = format!(
"/tmp/rch_test_hook_exit_nonzero_{}_{}.sock",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
);
let _overrides = TestOverridesGuard::set(
&socket_path,
MockConfig {
default_exit_code: 2,
..MockConfig::default()
},
MockRsyncConfig::success(),
);
mock::clear_global_invocations();
let response = SelectionResponse {
worker: Some(SelectedWorker {
id: rch_common::WorkerId::new("mock-worker"),
host: "mock.host.local".to_string(),
user: "mockuser".to_string(),
identity_file: "~/.ssh/mock_key".to_string(),
slots_available: 8,
speed_score: 90.0,
declared_os: None,
}),
reason: SelectionReason::Success,
build_id: None,
diagnostics: None,
};
spawn_mock_daemon(&socket_path, response).await;
tokio::time::sleep(tokio::time::Duration::from_millis(25)).await;
let input = HookInput {
tool_name: "Bash".to_string(),
tool_input: ToolInput {
command: "cargo build".to_string(),
description: None,
},
session_id: None,
};
let output = process_hook(input).await;
let _ = std::fs::remove_file(&socket_path);
assert!(output.is_allow());
assert!(
matches!(output, HookOutput::AllowWithModifiedCommand(_)),
"Expected AllowWithModifiedCommand for remote execution with non-zero exit"
);
}
#[test]
fn test_transfer_config_defaults() {
let _guard = test_guard!();
let config = TransferConfig::default();
assert!(!config.exclude_patterns.is_empty());
assert!(config.exclude_patterns.iter().any(|p| p.contains("target")));
}
#[test]
fn test_worker_config_from_selected_worker() {
let _guard = test_guard!();
let worker = SelectedWorker {
id: rch_common::WorkerId::new("worker-alpha"),
host: "alpha.example.com".to_string(),
user: "deploy".to_string(),
identity_file: "/keys/deploy.pem".to_string(),
slots_available: 32,
speed_score: 88.8,
declared_os: None,
};
let config = selected_worker_to_config(&worker);
assert_eq!(config.id.as_str(), "worker-alpha");
assert_eq!(config.host, "alpha.example.com");
assert_eq!(config.user, "deploy");
assert_eq!(config.identity_file, "/keys/deploy.pem");
assert_eq!(config.total_slots, 32);
assert_eq!(config.priority, 100); assert!(config.tags.is_empty()); }
#[tokio::test]
async fn test_fallback_no_workers_configured() {
let _lock = test_lock().lock().await;
let socket_path = format!(
"/tmp/rch_test_no_workers_{}_{}.sock",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
);
let _overrides = TestOverridesGuard::set(
&socket_path,
MockConfig::default(),
MockRsyncConfig::success(),
);
let response = SelectionResponse {
worker: None,
reason: SelectionReason::NoWorkersConfigured,
build_id: None,
diagnostics: None,
};
spawn_mock_daemon(&socket_path, response).await;
tokio::time::sleep(tokio::time::Duration::from_millis(25)).await;
let input = HookInput {
tool_name: "Bash".to_string(),
tool_input: ToolInput {
command: "cargo build".to_string(),
description: None,
},
session_id: None,
};
let output = process_hook(input).await;
let _ = std::fs::remove_file(&socket_path);
assert!(output.is_allow());
}
#[tokio::test]
async fn test_fallback_all_workers_unreachable() {
let _lock = test_lock().lock().await;
let socket_path = format!(
"/tmp/rch_test_unreachable_{}_{}.sock",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
);
let _overrides = TestOverridesGuard::set(
&socket_path,
MockConfig::default(),
MockRsyncConfig::success(),
);
let response = SelectionResponse {
worker: None,
reason: SelectionReason::AllWorkersUnreachable,
build_id: None,
diagnostics: None,
};
spawn_mock_daemon(&socket_path, response).await;
tokio::time::sleep(tokio::time::Duration::from_millis(25)).await;
let input = HookInput {
tool_name: "Bash".to_string(),
tool_input: ToolInput {
command: "cargo build --release".to_string(),
description: None,
},
session_id: None,
};
let output = process_hook(input).await;
let _ = std::fs::remove_file(&socket_path);
assert!(output.is_allow());
}
#[tokio::test]
async fn test_fallback_all_workers_busy() {
let _lock = test_lock().lock().await;
let socket_path = format!(
"/tmp/rch_test_busy_{}_{}.sock",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
);
let _overrides = TestOverridesGuard::set(
&socket_path,
MockConfig::default(),
MockRsyncConfig::success(),
);
let response = SelectionResponse {
worker: None,
reason: SelectionReason::AllWorkersBusy,
build_id: None,
diagnostics: None,
};
spawn_mock_daemon(&socket_path, response).await;
tokio::time::sleep(tokio::time::Duration::from_millis(25)).await;
let input = HookInput {
tool_name: "Bash".to_string(),
tool_input: ToolInput {
command: "cargo test".to_string(),
description: None,
},
session_id: None,
};
let output = process_hook(input).await;
let _ = std::fs::remove_file(&socket_path);
assert!(output.is_allow());
}
#[tokio::test]
async fn test_fallback_all_circuits_open() {
let _lock = test_lock().lock().await;
let socket_path = format!(
"/tmp/rch_test_circuits_{}_{}.sock",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
);
let _overrides = TestOverridesGuard::set(
&socket_path,
MockConfig::default(),
MockRsyncConfig::success(),
);
let response = SelectionResponse {
worker: None,
reason: SelectionReason::AllCircuitsOpen,
build_id: None,
diagnostics: None,
};
spawn_mock_daemon(&socket_path, response).await;
tokio::time::sleep(tokio::time::Duration::from_millis(25)).await;
let input = HookInput {
tool_name: "Bash".to_string(),
tool_input: ToolInput {
command: "cargo check".to_string(),
description: None,
},
session_id: None,
};
let output = process_hook(input).await;
let _ = std::fs::remove_file(&socket_path);
assert!(output.is_allow());
}
#[tokio::test]
async fn test_fallback_selection_error() {
let _lock = test_lock().lock().await;
let socket_path = format!(
"/tmp/rch_test_sel_err_{}_{}.sock",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
);
let _overrides = TestOverridesGuard::set(
&socket_path,
MockConfig::default(),
MockRsyncConfig::success(),
);
let response = SelectionResponse {
worker: None,
reason: SelectionReason::SelectionError("Internal error".to_string()),
build_id: None,
diagnostics: None,
};
spawn_mock_daemon(&socket_path, response).await;
tokio::time::sleep(tokio::time::Duration::from_millis(25)).await;
let input = HookInput {
tool_name: "Bash".to_string(),
tool_input: ToolInput {
command: "cargo build".to_string(),
description: None,
},
session_id: None,
};
let output = process_hook(input).await;
let _ = std::fs::remove_file(&socket_path);
assert!(output.is_allow());
}
#[tokio::test]
async fn test_fallback_daemon_error_response() {
let _lock = test_lock().lock().await;
let socket_path = format!(
"/tmp/rch_test_daemon_err_{}_{}.sock",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
);
let _overrides = TestOverridesGuard::set(
&socket_path,
MockConfig::default(),
MockRsyncConfig::success(),
);
let _ = std::fs::remove_file(&socket_path);
let listener = UnixListener::bind(&socket_path).expect("bind");
tokio::spawn(async move {
let (stream, _) = listener.accept().await.expect("accept");
let (reader, mut writer) = stream.into_split();
let mut buf_reader = TokioBufReader::new(reader);
let mut request_line = String::new();
buf_reader.read_line(&mut request_line).await.expect("read");
let http =
"HTTP/1.1 500 Internal Server Error\r\n\r\n Расположение: {\"error\": \"internal\"}";
writer.write_all(http.as_bytes()).await.expect("write");
});
tokio::time::sleep(tokio::time::Duration::from_millis(25)).await;
let input = HookInput {
tool_name: "Bash".to_string(),
tool_input: ToolInput {
command: "cargo build".to_string(),
description: None,
},
session_id: None,
};
let output = process_hook(input).await;
let _ = std::fs::remove_file(&socket_path);
assert!(output.is_allow());
}
#[tokio::test]
async fn test_fallback_daemon_malformed_json() {
let _lock = test_lock().lock().await;
let socket_path = format!(
"/tmp/rch_test_malformed_{}_{}.sock",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
);
let _overrides = TestOverridesGuard::set(
&socket_path,
MockConfig::default(),
MockRsyncConfig::success(),
);
let _ = std::fs::remove_file(&socket_path);
let listener = UnixListener::bind(&socket_path).expect("bind");
tokio::spawn(async move {
let (stream, _) = listener.accept().await.expect("accept");
let (reader, mut writer) = stream.into_split();
let mut buf_reader = TokioBufReader::new(reader);
let mut request_line = String::new();
buf_reader.read_line(&mut request_line).await.expect("read");
let http = "HTTP/1.1 200 OK\r\n\r\n{invalid json}";
writer.write_all(http.as_bytes()).await.expect("write");
});
tokio::time::sleep(tokio::time::Duration::from_millis(25)).await;
let input = HookInput {
tool_name: "Bash".to_string(),
tool_input: ToolInput {
command: "cargo build".to_string(),
description: None,
},
session_id: None,
};
let output = process_hook(input).await;
let _ = std::fs::remove_file(&socket_path);
assert!(output.is_allow());
}
#[tokio::test]
async fn test_fallback_daemon_connection_reset() {
let _lock = test_lock().lock().await;
let socket_path = format!(
"/tmp/rch_test_reset_{}_{}.sock",
std::process::id(), std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
);
let _overrides = TestOverridesGuard::set(
&socket_path,
MockConfig::default(),
MockRsyncConfig::success(),
);
let _ = std::fs::remove_file(&socket_path);
let listener = UnixListener::bind(&socket_path).expect("bind");
tokio::spawn(async move {
let (stream, _) = listener.accept().await.expect("accept");
drop(stream);
});
tokio::time::sleep(tokio::time::Duration::from_millis(25)).await;
let input = HookInput {
tool_name: "Bash".to_string(),
tool_input: ToolInput {
command: "cargo build".to_string(),
description: None,
},
session_id: None,
};
let output = process_hook(input).await;
let _ = std::fs::remove_file(&socket_path);
assert!(output.is_allow());
}
#[test]
fn test_is_signal_killed() {
let _guard = test_guard!();
assert!(is_signal_killed(0).is_none());
assert!(is_signal_killed(1).is_none());
assert!(is_signal_killed(101).is_none());
assert!(is_signal_killed(128).is_none());
assert_eq!(is_signal_killed(129), Some(1)); assert_eq!(is_signal_killed(130), Some(2)); assert_eq!(is_signal_killed(137), Some(9)); assert_eq!(is_signal_killed(139), Some(11)); assert_eq!(is_signal_killed(143), Some(15)); }
#[test]
fn test_signal_name() {
let _guard = test_guard!();
assert_eq!(signal_name(1), "SIGHUP");
assert_eq!(signal_name(2), "SIGINT");
assert_eq!(signal_name(4), "SIGILL");
assert_eq!(signal_name(7), "SIGBUS");
assert_eq!(signal_name(9), "SIGKILL");
assert_eq!(signal_name(11), "SIGSEGV");
assert_eq!(signal_name(15), "SIGTERM");
assert_eq!(signal_name(99), "UNKNOWN");
}
#[test]
fn test_sigill_classifies_as_cpu_capability_fault_not_oom() {
let _guard = test_guard!();
use super::remote_result::is_cpu_capability_signal;
assert_eq!(is_signal_killed(132), Some(4));
assert!(is_cpu_capability_signal(4));
assert!(
!is_cpu_capability_signal(9),
"OOM is not a capability fault"
);
assert!(!is_cpu_capability_signal(11));
assert!(
!is_cpu_capability_signal(7),
"SIGBUS is named but not auto-quarantined"
);
}
#[test]
fn test_exit_code_constants() {
let _guard = test_guard!();
assert_eq!(EXIT_SUCCESS, 0);
assert_eq!(EXIT_BUILD_ERROR, 1);
assert_eq!(EXIT_TEST_FAILURES, 101);
assert_eq!(EXIT_SIGNAL_BASE, 128);
}
#[test]
fn test_remote_pipeline_failure_policy_ssh_timeout_fails_closed() {
let _guard = test_guard!();
let error = anyhow::anyhow!("SSH command timed out after 1800s");
assert_eq!(
classify_remote_pipeline_failure(&error),
RemotePipelineFailurePolicy::FailClosedNoLocalFallback
);
}
#[test]
fn test_remote_pipeline_failure_policy_wrapped_ssh_timeout_fails_closed() {
let _guard = test_guard!();
let error =
anyhow::anyhow!("SSH command timed out after 1800s").context("remote execution failed");
assert_eq!(
classify_remote_pipeline_failure(&error),
RemotePipelineFailurePolicy::FailClosedNoLocalFallback
);
}
#[test]
fn test_remote_pipeline_failure_policy_non_timeout_allows_existing_fallback() {
let _guard = test_guard!();
let error = anyhow::anyhow!("rsync failed before remote execution");
assert_eq!(
classify_remote_pipeline_failure(&error),
RemotePipelineFailurePolicy::AllowLocalFallback
);
}
#[test]
fn test_unacknowledged_release_is_a_typed_no_replay_error() {
let _guard = test_guard!();
let error = release_unconfirmed_error(&WorkerId::new("worker-a"), 42);
assert!(is_remote_execution_unconfirmed(&error));
assert_eq!(
classify_remote_pipeline_failure(&error),
RemotePipelineFailurePolicy::FailClosedNoLocalFallback
);
let message = format!("{error:#}");
assert!(message.contains("build 42"));
assert!(message.contains("worker-a"));
assert!(message.contains("must not be replayed"));
}
#[test]
fn test_unconfirmed_remote_execution_retains_ownership_and_never_falls_back() {
let _guard = test_guard!();
for error in [
anyhow::Error::new(crate::transfer::RemoteExecutionUnconfirmed),
anyhow::anyhow!("source authority holder exited")
.context(crate::transfer::RemoteExecutionUnconfirmed)
.context("remote pipeline failed"),
anyhow::anyhow!("missing durable completion receipt")
.context(crate::transfer::RemoteExecutionUnconfirmed),
anyhow::Error::new(TransferError::TransferSkipped {
reason: "transfer budget".to_owned(),
})
.context(crate::transfer::RemoteExecutionUnconfirmed),
] {
assert!(is_remote_execution_unconfirmed(&error));
assert_eq!(
classify_remote_pipeline_failure(&error),
RemotePipelineFailurePolicy::FailClosedNoLocalFallback
);
let summary = remote_pipeline_failure_summary(&WorkerId::new("worker-a"), &error);
assert!(summary.contains("ownership retained for recovery"));
assert!(!summary.contains("SSH command timed out"));
}
let setup = anyhow::Error::new(crate::transfer::RemoteProcessSetupUnavailable)
.context("verified pre-workload refusal");
assert!(!is_remote_execution_unconfirmed(&setup));
assert_eq!(
classify_remote_pipeline_failure(&setup),
RemotePipelineFailurePolicy::AllowLocalFallback,
"verified setup refusal can release ownership and run locally"
);
}
#[test]
fn test_source_sync_stall_is_not_misclassified_as_ssh_timeout() {
let _guard = test_guard!();
let error: anyhow::Error = crate::transfer::SourceSyncStalled {
worker_id: "hz4".to_string(),
phase: "source_sync",
silence: std::time::Duration::from_secs(120),
detail: "sync_to_remote_streaming: no rsync output for 120s (wall-clock cap 3600s)"
.to_string(),
}
.into();
let error = error.context("remote execution failed");
assert_eq!(
classify_remote_pipeline_failure(&error),
RemotePipelineFailurePolicy::AllowLocalFallback,
"a sync stall fails over to another worker; it must not fail closed"
);
let stall = crate::transfer::find_source_sync_stall(&error)
.expect("typed stall must survive context wrapping for the failover downcast");
assert_eq!(stall.worker_id, "hz4");
assert_eq!(stall.phase, "source_sync");
assert_eq!(stall.silence.as_secs(), 120);
}
#[test]
fn test_sync_progress_line_marks_heartbeat_progress() {
let _guard = test_guard!();
let state = std::sync::Arc::new(std::sync::Mutex::new(
super::progress_reporting::BuildHeartbeatSnapshot::new(),
));
let before = state.lock().unwrap().progress_counter();
super::transfer_orchestration::sync_progress_line(None, Some(&state), " 1,234,567 42%");
super::transfer_orchestration::sync_progress_line(None, Some(&state), " 2,345,678 71%");
let after = state.lock().unwrap().progress_counter();
assert_eq!(
after,
before + 2,
"each sync output segment must count as heartbeat forward progress"
);
super::transfer_orchestration::sync_progress_line(None, None, "stats line");
}
#[test]
fn test_is_toolchain_failure_basic() {
let _guard = test_guard!();
assert!(is_toolchain_failure(
"error: toolchain 'nightly-2025-01-01' is not installed",
1
));
assert!(is_toolchain_failure("rustup: command not found", 127));
assert!(is_toolchain_failure(
"error: no default toolchain configured",
1
));
assert!(is_toolchain_failure(
"error: toolchain 'nightly-2025-01-01' does not have the binary `cargo`",
1
));
assert!(!is_toolchain_failure(
"error[E0425]: cannot find value `x`",
1
));
assert!(!is_toolchain_failure(
"test result: FAILED. 1 passed; 2 failed",
101
));
assert!(!is_toolchain_failure("anything", 0));
}
#[test]
fn test_is_toolchain_failure_ignores_rustup_toolchain_paths_in_normal_failures() {
let _guard = test_guard!();
let stderr = r#"error: could not compile `serde` (lib)
Caused by:
process didn't exit successfully: `/home/ubuntu/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/bin/rustc --crate-name serde ...` (signal: 9, SIGKILL: kill)
"#;
assert!(
!is_toolchain_failure(stderr, 137),
"SIGKILL/OOM stderr mentioning .rustup/toolchains paths must not trigger local fallback"
);
let compile_error = r#"error[E0425]: cannot find value `x` in this scope
note: the compiler executable is /home/ubuntu/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/bin/rustc
"#;
assert!(
!is_toolchain_failure(compile_error, EXIT_BUILD_ERROR),
"ordinary compile errors mentioning the rustc toolchain path must not trigger local fallback"
);
}
#[test]
fn test_detect_worker_system_dependency_failure_from_pkg_config_output() {
let _guard = test_guard!();
let stderr = r#"thread 'main' panicked at build.rs:42:14:
called `Result::unwrap()` on an `Err` value:
pkg-config exited with status code 1
> PKG_CONFIG_ALLOW_SYSTEM_LIBS=1 pkg-config --libs --cflags x11 'x11 >= 1.4.99.1'
The system library `x11` required by crate `x11` was not found.
The file `x11.pc` needs to be installed and the PKG_CONFIG_PATH environment variable must contain its parent directory.
"#;
let failure = detect_worker_system_dependency_failure(stderr, EXIT_BUILD_ERROR)
.expect("pkg-config/system dependency failure should be detected");
assert_eq!(failure.system_library.as_deref(), Some("x11"));
assert_eq!(failure.crate_name.as_deref(), Some("x11"));
assert_eq!(failure.pkg_config_file.as_deref(), Some("x11.pc"));
assert_eq!(failure.summary(), "missing worker system package x11.pc");
assert!(failure.remediation().contains("x11.pc"));
}
#[test]
fn remote_disk_exhaustion_requires_a_failed_execution_and_covers_quota_errors() {
for stderr in [
"error: failed to write: No space left on device (os error 28)",
"ld: output failed: ENOSPC",
"rustup: Disk quota exceeded",
"error[E0308]: mismatched types\nerror: write failed: no space left on device",
] {
assert!(remote_failure_is_disk_full(stderr, 101), "{stderr}");
assert!(remote_failure_is_worker_fault(stderr, 101), "{stderr}");
assert!(!remote_failure_is_disk_full(stderr, 0), "{stderr}");
}
for stderr in [
"error[E0308]: mismatched types",
"test result: FAILED. 3 passed; 1 failed",
"error: Permission denied (os error 13)",
"",
] {
assert!(!remote_failure_is_disk_full(stderr, 101), "{stderr}");
}
}
#[test]
fn source_upload_disk_release_requires_the_selected_worker_and_confirmed_ownership() {
let worker = WorkerId::new("upload-worker");
let error = anyhow::anyhow!("source receiver failed")
.context(crate::transfer::RemoteUploadDiskFull {
worker_id: worker.to_string(),
roots: vec!["/source-volume/project".into()],
})
.context("source preparation failed before remote command execution");
let result = Err(error);
let (worker_fault, disk_fault, roots) = remote_release_faults(&result, &worker);
assert!(worker_fault && disk_fault);
assert_eq!(roots, ["/source-volume/project"]);
assert_eq!(
remote_release_faults(&result, &WorkerId::new("other-worker")),
(false, false, &[] as &[String])
);
let result = result.map_err(|error| error.context(crate::transfer::RemoteExecutionUnconfirmed));
assert_eq!(
classify_remote_pipeline_failure(result.as_ref().unwrap_err()),
RemotePipelineFailurePolicy::FailClosedNoLocalFallback
);
assert_eq!(
remote_release_faults(&result, &worker),
(false, false, &[] as &[String])
);
assert!(is_remote_execution_unconfirmed(
result.as_ref().unwrap_err()
));
}
#[test]
fn source_upload_disk_release_never_infers_worker_pressure_from_untyped_transfer_errors() {
let worker = WorkerId::new("upload-worker");
let result = Err(TransferError::SyncFailed {
reason: "rsync artifact retrieval failed".into(),
exit_code: Some(23),
stderr: "rsync: [receiver] write failed on target/app: No space left on device (28)".into(),
}
.into());
assert_eq!(
remote_release_faults(&result, &worker),
(false, false, &[] as &[String])
);
for exit_code in [0, 101] {
let result = Ok(remote_result::RemoteExecutionResult {
deadline_triggered: false,
exit_code,
stderr: "No space left on device".into(),
duration_ms: 10,
timing: Default::default(),
result_dirs: Vec::new(),
disk_roots: vec!["/build-volume/target".into()],
cargo_stdout: None,
});
let (worker_fault, disk_fault, roots) = remote_release_faults(&result, &worker);
assert_eq!(worker_fault, exit_code != 0);
assert_eq!(disk_fault, exit_code != 0);
assert_eq!(roots, ["/build-volume/target"]);
}
}
#[test]
fn test_remote_failure_is_worker_fault_separates_worker_breakage_from_project_errors() {
let _guard = test_guard!();
let worker_faults = [
(
"error: toolchain 'nightly-2026-01-01-x86_64-unknown-linux-gnu' is not installed",
1,
),
(
"The system library `openssl` required by crate `openssl-sys` was not found.\n\
The file `openssl.pc` needs to be installed and the PKG_CONFIG_PATH environment variable must contain its parent directory.",
101,
),
("", 132),
(
"error: failed to run custom build command for `ring v0.17.8`\n\
process didn't exit successfully: `/t/build-script-build` (signal: 4, SIGILL: illegal instruction)",
101,
),
(
"error: failed to write /data/projects/p/target/debug/deps/libx.rlib: No space left on device (os error 28)",
101,
),
];
for (stderr, exit_code) in worker_faults {
assert!(
remote_failure_is_worker_fault(stderr, exit_code),
"{exit_code}: {stderr}"
);
}
let project_failures = [
(
"error[E0308]: mismatched types\nerror: could not compile `poolrepro`",
101,
),
("test result: FAILED. 3 passed; 1 failed", 101),
("", 130),
("", 137),
];
for (stderr, exit_code) in project_failures {
assert!(
!remote_failure_is_worker_fault(stderr, exit_code),
"{exit_code}: {stderr}"
);
}
assert!(!remote_failure_is_worker_fault(
"No space left on device",
0
));
}
#[test]
fn test_detect_worker_system_dependency_failure_ignores_normal_compile_errors() {
let _guard = test_guard!();
let stderr = r#"error[E0425]: cannot find value `oops` in this scope
--> src/main.rs:4:5
|
4 | oops();
| ^^^^ not found in this scope
"#;
assert!(
detect_worker_system_dependency_failure(stderr, EXIT_BUILD_ERROR).is_none(),
"ordinary compile errors must not be misclassified as worker env failures"
);
}
#[test]
fn test_wrap_command_with_telemetry_handles_comments() {
let _guard = test_guard!();
let worker_id = rch_common::WorkerId::new("worker1");
let command = "echo hello # my comment";
let wrapped = wrap_command_with_telemetry(command, &worker_id);
assert!(wrapped.contains(&format!("{}\nstatus=$?", command)));
let lines: Vec<&str> = wrapped.lines().collect();
assert!(lines.iter().any(|l| l.starts_with("status=$?")));
assert!(wrapped.contains("rch-telemetry collect"));
assert!(wrapped.contains("exit $status"));
}
#[test]
fn test_add_cargo_isolation_uses_durable_per_worker_cargo_home() {
let _guard = test_guard!();
let worker_id = rch_common::WorkerId::new("test-worker");
let cargo_command = "cargo build --release";
let isolated = add_cargo_isolation(cargo_command, &worker_id);
assert!(isolated.starts_with("sh -c "));
assert!(!isolated.starts_with("CARGO_HOME="));
assert!(
!isolated.contains("/tmp/rch-cargo-cache-"),
"must not hardcode /tmp: {isolated}"
);
assert!(isolated.contains("RCH_CH_BASE="));
assert!(isolated.contains("/data/tmp"));
assert!(isolated.contains("mkdir -p \"${RCH_CH_BASE}/rch-cargo-cache-test-worker\""));
assert!(isolated.contains("CARGO_HOME=\"${RCH_CH_BASE}/rch-cargo-cache-test-worker\""));
assert!(!isolated.contains("rch-cargo-home-"));
assert!(isolated.contains("cargo build --release"));
assert!(isolated.contains("CARGO_NET_GIT_FETCH_WITH_CLI=true"));
assert!(isolated.contains("command -v git"));
assert!(!isolated.contains("rm "));
let again = add_cargo_isolation(cargo_command, &worker_id);
assert_eq!(isolated, again);
}
#[test]
fn test_sanitize_cargo_home_token_collapses_unsafe_chars() {
assert_eq!(sanitize_cargo_home_token("worker-1_2"), "worker-1_2");
assert_eq!(sanitize_cargo_home_token("a b/c"), "a-b-c");
assert_eq!(sanitize_cargo_home_token(" weird!! "), "weird");
assert_eq!(sanitize_cargo_home_token("***"), "worker");
assert_eq!(sanitize_cargo_home_token(""), "worker");
}
#[test]
fn test_pooled_target_dir_same_dimensions_reuse_same_name() {
let _guard = test_guard!();
let worker = rch_common::WorkerId::new("ts2");
let root = Path::new("/data/projects/acme");
let tc = ToolchainInfo::new("nightly", Some("2025-11-01".to_string()), "x");
let a = remote_cargo_pooled_target_dir_name(&worker, root, Some(&tc), "cargo build");
let b = remote_cargo_pooled_target_dir_name(&worker, root, Some(&tc), "cargo build");
assert_eq!(a, b, "same dimensions must reuse the same pooled dir");
let f1 = remote_cargo_pooled_target_dir_name(
&worker,
root,
Some(&tc),
"cargo build --features serde,tokio",
);
let f2 = remote_cargo_pooled_target_dir_name(
&worker,
root,
Some(&tc),
"cargo build --features tokio --features serde",
);
assert_eq!(f1, f2, "feature set is order/dup-insensitive");
}
#[test]
fn test_pooled_target_dir_each_dimension_change_invalidates() {
let _guard = test_guard!();
let worker = rch_common::WorkerId::new("ts2");
let root = Path::new("/data/projects/acme");
let tc = ToolchainInfo::new("nightly", Some("2025-11-01".to_string()), "x");
let base = remote_cargo_pooled_target_dir_name(&worker, root, Some(&tc), "cargo build");
assert_ne!(
base,
remote_cargo_pooled_target_dir_name(&worker, root, Some(&tc), "cargo build --release"),
"profile change must invalidate"
);
assert_ne!(
base,
remote_cargo_pooled_target_dir_name(
&worker,
root,
Some(&tc),
"cargo build --target wasm32-unknown-unknown"
),
"triple change must invalidate"
);
let tc2 = ToolchainInfo::new("nightly", Some("2026-01-01".to_string()), "x");
assert_ne!(
base,
remote_cargo_pooled_target_dir_name(&worker, root, Some(&tc2), "cargo build"),
"toolchain change must invalidate"
);
assert_ne!(
base,
remote_cargo_pooled_target_dir_name(
&worker,
root,
Some(&tc),
"cargo build --features serde"
),
"feature change must invalidate"
);
assert_ne!(
base,
remote_cargo_pooled_target_dir_name(&worker, root, Some(&tc), "cargo build --all-features"),
"--all-features must invalidate"
);
assert_ne!(
base,
remote_cargo_pooled_target_dir_name(
&worker,
Path::new("/data/projects/other"),
Some(&tc),
"cargo build"
),
"project change must invalidate (no cross-project contamination)"
);
}
#[test]
fn test_pooled_target_dir_name_shape_is_single_segment_and_reapable() {
let _guard = test_guard!();
let worker = rch_common::WorkerId::new("ts2");
let root = Path::new("/data/projects/acme");
let tc = ToolchainInfo::new("nightly", Some("2025-11-01".to_string()), "x");
let name = remote_cargo_pooled_target_dir_name(&worker, root, Some(&tc), "cargo build");
assert!(
!name.contains('/'),
"pooled name must be a single segment: {name}"
);
assert!(
name.starts_with(".rch-target-"),
"must keep the reaper-recognized prefix: {name}"
);
assert!(
name.contains("-pool-"),
"must carry the -pool- marker the reaper globs match: {name}"
);
assert!(
rch_common::stale_target_reap::is_safe_reap_token(&name),
"pooled name must be reap-token-safe: {name}"
);
}
#[test]
fn test_target_reuse_opt_out_restores_unique_per_job_name() {
let _guard = test_guard!();
assert!(target_reuse_disabled_from_value(Some("1".to_string())));
assert!(target_reuse_disabled_from_value(Some("true".to_string())));
assert!(target_reuse_disabled_from_value(Some("YES".to_string())));
assert!(!target_reuse_disabled_from_value(None));
assert!(!target_reuse_disabled_from_value(Some("0".to_string())));
assert!(!target_reuse_disabled_from_value(Some("false".to_string())));
assert!(!target_reuse_disabled_from_value(Some(String::new())));
let worker = rch_common::WorkerId::new("ts2");
let root = Path::new("/data/projects/acme");
let tc = ToolchainInfo::new("nightly", Some("2025-11-01".to_string()), "x");
let pooled = remote_cargo_pooled_target_dir_name(&worker, root, Some(&tc), "cargo build");
let unique_a = remote_cargo_target_dir_name(Some(7), &worker);
let unique_b = remote_cargo_target_dir_name(Some(7), &worker);
assert_ne!(unique_a, unique_b, "opt-out name is unique per invocation");
assert_ne!(
pooled, unique_a,
"opt-out name differs from the pooled name"
);
assert!(
!unique_a.contains("-pool-"),
"opt-out name is not a pool dir"
);
}
#[test]
fn test_feature_and_triple_parsing_from_command() {
let _guard = test_guard!();
assert_eq!(
feature_set_for_command("cargo build --features a,b --features=c,d -F e"),
vec!["a", "b", "c", "d", "e"]
);
assert!(
feature_set_for_command("cargo build --all-features")
.iter()
.any(|f| f == "__rch_all_features")
);
assert!(
feature_set_for_command("cargo build --no-default-features")
.iter()
.any(|f| f == "__rch_no_default_features")
);
assert_eq!(
target_triple_for_command("cargo build --target wasm32-unknown-unknown"),
"wasm32-unknown-unknown"
);
assert_eq!(
target_triple_for_command("cargo build --target=aarch64-apple-darwin"),
"aarch64-apple-darwin"
);
let host = target_triple_for_command("cargo build");
assert!(!host.is_empty(), "host triple fallback must be non-empty");
assert_eq!(
host,
target_triple_for_command("cargo build"),
"host triple fallback must be stable"
);
}
#[test]
fn test_zigbuild_requires_the_zig_runtime_not_bare_rust() {
let _guard = test_guard!();
assert_eq!(
required_runtime_for_kind(Some(CompilationKind::CargoZigbuild)),
RequiredRuntime::Zig
);
for kind in [
CompilationKind::CargoBuild,
CompilationKind::CargoTest,
CompilationKind::CargoCheck,
CompilationKind::Rustc,
] {
assert_eq!(
required_runtime_for_kind(Some(kind)),
RequiredRuntime::Rust,
"{kind:?} must stay on the Rust runtime gate"
);
}
}
#[test]
fn test_kind_produces_transferable_artifacts() {
let _guard = test_guard!();
for kind in [
CompilationKind::CargoBuild,
CompilationKind::CargoDoc,
CompilationKind::Rustc,
CompilationKind::Gcc,
CompilationKind::Make,
CompilationKind::CmakeBuild,
CompilationKind::Ninja,
] {
assert!(
kind_produces_transferable_artifacts(Some(kind)),
"{kind:?} must be artifact-producing"
);
}
for kind in [
CompilationKind::CargoTest,
CompilationKind::CargoNextest,
CompilationKind::CargoBench,
CompilationKind::CargoCheck,
CompilationKind::CargoClippy,
CompilationKind::BunTest,
CompilationKind::BunTypecheck,
] {
assert!(
!kind_produces_transferable_artifacts(Some(kind)),
"{kind:?} must NOT be treated as artifact-producing"
);
}
assert!(!kind_produces_transferable_artifacts(None));
}
#[test]
fn test_job_admission_is_never_classifier_produced() {
let _guard = test_guard!();
let commands = [
"ls -la",
"git status",
"git push origin main",
"pytest -x -q",
"python3 script.py",
"echo hello > out.txt",
"curl https://example.com",
"grep -rn TODO src/",
"rm build/output.o",
"bun install",
"bun add left-pad",
"bun remove left-pad",
"bun run dev",
"bun build ./index.ts",
"bun x prettier .",
"bunx prettier .",
"bun test --watch",
"nix develop",
"nix run .#foo",
"nix repl",
"nix profile list",
"nix flake update",
"nix store gc",
"nix-env -iA nixpkgs.hello",
"cargo install rch",
"cargo clean",
"cargo build --watch",
"make test | tee log.txt",
"cmake --build build &",
"bash -lc \"cargo test && cargo test\"",
"cargo build --release",
"cargo test --workspace",
"go test ./...",
"tsc --noEmit",
"nix build .#default",
];
for cmd in commands {
let classification = classify_command(cmd);
assert_ne!(
classification.kind,
Some(CompilationKind::Job),
"classifier produced job admission for `{cmd}` — the hook would \
auto-delegate a command it did not intercept"
);
}
}
#[test]
fn test_job_kind_pipeline_contract() {
let _guard = test_guard!();
assert_eq!(
required_runtime_for_kind(Some(CompilationKind::Job)),
RequiredRuntime::None
);
assert!(get_artifact_patterns(Some(CompilationKind::Job), None).is_empty());
assert!(get_project_artifact_patterns(Some(CompilationKind::Job), None, false).is_empty());
assert!(get_project_artifact_patterns(Some(CompilationKind::Job), None, true).is_empty());
assert!(get_custom_target_artifact_patterns(Some(CompilationKind::Job), None).is_empty());
assert!(!kind_produces_transferable_artifacts(Some(
CompilationKind::Job
)));
assert!(!CompilationKind::Job.is_test_command());
}
#[test]
fn test_validate_job_result_dirs_accepts_relative_paths_and_dedupes() {
let _guard = test_guard!();
let validated = validate_job_result_dirs(vec![
PathBuf::from("results/shard-a"),
PathBuf::from("./fuzz/corpus"),
PathBuf::from("results/shard-a"),
])
.expect("valid result dirs");
assert_eq!(
validated,
vec![
PathBuf::from("results/shard-a"),
PathBuf::from("fuzz/corpus"),
],
"result dirs must normalize `.` segments and dedupe"
);
}
#[test]
fn test_validate_job_result_dirs_rejects_unsafe_paths() {
let _guard = test_guard!();
let unsafe_inputs = [
PathBuf::from("../escape"),
PathBuf::from("results/../../escape"),
PathBuf::from("/absolute/path"),
PathBuf::from(r"back\slash"),
PathBuf::from("results/café"),
PathBuf::new(),
];
for input in unsafe_inputs {
assert!(
validate_job_result_dirs(vec![input.clone()]).is_err(),
"unsafe result dir {:?} must be rejected before any transfer",
input
);
}
}
#[test]
fn test_add_cargo_isolation_skips_non_cargo_commands() {
let _guard = test_guard!();
let worker_id = rch_common::WorkerId::new("test-worker");
let non_cargo_command = "echo hello world";
let isolated = add_cargo_isolation(non_cargo_command, &worker_id);
assert_eq!(isolated, non_cargo_command);
assert!(!isolated.contains("CARGO_HOME"));
}
#[test]
fn test_add_cargo_isolation_handles_complex_cargo_commands() {
let _guard = test_guard!();
let worker_id = rch_common::WorkerId::new("worker-123");
let complex_command =
"cd /some/path && RUSTFLAGS=\"-C target-cpu=native\" cargo test --release --features=foo";
let isolated = add_cargo_isolation(complex_command, &worker_id);
assert!(isolated.starts_with("sh -c "));
assert!(
!isolated.contains("/tmp/rch-cargo-cache-"),
"must not hardcode /tmp: {isolated}"
);
assert!(isolated.contains("mkdir -p \"${RCH_CH_BASE}/rch-cargo-cache-worker-123\""));
assert!(isolated.contains("CARGO_HOME=\"${RCH_CH_BASE}/rch-cargo-cache-worker-123\""));
assert!(isolated.contains(
"cd /some/path && RUSTFLAGS=\"-C target-cpu=native\" cargo test --release --features=foo"
));
assert!(!isolated.contains("rch-cargo-cache-worker-123/ "));
assert!(!isolated.contains("rm "));
}
#[test]
fn test_add_cargo_isolation_survives_timeout_prefix_and_preserves_status() {
let _guard = test_guard!();
let worker_id = rch_common::WorkerId::new("timeout-worker");
let isolated = add_cargo_isolation("printf cargo >/dev/null; exit 42", &worker_id);
let status = std::process::Command::new("sh") .arg("-c")
.arg(format!(
"timeout --foreground --preserve-status 5 {}",
isolated
))
.status()
.expect("timeout-wrapped isolated command should execute");
assert_eq!(
status.code(),
Some(42),
"timeout must execute the shell wrapper and preserve the command status"
);
}
#[test]
fn test_add_cargo_isolation_repairs_dangling_registry_link() {
let _guard = test_guard!();
let base = tempfile::tempdir().unwrap();
let base_path = base.path().canonicalize().unwrap();
let worker_id = rch_common::WorkerId::new("dangling-worker");
let cache = base_path.join("rch-cargo-cache-dangling-worker");
std::fs::create_dir_all(&cache).unwrap();
let reclaimed = base_path.join("reclaimed-account-registry");
std::os::unix::fs::symlink(&reclaimed, cache.join("registry")).unwrap();
assert!(!cache.join("registry").exists(), "fixture link must dangle");
let isolated = add_cargo_isolation(
"printf cargo >/dev/null; test -d \"$CARGO_HOME/registry\" && touch \"$CARGO_HOME/registry/ok\"",
&worker_id,
);
let status = std::process::Command::new("sh") .arg("-c")
.arg(&isolated)
.env("TMPDIR", &base_path)
.env_remove(rch_common::RCH_CARGO_HOME_BASE_VAR)
.status()
.expect("isolated command should execute");
assert!(
status.success(),
"dangling registry link was not repaired: {status:?}"
);
assert!(
std::fs::symlink_metadata(cache.join("registry"))
.unwrap()
.file_type()
.is_symlink(),
"the operator's link is kept, not replaced"
);
assert!(
reclaimed.join("ok").exists(),
"writes land in the recreated link target"
);
}
#[test]
fn test_remote_cargo_target_dir_name_is_unique_and_path_safe() {
let _guard = test_guard!();
let worker_id = rch_common::WorkerId::new("worker/with spaces");
let first = remote_cargo_target_dir_name(Some(42), &worker_id);
let second = remote_cargo_target_dir_name(Some(42), &worker_id);
assert!(first.starts_with(".rch-target-worker-with-spaces-job-42-"));
assert!(!first.contains('/'));
assert!(!first.contains(' '));
assert_ne!(first, second);
}
#[test]
fn test_parse_stale_target_reap_idle_hours() {
assert_eq!(parse_stale_target_reap_idle_hours(None), 12);
assert_eq!(
parse_stale_target_reap_idle_hours(Some("not-a-number".into())),
12
);
assert_eq!(parse_stale_target_reap_idle_hours(Some(String::new())), 12);
assert_eq!(parse_stale_target_reap_idle_hours(Some("24".into())), 24);
assert_eq!(parse_stale_target_reap_idle_hours(Some(" 6 ".into())), 6);
assert_eq!(parse_stale_target_reap_idle_hours(Some("0".into())), 1);
}
#[test]
fn test_resolve_forwarded_cargo_target_dir_reads_env_without_allowlist() {
let _guard = test_guard!();
let reporter = HookReporter::new(OutputVisibility::Verbose);
let resolved = resolve_forwarded_cargo_target_dir_with_lookup(
Some(CompilationKind::CargoBuild),
Path::new("/tmp/rch"),
&reporter,
|_| Some("/tmp/rch-target-no-allowlist".to_string()),
None,
);
assert_eq!(
resolved,
Some(PathBuf::from("/tmp/rch-target-no-allowlist"))
);
}
#[test]
fn test_resolve_forwarded_cargo_target_dir_defaults_for_cargo() {
let _guard = test_guard!();
let reporter = HookReporter::new(OutputVisibility::Verbose);
let resolved = resolve_forwarded_cargo_target_dir_with_lookup(
Some(CompilationKind::CargoBuild),
Path::new("/data/projects/remote_compilation_helper"),
&reporter,
|_| None,
None,
);
assert_eq!(
resolved,
Some(PathBuf::from(
"/data/projects/remote_compilation_helper/target"
))
);
}
#[test]
fn test_resolve_forwarded_cargo_target_dir_ignores_non_cargo() {
let _guard = test_guard!();
let reporter = HookReporter::new(OutputVisibility::Verbose);
let resolved = resolve_forwarded_cargo_target_dir_with_lookup(
Some(CompilationKind::BunTest),
Path::new("/data/projects/remote_compilation_helper"),
&reporter,
|_| Some("/tmp/should-not-forward".to_string()),
None,
);
assert!(resolved.is_none());
}
#[test]
fn test_resolve_forwarded_cargo_target_dir_resolves_relative_path() {
let _guard = test_guard!();
let reporter = HookReporter::new(OutputVisibility::Verbose);
let resolved = resolve_forwarded_cargo_target_dir_with_lookup(
Some(CompilationKind::CargoBuild),
Path::new("/data/projects/remote_compilation_helper"),
&reporter,
|_| Some("tmp/custom-target".to_string()),
None,
);
assert_eq!(
resolved,
Some(PathBuf::from(
"/data/projects/remote_compilation_helper/tmp/custom-target"
))
);
}
#[test]
fn test_resolve_forwarded_cargo_target_dir_extracts_env_wrapper_assignment() {
let _guard = test_guard!();
let reporter = HookReporter::new(OutputVisibility::Verbose);
let command_tokens = vec![
"env".to_string(),
"-u".to_string(),
"RUST_LOG".to_string(),
"RUST_BACKTRACE=1".to_string(),
"CARGO_TARGET_DIR=/data/projects/custom-target".to_string(),
"cargo".to_string(),
"check".to_string(),
];
let resolved = resolve_forwarded_cargo_target_dir_with_lookup(
Some(CompilationKind::CargoBuild),
Path::new("/data/projects/remote_compilation_helper"),
&reporter,
|_| Some("/tmp/env-should-lose-to-command".to_string()),
Some(&command_tokens),
);
assert_eq!(
resolved,
Some(PathBuf::from("/data/projects/custom-target"))
);
}
#[test]
fn test_resolve_forwarded_cargo_target_dir_extracts_inline_assignment() {
let _guard = test_guard!();
let reporter = HookReporter::new(OutputVisibility::Verbose);
let command_tokens = vec![
"CARGO_TARGET_DIR=.rch-target-inline".to_string(),
"cargo".to_string(),
"build".to_string(),
];
let resolved = resolve_forwarded_cargo_target_dir_with_lookup(
Some(CompilationKind::CargoBuild),
Path::new("/data/projects/remote_compilation_helper"),
&reporter,
|_| None,
Some(&command_tokens),
);
assert_eq!(
resolved,
Some(PathBuf::from(
"/data/projects/remote_compilation_helper/.rch-target-inline"
))
);
}
#[test]
fn test_resolve_forwarded_cargo_target_dir_extracts_target_dir_flag() {
let _guard = test_guard!();
let reporter = HookReporter::new(OutputVisibility::Verbose);
let command_tokens = vec![
"cargo".to_string(),
"build".to_string(),
"--target-dir".to_string(),
"/data/tmp/rch-target-flag".to_string(),
"--release".to_string(),
];
let resolved = resolve_forwarded_cargo_target_dir_with_lookup(
Some(CompilationKind::CargoBuild),
Path::new("/data/projects/remote_compilation_helper"),
&reporter,
|_| None,
Some(&command_tokens),
);
assert_eq!(resolved, Some(PathBuf::from("/data/tmp/rch-target-flag")));
}
#[test]
fn test_resolve_forwarded_cargo_target_dir_extracts_target_dir_equals_flag() {
let _guard = test_guard!();
let reporter = HookReporter::new(OutputVisibility::Verbose);
let command_tokens = vec![
"cargo".to_string(),
"check".to_string(),
"--target-dir=/data/tmp/rch-target-equals".to_string(),
];
let resolved = resolve_forwarded_cargo_target_dir_with_lookup(
Some(CompilationKind::CargoCheck),
Path::new("/data/projects/remote_compilation_helper"),
&reporter,
|_| None,
Some(&command_tokens),
);
assert_eq!(resolved, Some(PathBuf::from("/data/tmp/rch-target-equals")));
}
#[test]
fn test_rewrite_cargo_target_dir_command_for_remote_strips_inline_assignment() {
let _guard = test_guard!();
let reporter = HookReporter::new(OutputVisibility::Verbose);
let command_tokens = vec![
"env".to_string(),
"-u".to_string(),
"RUST_LOG".to_string(),
"RUST_BACKTRACE=1".to_string(),
"CARGO_TARGET_DIR=/data/projects/custom-target".to_string(),
"cargo".to_string(),
"build".to_string(),
"--release".to_string(),
];
let rewritten = rewrite_cargo_target_dir_command_for_remote(
"env -u RUST_LOG RUST_BACKTRACE=1 CARGO_TARGET_DIR=/data/projects/custom-target cargo build --release",
Some(&command_tokens),
Some(&PathBuf::from("/data/projects/custom-target")),
&reporter,
);
assert_eq!(
rewritten,
"env -u RUST_LOG 'RUST_BACKTRACE=1' cargo build --release"
);
assert!(!rewritten.contains("CARGO_TARGET_DIR=/data/projects/custom-target"));
}
#[test]
fn test_rewrite_cargo_target_dir_command_for_remote_strips_target_dir_flag() {
let _guard = test_guard!();
let reporter = HookReporter::new(OutputVisibility::Verbose);
let command_tokens = vec![
"cargo".to_string(),
"build".to_string(),
"--target-dir".to_string(),
"/data/tmp/rch-target-flag".to_string(),
"--release".to_string(),
];
let rewritten = rewrite_cargo_target_dir_command_for_remote(
"cargo build --target-dir /data/tmp/rch-target-flag --release",
Some(&command_tokens),
Some(&PathBuf::from("/data/tmp/rch-target-flag")),
&reporter,
);
assert_eq!(rewritten, "cargo build --release");
assert!(!rewritten.contains("--target-dir"));
assert!(!rewritten.contains("/data/tmp/rch-target-flag"));
}
#[test]
fn test_rewrite_cargo_target_dir_command_for_remote_strips_target_dir_equals_flag() {
let _guard = test_guard!();
let reporter = HookReporter::new(OutputVisibility::Verbose);
let command_tokens = vec![
"cargo".to_string(),
"check".to_string(),
"--target-dir=/data/tmp/rch-target-equals".to_string(),
"--workspace".to_string(),
];
let rewritten = rewrite_cargo_target_dir_command_for_remote(
"cargo check --target-dir=/data/tmp/rch-target-equals --workspace",
Some(&command_tokens),
Some(&PathBuf::from("/data/tmp/rch-target-equals")),
&reporter,
);
assert_eq!(rewritten, "cargo check --workspace");
assert!(!rewritten.contains("--target-dir"));
assert!(!rewritten.contains("/data/tmp/rch-target-equals"));
}
#[test]
fn test_cargo_target_dir_scanner_ignores_args_after_delimiter() {
let _guard = test_guard!();
let reporter = HookReporter::new(OutputVisibility::Verbose);
let command_tokens = vec![
"cargo".to_string(),
"test".to_string(),
"--".to_string(),
"--target-dir".to_string(),
"test-filter".to_string(),
];
let resolved = resolve_forwarded_cargo_target_dir_with_lookup(
Some(CompilationKind::CargoTest),
Path::new("/data/projects/remote_compilation_helper"),
&reporter,
|_| None,
Some(&command_tokens),
);
let rewritten = rewrite_cargo_target_dir_command_for_remote(
"cargo test -- --target-dir test-filter",
Some(&command_tokens),
Some(&PathBuf::from(
"/data/projects/remote_compilation_helper/target",
)),
&reporter,
);
assert_eq!(
resolved,
Some(PathBuf::from(
"/data/projects/remote_compilation_helper/target"
))
);
assert_eq!(rewritten, "cargo test -- --target-dir test-filter");
}
#[test]
fn test_rewrite_cargo_target_dir_command_preserves_args_after_delimiter() {
let _guard = test_guard!();
let reporter = HookReporter::new(OutputVisibility::Verbose);
let command_tokens = vec![
"cargo".to_string(),
"test".to_string(),
"--target-dir".to_string(),
"/data/tmp/rch-target-flag".to_string(),
"--".to_string(),
"--nocapture".to_string(),
];
let rewritten = rewrite_cargo_target_dir_command_for_remote(
"cargo test --target-dir /data/tmp/rch-target-flag -- --nocapture",
Some(&command_tokens),
Some(&PathBuf::from("/data/tmp/rch-target-flag")),
&reporter,
);
assert_eq!(rewritten, "cargo test -- --nocapture");
}
fn env_key_strategy() -> impl Strategy<Value = String> {
prop::string::string_regex("[A-Z_][A-Z0-9_]{0,16}")
.expect("valid env key regex")
.prop_filter("not the target dir key under test", |key| {
key != "CARGO_TARGET_DIR"
})
}
fn shell_safe_value_strategy() -> impl Strategy<Value = String> {
prop::string::string_regex("[A-Za-z0-9_./:+-]{0,40}").expect("valid env value regex")
}
fn relative_target_dir_strategy() -> impl Strategy<Value = String> {
prop::string::string_regex("[A-Za-z0-9_.-]{1,16}(/[A-Za-z0-9_.-]{1,16}){0,2}")
.expect("valid relative path regex")
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(256))]
#[test]
fn env_prefix_target_dir_parser_round_trips_and_rewrites(
target_dir in relative_target_dir_strategy(),
extra_envs in prop::collection::vec((env_key_strategy(), shell_safe_value_strategy()), 0..4),
cargo_subcommand in prop_oneof![
Just("build".to_string()),
Just("check".to_string()),
Just("test".to_string()),
Just("clippy".to_string()),
],
) {
let _guard = test_guard!();
let reporter = HookReporter::new(OutputVisibility::Verbose);
let mut tokens = vec!["env".to_string()];
for (key, value) in &extra_envs {
tokens.push(format!("{key}={value}"));
}
tokens.push(format!("CARGO_TARGET_DIR={target_dir}"));
tokens.push("cargo".to_string());
tokens.push(cargo_subcommand);
tokens.push("--release".to_string());
let command = join_exec_command(&tokens);
let parsed = parse_command_tokens(&command, &reporter).expect("joined command should parse");
prop_assert_eq!(&parsed, &tokens);
prop_assert_eq!(
extract_cargo_target_dir_from_command_tokens(&parsed),
Some(target_dir.clone())
);
let invocation_cwd = Path::new("/tmp/rch-proptest-project");
let resolved = resolve_forwarded_cargo_target_dir_with_lookup(
Some(CompilationKind::CargoBuild),
invocation_cwd,
&reporter,
|_| Some("/tmp/ambient-target".to_string()),
Some(&parsed),
);
let expected_resolved = Some(invocation_cwd.join(&target_dir));
prop_assert_eq!(resolved.as_ref(), expected_resolved.as_ref());
let rewritten = rewrite_cargo_target_dir_command_for_remote(
&command,
Some(&parsed),
resolved.as_ref(),
&reporter,
);
prop_assert!(!rewritten.contains("CARGO_TARGET_DIR="));
let rewritten_tokens = parse_command_tokens(&rewritten, &reporter)
.expect("rewritten command should remain parseable");
prop_assert_eq!(rewritten_tokens.first().map(String::as_str), Some("env"));
for (key, value) in &extra_envs {
let expected_assignment = format!("{key}={value}");
prop_assert!(
rewritten_tokens.contains(&expected_assignment),
"rewritten command dropped env assignment {expected_assignment:?}"
);
}
prop_assert!(rewritten_tokens.iter().any(|token| token == "cargo"));
}
#[test]
fn env_prefix_helpers_do_not_panic_on_arbitrary_command_bytes(
bytes in prop::collection::vec(any::<u8>(), 0..192),
) {
let _guard = test_guard!();
let reporter = HookReporter::new(OutputVisibility::Verbose);
let command = String::from_utf8_lossy(&bytes).into_owned();
if let Some(tokens) = parse_command_tokens(&command, &reporter) {
let _ = extract_cargo_target_dir_from_command_tokens(&tokens);
let _ = strip_cargo_target_dir_assignments_from_command_tokens(&tokens);
let _ = strip_cargo_target_dir_flags_from_command_tokens(&tokens);
let rewritten = rewrite_cargo_target_dir_command_for_remote(
&command,
Some(&tokens),
Some(&PathBuf::from("/tmp/rch-proptest-target")),
&reporter,
);
prop_assert!(
parse_command_tokens(&rewritten, &reporter).is_some(),
"parsed command rewrote to an unparsable command: {rewritten:?}"
);
}
}
}
#[tokio::test]
async fn test_collect_repo_updater_roots_and_specs_filters_to_git_roots_with_origin() {
let _guard = test_guard!();
let temp_dir = tempfile::tempdir().expect("temp dir should be creatable");
let with_origin = temp_dir.path().join("with_origin");
let duplicate_origin = temp_dir.path().join("duplicate_origin");
let without_origin = temp_dir.path().join("without_origin");
let not_git = temp_dir.path().join("not_git");
std::fs::create_dir_all(&with_origin).expect("create with_origin");
std::fs::create_dir_all(&duplicate_origin).expect("create duplicate_origin");
std::fs::create_dir_all(&without_origin).expect("create without_origin");
std::fs::create_dir_all(¬_git).expect("create not_git");
let status = std::process::Command::new("git")
.arg("-C")
.arg(temp_dir.path())
.arg("init")
.arg("-q")
.status()
.expect("git init should run for the fixture boundary");
assert!(
status.success(),
"git init should succeed for the fixture boundary at {}",
temp_dir.path().display()
);
for repo in [&with_origin, &duplicate_origin, &without_origin] {
let status = std::process::Command::new("git")
.arg("-C")
.arg(repo)
.arg("init")
.arg("-q")
.status()
.expect("git init should run");
assert!(
status.success(),
"git init should succeed for {}",
repo.display()
);
}
let origin_url = "git@github.com:example/repo-with-origin.git";
for repo in [&with_origin, &duplicate_origin] {
let status = std::process::Command::new("git")
.arg("-C")
.arg(repo)
.arg("remote")
.arg("add")
.arg("origin")
.arg(origin_url)
.status()
.expect("git remote add should run");
assert!(
status.success(),
"git remote add should succeed for {}",
repo.display()
);
}
let collected = collect_repo_updater_roots_and_specs(&[
with_origin.clone(),
without_origin.clone(),
not_git.clone(),
duplicate_origin.clone(),
])
.await;
assert_eq!(
collected.roots,
vec![with_origin.clone(), duplicate_origin.clone()]
);
assert_eq!(collected.specs, vec![origin_url.to_string()]);
}
#[test]
fn test_auto_tune_repo_updater_contract_autoseeds_allowlist_and_mode() {
let _guard = test_guard!();
let mut contract = RepoUpdaterAdapterContract::default();
let repo_specs = vec!["github.com/example/repo".to_string()];
let auth_context = RepoUpdaterAuthContext {
source: RepoUpdaterCredentialSource::SshAgent,
credential_id: "ssh-agent".to_string(), issued_at_unix_ms: 1_700_000_000_000,
expires_at_unix_ms: 1_700_000_060_000,
granted_scopes: vec![],
revoked: false,
verified_hosts: vec![],
};
let reporter = HookReporter::new(OutputVisibility::None);
auto_tune_repo_updater_contract(
&mut contract,
&repo_specs,
Some(&auth_context),
false,
false,
&reporter,
);
assert_eq!(contract.trust_policy.allowlisted_repo_specs, repo_specs);
assert_eq!(
contract.auth_policy.mode,
RepoUpdaterAuthMode::InheritEnvironment
);
}
#[test]
fn test_hydrate_repo_updater_auth_context_defaults_populates_required_fields() {
let _guard = test_guard!();
let contract = RepoUpdaterAdapterContract::default();
let now_ms = 1_700_000_000_000_i64;
let mut auth_context = RepoUpdaterAuthContext {
source: RepoUpdaterCredentialSource::TokenEnv,
credential_id: String::new(),
issued_at_unix_ms: 0,
expires_at_unix_ms: 0,
granted_scopes: vec![],
revoked: false,
verified_hosts: vec![],
};
hydrate_repo_updater_auth_context_defaults(&mut auth_context, now_ms, &contract);
assert_eq!(auth_context.credential_id, "token-env");
assert!(auth_context.issued_at_unix_ms > 0);
assert!(auth_context.issued_at_unix_ms <= now_ms);
assert!(auth_context.expires_at_unix_ms > now_ms);
assert_eq!(
auth_context.granted_scopes,
contract.auth_policy.required_scopes
);
assert_eq!(
auth_context.verified_hosts.len(),
contract.auth_policy.trusted_host_identities.len()
);
}
#[test]
fn test_infer_repo_updater_auth_context_returns_none_without_local_auth() {
let _guard = test_guard!();
assert!(
infer_repo_updater_auth_context_with_env_lookup(1_700_000_000_000, |_| false).is_none()
);
}
#[test]
fn test_infer_repo_updater_auth_context_uses_token_env_when_present() {
let _guard = test_guard!();
let auth_context =
infer_repo_updater_auth_context_with_env_lookup(1_700_000_000_000, |key| key == "GH_TOKEN") .expect("token env should infer auth context");
assert_eq!(auth_context.source, RepoUpdaterCredentialSource::TokenEnv);
assert_eq!(auth_context.credential_id, "env:GH_TOKEN");
assert_eq!(auth_context.granted_scopes, vec!["repo:read".to_string()]);
}
#[test]
fn test_repo_updater_command_name_is_stable() {
let _guard = test_guard!();
assert_eq!(
repo_updater_command_name(RepoUpdaterAdapterCommand::SyncApply),
"sync-apply"
);
assert_eq!(
repo_updater_command_name(RepoUpdaterAdapterCommand::SyncDryRun),
"sync-dry-run"
);
assert_eq!(
repo_updater_command_name(RepoUpdaterAdapterCommand::StatusNoFetch),
"status-no-fetch"
);
}
#[test]
fn test_build_repo_sync_idempotency_key_for_command_distinguishes_commands() {
let _guard = test_guard!();
let worker_id = WorkerId::new("worker-a");
let sync_roots = vec![
PathBuf::from("/data/projects/repo-a"),
PathBuf::from("/data/projects/repo-b"),
];
let apply_key = build_repo_sync_idempotency_key_for_command(
&worker_id,
&sync_roots,
RepoUpdaterAdapterCommand::SyncApply,
);
let dry_run_key = build_repo_sync_idempotency_key_for_command(
&worker_id,
&sync_roots,
RepoUpdaterAdapterCommand::SyncDryRun,
);
let status_key = build_repo_sync_idempotency_key_for_command(
&worker_id,
&sync_roots,
RepoUpdaterAdapterCommand::StatusNoFetch,
);
assert_ne!(apply_key, dry_run_key);
assert_ne!(dry_run_key, status_key);
assert_ne!(apply_key, status_key);
assert!(apply_key.starts_with("rch-repo-sync-"));
}
#[test]
fn test_build_remote_dependency_preflight_command_empty_roots() {
let _guard = test_guard!();
assert!(build_remote_dependency_preflight_command(&[]).is_none());
}
#[test]
fn test_build_remote_dependency_preflight_command_separates_checks() {
let _guard = test_guard!();
let checks = vec![
DependencyPreflightCheck {
root: "/data/projects/repo-a".to_string(),
manifest: "/data/projects/repo-a/Cargo.toml".to_string(),
required_path: "/data/projects/repo-a/Cargo.toml".to_string(),
required_kind: "manifest",
is_primary: true,
},
DependencyPreflightCheck {
root: "/data/projects/repo-b".to_string(),
manifest: "/data/projects/repo-b/Cargo.toml".to_string(),
required_path: "/data/projects/repo-b/src/lib.rs".to_string(),
required_kind: "source_entrypoint",
is_primary: false,
},
];
let command =
build_remote_dependency_preflight_command(&checks).expect("command should be constructed");
assert!(
command.contains("for required in "),
"generated command must batch paths through one bounded shell loop"
);
assert!(
!command.contains("fi if ["),
"generated command must not concatenate checks without separator"
);
assert!(
command.contains("RCH_DEP_PRESENT:"),
"generated command must emit structured present marker"
);
assert!(
command.contains("RCH_DEP_MISSING:"),
"generated command must emit structured missing marker"
);
}
#[test]
fn test_windows_offload_control_commands_use_stdin_script_transport() {
let _guard = test_guard!();
let script = "for required in 'C:/rch/app/Cargo.toml'; do test -f \"$required\"; done";
let mut windows = make_test_worker_config("worker-windows-control-plane");
windows.tags.push("os:windows".to_string());
let (remote_arg, stdin_payload) = offload_remote_command_transport(&windows, script);
assert_eq!(remote_arg, "sh -s");
assert_eq!(stdin_payload, Some(script.as_bytes()));
let posix = make_test_worker_config("worker-posix-control-plane");
let (remote_arg, stdin_payload) = offload_remote_command_transport(&posix, script);
assert_eq!(remote_arg, script);
assert!(stdin_payload.is_none());
}
#[test]
fn test_build_remote_dependency_preflight_command_covers_large_workspaces() {
let _guard = test_guard!();
let checks = (0..2119)
.map(|idx| DependencyPreflightCheck {
root: "/data/projects/big".to_string(),
manifest: "/data/projects/big/Cargo.toml".to_string(),
required_path: format!("/data/projects/big/tests/case_{idx}.rs"),
required_kind: "source_entrypoint",
is_primary: true,
})
.collect::<Vec<_>>();
let command = build_remote_dependency_preflight_command(&checks).unwrap();
for check in &checks {
assert!(command.contains(&check.required_path));
}
}
#[test]
fn test_dependency_preflight_timeout_retains_total_probe_budget() {
for (paths, seconds) in [(0, 20), (1, 20), (128, 20), (129, 40), (2119, 340)] {
assert_eq!(
dependency_preflight_timeout(paths),
Duration::from_secs(seconds)
);
}
assert!(dependency_preflight_timeout(usize::MAX) >= Duration::from_secs(340));
}
#[test]
fn test_synced_dependency_preflight_checks_use_remote_paths() {
let _guard = test_guard!();
let temp_dir = tempfile::tempdir().expect("temp dir");
let package_root = temp_dir.path().join("package");
std::fs::create_dir_all(package_root.join("src")).expect("create package src");
std::fs::write(
package_root.join("Cargo.toml"),
r#"[package]
name = "package"
version = "0.1.0"
edition = "2024"
"#,
)
.expect("write manifest");
std::fs::write(package_root.join("src/lib.rs"), "pub fn package() {}\n").expect("write lib");
let root_outcomes = vec![
(
SyncClosurePlanEntry {
local_root: package_root,
remote_root: "/data/projects/frankenterm".to_string(),
project_id: "frankenterm".to_string(),
root_hash: "hash-primary".to_string(),
is_primary: true,
mode: SyncClosureMode::Full,
},
SyncRootOutcome::Synced,
),
(
SyncClosurePlanEntry {
local_root: PathBuf::from("/Users/jemanuel/projects/frankentui"),
remote_root: "/data/projects/frankentui".to_string(),
project_id: "frankentui".to_string(),
root_hash: "hash-dep".to_string(),
is_primary: false,
mode: SyncClosureMode::Full,
},
SyncRootOutcome::Failed {
error: "no sync".to_string(),
},
),
];
let synced = synced_dependency_preflight_checks(&root_outcomes);
let required_paths = synced
.iter()
.map(|check| check.required_path.as_str())
.collect::<Vec<_>>();
assert!(required_paths.contains(&"/data/projects/frankenterm/Cargo.toml"));
assert!(required_paths.contains(&"/data/projects/frankenterm/src/lib.rs"));
assert!(
!required_paths
.iter()
.any(|path| path.starts_with("/data/projects/frankentui")),
"failed roots must not be probed as freshly synced"
);
}
#[test]
fn test_parse_dependency_preflight_probe_output_extracts_markers() {
let _guard = test_guard!();
let stdout = "\
RCH_DEP_PRESENT:/data/projects/a/Cargo.toml
noise
RCH_DEP_MISSING:/data/projects/b/Cargo.toml
RCH_DEP_PRESENT:/data/projects/c/Cargo.toml
";
let (present, missing) = parse_dependency_preflight_probe_output(stdout);
assert_eq!(present.len(), 2);
assert_eq!(missing.len(), 1);
assert!(present.contains("/data/projects/a/Cargo.toml"));
assert!(present.contains("/data/projects/c/Cargo.toml"));
assert!(missing.contains("/data/projects/b/Cargo.toml"));
}
#[test]
fn test_dependency_preflight_error_codes_match_public_catalog() {
let _guard = test_guard!();
assert_eq!(
DEPENDENCY_PREFLIGHT_CODE_MISSING,
ErrorCode::DependencyPreflightMissing.code_string().as_str()
);
assert_eq!(
DEPENDENCY_PREFLIGHT_CODE_STALE,
ErrorCode::DependencyPreflightStale.code_string().as_str()
);
assert_eq!(
DEPENDENCY_PREFLIGHT_CODE_UNKNOWN,
ErrorCode::DependencyPreflightUnknown.code_string().as_str()
);
assert_eq!(
DEPENDENCY_PREFLIGHT_CODE_POLICY,
ErrorCode::DependencyPreflightPolicyViolation
.code_string()
.as_str()
);
assert_eq!(
DEPENDENCY_PREFLIGHT_CODE_TIMEOUT,
ErrorCode::DependencyPreflightTimeout.code_string().as_str()
);
assert_ne!(
DEPENDENCY_PREFLIGHT_CODE_MISSING,
ErrorCode::CancelSlotLeak.code_string().as_str(),
"dependency preflight must not reuse the cancellation slot-leak code"
);
}
fn make_sync_entry(root: &str, is_primary: bool) -> SyncClosurePlanEntry {
SyncClosurePlanEntry {
local_root: PathBuf::from(root),
remote_root: root.to_string(),
project_id: format!("id-{}", root.replace('/', "_")),
root_hash: format!("hash-{}", root.replace('/', "_")),
is_primary,
mode: SyncClosureMode::Full,
}
}
fn make_test_worker_config(id: &str) -> WorkerConfig {
WorkerConfig {
id: WorkerId::new(id),
host: "worker.host".to_string(),
user: "ubuntu".to_string(),
identity_file: "~/.ssh/id_ed25519".to_string(),
total_slots: 8,
priority: 100,
tags: Vec::new(),
tools: Vec::new(),
}
}
fn make_fail_open_plan(
fail_open_reason: Option<&str>,
issues: Vec<rch_common::DependencyPlanIssue>,
) -> DependencyClosurePlan {
DependencyClosurePlan {
state: rch_common::DependencyClosurePlanState::FailOpen,
entry_manifest_path: PathBuf::from("/data/projects/example/Cargo.toml"),
workspace_root: Some(PathBuf::from("/data/projects/example")),
canonical_roots: Vec::new(),
sync_order: Vec::new(),
fail_open: true,
fail_open_reason: fail_open_reason.map(ToString::to_string),
issues,
}
}
#[test]
fn test_classify_dependency_runtime_fail_open_policy_violation() {
let _guard = test_guard!();
let plan = make_fail_open_plan(
Some("resolver produced path policy violation"),
vec![rch_common::DependencyPlanIssue {
code: "path-policy-violation".to_string(),
message: "dependency path escapes canonical root".to_string(),
risk: rch_common::DependencyRiskClass::High,
diagnostics: vec!["dependency_path=/tmp/off-policy".to_string()],
}],
);
let decision = classify_dependency_runtime_fail_open(&plan);
assert_eq!(decision.reason_code, DEPENDENCY_PREFLIGHT_CODE_POLICY);
assert_eq!(
decision.remediation,
DEPENDENCY_PREFLIGHT_REMEDIATION_POLICY
);
}
#[test]
fn test_classify_dependency_runtime_fail_open_materialization_failure() {
let _guard = test_guard!();
let plan = make_fail_open_plan(
Some("materialization closure produced missing path dependency"),
vec![rch_common::DependencyPlanIssue {
code: "materialization-closure-unavailable".to_string(),
message: "inactive optional path dependency is missing".to_string(),
risk: rch_common::DependencyRiskClass::Critical,
diagnostics: vec!["dependency_name=optional_dep".to_string()],
}],
);
let decision = classify_dependency_runtime_fail_open(&plan);
assert_eq!(
decision.reason_code,
DEPENDENCY_PREFLIGHT_CODE_MATERIALIZATION
);
assert_eq!(
decision.remediation,
DEPENDENCY_PREFLIGHT_REMEDIATION_MATERIALIZATION
);
}
#[test]
fn test_classify_dependency_runtime_fail_open_timeout_signal() {
let _guard = test_guard!();
let plan = make_fail_open_plan(
Some("cargo metadata timed out after 10s"),
vec![rch_common::DependencyPlanIssue {
code: "metadata-invocation-failure".to_string(),
message: "metadata invocation timed out".to_string(),
risk: rch_common::DependencyRiskClass::Critical,
diagnostics: vec!["timeout=10s".to_string()],
}],
);
let decision = classify_dependency_runtime_fail_open(&plan);
assert_eq!(decision.reason_code, DEPENDENCY_PREFLIGHT_CODE_TIMEOUT);
assert_eq!(
decision.remediation,
DEPENDENCY_PREFLIGHT_REMEDIATION_TIMEOUT
);
}
#[test]
fn test_classify_dependency_runtime_fail_open_defaults_unknown() {
let _guard = test_guard!();
let plan = make_fail_open_plan(
Some("resolver returned unverifiable graph ordering"),
vec![rch_common::DependencyPlanIssue {
code: "non-deterministic-order".to_string(),
message: "graph order could not be proven".to_string(),
risk: rch_common::DependencyRiskClass::Critical,
diagnostics: vec!["planner_state=fail_open".to_string()],
}],
);
let decision = classify_dependency_runtime_fail_open(&plan);
assert_eq!(decision.reason_code, DEPENDENCY_PREFLIGHT_CODE_UNKNOWN);
assert_eq!(
decision.remediation,
DEPENDENCY_PREFLIGHT_REMEDIATION_UNKNOWN
);
}
#[test]
fn test_build_dependency_runtime_fail_open_report_uses_status_mapping() {
let _guard = test_guard!();
let worker = make_test_worker_config("worker-runtime-report");
let project_root = PathBuf::from("/data/projects/runtime-policy");
let decision = DependencyRuntimeFailOpenDecision {
reason_code: DEPENDENCY_PREFLIGHT_CODE_POLICY,
remediation: DEPENDENCY_PREFLIGHT_REMEDIATION_POLICY,
detail: "policy violation detail".to_string(),
};
let report = build_dependency_runtime_fail_open_report(&worker, &project_root, &decision);
assert!(!report.verified);
assert_eq!(report.reason_code, Some(DEPENDENCY_PREFLIGHT_CODE_POLICY));
assert_eq!(
report.remediation,
Some(DEPENDENCY_PREFLIGHT_REMEDIATION_POLICY)
);
assert_eq!(report.evidence.len(), 1);
assert_eq!(
report.evidence[0].status,
DependencyPreflightStatus::PolicyViolation
);
}
#[test]
fn test_build_dependency_runtime_fail_open_report_maps_materialization_failure() {
let _guard = test_guard!();
let worker = make_test_worker_config("worker-runtime-materialization");
let project_root = PathBuf::from("/data/projects/runtime-materialization");
let decision = DependencyRuntimeFailOpenDecision {
reason_code: DEPENDENCY_PREFLIGHT_CODE_MATERIALIZATION,
remediation: DEPENDENCY_PREFLIGHT_REMEDIATION_MATERIALIZATION,
detail: "optional path root unavailable".to_string(),
};
let report = build_dependency_runtime_fail_open_report(&worker, &project_root, &decision);
assert!(!report.verified);
assert_eq!(
report.reason_code,
Some(DEPENDENCY_PREFLIGHT_CODE_MATERIALIZATION)
);
assert_eq!(
report.evidence[0].status,
DependencyPreflightStatus::MaterializationUnavailable
);
}
#[test]
fn test_should_force_local_fallback_for_unsafe_runtime_fail_open() {
let _guard = test_guard!();
assert!(should_force_local_fallback_for_runtime_fail_open(
DEPENDENCY_PREFLIGHT_CODE_POLICY
));
assert!(should_force_local_fallback_for_runtime_fail_open(
DEPENDENCY_PREFLIGHT_CODE_MATERIALIZATION
));
assert!(!should_force_local_fallback_for_runtime_fail_open(
DEPENDENCY_PREFLIGHT_CODE_UNKNOWN
));
assert!(!should_force_local_fallback_for_runtime_fail_open(
DEPENDENCY_PREFLIGHT_CODE_TIMEOUT
));
}
#[test]
fn test_e2e_dependency_preflight_verified_success_path() {
let _guard = test_guard!();
let worker = make_test_worker_config("worker-success");
let entry = make_sync_entry("/data/projects/repo-success", true);
let manifest = entry
.local_root
.join("Cargo.toml")
.to_string_lossy()
.to_string();
let outcomes = vec![(entry, SyncRootOutcome::Synced)];
let present = std::collections::BTreeSet::from([manifest]);
let missing = std::collections::BTreeSet::new();
let report = build_dependency_preflight_report(&worker, &outcomes, &present, &missing, None);
assert!(report.verified, "all-present manifests should verify");
assert!(report.reason_code.is_none());
assert!(report.remediation.is_none());
assert_eq!(report.evidence.len(), 1);
assert_eq!(
report.evidence[0].status,
DependencyPreflightStatus::Present,
"evidence must mark synced+present roots as present"
);
}
#[test]
fn test_build_dependency_preflight_report_uses_remote_manifest_paths() {
let _guard = test_guard!();
let worker = make_test_worker_config("worker-remote-paths");
let entry = SyncClosurePlanEntry {
local_root: PathBuf::from("/Users/jemanuel/projects/repo-success"),
remote_root: "/data/projects/repo-success".to_string(),
project_id: "id-remote-paths".to_string(),
root_hash: "hash-remote-paths".to_string(),
is_primary: true,
mode: SyncClosureMode::Full,
};
let outcomes = vec![(entry, SyncRootOutcome::Synced)];
let present =
std::collections::BTreeSet::from([String::from("/data/projects/repo-success/Cargo.toml")]);
let missing = std::collections::BTreeSet::new();
let report = build_dependency_preflight_report(&worker, &outcomes, &present, &missing, None);
assert!(report.verified, "remote manifest markers should verify");
assert_eq!(report.evidence.len(), 1);
assert_eq!(
report.evidence[0].root, "/data/projects/repo-success",
"evidence should report the remote synced root"
);
assert_eq!(
report.evidence[0].manifest, "/data/projects/repo-success/Cargo.toml",
"manifest matching must use remote paths from the probe"
);
assert_eq!(
report.evidence[0].status,
DependencyPreflightStatus::Present
);
}
#[test]
fn test_build_dependency_preflight_report_missing_stale_and_unknown_paths() {
let _guard = test_guard!();
let worker = make_test_worker_config("worker-mixed");
let synced_missing = make_sync_entry("/data/projects/repo-missing", true);
let skipped_stale = make_sync_entry("/data/projects/repo-stale", false);
let failed_unknown = make_sync_entry("/data/projects/repo-unknown", false);
let missing_manifest = synced_missing
.local_root
.join("Cargo.toml")
.to_string_lossy()
.to_string();
let outcomes = vec![
(synced_missing, SyncRootOutcome::Synced),
(
skipped_stale,
SyncRootOutcome::Skipped {
reason: "transfer skipped by estimator".to_string(),
},
),
(
failed_unknown,
SyncRootOutcome::Failed {
error: "rsync timeout".to_string(),
},
),
];
let present = std::collections::BTreeSet::new();
let missing = std::collections::BTreeSet::from([missing_manifest]);
let report = build_dependency_preflight_report(
&worker,
&outcomes,
&present,
&missing,
Some("probe returned missing markers"),
);
assert!(
!report.verified,
"missing primary root evidence must block remote execution"
);
assert_eq!(
report.reason_code,
Some(DEPENDENCY_PREFLIGHT_CODE_MISSING),
"missing primary should dominate failure reason"
);
assert_eq!(
report.remediation,
Some(DEPENDENCY_PREFLIGHT_REMEDIATION_MISSING)
);
assert!(
report
.evidence
.iter()
.any(|item| item.status == DependencyPreflightStatus::Missing)
);
assert!(
report
.evidence
.iter()
.any(|item| item.status == DependencyPreflightStatus::Stale)
);
assert!(
report
.evidence
.iter()
.any(|item| item.status == DependencyPreflightStatus::Unknown)
);
}
#[test]
fn test_e2e_dependency_preflight_stale_fallback_path_maps_reason_code() {
let _guard = test_guard!();
let worker = make_test_worker_config("worker-stale");
let stale_entry = make_sync_entry("/data/projects/repo-stale-only", true);
let outcomes = vec![(
stale_entry,
SyncRootOutcome::Skipped {
reason: "bandwidth guard skip".to_string(),
},
)];
let present = std::collections::BTreeSet::new();
let missing = std::collections::BTreeSet::new();
let report = build_dependency_preflight_report(&worker, &outcomes, &present, &missing, None);
assert!(!report.verified);
assert_eq!(report.reason_code, Some(DEPENDENCY_PREFLIGHT_CODE_STALE));
assert_eq!(
report.remediation,
Some(DEPENDENCY_PREFLIGHT_REMEDIATION_STALE)
);
}
#[test]
fn test_e2e_dependency_preflight_missing_fallback_path_maps_reason_code() {
let _guard = test_guard!();
let worker = make_test_worker_config("worker-missing");
let entry = make_sync_entry("/data/projects/repo-missing-only", true);
let manifest = entry
.local_root
.join("Cargo.toml")
.to_string_lossy()
.to_string();
let outcomes = vec![(entry, SyncRootOutcome::Synced)];
let present = std::collections::BTreeSet::new();
let missing = std::collections::BTreeSet::from([manifest]);
let report = build_dependency_preflight_report(&worker, &outcomes, &present, &missing, None);
assert!(!report.verified);
assert_eq!(report.reason_code, Some(DEPENDENCY_PREFLIGHT_CODE_MISSING));
assert_eq!(
report.remediation,
Some(DEPENDENCY_PREFLIGHT_REMEDIATION_MISSING)
);
}
#[test]
fn test_cargo_package_source_entrypoints_include_auto_discovered_targets() {
let _guard = test_guard!();
let temp_dir = tempfile::tempdir().expect("temp dir");
let package_root = temp_dir.path().join("auto-targets");
for dir in [
"src",
"src/bin/nested",
"examples/demo",
"tests/integration",
"benches/speed",
] {
std::fs::create_dir_all(package_root.join(dir)).expect("create target dir");
}
std::fs::write(
package_root.join("Cargo.toml"),
r#"[package]
name = "auto-targets"
version = "0.1.0"
edition = "2024"
"#,
)
.expect("write manifest");
for path in [
"src/lib.rs",
"src/main.rs",
"src/bin/tool.rs",
"src/bin/nested/main.rs",
"examples/example.rs",
"examples/demo/main.rs",
"tests/integration.rs",
"tests/integration/main.rs",
"benches/speed.rs",
"benches/speed/main.rs",
] {
std::fs::write(package_root.join(path), "fn main() {}\n").expect("write entrypoint");
}
let entrypoints = cargo_package_source_entrypoints(&package_root);
for path in [
"src/lib.rs",
"src/main.rs",
"src/bin/tool.rs",
"src/bin/nested/main.rs",
"examples/example.rs",
"examples/demo/main.rs",
"tests/integration.rs",
"tests/integration/main.rs",
"benches/speed.rs",
"benches/speed/main.rs",
] {
assert!(
entrypoints.contains(&PathBuf::from(path)),
"missing auto-discovered entrypoint {path}"
);
}
}
#[test]
fn test_cargo_package_source_entrypoints_respect_auto_discovery_flags() {
let _guard = test_guard!();
let temp_dir = tempfile::tempdir().expect("temp dir");
let package_root = temp_dir.path().join("manual-targets");
for dir in ["src/bin", "examples", "tests", "benches", "custom"] {
std::fs::create_dir_all(package_root.join(dir)).expect("create target dir");
}
std::fs::write(
package_root.join("Cargo.toml"),
r#"[package]
name = "manual-targets"
version = "0.1.0"
edition = "2024"
autolib = false
autobins = false
autoexamples = false
autotests = false
autobenches = false
[lib]
path = "custom/lib.rs"
[[bin]]
path = "custom/bin.rs"
"#,
)
.expect("write manifest");
for path in [
"src/lib.rs",
"src/main.rs",
"src/bin/tool.rs",
"examples/example.rs",
"tests/integration.rs",
"benches/speed.rs",
"custom/lib.rs",
"custom/bin.rs",
] {
std::fs::write(package_root.join(path), "fn main() {}\n").expect("write entrypoint");
}
let entrypoints = cargo_package_source_entrypoints(&package_root);
assert_eq!(
entrypoints,
vec![
PathBuf::from("custom/bin.rs"),
PathBuf::from("custom/lib.rs")
]
);
}
#[test]
fn test_workspace_member_source_entrypoints_include_all_targets_and_exclusions() {
let _guard = test_guard!();
let temp_dir = tempfile::tempdir().expect("temp dir");
let workspace_root = temp_dir.path().join("workspace");
for dir in [
"crates/core/src",
"crates/core/benches",
"crates/core/examples",
"crates/atlas-types/src",
"crates/skipped/src",
"tools/cli/src",
] {
std::fs::create_dir_all(workspace_root.join(dir)).expect("create workspace dir");
}
std::fs::write(
workspace_root.join("Cargo.toml"),
r#"[workspace]
members = ["crates/*", "tools/cli"]
exclude = ["crates/skipped"]
"#,
)
.expect("write workspace manifest");
for (manifest, name) in [
("crates/core/Cargo.toml", "core"),
("crates/atlas-types/Cargo.toml", "atlas-types"),
("crates/skipped/Cargo.toml", "skipped"),
("tools/cli/Cargo.toml", "cli"),
] {
std::fs::write(
workspace_root.join(manifest),
format!(
r#"[package]
name = "{name}"
version = "0.1.0"
edition = "2024"
"#
),
)
.expect("write member manifest");
}
for path in [
"crates/core/src/lib.rs",
"crates/core/benches/interval_tree_bench.rs",
"crates/core/examples/atlas_packing_attestation.rs",
"crates/atlas-types/src/lib.rs",
"crates/skipped/src/lib.rs",
"tools/cli/src/lib.rs",
] {
std::fs::write(workspace_root.join(path), "pub fn marker() {}\n")
.expect("write member entrypoint");
}
let entrypoints = cargo_workspace_member_source_entrypoints(&workspace_root);
for path in [
"crates/core/Cargo.toml",
"crates/core/src/lib.rs",
"crates/core/benches/interval_tree_bench.rs",
"crates/core/examples/atlas_packing_attestation.rs",
"crates/atlas-types/Cargo.toml",
"crates/atlas-types/src/lib.rs",
"tools/cli/Cargo.toml",
"tools/cli/src/lib.rs",
] {
assert!(
entrypoints.contains(&PathBuf::from(path)),
"missing workspace member entrypoint {path}"
);
}
assert!(
!entrypoints
.iter()
.any(|path| path.starts_with("crates/skipped")),
"workspace exclude entries must not be preflighted"
);
}
#[test]
fn test_dependency_preflight_checks_expand_virtual_workspace_all_targets() {
let _guard = test_guard!();
let temp_dir = tempfile::tempdir().expect("temp dir");
let workspace_root = temp_dir.path().join("frankenterm");
for dir in [
"crates/frankenterm-core/src",
"crates/frankenterm-core/benches",
"crates/frankenterm-core/examples",
"crates/frankenterm-core-atlas-pack-types/src",
"crates/frankenterm-core-connectors/src",
"crates/skipped/src",
] {
std::fs::create_dir_all(workspace_root.join(dir)).expect("create workspace dir");
}
std::fs::write(
workspace_root.join("Cargo.toml"),
r#"[workspace]
members = ["crates/frankenterm-core", "crates/frankenterm-core-*", "crates/skipped"]
exclude = ["crates/skipped"]
"#,
)
.expect("write workspace manifest");
for (manifest, name) in [
("crates/frankenterm-core/Cargo.toml", "frankenterm-core"),
(
"crates/frankenterm-core-atlas-pack-types/Cargo.toml",
"frankenterm-core-atlas-pack-types",
),
(
"crates/frankenterm-core-connectors/Cargo.toml",
"frankenterm-core-connectors",
),
("crates/skipped/Cargo.toml", "skipped"),
] {
std::fs::write(
workspace_root.join(manifest),
format!(
r#"[package]
name = "{name}"
version = "0.1.0"
edition = "2024"
"#
),
)
.expect("write member manifest");
}
for path in [
"crates/frankenterm-core/src/lib.rs",
"crates/frankenterm-core/benches/interval_tree_bench.rs",
"crates/frankenterm-core/examples/atlas_packing_attestation.rs",
"crates/frankenterm-core-atlas-pack-types/src/lib.rs",
"crates/frankenterm-core-connectors/src/lib.rs",
"crates/skipped/src/lib.rs",
] {
std::fs::write(workspace_root.join(path), "pub fn marker() {}\n")
.expect("write member entrypoint");
}
let entry = SyncClosurePlanEntry {
local_root: workspace_root,
remote_root: "/data/projects/frankenterm".to_string(),
project_id: "frankenterm".to_string(),
root_hash: "frankenterm-hash".to_string(),
is_primary: true,
mode: SyncClosureMode::Full,
};
let checks = dependency_preflight_checks_for_entry(&entry);
let required_paths = checks
.iter()
.map(|check| check.required_path.as_str())
.collect::<std::collections::BTreeSet<_>>();
for path in [
"/data/projects/frankenterm/Cargo.toml",
"/data/projects/frankenterm/crates/frankenterm-core/Cargo.toml",
"/data/projects/frankenterm/crates/frankenterm-core/src/lib.rs",
"/data/projects/frankenterm/crates/frankenterm-core/benches/interval_tree_bench.rs",
"/data/projects/frankenterm/crates/frankenterm-core/examples/atlas_packing_attestation.rs",
"/data/projects/frankenterm/crates/frankenterm-core-atlas-pack-types/src/lib.rs",
"/data/projects/frankenterm/crates/frankenterm-core-connectors/src/lib.rs",
] {
assert!(
required_paths.contains(path),
"missing dependency preflight check for {path}"
);
}
assert!(
!required_paths
.iter()
.any(|path| path.contains("/crates/skipped/")),
"workspace excluded members must not be preflighted"
);
}
#[test]
fn test_dependency_preflight_blocks_missing_source_entrypoint() {
let _guard = test_guard!();
let worker = make_test_worker_config("worker-missing-source");
let temp_dir = tempfile::tempdir().expect("temp dir");
let package_root = temp_dir.path().join("member");
std::fs::create_dir_all(package_root.join("src")).expect("create src");
std::fs::write(
package_root.join("Cargo.toml"),
r#"[package]
name = "member"
version = "0.1.0"
edition = "2024"
"#,
)
.expect("write manifest");
std::fs::write(package_root.join("src/lib.rs"), "pub fn member() {}\n").expect("write lib");
let entry = SyncClosurePlanEntry {
local_root: package_root,
remote_root: "/data/projects/app/crates/member".to_string(),
project_id: "member".to_string(),
root_hash: "member-hash".to_string(),
is_primary: false,
mode: SyncClosureMode::Full,
};
let outcomes = vec![(entry, SyncRootOutcome::Synced)];
let present = std::collections::BTreeSet::from([String::from(
"/data/projects/app/crates/member/Cargo.toml",
)]);
let missing = std::collections::BTreeSet::from([String::from(
"/data/projects/app/crates/member/src/lib.rs",
)]);
let report = build_dependency_preflight_report(&worker, &outcomes, &present, &missing, None);
assert!(
!report.verified,
"a synced root with a missing package source entrypoint must not reach Cargo"
);
assert_eq!(report.reason_code, Some(DEPENDENCY_PREFLIGHT_CODE_MISSING));
assert!(report.evidence.iter().any(|item| {
item.required_kind == "source_entrypoint"
&& item.required_path == "/data/projects/app/crates/member/src/lib.rs"
&& item.status == DependencyPreflightStatus::Missing
}));
let failure = DependencyPreflightFailure::from_report(report);
let summary = failure.evidence_summary();
assert!(
summary.contains("/data/projects/app/crates/member/src/lib.rs"),
"summary should expose the missing path, got {summary}"
);
assert!(
summary.contains("missing source_entrypoint"),
"summary should expose the failure class and path kind, got {summary}"
);
}
#[test]
fn test_dependency_preflight_probe_failure_compacts_unknown_source_entrypoints() {
let _guard = test_guard!();
let worker = make_test_worker_config("worker-probe-reset");
let temp_dir = tempfile::tempdir().expect("temp dir");
let package_root = temp_dir.path().join("large-member");
std::fs::create_dir_all(package_root.join("src")).expect("create src");
std::fs::create_dir_all(package_root.join("tests")).expect("create tests");
std::fs::write(
package_root.join("Cargo.toml"),
r#"[package]
name = "large-member"
version = "0.1.0"
edition = "2024"
"#,
)
.expect("write manifest");
std::fs::write(package_root.join("src/lib.rs"), "pub fn member() {}\n").expect("write lib");
for idx in 0..150 {
std::fs::write(
package_root.join("tests").join(format!("case_{idx}.rs")),
"#[test]\nfn case() {}\n",
)
.expect("write test entrypoint");
}
let entry = SyncClosurePlanEntry {
local_root: package_root,
remote_root: "/data/projects/app/crates/large-member".to_string(),
project_id: "large-member".to_string(),
root_hash: "large-member-hash".to_string(),
is_primary: false,
mode: SyncClosureMode::Full,
};
let outcomes = vec![(entry, SyncRootOutcome::Synced)];
let present = std::collections::BTreeSet::new();
let missing = std::collections::BTreeSet::new();
let report = build_dependency_preflight_report(
&worker,
&outcomes,
&present,
&missing,
Some("probe exited with status Some(255); connection reset"),
);
assert!(!report.verified);
assert_eq!(report.reason_code, Some(DEPENDENCY_PREFLIGHT_CODE_UNKNOWN));
let unknown_source_entrypoints = report
.evidence
.iter()
.filter(|item| {
item.status == DependencyPreflightStatus::Unknown
&& item.required_kind == "source_entrypoint"
})
.collect::<Vec<_>>();
assert_eq!(
unknown_source_entrypoints.len(),
1,
"transport failures should keep one sample per root/kind instead of duplicating every source entrypoint"
);
assert!(
unknown_source_entrypoints[0]
.detail
.contains("additional unreported paths"),
"unknown sample should explain why the report is compacted"
);
assert!(
report.evidence.len() < 10,
"large all-unknown reports should be compact, got {} evidence rows",
report.evidence.len()
);
}
#[tokio::test]
async fn test_verify_remote_dependency_manifests_blocks_stale_outcomes_deterministically() {
let _guard = test_guard!();
mock::set_thread_mock_override(Some(false));
let worker = make_test_worker_config("worker-stale-verify");
let outcomes = vec![(
make_sync_entry("/data/projects/repo-stale-verify", true),
SyncRootOutcome::Skipped {
reason: "transfer budget skip".to_string(),
},
)];
let reporter = HookReporter::new(OutputVisibility::Verbose);
let err = verify_remote_dependency_manifests(&worker, &outcomes, &reporter, None)
.await
.expect_err("stale dependency evidence should block remote execution");
let preflight = err
.downcast_ref::<DependencyPreflightFailure>()
.expect("error should preserve DependencyPreflightFailure type");
assert_eq!(preflight.reason_code, DEPENDENCY_PREFLIGHT_CODE_STALE);
assert_eq!(
preflight.remediation,
format!(
"{} To explicitly refresh this worker's RCH-managed source cache, run `rch sync --worker worker-stale-verify --force`, then rerun the Cargo command.",
DEPENDENCY_PREFLIGHT_REMEDIATION_STALE
)
);
mock::set_thread_mock_override(None);
}
#[test]
fn stale_source_exact_cargo_closure_sync_requires_content_checks() {
let _guard = test_guard!();
for (exact_dependency_closure_sync, source_content_receipt, expected_checksum) in [
(false, false, false),
(true, false, true),
(false, true, true),
(true, true, true),
] {
let pipeline = TransferPipeline::new(
PathBuf::from("/tmp/exact-closure-root"),
"exact-closure-root".to_string(),
"abc123".to_string(),
TransferConfig::default(),
);
let configured = apply_source_sync_integrity_policy(
pipeline,
exact_dependency_closure_sync,
source_content_receipt,
);
let (_, _, _, checksum_transfer) = configured.source_content_filter_policy();
assert_eq!(
checksum_transfer, expected_checksum,
"exact Cargo closure and receipt syncs must compare source bytes"
);
}
}
#[test]
fn test_non_primary_missing_deps_block_preflight() {
let _guard = test_guard!();
let worker = make_test_worker_config("worker-non-primary");
let primary = make_sync_entry("/data/projects/main-project", true);
let dep = make_sync_entry("/data/projects/sibling-dep", false);
let primary_manifest = primary
.local_root
.join("Cargo.toml")
.to_string_lossy()
.to_string();
let dep_manifest = dep
.local_root
.join("Cargo.toml")
.to_string_lossy()
.to_string();
let outcomes = vec![
(primary, SyncRootOutcome::Synced),
(dep, SyncRootOutcome::Synced),
];
let present = std::collections::BTreeSet::from([primary_manifest]);
let missing = std::collections::BTreeSet::from([dep_manifest]);
let report = build_dependency_preflight_report(&worker, &outcomes, &present, &missing, None);
assert!(
!report.verified,
"non-primary missing dep must block preflight to avoid stale sibling builds"
);
assert_eq!(report.reason_code, Some(DEPENDENCY_PREFLIGHT_CODE_MISSING));
}
#[test]
fn test_non_primary_stale_deps_block_preflight() {
let _guard = test_guard!();
let worker = make_test_worker_config("worker-non-primary-stale");
let primary = make_sync_entry("/data/projects/main-project", true);
let dep = make_sync_entry("/data/projects/sibling-dep-stale", false);
let primary_manifest = primary
.local_root
.join("Cargo.toml")
.to_string_lossy()
.to_string();
let outcomes = vec![
(primary, SyncRootOutcome::Synced),
(
dep,
SyncRootOutcome::Skipped {
reason: "estimator skip".to_string(),
},
),
];
let present = std::collections::BTreeSet::from([primary_manifest]);
let missing = std::collections::BTreeSet::new();
let report = build_dependency_preflight_report(&worker, &outcomes, &present, &missing, None);
assert!(
!report.verified,
"non-primary stale dep must block preflight to avoid stale sibling builds"
);
assert_eq!(report.reason_code, Some(DEPENDENCY_PREFLIGHT_CODE_STALE));
}
#[test]
fn test_build_sync_closure_plan_deterministic_under_permutation() {
let _guard = test_guard!();
let (temp_dir, policy) = topology_tempdir();
let project_root = temp_dir.path().join("project");
let dep_a = temp_dir.path().join("dep_a");
let dep_b = temp_dir.path().join("dep_b");
std::fs::create_dir_all(&project_root).expect("create project root");
std::fs::create_dir_all(&dep_a).expect("create dep_a");
std::fs::create_dir_all(&dep_b).expect("create dep_b");
let project_hash = "1234abcd";
let plan_a = build_sync_closure_plan(
&[dep_b.clone(), project_root.clone(), dep_a.clone()],
&project_root,
project_hash,
&policy,
);
let plan_b = build_sync_closure_plan(
&[dep_a.clone(), dep_b.clone(), project_root.clone()],
&project_root,
project_hash,
&policy,
);
assert_eq!(plan_a, plan_b, "sync closure plan should be deterministic");
assert!(
plan_a
.iter()
.any(|entry| entry.is_primary && entry.root_hash == project_hash),
"primary root must retain the closure hash"
);
}
#[cfg(unix)]
#[test]
fn test_build_sync_closure_plan_dedupes_alias_entries() {
let _guard = test_guard!();
use std::os::unix::fs::symlink;
let (temp_dir, policy) = topology_tempdir();
let project_root = temp_dir.path().join("project");
let dep = temp_dir.path().join("dep");
let dep_alias = temp_dir.path().join("dep_alias");
std::fs::create_dir_all(&project_root).expect("create project root");
std::fs::create_dir_all(&dep).expect("create dep root");
symlink(&dep, &dep_alias).expect("create dep alias symlink");
let dep_canonical = std::fs::canonicalize(&dep).expect("canonicalize dep");
let plan = build_sync_closure_plan(
&[dep_alias.clone(), dep.clone(), project_root.clone()],
&project_root,
"beefcafe",
&policy,
);
let dep_entries = plan
.iter()
.filter(|entry| {
std::fs::canonicalize(&entry.local_root)
.map(|canonical| canonical == dep_canonical)
.unwrap_or(false)
})
.count();
assert_eq!(dep_entries, 1, "alias/canonical roots should deduplicate");
}
#[test]
fn test_build_sync_closure_plan_adds_workspace_metadata_for_member_roots() {
let _guard = test_guard!();
let (temp_dir, policy) = topology_tempdir();
let project_root = temp_dir.path().join("project");
let dep_workspace_root = temp_dir.path().join("dep_workspace");
let dep_member_root = dep_workspace_root.join("crates/member");
std::fs::create_dir_all(&project_root).expect("create project root");
std::fs::create_dir_all(&dep_member_root).expect("create member root");
std::fs::write(
dep_workspace_root.join("Cargo.toml"),
r#"[workspace]
members = ["crates/member"]
"#,
)
.expect("write workspace manifest");
std::fs::write(
dep_member_root.join("Cargo.toml"),
r#"[package]
name = "member"
version = "0.1.0"
edition = "2024"
"#,
)
.expect("write member manifest");
let plan = build_sync_closure_plan(
&[dep_member_root.clone(), project_root.clone()],
&project_root,
"workspace_hash",
&policy,
);
assert!(
plan.iter().any(|entry| entry.local_root == dep_member_root
&& entry.mode == SyncClosureMode::Full
&& !entry.is_primary),
"workspace member root should remain a full sync root"
);
assert!(
plan.iter()
.any(|entry| entry.local_root == dep_workspace_root
&& entry.mode == SyncClosureMode::WorkspaceMetadata
&& !entry.is_primary),
"workspace member roots should add a thin workspace metadata sync"
);
assert!(
!plan
.iter()
.any(|entry| entry.local_root == dep_workspace_root
&& entry.mode == SyncClosureMode::Full),
"workspace root should not become a full sync root unless it was explicitly requested"
);
}
#[test]
fn test_build_sync_closure_plan_collapses_intra_workspace_members() {
let _guard = test_guard!();
let (temp_dir, policy) = topology_tempdir();
let workspace_root = temp_dir.path().join("workspace");
let alpha = workspace_root.join("crates/alpha");
let beta = workspace_root.join("crates/beta");
let gamma = workspace_root.join("crates/gamma");
for dir in [&alpha, &beta, &gamma] {
std::fs::create_dir_all(dir).expect("create member dir");
}
let plan = build_sync_closure_plan(
&[
workspace_root.clone(),
alpha.clone(),
beta.clone(),
gamma.clone(),
],
&workspace_root,
"ws_hash",
&policy,
);
assert_eq!(
plan.len(),
1,
"intra-workspace members must collapse to the single workspace root, got {:?}",
plan.iter()
.map(|e| e.local_root.clone())
.collect::<Vec<_>>()
);
let primary = &plan[0];
assert!(primary.is_primary, "the sole root must be the primary");
assert_eq!(primary.local_root, workspace_root);
assert_eq!(primary.mode, SyncClosureMode::Full);
}
#[test]
fn test_build_dependency_runtime_plan_keeps_workspace_member_roots() {
let _guard = test_guard!();
let (temp_dir, policy) = topology_tempdir();
let project_root = temp_dir.path().join("project");
let dep_workspace_root = temp_dir.path().join("dep_workspace");
let dep_member_root = dep_workspace_root.join("crates/member");
std::fs::create_dir_all(project_root.join("src")).expect("create project src");
std::fs::create_dir_all(dep_member_root.join("src")).expect("create member src");
std::fs::write(
project_root.join("Cargo.toml"),
r#"[package]
name = "project"
version = "0.1.0"
edition = "2024"
[dependencies]
member = { path = "../dep_workspace/crates/member" }
"#,
)
.expect("write project manifest");
std::fs::write(project_root.join("src/lib.rs"), "pub fn project() {}\n")
.expect("write project lib");
std::fs::write(
dep_workspace_root.join("Cargo.toml"),
r#"[workspace]
members = ["crates/member"]
"#,
)
.expect("write workspace manifest");
std::fs::write(
dep_member_root.join("Cargo.toml"),
r#"[package]
name = "member"
version = "0.1.0"
edition = "2024"
"#,
)
.expect("write member manifest");
std::fs::write(dep_member_root.join("src/lib.rs"), "pub fn member() {}\n")
.expect("write member lib");
let project_root = std::fs::canonicalize(&project_root).expect("canonicalize project");
let dep_workspace_root =
std::fs::canonicalize(&dep_workspace_root).expect("canonicalize workspace");
let dep_member_root = std::fs::canonicalize(&dep_member_root).expect("canonicalize member");
let reporter = HookReporter::new(OutputVisibility::None);
let plan = build_dependency_runtime_plan(
&project_root,
Some(CompilationKind::CargoCheck),
&reporter,
&policy,
);
assert!(
plan.fail_open_decision.is_none(),
"dependency runtime planning should stay on the ready path"
);
assert!(
plan.sync_roots.contains(&dep_member_root),
"workspace member root must stay in the runtime sync roots"
);
assert!(
!plan.sync_roots.contains(&dep_workspace_root),
"workspace root should be added later as metadata-only sync, not full runtime root"
);
assert!(
plan.sync_roots.contains(&project_root),
"primary project root must remain in the sync roots"
);
}
#[tokio::test]
#[serial(mock_global)]
async fn registered_preflight_rejection_sends_heartbeat_and_stops_guard() {
let _lock = test_lock().lock().await;
let _guard = test_guard!();
let (temp_dir, policy) = topology_tempdir();
let retained = temp_dir._dir.keep();
let socket = retained.join("heartbeat.sock");
let listener = UnixListener::bind(&socket).expect("bind heartbeat receiver");
let socket_path = socket.to_string_lossy().into_owned();
let (received_tx, mut received_rx) = tokio::sync::mpsc::unbounded_channel();
let receiver = tokio::spawn(async move {
loop {
let (stream, _) = listener.accept().await.expect("accept heartbeat");
let (reader, mut writer) = stream.into_split();
let mut reader = TokioBufReader::new(reader);
let mut request = String::new();
if reader.read_line(&mut request).await.expect("read route") == 0 {
continue;
}
assert_eq!(request, "POST /build-heartbeat\n");
let mut body = String::new();
if reader.read_line(&mut body).await.expect("read heartbeat") == 0 {
continue;
}
let heartbeat: BuildHeartbeatRequest = serde_json::from_str(&body).unwrap();
let response = serde_json::json!({
"status": "ok",
"build_id": heartbeat.build_id,
"worker_id": heartbeat.worker_id,
"phase": match heartbeat.phase {
BuildHeartbeatPhase::SyncUp => "sync_up",
BuildHeartbeatPhase::Execute => "execute",
BuildHeartbeatPhase::SyncDown => "sync_down",
BuildHeartbeatPhase::Finalize => "finalize",
},
});
let response =
format!("HTTP/1.0 200 OK\r\nContent-Type: application/json\r\n\r\n{response}\n");
received_tx.send(heartbeat).expect("retain heartbeat");
if let Err(error) = writer.write_all(response.as_bytes()).await {
assert!(
matches!(
error.kind(),
std::io::ErrorKind::BrokenPipe | std::io::ErrorKind::ConnectionReset
),
"unexpected heartbeat acknowledgement error: {error}"
);
}
}
});
let worker = SelectedWorker {
id: WorkerId::new("preflight-worker"),
host: "unused.invalid".to_string(),
user: "unused".to_string(),
identity_file: "unused".to_string(),
slots_available: 1,
speed_score: 1.0,
declared_os: Some("windows".to_string()),
};
let reporter = HookReporter::new(OutputVisibility::None);
let result = Box::pin(execute_remote_compilation(
&worker,
"cargo check",
TransferConfig::default(),
Vec::new(),
None,
&rch_common::CompilationConfig::default(),
None,
Some(CompilationKind::CargoCheck),
&reporter,
&socket_path,
ColorMode::Auto,
Some(71),
Some("preflight-wrapper"),
None,
&policy,
None,
true,
&[],
&[],
rch_common::remediation_config::DEFAULT_POOLED_REAPER_POOLED_IDLE_HOURS,
None,
))
.await;
assert!(result.unwrap_err().to_string().contains("require the Unix"));
let heartbeat = received_rx.try_recv().expect("heartbeat before rejection");
assert_eq!(heartbeat.build_id, 71);
assert_eq!(heartbeat.worker_id, worker.id);
assert_eq!(heartbeat.hook_pid, Some(std::process::id()));
assert_eq!(
heartbeat.local_wrapper_id.as_deref(),
Some("preflight-wrapper")
);
assert_eq!(heartbeat.phase, BuildHeartbeatPhase::SyncUp);
assert_eq!(heartbeat.detail.as_deref(), Some("source_validation"));
assert!(heartbeat.remote_pgid_file.is_none());
tokio::task::yield_now().await;
while received_rx.try_recv().is_ok() {}
assert!(
tokio::time::timeout(Duration::from_secs(6), received_rx.recv())
.await
.is_err()
);
let heartbeat_loop = BuildHeartbeatLoop::start(
&socket_path,
72,
&worker.id,
Some("preflight-wrapper"),
None,
);
heartbeat_loop.update_phase(
BuildHeartbeatPhase::SyncUp,
Some("source_validation".to_string()),
);
let first = tokio::time::timeout(Duration::from_secs(6), received_rx.recv())
.await
.expect("initial interval heartbeat")
.unwrap();
let next = tokio::time::timeout(Duration::from_secs(6), received_rx.recv())
.await
.expect("periodic preflight heartbeat")
.unwrap();
assert_eq!(first.build_id, 72);
assert_eq!(next.build_id, first.build_id);
assert_eq!(next.progress_counter, first.progress_counter);
assert_eq!(next.detail.as_deref(), Some("source_validation"));
assert!(next.remote_pgid_file.is_none());
drop(heartbeat_loop);
receiver.abort();
assert!(receiver.await.unwrap_err().is_cancelled());
}
#[tokio::test]
#[serial(mock_global)]
async fn test_execute_remote_compilation_syncs_custom_cargo_target_dir_artifacts() {
let _lock = test_lock().lock().await;
let _guard = test_guard!();
let socket_path = format!(
"/tmp/rch_test_custom_target_artifacts_{}_{}.sock",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("clock should be after epoch")
.as_nanos()
);
let _overrides = TestOverridesGuard::set(
&socket_path,
MockConfig::default(),
MockRsyncConfig::success(),
);
mock::clear_global_invocations();
let (temp_dir, policy) = topology_tempdir();
let project_dir = temp_dir.path().join("remote_compilation_helper");
std::fs::create_dir_all(&project_dir).expect("create project dir");
let custom_target_dir_path = project_dir.join(".rch-test-target-cache");
let custom_target_dir = custom_target_dir_path.to_string_lossy().to_string();
let prev_cwd = std::env::current_dir().ok();
std::env::set_current_dir(&project_dir).expect("cd into project dir");
let worker = SelectedWorker {
id: rch_common::WorkerId::new("mock-worker"),
host: "mock.host.local".to_string(),
user: "mockuser".to_string(),
identity_file: "~/.ssh/mock_key".to_string(),
slots_available: 8,
speed_score: 90.0,
declared_os: None,
};
let reporter = HookReporter::new(OutputVisibility::None);
let result = Box::pin(execute_remote_compilation(
&worker,
"cargo build",
TransferConfig::default(),
Vec::new(),
Some(PathBuf::from(&custom_target_dir)),
&rch_common::CompilationConfig::default(),
None,
Some(CompilationKind::CargoBuild),
&reporter,
&socket_path,
ColorMode::Auto,
None,
None,
None,
&policy,
None,
false,
&[],
&[],
rch_common::remediation_config::DEFAULT_POOLED_REAPER_POOLED_IDLE_HOURS,
None,
))
.await;
if let Some(prev) = prev_cwd {
let _ = std::env::set_current_dir(prev);
}
let execution = result.expect("remote execution should succeed in mock mode");
assert_eq!(execution.exit_code, 0);
let rsync_logs = mock::global_rsync_invocations_snapshot();
let custom_target_artifact_sync = rsync_logs
.iter()
.find(|entry| {
entry.phase == mock::Phase::Artifacts
&& entry.destination == custom_target_dir
&& entry.source.contains(".rch-target")
})
.expect(
"expected artifact retrieval into custom CARGO_TARGET_DIR from worker .rch-target path",
);
assert!(
custom_target_artifact_sync
.source
.contains(".rch-target-mock-worker-"),
"expected per-job remote target dir, got {}",
custom_target_artifact_sync.source
);
assert!(
!custom_target_artifact_sync.source.contains("/.rch-target/"),
"custom target sync must not use the shared .rch-target dir: {}",
custom_target_artifact_sync.source
);
let ssh_logs = mock::global_ssh_invocations_snapshot();
let execute_command = ssh_logs
.iter()
.find(|entry| entry.phase == mock::Phase::Execute)
.and_then(|entry| entry.command.as_deref())
.expect("execute command should be recorded");
assert!(
execute_command.contains("CARGO_TARGET_DIR=")
&& execute_command.contains(".rch-target-mock-worker-"),
"expected remote Cargo execution to force per-job worker CARGO_TARGET_DIR, got {execute_command}"
);
}
#[tokio::test]
#[serial(mock_global)]
async fn test_terminal_source_sync_failure_never_launches_remote_cargo() {
let _lock = test_lock().lock().await;
let _guard = test_guard!();
let socket_path = format!(
"/tmp/rch_test_pre_cargo_sync_failure_{}_{}.sock",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("clock should be after epoch")
.as_nanos()
);
let rsync_config = MockRsyncConfig {
fail_sync_attempts: 3,
..MockRsyncConfig::success()
};
let _overrides = TestOverridesGuard::set(&socket_path, MockConfig::default(), rsync_config);
mock::clear_global_invocations();
let (temp_dir, policy) = topology_tempdir();
let project_dir = temp_dir.path().join("remote_compilation_helper");
std::fs::create_dir_all(&project_dir).expect("create project dir");
let prev_cwd = std::env::current_dir().ok();
std::env::set_current_dir(&project_dir).expect("cd into project dir");
let worker = SelectedWorker {
id: rch_common::WorkerId::new("mock-worker"),
host: "mock.host.local".to_string(),
user: "mockuser".to_string(),
identity_file: "~/.ssh/mock_key".to_string(),
slots_available: 8,
speed_score: 90.0,
declared_os: None,
};
let transfer_config = TransferConfig {
sync_timeout_ms: Some(1_000),
retry: rch_common::RetryConfig {
max_attempts: 3,
base_delay_ms: 0,
max_delay_ms: 0,
jitter_factor: 0.0,
total_timeout_ms: 1,
},
..TransferConfig::default()
};
let reporter = HookReporter::new(OutputVisibility::Summary);
let result = Box::pin(execute_remote_compilation(
&worker,
"cargo check",
transfer_config,
Vec::new(),
None,
&rch_common::CompilationConfig::default(),
None,
Some(CompilationKind::CargoCheck),
&reporter,
&socket_path,
ColorMode::Auto,
None,
None,
None,
&policy,
None,
false,
&[],
&[],
rch_common::remediation_config::DEFAULT_POOLED_REAPER_POOLED_IDLE_HOURS,
None,
))
.await;
if let Some(prev) = prev_cwd {
let _ = std::env::set_current_dir(prev);
}
let error = result.expect_err("source sync exhaustion must stop before Cargo");
let history = error
.downcast_ref::<crate::transfer::TransferAttemptsExhausted>()
.expect("typed source-transfer history");
assert_eq!(history.attempts.len(), 3);
assert_eq!(
mock::global_rsync_invocations_snapshot()
.iter()
.filter(|entry| entry.phase == mock::Phase::Sync)
.count(),
3
);
assert_eq!(
mock::global_ssh_invocations_snapshot()
.iter()
.filter(|entry| entry.phase == mock::Phase::Execute)
.count(),
0,
"terminal source-transfer failure must launch remote Cargo exactly zero times"
);
assert_eq!(
source_sync_terminal_summary(&history.attempts, false).as_deref(),
Some(
"[RCH] source sync failed before remote Cargo execution after 3/3 attempts; remote Cargo was not started: Mock: Sync failed (transient) - Connection timed out"
)
);
}
#[tokio::test]
#[serial(mock_global)]
async fn test_artifact_sync_failure_fails_an_artifact_producing_build() {
let _lock = test_lock().lock().await;
let _guard = test_guard!();
let socket_path = format!(
"/tmp/rch_test_artifact_fail_{}_{}.sock",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("clock should be after epoch")
.as_nanos()
);
let _overrides = TestOverridesGuard::set(
&socket_path,
MockConfig::default(),
MockRsyncConfig::artifact_failure(),
);
mock::clear_global_invocations();
let (temp_dir, policy) = topology_tempdir();
let project_dir = temp_dir.path().join("remote_compilation_helper");
std::fs::create_dir_all(&project_dir).expect("create project dir");
let prev_cwd = std::env::current_dir().ok();
std::env::set_current_dir(&project_dir).expect("cd into project dir");
let worker = SelectedWorker {
id: rch_common::WorkerId::new("mock-worker"),
host: "mock.host.local".to_string(),
user: "mockuser".to_string(),
identity_file: "~/.ssh/mock_key".to_string(),
slots_available: 8,
speed_score: 90.0,
declared_os: None,
};
let reporter = HookReporter::new(OutputVisibility::None);
let build = Box::pin(execute_remote_compilation(
&worker,
"cargo build",
TransferConfig::default(),
Vec::new(),
None,
&rch_common::CompilationConfig::default(),
None,
Some(CompilationKind::CargoBuild),
&reporter,
&socket_path,
ColorMode::Auto,
None,
None,
None,
&policy,
None,
false,
&[],
&[],
rch_common::remediation_config::DEFAULT_POOLED_REAPER_POOLED_IDLE_HOURS,
None,
))
.await;
mock::clear_global_invocations();
let test_run = Box::pin(execute_remote_compilation(
&worker,
"cargo test",
TransferConfig::default(),
Vec::new(),
None,
&rch_common::CompilationConfig::default(),
None,
Some(CompilationKind::CargoTest),
&reporter,
&socket_path,
ColorMode::Auto,
None,
None,
None,
&policy,
None,
false,
&[],
&[],
rch_common::remediation_config::DEFAULT_POOLED_REAPER_POOLED_IDLE_HOURS,
None,
))
.await;
if let Some(prev) = prev_cwd {
let _ = std::env::set_current_dir(prev);
}
let build = build.expect("remote execution should return Ok in mock mode");
assert_ne!(
build.exit_code, 0,
"a successful compile with a failed artifact sync-back must NOT exit 0"
);
assert_eq!(
build.exit_code, EXIT_ARTIFACT_TRANSFER_FAILED,
"artifact-transfer failure must surface the build-failure-class exit code"
);
let test_run = test_run.expect("remote execution should return Ok in mock mode");
assert_eq!(
test_run.exit_code, 0,
"cargo test streams its output; a missing artifact must not fail it"
);
}
#[tokio::test]
#[serial(mock_global)]
async fn test_cargo_test_delegates_to_rch_exec() {
let _lock = test_lock().lock().await;
let _guard = test_guard!();
mock::clear_global_invocations();
crate::config::set_test_config_override(Some(rch_common::RchConfig::default()));
let input = HookInput {
tool_name: "Bash".to_string(),
tool_input: ToolInput {
command: "cargo test".to_string(),
description: None,
},
session_id: None,
};
let output = process_hook(input).await;
crate::config::set_test_config_override(None);
assert!(
output.is_allow(),
"cargo test should be allowed via delegation"
);
let cmd = delegated_command(&output);
assert_eq!(cmd, "rch exec -- cargo test");
let rsync_logs = mock::global_rsync_invocations_snapshot();
let ssh_logs = mock::global_ssh_invocations_snapshot();
assert!(rsync_logs.is_empty(), "Hook should not invoke rsync");
assert!(ssh_logs.is_empty(), "Hook should not invoke SSH");
}
#[tokio::test]
#[serial(mock_global)]
async fn test_compound_chain_delegates_every_compilation_segment() {
let _lock = test_lock().lock().await;
let _guard = test_guard!();
mock::clear_global_invocations();
crate::config::set_test_config_override(Some(rch_common::RchConfig::default()));
let input = HookInput {
tool_name: "Bash".to_string(),
tool_input: ToolInput {
command: "cargo build --release && cargo test".to_string(),
description: None,
},
session_id: None,
};
let output = process_hook(input).await;
crate::config::set_test_config_override(None);
assert!(output.is_allow());
let cmd = delegated_command(&output);
assert_eq!(
cmd,
"rch exec -- cargo build --release && rch exec -- cargo test"
);
}
#[tokio::test]
#[serial(mock_global)]
async fn test_hook_leaves_non_allowlisted_tool_local() {
let _lock = test_lock().lock().await;
let _guard = test_guard!();
mock::clear_global_invocations();
let mut config = rch_common::RchConfig::default();
config.execution.allowlist = vec!["cargo".to_string()];
assert_eq!(
config.execution.missing_builtin_bases().first().copied(),
Some("rustc"),
"the allowlist gap must be discoverable from config"
);
assert!(config.execution.missing_builtin_bases().contains(&"go"));
crate::config::set_test_config_override(Some(config));
let input = HookInput {
tool_name: "Bash".to_string(),
tool_input: ToolInput {
command: "go test ./...".to_string(),
description: None,
},
session_id: None,
};
let output = process_hook(input).await;
crate::config::set_test_config_override(None);
assert!(output.is_allow());
assert!(
!matches!(output, HookOutput::AllowWithModifiedCommand(_)),
"non-allowlisted command must be allowed unchanged, got {output:?}"
);
}
#[test]
fn config_local_policy_matches_hook_semantics() {
let _guard = test_guard!();
let mut config = rch_common::RchConfig::default();
assert_eq!(
config_local_policy(&config, Some(CompilationKind::CargoBuild)),
None
);
assert_eq!(config_local_policy(&config, None), None);
config.general.force_local = true;
assert_eq!(
config_local_policy(&config, Some(CompilationKind::CargoBuild)),
Some(ConfigLocalPolicy::ForceLocal)
);
assert_eq!(
config_local_policy(&config, Some(CompilationKind::Job)),
Some(ConfigLocalPolicy::ForceLocal)
);
config.general.force_remote = true;
assert_eq!(
config_local_policy(&config, Some(CompilationKind::CargoBuild)),
Some(ConfigLocalPolicy::ConflictingForceFlags)
);
config.general.force_local = false;
config.general.force_remote = false;
config.general.enabled = false;
assert_eq!(
config_local_policy(&config, Some(CompilationKind::CargoBuild)),
Some(ConfigLocalPolicy::Disabled)
);
config.general.enabled = true;
config.execution.allowlist = vec!["cargo".to_string()];
assert_eq!(
config_local_policy(&config, Some(CompilationKind::GoTest)),
Some(ConfigLocalPolicy::NotAllowlisted("go"))
);
assert_eq!(
config_local_policy(&config, Some(CompilationKind::CargoTest)),
None
);
assert_eq!(
config_local_policy(&config, Some(CompilationKind::Job)),
None
);
assert!(ConfigLocalPolicy::ForceLocal.overrides_require_remote());
assert!(ConfigLocalPolicy::Disabled.overrides_require_remote());
assert!(ConfigLocalPolicy::ConflictingForceFlags.overrides_require_remote());
assert!(!ConfigLocalPolicy::NotAllowlisted("go").overrides_require_remote());
for policy in [
ConfigLocalPolicy::ForceLocal,
ConfigLocalPolicy::Disabled,
ConfigLocalPolicy::ConflictingForceFlags,
ConfigLocalPolicy::NotAllowlisted("go"),
] {
assert!(
!remote_required_refusal_is_retryable(&policy.reason()),
"{policy:?} must not be retryable"
);
}
assert!(remote_required_refusal_is_retryable("no workers available"));
assert_eq!(
ConfigLocalPolicy::NotAllowlisted("nix").reason(),
"command 'nix' not in execution.allowlist"
);
}
#[tokio::test]
#[serial(mock_global)]
async fn test_cargo_test_with_args_delegates_correctly() {
let _lock = test_lock().lock().await;
let _guard = test_guard!();
mock::clear_global_invocations();
crate::config::set_test_config_override(Some(rch_common::RchConfig::default()));
let input = HookInput {
tool_name: "Bash".to_string(),
tool_input: ToolInput {
command: "cargo test --release -- --nocapture".to_string(),
description: None,
},
session_id: None,
};
let output = process_hook(input).await;
crate::config::set_test_config_override(None);
assert!(output.is_allow());
let cmd = delegated_command(&output);
assert_eq!(cmd, "rch exec -- cargo test --release -- --nocapture");
let rsync_logs = mock::global_rsync_invocations_snapshot();
let ssh_logs = mock::global_ssh_invocations_snapshot();
assert!(rsync_logs.is_empty(), "Hook should not invoke rsync");
assert!(ssh_logs.is_empty(), "Hook should not invoke SSH");
}
#[tokio::test]
#[serial(mock_global)]
async fn test_cargo_test_remote_build_failure() {
let _lock = test_lock().lock().await;
let socket_path = format!(
"/tmp/rch_test_cargo_test_build_fail_{}_{}.sock",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
);
let _overrides = TestOverridesGuard::set(
&socket_path,
MockConfig {
default_exit_code: 1,
default_stderr: "error[E0425]: cannot find value `undefined_var` in this scope\n --> src/lib.rs:10:5\n".to_string(),
..MockConfig::default()
},
MockRsyncConfig::success(),
);
mock::clear_global_invocations();
let response = SelectionResponse {
worker: Some(SelectedWorker {
id: rch_common::WorkerId::new("test-worker"),
host: "test.host.local".to_string(),
user: "testuser".to_string(),
identity_file: "~/.ssh/test_key".to_string(),
slots_available: 8,
speed_score: 85.0,
declared_os: None,
}),
reason: SelectionReason::Success,
build_id: None,
diagnostics: None,
};
spawn_mock_daemon(&socket_path, response).await;
tokio::time::sleep(tokio::time::Duration::from_millis(25)).await;
let input = HookInput {
tool_name: "Bash".to_string(),
tool_input: ToolInput {
command: "cargo test".to_string(),
description: None,
},
session_id: None,
};
let output = process_hook(input).await;
let _ = std::fs::remove_file(&socket_path);
assert!(
output.is_allow(),
"cargo test build failure should use transparent interception"
);
assert!(
matches!(output, HookOutput::AllowWithModifiedCommand(_)),
"cargo test build failure should return AllowWithModifiedCommand"
);
}
#[tokio::test]
#[serial(mock_global)]
async fn test_cargo_test_with_filter() {
let _lock = test_lock().lock().await;
let socket_path = format!(
"/tmp/rch_test_cargo_test_filter_{}_{}.sock",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
);
let _overrides = TestOverridesGuard::set(
&socket_path,
MockConfig::default(),
MockRsyncConfig::success(),
);
mock::clear_global_invocations();
let response = SelectionResponse {
worker: Some(SelectedWorker {
id: rch_common::WorkerId::new("test-worker"),
host: "test.host.local".to_string(),
user: "testuser".to_string(),
identity_file: "~/.ssh/test_key".to_string(),
slots_available: 8,
speed_score: 85.0,
declared_os: None,
}),
reason: SelectionReason::Success,
build_id: None,
diagnostics: None,
};
spawn_mock_daemon(&socket_path, response).await;
tokio::time::sleep(tokio::time::Duration::from_millis(25)).await;
let input = HookInput {
tool_name: "Bash".to_string(),
tool_input: ToolInput {
command: "cargo test specific_test".to_string(),
description: None,
},
session_id: None,
};
let output = process_hook(input).await;
let _ = std::fs::remove_file(&socket_path);
assert!(
output.is_allow(),
"Filtered cargo test should use transparent interception"
);
assert!(
matches!(output, HookOutput::AllowWithModifiedCommand(_)),
"Filtered cargo test should return AllowWithModifiedCommand"
);
}
#[tokio::test]
#[serial(mock_global)]
async fn test_cargo_test_with_test_threads() {
let _lock = test_lock().lock().await;
let socket_path = format!(
"/tmp/rch_test_cargo_test_threads_{}_{}.sock",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
);
let _overrides = TestOverridesGuard::set(
&socket_path,
MockConfig::default(),
MockRsyncConfig::success(),
);
mock::clear_global_invocations();
let response = SelectionResponse {
worker: Some(SelectedWorker {
id: rch_common::WorkerId::new("test-worker"),
host: "test.host.local".to_string(),
user: "testuser".to_string(),
identity_file: "~/.ssh/test_key".to_string(),
slots_available: 8,
speed_score: 85.0,
declared_os: None,
}),
reason: SelectionReason::Success,
build_id: None,
diagnostics: None,
};
spawn_mock_daemon(&socket_path, response).await;
tokio::time::sleep(tokio::time::Duration::from_millis(25)).await;
let input = HookInput {
tool_name: "Bash".to_string(),
tool_input: ToolInput {
command: "cargo test -- --test-threads=4".to_string(),
description: None,
},
session_id: None,
};
let output = process_hook(input).await;
let _ = std::fs::remove_file(&socket_path);
assert!(
output.is_allow(),
"cargo test with --test-threads should use transparent interception"
);
assert!(
matches!(output, HookOutput::AllowWithModifiedCommand(_)),
"cargo test with --test-threads should return AllowWithModifiedCommand"
);
}
#[tokio::test]
#[serial(mock_global)]
async fn test_cargo_test_signal_killed() {
let _lock = test_lock().lock().await;
let socket_path = format!(
"/tmp/rch_test_cargo_test_signal_{}_{}.sock",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
);
let _overrides = TestOverridesGuard::set(
&socket_path,
MockConfig {
default_exit_code: 137,
default_stderr: "Killed\n".to_string(),
..MockConfig::default()
},
MockRsyncConfig::success(),
);
mock::clear_global_invocations();
let response = SelectionResponse {
worker: Some(SelectedWorker {
id: rch_common::WorkerId::new("test-worker"),
host: "test.host.local".to_string(),
user: "testuser".to_string(),
identity_file: "~/.ssh/test_key".to_string(),
slots_available: 8,
speed_score: 85.0,
declared_os: None,
}),
reason: SelectionReason::Success,
build_id: None,
diagnostics: None,
};
spawn_mock_daemon(&socket_path, response).await;
tokio::time::sleep(tokio::time::Duration::from_millis(25)).await;
let input = HookInput {
tool_name: "Bash".to_string(),
tool_input: ToolInput {
command: "cargo test".to_string(),
description: None,
},
session_id: None,
};
let output = process_hook(input).await;
let _ = std::fs::remove_file(&socket_path);
assert!(
output.is_allow(),
"Signal-killed cargo test should use transparent interception"
);
assert!(
matches!(output, HookOutput::AllowWithModifiedCommand(_)),
"Signal-killed cargo test should return AllowWithModifiedCommand"
);
}
#[tokio::test]
#[serial(mock_global)]
async fn test_cargo_test_signal_killed_with_toolchain_path_does_not_fallback_local() {
let _lock = test_lock().lock().await;
let socket_path = format!(
"/tmp/rch_test_cargo_test_signal_toolchain_path_{}_{}.sock",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
);
let stderr = "error: could not compile `serde` (lib)\nCaused by:\n process didn't exit successfully: `/home/ubuntu/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/bin/rustc --crate-name serde ...` (signal: 9, SIGKILL: kill)\n";
let _overrides = TestOverridesGuard::set(
&socket_path,
MockConfig {
default_exit_code: 137,
default_stderr: stderr.to_string(),
..MockConfig::default()
},
MockRsyncConfig::success(),
);
mock::clear_global_invocations();
let response = SelectionResponse {
worker: Some(SelectedWorker {
id: rch_common::WorkerId::new("test-worker"),
host: "test.host.local".to_string(),
user: "testuser".to_string(),
identity_file: "~/.ssh/test_key".to_string(),
slots_available: 8,
speed_score: 85.0,
declared_os: None,
}),
reason: SelectionReason::Success,
build_id: None,
diagnostics: None,
};
spawn_mock_daemon(&socket_path, response).await;
tokio::time::sleep(tokio::time::Duration::from_millis(25)).await;
let input = HookInput {
tool_name: "Bash".to_string(),
tool_input: ToolInput {
command: "cargo test".to_string(),
description: None,
},
session_id: None,
};
let output = process_hook(input).await;
let _ = std::fs::remove_file(&socket_path);
assert!(
matches!(output, HookOutput::AllowWithModifiedCommand(_)),
"signal-killed remote failures that mention .rustup/toolchains must preserve the remote exit code instead of falling back local"
);
}
#[tokio::test]
#[serial(mock_global)]
async fn test_cargo_test_toolchain_fallback() {
let _lock = test_lock().lock().await;
let socket_path = format!(
"/tmp/rch_test_cargo_test_toolchain_{}_{}.sock",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
);
let _overrides = TestOverridesGuard::set(
&socket_path,
MockConfig {
default_exit_code: 1,
default_stderr: "error: toolchain 'nightly-2025-01-15' is not installed\n".to_string(),
..MockConfig::default()
},
MockRsyncConfig::success(),
);
mock::clear_global_invocations();
let response = SelectionResponse {
worker: Some(SelectedWorker {
id: rch_common::WorkerId::new("test-worker"),
host: "test.host.local".to_string(),
user: "testuser".to_string(),
identity_file: "~/.ssh/test_key".to_string(),
slots_available: 8,
speed_score: 85.0,
declared_os: None,
}),
reason: SelectionReason::Success,
build_id: None,
diagnostics: None,
};
spawn_mock_daemon(&socket_path, response).await;
tokio::time::sleep(tokio::time::Duration::from_millis(25)).await;
let input = HookInput {
tool_name: "Bash".to_string(),
tool_input: ToolInput {
command: "cargo test".to_string(),
description: None,
},
session_id: None,
};
let output = process_hook(input).await;
let _ = std::fs::remove_file(&socket_path);
assert!(
output.is_allow(),
"Toolchain failure should allow local fallback"
);
}
#[test]
fn test_cargo_test_classification() {
let _guard = test_guard!();
let result = classify_command("cargo test");
assert!(result.is_compilation, "cargo test should be compilation");
assert_eq!(
result.kind,
Some(CompilationKind::CargoTest),
"Should be CargoTest kind"
);
let result = classify_command("cargo test specific_test");
assert!(result.is_compilation);
assert_eq!(result.kind, Some(CompilationKind::CargoTest));
let result = classify_command("cargo test -- --test-threads=4");
assert!(result.is_compilation);
assert_eq!(result.kind, Some(CompilationKind::CargoTest));
let result = classify_command("cargo test --release");
assert!(result.is_compilation);
assert_eq!(result.kind, Some(CompilationKind::CargoTest));
let result = classify_command("cargo test -p mypackage");
assert!(result.is_compilation);
assert_eq!(result.kind, Some(CompilationKind::CargoTest));
}
#[test]
fn test_cargo_nextest_classification() {
let _guard = test_guard!();
let result = classify_command("cargo nextest run");
assert!(result.is_compilation, "cargo nextest should be compilation");
assert_eq!(
result.kind,
Some(CompilationKind::CargoNextest),
"Should be CargoNextest kind"
);
let result = classify_command("cargo nextest run --no-fail-fast");
assert!(result.is_compilation);
assert_eq!(result.kind, Some(CompilationKind::CargoNextest));
}
#[test]
fn test_artifact_patterns_for_test_commands() {
let _guard = test_guard!();
let test_patterns = get_artifact_patterns(Some(CompilationKind::CargoTest), None);
let check_patterns = get_artifact_patterns(Some(CompilationKind::CargoCheck), None);
let clippy_patterns = get_artifact_patterns(Some(CompilationKind::CargoClippy), None);
let build_patterns = get_artifact_patterns(Some(CompilationKind::CargoBuild), None);
assert!(
!test_patterns.iter().any(|p| p == "target/"),
"Test artifacts should not include full target/"
);
assert!(
build_patterns.iter().any(|p| p == "target/debug/**"),
"Build artifacts should include target/debug/**"
);
assert!(
build_patterns.iter().any(|p| p == "target/release/**"),
"Build artifacts should include target/release/**"
);
assert!(
!test_patterns.iter().any(|p| p == "target/debug/**"),
"Test artifacts should not include target/debug/**"
);
assert!(
!test_patterns.iter().any(|p| p == "target/release/**"),
"Test artifacts should not include target/release/**"
);
assert!(
!check_patterns.iter().any(|p| p == "target/debug/**"),
"Cargo check artifacts should not include target/debug/**"
);
assert!(
!clippy_patterns.iter().any(|p| p == "target/debug/**"),
"Cargo clippy artifacts should not include target/debug/**"
);
}
#[test]
fn test_cargo_package_verification_artifacts_are_exact_archives() {
let _guard = test_guard!();
for command in [
"cargo package --workspace --locked",
"cargo package --workspace --target-dir /data/tmp/package-verification",
"cargo +nightly publish --dry-run -p asupersync",
"env CARGO_INCREMENTAL=0 cargo publish -n --workspace",
] {
let kind = classify_command(command).kind;
assert_eq!(kind, Some(CompilationKind::CargoBuild), "{command}");
assert_eq!(
get_artifact_patterns(kind, Some(command)),
vec![
"target/package/*.crate".to_string(),
"target/package/tmp-registry/*.crate".to_string(),
"target/package/tmp-crate/*.crate".to_string(),
],
"return archives without registry indexes or extracted sources"
);
let custom = get_custom_target_artifact_patterns(kind, Some(command));
assert_eq!(
expected_output_glob_list(&custom),
vec![
"package/*.crate",
"package/tmp-registry/*.crate",
"package/tmp-crate/*.crate"
]
);
assert!(get_project_artifact_patterns(kind, Some(command), true).is_empty());
for archive in [
"package/asupersync-0.4.11.crate",
"package/tmp-registry/asupersync-0.4.11.crate",
"package/tmp-crate/asupersync-0.4.11.crate",
] {
assert!(!sync_back_verified_zero_build_outputs(
&[archive.to_string()],
Some(1),
kind,
true,
));
}
}
assert!(
!get_artifact_patterns(Some(CompilationKind::CargoBuild), Some("cargo build"))
.iter()
.any(|pattern| pattern.contains("package/")),
"ordinary builds must not return stale package archives"
);
}
#[test]
fn test_cargo_package_verification_rejects_known_empty_archive_sync() {
let _guard = test_guard!();
for command in [
"cargo package --workspace --exclude drop_unwrap_finder --locked --allow-dirty",
"cargo publish --dry-run --workspace --exclude drop_unwrap_finder --locked --allow-dirty",
"env 'CARGO_HOME=/data/tmp/rch-cargo-cache-vmi1293453' 'CARGO_INCREMENTAL=0' 'CARGO_PROFILE_DEV_DEBUG=0' cargo publish --dry-run -j2 --locked --allow-dirty -p franken-kernel -p franken-evidence -p franken-decision -p asupersync-macros",
] {
assert!(
sync_back_verified_zero_package_archives(Some(0), command),
"successful verification with a proven empty sync must fail: {command}"
);
assert!(!sync_back_verified_zero_package_archives(Some(4), command));
assert!(!sync_back_verified_zero_package_archives(None, command));
}
for command in [
"cargo build --help",
"cargo package --help",
"cargo package --list",
"cargo package --no-verify",
"cargo publish",
"cargo publish --dry-run --no-verify",
"cargo publish -- --dry-run",
] {
assert!(
!sync_back_verified_zero_package_archives(Some(0), command),
"the archive guard must remain scoped to verification: {command}"
);
}
}
#[test]
fn test_custom_target_artifact_patterns_for_cargo_test_are_skipped() {
let _guard = test_guard!();
let patterns = get_custom_target_artifact_patterns(Some(CompilationKind::CargoTest), None);
assert!(
patterns.is_empty(),
"cargo test output is streamed; do not sync a custom target dir after tests"
);
}
#[test]
fn test_custom_target_artifact_patterns_for_diagnostic_commands_are_skipped() {
let _guard = test_guard!();
assert!(
get_custom_target_artifact_patterns(Some(CompilationKind::CargoCheck), None).is_empty(),
"cargo check output is streamed; do not sync a custom target dir"
);
assert!(
get_custom_target_artifact_patterns(Some(CompilationKind::CargoClippy), None).is_empty(),
"cargo clippy output is streamed; do not sync a custom target dir"
);
}
#[test]
fn test_project_artifact_patterns_drop_target_prefixed_globs_under_custom_target_sync() {
let _guard = test_guard!();
for kind in [
CompilationKind::CargoBuild,
CompilationKind::CargoDoc,
CompilationKind::Rustc,
CompilationKind::CargoZigbuild,
] {
let with_custom = get_project_artifact_patterns(Some(kind), None, true);
assert!(
with_custom.is_empty(),
"{kind:?}: all project-root patterns are target/-prefixed and must drop under a custom target sync, got {with_custom:?}"
);
assert_eq!(
get_project_artifact_patterns(Some(kind), None, false),
get_artifact_patterns(Some(kind), None),
"{kind:?}: project-root patterns must be unchanged without a custom target sync"
);
}
}
#[test]
fn test_project_artifact_patterns_keep_non_target_reports_under_custom_target_sync() {
let _guard = test_guard!();
let test_patterns = get_project_artifact_patterns(Some(CompilationKind::CargoTest), None, true);
assert!(
test_patterns.iter().all(|p| !p.starts_with("target/")),
"no target/-prefixed patterns should remain: {test_patterns:?}"
);
for expected in ["tarpaulin-report.html", "cobertura.xml", "junit.xml"] {
assert!(
test_patterns.iter().any(|p| p == expected),
"expected non-target report {expected} to be retained: {test_patterns:?}"
);
}
assert_eq!(
get_project_artifact_patterns(Some(CompilationKind::Gcc), None, true),
get_artifact_patterns(Some(CompilationKind::Gcc), None),
"C/C++ project-root outputs are unaffected by a custom cargo target dir"
);
}
#[test]
fn test_custom_target_artifact_patterns_for_nextest_are_target_relative() {
let _guard = test_guard!();
let patterns = get_custom_target_artifact_patterns(Some(CompilationKind::CargoNextest), None);
assert!(
!patterns.iter().any(|p| p == "**"),
"nextest custom target retrieval must not sync the full target dir"
);
assert!(
!patterns.iter().any(|p| p.starts_with("target/")),
"custom target retrieval is already rooted at the target dir"
);
assert!(
patterns.iter().any(|p| p == "nextest/**"),
"nextest custom target retrieval should keep targeted test artifacts"
);
}
#[test]
fn test_custom_target_artifact_patterns_for_build_commands_capture_outputs_only() {
let _guard = test_guard!();
for kind in [
CompilationKind::CargoBuild,
CompilationKind::CargoDoc,
CompilationKind::Rustc,
] {
let patterns = get_custom_target_artifact_patterns(Some(kind), None);
assert!(
!patterns.iter().any(|p| p == "**"),
"{kind:?}: build sync-back must not pull the whole target dir"
);
assert!(
!patterns.iter().any(|p| p.starts_with("target/")),
"{kind:?}: custom-target patterns must be target-dir-relative: {patterns:?}"
);
assert!(
patterns.iter().any(|p| p == "debug/**"),
"{kind:?}: must retain debug profile outputs (incl. the binary): {patterns:?}"
);
assert!(
patterns.iter().any(|p| p == "release/**"),
"{kind:?}: must retain release profile outputs (incl. the binary): {patterns:?}"
);
assert!(
patterns.iter().any(|p| p == "*/release/**"),
"{kind:?}: must retain triple-nested release outputs: {patterns:?}"
);
assert!(
patterns.iter().any(|p| p == "- */release/incremental/"),
"{kind:?}: must exclude triple-nested release cache trees: {patterns:?}"
);
for needle in ["incremental/", ".fingerprint/", "build/", "*.d"] {
assert!(
patterns
.iter()
.any(|p| p.starts_with("- ") && p.contains(needle)),
"{kind:?}: must exclude cargo cache tree {needle:?}: {patterns:?}"
);
}
}
}
#[test]
fn test_custom_profile_output_dir_extraction() {
assert_eq!(
cargo_custom_profile_output_dir("cargo build --profile release-perf"),
Some("release-perf".to_string())
);
assert_eq!(
cargo_custom_profile_output_dir("cargo build --profile=thin-lto"),
Some("thin-lto".to_string())
);
assert_eq!(
cargo_custom_profile_output_dir(
"cargo build --package mycrate --profile release-perf --example app"
),
Some("release-perf".to_string())
);
assert_eq!(cargo_custom_profile_output_dir("cargo build"), None);
assert_eq!(
cargo_custom_profile_output_dir("cargo build --release"),
None
);
assert_eq!(cargo_custom_profile_output_dir("cargo build -r"), None);
assert_eq!(
cargo_custom_profile_output_dir("cargo build --profile dev"),
None
);
assert_eq!(
cargo_custom_profile_output_dir("cargo build --profile test"),
None
);
assert_eq!(
cargo_custom_profile_output_dir("cargo build --profile release"),
None
);
assert_eq!(
cargo_custom_profile_output_dir("cargo build --profile bench"),
None
);
assert_eq!(
cargo_custom_profile_output_dir("cargo test -- --profile release-perf"),
None
);
assert_eq!(
cargo_custom_profile_output_dir("cargo build --profile ../../etc"),
None
);
assert_eq!(
cargo_custom_profile_output_dir("cargo build --profile 'a*b'"),
None
);
assert_eq!(
cargo_custom_profile_output_dir("cargo build --profile"),
None
);
}
#[test]
fn test_custom_profile_globs_in_default_root_patterns() {
let plain = get_artifact_patterns(Some(CompilationKind::CargoBuild), None);
let custom = get_artifact_patterns(
Some(CompilationKind::CargoBuild),
Some("cargo build --profile release-perf"),
);
assert_eq!(custom.len(), plain.len() + 2, "exactly two new globs");
assert!(
custom.iter().any(|p| p == "target/release-perf/**"),
"custom profile output glob missing: {custom:?}"
);
assert!(
custom.iter().any(|p| p == "target/*/release-perf/**"),
"custom profile triple-aware glob missing: {custom:?}"
);
for needle in [
"target/debug/**",
"target/release/**",
"target/*/release/**",
] {
assert!(
custom.iter().any(|p| p == needle),
"built-in glob {needle} must be retained: {custom:?}"
);
}
for command in [
"cargo build",
"cargo build --release",
"cargo build -r",
"cargo build --profile dev",
"cargo build --profile test",
"cargo build --profile release",
"cargo build --profile bench",
] {
assert_eq!(
get_artifact_patterns(Some(CompilationKind::CargoBuild), Some(command)),
plain,
"{command}: built-in profile output dirs are already covered; expected no new globs"
);
}
let zig = get_artifact_patterns(
Some(CompilationKind::CargoZigbuild),
Some("cargo zigbuild --profile release-perf --target aarch64-unknown-linux-gnu"),
);
assert!(zig.iter().any(|p| p == "target/release-perf/**"), "{zig:?}");
assert!(
zig.iter().any(|p| p == "target/*/release-perf/**"),
"{zig:?}"
);
let test_patterns = get_artifact_patterns(
Some(CompilationKind::CargoTest),
Some("cargo test --profile release-perf"),
);
assert!(
!test_patterns.iter().any(|p| p.contains("release-perf")),
"streaming kinds must not gain target globs: {test_patterns:?}"
);
}
#[test]
fn test_custom_profile_globs_in_custom_target_patterns() {
let patterns = get_custom_target_artifact_patterns(
Some(CompilationKind::CargoBuild),
Some("cargo build --profile release-perf"),
);
let (excludes, includes): (Vec<&String>, Vec<&String>) =
patterns.iter().partition(|p| p.starts_with("- "));
assert!(
includes.iter().any(|p| p.as_str() == "release-perf/**"),
"rebased profile include missing: {patterns:?}"
);
assert!(
includes.iter().any(|p| p.as_str() == "*/release-perf/**"),
"rebased triple-aware profile include missing: {patterns:?}"
);
for exclude in [
"- release-perf/incremental/",
"- release-perf/.fingerprint/",
"- release-perf/build/",
"- */release-perf/incremental/",
"- */release-perf/.fingerprint/",
"- */release-perf/build/",
] {
assert!(
excludes.iter().any(|p| p.as_str() == exclude),
"profile cache exclude {exclude} missing: {patterns:?}"
);
}
let first_include = patterns
.iter()
.position(|p| !p.starts_with("- "))
.expect("at least one include");
let last_exclude = patterns
.iter()
.rposition(|p| p.starts_with("- "))
.expect("at least one exclude");
assert!(
last_exclude < first_include,
"all excludes must precede the includes: {patterns:?}"
);
assert_eq!(
get_custom_target_artifact_patterns(
Some(CompilationKind::CargoBuild),
Some("cargo build --profile bench")
),
get_custom_target_artifact_patterns(Some(CompilationKind::CargoBuild), None),
"--profile bench maps to target/release/ — no extra patterns"
);
}
#[test]
fn test_sync_back_zero_output_detection() {
let metadata_only = vec![".rustc_info.json".to_string(), "CACHEDIR.TAG".to_string()];
let with_binary = vec![
".rustc_info.json".to_string(),
"CACHEDIR.TAG".to_string(),
"release-perf/examples/fmd_perf_harness".to_string(),
];
let kind = Some(CompilationKind::CargoBuild);
assert!(
sync_back_verified_zero_build_outputs(&metadata_only, Some(2), kind, true),
"metadata-only manifest must be classified as a zero-output failure"
);
assert!(
!sync_back_verified_zero_build_outputs(&with_binary, Some(3), kind, true),
"a manifest containing the output binary must NOT fail"
);
assert!(
!sync_back_verified_zero_build_outputs(
&[
".rustc_info.json".to_string(),
"release-perf/examples/fmd_perf_harness".to_string()
],
Some(2),
kind,
true
),
"up-to-date outputs in the manifest must NOT fail"
);
assert!(
sync_back_verified_zero_build_outputs(
&["debug/incremental/session.bin".to_string()],
Some(1),
kind,
true
),
"cache-only manifest must be classified as a zero-output failure"
);
assert!(!sync_back_verified_zero_build_outputs(
&metadata_only,
None,
kind,
true
));
assert!(!sync_back_verified_zero_build_outputs(
&[],
Some(0),
kind,
true
));
assert!(!sync_back_verified_zero_build_outputs(
&metadata_only,
Some(4),
kind,
true
));
for non_enumerable in [
CompilationKind::Rustc,
CompilationKind::Gcc,
CompilationKind::Make,
CompilationKind::CargoTest,
] {
assert!(
!sync_back_verified_zero_build_outputs(
&metadata_only,
Some(2),
Some(non_enumerable),
true
),
"{non_enumerable:?} has no enumerable output contract"
);
}
assert!(!sync_back_verified_zero_build_outputs(
&metadata_only,
Some(2),
None,
true
));
let root_metadata_only = vec![
"target/.rustc_info.json".to_string(),
"target/CACHEDIR.TAG".to_string(),
];
assert!(sync_back_verified_zero_build_outputs(
&root_metadata_only,
Some(2),
kind,
false
));
assert!(!sync_back_verified_zero_build_outputs(
&[
"target/.rustc_info.json".to_string(),
"target/debug/my_app".to_string()
],
Some(2),
kind,
false
));
assert!(sync_back_verified_zero_build_outputs(
&[
"target/.rustc_info.json".to_string(),
"target/debug/incremental/foo.bin".to_string()
],
Some(2),
kind,
false
));
}
#[test]
fn test_custom_target_patterns_match_a_binary_but_not_cache() {
let _guard = test_guard!();
let patterns = get_custom_target_artifact_patterns(Some(CompilationKind::CargoBuild), None);
let (excludes, includes): (Vec<&String>, Vec<&String>) =
patterns.iter().partition(|p| p.starts_with("- "));
let exclude_payloads: Vec<&str> = excludes
.iter()
.map(|p| p.trim_start_matches("- "))
.collect();
let excluded = |path: &str| -> bool {
exclude_payloads.iter().any(|ex| {
if let Some(dir) = ex.strip_suffix('/') {
let segment = dir.trim_start_matches("*/");
path.split('/').any(|comp| comp == segment)
} else if let Some(suffix) = ex.strip_prefix('*') {
path.ends_with(suffix)
} else {
path == *ex
}
})
};
let included = |path: &str| -> bool {
if excluded(path) {
return false;
}
includes.iter().any(|inc| {
if let Some(prefix) = inc.strip_suffix("/**") {
path.starts_with(&format!("{prefix}/"))
} else {
path == inc.as_str()
}
})
};
assert!(
included("debug/my_app"),
"the final debug binary must be synced back: {patterns:?}"
);
assert!(
included("release/my_app"),
"the final release binary must be synced back: {patterns:?}"
);
assert!(
included("debug/deps/libmy_app.rlib"),
"crate deps artifacts must be synced back: {patterns:?}"
);
assert!(
!included("debug/incremental/foo/bar.bin"),
"incremental cache must not be synced back: {patterns:?}"
);
assert!(
!included("debug/.fingerprint/my_app/lib.json"),
".fingerprint cache must not be synced back: {patterns:?}"
);
assert!(
!included("debug/build/somecrate/out/generated.rs"),
"build-script cache must not be synced back: {patterns:?}"
);
assert!(
!included("debug/deps/my_app.d"),
"dep (*.d) files must not be synced back: {patterns:?}"
);
}
#[test]
fn test_expected_output_glob_list_drops_excludes_and_metadata() {
let patterns = get_custom_target_artifact_patterns(
Some(CompilationKind::CargoBuild),
Some("cargo build --profile release-perf"),
);
let expected = expected_output_glob_list(&patterns);
assert!(
!expected.is_empty(),
"expected list must retain the output globs: {expected:?}"
);
assert!(
expected.iter().all(|p| !p.starts_with("- ")),
"no exclude rules in the expected list: {expected:?}"
);
for metadata in [".rustc_info.json", "CACHEDIR.TAG"] {
assert!(
!expected.iter().any(|p| p == metadata),
"metadata file {metadata} must not be listed as an expected output"
);
}
assert!(
expected.iter().any(|p| p == "release-perf/**"),
"the custom profile glob must be named: {expected:?}"
);
}
#[test]
fn test_is_filtered_test_command_basic() {
let _guard = test_guard!();
assert!(
is_filtered_test_command("cargo test my_test"),
"Should detect test name filter"
);
assert!(
is_filtered_test_command("cargo test test_foo"),
"Should detect test name filter"
);
assert!(
is_filtered_test_command("cargo test some::module::test"),
"Should detect module path filter"
);
assert!(
!is_filtered_test_command("cargo test"),
"No filter in basic cargo test"
);
assert!(
!is_filtered_test_command("cargo test --release"),
"Flags are not filters"
);
}
#[test]
fn test_is_filtered_test_command_with_flags() {
let _guard = test_guard!();
assert!(
is_filtered_test_command("cargo test --release my_test"),
"Should detect filter after flags"
);
assert!(
is_filtered_test_command("cargo test -p mypackage my_test"),
"Should detect filter after package flag"
);
assert!(
!is_filtered_test_command("cargo test -p mypackage"),
"Package is not a test name filter"
);
assert!(
!is_filtered_test_command("cargo test --lib"),
"--lib is not a test name filter"
);
}
#[test]
fn test_is_filtered_test_command_with_separator() {
let _guard = test_guard!();
assert!(
is_filtered_test_command("cargo test my_test -- --nocapture"),
"Should detect filter before separator"
);
assert!(
!is_filtered_test_command("cargo test -- --nocapture"),
"Args after -- are not test name filters"
);
assert!(
!is_filtered_test_command("cargo test -- --test-threads=4"),
"Args after -- are not test name filters"
);
}
#[test]
fn test_has_ignored_only_flag() {
let _guard = test_guard!();
assert!(
has_ignored_only_flag("cargo test -- --ignored"),
"Should detect --ignored"
);
assert!(
!has_ignored_only_flag("cargo test -- --include-ignored"),
"--include-ignored runs all tests"
);
assert!(
!has_ignored_only_flag("cargo test -- --ignored --include-ignored"),
"--include-ignored takes precedence"
);
assert!(!has_ignored_only_flag("cargo test"), "No flags");
}
#[test]
fn test_has_exact_flag() {
let _guard = test_guard!();
assert!(
has_exact_flag("cargo test my_test -- --exact"),
"--exact detected"
);
assert!(!has_exact_flag("cargo test my_test"), "No --exact");
assert!(!has_exact_flag("cargo test -- --nocapture"), "No --exact");
}
#[test]
fn test_estimate_cores_filtered_tests() {
let _guard = test_guard!();
let config = rch_common::CompilationConfig {
build_slots: 6,
test_slots: 10,
check_slots: 3,
..Default::default()
};
let full = estimate_cores_for_command(Some(CompilationKind::CargoTest), "cargo test", &config);
assert_eq!(full, 10, "Full test suite uses default test_slots");
let filtered = estimate_cores_for_command(
Some(CompilationKind::CargoTest),
"cargo test my_test",
&config,
);
assert_eq!(filtered, 5, "Filtered test uses reduced slots");
let exact = estimate_cores_for_command(
Some(CompilationKind::CargoTest),
"cargo test my_test -- --exact",
&config,
);
assert_eq!(exact, 5, "--exact uses reduced slots");
let ignored = estimate_cores_for_command(
Some(CompilationKind::CargoTest),
"cargo test -- --ignored",
&config,
);
assert_eq!(ignored, 5, "--ignored uses reduced slots");
let include_ignored = estimate_cores_for_command(
Some(CompilationKind::CargoTest),
"cargo test -- --include-ignored",
&config,
);
assert_eq!(include_ignored, 10, "--include-ignored uses full slots");
}
#[test]
fn test_estimate_cores_explicit_threads_overrides_filter() {
let _guard = test_guard!();
let config = rch_common::CompilationConfig {
build_slots: 6,
test_slots: 10,
check_slots: 3,
..Default::default()
};
let explicit = estimate_cores_for_command(
Some(CompilationKind::CargoTest),
"cargo test my_test -- --test-threads=8",
&config,
);
assert_eq!(explicit, 8, "Explicit --test-threads overrides filtering");
let env = estimate_cores_for_command(
Some(CompilationKind::CargoTest),
"RUST_TEST_THREADS=6 cargo test my_test",
&config,
);
assert_eq!(env, 6, "RUST_TEST_THREADS overrides filtering");
}
#[test]
fn test_estimate_cores_filtered_minimum() {
let _guard = test_guard!();
let config = rch_common::CompilationConfig {
build_slots: 6,
test_slots: 2, check_slots: 3,
..Default::default()
};
let filtered = estimate_cores_for_command(
Some(CompilationKind::CargoTest),
"cargo test my_test",
&config,
);
assert!(filtered >= 2, "Filtered slots should be at least 2");
}
#[test]
fn test_estimate_cores_filtered_never_exceeds_default() {
let _guard = test_guard!();
let config = rch_common::CompilationConfig {
build_slots: 6,
test_slots: 1, check_slots: 3,
..Default::default()
};
let filtered = estimate_cores_for_command(
Some(CompilationKind::CargoTest),
"cargo test my_test",
&config,
);
assert_eq!(
filtered, 1,
"Filtered tests should not request more slots than test_slots"
);
}
#[test]
fn test_nocapture_does_not_affect_slots() {
let _guard = test_guard!();
let config = rch_common::CompilationConfig {
build_slots: 6,
test_slots: 10,
check_slots: 3,
..Default::default()
};
let with_nocapture = estimate_cores_for_command(
Some(CompilationKind::CargoTest),
"cargo test -- --nocapture",
&config,
);
let without =
estimate_cores_for_command(Some(CompilationKind::CargoTest), "cargo test", &config);
assert_eq!(with_nocapture, without, "--nocapture doesn't affect slots");
let with_show = estimate_cores_for_command(
Some(CompilationKind::CargoTest),
"cargo test -- --show-output",
&config,
);
assert_eq!(with_show, without, "--show-output doesn't affect slots");
}
#[test]
fn test_skip_pattern_uses_full_slots() {
let _guard = test_guard!();
let config = rch_common::CompilationConfig {
build_slots: 6,
test_slots: 10,
check_slots: 3,
..Default::default()
};
let with_skip = estimate_cores_for_command(
Some(CompilationKind::CargoTest),
"cargo test -- --skip slow_test",
&config,
);
assert_eq!(with_skip, 10, "--skip uses full slots");
}
#[test]
fn test_parse_selection_response_accepts_known_newer_health_reason() {
let _guard = test_guard!();
let json = serde_json::json!({
"selection_protocol_version": rch_common::SELECTION_RESPONSE_PROTOCOL_VERSION,
"worker": null,
"reason": "no_workers_passed_health",
"build_id": null,
"diagnostics": null
})
.to_string();
let response = parse_selection_response(&json).expect("selection response parses");
assert_eq!(response.reason, SelectionReason::NoWorkersPassedHealth);
assert!(response.worker.is_none());
}
#[test]
fn test_parse_selection_response_tolerates_unknown_unit_reason() {
let _guard = test_guard!();
let json = serde_json::json!({
"selection_protocol_version": rch_common::SELECTION_RESPONSE_PROTOCOL_VERSION,
"worker": null,
"reason": "future_selector_gate",
"build_id": null,
"diagnostics": null
})
.to_string();
let response = parse_selection_response(&json).expect("unknown reason should not fail");
assert!(response.worker.is_none());
assert!(matches!(
response.reason,
SelectionReason::SelectionError(_)
));
assert!(
response
.reason
.to_string()
.contains("unknown daemon selection reason")
);
assert!(
response.reason.to_string().contains("future_selector_gate"),
"unknown unit reason should preserve daemon detail: {}",
response.reason
);
}
#[test]
fn test_parse_selection_response_preserves_unknown_structured_reason_detail() {
let _guard = test_guard!();
let json = serde_json::json!({
"selection_protocol_version": rch_common::SELECTION_RESPONSE_PROTOCOL_VERSION,
"worker": null,
"reason": { "future_selector_gate": "runtime_probe_missing" },
"build_id": null,
"diagnostics": null
})
.to_string();
let response = parse_selection_response(&json).expect("unknown reason should not fail");
let detail = response.reason.to_string();
assert!(matches!(
response.reason,
SelectionReason::SelectionError(_)
));
assert!(
detail.contains("future_selector_gate"),
"unknown structured reason should preserve variant name: {detail}"
);
assert!(
detail.contains("runtime_probe_missing"),
"unknown structured reason should preserve daemon payload: {detail}"
);
}
#[test]
fn test_parse_selection_response_rejects_unsupported_protocol_version() {
let _guard = test_guard!();
let json = serde_json::json!({
"selection_protocol_version": rch_common::SELECTION_RESPONSE_PROTOCOL_VERSION + 1,
"worker": null,
"reason": "all_workers_busy",
"build_id": null,
"diagnostics": null
})
.to_string();
let error = parse_selection_response(&json).expect_err("future protocol should fail");
assert!(
error.to_string().contains("exceeds client support"),
"unexpected error: {error}"
);
}
#[tokio::test]
async fn test_daemon_query_connect_timeout_fail_open() {
let _lock = test_lock().lock().await;
let socket_path = format!(
"/tmp/rch_test_connect_timeout_{}_{}.sock",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
);
let _ = std::fs::remove_file(&socket_path);
let listener = UnixListener::bind(&socket_path).expect("Failed to create test socket");
let socket_path_clone = socket_path.clone();
tokio::spawn(async move {
let _ = listener.accept().await;
tokio::time::sleep(tokio::time::Duration::from_secs(5)).await;
});
tokio::time::sleep(tokio::time::Duration::from_millis(25)).await;
let result: anyhow::Result<SelectionResponse> = query_daemon(
&socket_path,
"test-project",
4,
0,
"cargo build",
None,
RequiredRuntime::None,
CommandPriority::Normal,
100,
None,
None,
false,
&[],
false,
&[],
)
.await;
let _ = std::fs::remove_file(&socket_path_clone);
assert!(
result.is_err(),
"Query should fail when daemon doesn't respond"
);
}
#[tokio::test]
async fn test_process_hook_timeout_fail_open() {
let _lock = test_lock().lock().await;
let socket_path = format!(
"/tmp/rch_test_process_timeout_{}_{}.sock",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
);
let _overrides = TestOverridesGuard::set(
&socket_path,
MockConfig::default(),
MockRsyncConfig::success(),
);
let _ = std::fs::remove_file(&socket_path);
let listener = UnixListener::bind(&socket_path).expect("bind");
tokio::spawn(async move {
let (stream, _) = listener.accept().await.expect("accept");
let (_reader, _writer) = stream.into_split();
tokio::time::sleep(tokio::time::Duration::from_secs(5)).await;
});
tokio::time::sleep(tokio::time::Duration::from_millis(25)).await;
let input = HookInput {
tool_name: "Bash".to_string(),
tool_input: ToolInput {
command: "cargo build".to_string(),
description: None,
},
session_id: None,
};
let output = process_hook(input).await;
let _ = std::fs::remove_file(&socket_path);
assert!(
output.is_allow(),
"Hook should fail-open when daemon query times out"
);
}
#[tokio::test]
async fn test_daemon_query_partial_response_timeout() {
let _lock = test_lock().lock().await;
let socket_path = format!(
"/tmp/rch_test_partial_timeout_{}_{}.sock",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
);
let _ = std::fs::remove_file(&socket_path);
let listener = UnixListener::bind(&socket_path).expect("bind");
let socket_path_clone = socket_path.clone();
tokio::spawn(async move {
let (stream, _) = listener.accept().await.expect("accept");
let (reader, mut writer) = stream.into_split();
let mut buf_reader = TokioBufReader::new(reader);
let mut request_line = String::new();
let _ = buf_reader.read_line(&mut request_line).await;
writer
.write_all(b"HTTP/1.1 200 OK\r\n")
.await
.expect("write");
tokio::time::sleep(tokio::time::Duration::from_secs(5)).await;
});
tokio::time::sleep(tokio::time::Duration::from_millis(25)).await;
let result = query_daemon(
&socket_path,
"test-project",
4,
0,
"cargo build",
None,
RequiredRuntime::None,
CommandPriority::Normal,
100,
None,
None,
false,
&[],
false,
&[],
)
.await;
let _ = std::fs::remove_file(&socket_path_clone);
assert!(result.is_err(), "Partial response should result in error");
}
#[test]
fn test_queue_when_busy_enabled_parser() {
let _guard = test_guard!();
assert!(queue_when_busy_enabled_from(None));
assert!(queue_when_busy_enabled_from(Some("1")));
assert!(queue_when_busy_enabled_from(Some("true")));
assert!(queue_when_busy_enabled_from(Some("yes")));
assert!(!queue_when_busy_enabled_from(Some("0")));
assert!(!queue_when_busy_enabled_from(Some("false")));
assert!(!queue_when_busy_enabled_from(Some("off")));
}
#[test]
fn test_daemon_response_timeout_defaults_and_overrides() {
let _guard = test_guard!();
assert_eq!(
daemon_response_timeout_for(false, None, None),
Duration::from_secs(DEFAULT_DAEMON_RESPONSE_TIMEOUT_SECS)
);
assert_eq!(
daemon_response_timeout_for(true, None, None),
Duration::from_secs(DEFAULT_DAEMON_WAIT_RESPONSE_TIMEOUT_SECS)
);
assert_eq!(
daemon_response_timeout_for(true, None, Some("900")),
Duration::from_secs(900)
);
assert_eq!(
daemon_response_timeout_for(true, Some("45"), Some("900")),
Duration::from_secs(45)
);
assert_eq!(
daemon_response_timeout_for(true, Some("invalid"), Some("invalid")),
Duration::from_secs(DEFAULT_DAEMON_WAIT_RESPONSE_TIMEOUT_SECS)
);
}
fn create_test_state_dir() -> tempfile::TempDir {
tempfile::TempDir::new().expect("Failed to create temp dir")
}
#[test]
fn test_classify_socket_failure_missing_when_socket_not_found() {
let err: anyhow::Error = super::DaemonError::SocketNotFound {
socket_path: "/run/rch/rch.sock".to_string(),
}
.into();
assert_eq!(
super::classify_socket_failure(&err, false),
super::SocketFailureKind::Missing
);
}
#[test]
fn test_classify_socket_failure_refused_socket() {
let err = anyhow::Error::from(std::io::Error::from(std::io::ErrorKind::ConnectionRefused));
assert_eq!(
super::classify_socket_failure(&err, true),
super::SocketFailureKind::Refused
);
}
#[test]
fn test_classify_socket_failure_stale_socket_on_timeout() {
let err = anyhow::anyhow!("Daemon connect timed out after 5s");
assert_eq!(
super::classify_socket_failure(&err, true),
super::SocketFailureKind::Stale
);
let io_timeout = anyhow::Error::from(std::io::Error::from(std::io::ErrorKind::TimedOut));
assert_eq!(
super::classify_socket_failure(&io_timeout, true),
super::SocketFailureKind::Stale
);
}
#[test]
fn test_detect_socket_path_mismatch_wrong_configured_socket() {
let mismatch = super::detect_socket_path_mismatch(
"/tmp/custom-rch.sock",
"/home/dev/.cache/rch/rch.sock",
true,
)
.expect("differing paths must be reported as a mismatch");
assert_eq!(mismatch.configured, "/tmp/custom-rch.sock");
assert_eq!(mismatch.canonical, "/home/dev/.cache/rch/rch.sock");
assert!(mismatch.canonical_exists);
assert!(
super::detect_socket_path_mismatch(
" /home/dev/.cache/rch/rch.sock ",
"/home/dev/.cache/rch/rch.sock",
true,
)
.is_none()
);
}
#[test]
fn test_decide_recovery_action_daemon_start_success_proceeds_remote() {
assert_eq!(
super::decide_recovery_action(true, false),
super::DaemonRecoveryAction::ProceedRemote
);
assert_eq!(
super::decide_recovery_action(true, true),
super::DaemonRecoveryAction::ProceedRemote
);
}
#[test]
fn test_decide_recovery_action_daemon_start_failure_falls_back_open() {
assert_eq!(
super::decide_recovery_action(false, false),
super::DaemonRecoveryAction::LocalFallback
);
}
#[test]
fn test_decide_recovery_action_proof_mode_refuses() {
assert_eq!(
super::decide_recovery_action(false, true),
super::DaemonRecoveryAction::Refuse
);
}
#[test]
fn test_build_socket_failure_incident_records_reason_and_mismatch() {
let mismatch = super::SocketPathMismatch {
configured: "/home/alice/.cache/rch/rch.sock".to_string(),
canonical: "/home/alice/.config/rch/rch.sock".to_string(),
canonical_exists: true,
};
let event = super::build_socket_failure_incident(
super::SocketFailureKind::Refused,
Some(&mismatch),
"demo-project",
"cargo build --release",
false,
1_700_000_000_000,
);
assert_eq!(
event.reason_code,
rch_common::IncidentReasonCode::DaemonSocketRefused
);
assert_eq!(event.reason_code.code(), "RCH-I010");
assert_eq!(event.source, rch_common::IncidentSource::Hook);
assert_eq!(event.selected_mode, rch_common::SelectedMode::Local);
assert!(
event.local_fallback_allowed,
"convenience lane permits fallback"
);
assert_eq!(
event.details.get("socket_failure").map(String::as_str),
Some("refused")
);
assert_eq!(
event
.details
.get("socket_path_mismatch")
.map(String::as_str),
Some("true")
);
let configured = event.details.get("configured_socket").unwrap();
assert!(
configured.contains("<redacted>"),
"home user must be masked: {configured}"
);
assert!(
!configured.contains("alice"),
"raw username must not leak: {configured}"
);
assert_eq!(
event
.details
.get("canonical_socket_exists")
.map(String::as_str),
Some("true")
);
}
#[test]
fn test_build_recovery_terminal_incident_proof_vs_fallback() {
let refusal = super::build_recovery_terminal_incident(
true,
"demo",
"cargo test",
"daemon unavailable",
1_700_000_000_001,
);
assert_eq!(
refusal.reason_code,
rch_common::IncidentReasonCode::ProofRefusal
);
assert_eq!(refusal.reason_code.code(), "RCH-I012");
assert!(!refusal.local_fallback_allowed);
assert!(refusal.control.strict_remote_policy);
let fallback = super::build_recovery_terminal_incident(
false,
"demo",
"cargo test",
"daemon unavailable",
1_700_000_000_002,
);
assert_eq!(
fallback.reason_code,
rch_common::IncidentReasonCode::LocalFallback
);
assert_eq!(fallback.reason_code.code(), "RCH-I011");
assert!(fallback.local_fallback_allowed);
assert!(!fallback.control.strict_remote_policy);
}
#[test]
fn test_local_fallback_incident_records_reason_and_redacts_command() {
let command = "GITHUB_TOKEN=ghp_supersecretvalue123456 cargo build --release";
let local = super::build_local_fallback_incident(
command,
"no admissible workers (critical_pressure=5)",
false,
1_700_000_000_010,
None,
);
assert!(!local.details.contains_key("error"));
assert_eq!(
local.reason_code,
rch_common::IncidentReasonCode::LocalFallback
);
assert_eq!(local.reason_code.code(), "RCH-I011");
assert!(local.local_fallback_allowed);
assert_eq!(
local.details.get("reason").map(String::as_str),
Some("no admissible workers (critical_pressure=5)")
);
assert!(!local.command_fingerprint.contains("supersecretvalue"));
assert!(local.command_fingerprint.contains("cargo build --release"));
assert!(!local.project_id.is_empty());
let refused = super::build_local_fallback_incident(
"cargo test",
"remote execution failed; remote retries exhausted",
true,
1_700_000_000_011,
Some(&format!(
"hz3: remote execution completion is unconfirmed: GITHUB_TOKEN=ghp_supersecretvalue123456 {}",
"x".repeat(2_000)
)),
);
assert_eq!(
refused.reason_code,
rch_common::IncidentReasonCode::ProofRefusal
);
assert_eq!(
refused.details.get("reason").map(String::as_str),
Some("remote execution failed; remote retries exhausted")
);
let error = refused.details.get("error").expect("error chain recorded");
assert!(error.starts_with("hz3: remote execution completion is unconfirmed"));
assert!(!error.contains("supersecretvalue"), "{error}");
assert!(
error.chars().count() <= 601,
"bounded: {}",
error.chars().count()
);
assert!(!refused.local_fallback_allowed);
assert!(refused.control.strict_remote_policy);
let dir = create_test_state_dir();
let ledger = rch_common::IncidentLedger::with_path(dir.path().join("incidents.jsonl"));
ledger.append(&local).expect("append must succeed");
ledger.append(&refused).expect("append must succeed");
let read = rch_common::IncidentLedger::with_path(ledger.path()).read_all();
assert_eq!(read.len(), 2);
assert_eq!(
read[0].details.get("reason").map(String::as_str),
Some("no admissible workers (critical_pressure=5)")
);
}
#[test]
fn test_socket_failure_incident_durably_appends_to_ledger() {
let dir = create_test_state_dir();
let ledger = rch_common::IncidentLedger::with_path(dir.path().join("incidents.jsonl"));
let event = super::build_socket_failure_incident(
super::SocketFailureKind::Stale,
None,
"demo",
"cargo check",
true,
1_700_000_000_003,
);
ledger.append(&event).expect("append must succeed");
let read = rch_common::IncidentLedger::with_path(ledger.path()).read_all();
assert_eq!(read.len(), 1);
assert_eq!(
read[0].reason_code,
rch_common::IncidentReasonCode::DaemonSocketRefused
);
assert_eq!(
read[0].details.get("socket_failure").map(String::as_str),
Some("stale")
);
assert!(
!read[0].local_fallback_allowed,
"proof mode records no fallback"
);
}
#[test]
fn test_timing_record_creation() {
let _guard = test_guard!();
let now_secs = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs();
let record = TimingRecord {
timestamp: now_secs,
duration_ms: 5000,
remote: true,
};
assert_eq!(record.duration_ms, 5000);
assert!(record.remote);
assert!(record.timestamp >= now_secs - 1 && record.timestamp <= now_secs + 1);
}
#[test]
fn test_project_timing_data_add_sample() {
let _guard = test_guard!();
let mut data = ProjectTimingData::default();
data.add_sample(1000, false);
assert_eq!(data.local_samples.len(), 1);
assert_eq!(data.remote_samples.len(), 0);
assert_eq!(data.local_samples[0].duration_ms, 1000);
data.add_sample(500, true);
assert_eq!(data.local_samples.len(), 1);
assert_eq!(data.remote_samples.len(), 1);
assert_eq!(data.remote_samples[0].duration_ms, 500);
}
#[test]
fn test_project_timing_data_median_odd_count() {
let _guard = test_guard!();
let mut data = ProjectTimingData::default();
data.add_sample(100, false);
data.add_sample(300, false);
data.add_sample(200, false);
assert_eq!(data.median_duration(false), Some(200));
}
#[test]
fn test_project_timing_data_median_even_count() {
let _guard = test_guard!();
let mut data = ProjectTimingData::default();
data.add_sample(100, true);
data.add_sample(300, true);
data.add_sample(200, true);
data.add_sample(400, true);
assert_eq!(data.median_duration(true), Some(250));
}
#[test]
fn test_project_timing_data_median_empty() {
let _guard = test_guard!();
let data = ProjectTimingData::default();
assert_eq!(data.median_duration(false), None);
assert_eq!(data.median_duration(true), None);
}
#[test]
fn test_project_timing_data_speedup_ratio() {
let _guard = test_guard!();
let mut data = ProjectTimingData::default();
data.add_sample(1000, false);
data.add_sample(500, true);
assert_eq!(data.speedup_ratio(), Some(2.0));
}
#[test]
fn test_project_timing_data_speedup_no_data() {
let _guard = test_guard!();
let mut data = ProjectTimingData::default();
data.add_sample(1000, false);
assert_eq!(data.speedup_ratio(), None);
}
#[test]
fn test_project_timing_data_sample_truncation() {
let _guard = test_guard!();
let mut data = ProjectTimingData::default();
for i in 0..25 {
data.add_sample(i * 100, false);
}
assert_eq!(data.local_samples.len(), MAX_TIMING_SAMPLES);
assert_eq!(data.local_samples[0].duration_ms, 500); }
#[test]
fn test_timing_history_key() {
let _guard = test_guard!();
let key = TimingHistory::key("my_project", Some(CompilationKind::CargoTest));
assert!(key.contains("my_project"));
assert!(key.contains("CargoTest"));
let key_unknown = TimingHistory::key("project2", None);
assert!(key_unknown.contains("project2"));
assert!(key_unknown.contains("Unknown"));
}
#[test]
fn test_timing_history_record_and_get() {
let _guard = test_guard!();
let mut history = TimingHistory::default();
history.record("proj1", Some(CompilationKind::CargoBuild), 1000, true);
history.record("proj1", Some(CompilationKind::CargoBuild), 800, true);
let data = history.get("proj1", Some(CompilationKind::CargoBuild));
assert!(data.is_some());
let data = data.unwrap();
assert_eq!(data.remote_samples.len(), 2);
assert_eq!(data.median_duration(true), Some(900));
let data2 = history.get("proj1", Some(CompilationKind::CargoTest));
assert!(data2.is_none());
}
#[test]
fn test_timing_history_serialization() {
let _guard = test_guard!();
let mut history = TimingHistory::default();
history.record("proj", Some(CompilationKind::CargoCheck), 500, false);
history.record("proj", Some(CompilationKind::CargoCheck), 250, true);
let json = serde_json::to_string(&history).unwrap();
let loaded: TimingHistory = serde_json::from_str(&json).unwrap();
let data = loaded
.get("proj", Some(CompilationKind::CargoCheck))
.unwrap();
assert_eq!(data.local_samples.len(), 1);
assert_eq!(data.remote_samples.len(), 1);
}
#[test]
fn test_record_build_timing_releases_guard_before_disk_io() {
let _guard = test_guard!();
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::thread;
use std::time::{Duration, Instant};
let unique = format!(
"t18-conc-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0)
);
let n_threads = 8;
let calls_per_thread = 5;
let started = Arc::new(AtomicUsize::new(0));
let mut handles = Vec::new();
let t0 = Instant::now();
for t in 0..n_threads {
let started = Arc::clone(&started);
let unique = unique.clone();
handles.push(thread::spawn(move || {
for i in 0..calls_per_thread {
started.fetch_add(1, Ordering::Relaxed);
let project = format!("{unique}-{t}-{i}");
record_build_timing(
&project,
Some(CompilationKind::CargoBuild),
100 + (i as u64),
true,
);
}
}));
}
for h in handles {
h.join().expect("worker thread panicked");
}
let elapsed = t0.elapsed();
let total_calls = n_threads * calls_per_thread;
assert_eq!(
started.load(Ordering::Relaxed),
total_calls,
"all threads should have started"
);
assert!(
elapsed < Duration::from_millis(4000),
"{total_calls} concurrent record_build_timing calls took {elapsed:?} (expected <4s)"
);
}
#[test]
fn test_record_build_timing_in_memory_state_survives_disk_failure() {
let _guard = test_guard!();
let unique = format!(
"t18-disk-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0)
);
record_build_timing(&unique, Some(CompilationKind::CargoBuild), 1234, true);
let history = timing_cache().read().expect("read");
let entry = history.get(&unique, Some(CompilationKind::CargoBuild));
assert!(
entry.is_some(),
"in-memory entry for key {unique:?} must be present even if disk write failed"
);
let data = entry.unwrap();
assert!(
!data.remote_samples.is_empty(),
"at least one remote sample recorded"
);
assert_eq!(
data.remote_samples.last().unwrap().duration_ms,
1234,
"recorded duration matches the call"
);
}
#[tokio::test]
async fn test_spawn_blocking_load_with_valid_file() {
let _guard = test_guard!();
let temp_dir = tempfile::tempdir().unwrap();
let history_path = temp_dir.path().join("timing_history.json");
let mut history = TimingHistory::default();
history.record(
"test-project",
Some(CompilationKind::CargoBuild),
1000,
false,
);
let json = serde_json::to_string_pretty(&history).unwrap();
std::fs::write(&history_path, json).unwrap();
let path = history_path.clone();
let loaded = tokio::task::spawn_blocking(move || {
std::fs::read_to_string(&path)
.ok()
.and_then(|content| serde_json::from_str::<TimingHistory>(&content).ok())
.unwrap_or_default()
})
.await
.unwrap();
let data = loaded.get("test-project", Some(CompilationKind::CargoBuild));
assert!(data.is_some());
assert_eq!(data.unwrap().local_samples.len(), 1);
}
#[tokio::test]
async fn test_spawn_blocking_load_missing_file() {
let _guard = test_guard!();
let temp_dir = tempfile::tempdir().unwrap();
let missing_path = temp_dir.path().join("nonexistent.json");
let loaded = tokio::task::spawn_blocking(move || {
std::fs::read_to_string(&missing_path)
.ok()
.and_then(|content| serde_json::from_str::<TimingHistory>(&content).ok())
.unwrap_or_default()
})
.await
.unwrap();
assert!(loaded.entries.is_empty());
}
#[tokio::test]
async fn test_spawn_blocking_load_corrupt_json() {
let _guard = test_guard!();
let temp_dir = tempfile::tempdir().unwrap();
let corrupt_path = temp_dir.path().join("corrupt.json");
std::fs::write(&corrupt_path, "not valid json {{{").unwrap();
let loaded = tokio::task::spawn_blocking(move || {
std::fs::read_to_string(&corrupt_path)
.ok()
.and_then(|content| serde_json::from_str::<TimingHistory>(&content).ok())
.unwrap_or_default()
})
.await
.unwrap();
assert!(loaded.entries.is_empty());
}
#[tokio::test]
async fn test_spawn_blocking_save_creates_file() {
let _guard = test_guard!();
let temp_dir = tempfile::tempdir().unwrap();
let save_path = temp_dir.path().join("saved_history.json");
let mut history = TimingHistory::default();
history.record(
"saved-project",
Some(CompilationKind::CargoTest),
2000,
true,
);
let path = save_path.clone();
tokio::task::spawn_blocking(move || {
let content = serde_json::to_string_pretty(&history).unwrap();
std::fs::write(&path, content).unwrap();
})
.await
.unwrap();
assert!(save_path.exists());
let content = std::fs::read_to_string(&save_path).unwrap();
let loaded: TimingHistory = serde_json::from_str(&content).unwrap();
let data = loaded.get("saved-project", Some(CompilationKind::CargoTest));
assert!(data.is_some());
assert_eq!(data.unwrap().remote_samples.len(), 1);
}
#[tokio::test]
async fn test_spawn_blocking_timeout_protection() {
let _guard = test_guard!();
let result = tokio::time::timeout(
std::time::Duration::from_secs(5),
tokio::task::spawn_blocking(|| {
let history = TimingHistory::default();
std::thread::sleep(std::time::Duration::from_millis(10));
history
}),
)
.await;
assert!(
result.is_ok(),
"spawn_blocking should complete within 5s timeout"
);
}
#[tokio::test]
async fn test_spawn_blocking_concurrent_loads() {
let _guard = test_guard!();
let temp_dir = tempfile::tempdir().unwrap();
let history_path = temp_dir.path().join("concurrent.json");
let mut history = TimingHistory::default();
history.record("concurrent", Some(CompilationKind::CargoBuild), 500, false);
std::fs::write(&history_path, serde_json::to_string(&history).unwrap()).unwrap();
let mut handles = Vec::new();
for _ in 0..5 {
let path = history_path.clone();
handles.push(tokio::task::spawn_blocking(move || {
std::fs::read_to_string(&path)
.ok()
.and_then(|c| serde_json::from_str::<TimingHistory>(&c).ok())
.unwrap_or_default()
}));
}
for handle in handles {
let loaded = handle.await.unwrap();
assert!(
loaded
.get("concurrent", Some(CompilationKind::CargoBuild))
.is_some()
);
}
}
#[tokio::test]
async fn test_spawn_blocking_concurrent_saves() {
let _guard = test_guard!();
let temp_dir = tempfile::tempdir().unwrap();
let mut handles = Vec::new();
for i in 0..5 {
let path = temp_dir.path().join(format!("save_{}.json", i));
let mut history = TimingHistory::default();
history.record(
&format!("project-{}", i),
Some(CompilationKind::CargoBuild),
100 * i as u64,
false,
);
handles.push(tokio::task::spawn_blocking(move || {
let content = serde_json::to_string(&history).unwrap();
std::fs::write(&path, content).unwrap();
path
}));
}
for handle in handles {
let path = handle.await.unwrap();
assert!(path.exists(), "File should be created: {:?}", path);
}
}
#[tokio::test]
async fn test_spawn_blocking_performance_budget() {
let _guard = test_guard!();
let temp_dir = tempfile::tempdir().unwrap();
let history_path = temp_dir.path().join("perf_test.json");
let mut history = TimingHistory::default();
for i in 0..10 {
history.record(
&format!("project-{}", i),
Some(CompilationKind::CargoBuild),
1000 + i * 100,
false,
);
history.record(
&format!("project-{}", i),
Some(CompilationKind::CargoBuild),
800 + i * 50,
true,
);
}
std::fs::write(
&history_path,
serde_json::to_string_pretty(&history).unwrap(),
)
.unwrap();
let load_path = history_path.clone();
let start = std::time::Instant::now();
let _loaded = tokio::task::spawn_blocking(move || {
std::fs::read_to_string(&load_path)
.ok()
.and_then(|c| serde_json::from_str::<TimingHistory>(&c).ok())
.unwrap_or_default()
})
.await
.unwrap();
let load_duration = start.elapsed();
let save_path = temp_dir.path().join("perf_save.json");
let start = std::time::Instant::now();
tokio::task::spawn_blocking(move || {
let content = serde_json::to_string_pretty(&history).unwrap();
std::fs::write(&save_path, content).unwrap();
})
.await
.unwrap();
let save_duration = start.elapsed();
let total = load_duration + save_duration;
eprintln!("Performance test results:");
eprintln!(" Load: {:?}", load_duration);
eprintln!(" Save: {:?}", save_duration);
eprintln!(" Total: {:?}", total);
assert!(
total < std::time::Duration::from_millis(50),
"Load+save took {:?}, should be <50ms for CI compatibility",
total
);
}
#[test]
fn test_source_intent_topology_root_has_no_trailing_separator() {
let _guard = test_guard!();
let (directory, policy) = topology_tempdir();
let project_root = directory.path().to_path_buf();
let plan = build_sync_closure_plan(
std::slice::from_ref(&project_root),
&project_root,
"topology-root-hash",
&policy,
);
assert_eq!(plan.len(), 1);
assert!(plan[0].is_primary);
assert_eq!(
plan[0].remote_root,
policy.canonical_root().to_str().unwrap()
);
let target = format!("{}/.rch-target", plan[0].remote_root);
assert_eq!(
target,
policy
.canonical_root()
.join(".rch-target")
.to_str()
.unwrap()
);
let child = project_root.join("nested");
std::fs::create_dir(&child).unwrap();
let child_plan = build_sync_closure_plan(
std::slice::from_ref(&child),
&child,
"topology-child-hash",
&policy,
);
assert_eq!(child_plan.len(), 1);
assert_eq!(child_plan[0].remote_root, child.to_str().unwrap());
}
#[test]
fn test_build_sync_closure_manifest_deterministic_entries() {
let _guard = test_guard!();
let (temp_dir, policy) = topology_tempdir();
let project_root = temp_dir.path().join("project");
let dep_a = temp_dir.path().join("dep_a");
let dep_b = temp_dir.path().join("dep_b");
std::fs::create_dir_all(&project_root).expect("create project root");
std::fs::create_dir_all(&dep_a).expect("create dep_a");
std::fs::create_dir_all(&dep_b).expect("create dep_b");
let plan = build_sync_closure_plan(
&[dep_b.clone(), dep_a.clone(), project_root.clone()],
&project_root,
"abc123",
&policy,
);
let manifest_a = build_sync_closure_manifest(&plan, &project_root);
let manifest_b = build_sync_closure_manifest(&plan, &project_root);
assert_eq!(
manifest_a.entries, manifest_b.entries,
"manifest entries should be deterministic for the same plan"
);
assert_eq!(
manifest_a.schema_version, manifest_b.schema_version,
"schema version must be stable"
);
assert_eq!(
manifest_a.project_root, manifest_b.project_root,
"project root must be stable"
);
}
#[test]
fn test_build_sync_closure_manifest_schema_version_stable() {
let _guard = test_guard!();
let (temp_dir, policy) = topology_tempdir();
let project_root = temp_dir.path().join("project");
std::fs::create_dir_all(&project_root).expect("create project root");
let plan = build_sync_closure_plan(
std::slice::from_ref(&project_root),
&project_root,
"deadbeef",
&policy,
);
let manifest = build_sync_closure_manifest(&plan, &project_root);
assert_eq!(
manifest.schema_version, "rch.sync_closure_manifest.v2",
"schema version must match the documented v2 contract"
);
}
#[test]
fn test_build_sync_closure_manifest_entries_faithfully_represent_plan() {
let _guard = test_guard!();
let (temp_dir, policy) = topology_tempdir();
let project_root = temp_dir.path().join("project");
let dep = temp_dir.path().join("dep");
std::fs::create_dir_all(&project_root).expect("create project root");
std::fs::create_dir_all(&dep).expect("create dep");
let plan = build_sync_closure_plan(
&[dep.clone(), project_root.clone()],
&project_root,
"cafe0001",
&policy,
);
let manifest = build_sync_closure_manifest(&plan, &project_root);
assert_eq!(
manifest.entries.len(),
plan.len(),
"manifest must have one entry per plan entry"
);
for (idx, (plan_entry, manifest_entry)) in plan.iter().zip(manifest.entries.iter()).enumerate()
{
assert_eq!(manifest_entry.order, idx + 1, "order must be 1-indexed");
assert_eq!(
manifest_entry.local_root,
plan_entry.local_root.to_string_lossy().to_string()
);
assert_eq!(manifest_entry.remote_root, plan_entry.remote_root);
assert_eq!(manifest_entry.project_id, plan_entry.project_id);
assert_eq!(manifest_entry.root_hash, plan_entry.root_hash);
assert_eq!(manifest_entry.is_primary, plan_entry.is_primary);
}
}
#[test]
fn test_build_sync_closure_manifest_primary_root_present() {
let _guard = test_guard!();
let (temp_dir, policy) = topology_tempdir();
let project_root = temp_dir.path().join("project");
let dep = temp_dir.path().join("dep");
std::fs::create_dir_all(&project_root).expect("create project root");
std::fs::create_dir_all(&dep).expect("create dep");
let plan = build_sync_closure_plan(
&[dep.clone(), project_root.clone()],
&project_root,
"primary_hash",
&policy,
);
let manifest = build_sync_closure_manifest(&plan, &project_root);
let primary_entries: Vec<_> = manifest.entries.iter().filter(|e| e.is_primary).collect();
assert_eq!(
primary_entries.len(),
1,
"exactly one manifest entry should be the primary root"
);
assert_eq!(
primary_entries[0].root_hash, "primary_hash",
"primary entry must carry the project-level hash"
);
}
#[test]
fn test_build_sync_closure_plan_adds_primary_even_when_absent_from_roots() {
let _guard = test_guard!();
let (temp_dir, policy) = topology_tempdir();
let project_root = temp_dir.path().join("project");
let dep = temp_dir.path().join("dep");
std::fs::create_dir_all(&project_root).expect("create project root");
std::fs::create_dir_all(&dep).expect("create dep");
let plan = build_sync_closure_plan(
std::slice::from_ref(&dep),
&project_root,
"hash_auto_add",
&policy,
);
let has_primary = plan.iter().any(|e| e.is_primary);
assert!(
has_primary,
"primary root must be auto-added to plan even when not in sync_roots"
);
let primary = plan.iter().find(|e| e.is_primary).unwrap();
assert_eq!(primary.root_hash, "hash_auto_add");
}
#[test]
fn test_sync_root_outcome_diagnostic_counting() {
let _guard = test_guard!();
let (temp_dir, policy) = topology_tempdir();
let project_root = temp_dir.path().join("project");
let dep_a = temp_dir.path().join("dep_a");
let dep_b = temp_dir.path().join("dep_b");
let dep_c = temp_dir.path().join("dep_c");
std::fs::create_dir_all(&project_root).expect("create project root");
std::fs::create_dir_all(&dep_a).expect("create dep_a");
std::fs::create_dir_all(&dep_b).expect("create dep_b");
std::fs::create_dir_all(&dep_c).expect("create dep_c");
let plan = build_sync_closure_plan(
&[
dep_a.clone(),
dep_b.clone(),
dep_c.clone(),
project_root.clone(),
],
&project_root,
"diag_hash",
&policy,
);
let outcomes: Vec<(&SyncClosurePlanEntry, SyncRootOutcome)> = plan
.iter()
.map(|entry| {
let outcome = if entry.is_primary || entry.local_root.ends_with("dep_a") {
SyncRootOutcome::Synced
} else if entry.local_root.ends_with("dep_b") {
SyncRootOutcome::Skipped {
reason: "size too small".to_string(),
}
} else {
SyncRootOutcome::Failed {
error: "rsync timeout".to_string(),
}
};
(entry, outcome)
})
.collect();
let failed_count = outcomes
.iter()
.filter(|(_, o)| !matches!(o, SyncRootOutcome::Synced))
.count();
assert_eq!(
failed_count, 2,
"skipped + failed should count as non-synced"
);
let synced_count = outcomes
.iter()
.filter(|(_, o)| matches!(o, SyncRootOutcome::Synced))
.count();
assert_eq!(synced_count, 2, "primary + dep_a should be synced");
}
#[test]
fn test_build_sync_closure_manifest_serializes_to_json() {
let _guard = test_guard!();
let (temp_dir, policy) = topology_tempdir();
let project_root = temp_dir.path().join("project");
let dep = temp_dir.path().join("dep");
std::fs::create_dir_all(&project_root).expect("create project root");
std::fs::create_dir_all(&dep).expect("create dep");
let plan = build_sync_closure_plan(
&[dep.clone(), project_root.clone()],
&project_root,
"serial_hash",
&policy,
);
let manifest = build_sync_closure_manifest(&plan, &project_root);
let json = serde_json::to_string_pretty(&manifest).expect("manifest should serialize to JSON");
assert!(
json.contains("rch.sync_closure_manifest.v2"),
"JSON must contain schema_version"
);
assert!(
json.contains("serial_hash"),
"JSON must contain the primary root hash"
);
assert!(
json.contains("\"is_primary\": true"),
"JSON must contain primary flag"
);
let parsed: serde_json::Value =
serde_json::from_str(&json).expect("manifest JSON should be valid");
let entries = parsed["entries"]
.as_array()
.expect("entries should be an array");
assert_eq!(entries.len(), plan.len());
}
#[test]
fn test_is_within_sync_topology_accepts_canonical_root() {
let _guard = test_guard!();
let policy = rch_common::path_topology::PathTopologyPolicy::default();
let path = PathBuf::from("/data/projects/my_project");
assert!(
is_within_sync_topology(&path, &policy),
"paths under /data/projects should be accepted"
);
}
#[test]
fn test_is_within_sync_topology_accepts_alias_root() {
let _guard = test_guard!();
let policy = rch_common::path_topology::PathTopologyPolicy::default();
let path = PathBuf::from("/dp/my_project");
assert!(
is_within_sync_topology(&path, &policy),
"paths under /dp alias should be accepted"
);
}
#[test]
fn test_is_within_sync_topology_rejects_outside_paths() {
let _guard = test_guard!();
let policy = rch_common::path_topology::PathTopologyPolicy::default();
assert!(
!is_within_sync_topology(Path::new("/tmp/evil"), &policy),
"/tmp paths should be rejected"
);
assert!(
!is_within_sync_topology(Path::new("/home/user/project"), &policy),
"/home paths should be rejected"
);
assert!(
!is_within_sync_topology(Path::new("/var/lib/data"), &policy),
"/var paths should be rejected"
);
}
#[test]
fn test_build_sync_closure_plan_excludes_out_of_topology_roots() {
let _guard = test_guard!();
let project_root = PathBuf::from("/data/projects/test_proj");
let valid_dep = PathBuf::from("/data/projects/valid_dep");
let invalid_dep = PathBuf::from("/tmp/not_allowed");
let plan = build_sync_closure_plan(
&[valid_dep.clone(), invalid_dep.clone(), project_root.clone()],
&project_root,
"topo_hash",
&PathTopologyPolicy::default(),
);
let plan_paths: Vec<_> = plan.iter().map(|e| &e.local_root).collect();
assert!(
plan_paths
.iter()
.any(|p| p.starts_with("/data/projects/test_proj")),
"primary root must be in plan"
);
assert!(
plan_paths
.iter()
.any(|p| p.starts_with("/data/projects/valid_dep")),
"valid dependency root must be in plan"
);
assert!(
!plan_paths.iter().any(|p| p.starts_with("/tmp")),
"out-of-topology dependency must be excluded from plan"
);
}
#[test]
fn test_build_sync_closure_plan_topology_filter_preserves_primary() {
let _guard = test_guard!();
let project_root = PathBuf::from("/data/projects/primary_proj");
let bad_dep_a = PathBuf::from("/home/user/dep_a");
let bad_dep_b = PathBuf::from("/var/lib/dep_b");
let plan = build_sync_closure_plan(
&[bad_dep_a, bad_dep_b],
&project_root,
"lonely_hash",
&PathTopologyPolicy::default(),
);
assert_eq!(plan.len(), 1, "only the primary root should remain");
assert!(
plan[0].is_primary,
"surviving entry must be the primary root"
);
}
#[test]
fn test_canonicalize_existing_path() {
let _guard = test_guard!();
let (temp_dir, policy) = topology_tempdir();
let dir = temp_dir.path().join("real_dir");
std::fs::create_dir_all(&dir).expect("create dir");
let result = canonicalize_sync_root_for_plan(&dir, &policy);
assert!(result.is_absolute());
assert!(
result.to_string_lossy().contains("real_dir"),
"canonicalized path should contain dir name: {}",
result.display()
);
}
#[test]
fn test_canonicalize_nonexistent_path() {
let _guard = test_guard!();
let path = PathBuf::from("/data/projects/does_not_exist_xyz_12345");
let policy = rch_common::path_topology::PathTopologyPolicy::default();
let result = canonicalize_sync_root_for_plan(&path, &policy);
assert_eq!(result, path);
}
#[test]
fn test_canonicalize_trailing_slash() {
let _guard = test_guard!();
let (temp_dir, policy) = topology_tempdir();
let dir = temp_dir.path().join("trail");
std::fs::create_dir_all(&dir).expect("create dir");
let with_trailing = PathBuf::from(format!("{}/", dir.display()));
let without_trailing = canonicalize_sync_root_for_plan(&dir, &policy);
let with_result = canonicalize_sync_root_for_plan(&with_trailing, &policy);
assert_eq!(with_result, without_trailing);
}
#[cfg(unix)]
#[test]
fn test_canonicalize_symlink() {
let _guard = test_guard!();
use std::os::unix::fs::symlink;
let (temp_dir, policy) = topology_tempdir();
let real_dir = temp_dir.path().join("real");
let link_dir = temp_dir.path().join("link");
std::fs::create_dir_all(&real_dir).expect("create real dir");
symlink(&real_dir, &link_dir).expect("create symlink");
let from_real = canonicalize_sync_root_for_plan(&real_dir, &policy);
let from_link = canonicalize_sync_root_for_plan(&link_dir, &policy);
assert_eq!(
from_real, from_link,
"symlink and real path should canonicalize to the same path"
);
}
#[test]
fn test_canonicalize_dp_alias() {
let _guard = test_guard!();
let dp_path = PathBuf::from("/dp/remote_compilation_helper");
let canonical_expected = PathBuf::from("/data/projects/remote_compilation_helper");
let (Ok(dp_canonical), true) = (std::fs::canonicalize(&dp_path), canonical_expected.exists())
else {
return;
};
if dp_canonical != canonical_expected {
return;
}
let policy = rch_common::path_topology::PathTopologyPolicy::default();
let result = canonicalize_sync_root_for_plan(&dp_path, &policy);
assert_eq!(
result, canonical_expected,
"/dp alias should resolve to /data/projects"
);
}
#[test]
fn test_topology_deeply_nested_accepted() {
let _guard = test_guard!();
let policy = rch_common::path_topology::PathTopologyPolicy::default();
let path = PathBuf::from("/data/projects/a/b/c/d/e/f/g");
assert!(
is_within_sync_topology(&path, &policy),
"deeply nested /data/projects subpaths should be accepted"
);
}
#[test]
fn test_topology_exact_root_match() {
let _guard = test_guard!();
let policy = rch_common::path_topology::PathTopologyPolicy::default();
assert!(
is_within_sync_topology(Path::new("/data/projects"), &policy),
"/data/projects itself should be accepted"
);
assert!(
is_within_sync_topology(Path::new("/dp"), &policy),
"/dp itself should be accepted"
);
}
#[test]
fn test_topology_parent_of_root_rejected() {
let _guard = test_guard!();
let policy = rch_common::path_topology::PathTopologyPolicy::default();
assert!(
!is_within_sync_topology(Path::new("/data"), &policy),
"/data (parent of root) should be rejected"
);
}
#[test]
fn test_topology_prefix_collision_rejected() {
let _guard = test_guard!();
let policy = rch_common::path_topology::PathTopologyPolicy::default();
assert!(
!is_within_sync_topology(Path::new("/data/projects_extra"), &policy),
"/data/projects_extra should be rejected (not a child path)"
);
}
#[test]
fn test_topology_empty_path_rejected() {
let _guard = test_guard!();
let policy = rch_common::path_topology::PathTopologyPolicy::default();
assert!(
!is_within_sync_topology(Path::new(""), &policy),
"empty path should be rejected"
);
}
#[test]
fn test_topology_root_slash_rejected() {
let _guard = test_guard!();
let policy = rch_common::path_topology::PathTopologyPolicy::default();
assert!(
!is_within_sync_topology(Path::new("/"), &policy),
"root path (/) should be rejected"
);
}
#[test]
fn test_plan_empty_sync_roots() {
let _guard = test_guard!();
let project_root = PathBuf::from("/data/projects/solo_project");
let plan = build_sync_closure_plan(
&[],
&project_root,
"solo_hash",
&PathTopologyPolicy::default(),
);
assert_eq!(
plan.len(),
1,
"empty sync_roots should produce single primary entry"
);
assert!(plan[0].is_primary);
assert_eq!(plan[0].root_hash, "solo_hash");
}
#[test]
fn test_plan_primary_is_only_root() {
let _guard = test_guard!();
let (temp_dir, policy) = topology_tempdir();
let project_root = temp_dir.path().join("only");
std::fs::create_dir_all(&project_root).expect("create dir");
let plan = build_sync_closure_plan(
std::slice::from_ref(&project_root),
&project_root,
"only_hash",
&policy,
);
assert_eq!(plan.len(), 1);
assert!(plan[0].is_primary);
assert_eq!(plan[0].root_hash, "only_hash");
}
#[test]
fn test_plan_large_root_set() {
let _guard = test_guard!();
let (temp_dir, policy) = topology_tempdir();
let project_root = temp_dir.path().join("main_proj");
std::fs::create_dir_all(&project_root).expect("create main");
let mut roots = Vec::new();
for i in 0..100u32 {
let dep = temp_dir.path().join(format!("dep_{i:04}"));
std::fs::create_dir_all(&dep).expect("create dep");
roots.push(dep);
}
roots.push(project_root.clone());
let start = std::time::Instant::now();
let plan = build_sync_closure_plan(&roots, &project_root, "large_hash", &policy);
let elapsed = start.elapsed();
assert_eq!(plan.len(), 101);
assert!(
elapsed.as_millis() < 500,
"plan build took too long: {elapsed:?}"
);
for window in plan.windows(2) {
assert!(
window[0].local_root <= window[1].local_root,
"plan should be lexicographically ordered: {} > {}",
window[0].local_root.display(),
window[1].local_root.display(),
);
}
}
#[test]
fn test_plan_duplicate_roots_deduped() {
let _guard = test_guard!();
let (temp_dir, policy) = topology_tempdir();
let project_root = temp_dir.path().join("proj");
let dep = temp_dir.path().join("dep");
std::fs::create_dir_all(&project_root).expect("create proj");
std::fs::create_dir_all(&dep).expect("create dep");
let plan = build_sync_closure_plan(
&[dep.clone(), dep.clone(), dep.clone(), project_root.clone()],
&project_root,
"dup_hash",
&policy,
);
assert_eq!(plan.len(), 2, "duplicate roots should be deduped");
}
#[test]
fn test_plan_primary_via_dp_alias_canonical() {
let _guard = test_guard!();
let dp_path = PathBuf::from("/dp/remote_compilation_helper");
let Ok(canonical) = std::fs::canonicalize(&dp_path) else {
return;
};
let plan = build_sync_closure_plan(&[], &dp_path, "dp_hash", &PathTopologyPolicy::default());
assert_eq!(plan.len(), 1);
assert!(plan[0].is_primary);
assert_eq!(
plan[0].local_root, canonical,
"primary via /dp alias should canonicalize to the alias target"
);
assert_eq!(
plan[0].remote_root, "/data/projects/remote_compilation_helper",
"remote root should stay in worker canonical topology"
);
}
#[test]
fn test_build_sync_closure_plan_maps_alias_target_roots_to_worker_canonical_topology() {
let _guard = test_guard!();
if let Ok(local_projects_root) = std::fs::canonicalize("/dp") {
let project_root = local_projects_root.join("frankenterm");
let dep_root = local_projects_root.join("frankentui");
let plan = build_sync_closure_plan(
&[dep_root.clone(), project_root.clone()],
&project_root,
"mapped_hash",
&PathTopologyPolicy::default(),
);
assert!(
plan.iter().any(|entry| entry.local_root == project_root
&& entry.remote_root == "/data/projects/frankenterm"),
"primary root should map back to worker canonical topology"
);
assert!(
plan.iter().any(|entry| entry.local_root == dep_root
&& entry.remote_root == "/data/projects/frankentui"),
"dependency root should map back to worker canonical topology"
);
}
}
#[test]
fn test_plan_entry_ordering_is_lexicographic() {
let _guard = test_guard!();
let (temp_dir, policy) = topology_tempdir();
let project_root = temp_dir.path().join("proj");
let dep_z = temp_dir.path().join("z_dep");
let dep_a = temp_dir.path().join("a_dep");
let dep_m = temp_dir.path().join("m_dep");
std::fs::create_dir_all(&project_root).expect("create proj");
std::fs::create_dir_all(&dep_z).expect("create dep_z");
std::fs::create_dir_all(&dep_a).expect("create dep_a");
std::fs::create_dir_all(&dep_m).expect("create dep_m");
let plan = build_sync_closure_plan(
&[dep_z, dep_a, dep_m, project_root.clone()],
&project_root,
"order_hash",
&policy,
);
for window in plan.windows(2) {
assert!(
window[0].local_root <= window[1].local_root,
"entries must be lexicographically sorted"
);
}
}
#[test]
fn test_manifest_empty_plan() {
let _guard = test_guard!();
let project_root = PathBuf::from("/data/projects/empty_proj");
let manifest = build_sync_closure_manifest(&[], &project_root);
assert_eq!(manifest.entries.len(), 0);
assert_eq!(manifest.project_root, "/data/projects/empty_proj");
assert_eq!(manifest.schema_version, "rch.sync_closure_manifest.v2");
}
#[test]
fn test_manifest_generated_at_is_recent() {
let _guard = test_guard!();
let (temp_dir, policy) = topology_tempdir();
let project_root = temp_dir.path().join("proj");
std::fs::create_dir_all(&project_root).expect("create proj");
let before_ms = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_millis() as i64;
let plan = build_sync_closure_plan(
std::slice::from_ref(&project_root),
&project_root,
"ts_hash",
&policy,
);
let manifest = build_sync_closure_manifest(&plan, &project_root);
let after_ms = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_millis() as i64;
assert!(
manifest.generated_at_unix_ms >= before_ms,
"generated_at should be >= start time"
);
assert!(
manifest.generated_at_unix_ms <= after_ms,
"generated_at should be <= end time"
);
}
#[test]
fn test_manifest_order_field_sequential() {
let _guard = test_guard!();
let (temp_dir, policy) = topology_tempdir();
let project_root = temp_dir.path().join("proj");
std::fs::create_dir_all(&project_root).expect("create proj");
let mut roots = Vec::new();
for i in 0..10u32 {
let dep = temp_dir.path().join(format!("dep_{i:02}"));
std::fs::create_dir_all(&dep).expect("create dep");
roots.push(dep);
}
roots.push(project_root.clone());
let plan = build_sync_closure_plan(&roots, &project_root, "seq_hash", &policy);
let manifest = build_sync_closure_manifest(&plan, &project_root);
for (idx, entry) in manifest.entries.iter().enumerate() {
assert_eq!(
entry.order,
idx + 1,
"order should be 1-indexed sequential, got {} at position {}",
entry.order,
idx
);
}
}
#[test]
fn test_manifest_unicode_paths() {
let _guard = test_guard!();
let entries = vec![SyncClosurePlanEntry {
local_root: PathBuf::from("/data/projects/日本語プロジェクト"),
remote_root: "/data/projects/日本語プロジェクト".to_string(),
project_id: "日本語".to_string(),
root_hash: "unicode_hash".to_string(),
is_primary: true,
mode: SyncClosureMode::Full,
}];
let manifest =
build_sync_closure_manifest(&entries, Path::new("/data/projects/日本語プロジェクト"));
assert_eq!(manifest.entries.len(), 1);
assert!(manifest.entries[0].local_root.contains("日本語"));
let json = serde_json::to_string(&manifest).expect("should serialize unicode");
assert!(json.contains("日本語"));
}
#[test]
fn test_manifest_long_strings() {
let _guard = test_guard!();
let long_id = "x".repeat(10_000);
let long_hash = "h".repeat(10_000);
let entries = vec![SyncClosurePlanEntry {
local_root: PathBuf::from("/data/projects/long_test"),
remote_root: "/data/projects/long_test".to_string(),
project_id: long_id.clone(),
root_hash: long_hash.clone(),
is_primary: true,
mode: SyncClosureMode::Full,
}];
let manifest = build_sync_closure_manifest(&entries, Path::new("/data/projects/long_test"));
assert_eq!(
manifest.entries[0].project_id, long_id,
"project_id should not be truncated"
);
assert_eq!(
manifest.entries[0].root_hash, long_hash,
"root_hash should not be truncated"
);
}
#[test]
fn test_sync_root_outcome_all_synced() {
let _guard = test_guard!();
let outcomes: Vec<SyncRootOutcome> = (0..5).map(|_| SyncRootOutcome::Synced).collect();
let non_synced = outcomes
.iter()
.filter(|o| !matches!(o, SyncRootOutcome::Synced))
.count();
assert_eq!(non_synced, 0);
}
#[test]
fn test_sync_root_outcome_all_failed() {
let _guard = test_guard!();
let outcomes: Vec<SyncRootOutcome> = (0..3)
.map(|i| SyncRootOutcome::Failed {
error: format!("error_{i}"),
})
.collect();
let failed_count = outcomes
.iter()
.filter(|o| matches!(o, SyncRootOutcome::Failed { .. }))
.count();
assert_eq!(failed_count, 3);
let errors: Vec<&str> = outcomes
.iter()
.filter_map(|o| match o {
SyncRootOutcome::Failed { error } => Some(error.as_str()),
_ => None,
})
.collect();
assert_eq!(errors, vec!["error_0", "error_1", "error_2"]);
}
#[test]
fn test_sync_root_outcome_all_skipped() {
let _guard = test_guard!();
let outcomes: Vec<SyncRootOutcome> = (0..4)
.map(|i| SyncRootOutcome::Skipped {
reason: format!("reason_{i}"),
})
.collect();
let skipped_count = outcomes
.iter()
.filter(|o| matches!(o, SyncRootOutcome::Skipped { .. }))
.count();
assert_eq!(skipped_count, 4);
}
#[test]
fn test_sync_root_outcome_empty_collection() {
let _guard = test_guard!();
let outcomes: Vec<SyncRootOutcome> = vec![];
let synced = outcomes
.iter()
.filter(|o| matches!(o, SyncRootOutcome::Synced))
.count();
let failed = outcomes
.iter()
.filter(|o| matches!(o, SyncRootOutcome::Failed { .. }))
.count();
let skipped = outcomes
.iter()
.filter(|o| matches!(o, SyncRootOutcome::Skipped { .. }))
.count();
assert_eq!(synced, 0);
assert_eq!(failed, 0);
assert_eq!(skipped, 0);
}
#[test]
fn test_sync_root_outcome_mixed_with_reasons() {
let _guard = test_guard!();
let outcomes = [
SyncRootOutcome::Synced,
SyncRootOutcome::Synced,
SyncRootOutcome::Skipped {
reason: "stale".to_string(),
},
SyncRootOutcome::Failed {
error: "timeout".to_string(),
},
SyncRootOutcome::Skipped {
reason: "denied".to_string(),
},
];
let synced = outcomes
.iter()
.filter(|o| matches!(o, SyncRootOutcome::Synced))
.count();
let failed = outcomes
.iter()
.filter(|o| matches!(o, SyncRootOutcome::Failed { .. }))
.count();
let skipped = outcomes
.iter()
.filter(|o| matches!(o, SyncRootOutcome::Skipped { .. }))
.count();
assert_eq!(synced, 2);
assert_eq!(failed, 1);
assert_eq!(skipped, 2);
let skip_reasons: Vec<&str> = outcomes
.iter()
.filter_map(|o| match o {
SyncRootOutcome::Skipped { reason } => Some(reason.as_str()),
_ => None,
})
.collect();
assert_eq!(skip_reasons, vec!["stale", "denied"]);
let error_msgs: Vec<&str> = outcomes
.iter()
.filter_map(|o| match o {
SyncRootOutcome::Failed { error } => Some(error.as_str()),
_ => None,
})
.collect();
assert_eq!(error_msgs, vec!["timeout"]);
}
#[test]
fn test_e2e_sync_closure_plan_and_manifest() {
let _guard = test_guard!();
let (temp_dir, policy) = topology_tempdir();
let primary = temp_dir.path().join("primary_project");
let dep_a = temp_dir.path().join("dep_a");
let dep_b = temp_dir.path().join("dep_b");
std::fs::create_dir_all(&primary).expect("create primary");
std::fs::create_dir_all(&dep_a).expect("create dep_a");
std::fs::create_dir_all(&dep_b).expect("create dep_b");
let out_of_topology = PathBuf::from("/var/empty/invalid_dep");
let plan = build_sync_closure_plan(
&[
primary.clone(),
dep_a.clone(),
dep_b.clone(),
out_of_topology.clone(),
],
&primary,
"e2e_hash",
&policy,
);
assert_eq!(
plan.len(),
3,
"plan should have 3 entries (primary + 2 deps), got {}",
plan.len()
);
let out_of_topology_str = out_of_topology.to_string_lossy().to_string();
assert!(
!plan
.iter()
.any(|e| e.local_root.to_string_lossy() == out_of_topology_str),
"out-of-topology dep should be excluded by topology filter"
);
for window in plan.windows(2) {
assert!(
window[0].local_root <= window[1].local_root,
"plan entries should be lexicographically sorted"
);
}
let primary_entry = plan
.iter()
.find(|e| e.is_primary)
.expect("primary must exist");
assert_eq!(primary_entry.root_hash, "e2e_hash");
let non_primary: Vec<_> = plan.iter().filter(|e| !e.is_primary).collect();
assert_eq!(non_primary.len(), 2, "should have 2 non-primary entries");
let before_ms = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_millis() as i64;
let manifest = build_sync_closure_manifest(&plan, &primary);
let after_ms = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_millis() as i64;
assert_eq!(manifest.schema_version, "rch.sync_closure_manifest.v2");
assert_eq!(manifest.entries.len(), 3);
assert!(manifest.generated_at_unix_ms >= before_ms);
assert!(manifest.generated_at_unix_ms <= after_ms);
let json = serde_json::to_string_pretty(&manifest).expect("serialize");
let parsed: serde_json::Value = serde_json::from_str(&json).expect("parse");
let entries = parsed["entries"].as_array().expect("entries array");
assert_eq!(entries.len(), 3);
for (idx, entry) in manifest.entries.iter().enumerate() {
assert_eq!(entry.order, idx + 1, "order should be 1-indexed sequential");
assert_eq!(entry.is_primary, plan[idx].is_primary);
assert_eq!(entry.root_hash, plan[idx].root_hash);
}
}
#[cfg(unix)]
#[test]
fn test_e2e_topology_validation_with_symlinks() {
let _guard = test_guard!();
use std::os::unix::fs::symlink;
let (temp_dir, policy) = topology_tempdir();
let valid_root = temp_dir.path().join("valid_root");
let valid_sub = valid_root.join("sub");
std::fs::create_dir_all(&valid_sub).expect("create valid_root/sub");
let primary = temp_dir.path().join("primary");
std::fs::create_dir_all(&primary).expect("create primary");
let alias_link = temp_dir.path().join("alias_for_valid");
symlink(&valid_root, &alias_link).expect("create symlink");
let reject_a = PathBuf::from("/etc/rch_should_reject");
let reject_b = PathBuf::from("/usr/local/fake_project");
let reject_c = PathBuf::from("/opt/fake_thing");
let plan = build_sync_closure_plan(
&[
valid_root.clone(),
alias_link.clone(), reject_a.clone(),
reject_b.clone(),
reject_c.clone(),
primary.clone(),
],
&primary,
"topo_e2e_hash",
&policy,
);
assert_eq!(
plan.len(),
2,
"plan should have 2 entries (primary + deduped valid_root), got {}",
plan.len()
);
let reject_strs: Vec<String> = [reject_a, reject_b, reject_c]
.iter()
.map(|p| p.to_string_lossy().to_string())
.collect();
for entry in &plan {
let path_str = entry.local_root.to_string_lossy().to_string();
assert!(
!reject_strs.contains(&path_str),
"out-of-topology path should not appear in plan: {}",
path_str
);
}
let valid_canonical = std::fs::canonicalize(&valid_root).expect("canonicalize");
let matching_entries = plan
.iter()
.filter(|e| {
std::fs::canonicalize(&e.local_root)
.map(|c| c == valid_canonical)
.unwrap_or(false)
})
.count();
assert_eq!(
matching_entries, 1,
"symlink alias should be deduplicated with canonical path"
);
assert!(
plan.iter().any(|e| e.is_primary),
"primary root must always be in plan"
);
}
#[test]
fn test_classification_timing_budget_non_compilation() {
let _guard = test_guard!();
let non_compilation_cmds = [
"ls -la",
"pwd",
"git status",
"echo hello world",
"cat Cargo.toml",
"npm install",
"python main.py",
"docker build -t myapp .",
"mkdir -p build",
"rm -rf target/",
];
for cmd in non_compilation_cmds {
let start = std::time::Instant::now();
for _ in 0..100 {
let _ = classify_command(cmd);
}
let elapsed = start.elapsed();
let per_call_us = elapsed.as_micros() / 100;
assert!(
per_call_us < 1000,
"Non-compilation command {:?} exceeded 1ms budget: {}us per call",
cmd,
per_call_us
);
}
}
#[test]
fn test_classification_timing_budget_compilation() {
let _guard = test_guard!();
let compilation_cmds = [
"cargo build --release",
"cargo test --workspace",
"cargo clippy --all-targets",
"gcc -c main.c -o main.o",
"make -j8",
"bun test",
"rustc main.rs",
"ninja -j4",
];
for cmd in compilation_cmds {
let start = std::time::Instant::now();
for _ in 0..100 {
let _ = classify_command(cmd);
}
let elapsed = start.elapsed();
let per_call_us = elapsed.as_micros() / 100;
assert!(
per_call_us < 5000,
"Compilation command {:?} exceeded 5ms budget: {}us per call",
cmd,
per_call_us
);
}
}
#[tokio::test]
async fn test_hook_classification_fail_open_all_compilation_kinds() {
let _lock = test_lock().lock().await;
mock::set_mock_enabled_override(Some(false));
let compilation_commands = [
("cargo build --release", "CargoBuild"),
("cargo test --workspace", "CargoTest"),
("cargo check --all-targets", "CargoCheck"),
("cargo clippy", "CargoClippy"),
("cargo doc --no-deps", "CargoDoc"),
("cargo run", "CargoRun"),
("cargo bench", "CargoBench"),
("cargo nextest run", "CargoNextest"),
("bun test", "BunTest"),
("bun typecheck", "BunTypecheck"),
];
for (cmd, label) in compilation_commands {
let input = HookInput {
tool_name: "Bash".to_string(),
tool_input: ToolInput {
command: cmd.to_string(),
description: None,
},
session_id: None,
};
let output = process_hook(input).await;
assert!(
output.is_allow(),
"Hook should fail-open for {} ({}) when daemon absent",
label,
cmd
);
}
mock::set_mock_enabled_override(None);
}
#[tokio::test]
async fn test_hook_non_compilation_passthrough() {
let non_compilation = [
"ls -la",
"git status",
"cargo fmt --check",
"cargo install ripgrep",
"bun install",
"bun run dev",
"echo hello",
"cat Cargo.toml",
];
for cmd in non_compilation {
let input = HookInput {
tool_name: "Bash".to_string(),
tool_input: ToolInput {
command: cmd.to_string(),
description: None,
},
session_id: None,
};
let output = process_hook(input).await;
assert!(
output.is_allow(),
"Non-compilation command {:?} should pass through the hook (Allow)",
cmd
);
}
}
#[tokio::test]
async fn test_hook_non_bash_tools_always_allowed() {
let tools = ["Read", "Write", "Edit", "Glob", "Grep", "WebSearch"];
for tool in tools {
let input = HookInput {
tool_name: tool.to_string(),
tool_input: ToolInput {
command: "cargo build".to_string(), description: None,
},
session_id: None,
};
let output = process_hook(input).await;
assert!(
output.is_allow(),
"Non-Bash tool {:?} should always be allowed, even with compilation keyword",
tool
);
}
}
#[test]
fn test_structured_log_output_per_tier() {
let _guard = test_guard!();
let d = classify_command_detailed("");
assert_eq!(d.tiers.len(), 1);
assert_eq!(d.tiers[0].tier, 0);
assert_eq!(d.tiers[0].decision, TierDecision::Reject);
assert!(!d.tiers[0].reason.is_empty());
let d = classify_command_detailed("cargo build < input.txt");
assert!(
d.tiers
.iter()
.any(|t| t.tier == 1 && t.decision == TierDecision::Reject)
);
let d = classify_command_detailed("ls -la");
assert!(
d.tiers
.iter()
.any(|t| t.tier == 2 && t.decision == TierDecision::Reject)
);
let d = classify_command_detailed("cargo install serde");
assert!(
d.tiers
.iter()
.any(|t| t.tier == 3 && t.decision == TierDecision::Reject)
);
let d = classify_command_detailed("cargo build --release");
assert!(
d.tiers
.iter()
.any(|t| t.tier == 4 && t.decision == TierDecision::Pass)
);
assert!(d.classification.is_compilation);
assert!(d.classification.confidence > 0.0);
assert!(d.classification.kind.is_some());
let d = classify_command_detailed("cargo tree");
assert!(
d.tiers
.iter()
.any(|t| t.tier == 4 && t.decision == TierDecision::Reject)
);
}
#[test]
fn rustup_toolchain_from_command_tokens_parses_env_prefix_and_selector() {
let tokens = |cmd: &str| -> Vec<String> { cmd.split_whitespace().map(String::from).collect() };
assert_eq!(
rustup_toolchain_from_command_tokens(&tokens(
"env RCH_CARGO_WRAPPER_BYPASS=1 RUSTUP_TOOLCHAIN=nightly-2026-08-25 \
CARGO_TARGET_DIR=/data/p/.rch-target cargo clippy --workspace -- -D warnings"
)),
Some("nightly-2026-08-25".to_string())
);
assert_eq!(
rustup_toolchain_from_command_tokens(&tokens("RUSTUP_TOOLCHAIN=stable cargo clippy")),
Some("stable".to_string())
);
assert_eq!(
rustup_toolchain_from_command_tokens(&tokens("cargo +nightly-2026-08-25 clippy")),
Some("nightly-2026-08-25".to_string())
);
assert_eq!(
rustup_toolchain_from_command_tokens(&tokens("/usr/bin/cargo +beta build")),
Some("beta".to_string())
);
assert_eq!(
rustup_toolchain_from_command_tokens(&tokens("cargo build --features +weird")),
None
);
assert_eq!(
rustup_toolchain_from_command_tokens(&tokens("go build ./...")),
None
);
assert_eq!(
rustup_toolchain_from_command_tokens(&tokens("RUSTUP_TOOLCHAIN= cargo clippy")),
None
);
}
#[test]
fn job_admission_exhausted_is_a_typed_retryable_refusal() {
assert!(strict_remote_for_exhausted_admission(false, true));
assert!(strict_remote_for_exhausted_admission(true, false));
assert!(strict_remote_for_exhausted_admission(true, true));
assert!(!strict_remote_for_exhausted_admission(false, false));
let reason = "job admission exhausted: no admissible workers: missing_toolchain_component=14";
assert!(remote_required_refusal_is_retryable(reason));
let summary = remote_required_refusal_summary(reason);
assert!(summary.contains("refusing local fallback"));
assert!(summary.contains("retryable"));
}
#[test]
fn ssh_timeout_unverified_cleanup_detection() {
use crate::transfer::{RemoteTimeoutCleanup, SshCommandTimedOut};
let unverified: anyhow::Error = SshCommandTimedOut {
timeout: std::time::Duration::from_secs(1800),
cleanup: RemoteTimeoutCleanup::Unverified,
detail: "kill probe timed out".to_string(),
evidence: None,
}
.into();
let unverified = unverified.context("remote execution failed");
assert!(ssh_timeout_with_unverified_cleanup(&unverified).is_some());
assert!(is_ssh_command_timeout_error(&unverified));
let verified: anyhow::Error = SshCommandTimedOut {
timeout: std::time::Duration::from_secs(1800),
cleanup: RemoteTimeoutCleanup::Verified,
detail: "remote process group SIGKILLed and verified dead".to_string(),
evidence: None,
}
.into();
assert!(ssh_timeout_with_unverified_cleanup(&verified).is_none());
assert!(is_ssh_command_timeout_error(&verified));
let unrelated = anyhow::anyhow!("some other failure");
assert!(ssh_timeout_with_unverified_cleanup(&unrelated).is_none());
}
#[test]
fn project_topology_local_reason_admits_projects_under_the_canonical_root_only() {
let root = tempfile::tempdir().unwrap();
let canonical = root.path().canonicalize().unwrap();
let inside = canonical.join("repo");
std::fs::create_dir_all(&inside).unwrap();
let outside_parent = tempfile::tempdir().unwrap();
let outside = outside_parent.path().canonicalize().unwrap().join("repo");
std::fs::create_dir_all(&outside).unwrap();
let policy = PathTopologyPolicy::new(canonical.clone(), canonical.clone());
assert_eq!(project_topology_local_reason(&policy, &inside), None);
let reason = project_topology_local_reason(&policy, &outside)
.expect("a project outside the canonical root must run locally");
assert!(reason.contains("outside canonical root"), "{reason}");
assert!(reason.contains(&outside.display().to_string()), "{reason}");
assert!(
reason.contains("canonical_root"),
"the reason names the fix: {reason}"
);
}
#[tokio::test]
async fn fast_passthrough_answers_only_what_process_hook_plainly_allows() {
let request = |tool: &str, command: &str| {
serde_json::json!({
"session_id": "s",
"hook_event_name": "PreToolUse",
"tool_name": tool,
"tool_input": {"command": command},
})
.to_string()
};
for command in [
"ls -la",
"git status",
"cd /tmp && ls",
r#"br comments add x "cargo test -p rchd""#,
"echo cargo build",
] {
let input = request("Bash", command);
assert!(super::fast_passthrough_allows(&input), "{command}");
let output = process_hook(serde_json::from_str(&input).unwrap()).await;
assert!(
matches!(output, HookOutput::Allow(_)),
"fast path allowed what process_hook would not: {command} -> {output:?}"
);
}
assert!(super::fast_passthrough_allows(""));
assert!(super::fast_passthrough_allows(&request(
"Read",
"cargo build"
)));
for command in [
"cargo build --release",
"cargo test -p rch",
"cargo build | tee build.log",
"cargo build > build.log 2>&1 &",
] {
assert!(
!super::fast_passthrough_allows(&request("Bash", command)),
"{command} must reach the full path"
);
}
assert!(!super::fast_passthrough_allows("{not json"));
}
#[tokio::test]
async fn a_finished_restart_admission_wait_hands_the_lease_back_to_admission() {
let retained = tempfile::tempdir().unwrap().keep();
let socket = retained.join("rchd.sock");
let listener = UnixListener::bind(&socket).unwrap();
let server = tokio::spawn(async move {
for closed in [true, false] {
let (stream, _) = listener.accept().await.unwrap();
let (reader, mut writer) = stream.into_split();
let mut request = String::new();
TokioBufReader::new(reader)
.read_line(&mut request)
.await
.unwrap();
assert!(request.starts_with("GET /restart-admission"), "{request}");
writer
.write_all(
format!("HTTP/1.1 200 OK\r\n\r\n{{\"admission_closed\":{closed}}}").as_bytes(),
)
.await
.unwrap();
}
});
let lease = queued_selection_test_lease(retained.join("lease.json"));
let outcome = tokio::time::timeout(
Duration::from_secs(10),
await_restart_admission(
socket.to_str().unwrap(),
&lease,
&HookReporter::new(OutputVisibility::None),
),
)
.await
.expect("one closed poll must finish");
server.await.unwrap();
assert_eq!(outcome, RestartAdmissionWait::Open);
let snapshot = lease.snapshot();
assert_eq!(snapshot.phase, "admission");
assert!(
snapshot.heartbeat_unix_ms > 0,
"the hand-back is a heartbeat"
);
}
#[test]
fn only_the_daemon_restart_barrier_refusal_triggers_an_admission_wait() {
let refusal = |reason: &str| SelectionResponse {
worker: None,
reason: SelectionReason::SelectionError(reason.into()),
build_id: None,
diagnostics: None,
};
assert!(is_restart_barrier_refusal(&refusal(
"restart_admission_barrier_active"
)));
assert!(!is_restart_barrier_refusal(&refusal(
"job_cancelled_before_start"
)));
assert!(!is_restart_barrier_refusal(&SelectionResponse {
worker: None,
reason: SelectionReason::AllWorkersBusy,
build_id: None,
diagnostics: None,
}));
}