Skip to main content

rch_common/
remote_compilation.rs

1//! Remote Compilation Verification via SSH.
2//!
3//! This module provides infrastructure for verifying that remote compilation
4//! actually works correctly by:
5//! 1. Applying a test change to make the binary unique
6//! 2. Building locally for a reference hash
7//! 3. Syncing source to worker via rsync
8//! 4. Building on the remote worker
9//! 5. Syncing artifacts back
10//! 6. Comparing binary hashes to verify correctness
11
12use crate::binary_hash::{
13    BinaryHashResult, CanaryVerdict, binary_contains_marker, classify_canary, compute_binary_hash,
14};
15use crate::gc_roots::{remote_cargo_home_base_prelude, remote_cargo_home_expr};
16use crate::mock::{self, MockConfig, MockRsync, MockRsyncConfig, MockSshClient};
17use crate::ssh::{SshClient, SshOptions};
18use crate::test_change::{TestChangeGuard, TestCodeChange};
19use crate::types::WorkerConfig;
20use anyhow::{Context, Result, bail};
21use serde::{Deserialize, Serialize};
22use shell_escape::escape;
23use std::borrow::Cow;
24use std::path::{Path, PathBuf};
25use std::process::Stdio;
26use std::time::{Duration, Instant};
27use tokio::process::Command;
28use tracing::{debug, info, warn};
29use uuid::Uuid;
30
31/// Result of a remote compilation verification test.
32#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct VerificationResult {
34    /// Whether the verification succeeded (hashes match).
35    pub success: bool,
36    /// Hash result from local build.
37    pub local_hash: BinaryHashResult,
38    /// Hash result from remote build.
39    pub remote_hash: BinaryHashResult,
40    /// Time spent on local compilation in milliseconds.
41    pub local_build_ms: u64,
42    /// Time spent on rsync upload in milliseconds.
43    pub rsync_up_ms: u64,
44    /// Time spent on remote compilation in milliseconds.
45    pub compilation_ms: u64,
46    /// Time spent on rsync download in milliseconds.
47    pub rsync_down_ms: u64,
48    /// Total time for the entire verification in milliseconds.
49    pub total_ms: u64,
50    /// Error message if verification failed.
51    pub error: Option<String>,
52    /// The test marker ID that was embedded in the binary.
53    pub test_marker: String,
54    /// The canary verdict token (bd-784xt): `reproduced` (byte-equivalent),
55    /// `toolchain_drift` (bytes differ but the worker is healthy — advisory PASS),
56    /// `miscompile` (same-toolchain byte mismatch — FAIL), or `missing_marker`
57    /// (the build is corrupt / not ours — FAIL). [`Self::success`] is true for the
58    /// first two. See [`crate::binary_hash::CanaryVerdict`].
59    pub verdict: String,
60}
61
62/// Configuration for a remote compilation test.
63#[derive(Debug, Clone)]
64pub struct RemoteCompilationTest {
65    /// Worker to test compilation on.
66    pub worker: WorkerConfig,
67    /// Path to the test project (must be a Rust project with src/main.rs or src/lib.rs).
68    pub test_project: PathBuf,
69    /// Timeout for the entire test operation.
70    pub timeout: Duration,
71    /// SSH options for connections.
72    pub ssh_options: SshOptions,
73    /// Whether to use release mode for builds.
74    pub release_mode: bool,
75    /// Binary name to verify (defaults to project directory name).
76    pub binary_name: Option<String>,
77    /// Remote base directory for builds.
78    pub remote_base: String,
79    /// Unique suffix appended to the remote project directory for this test run.
80    pub remote_path_suffix: String,
81}
82
83/// Resolve the `cargo` executable for the LOCAL reference build.
84///
85/// The daemon runs with a minimal, systemd-style `PATH` (e.g.
86/// `/usr/local/bin:/usr/bin:...`) that does NOT include `~/.cargo/bin`, so a bare
87/// `Command::new("cargo")` fails with ENOENT and silently wedges the self-test
88/// canary. Resolve robustly: honor an explicit `CARGO` override, then the rustup
89/// default at `$HOME/.cargo/bin/cargo`, and only fall back to a bare `cargo`
90/// (PATH lookup) when neither is present.
91fn resolve_cargo_binary() -> std::ffi::OsString {
92    resolve_cargo_binary_from(std::env::var_os("CARGO"), std::env::var_os("HOME"))
93}
94
95/// Pure core of [`resolve_cargo_binary`]: pick the cargo executable from an
96/// explicit `CARGO` override and a `HOME` directory. Split out so the resolution
97/// order is unit-testable without mutating process-global env vars.
98fn resolve_cargo_binary_from(
99    cargo_override: Option<std::ffi::OsString>,
100    home: Option<std::ffi::OsString>,
101) -> std::ffi::OsString {
102    if let Some(explicit) = cargo_override
103        && !explicit.is_empty()
104    {
105        return explicit;
106    }
107    if let Some(home) = home {
108        let candidate = PathBuf::from(home).join(".cargo").join("bin").join("cargo");
109        if candidate.is_file() {
110            return candidate.into_os_string();
111        }
112    }
113    std::ffi::OsString::from("cargo")
114}
115
116/// Read the Cargo package name from `<project>/Cargo.toml` (the `[package] name`
117/// field). Cargo names the built binary after the package (not the directory),
118/// so the verification hash step must resolve the artifact by package name.
119/// Returns `None` if the manifest is absent/unparseable or has no package name.
120fn cargo_package_name(project: &Path) -> Option<String> {
121    let manifest = std::fs::read_to_string(project.join("Cargo.toml")).ok()?;
122    let table = toml::from_str::<toml::Table>(&manifest).ok()?;
123    table
124        .get("package")
125        .and_then(toml::Value::as_table)
126        .and_then(|pkg| pkg.get("name"))
127        .and_then(toml::Value::as_str)
128        .map(ToString::to_string)
129}
130
131fn use_mock_transport(worker: &WorkerConfig) -> bool {
132    mock::is_mock_enabled() || mock::is_mock_worker(worker)
133}
134
135fn sanitize_remote_path_component(component: &str, fallback: &str) -> String {
136    let sanitized = component
137        .chars()
138        .map(|ch| {
139            if ch.is_ascii_alphanumeric() || matches!(ch, '-' | '_' | '.') {
140                ch
141            } else {
142                '-'
143            }
144        })
145        .collect::<String>();
146    let trimmed = sanitized.trim_matches('-');
147    if trimmed.is_empty() {
148        fallback.to_string()
149    } else {
150        trimmed.to_string()
151    }
152}
153
154fn sanitize_remote_path_suffix(suffix: &str) -> String {
155    sanitize_remote_path_component(suffix, "run")
156}
157
158fn join_remote_path(base: &str, child: &str) -> String {
159    let base = base.trim_end_matches('/');
160    if base.is_empty() {
161        format!("/{child}")
162    } else {
163        format!("{base}/{child}")
164    }
165}
166
167impl Default for RemoteCompilationTest {
168    fn default() -> Self {
169        Self {
170            worker: WorkerConfig::default(),
171            test_project: PathBuf::new(),
172            timeout: Duration::from_secs(300),
173            ssh_options: SshOptions::default(),
174            release_mode: true,
175            binary_name: None,
176            remote_base: "/tmp/rch_self_test".to_string(),
177            remote_path_suffix: format!("run-{}", Uuid::new_v4()),
178        }
179    }
180}
181
182impl RemoteCompilationTest {
183    /// Create a new remote compilation test.
184    pub fn new(worker: WorkerConfig, test_project: PathBuf) -> Self {
185        Self {
186            worker,
187            test_project,
188            ..Default::default()
189        }
190    }
191
192    /// Set the timeout for the test.
193    pub fn with_timeout(mut self, timeout: Duration) -> Self {
194        self.timeout = timeout;
195        self
196    }
197
198    /// Set SSH options.
199    pub fn with_ssh_options(mut self, options: SshOptions) -> Self {
200        self.ssh_options = options;
201        self
202    }
203
204    /// Set release mode (default: true).
205    pub fn with_release_mode(mut self, release: bool) -> Self {
206        self.release_mode = release;
207        self
208    }
209
210    /// Set the binary name to verify.
211    pub fn with_binary_name(mut self, name: impl Into<String>) -> Self {
212        self.binary_name = Some(name.into());
213        self
214    }
215
216    /// Set the remote project path suffix.
217    pub fn with_remote_path_suffix(mut self, suffix: impl AsRef<str>) -> Self {
218        self.remote_path_suffix = sanitize_remote_path_suffix(suffix.as_ref());
219        self
220    }
221
222    /// Run the remote compilation verification test.
223    ///
224    /// This performs the full pipeline:
225    /// 1. Apply test change to make binary unique
226    /// 2. Build locally for reference hash
227    /// 3. rsync source to worker
228    /// 4. Build on worker
229    /// 5. rsync artifacts back
230    /// 6. Compare hashes
231    pub async fn run(&self) -> Result<VerificationResult> {
232        let start = Instant::now();
233
234        info!(
235            "Starting remote compilation verification for {:?} on worker {}",
236            self.test_project, self.worker.id
237        );
238
239        // 1. Apply test change to make binary unique
240        let change = TestCodeChange::for_main_rs(&self.test_project)
241            .or_else(|_| TestCodeChange::for_lib_rs(&self.test_project))
242            .context("Failed to create test change - no src/main.rs or src/lib.rs found")?;
243        let _guard = TestChangeGuard::new(change.clone())?;
244        let test_marker = change.change_id.clone();
245
246        info!("Applied test marker: {}", test_marker);
247
248        // 2. Build locally first
249        info!("Building locally for reference hash");
250        let local_build_start = Instant::now();
251        self.build_local().await.context("Local build failed")?;
252        let local_build_ms = local_build_start.elapsed().as_millis() as u64;
253        info!("Local build complete in {}ms", local_build_ms);
254
255        let local_binary_path = self.local_binary_path();
256        let local_hash = compute_binary_hash(&local_binary_path)
257            .with_context(|| format!("Failed to hash local binary: {:?}", local_binary_path))?;
258        info!("Local hash: {}", &local_hash.code_hash[..16]);
259
260        // 3. rsync up to worker
261        info!("Syncing source to worker {}", self.worker.id);
262        let rsync_up_start = Instant::now();
263        self.rsync_to_worker()
264            .await
265            .context("rsync to worker failed")?;
266        let rsync_up_ms = rsync_up_start.elapsed().as_millis() as u64;
267        info!("rsync upload complete in {}ms", rsync_up_ms);
268
269        // 4. Build on worker
270        info!("Building on remote worker");
271        let compile_start = Instant::now();
272        self.build_remote().await.context("Remote build failed")?;
273        let compilation_ms = compile_start.elapsed().as_millis() as u64;
274        info!("Remote build complete in {}ms", compilation_ms);
275
276        // 5. rsync back
277        info!("Syncing artifacts from worker");
278        let rsync_down_start = Instant::now();
279        self.rsync_from_worker()
280            .await
281            .context("rsync from worker failed")?;
282        let rsync_down_ms = rsync_down_start.elapsed().as_millis() as u64;
283        info!("rsync download complete in {}ms", rsync_down_ms);
284
285        // 6. Compute remote binary hash
286        let remote_binary_path = self.remote_binary_path();
287        let remote_hash = compute_binary_hash(&remote_binary_path)
288            .with_context(|| format!("Failed to hash remote binary: {:?}", remote_binary_path))?;
289        info!("Remote hash: {}", &remote_hash.code_hash[..16]);
290
291        // 7. Classify the canary verdict (bd-784xt). Byte-for-byte identity to the
292        // orchestrator's reference build is ADVISORY: a healthy worker whose rustc
293        // nightly differs legitimately produces different codegen, so a strict
294        // equality check false-failed healthy workers on a heterogeneous fleet.
295        // The injected marker proves the binary is the build we asked for (catches
296        // corruption / a stale artifact); a byte mismatch on a *confirmed-
297        // identical* toolchain is a genuine miscompile. Toolchain parity is not
298        // probed in this path, so a mismatch with the marker present resolves to
299        // advisory drift (PASS) rather than a false failure.
300        let marker_present =
301            binary_contains_marker(&remote_binary_path, &test_marker).unwrap_or(true);
302        let verdict = classify_canary(&local_hash, &remote_hash, marker_present, None);
303        let success = verdict.passed();
304        let total_ms = start.elapsed().as_millis() as u64;
305
306        let local_short = &local_hash.code_hash[..local_hash.code_hash.len().min(16)];
307        let remote_short = &remote_hash.code_hash[..remote_hash.code_hash.len().min(16)];
308        let error = if success {
309            if verdict.is_advisory() {
310                info!(
311                    "Verification PASSED (advisory {}): worker healthy; codegen differs from the \
312                     orchestrator toolchain (local={local_short} remote={remote_short})",
313                    verdict.reason_code().unwrap_or("RCH-I018"),
314                );
315            } else {
316                info!("Verification PASSED: remote build reproduced the reference binary");
317            }
318            None
319        } else {
320            let code = verdict.reason_code().unwrap_or("RCH-E203");
321            let msg = if verdict == CanaryVerdict::MissingMarker {
322                format!(
323                    "[{code}] canary missing test marker '{test_marker}' — remote build is corrupt or not ours"
324                )
325            } else {
326                format!(
327                    "[{code}] canary {} — local={local_short} remote={remote_short}",
328                    verdict.as_str(),
329                )
330            };
331            warn!("Verification FAILED: {}", msg);
332            Some(msg)
333        };
334
335        Ok(VerificationResult {
336            success,
337            local_hash,
338            remote_hash,
339            local_build_ms,
340            rsync_up_ms,
341            compilation_ms,
342            rsync_down_ms,
343            total_ms,
344            error,
345            test_marker,
346            verdict: verdict.as_str().to_string(),
347        })
348    }
349
350    /// Get the path to the local binary after build.
351    fn local_binary_path(&self) -> PathBuf {
352        let binary_name = self.get_binary_name();
353        let profile = if self.release_mode {
354            "release"
355        } else {
356            "debug"
357        };
358        self.test_project
359            .join("target")
360            .join(profile)
361            .join(binary_name)
362    }
363
364    /// Get the path where remote binary will be stored locally after retrieval.
365    fn remote_binary_path(&self) -> PathBuf {
366        let binary_name = self.get_binary_name();
367        let profile = if self.release_mode {
368            "release"
369        } else {
370            "debug"
371        };
372        self.test_project
373            .join("target")
374            .join(format!("{}_remote", profile))
375            .join(binary_name)
376    }
377
378    /// Get the binary name to verify.
379    ///
380    /// Prefers an explicit override, then the actual Cargo package name from the
381    /// project's `Cargo.toml` (cargo names the binary after the PACKAGE, with `-`
382    /// normalized to `_`), and only falls back to the directory name. The
383    /// directory-name heuristic is wrong whenever the dir and package names
384    /// differ — e.g. the self-test fixture lives in a `project/` dir but its
385    /// package is `rch_self_test`, so the produced binary is `rch_self_test`, not
386    /// `project`; reading Cargo.toml makes the hash step find the real artifact.
387    fn get_binary_name(&self) -> String {
388        if let Some(name) = self.binary_name.clone() {
389            return name;
390        }
391        // An empty project path is not a real project root: `cargo_package_name`
392        // would join "Cargo.toml" against an empty path and read `<cwd>/Cargo.toml`,
393        // misattributing the binary to whatever package the process happens to run
394        // in. Only consult the manifest for a real path; otherwise fall through to
395        // the file-name / "unknown" fallback.
396        if !self.test_project.as_os_str().is_empty()
397            && let Some(pkg) = cargo_package_name(&self.test_project)
398        {
399            return pkg.replace('-', "_");
400        }
401        self.test_project
402            .file_name()
403            .and_then(|n| n.to_str())
404            .unwrap_or("unknown")
405            .replace('-', "_")
406    }
407
408    /// Get the remote project path on the worker.
409    fn remote_project_path(&self) -> String {
410        let project_name = self
411            .test_project
412            .file_name()
413            .and_then(|n| n.to_str())
414            .map(|n| sanitize_remote_path_component(n, "self_test"))
415            .unwrap_or_else(|| "self_test".to_string());
416        let remote_path_suffix = sanitize_remote_path_suffix(&self.remote_path_suffix);
417        let project_dir = format!("{project_name}-{remote_path_suffix}");
418        join_remote_path(&self.remote_base, &project_dir)
419    }
420
421    /// Get the remote artifact source for rsync retrieval.
422    fn remote_artifact_source(&self, profile: &str) -> String {
423        let remote_path = self.remote_project_path();
424        let remote_target = format!("{}/target/{profile}/", remote_path.trim_end_matches('/'));
425        format!(
426            "{}@{}:{}",
427            self.worker.user,
428            self.worker.host,
429            escape(Cow::from(remote_target))
430        )
431    }
432
433    /// Build the isolated remote CARGO_HOME shell *expression* for a single
434    /// build attempt. The temp base is resolved on the worker at execution time
435    /// (see [`remote_cargo_home_base_prelude`]), honoring `$TMPDIR` / `/data/tmp`
436    /// rather than hardcoding `/tmp`, so large caches stay off a tmpfs `/tmp`.
437    /// The returned string references the prelude's shell variable and must be
438    /// used in a `$VAR`-expanding (double-quoted) context.
439    fn remote_cargo_home_path(&self, session_id: &str) -> String {
440        let worker_id = sanitize_remote_path_component(self.worker.id.as_str(), "worker");
441        let session_id = sanitize_remote_path_component(session_id, "session");
442        remote_cargo_home_expr(&format!("{worker_id}-{session_id}"))
443    }
444
445    /// Build the project locally.
446    async fn build_local(&self) -> Result<()> {
447        let mut cmd = Command::new(resolve_cargo_binary());
448        cmd.arg("build");
449
450        if self.release_mode {
451            cmd.arg("--release");
452        }
453
454        cmd.current_dir(&self.test_project)
455            .env("CARGO_INCREMENTAL", "0") // Disable incremental for reproducibility
456            // Pin the output to the test project's own `target/` so the produced
457            // binary lands exactly where `local_binary_path()` reads it. The
458            // daemon inherits a process-wide `CARGO_TARGET_DIR` (it redirects its
459            // OWN builds to a shared cache); left unoverridden it sends the canary
460            // binary there instead, and the subsequent local-hash step fails.
461            .env("CARGO_TARGET_DIR", self.test_project.join("target"))
462            .stdout(Stdio::piped())
463            .stderr(Stdio::piped());
464
465        debug!(
466            "Running local build: cargo build {}",
467            if self.release_mode { "--release" } else { "" }
468        );
469
470        let output = cmd
471            .output()
472            .await
473            .context("Failed to execute cargo build")?;
474
475        if !output.status.success() {
476            let stderr = String::from_utf8_lossy(&output.stderr);
477            bail!("Local build failed: {}", stderr);
478        }
479
480        Ok(())
481    }
482
483    /// Sync source files to the worker via rsync.
484    async fn rsync_to_worker(&self) -> Result<()> {
485        let remote_path = self.remote_project_path();
486        let escaped_remote_path = escape(Cow::from(&remote_path));
487
488        if use_mock_transport(&self.worker) {
489            let mut client = MockSshClient::new(self.worker.clone(), MockConfig::from_env());
490            client.connect().await?;
491            let mkdir_cmd = format!("mkdir -p -- {}", escaped_remote_path);
492            let mkdir_result = client.execute(&mkdir_cmd).await?;
493            client.disconnect().await?;
494
495            if !mkdir_result.success() {
496                bail!("Failed to create remote directory: {}", mkdir_result.stderr);
497            }
498
499            let rsync = MockRsync::new(MockRsyncConfig::from_env());
500            let destination = format!(
501                "{}@{}:{}",
502                self.worker.user, self.worker.host, escaped_remote_path
503            );
504            rsync
505                .sync_to_remote(&self.test_project.display().to_string(), &destination, &[])
506                .await?;
507            return Ok(());
508        }
509
510        // First ensure remote directory exists
511        let mut client = SshClient::new(self.worker.clone(), self.ssh_options.clone());
512        client.connect().await?;
513        let mkdir_cmd = format!("mkdir -p -- {}", escaped_remote_path);
514        // Always disconnect, even on error/timeout — see the build path below: a
515        // dropped RemoteChild is not reaped while the ControlMaster is alive, so
516        // `?` before disconnect() would leak the remote process.
517        let mkdir_result = client.execute(&mkdir_cmd).await;
518        if let Err(e) = client.disconnect().await {
519            warn!(
520                "Failed to disconnect from {} after remote mkdir: {}",
521                self.worker.id, e
522            );
523        }
524        let mkdir_result = mkdir_result?;
525
526        if !mkdir_result.success() {
527            bail!("Failed to create remote directory: {}", mkdir_result.stderr);
528        }
529
530        let destination = format!(
531            "{}@{}:{}",
532            self.worker.user, self.worker.host, escaped_remote_path
533        );
534
535        let identity_file = shellexpand::tilde(&self.worker.identity_file);
536        let escaped_identity = escape(Cow::from(identity_file.as_ref()));
537
538        let mut cmd = Command::new("rsync");
539        cmd.arg("-az")
540            .arg("--no-owner")
541            .arg("--no-group")
542            .arg("--delete")
543            .arg("--exclude")
544            .arg("target/")
545            .arg("--exclude")
546            .arg(".git/")
547            .arg("-e")
548            .arg(format!(
549                "ssh -i {} -o StrictHostKeyChecking=accept-new -o BatchMode=yes",
550                escaped_identity
551            ))
552            .arg(format!("{}/", self.test_project.display()))
553            .arg(&destination)
554            .stdout(Stdio::piped())
555            .stderr(Stdio::piped());
556
557        debug!(
558            "Running rsync to worker: {:?}",
559            cmd.as_std().get_args().collect::<Vec<_>>()
560        );
561
562        let output = cmd.output().await.context("Failed to execute rsync")?;
563
564        if !output.status.success() {
565            let stderr = String::from_utf8_lossy(&output.stderr);
566            bail!("rsync to worker failed: {}", stderr);
567        }
568
569        Ok(())
570    }
571
572    /// Build the project on the remote worker.
573    async fn build_remote(&self) -> Result<()> {
574        let remote_path = self.remote_project_path();
575        let escaped_remote_path = escape(Cow::from(&remote_path));
576
577        // Generate a unique cargo home per build attempt to prevent cache lock contention.
578        let session_id = Uuid::new_v4().simple().to_string();
579        // `cargo_home` is a shell *expression* (`${RCH_CH_BASE}/rch-cargo-home-…`)
580        // resolved on the worker by the prelude below — double-quote it (not
581        // shell-escape) so `$RCH_CH_BASE` expands; its suffix is charset-safe.
582        let cargo_home = self.remote_cargo_home_path(&session_id);
583        let quoted_cargo_home = format!("\"{cargo_home}\"");
584        let cargo_target_dir = format!("{}/target", remote_path);
585        let escaped_cargo_target_dir = escape(Cow::from(&cargo_target_dir));
586        let cargo_home_base_prelude = remote_cargo_home_base_prelude();
587
588        let cargo_args = if self.release_mode {
589            "cargo build --release"
590        } else {
591            "cargo build"
592        };
593        // Preserve the cargo status after removing the isolated cache.
594        let build_cmd = format!(
595            "{cargo_home_base_prelude}; mkdir -p -- {} {} && cd {} && CARGO_HOME={} CARGO_TARGET_DIR={} CARGO_INCREMENTAL=0 {}; status=$?; rm -rf -- {}; exit $status",
596            quoted_cargo_home,
597            escaped_cargo_target_dir,
598            escaped_remote_path,
599            quoted_cargo_home,
600            escaped_cargo_target_dir,
601            cargo_args,
602            quoted_cargo_home
603        );
604
605        debug!(
606            "Running remote build with isolated cargo cache: {}",
607            build_cmd
608        );
609
610        if use_mock_transport(&self.worker) {
611            let mut client = MockSshClient::new(self.worker.clone(), MockConfig::from_env());
612            client.connect().await?;
613            let result = client.execute(&build_cmd).await?;
614            client.disconnect().await?;
615
616            if !result.success() {
617                bail!(
618                    "Remote build failed (exit {}): {}",
619                    result.exit_code,
620                    result.stderr
621                );
622            }
623
624            return Ok(());
625        }
626
627        let mut client = SshClient::new(self.worker.clone(), self.ssh_options.clone());
628        client.connect().await?;
629        // Capture the result and ALWAYS disconnect, even on error/timeout. On an
630        // execute() timeout the inner RemoteChild is dropped, but dropping it does
631        // NOT kill the remote process while the SSH ControlMaster is alive — only
632        // closing the session (disconnect) reaps it. Propagating `?` straight from
633        // execute() would skip disconnect() and leak a timed-out `cargo build`
634        // that keeps burning worker CPU and holding cache/target locks until sshd
635        // reaps it. The disconnect error is best-effort and must not mask the
636        // primary build error.
637        let result = client.execute(&build_cmd).await;
638        if let Err(e) = client.disconnect().await {
639            warn!(
640                "Failed to disconnect from {} after remote build: {}",
641                self.worker.id, e
642            );
643        }
644        let result = result?;
645
646        if !result.success() {
647            bail!(
648                "Remote build failed (exit {}): {}",
649                result.exit_code,
650                result.stderr
651            );
652        }
653
654        Ok(())
655    }
656
657    /// Sync build artifacts from the worker via rsync.
658    async fn rsync_from_worker(&self) -> Result<()> {
659        let profile = if self.release_mode {
660            "release"
661        } else {
662            "debug"
663        };
664
665        // Create local destination directory for remote artifacts
666        let local_dest = self
667            .test_project
668            .join("target")
669            .join(format!("{}_remote", profile));
670        std::fs::create_dir_all(&local_dest)
671            .with_context(|| format!("Failed to create directory: {:?}", local_dest))?;
672
673        if use_mock_transport(&self.worker) {
674            let rsync = MockRsync::new(MockRsyncConfig::from_env());
675            let remote_target = self.remote_artifact_source(profile);
676            rsync
677                .retrieve_artifacts(&remote_target, &local_dest.display().to_string(), &[])
678                .await?;
679            return Ok(());
680        }
681
682        let remote_target = self.remote_artifact_source(profile);
683
684        let identity_file = shellexpand::tilde(&self.worker.identity_file);
685        let escaped_identity = escape(Cow::from(identity_file.as_ref()));
686
687        let mut cmd = Command::new("rsync");
688        cmd.arg("-az")
689            .arg("--no-owner")
690            .arg("--no-group")
691            .arg("-e")
692            .arg(format!(
693                "ssh -i {} -o StrictHostKeyChecking=accept-new -o BatchMode=yes",
694                escaped_identity
695            ))
696            .arg(&remote_target)
697            .arg(format!("{}/", local_dest.display()))
698            .stdout(Stdio::piped())
699            .stderr(Stdio::piped());
700
701        debug!(
702            "Running rsync from worker: {:?}",
703            cmd.as_std().get_args().collect::<Vec<_>>()
704        );
705
706        let output = cmd.output().await.context("Failed to retrieve artifacts")?;
707
708        if !output.status.success() {
709            let stderr = String::from_utf8_lossy(&output.stderr);
710            bail!("rsync from worker failed: {}", stderr);
711        }
712
713        Ok(())
714    }
715}
716
717#[cfg(test)]
718mod tests {
719    use super::*;
720    use crate::gc_roots::RCH_CARGO_HOME_BASE_VAR;
721    use crate::mock::{
722        MockConfig, MockRsyncConfig, clear_global_invocations, clear_mock_overrides,
723        global_rsync_invocations_snapshot, global_ssh_invocations_snapshot, is_mock_enabled,
724        set_mock_enabled_override, set_mock_rsync_config_override, set_mock_ssh_config_override,
725    };
726    use crate::test_guard;
727    use crate::testing::{TestLogger, TestPhase};
728    use crate::types::WorkerId;
729
730    #[test]
731    fn resolve_cargo_binary_prefers_override_then_rustup_then_path() {
732        use std::ffi::OsString;
733
734        // An explicit CARGO override always wins.
735        assert_eq!(
736            resolve_cargo_binary_from(Some(OsString::from("/opt/cargo")), None),
737            OsString::from("/opt/cargo")
738        );
739
740        // An empty override is ignored (falls through to HOME / PATH).
741        assert_eq!(
742            resolve_cargo_binary_from(Some(OsString::new()), None),
743            OsString::from("cargo")
744        );
745
746        // With no override and no rustup cargo present under HOME, fall back to a
747        // bare `cargo` (PATH lookup).
748        let empty_home = tempfile::tempdir().unwrap();
749        assert_eq!(
750            resolve_cargo_binary_from(None, Some(empty_home.path().as_os_str().to_os_string())),
751            OsString::from("cargo")
752        );
753
754        // When `$HOME/.cargo/bin/cargo` exists (the rustup default the daemon's
755        // minimal PATH omits), resolve to it absolutely.
756        let home = tempfile::tempdir().unwrap();
757        let cargo_bin = home.path().join(".cargo").join("bin");
758        std::fs::create_dir_all(&cargo_bin).unwrap();
759        let cargo_path = cargo_bin.join("cargo");
760        std::fs::write(&cargo_path, b"#!/bin/sh\n").unwrap();
761        assert_eq!(
762            resolve_cargo_binary_from(None, Some(home.path().as_os_str().to_os_string())),
763            cargo_path.into_os_string()
764        );
765    }
766
767    #[test]
768    fn cargo_package_name_reads_package_not_directory() {
769        // The self-test fixture's real shape: a `project/` dir whose package is
770        // `rch_self_test` — cargo names the binary after the package, so reading
771        // the directory name (`project`) would look for a non-existent artifact.
772        let dir = tempfile::tempdir().unwrap();
773        let proj = dir.path().join("project");
774        std::fs::create_dir_all(&proj).unwrap();
775        std::fs::write(
776            proj.join("Cargo.toml"),
777            "[package]\nname = \"rch_self_test\"\nversion = \"0.1.0\"\nedition = \"2024\"\n\n[dependencies]\n",
778        )
779        .unwrap();
780        assert_eq!(cargo_package_name(&proj).as_deref(), Some("rch_self_test"));
781
782        // No manifest -> None (caller falls back to the directory name).
783        let empty = dir.path().join("empty");
784        std::fs::create_dir_all(&empty).unwrap();
785        assert_eq!(cargo_package_name(&empty), None);
786    }
787
788    fn init_test_logging() {
789        let _ = tracing_subscriber::fmt()
790            .with_test_writer()
791            .with_max_level(tracing::Level::DEBUG)
792            .try_init();
793    }
794
795    // ========================
796    // VerificationResult tests
797    // ========================
798
799    #[test]
800    fn verification_result_serializes_roundtrip() {
801        let _guard = test_guard!();
802        let logger = TestLogger::for_test("verification_result_serializes_roundtrip");
803
804        let result = VerificationResult {
805            success: true,
806            local_hash: BinaryHashResult {
807                full_hash: "abc123".to_string(),
808                code_hash: "def456".to_string(),
809                text_section_size: 12345,
810                is_debug: false,
811            },
812            remote_hash: BinaryHashResult {
813                full_hash: "abc123".to_string(),
814                code_hash: "def456".to_string(),
815                text_section_size: 12345,
816                is_debug: false,
817            },
818            local_build_ms: 1200,
819            rsync_up_ms: 100,
820            compilation_ms: 5000,
821            rsync_down_ms: 200,
822            total_ms: 5300,
823            error: None,
824            test_marker: "RCH_TEST_12345".to_string(),
825            verdict: "reproduced".to_string(),
826        };
827
828        logger.log(TestPhase::Execute, "Serializing VerificationResult to JSON");
829        let json = serde_json::to_string(&result).unwrap();
830        logger.log_with_data(
831            TestPhase::Execute,
832            "Serialized result",
833            serde_json::json!({ "json_len": json.len() }),
834        );
835
836        let restored: VerificationResult = serde_json::from_str(&json).unwrap();
837
838        logger.log(TestPhase::Verify, "Checking restored fields");
839        assert!(restored.success);
840        assert_eq!(restored.rsync_up_ms, 100);
841        assert_eq!(restored.compilation_ms, 5000);
842        assert_eq!(restored.test_marker, "RCH_TEST_12345");
843
844        logger.pass();
845    }
846
847    #[test]
848    fn verification_result_with_error() {
849        let _guard = test_guard!();
850        let logger = TestLogger::for_test("verification_result_with_error");
851
852        let result = VerificationResult {
853            success: false,
854            local_hash: BinaryHashResult {
855                full_hash: "abc".to_string(),
856                code_hash: "local_hash".to_string(),
857                text_section_size: 1000,
858                is_debug: false,
859            },
860            remote_hash: BinaryHashResult {
861                full_hash: "xyz".to_string(),
862                code_hash: "remote_hash".to_string(),
863                text_section_size: 1000,
864                is_debug: false,
865            },
866            local_build_ms: 900,
867            rsync_up_ms: 50,
868            compilation_ms: 3000,
869            rsync_down_ms: 50,
870            total_ms: 3100,
871            error: Some("Binary hash mismatch".to_string()),
872            test_marker: "RCH_TEST_99999".to_string(),
873            verdict: "reproduced".to_string(),
874        };
875
876        logger.log(TestPhase::Verify, "Checking failed verification result");
877        assert!(!result.success);
878        assert!(result.error.is_some());
879        assert_eq!(result.error.as_ref().unwrap(), "Binary hash mismatch");
880
881        logger.log_with_data(
882            TestPhase::Verify,
883            "Error captured correctly",
884            serde_json::json!({ "error": result.error }),
885        );
886        logger.pass();
887    }
888
889    #[test]
890    fn verification_result_serializes_with_all_fields() {
891        let _guard = test_guard!();
892        let logger = TestLogger::for_test("verification_result_serializes_with_all_fields");
893
894        let result = VerificationResult {
895            success: false,
896            local_hash: BinaryHashResult {
897                full_hash: "full_local".to_string(),
898                code_hash: "code_local".to_string(),
899                text_section_size: 5000,
900                is_debug: true,
901            },
902            remote_hash: BinaryHashResult {
903                full_hash: "full_remote".to_string(),
904                code_hash: "code_remote".to_string(),
905                text_section_size: 5001,
906                is_debug: true,
907            },
908            local_build_ms: 2500,
909            rsync_up_ms: 300,
910            compilation_ms: 15000,
911            rsync_down_ms: 400,
912            total_ms: 18200,
913            error: Some("Size mismatch: 5000 vs 5001".to_string()),
914            test_marker: "RCH_FULL_TEST".to_string(),
915            verdict: "reproduced".to_string(),
916        };
917
918        logger.log(TestPhase::Execute, "Serializing result with all fields");
919        let json = serde_json::to_string_pretty(&result).unwrap();
920
921        // Verify all fields are present in serialization
922        assert!(json.contains("\"success\": false"));
923        assert!(json.contains("\"local_build_ms\": 2500"));
924        assert!(json.contains("\"rsync_up_ms\": 300"));
925        assert!(json.contains("\"compilation_ms\": 15000"));
926        assert!(json.contains("\"rsync_down_ms\": 400"));
927        assert!(json.contains("\"total_ms\": 18200"));
928        assert!(json.contains("RCH_FULL_TEST"));
929        assert!(json.contains("Size mismatch"));
930
931        logger.log_with_data(
932            TestPhase::Verify,
933            "All fields serialized",
934            serde_json::json!({ "fields_checked": 8 }),
935        );
936        logger.pass();
937    }
938
939    // ========================
940    // RemoteCompilationTest tests
941    // ========================
942
943    #[test]
944    fn remote_compilation_test_default_values() {
945        let _guard = test_guard!();
946        let logger = TestLogger::for_test("remote_compilation_test_default_values");
947
948        let test = RemoteCompilationTest::default();
949
950        logger.log(TestPhase::Verify, "Checking default values");
951        assert_eq!(test.timeout, Duration::from_secs(300));
952        assert!(test.release_mode);
953        assert!(test.binary_name.is_none());
954        assert_eq!(test.remote_base, "/tmp/rch_self_test");
955        assert!(test.remote_path_suffix.starts_with("run-"));
956
957        logger.log_with_data(
958            TestPhase::Verify,
959            "Default values correct",
960            serde_json::json!({
961                "timeout_secs": 300,
962                "release_mode": true,
963                "remote_base": "/tmp/rch_self_test",
964                "remote_path_suffix": &test.remote_path_suffix
965            }),
966        );
967        logger.pass();
968    }
969
970    #[test]
971    fn remote_compilation_test_builder_pattern() {
972        let _guard = test_guard!();
973        let logger = TestLogger::for_test("remote_compilation_test_builder_pattern");
974
975        let worker = WorkerConfig {
976            id: WorkerId::new("test-worker"),
977            host: "worker.example.com".to_string(),
978            user: "builder".to_string(),
979            identity_file: "~/.ssh/id_rsa".to_string(),
980            total_slots: 4,
981            ..Default::default()
982        };
983
984        logger.log(TestPhase::Execute, "Building test with custom options");
985        let test = RemoteCompilationTest::new(worker.clone(), PathBuf::from("/tmp/project"))
986            .with_timeout(Duration::from_secs(120))
987            .with_release_mode(false)
988            .with_binary_name("my_binary");
989
990        logger.log(TestPhase::Verify, "Checking builder-set values");
991        assert_eq!(test.timeout, Duration::from_secs(120));
992        assert!(!test.release_mode);
993        assert_eq!(test.binary_name, Some("my_binary".to_string()));
994        assert_eq!(test.worker.id.as_str(), "test-worker");
995
996        logger.pass();
997    }
998
999    #[test]
1000    fn remote_compilation_test_with_ssh_options() {
1001        let _guard = test_guard!();
1002        let logger = TestLogger::for_test("remote_compilation_test_with_ssh_options");
1003
1004        let worker = WorkerConfig {
1005            id: WorkerId::new("w1"),
1006            host: "host".to_string(),
1007            user: "user".to_string(),
1008            identity_file: "~/.ssh/id_rsa".to_string(),
1009            total_slots: 1,
1010            ..Default::default()
1011        };
1012
1013        let ssh_opts = SshOptions {
1014            connect_timeout: Duration::from_secs(30),
1015            command_timeout: Duration::from_secs(600),
1016            ..Default::default()
1017        };
1018
1019        logger.log(TestPhase::Execute, "Creating test with custom SSH options");
1020        let test = RemoteCompilationTest::new(worker, PathBuf::from("/tmp/project"))
1021            .with_ssh_options(ssh_opts);
1022
1023        logger.log(TestPhase::Verify, "Checking SSH options");
1024        assert_eq!(test.ssh_options.connect_timeout, Duration::from_secs(30));
1025        assert_eq!(test.ssh_options.command_timeout, Duration::from_secs(600));
1026
1027        logger.pass();
1028    }
1029
1030    #[test]
1031    fn remote_compilation_test_binary_path_release() {
1032        let _guard = test_guard!();
1033        let logger = TestLogger::for_test("remote_compilation_test_binary_path_release");
1034
1035        let worker = WorkerConfig {
1036            id: WorkerId::new("w1"),
1037            host: "host".to_string(),
1038            user: "user".to_string(),
1039            identity_file: "~/.ssh/id_rsa".to_string(),
1040            total_slots: 1,
1041            ..Default::default()
1042        };
1043
1044        let test = RemoteCompilationTest::new(worker, PathBuf::from("/home/user/my-project"))
1045            .with_release_mode(true)
1046            .with_binary_name("my_binary");
1047
1048        let local_path = test.local_binary_path();
1049        let remote_path = test.remote_binary_path();
1050
1051        logger.log_with_data(
1052            TestPhase::Execute,
1053            "Computed binary paths",
1054            serde_json::json!({
1055                "local": local_path.to_string_lossy(),
1056                "remote": remote_path.to_string_lossy()
1057            }),
1058        );
1059
1060        assert!(
1061            local_path
1062                .to_string_lossy()
1063                .contains("target/release/my_binary")
1064        );
1065        assert!(
1066            remote_path
1067                .to_string_lossy()
1068                .contains("target/release_remote/my_binary")
1069        );
1070
1071        logger.pass();
1072    }
1073
1074    #[test]
1075    fn remote_compilation_test_binary_path_debug() {
1076        let _guard = test_guard!();
1077        let logger = TestLogger::for_test("remote_compilation_test_binary_path_debug");
1078
1079        let worker = WorkerConfig {
1080            id: WorkerId::new("w1"),
1081            host: "host".to_string(),
1082            user: "user".to_string(),
1083            identity_file: "~/.ssh/id_rsa".to_string(),
1084            total_slots: 1,
1085            ..Default::default()
1086        };
1087
1088        let test = RemoteCompilationTest::new(worker, PathBuf::from("/home/user/my-project"))
1089            .with_release_mode(false)
1090            .with_binary_name("my_binary");
1091
1092        let local_path = test.local_binary_path();
1093        let remote_path = test.remote_binary_path();
1094
1095        logger.log_with_data(
1096            TestPhase::Execute,
1097            "Computed debug binary paths",
1098            serde_json::json!({
1099                "local": local_path.to_string_lossy(),
1100                "remote": remote_path.to_string_lossy()
1101            }),
1102        );
1103
1104        assert!(
1105            local_path
1106                .to_string_lossy()
1107                .contains("target/debug/my_binary")
1108        );
1109        assert!(
1110            remote_path
1111                .to_string_lossy()
1112                .contains("target/debug_remote/my_binary")
1113        );
1114
1115        logger.pass();
1116    }
1117
1118    #[test]
1119    fn remote_compilation_test_infers_binary_name() {
1120        let _guard = test_guard!();
1121        let logger = TestLogger::for_test("remote_compilation_test_infers_binary_name");
1122
1123        let worker = WorkerConfig {
1124            id: WorkerId::new("w1"),
1125            host: "host".to_string(),
1126            user: "user".to_string(),
1127            identity_file: "~/.ssh/id_rsa".to_string(),
1128            total_slots: 1,
1129            ..Default::default()
1130        };
1131
1132        // Test with hyphenated project name (should convert to underscore)
1133        let test = RemoteCompilationTest::new(worker, PathBuf::from("/home/user/my-cool-project"));
1134
1135        let binary_name = test.get_binary_name();
1136        logger.log_with_data(
1137            TestPhase::Execute,
1138            "Inferred binary name",
1139            serde_json::json!({ "binary_name": &binary_name }),
1140        );
1141
1142        assert_eq!(binary_name, "my_cool_project");
1143
1144        logger.pass();
1145    }
1146
1147    #[test]
1148    fn remote_compilation_test_infers_binary_name_edge_cases() {
1149        let _guard = test_guard!();
1150        let logger = TestLogger::for_test("remote_compilation_test_infers_binary_name_edge_cases");
1151
1152        let worker = WorkerConfig {
1153            id: WorkerId::new("w1"),
1154            host: "host".to_string(),
1155            user: "user".to_string(),
1156            identity_file: "~/.ssh/id_rsa".to_string(),
1157            total_slots: 1,
1158            ..Default::default()
1159        };
1160
1161        // Test with multiple hyphens
1162        let test1 =
1163            RemoteCompilationTest::new(worker.clone(), PathBuf::from("/a/b/my-very-cool-project"));
1164        assert_eq!(test1.get_binary_name(), "my_very_cool_project");
1165
1166        // Test with underscores (should remain unchanged)
1167        let test2 =
1168            RemoteCompilationTest::new(worker.clone(), PathBuf::from("/a/b/my_project_name"));
1169        assert_eq!(test2.get_binary_name(), "my_project_name");
1170
1171        // Test with mixed
1172        let test3 = RemoteCompilationTest::new(worker.clone(), PathBuf::from("/a/b/my-project_v2"));
1173        assert_eq!(test3.get_binary_name(), "my_project_v2");
1174
1175        // Test with explicit binary name (should override)
1176        let test4 = RemoteCompilationTest::new(worker.clone(), PathBuf::from("/a/b/my-project"))
1177            .with_binary_name("custom");
1178        assert_eq!(test4.get_binary_name(), "custom");
1179
1180        logger.log_with_data(
1181            TestPhase::Verify,
1182            "All edge cases handled",
1183            serde_json::json!({ "cases_tested": 4 }),
1184        );
1185        logger.pass();
1186    }
1187
1188    #[test]
1189    fn remote_compilation_test_remote_project_path() {
1190        let _guard = test_guard!();
1191        let logger = TestLogger::for_test("remote_compilation_test_remote_project_path");
1192
1193        let worker = WorkerConfig {
1194            id: WorkerId::new("w1"),
1195            host: "host".to_string(),
1196            user: "user".to_string(),
1197            identity_file: "~/.ssh/id_rsa".to_string(),
1198            total_slots: 1,
1199            ..Default::default()
1200        };
1201
1202        let test = RemoteCompilationTest::new(worker, PathBuf::from("/home/user/test-project"));
1203
1204        let remote_path = test.remote_project_path();
1205        logger.log_with_data(
1206            TestPhase::Execute,
1207            "Computed remote path",
1208            serde_json::json!({ "remote_path": &remote_path }),
1209        );
1210
1211        assert!(
1212            remote_path.starts_with("/tmp/rch_self_test/test-project-run-"),
1213            "remote path should include a unique run suffix: {remote_path}"
1214        );
1215
1216        logger.pass();
1217    }
1218
1219    #[test]
1220    fn remote_compilation_test_custom_remote_path_suffix() {
1221        let _guard = test_guard!();
1222        let logger = TestLogger::for_test("remote_compilation_test_custom_remote_path_suffix");
1223
1224        let worker = WorkerConfig {
1225            id: WorkerId::new("w1"),
1226            host: "host".to_string(),
1227            user: "user".to_string(),
1228            identity_file: "~/.ssh/id_rsa".to_string(),
1229            total_slots: 1,
1230            ..Default::default()
1231        };
1232
1233        let test = RemoteCompilationTest::new(worker, PathBuf::from("/home/user/test-project"))
1234            .with_remote_path_suffix("run/42 attempt 1");
1235
1236        let remote_path = test.remote_project_path();
1237        logger.log_with_data(
1238            TestPhase::Execute,
1239            "Computed remote path with custom suffix",
1240            serde_json::json!({ "remote_path": &remote_path }),
1241        );
1242
1243        assert_eq!(
1244            remote_path,
1245            "/tmp/rch_self_test/test-project-run-42-attempt-1"
1246        );
1247
1248        logger.pass();
1249    }
1250
1251    #[test]
1252    fn remote_compilation_test_remote_path_sanitizes_public_suffix_field() {
1253        let _guard = test_guard!();
1254        let logger = TestLogger::for_test(
1255            "remote_compilation_test_remote_path_sanitizes_public_suffix_field",
1256        );
1257
1258        let worker = WorkerConfig {
1259            id: WorkerId::new("w1"),
1260            host: "host".to_string(),
1261            user: "user".to_string(),
1262            identity_file: "~/.ssh/id_rsa".to_string(),
1263            total_slots: 1,
1264            ..Default::default()
1265        };
1266
1267        let mut test = RemoteCompilationTest::new(worker, PathBuf::from("/home/user/test-project"));
1268        test.remote_path_suffix = "../unsafe path".to_string();
1269
1270        let remote_path = test.remote_project_path();
1271        logger.log_with_data(
1272            TestPhase::Execute,
1273            "Computed remote path with directly-mutated suffix",
1274            serde_json::json!({ "remote_path": &remote_path }),
1275        );
1276
1277        assert_eq!(
1278            remote_path,
1279            "/tmp/rch_self_test/test-project-..-unsafe-path"
1280        );
1281
1282        logger.pass();
1283    }
1284
1285    #[test]
1286    fn remote_compilation_test_instances_do_not_share_remote_paths() {
1287        let _guard = test_guard!();
1288        let logger =
1289            TestLogger::for_test("remote_compilation_test_instances_do_not_share_remote_paths");
1290
1291        let worker = WorkerConfig {
1292            id: WorkerId::new("w1"),
1293            host: "host".to_string(),
1294            user: "user".to_string(),
1295            identity_file: "~/.ssh/id_rsa".to_string(),
1296            total_slots: 1,
1297            ..Default::default()
1298        };
1299
1300        let first = RemoteCompilationTest::new(worker.clone(), PathBuf::from("/home/user/project"));
1301        let second = RemoteCompilationTest::new(worker, PathBuf::from("/home/user/project"));
1302
1303        let first_path = first.remote_project_path();
1304        let second_path = second.remote_project_path();
1305        logger.log_with_data(
1306            TestPhase::Verify,
1307            "Computed independent remote paths",
1308            serde_json::json!({
1309                "first_path": &first_path,
1310                "second_path": &second_path
1311            }),
1312        );
1313
1314        assert_ne!(first_path, second_path);
1315
1316        logger.pass();
1317    }
1318
1319    #[test]
1320    fn remote_compilation_test_with_custom_remote_base() {
1321        let _guard = test_guard!();
1322        let logger = TestLogger::for_test("remote_compilation_test_with_custom_remote_base");
1323
1324        let worker = WorkerConfig {
1325            id: WorkerId::new("w1"),
1326            host: "host".to_string(),
1327            user: "user".to_string(),
1328            identity_file: "~/.ssh/id_rsa".to_string(),
1329            total_slots: 1,
1330            ..Default::default()
1331        };
1332
1333        let mut test = RemoteCompilationTest::new(worker, PathBuf::from("/home/user/project"))
1334            .with_remote_path_suffix("custom-suffix");
1335        test.remote_base = "/custom/build/dir/".to_string();
1336
1337        let remote_path = test.remote_project_path();
1338        logger.log_with_data(
1339            TestPhase::Execute,
1340            "Computed remote path with custom base",
1341            serde_json::json!({
1342                "remote_base": "/custom/build/dir/",
1343                "remote_path": &remote_path
1344            }),
1345        );
1346
1347        assert_eq!(remote_path, "/custom/build/dir/project-custom-suffix");
1348
1349        logger.pass();
1350    }
1351
1352    #[test]
1353    fn remote_compilation_test_artifact_source_quotes_remote_base() {
1354        let _guard = test_guard!();
1355        let logger =
1356            TestLogger::for_test("remote_compilation_test_artifact_source_quotes_remote_base");
1357
1358        let worker = WorkerConfig {
1359            id: WorkerId::new("w1"),
1360            host: "host".to_string(),
1361            user: "user".to_string(),
1362            identity_file: "~/.ssh/id_rsa".to_string(),
1363            total_slots: 1,
1364            ..Default::default()
1365        };
1366
1367        let mut test = RemoteCompilationTest::new(worker, PathBuf::from("/home/user/project"))
1368            .with_remote_path_suffix("run-1");
1369        test.remote_base = "/tmp/rch self test".to_string();
1370
1371        let source = test.remote_artifact_source("release");
1372        logger.log_with_data(
1373            TestPhase::Verify,
1374            "Computed quoted artifact source",
1375            serde_json::json!({ "source": &source }),
1376        );
1377
1378        assert_eq!(
1379            source,
1380            "user@host:'/tmp/rch self test/project-run-1/target/release/'"
1381        );
1382
1383        logger.pass();
1384    }
1385
1386    #[test]
1387    fn remote_compilation_test_cargo_home_sanitizes_worker_id() {
1388        let _guard = test_guard!();
1389        let logger = TestLogger::for_test("remote_compilation_test_cargo_home_sanitizes_worker_id");
1390
1391        let worker = WorkerConfig {
1392            id: WorkerId::new("worker/one with spaces"),
1393            host: "host".to_string(),
1394            user: "user".to_string(),
1395            identity_file: "~/.ssh/id_rsa".to_string(),
1396            total_slots: 1,
1397            ..Default::default()
1398        };
1399
1400        let test = RemoteCompilationTest::new(worker, PathBuf::from("/home/user/project"));
1401        let cargo_home = test.remote_cargo_home_path("run/7 with spaces");
1402        logger.log_with_data(
1403            TestPhase::Verify,
1404            "Computed sanitized cargo home",
1405            serde_json::json!({ "cargo_home": &cargo_home }),
1406        );
1407
1408        // The worker_id/session_id are still sanitized into the basename, but the
1409        // base is now a worker-resolved shell variable (honoring $TMPDIR /
1410        // /data/tmp) instead of a hardcoded /tmp.
1411        assert_eq!(
1412            cargo_home,
1413            "${RCH_CH_BASE}/rch-cargo-home-worker-one-with-spaces-run-7-with-spaces"
1414        );
1415
1416        logger.pass();
1417    }
1418
1419    /// The prelude must never bake in `/tmp`: it resolves the staging base on the
1420    /// worker from `$TMPDIR` → `/data/tmp` → `/tmp` at execution time. rchd runs
1421    /// under systemd without PAM's `/etc/environment`, so this explicit resolution
1422    /// (not a client-baked path) is what keeps caches off a tmpfs `/tmp`.
1423    #[test]
1424    fn cargo_home_base_prelude_does_not_hardcode_tmp() {
1425        let prelude = remote_cargo_home_base_prelude();
1426        // Sets the documented variable, references $TMPDIR and /data/tmp.
1427        assert!(prelude.contains(RCH_CARGO_HOME_BASE_VAR), "{prelude}");
1428        assert!(
1429            prelude.contains("$TMPDIR") || prelude.contains("${TMPDIR"),
1430            "{prelude}"
1431        );
1432        assert!(prelude.contains("/data/tmp"), "{prelude}");
1433        // The guards must inspect the shell variable's value, not the literal
1434        // string "RCH_CH_BASE"; otherwise the prelude falls through to /tmp.
1435        assert!(prelude.contains("[ -n \"${RCH_CH_BASE}\" ]"), "{prelude}");
1436        assert!(prelude.contains("[ -d \"${RCH_CH_BASE}\" ]"), "{prelude}");
1437        assert!(!prelude.contains("[ -n \"RCH_CH_BASE\" ]"), "{prelude}");
1438        assert!(!prelude.contains("[ -d \"RCH_CH_BASE\" ]"), "{prelude}");
1439        // /tmp only as the last-resort fallback, never as the leading base.
1440        assert!(
1441            !prelude.contains("/tmp/rch"),
1442            "must not bake a /tmp path: {prelude}"
1443        );
1444    }
1445
1446    /// End-to-end behavioral check of the prelude's resolution order, executed
1447    /// under POSIX `sh` so a regression in the snippet is actually caught.
1448    #[cfg(unix)]
1449    #[test]
1450    fn cargo_home_base_prelude_resolves_tmpdir_first_then_data_tmp() {
1451        use std::process::Command;
1452        use tempfile::tempdir;
1453
1454        let prelude = remote_cargo_home_base_prelude();
1455        let var = RCH_CARGO_HOME_BASE_VAR;
1456        // Run the prelude then echo the resolved base, under controlled $TMPDIR.
1457        let run = |tmpdir_env: Option<&str>| -> String {
1458            let mut cmd = Command::new("sh");
1459            cmd.arg("-c")
1460                .arg(format!("{prelude}; printf '%s' \"${var}\""));
1461            cmd.env_remove("TMPDIR");
1462            if let Some(v) = tmpdir_env {
1463                cmd.env("TMPDIR", v);
1464            }
1465            let out = cmd.output().expect("run prelude under sh");
1466            String::from_utf8_lossy(&out.stdout).into_owned()
1467        };
1468
1469        // $TMPDIR set to a real dir wins.
1470        let dir = tempdir().expect("tempdir");
1471        let real = dir.path().to_str().unwrap();
1472        assert_eq!(run(Some(real)), real, "a real $TMPDIR must be honored");
1473
1474        // $TMPDIR unset → /data/tmp if it exists, else /tmp. Either is acceptable
1475        // and proves we never depend on $TMPDIR being present (the systemd case).
1476        let resolved = run(None);
1477        assert!(
1478            resolved == "/data/tmp" || resolved == "/tmp",
1479            "unset $TMPDIR must fall back to /data/tmp or /tmp, got {resolved:?}"
1480        );
1481
1482        // A bogus $TMPDIR (not a directory) must be rejected in favor of the
1483        // existing fallback, never used blindly.
1484        let bogus = run(Some("/nonexistent-rch-tmpdir-xyz"));
1485        assert!(
1486            bogus == "/data/tmp" || bogus == "/tmp",
1487            "a non-directory $TMPDIR must be rejected, got {bogus:?}"
1488        );
1489    }
1490
1491    #[test]
1492    fn verification_result_timing_fields() {
1493        let _guard = test_guard!();
1494        let logger = TestLogger::for_test("verification_result_timing_fields");
1495
1496        let result = VerificationResult {
1497            success: true,
1498            local_hash: BinaryHashResult {
1499                full_hash: "a".to_string(),
1500                code_hash: "b".to_string(),
1501                text_section_size: 100,
1502                is_debug: false,
1503            },
1504            remote_hash: BinaryHashResult {
1505                full_hash: "a".to_string(),
1506                code_hash: "b".to_string(),
1507                text_section_size: 100,
1508                is_debug: false,
1509            },
1510            local_build_ms: 1200,
1511            rsync_up_ms: 150,
1512            compilation_ms: 8000,
1513            rsync_down_ms: 200,
1514            total_ms: 8500,
1515            error: None,
1516            test_marker: "RCH_TEST_1".to_string(),
1517            verdict: "reproduced".to_string(),
1518        };
1519
1520        // Verify timing breakdown makes sense
1521        let sum = result.rsync_up_ms + result.compilation_ms + result.rsync_down_ms;
1522        logger.log_with_data(
1523            TestPhase::Verify,
1524            "Checking timing consistency",
1525            serde_json::json!({
1526                "sum_of_phases": sum,
1527                "total_reported": result.total_ms
1528            }),
1529        );
1530
1531        // Total should be >= sum (may include overhead)
1532        assert!(result.total_ms >= sum - 100); // Allow some timing variance
1533
1534        logger.pass();
1535    }
1536
1537    // ========================
1538    // use_mock_transport tests
1539    // ========================
1540
1541    #[test]
1542    fn use_mock_transport_with_mock_host() {
1543        let _guard = test_guard!();
1544        let logger = TestLogger::for_test("use_mock_transport_with_mock_host");
1545
1546        let worker = WorkerConfig {
1547            id: WorkerId::new("mock-worker"),
1548            host: "mock://localhost".to_string(),
1549            user: "user".to_string(),
1550            identity_file: "~/.ssh/id".to_string(),
1551            total_slots: 4,
1552            ..Default::default()
1553        };
1554
1555        logger.log(TestPhase::Execute, "Testing mock:// host detection");
1556        assert!(use_mock_transport(&worker));
1557
1558        logger.pass();
1559    }
1560
1561    /// Test use_mock_transport with real host and mock disabled.
1562    /// Uses global mock state - run with `cargo test -- --test-threads=1`.
1563    #[test]
1564    #[ignore = "uses global mock state; run with --test-threads=1"]
1565    fn use_mock_transport_with_real_host() {
1566        let logger = TestLogger::for_test("use_mock_transport_with_real_host");
1567
1568        // Ensure mock mode is disabled
1569        set_mock_enabled_override(Some(false));
1570
1571        let worker = WorkerConfig {
1572            id: WorkerId::new("real-worker"),
1573            host: "192.168.1.100".to_string(),
1574            user: "user".to_string(),
1575            identity_file: "~/.ssh/id".to_string(),
1576            total_slots: 4,
1577            ..Default::default()
1578        };
1579
1580        logger.log(TestPhase::Execute, "Testing real host (mock disabled)");
1581        assert!(!use_mock_transport(&worker));
1582
1583        clear_mock_overrides();
1584        logger.pass();
1585    }
1586
1587    /// Test use_mock_transport with real host but global mock override.
1588    /// Uses global mock state - run with `cargo test -- --test-threads=1`.
1589    #[test]
1590    #[ignore = "uses global mock state; run with --test-threads=1"]
1591    fn use_mock_transport_with_global_override() {
1592        let logger = TestLogger::for_test("use_mock_transport_with_global_override");
1593
1594        // Enable mock mode globally
1595        set_mock_enabled_override(Some(true));
1596
1597        let worker = WorkerConfig {
1598            id: WorkerId::new("real-worker"),
1599            host: "192.168.1.100".to_string(),
1600            user: "user".to_string(),
1601            identity_file: "~/.ssh/id".to_string(),
1602            total_slots: 4,
1603            ..Default::default()
1604        };
1605
1606        logger.log(
1607            TestPhase::Execute,
1608            "Testing real host with global mock override",
1609        );
1610        assert!(use_mock_transport(&worker));
1611
1612        clear_mock_overrides();
1613        logger.pass();
1614    }
1615
1616    // ========================
1617    // Async transport tests (using mock)
1618    // These tests use global mock state and must be run with --test-threads=1
1619    // ========================
1620
1621    /// Test rsync_to_worker with mock transport succeeds.
1622    #[tokio::test]
1623    #[ignore = "uses global mock state; run with --test-threads=1"]
1624    async fn rsync_to_worker_mock_success() {
1625        init_test_logging();
1626        let logger = TestLogger::for_test("rsync_to_worker_mock_success");
1627
1628        // Enable mock mode
1629        set_mock_enabled_override(Some(true));
1630        set_mock_ssh_config_override(Some(MockConfig::success()));
1631        set_mock_rsync_config_override(Some(MockRsyncConfig::success()));
1632        clear_global_invocations();
1633
1634        let worker = WorkerConfig {
1635            id: WorkerId::new("mock-worker"),
1636            host: "mock://localhost".to_string(),
1637            user: "testuser".to_string(),
1638            identity_file: "~/.ssh/mock_key".to_string(),
1639            total_slots: 4,
1640            ..Default::default()
1641        };
1642
1643        // Create a temporary directory for testing
1644        let temp_dir = tempfile::tempdir().unwrap();
1645        let test_project = temp_dir.path().to_path_buf();
1646
1647        let test = RemoteCompilationTest::new(worker, test_project);
1648
1649        logger.log(
1650            TestPhase::Execute,
1651            "Calling rsync_to_worker with mock transport",
1652        );
1653        let result = test.rsync_to_worker().await;
1654
1655        logger.log_with_data(
1656            TestPhase::Verify,
1657            "Checking rsync result",
1658            serde_json::json!({ "success": result.is_ok() }),
1659        );
1660
1661        assert!(result.is_ok());
1662
1663        // Verify invocations were recorded
1664        let ssh_invocations = global_ssh_invocations_snapshot();
1665        let rsync_invocations = global_rsync_invocations_snapshot();
1666
1667        logger.log_with_data(
1668            TestPhase::Verify,
1669            "Mock invocations recorded",
1670            serde_json::json!({
1671                "ssh_calls": ssh_invocations.len(),
1672                "rsync_calls": rsync_invocations.len()
1673            }),
1674        );
1675
1676        // Should have SSH calls (connect, mkdir, disconnect) and rsync call
1677        assert!(!ssh_invocations.is_empty());
1678        assert!(!rsync_invocations.is_empty());
1679
1680        clear_mock_overrides();
1681        logger.pass();
1682    }
1683
1684    /// Test rsync_to_worker fails when SSH connection fails.
1685    #[tokio::test]
1686    #[ignore = "uses global mock state; run with --test-threads=1"]
1687    async fn rsync_to_worker_mock_ssh_failure() {
1688        init_test_logging();
1689        let logger = TestLogger::for_test("rsync_to_worker_mock_ssh_failure");
1690
1691        // Enable mock mode with SSH connection failure
1692        set_mock_enabled_override(Some(true));
1693        set_mock_ssh_config_override(Some(MockConfig::connection_failure()));
1694        clear_global_invocations();
1695
1696        let worker = WorkerConfig {
1697            id: WorkerId::new("mock-worker"),
1698            host: "mock://localhost".to_string(),
1699            user: "testuser".to_string(),
1700            identity_file: "~/.ssh/mock_key".to_string(),
1701            total_slots: 4,
1702            ..Default::default()
1703        };
1704
1705        let temp_dir = tempfile::tempdir().unwrap();
1706        let test_project = temp_dir.path().to_path_buf();
1707
1708        let test = RemoteCompilationTest::new(worker, test_project);
1709
1710        logger.log(
1711            TestPhase::Execute,
1712            "Calling rsync_to_worker with failing SSH",
1713        );
1714        let result = test.rsync_to_worker().await;
1715
1716        logger.log_with_data(
1717            TestPhase::Verify,
1718            "Checking failure result",
1719            serde_json::json!({
1720                "is_err": result.is_err(),
1721                "error": result.as_ref().err().map(|e| e.to_string())
1722            }),
1723        );
1724
1725        assert!(result.is_err());
1726        let err_msg = result.unwrap_err().to_string();
1727        assert!(err_msg.contains("Connection failed") || err_msg.contains("failed"));
1728
1729        clear_mock_overrides();
1730        logger.pass();
1731    }
1732
1733    /// Test build_remote with mock transport succeeds.
1734    #[tokio::test]
1735    #[ignore = "uses global mock state; run with --test-threads=1"]
1736    async fn build_remote_mock_success() {
1737        init_test_logging();
1738        let logger = TestLogger::for_test("build_remote_mock_success");
1739
1740        set_mock_enabled_override(Some(true));
1741        set_mock_ssh_config_override(Some(MockConfig::success()));
1742        clear_global_invocations();
1743
1744        let worker = WorkerConfig {
1745            id: WorkerId::new("mock-worker"),
1746            host: "mock://localhost".to_string(),
1747            user: "testuser".to_string(),
1748            identity_file: "~/.ssh/mock_key".to_string(),
1749            total_slots: 4,
1750            ..Default::default()
1751        };
1752
1753        let temp_dir = tempfile::tempdir().unwrap();
1754        let test_project = temp_dir.path().to_path_buf();
1755
1756        let test = RemoteCompilationTest::new(worker, test_project);
1757
1758        logger.log(
1759            TestPhase::Execute,
1760            "Calling build_remote with mock transport",
1761        );
1762        let result = test.build_remote().await;
1763
1764        logger.log_with_data(
1765            TestPhase::Verify,
1766            "Checking build result",
1767            serde_json::json!({ "success": result.is_ok() }),
1768        );
1769
1770        assert!(result.is_ok());
1771
1772        // Verify SSH execution happened
1773        let ssh_invocations = global_ssh_invocations_snapshot();
1774        assert!(ssh_invocations.iter().any(|inv| {
1775            inv.command
1776                .as_ref()
1777                .is_some_and(|c| c.contains("cargo build"))
1778        }));
1779
1780        clear_mock_overrides();
1781        logger.pass();
1782    }
1783
1784    /// Test build_remote fails when remote command fails.
1785    #[tokio::test]
1786    #[ignore = "uses global mock state; run with --test-threads=1"]
1787    async fn build_remote_mock_command_failure() {
1788        init_test_logging();
1789        let logger = TestLogger::for_test("build_remote_mock_command_failure");
1790
1791        // Configure mock to fail command execution
1792        set_mock_enabled_override(Some(true));
1793        set_mock_ssh_config_override(Some(MockConfig::command_failure(1, "compilation error")));
1794        clear_global_invocations();
1795
1796        let worker = WorkerConfig {
1797            id: WorkerId::new("mock-worker"),
1798            host: "mock://localhost".to_string(),
1799            user: "testuser".to_string(),
1800            identity_file: "~/.ssh/mock_key".to_string(),
1801            total_slots: 4,
1802            ..Default::default()
1803        };
1804
1805        let temp_dir = tempfile::tempdir().unwrap();
1806        let test_project = temp_dir.path().to_path_buf();
1807
1808        let test = RemoteCompilationTest::new(worker, test_project);
1809
1810        logger.log(
1811            TestPhase::Execute,
1812            "Calling build_remote with failing command",
1813        );
1814        let result = test.build_remote().await;
1815
1816        logger.log_with_data(
1817            TestPhase::Verify,
1818            "Checking failure",
1819            serde_json::json!({
1820                "is_err": result.is_err(),
1821                "error": result.as_ref().err().map(|e| e.to_string())
1822            }),
1823        );
1824
1825        // Note: command_failure sets fail_execute=true, which returns Err
1826        assert!(result.is_err());
1827
1828        clear_mock_overrides();
1829        logger.pass();
1830    }
1831
1832    /// Test rsync_from_worker with mock transport succeeds.
1833    #[tokio::test]
1834    #[ignore = "uses global mock state; run with --test-threads=1"]
1835    async fn rsync_from_worker_mock_success() {
1836        init_test_logging();
1837        let logger = TestLogger::for_test("rsync_from_worker_mock_success");
1838
1839        set_mock_enabled_override(Some(true));
1840        set_mock_rsync_config_override(Some(MockRsyncConfig::success()));
1841        clear_global_invocations();
1842
1843        let worker = WorkerConfig {
1844            id: WorkerId::new("mock-worker"),
1845            host: "mock://localhost".to_string(),
1846            user: "testuser".to_string(),
1847            identity_file: "~/.ssh/mock_key".to_string(),
1848            total_slots: 4,
1849            ..Default::default()
1850        };
1851
1852        let temp_dir = tempfile::tempdir().unwrap();
1853        let test_project = temp_dir.path().to_path_buf();
1854
1855        let test = RemoteCompilationTest::new(worker, test_project);
1856
1857        logger.log(
1858            TestPhase::Execute,
1859            "Calling rsync_from_worker with mock transport",
1860        );
1861        let result = test.rsync_from_worker().await;
1862
1863        logger.log_with_data(
1864            TestPhase::Verify,
1865            "Checking rsync result",
1866            serde_json::json!({ "success": result.is_ok() }),
1867        );
1868
1869        assert!(result.is_ok());
1870
1871        // Verify artifact retrieval was called
1872        let rsync_invocations = global_rsync_invocations_snapshot();
1873        assert!(!rsync_invocations.is_empty());
1874
1875        clear_mock_overrides();
1876        logger.pass();
1877    }
1878
1879    /// Test rsync_from_worker fails when artifact retrieval fails.
1880    #[tokio::test]
1881    #[ignore = "uses global mock state; run with --test-threads=1"]
1882    async fn rsync_from_worker_mock_artifact_failure() {
1883        init_test_logging();
1884        let logger = TestLogger::for_test("rsync_from_worker_mock_artifact_failure");
1885
1886        set_mock_enabled_override(Some(true));
1887        set_mock_rsync_config_override(Some(MockRsyncConfig::artifact_failure()));
1888        clear_global_invocations();
1889
1890        let worker = WorkerConfig {
1891            id: WorkerId::new("mock-worker"),
1892            host: "mock://localhost".to_string(),
1893            user: "testuser".to_string(),
1894            identity_file: "~/.ssh/mock_key".to_string(),
1895            total_slots: 4,
1896            ..Default::default()
1897        };
1898
1899        let temp_dir = tempfile::tempdir().unwrap();
1900        let test_project = temp_dir.path().to_path_buf();
1901
1902        let test = RemoteCompilationTest::new(worker, test_project);
1903
1904        logger.log(
1905            TestPhase::Execute,
1906            "Calling rsync_from_worker with failing artifacts",
1907        );
1908        let result = test.rsync_from_worker().await;
1909
1910        logger.log_with_data(
1911            TestPhase::Verify,
1912            "Checking failure",
1913            serde_json::json!({
1914                "is_err": result.is_err(),
1915                "error": result.as_ref().err().map(|e| e.to_string())
1916            }),
1917        );
1918
1919        assert!(result.is_err());
1920
1921        clear_mock_overrides();
1922        logger.pass();
1923    }
1924
1925    // ========================
1926    // Edge case tests
1927    // ========================
1928
1929    #[test]
1930    fn remote_compilation_test_empty_project_path() {
1931        let _guard = test_guard!();
1932        let logger = TestLogger::for_test("remote_compilation_test_empty_project_path");
1933
1934        let worker = WorkerConfig {
1935            id: WorkerId::new("w1"),
1936            host: "host".to_string(),
1937            user: "user".to_string(),
1938            identity_file: "~/.ssh/id_rsa".to_string(),
1939            total_slots: 1,
1940            ..Default::default()
1941        };
1942
1943        // Empty path should still work (uses "unknown" as fallback)
1944        let test = RemoteCompilationTest::new(worker, PathBuf::new());
1945
1946        let binary_name = test.get_binary_name();
1947        logger.log_with_data(
1948            TestPhase::Execute,
1949            "Binary name for empty path",
1950            serde_json::json!({ "binary_name": &binary_name }),
1951        );
1952
1953        // Should fall back to "unknown"
1954        assert_eq!(binary_name, "unknown");
1955
1956        logger.pass();
1957    }
1958
1959    #[test]
1960    fn verification_result_zero_timings() {
1961        let _guard = test_guard!();
1962        let logger = TestLogger::for_test("verification_result_zero_timings");
1963
1964        let result = VerificationResult {
1965            success: true,
1966            local_hash: BinaryHashResult {
1967                full_hash: "h".to_string(),
1968                code_hash: "c".to_string(),
1969                text_section_size: 0,
1970                is_debug: false,
1971            },
1972            remote_hash: BinaryHashResult {
1973                full_hash: "h".to_string(),
1974                code_hash: "c".to_string(),
1975                text_section_size: 0,
1976                is_debug: false,
1977            },
1978            local_build_ms: 0,
1979            rsync_up_ms: 0,
1980            compilation_ms: 0,
1981            rsync_down_ms: 0,
1982            total_ms: 0,
1983            error: None,
1984            test_marker: "ZERO".to_string(),
1985            verdict: "reproduced".to_string(),
1986        };
1987
1988        logger.log(TestPhase::Execute, "Serializing zero-timing result");
1989        let json = serde_json::to_string(&result).unwrap();
1990        let restored: VerificationResult = serde_json::from_str(&json).unwrap();
1991
1992        assert_eq!(restored.local_build_ms, 0);
1993        assert_eq!(restored.total_ms, 0);
1994        assert!(restored.success);
1995
1996        logger.pass();
1997    }
1998
1999    #[test]
2000    fn verification_result_max_timings() {
2001        let _guard = test_guard!();
2002        let logger = TestLogger::for_test("verification_result_max_timings");
2003
2004        let result = VerificationResult {
2005            success: true,
2006            local_hash: BinaryHashResult {
2007                full_hash: "h".to_string(),
2008                code_hash: "c".to_string(),
2009                text_section_size: u64::MAX,
2010                is_debug: false,
2011            },
2012            remote_hash: BinaryHashResult {
2013                full_hash: "h".to_string(),
2014                code_hash: "c".to_string(),
2015                text_section_size: u64::MAX,
2016                is_debug: false,
2017            },
2018            local_build_ms: u64::MAX,
2019            rsync_up_ms: u64::MAX,
2020            compilation_ms: u64::MAX,
2021            rsync_down_ms: u64::MAX,
2022            total_ms: u64::MAX,
2023            error: None,
2024            test_marker: "MAX".to_string(),
2025            verdict: "reproduced".to_string(),
2026        };
2027
2028        logger.log(TestPhase::Execute, "Serializing max-timing result");
2029        let json = serde_json::to_string(&result).unwrap();
2030        let restored: VerificationResult = serde_json::from_str(&json).unwrap();
2031
2032        assert_eq!(restored.total_ms, u64::MAX);
2033        assert!(restored.success);
2034
2035        logger.pass();
2036    }
2037
2038    #[test]
2039    fn is_mock_enabled_respects_overrides() {
2040        let _guard = test_guard!();
2041        let logger = TestLogger::for_test("is_mock_enabled_respects_overrides");
2042
2043        // Start with clean state
2044        clear_mock_overrides();
2045
2046        // Without override, depends on env (we'll set it false)
2047        set_mock_enabled_override(Some(false));
2048        assert!(!is_mock_enabled());
2049
2050        // With override true
2051        set_mock_enabled_override(Some(true));
2052        assert!(is_mock_enabled());
2053
2054        // Clear and test again
2055        clear_mock_overrides();
2056        set_mock_enabled_override(Some(false));
2057        assert!(!is_mock_enabled());
2058
2059        clear_mock_overrides();
2060        logger.pass();
2061    }
2062}