agent_first_http/shared/
artifacts.rs1use serde::{Deserialize, Serialize};
9
10use crate::shared::ids::RequestId;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
18#[serde(rename_all = "snake_case")]
19pub enum Artifact {
20 Body,
21 RenderedHtml,
22 Text,
23 Screenshot,
24 Network,
25 Console,
26 Observation,
27 Storage,
29}
30
31impl Artifact {
32 pub const ALL: [Self; 7] = [
36 Self::Body,
37 Self::RenderedHtml,
38 Self::Text,
39 Self::Screenshot,
40 Self::Network,
41 Self::Console,
42 Self::Observation,
43 ];
44
45 #[must_use]
49 pub const fn filename_template(self) -> &'static str {
50 match self {
51 Self::Body => "body",
52 Self::RenderedHtml => "rendered.html",
53 Self::Text => "text.txt",
54 Self::Screenshot => "page.png",
55 Self::Network => "network.json",
56 Self::Console => "console.json",
57 Self::Observation => "observation.json",
58 Self::Storage => "storage.json",
59 }
60 }
61
62 #[must_use]
63 pub const fn as_str(self) -> &'static str {
64 match self {
65 Self::Body => "body",
66 Self::RenderedHtml => "rendered_html",
67 Self::Text => "text",
68 Self::Screenshot => "screenshot",
69 Self::Network => "network",
70 Self::Console => "console",
71 Self::Observation => "observation",
72 Self::Storage => "storage",
73 }
74 }
75}
76
77impl std::fmt::Display for Artifact {
78 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
79 f.write_str(self.as_str())
80 }
81}
82
83#[derive(Debug, Clone)]
86pub struct ArtifactPaths {
87 pub root: std::path::PathBuf,
88}
89
90impl ArtifactPaths {
91 #[must_use]
96 pub fn new(base: impl Into<std::path::PathBuf>, request_id: &RequestId) -> Self {
97 let base = crate::shared::path::absolute_lexical(base.into());
98 Self {
99 root: base.join(request_id.as_str()),
100 }
101 }
102
103 #[must_use]
106 pub fn file_for(&self, artifact: Artifact) -> std::path::PathBuf {
107 self.root.join(artifact.filename_template())
108 }
109
110 #[must_use]
114 pub fn network_bodies_dir(&self) -> std::path::PathBuf {
115 self.root.join("network-bodies")
116 }
117}
118
119#[cfg(test)]
120mod tests {
121 use super::*;
122
123 #[test]
124 fn all_seven_tokens_present() {
125 assert_eq!(Artifact::ALL.len(), 7);
126 assert!(!Artifact::ALL.contains(&Artifact::Storage));
128 }
129
130 #[test]
131 fn filename_templates_match_spec_table() {
132 let table = [
133 (Artifact::Body, "body"),
134 (Artifact::RenderedHtml, "rendered.html"),
135 (Artifact::Text, "text.txt"),
136 (Artifact::Screenshot, "page.png"),
137 (Artifact::Network, "network.json"),
138 (Artifact::Console, "console.json"),
139 (Artifact::Observation, "observation.json"),
140 (Artifact::Storage, "storage.json"),
141 ];
142 for (a, f) in table {
143 assert_eq!(a.filename_template(), f);
144 }
145 }
146
147 #[test]
148 fn artifact_paths_compose_under_request_id() {
149 let rid = RequestId("abc-123".into());
150 let paths = ArtifactPaths::new("/tmp/out", &rid);
151 assert_eq!(
152 paths.file_for(Artifact::Observation),
153 std::path::PathBuf::from("/tmp/out/abc-123/observation.json"),
154 );
155 assert_eq!(
156 paths.network_bodies_dir(),
157 std::path::PathBuf::from("/tmp/out/abc-123/network-bodies"),
158 );
159 }
160
161 #[test]
162 fn artifact_paths_absolutize_relative_out_dir() {
163 let rid = RequestId("abc-123".into());
164 let paths = ArtifactPaths::new("relative-out", &rid);
165 assert!(
166 paths.root.is_absolute(),
167 "artifact root must be absolute: {}",
168 paths.root.display()
169 );
170 }
171
172 #[test]
173 fn artifacts_serialize_to_snake_case() {
174 let all_including_storage: &[Artifact] = &[
175 Artifact::Body,
176 Artifact::RenderedHtml,
177 Artifact::Text,
178 Artifact::Screenshot,
179 Artifact::Network,
180 Artifact::Console,
181 Artifact::Observation,
182 Artifact::Storage,
183 ];
184 for a in all_including_storage {
185 let s = serde_json::to_string(a).unwrap_or_default();
186 assert_eq!(s, format!("\"{}\"", a.as_str()));
187 }
188 }
189}