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