Skip to main content

zccache_protocol/
messages.rs

1//! Protocol message definitions.
2
3use serde::{Deserialize, Serialize};
4use std::sync::Arc;
5use zccache_core::NormalizedPath;
6
7/// A request from client to daemon.
8#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
9pub enum Request {
10    /// Health check.
11    Ping,
12    /// Request daemon shutdown.
13    Shutdown,
14    /// Request daemon status/statistics.
15    Status,
16    /// Look up a cached artifact by cache key.
17    Lookup {
18        /// Hex-encoded cache key.
19        cache_key: String,
20    },
21    /// Store a compilation artifact.
22    Store {
23        /// Hex-encoded cache key.
24        cache_key: String,
25        /// The artifact data to store.
26        artifact: ArtifactData,
27    },
28    /// Start a new session with the daemon.
29    SessionStart {
30        /// Client process ID.
31        client_pid: u32,
32        /// Client working directory.
33        working_dir: NormalizedPath,
34        /// Optional path to a log file for this session.
35        log_file: Option<NormalizedPath>,
36        /// Whether to track per-session statistics.
37        track_stats: bool,
38        /// Path for per-session JSONL compile journal (must end in .jsonl).
39        journal_path: Option<NormalizedPath>,
40    },
41    /// Compile a source file within an existing session.
42    Compile {
43        /// Session ID from a prior SessionStart (UUID string).
44        session_id: String,
45        /// Compiler arguments (e.g., ["-c", "hello.cpp", "-o", "hello.o"]).
46        args: Vec<String>,
47        /// Working directory for the compilation.
48        cwd: NormalizedPath,
49        /// Path to the compiler executable (required).
50        compiler: NormalizedPath,
51        /// Client environment variables to pass to the compiler process.
52        /// If `None`, the daemon's own environment is inherited (backward compat).
53        /// If `Some`, the compiler process uses exactly these env vars.
54        env: Option<Vec<(String, String)>>,
55    },
56    /// End a session.
57    SessionEnd {
58        /// Session ID to end (UUID string).
59        session_id: String,
60    },
61    /// Clear all caches (artifacts, metadata, dep graph).
62    Clear,
63    /// Single-roundtrip ephemeral compile: session start + compile + session end
64    /// in one message. Used by the CLI in drop-in wrapper mode to avoid 3 IPC
65    /// roundtrips per invocation.
66    CompileEphemeral {
67        /// Client process ID.
68        client_pid: u32,
69        /// Client working directory.
70        working_dir: NormalizedPath,
71        /// Path to the compiler executable.
72        compiler: NormalizedPath,
73        /// Compiler arguments (e.g., ["-c", "hello.cpp", "-o", "hello.o"]).
74        args: Vec<String>,
75        /// Working directory for the compilation.
76        cwd: NormalizedPath,
77        /// Client environment variables to pass to the compiler process.
78        env: Option<Vec<(String, String)>>,
79    },
80    /// Single-roundtrip ephemeral link/archive: used for `zccache ar ...` or
81    /// `zccache ld ...` in drop-in wrapper mode.
82    LinkEphemeral {
83        /// Client process ID.
84        client_pid: u32,
85        /// Path to the linker/archiver tool (ar, ld, lib.exe, link.exe, etc.).
86        tool: NormalizedPath,
87        /// Tool arguments (e.g., ["rcs", "libfoo.a", "a.o", "b.o"]).
88        args: Vec<String>,
89        /// Working directory for the link operation.
90        cwd: NormalizedPath,
91        /// Client environment variables.
92        env: Option<Vec<(String, String)>>,
93    },
94    /// Query per-session statistics without ending the session.
95    /// NOTE: Appended at end to preserve bincode variant indices.
96    SessionStats {
97        /// Session ID to query (UUID string).
98        session_id: String,
99    },
100    /// Check if files have changed since last successful fingerprint.
101    /// NOTE: Appended at end to preserve bincode variant indices.
102    FingerprintCheck {
103        /// Path to the cache file (e.g., .cache/lint.json).
104        cache_file: NormalizedPath,
105        /// Cache algorithm: "hash" or "two-layer".
106        cache_type: String,
107        /// Root directory to scan.
108        root: NormalizedPath,
109        /// File extensions to include (without dot, e.g., "rs", "cpp").
110        /// Empty = all files. Conflicts with `include_globs`.
111        extensions: Vec<String>,
112        /// Glob patterns for files to include (e.g., "**/*.rs").
113        /// Empty = use extensions filter.
114        include_globs: Vec<String>,
115        /// Patterns or directory names to exclude.
116        exclude: Vec<String>,
117    },
118    /// Mark the previous fingerprint check as successful.
119    FingerprintMarkSuccess {
120        /// Path to the cache file.
121        cache_file: NormalizedPath,
122    },
123    /// Mark the previous fingerprint check as failed.
124    FingerprintMarkFailure {
125        /// Path to the cache file.
126        cache_file: NormalizedPath,
127    },
128    /// Invalidate a fingerprint cache (delete all state).
129    FingerprintInvalidate {
130        /// Path to the cache file.
131        cache_file: NormalizedPath,
132    },
133    /// List all cached Rust artifacts with their output paths.
134    /// NOTE: Appended at end to preserve bincode variant indices.
135    ListRustArtifacts,
136}
137
138/// A response from daemon to client.
139#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
140pub enum Response {
141    /// Response to Ping.
142    Pong,
143    /// Shutdown acknowledged.
144    ShuttingDown,
145    /// Daemon status information.
146    Status(DaemonStatus),
147    /// Cache lookup result.
148    LookupResult(LookupResult),
149    /// Store result.
150    StoreResult(StoreResult),
151    /// Session successfully started.
152    SessionStarted {
153        /// Assigned session ID (UUID string).
154        session_id: String,
155        /// Path to the per-session JSONL journal file (if journal was requested).
156        journal_path: Option<NormalizedPath>,
157    },
158    /// Result of a compilation request.
159    CompileResult {
160        /// Compiler exit code.
161        exit_code: i32,
162        /// Captured stdout (Arc-wrapped to avoid copies on cache hits).
163        stdout: Arc<Vec<u8>>,
164        /// Captured stderr (Arc-wrapped to avoid copies on cache hits).
165        stderr: Arc<Vec<u8>>,
166        /// Whether this was served from cache.
167        cached: bool,
168    },
169    /// Session ended successfully.
170    SessionEnded {
171        /// Per-session stats, if the session opted in to tracking.
172        stats: Option<SessionStats>,
173    },
174    /// Result of a link/archive request.
175    LinkResult {
176        /// Tool exit code.
177        exit_code: i32,
178        /// Captured stdout (Arc-wrapped to avoid copies on cache hits).
179        stdout: Arc<Vec<u8>>,
180        /// Captured stderr (Arc-wrapped to avoid copies on cache hits).
181        stderr: Arc<Vec<u8>>,
182        /// Whether this was served from cache.
183        cached: bool,
184        /// Non-determinism warning (if tool invocation uses non-deterministic flags).
185        warning: Option<String>,
186    },
187    /// An error occurred processing the request.
188    Error {
189        /// Human-readable error message.
190        message: String,
191    },
192    /// Cache cleared successfully.
193    Cleared {
194        /// Number of in-memory artifacts removed.
195        artifacts_removed: u64,
196        /// Number of metadata cache entries cleared.
197        metadata_cleared: u64,
198        /// Number of dep graph contexts cleared.
199        dep_graph_contexts_cleared: u64,
200        /// Bytes freed from on-disk artifact cache.
201        on_disk_bytes_freed: u64,
202    },
203    /// Mid-session statistics snapshot.
204    /// NOTE: Appended at end to preserve bincode variant indices.
205    SessionStatsResult {
206        /// Per-session stats, if the session exists and opted in to tracking.
207        stats: Option<SessionStats>,
208    },
209    /// Result of a fingerprint check.
210    /// NOTE: Appended at end to preserve bincode variant indices.
211    FingerprintCheckResult {
212        /// "skip" or "run".
213        decision: String,
214        /// Reason for run (e.g., "no cache file", "content changed").
215        reason: Option<String>,
216        /// Files that changed (if available).
217        changed_files: Vec<String>,
218    },
219    /// Fingerprint mark/invalidate acknowledged.
220    FingerprintAck,
221    /// List of cached Rust artifacts.
222    /// NOTE: Appended at end to preserve bincode variant indices.
223    RustArtifactList {
224        artifacts: Vec<RustArtifactInfo>,
225    },
226}
227
228/// Daemon status information.
229#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
230pub struct DaemonStatus {
231    /// Daemon version (e.g. "1.0.8"). Used by CLI to detect stale daemons.
232    pub version: String,
233    /// Number of artifacts in cache.
234    pub artifact_count: u64,
235    /// Total size of cached artifacts in bytes.
236    pub cache_size_bytes: u64,
237    /// Number of entries in the metadata cache.
238    pub metadata_entries: u64,
239    /// Daemon uptime in seconds.
240    pub uptime_secs: u64,
241    /// Total cache hits since startup.
242    pub cache_hits: u64,
243    /// Total cache misses since startup.
244    pub cache_misses: u64,
245    /// Total compile requests received.
246    pub total_compilations: u64,
247    /// Non-cacheable invocations (linking, preprocessing, etc.).
248    pub non_cacheable: u64,
249    /// Compilations that exited with non-zero status.
250    pub compile_errors: u64,
251    /// Estimated wall-clock time saved in milliseconds.
252    pub time_saved_ms: u64,
253    /// Total link/archive requests received.
254    pub total_links: u64,
255    /// Link cache hits.
256    pub link_hits: u64,
257    /// Link cache misses.
258    pub link_misses: u64,
259    /// Non-cacheable link invocations.
260    pub link_non_cacheable: u64,
261    /// Number of compilation contexts in the dependency graph.
262    pub dep_graph_contexts: u64,
263    /// Number of tracked files in the dependency graph.
264    pub dep_graph_files: u64,
265    /// Total sessions created since daemon start.
266    pub sessions_total: u64,
267    /// Currently active sessions.
268    pub sessions_active: u64,
269    /// Path to the cache directory.
270    pub cache_dir: NormalizedPath,
271    /// On-disk depgraph snapshot format version.
272    pub dep_graph_version: u32,
273    /// Size of the depgraph snapshot file on disk (0 = not persisted).
274    pub dep_graph_disk_size: u64,
275}
276
277/// Result of a cache lookup.
278#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
279pub enum LookupResult {
280    /// Cache hit.
281    Hit {
282        /// The cached artifact data.
283        artifact: ArtifactData,
284    },
285    /// Cache miss.
286    Miss,
287}
288
289/// Result of storing an artifact.
290#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
291pub enum StoreResult {
292    /// Successfully stored.
293    Stored,
294    /// Already existed in cache.
295    AlreadyExists,
296}
297
298/// Artifact data exchanged over the protocol.
299#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
300pub struct ArtifactData {
301    /// The output files (filename to contents).
302    pub outputs: Vec<ArtifactOutput>,
303    /// Captured stdout from the compiler.
304    pub stdout: Arc<Vec<u8>>,
305    /// Captured stderr from the compiler.
306    pub stderr: Arc<Vec<u8>>,
307    /// Compiler exit code.
308    pub exit_code: i32,
309}
310
311/// Per-session statistics, returned when the session opted in to tracking.
312#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
313pub struct SessionStats {
314    /// Wall-clock duration of the session in milliseconds.
315    pub duration_ms: u64,
316    /// Total compile requests in this session.
317    pub compilations: u64,
318    /// Cache hits in this session.
319    pub hits: u64,
320    /// Cache misses (cold compiles) in this session.
321    pub misses: u64,
322    /// Non-cacheable invocations (linking, preprocessing, etc.).
323    pub non_cacheable: u64,
324    /// Compilations that exited with non-zero status.
325    pub errors: u64,
326    /// Estimated wall-clock time saved in milliseconds.
327    pub time_saved_ms: u64,
328    /// Distinct source files compiled.
329    pub unique_sources: u64,
330    /// Total artifact bytes served from cache.
331    pub bytes_read: u64,
332    /// Total artifact bytes stored into cache.
333    pub bytes_written: u64,
334}
335
336/// A single output file from compilation.
337#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
338pub struct ArtifactOutput {
339    /// Relative filename (e.g., "foo.o").
340    pub name: String,
341    /// File contents (Arc-wrapped to avoid deep copies during caching).
342    pub data: Arc<Vec<u8>>,
343}
344
345/// Information about a cached Rust compilation artifact.
346#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
347pub struct RustArtifactInfo {
348    /// Cache key hex.
349    pub cache_key: String,
350    /// Output file names (e.g., ["libfoo-abc123.rlib", "libfoo-abc123.rmeta", "foo-abc123.d"]).
351    pub output_names: Vec<String>,
352    /// Number of payload files.
353    pub payload_count: usize,
354}
355
356#[cfg(test)]
357mod tests {
358    use super::*;
359
360    /// Helper: roundtrip a value through bincode.
361    fn roundtrip<T: Serialize + serde::de::DeserializeOwned + PartialEq + std::fmt::Debug>(
362        val: &T,
363    ) {
364        let bytes = bincode::serialize(val).unwrap();
365        let decoded: T = bincode::deserialize(&bytes).unwrap();
366        assert_eq!(*val, decoded);
367    }
368
369    #[test]
370    fn session_stats_roundtrip() {
371        let stats = SessionStats {
372            duration_ms: 12345,
373            compilations: 100,
374            hits: 80,
375            misses: 15,
376            non_cacheable: 5,
377            errors: 2,
378            time_saved_ms: 8000,
379            unique_sources: 42,
380            bytes_read: 1024 * 1024,
381            bytes_written: 512 * 1024,
382        };
383        roundtrip(&stats);
384    }
385
386    #[test]
387    fn session_stats_default_zeros() {
388        let stats = SessionStats {
389            duration_ms: 0,
390            compilations: 0,
391            hits: 0,
392            misses: 0,
393            non_cacheable: 0,
394            errors: 0,
395            time_saved_ms: 0,
396            unique_sources: 0,
397            bytes_read: 0,
398            bytes_written: 0,
399        };
400        roundtrip(&stats);
401    }
402
403    #[test]
404    fn daemon_status_expanded_roundtrip() {
405        let status = DaemonStatus {
406            version: env!("CARGO_PKG_VERSION").to_string(),
407            artifact_count: 892,
408            cache_size_bytes: 147_000_000,
409            metadata_entries: 5430,
410            uptime_secs: 8040,
411            cache_hits: 1089,
412            cache_misses: 143,
413            total_compilations: 1247,
414            non_cacheable: 15,
415            compile_errors: 3,
416            time_saved_ms: 750_000,
417            total_links: 50,
418            link_hits: 38,
419            link_misses: 10,
420            link_non_cacheable: 2,
421            dep_graph_contexts: 892,
422            dep_graph_files: 4201,
423            sessions_total: 41,
424            sessions_active: 3,
425            cache_dir: "/home/user/.zccache".into(),
426            dep_graph_version: 1,
427            dep_graph_disk_size: 2_500_000,
428        };
429        roundtrip(&status);
430    }
431
432    #[test]
433    fn session_start_with_track_stats_roundtrip() {
434        let req = Request::SessionStart {
435            client_pid: 1234,
436            working_dir: "/home/user/project".into(),
437            log_file: None,
438            track_stats: true,
439            journal_path: None,
440        };
441        roundtrip(&req);
442
443        let req_no_stats = Request::SessionStart {
444            client_pid: 1234,
445            working_dir: "/home/user/project".into(),
446            log_file: None,
447            track_stats: false,
448            journal_path: None,
449        };
450        roundtrip(&req_no_stats);
451    }
452
453    #[test]
454    fn session_start_with_journal_path_roundtrip() {
455        let req = Request::SessionStart {
456            client_pid: 5678,
457            working_dir: "/home/user/project".into(),
458            log_file: None,
459            track_stats: false,
460            journal_path: Some("/tmp/build.jsonl".into()),
461        };
462        roundtrip(&req);
463
464        let req_no_journal = Request::SessionStart {
465            client_pid: 5678,
466            working_dir: "/home/user/project".into(),
467            log_file: None,
468            track_stats: false,
469            journal_path: None,
470        };
471        roundtrip(&req_no_journal);
472    }
473
474    #[test]
475    fn session_started_with_journal_path_roundtrip() {
476        let resp = Response::SessionStarted {
477            session_id: "550e8400-e29b-41d4-a716-446655440000".into(),
478            journal_path: Some("/home/user/.zccache/logs/sessions/test.jsonl".into()),
479        };
480        roundtrip(&resp);
481
482        let resp_no_journal = Response::SessionStarted {
483            session_id: "550e8400-e29b-41d4-a716-446655440000".into(),
484            journal_path: None,
485        };
486        roundtrip(&resp_no_journal);
487    }
488
489    #[test]
490    fn session_ended_with_stats_roundtrip() {
491        let stats = SessionStats {
492            duration_ms: 34000,
493            compilations: 32,
494            hits: 28,
495            misses: 3,
496            non_cacheable: 1,
497            errors: 0,
498            time_saved_ms: 8200,
499            unique_sources: 30,
500            bytes_read: 2_000_000,
501            bytes_written: 500_000,
502        };
503        let resp = Response::SessionEnded { stats: Some(stats) };
504        roundtrip(&resp);
505
506        let resp_no_stats = Response::SessionEnded { stats: None };
507        roundtrip(&resp_no_stats);
508    }
509
510    #[test]
511    fn clear_request_roundtrip() {
512        roundtrip(&Request::Clear);
513    }
514
515    #[test]
516    fn cleared_response_roundtrip() {
517        roundtrip(&Response::Cleared {
518            artifacts_removed: 42,
519            metadata_cleared: 100,
520            dep_graph_contexts_cleared: 25,
521            on_disk_bytes_freed: 1024 * 1024,
522        });
523    }
524
525    #[test]
526    fn compile_ephemeral_roundtrip() {
527        roundtrip(&Request::CompileEphemeral {
528            client_pid: 9876,
529            working_dir: "/home/user/project".into(),
530            compiler: "/usr/bin/clang++".into(),
531            args: vec!["-c".into(), "main.cpp".into(), "-o".into(), "main.o".into()],
532            cwd: "/home/user/project/build".into(),
533            env: Some(vec![("PATH".into(), "/usr/bin".into())]),
534        });
535        // Also test with env = None
536        roundtrip(&Request::CompileEphemeral {
537            client_pid: 1,
538            working_dir: ".".into(),
539            compiler: "gcc".into(),
540            args: vec![],
541            cwd: ".".into(),
542            env: None,
543        });
544    }
545
546    #[test]
547    fn link_ephemeral_roundtrip() {
548        roundtrip(&Request::LinkEphemeral {
549            client_pid: 5555,
550            tool: "/usr/bin/ar".into(),
551            args: vec!["rcs".into(), "libfoo.a".into(), "a.o".into(), "b.o".into()],
552            cwd: "/home/user/project/build".into(),
553            env: Some(vec![("PATH".into(), "/usr/bin".into())]),
554        });
555        roundtrip(&Request::LinkEphemeral {
556            client_pid: 1,
557            tool: "lib.exe".into(),
558            args: vec!["/OUT:foo.lib".into(), "a.obj".into()],
559            cwd: ".".into(),
560            env: None,
561        });
562    }
563
564    #[test]
565    fn link_result_roundtrip() {
566        roundtrip(&Response::LinkResult {
567            exit_code: 0,
568            stdout: Arc::new(vec![]),
569            stderr: Arc::new(vec![]),
570            cached: true,
571            warning: None,
572        });
573        roundtrip(&Response::LinkResult {
574            exit_code: 0,
575            stdout: Arc::new(vec![]),
576            stderr: Arc::new(b"some warning".to_vec()),
577            cached: false,
578            warning: Some("non-deterministic: missing D flag".into()),
579        });
580    }
581
582    #[test]
583    fn session_stats_request_roundtrip() {
584        roundtrip(&Request::SessionStats {
585            session_id: "550e8400-e29b-41d4-a716-446655440000".into(),
586        });
587    }
588
589    #[test]
590    fn session_stats_result_roundtrip() {
591        let stats = SessionStats {
592            duration_ms: 5000,
593            compilations: 10,
594            hits: 7,
595            misses: 2,
596            non_cacheable: 1,
597            errors: 0,
598            time_saved_ms: 3000,
599            unique_sources: 9,
600            bytes_read: 50_000,
601            bytes_written: 20_000,
602        };
603        roundtrip(&Response::SessionStatsResult { stats: Some(stats) });
604        roundtrip(&Response::SessionStatsResult { stats: None });
605    }
606
607    #[test]
608    fn existing_request_variants_still_work() {
609        roundtrip(&Request::Ping);
610        roundtrip(&Request::Shutdown);
611        roundtrip(&Request::Status);
612        roundtrip(&Request::SessionEnd {
613            session_id: "550e8400-e29b-41d4-a716-446655440000".into(),
614        });
615        roundtrip(&Request::Compile {
616            session_id: "550e8400-e29b-41d4-a716-446655440000".into(),
617            args: vec!["-c".into(), "foo.c".into()],
618            cwd: "/tmp".into(),
619            compiler: "/usr/bin/gcc".into(),
620            env: None,
621        });
622    }
623
624    #[test]
625    fn existing_response_variants_still_work() {
626        roundtrip(&Response::Pong);
627        roundtrip(&Response::ShuttingDown);
628        roundtrip(&Response::CompileResult {
629            exit_code: 0,
630            stdout: Arc::new(vec![]),
631            stderr: Arc::new(vec![]),
632            cached: true,
633        });
634        roundtrip(&Response::Error {
635            message: "test".into(),
636        });
637    }
638
639    #[test]
640    fn daemon_status_version_field_roundtrips() {
641        let with_version = DaemonStatus {
642            version: "1.2.3".to_string(),
643            artifact_count: 0,
644            cache_size_bytes: 0,
645            metadata_entries: 0,
646            uptime_secs: 0,
647            cache_hits: 0,
648            cache_misses: 0,
649            total_compilations: 0,
650            non_cacheable: 0,
651            compile_errors: 0,
652            time_saved_ms: 0,
653            total_links: 0,
654            link_hits: 0,
655            link_misses: 0,
656            link_non_cacheable: 0,
657            dep_graph_contexts: 0,
658            dep_graph_files: 0,
659            sessions_total: 0,
660            sessions_active: 0,
661            cache_dir: "".into(),
662            dep_graph_version: 0,
663            dep_graph_disk_size: 0,
664        };
665        roundtrip(&with_version);
666    }
667
668    // Compile-time check: PROTOCOL_VERSION must be positive.
669    const _: () = assert!(crate::PROTOCOL_VERSION > 0);
670    // Compile-time check: PROTOCOL_VERSION == 6 after ListRustArtifacts addition.
671    const _FINGERPRINT_VERSION: () = assert!(crate::PROTOCOL_VERSION == 6);
672
673    #[test]
674    fn fingerprint_check_roundtrip() {
675        roundtrip(&Request::FingerprintCheck {
676            cache_file: "/tmp/lint.json".into(),
677            cache_type: "two-layer".into(),
678            root: "/home/user/project/src".into(),
679            extensions: vec!["rs".into(), "toml".into()],
680            include_globs: vec![],
681            exclude: vec![".git".into(), "target".into()],
682        });
683        roundtrip(&Request::FingerprintCheck {
684            cache_file: "cache.json".into(),
685            cache_type: "hash".into(),
686            root: ".".into(),
687            extensions: vec![],
688            include_globs: vec!["**/*.cpp".into(), "**/*.h".into()],
689            exclude: vec![],
690        });
691    }
692
693    #[test]
694    fn fingerprint_mark_success_roundtrip() {
695        roundtrip(&Request::FingerprintMarkSuccess {
696            cache_file: "/tmp/lint.json".into(),
697        });
698    }
699
700    #[test]
701    fn fingerprint_mark_failure_roundtrip() {
702        roundtrip(&Request::FingerprintMarkFailure {
703            cache_file: "/tmp/lint.json".into(),
704        });
705    }
706
707    #[test]
708    fn fingerprint_invalidate_roundtrip() {
709        roundtrip(&Request::FingerprintInvalidate {
710            cache_file: "/tmp/lint.json".into(),
711        });
712    }
713
714    #[test]
715    fn fingerprint_check_result_roundtrip() {
716        roundtrip(&Response::FingerprintCheckResult {
717            decision: "skip".into(),
718            reason: None,
719            changed_files: vec![],
720        });
721        roundtrip(&Response::FingerprintCheckResult {
722            decision: "run".into(),
723            reason: Some("content changed".into()),
724            changed_files: vec!["src/main.rs".into(), "src/lib.rs".into()],
725        });
726        roundtrip(&Response::FingerprintCheckResult {
727            decision: "run".into(),
728            reason: Some("no cache file".into()),
729            changed_files: vec![],
730        });
731    }
732
733    #[test]
734    fn fingerprint_ack_roundtrip() {
735        roundtrip(&Response::FingerprintAck);
736    }
737
738    #[test]
739    fn list_rust_artifacts_request_roundtrip() {
740        roundtrip(&Request::ListRustArtifacts);
741    }
742
743    #[test]
744    fn rust_artifact_list_response_roundtrip() {
745        roundtrip(&Response::RustArtifactList {
746            artifacts: vec![
747                RustArtifactInfo {
748                    cache_key: "abc123def456".into(),
749                    output_names: vec![
750                        "libfoo-abc123.rlib".into(),
751                        "libfoo-abc123.rmeta".into(),
752                        "foo-abc123.d".into(),
753                    ],
754                    payload_count: 3,
755                },
756                RustArtifactInfo {
757                    cache_key: "deadbeef".into(),
758                    output_names: vec!["libbar-deadbeef.rlib".into()],
759                    payload_count: 1,
760                },
761            ],
762        });
763        // Empty list
764        roundtrip(&Response::RustArtifactList {
765            artifacts: vec![],
766        });
767    }
768
769    #[test]
770    fn rust_artifact_info_roundtrip() {
771        roundtrip(&RustArtifactInfo {
772            cache_key: "0123456789abcdef".into(),
773            output_names: vec!["test.o".into()],
774            payload_count: 1,
775        });
776    }
777
778    #[test]
779    fn artifact_clone_shares_payload_via_arc() {
780        let artifact = ArtifactData {
781            outputs: vec![ArtifactOutput {
782                name: "test.o".into(),
783                data: Arc::new(vec![1, 2, 3, 4]),
784            }],
785            stdout: Arc::new(vec![5, 6]),
786            stderr: Arc::new(vec![7, 8]),
787            exit_code: 0,
788        };
789
790        let cloned = artifact.clone();
791
792        // Arc::clone bumps refcount — both point to the same allocation.
793        assert!(Arc::ptr_eq(
794            &artifact.outputs[0].data,
795            &cloned.outputs[0].data
796        ));
797        assert!(Arc::ptr_eq(&artifact.stdout, &cloned.stdout));
798        assert!(Arc::ptr_eq(&artifact.stderr, &cloned.stderr));
799    }
800
801    #[test]
802    fn arc_vec_u8_roundtrip_matches_plain_vec() {
803        // Prove Arc<Vec<u8>> serializes identically to Vec<u8>.
804        let plain: Vec<u8> = vec![0xDE, 0xAD, 0xBE, 0xEF];
805        let arc_wrapped: Arc<Vec<u8>> = Arc::new(plain.clone());
806
807        let plain_bytes = bincode::serialize(&plain).unwrap();
808        let arc_bytes = bincode::serialize(&arc_wrapped).unwrap();
809        assert_eq!(
810            plain_bytes, arc_bytes,
811            "Arc<Vec<u8>> must serialize identically to Vec<u8>"
812        );
813
814        // Deserialize Arc bytes back as plain Vec and vice versa.
815        let decoded_plain: Vec<u8> = bincode::deserialize(&arc_bytes).unwrap();
816        let decoded_arc: Arc<Vec<u8>> = bincode::deserialize(&plain_bytes).unwrap();
817        assert_eq!(decoded_plain, plain);
818        assert_eq!(*decoded_arc, plain);
819    }
820}