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 { artifacts: Vec<RustArtifactInfo> },
224}
225
226/// Daemon status information.
227#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
228pub struct DaemonStatus {
229    /// Daemon version (e.g. "1.0.8"). Used by CLI to detect stale daemons.
230    pub version: String,
231    /// Number of artifacts in cache.
232    pub artifact_count: u64,
233    /// Total size of cached artifacts in bytes.
234    pub cache_size_bytes: u64,
235    /// Number of entries in the metadata cache.
236    pub metadata_entries: u64,
237    /// Daemon uptime in seconds.
238    pub uptime_secs: u64,
239    /// Total cache hits since startup.
240    pub cache_hits: u64,
241    /// Total cache misses since startup.
242    pub cache_misses: u64,
243    /// Total compile requests received.
244    pub total_compilations: u64,
245    /// Non-cacheable invocations (linking, preprocessing, etc.).
246    pub non_cacheable: u64,
247    /// Compilations that exited with non-zero status.
248    pub compile_errors: u64,
249    /// Estimated wall-clock time saved in milliseconds.
250    pub time_saved_ms: u64,
251    /// Total link/archive requests received.
252    pub total_links: u64,
253    /// Link cache hits.
254    pub link_hits: u64,
255    /// Link cache misses.
256    pub link_misses: u64,
257    /// Non-cacheable link invocations.
258    pub link_non_cacheable: u64,
259    /// Number of compilation contexts in the dependency graph.
260    pub dep_graph_contexts: u64,
261    /// Number of tracked files in the dependency graph.
262    pub dep_graph_files: u64,
263    /// Total sessions created since daemon start.
264    pub sessions_total: u64,
265    /// Currently active sessions.
266    pub sessions_active: u64,
267    /// Path to the cache directory.
268    pub cache_dir: NormalizedPath,
269    /// On-disk depgraph snapshot format version.
270    pub dep_graph_version: u32,
271    /// Size of the depgraph snapshot file on disk (0 = not persisted).
272    pub dep_graph_disk_size: u64,
273}
274
275/// Result of a cache lookup.
276#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
277pub enum LookupResult {
278    /// Cache hit.
279    Hit {
280        /// The cached artifact data.
281        artifact: ArtifactData,
282    },
283    /// Cache miss.
284    Miss,
285}
286
287/// Result of storing an artifact.
288#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
289pub enum StoreResult {
290    /// Successfully stored.
291    Stored,
292    /// Already existed in cache.
293    AlreadyExists,
294}
295
296/// Artifact data exchanged over the protocol.
297#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
298pub struct ArtifactData {
299    /// The output files (filename to contents).
300    pub outputs: Vec<ArtifactOutput>,
301    /// Captured stdout from the compiler.
302    pub stdout: Arc<Vec<u8>>,
303    /// Captured stderr from the compiler.
304    pub stderr: Arc<Vec<u8>>,
305    /// Compiler exit code.
306    pub exit_code: i32,
307}
308
309/// Per-session statistics, returned when the session opted in to tracking.
310#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
311pub struct SessionStats {
312    /// Wall-clock duration of the session in milliseconds.
313    pub duration_ms: u64,
314    /// Total compile requests in this session.
315    pub compilations: u64,
316    /// Cache hits in this session.
317    pub hits: u64,
318    /// Cache misses (cold compiles) in this session.
319    pub misses: u64,
320    /// Non-cacheable invocations (linking, preprocessing, etc.).
321    pub non_cacheable: u64,
322    /// Compilations that exited with non-zero status.
323    pub errors: u64,
324    /// Estimated wall-clock time saved in milliseconds.
325    pub time_saved_ms: u64,
326    /// Distinct source files compiled.
327    pub unique_sources: u64,
328    /// Total artifact bytes served from cache.
329    pub bytes_read: u64,
330    /// Total artifact bytes stored into cache.
331    pub bytes_written: u64,
332}
333
334/// A single output file from compilation.
335#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
336pub struct ArtifactOutput {
337    /// Relative filename (e.g., "foo.o").
338    pub name: String,
339    /// File contents (Arc-wrapped to avoid deep copies during caching).
340    pub data: Arc<Vec<u8>>,
341}
342
343/// Information about a cached Rust compilation artifact.
344#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
345pub struct RustArtifactInfo {
346    /// Cache key hex.
347    pub cache_key: String,
348    /// Output file names (e.g., ["libfoo-abc123.rlib", "libfoo-abc123.rmeta", "foo-abc123.d"]).
349    pub output_names: Vec<String>,
350    /// Number of payload files.
351    pub payload_count: usize,
352}
353
354#[cfg(test)]
355mod tests {
356    use super::*;
357
358    /// Helper: roundtrip a value through bincode.
359    fn roundtrip<T: Serialize + serde::de::DeserializeOwned + PartialEq + std::fmt::Debug>(
360        val: &T,
361    ) {
362        let bytes = bincode::serialize(val).unwrap();
363        let decoded: T = bincode::deserialize(&bytes).unwrap();
364        assert_eq!(*val, decoded);
365    }
366
367    #[test]
368    fn session_stats_roundtrip() {
369        let stats = SessionStats {
370            duration_ms: 12345,
371            compilations: 100,
372            hits: 80,
373            misses: 15,
374            non_cacheable: 5,
375            errors: 2,
376            time_saved_ms: 8000,
377            unique_sources: 42,
378            bytes_read: 1024 * 1024,
379            bytes_written: 512 * 1024,
380        };
381        roundtrip(&stats);
382    }
383
384    #[test]
385    fn session_stats_default_zeros() {
386        let stats = SessionStats {
387            duration_ms: 0,
388            compilations: 0,
389            hits: 0,
390            misses: 0,
391            non_cacheable: 0,
392            errors: 0,
393            time_saved_ms: 0,
394            unique_sources: 0,
395            bytes_read: 0,
396            bytes_written: 0,
397        };
398        roundtrip(&stats);
399    }
400
401    #[test]
402    fn daemon_status_expanded_roundtrip() {
403        let status = DaemonStatus {
404            version: env!("CARGO_PKG_VERSION").to_string(),
405            artifact_count: 892,
406            cache_size_bytes: 147_000_000,
407            metadata_entries: 5430,
408            uptime_secs: 8040,
409            cache_hits: 1089,
410            cache_misses: 143,
411            total_compilations: 1247,
412            non_cacheable: 15,
413            compile_errors: 3,
414            time_saved_ms: 750_000,
415            total_links: 50,
416            link_hits: 38,
417            link_misses: 10,
418            link_non_cacheable: 2,
419            dep_graph_contexts: 892,
420            dep_graph_files: 4201,
421            sessions_total: 41,
422            sessions_active: 3,
423            cache_dir: "/home/user/.zccache".into(),
424            dep_graph_version: 1,
425            dep_graph_disk_size: 2_500_000,
426        };
427        roundtrip(&status);
428    }
429
430    #[test]
431    fn session_start_with_track_stats_roundtrip() {
432        let req = Request::SessionStart {
433            client_pid: 1234,
434            working_dir: "/home/user/project".into(),
435            log_file: None,
436            track_stats: true,
437            journal_path: None,
438        };
439        roundtrip(&req);
440
441        let req_no_stats = Request::SessionStart {
442            client_pid: 1234,
443            working_dir: "/home/user/project".into(),
444            log_file: None,
445            track_stats: false,
446            journal_path: None,
447        };
448        roundtrip(&req_no_stats);
449    }
450
451    #[test]
452    fn session_start_with_journal_path_roundtrip() {
453        let req = Request::SessionStart {
454            client_pid: 5678,
455            working_dir: "/home/user/project".into(),
456            log_file: None,
457            track_stats: false,
458            journal_path: Some("/tmp/build.jsonl".into()),
459        };
460        roundtrip(&req);
461
462        let req_no_journal = Request::SessionStart {
463            client_pid: 5678,
464            working_dir: "/home/user/project".into(),
465            log_file: None,
466            track_stats: false,
467            journal_path: None,
468        };
469        roundtrip(&req_no_journal);
470    }
471
472    #[test]
473    fn session_started_with_journal_path_roundtrip() {
474        let resp = Response::SessionStarted {
475            session_id: "550e8400-e29b-41d4-a716-446655440000".into(),
476            journal_path: Some("/home/user/.zccache/logs/sessions/test.jsonl".into()),
477        };
478        roundtrip(&resp);
479
480        let resp_no_journal = Response::SessionStarted {
481            session_id: "550e8400-e29b-41d4-a716-446655440000".into(),
482            journal_path: None,
483        };
484        roundtrip(&resp_no_journal);
485    }
486
487    #[test]
488    fn session_ended_with_stats_roundtrip() {
489        let stats = SessionStats {
490            duration_ms: 34000,
491            compilations: 32,
492            hits: 28,
493            misses: 3,
494            non_cacheable: 1,
495            errors: 0,
496            time_saved_ms: 8200,
497            unique_sources: 30,
498            bytes_read: 2_000_000,
499            bytes_written: 500_000,
500        };
501        let resp = Response::SessionEnded { stats: Some(stats) };
502        roundtrip(&resp);
503
504        let resp_no_stats = Response::SessionEnded { stats: None };
505        roundtrip(&resp_no_stats);
506    }
507
508    #[test]
509    fn clear_request_roundtrip() {
510        roundtrip(&Request::Clear);
511    }
512
513    #[test]
514    fn cleared_response_roundtrip() {
515        roundtrip(&Response::Cleared {
516            artifacts_removed: 42,
517            metadata_cleared: 100,
518            dep_graph_contexts_cleared: 25,
519            on_disk_bytes_freed: 1024 * 1024,
520        });
521    }
522
523    #[test]
524    fn compile_ephemeral_roundtrip() {
525        roundtrip(&Request::CompileEphemeral {
526            client_pid: 9876,
527            working_dir: "/home/user/project".into(),
528            compiler: "/usr/bin/clang++".into(),
529            args: vec!["-c".into(), "main.cpp".into(), "-o".into(), "main.o".into()],
530            cwd: "/home/user/project/build".into(),
531            env: Some(vec![("PATH".into(), "/usr/bin".into())]),
532        });
533        // Also test with env = None
534        roundtrip(&Request::CompileEphemeral {
535            client_pid: 1,
536            working_dir: ".".into(),
537            compiler: "gcc".into(),
538            args: vec![],
539            cwd: ".".into(),
540            env: None,
541        });
542    }
543
544    #[test]
545    fn link_ephemeral_roundtrip() {
546        roundtrip(&Request::LinkEphemeral {
547            client_pid: 5555,
548            tool: "/usr/bin/ar".into(),
549            args: vec!["rcs".into(), "libfoo.a".into(), "a.o".into(), "b.o".into()],
550            cwd: "/home/user/project/build".into(),
551            env: Some(vec![("PATH".into(), "/usr/bin".into())]),
552        });
553        roundtrip(&Request::LinkEphemeral {
554            client_pid: 1,
555            tool: "lib.exe".into(),
556            args: vec!["/OUT:foo.lib".into(), "a.obj".into()],
557            cwd: ".".into(),
558            env: None,
559        });
560    }
561
562    #[test]
563    fn link_result_roundtrip() {
564        roundtrip(&Response::LinkResult {
565            exit_code: 0,
566            stdout: Arc::new(vec![]),
567            stderr: Arc::new(vec![]),
568            cached: true,
569            warning: None,
570        });
571        roundtrip(&Response::LinkResult {
572            exit_code: 0,
573            stdout: Arc::new(vec![]),
574            stderr: Arc::new(b"some warning".to_vec()),
575            cached: false,
576            warning: Some("non-deterministic: missing D flag".into()),
577        });
578    }
579
580    #[test]
581    fn session_stats_request_roundtrip() {
582        roundtrip(&Request::SessionStats {
583            session_id: "550e8400-e29b-41d4-a716-446655440000".into(),
584        });
585    }
586
587    #[test]
588    fn session_stats_result_roundtrip() {
589        let stats = SessionStats {
590            duration_ms: 5000,
591            compilations: 10,
592            hits: 7,
593            misses: 2,
594            non_cacheable: 1,
595            errors: 0,
596            time_saved_ms: 3000,
597            unique_sources: 9,
598            bytes_read: 50_000,
599            bytes_written: 20_000,
600        };
601        roundtrip(&Response::SessionStatsResult { stats: Some(stats) });
602        roundtrip(&Response::SessionStatsResult { stats: None });
603    }
604
605    #[test]
606    fn existing_request_variants_still_work() {
607        roundtrip(&Request::Ping);
608        roundtrip(&Request::Shutdown);
609        roundtrip(&Request::Status);
610        roundtrip(&Request::SessionEnd {
611            session_id: "550e8400-e29b-41d4-a716-446655440000".into(),
612        });
613        roundtrip(&Request::Compile {
614            session_id: "550e8400-e29b-41d4-a716-446655440000".into(),
615            args: vec!["-c".into(), "foo.c".into()],
616            cwd: "/tmp".into(),
617            compiler: "/usr/bin/gcc".into(),
618            env: None,
619        });
620    }
621
622    #[test]
623    fn existing_response_variants_still_work() {
624        roundtrip(&Response::Pong);
625        roundtrip(&Response::ShuttingDown);
626        roundtrip(&Response::CompileResult {
627            exit_code: 0,
628            stdout: Arc::new(vec![]),
629            stderr: Arc::new(vec![]),
630            cached: true,
631        });
632        roundtrip(&Response::Error {
633            message: "test".into(),
634        });
635    }
636
637    #[test]
638    fn daemon_status_version_field_roundtrips() {
639        let with_version = DaemonStatus {
640            version: "1.2.3".to_string(),
641            artifact_count: 0,
642            cache_size_bytes: 0,
643            metadata_entries: 0,
644            uptime_secs: 0,
645            cache_hits: 0,
646            cache_misses: 0,
647            total_compilations: 0,
648            non_cacheable: 0,
649            compile_errors: 0,
650            time_saved_ms: 0,
651            total_links: 0,
652            link_hits: 0,
653            link_misses: 0,
654            link_non_cacheable: 0,
655            dep_graph_contexts: 0,
656            dep_graph_files: 0,
657            sessions_total: 0,
658            sessions_active: 0,
659            cache_dir: "".into(),
660            dep_graph_version: 0,
661            dep_graph_disk_size: 0,
662        };
663        roundtrip(&with_version);
664    }
665
666    // Compile-time check: PROTOCOL_VERSION must be positive.
667    const _: () = assert!(crate::PROTOCOL_VERSION > 0);
668    // Compile-time check: PROTOCOL_VERSION == 6 after ListRustArtifacts addition.
669    const _FINGERPRINT_VERSION: () = assert!(crate::PROTOCOL_VERSION == 6);
670
671    #[test]
672    fn fingerprint_check_roundtrip() {
673        roundtrip(&Request::FingerprintCheck {
674            cache_file: "/tmp/lint.json".into(),
675            cache_type: "two-layer".into(),
676            root: "/home/user/project/src".into(),
677            extensions: vec!["rs".into(), "toml".into()],
678            include_globs: vec![],
679            exclude: vec![".git".into(), "target".into()],
680        });
681        roundtrip(&Request::FingerprintCheck {
682            cache_file: "cache.json".into(),
683            cache_type: "hash".into(),
684            root: ".".into(),
685            extensions: vec![],
686            include_globs: vec!["**/*.cpp".into(), "**/*.h".into()],
687            exclude: vec![],
688        });
689    }
690
691    #[test]
692    fn fingerprint_mark_success_roundtrip() {
693        roundtrip(&Request::FingerprintMarkSuccess {
694            cache_file: "/tmp/lint.json".into(),
695        });
696    }
697
698    #[test]
699    fn fingerprint_mark_failure_roundtrip() {
700        roundtrip(&Request::FingerprintMarkFailure {
701            cache_file: "/tmp/lint.json".into(),
702        });
703    }
704
705    #[test]
706    fn fingerprint_invalidate_roundtrip() {
707        roundtrip(&Request::FingerprintInvalidate {
708            cache_file: "/tmp/lint.json".into(),
709        });
710    }
711
712    #[test]
713    fn fingerprint_check_result_roundtrip() {
714        roundtrip(&Response::FingerprintCheckResult {
715            decision: "skip".into(),
716            reason: None,
717            changed_files: vec![],
718        });
719        roundtrip(&Response::FingerprintCheckResult {
720            decision: "run".into(),
721            reason: Some("content changed".into()),
722            changed_files: vec!["src/main.rs".into(), "src/lib.rs".into()],
723        });
724        roundtrip(&Response::FingerprintCheckResult {
725            decision: "run".into(),
726            reason: Some("no cache file".into()),
727            changed_files: vec![],
728        });
729    }
730
731    #[test]
732    fn fingerprint_ack_roundtrip() {
733        roundtrip(&Response::FingerprintAck);
734    }
735
736    #[test]
737    fn list_rust_artifacts_request_roundtrip() {
738        roundtrip(&Request::ListRustArtifacts);
739    }
740
741    #[test]
742    fn rust_artifact_list_response_roundtrip() {
743        roundtrip(&Response::RustArtifactList {
744            artifacts: vec![
745                RustArtifactInfo {
746                    cache_key: "abc123def456".into(),
747                    output_names: vec![
748                        "libfoo-abc123.rlib".into(),
749                        "libfoo-abc123.rmeta".into(),
750                        "foo-abc123.d".into(),
751                    ],
752                    payload_count: 3,
753                },
754                RustArtifactInfo {
755                    cache_key: "deadbeef".into(),
756                    output_names: vec!["libbar-deadbeef.rlib".into()],
757                    payload_count: 1,
758                },
759            ],
760        });
761        // Empty list
762        roundtrip(&Response::RustArtifactList { artifacts: vec![] });
763    }
764
765    #[test]
766    fn rust_artifact_info_roundtrip() {
767        roundtrip(&RustArtifactInfo {
768            cache_key: "0123456789abcdef".into(),
769            output_names: vec!["test.o".into()],
770            payload_count: 1,
771        });
772    }
773
774    #[test]
775    fn artifact_clone_shares_payload_via_arc() {
776        let artifact = ArtifactData {
777            outputs: vec![ArtifactOutput {
778                name: "test.o".into(),
779                data: Arc::new(vec![1, 2, 3, 4]),
780            }],
781            stdout: Arc::new(vec![5, 6]),
782            stderr: Arc::new(vec![7, 8]),
783            exit_code: 0,
784        };
785
786        let cloned = artifact.clone();
787
788        // Arc::clone bumps refcount — both point to the same allocation.
789        assert!(Arc::ptr_eq(
790            &artifact.outputs[0].data,
791            &cloned.outputs[0].data
792        ));
793        assert!(Arc::ptr_eq(&artifact.stdout, &cloned.stdout));
794        assert!(Arc::ptr_eq(&artifact.stderr, &cloned.stderr));
795    }
796
797    #[test]
798    fn arc_vec_u8_roundtrip_matches_plain_vec() {
799        // Prove Arc<Vec<u8>> serializes identically to Vec<u8>.
800        let plain: Vec<u8> = vec![0xDE, 0xAD, 0xBE, 0xEF];
801        let arc_wrapped: Arc<Vec<u8>> = Arc::new(plain.clone());
802
803        let plain_bytes = bincode::serialize(&plain).unwrap();
804        let arc_bytes = bincode::serialize(&arc_wrapped).unwrap();
805        assert_eq!(
806            plain_bytes, arc_bytes,
807            "Arc<Vec<u8>> must serialize identically to Vec<u8>"
808        );
809
810        // Deserialize Arc bytes back as plain Vec and vice versa.
811        let decoded_plain: Vec<u8> = bincode::deserialize(&arc_bytes).unwrap();
812        let decoded_arc: Arc<Vec<u8>> = bincode::deserialize(&plain_bytes).unwrap();
813        assert_eq!(decoded_plain, plain);
814        assert_eq!(*decoded_arc, plain);
815    }
816}