Skip to main content

zccache_protocol/
messages.rs

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