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