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}
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<NormalizedPath>,
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: NormalizedPath,
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: "/home/user/.zccache".into(),
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: "/home/user/project".into(),
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: "/home/user/project".into(),
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: "/home/user/project".into(),
439            log_file: None,
440            track_stats: false,
441            journal_path: Some("/tmp/build.jsonl".into()),
442        };
443        roundtrip(&req);
444
445        let req_no_journal = Request::SessionStart {
446            client_pid: 5678,
447            working_dir: "/home/user/project".into(),
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("/home/user/.zccache/logs/sessions/test.jsonl".into()),
460        };
461        roundtrip(&resp);
462
463        let resp_no_journal = Response::SessionStarted {
464            session_id: "550e8400-e29b-41d4-a716-446655440000".into(),
465            journal_path: None,
466        };
467        roundtrip(&resp_no_journal);
468    }
469
470    #[test]
471    fn session_ended_with_stats_roundtrip() {
472        let stats = SessionStats {
473            duration_ms: 34000,
474            compilations: 32,
475            hits: 28,
476            misses: 3,
477            non_cacheable: 1,
478            errors: 0,
479            time_saved_ms: 8200,
480            unique_sources: 30,
481            bytes_read: 2_000_000,
482            bytes_written: 500_000,
483        };
484        let resp = Response::SessionEnded { stats: Some(stats) };
485        roundtrip(&resp);
486
487        let resp_no_stats = Response::SessionEnded { stats: None };
488        roundtrip(&resp_no_stats);
489    }
490
491    #[test]
492    fn clear_request_roundtrip() {
493        roundtrip(&Request::Clear);
494    }
495
496    #[test]
497    fn cleared_response_roundtrip() {
498        roundtrip(&Response::Cleared {
499            artifacts_removed: 42,
500            metadata_cleared: 100,
501            dep_graph_contexts_cleared: 25,
502            on_disk_bytes_freed: 1024 * 1024,
503        });
504    }
505
506    #[test]
507    fn compile_ephemeral_roundtrip() {
508        roundtrip(&Request::CompileEphemeral {
509            client_pid: 9876,
510            working_dir: "/home/user/project".into(),
511            compiler: "/usr/bin/clang++".into(),
512            args: vec!["-c".into(), "main.cpp".into(), "-o".into(), "main.o".into()],
513            cwd: "/home/user/project/build".into(),
514            env: Some(vec![("PATH".into(), "/usr/bin".into())]),
515        });
516        // Also test with env = None
517        roundtrip(&Request::CompileEphemeral {
518            client_pid: 1,
519            working_dir: ".".into(),
520            compiler: "gcc".into(),
521            args: vec![],
522            cwd: ".".into(),
523            env: None,
524        });
525    }
526
527    #[test]
528    fn link_ephemeral_roundtrip() {
529        roundtrip(&Request::LinkEphemeral {
530            client_pid: 5555,
531            tool: "/usr/bin/ar".into(),
532            args: vec!["rcs".into(), "libfoo.a".into(), "a.o".into(), "b.o".into()],
533            cwd: "/home/user/project/build".into(),
534            env: Some(vec![("PATH".into(), "/usr/bin".into())]),
535        });
536        roundtrip(&Request::LinkEphemeral {
537            client_pid: 1,
538            tool: "lib.exe".into(),
539            args: vec!["/OUT:foo.lib".into(), "a.obj".into()],
540            cwd: ".".into(),
541            env: None,
542        });
543    }
544
545    #[test]
546    fn link_result_roundtrip() {
547        roundtrip(&Response::LinkResult {
548            exit_code: 0,
549            stdout: Arc::new(vec![]),
550            stderr: Arc::new(vec![]),
551            cached: true,
552            warning: None,
553        });
554        roundtrip(&Response::LinkResult {
555            exit_code: 0,
556            stdout: Arc::new(vec![]),
557            stderr: Arc::new(b"some warning".to_vec()),
558            cached: false,
559            warning: Some("non-deterministic: missing D flag".into()),
560        });
561    }
562
563    #[test]
564    fn session_stats_request_roundtrip() {
565        roundtrip(&Request::SessionStats {
566            session_id: "550e8400-e29b-41d4-a716-446655440000".into(),
567        });
568    }
569
570    #[test]
571    fn session_stats_result_roundtrip() {
572        let stats = SessionStats {
573            duration_ms: 5000,
574            compilations: 10,
575            hits: 7,
576            misses: 2,
577            non_cacheable: 1,
578            errors: 0,
579            time_saved_ms: 3000,
580            unique_sources: 9,
581            bytes_read: 50_000,
582            bytes_written: 20_000,
583        };
584        roundtrip(&Response::SessionStatsResult { stats: Some(stats) });
585        roundtrip(&Response::SessionStatsResult { stats: None });
586    }
587
588    #[test]
589    fn existing_request_variants_still_work() {
590        roundtrip(&Request::Ping);
591        roundtrip(&Request::Shutdown);
592        roundtrip(&Request::Status);
593        roundtrip(&Request::SessionEnd {
594            session_id: "550e8400-e29b-41d4-a716-446655440000".into(),
595        });
596        roundtrip(&Request::Compile {
597            session_id: "550e8400-e29b-41d4-a716-446655440000".into(),
598            args: vec!["-c".into(), "foo.c".into()],
599            cwd: "/tmp".into(),
600            compiler: "/usr/bin/gcc".into(),
601            env: None,
602        });
603    }
604
605    #[test]
606    fn existing_response_variants_still_work() {
607        roundtrip(&Response::Pong);
608        roundtrip(&Response::ShuttingDown);
609        roundtrip(&Response::CompileResult {
610            exit_code: 0,
611            stdout: Arc::new(vec![]),
612            stderr: Arc::new(vec![]),
613            cached: true,
614        });
615        roundtrip(&Response::Error {
616            message: "test".into(),
617        });
618    }
619
620    #[test]
621    fn daemon_status_version_field_roundtrips() {
622        let with_version = DaemonStatus {
623            version: "1.2.3".to_string(),
624            artifact_count: 0,
625            cache_size_bytes: 0,
626            metadata_entries: 0,
627            uptime_secs: 0,
628            cache_hits: 0,
629            cache_misses: 0,
630            total_compilations: 0,
631            non_cacheable: 0,
632            compile_errors: 0,
633            time_saved_ms: 0,
634            total_links: 0,
635            link_hits: 0,
636            link_misses: 0,
637            link_non_cacheable: 0,
638            dep_graph_contexts: 0,
639            dep_graph_files: 0,
640            sessions_total: 0,
641            sessions_active: 0,
642            cache_dir: "".into(),
643            dep_graph_version: 0,
644            dep_graph_disk_size: 0,
645        };
646        roundtrip(&with_version);
647    }
648
649    // Compile-time check: PROTOCOL_VERSION must be positive.
650    const _: () = assert!(crate::PROTOCOL_VERSION > 0);
651    // Compile-time check: PROTOCOL_VERSION == 5 after LinkEphemeral working_dir removal.
652    const _FINGERPRINT_VERSION: () = assert!(crate::PROTOCOL_VERSION == 5);
653
654    #[test]
655    fn fingerprint_check_roundtrip() {
656        roundtrip(&Request::FingerprintCheck {
657            cache_file: "/tmp/lint.json".into(),
658            cache_type: "two-layer".into(),
659            root: "/home/user/project/src".into(),
660            extensions: vec!["rs".into(), "toml".into()],
661            include_globs: vec![],
662            exclude: vec![".git".into(), "target".into()],
663        });
664        roundtrip(&Request::FingerprintCheck {
665            cache_file: "cache.json".into(),
666            cache_type: "hash".into(),
667            root: ".".into(),
668            extensions: vec![],
669            include_globs: vec!["**/*.cpp".into(), "**/*.h".into()],
670            exclude: vec![],
671        });
672    }
673
674    #[test]
675    fn fingerprint_mark_success_roundtrip() {
676        roundtrip(&Request::FingerprintMarkSuccess {
677            cache_file: "/tmp/lint.json".into(),
678        });
679    }
680
681    #[test]
682    fn fingerprint_mark_failure_roundtrip() {
683        roundtrip(&Request::FingerprintMarkFailure {
684            cache_file: "/tmp/lint.json".into(),
685        });
686    }
687
688    #[test]
689    fn fingerprint_invalidate_roundtrip() {
690        roundtrip(&Request::FingerprintInvalidate {
691            cache_file: "/tmp/lint.json".into(),
692        });
693    }
694
695    #[test]
696    fn fingerprint_check_result_roundtrip() {
697        roundtrip(&Response::FingerprintCheckResult {
698            decision: "skip".into(),
699            reason: None,
700            changed_files: vec![],
701        });
702        roundtrip(&Response::FingerprintCheckResult {
703            decision: "run".into(),
704            reason: Some("content changed".into()),
705            changed_files: vec!["src/main.rs".into(), "src/lib.rs".into()],
706        });
707        roundtrip(&Response::FingerprintCheckResult {
708            decision: "run".into(),
709            reason: Some("no cache file".into()),
710            changed_files: vec![],
711        });
712    }
713
714    #[test]
715    fn fingerprint_ack_roundtrip() {
716        roundtrip(&Response::FingerprintAck);
717    }
718
719    #[test]
720    fn artifact_clone_shares_payload_via_arc() {
721        let artifact = ArtifactData {
722            outputs: vec![ArtifactOutput {
723                name: "test.o".into(),
724                data: Arc::new(vec![1, 2, 3, 4]),
725            }],
726            stdout: Arc::new(vec![5, 6]),
727            stderr: Arc::new(vec![7, 8]),
728            exit_code: 0,
729        };
730
731        let cloned = artifact.clone();
732
733        // Arc::clone bumps refcount — both point to the same allocation.
734        assert!(Arc::ptr_eq(
735            &artifact.outputs[0].data,
736            &cloned.outputs[0].data
737        ));
738        assert!(Arc::ptr_eq(&artifact.stdout, &cloned.stdout));
739        assert!(Arc::ptr_eq(&artifact.stderr, &cloned.stderr));
740    }
741
742    #[test]
743    fn arc_vec_u8_roundtrip_matches_plain_vec() {
744        // Prove Arc<Vec<u8>> serializes identically to Vec<u8>.
745        let plain: Vec<u8> = vec![0xDE, 0xAD, 0xBE, 0xEF];
746        let arc_wrapped: Arc<Vec<u8>> = Arc::new(plain.clone());
747
748        let plain_bytes = bincode::serialize(&plain).unwrap();
749        let arc_bytes = bincode::serialize(&arc_wrapped).unwrap();
750        assert_eq!(
751            plain_bytes, arc_bytes,
752            "Arc<Vec<u8>> must serialize identically to Vec<u8>"
753        );
754
755        // Deserialize Arc bytes back as plain Vec and vice versa.
756        let decoded_plain: Vec<u8> = bincode::deserialize(&arc_bytes).unwrap();
757        let decoded_arc: Arc<Vec<u8>> = bincode::deserialize(&plain_bytes).unwrap();
758        assert_eq!(decoded_plain, plain);
759        assert_eq!(*decoded_arc, plain);
760    }
761}