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