1use std::time::Duration;
4
5use serde::{Deserialize, Deserializer, Serialize, Serializer};
6use serde_json::{Map, Value};
7
8use crate::{agent::verdict::ValidatedResult, runtime::Verdict};
9
10#[derive(Debug, Clone, PartialEq)]
13pub enum ExecutionResult {
14 Owner(ValidatedResult),
15 Runtime(RuntimeResult),
16}
17
18#[derive(Debug, Clone, PartialEq, Default)]
20pub enum Field<T> {
21 #[default]
22 Missing,
23 Null,
24 Value(T),
25}
26
27impl<T> Field<T> {
28 pub fn value(&self) -> Option<&T> {
29 match self {
30 Self::Value(value) => Some(value),
31 Self::Missing | Self::Null => None,
32 }
33 }
34}
35
36#[derive(Debug, Clone, PartialEq)]
37pub struct RuntimeResult {
38 pub verdict: Verdict,
39 pub exit_code: Field<i32>,
40 pub duration: Field<Duration>,
41 pub truncated: Field<bool>,
42 pub stdout: Field<String>,
43 pub stderr: Field<String>,
44 pub fields: Map<String, Value>,
46}
47
48impl RuntimeResult {
49 pub fn summary(&self) -> Self {
50 Self {
51 stdout: Field::Missing,
52 stderr: Field::Missing,
53 fields: Map::new(),
54 exit_code: if matches!(self.exit_code, Field::Missing) {
55 Field::Null
56 } else {
57 self.exit_code.clone()
58 },
59 duration: if matches!(self.duration, Field::Missing) {
60 Field::Null
61 } else {
62 self.duration.clone()
63 },
64 truncated: if matches!(self.truncated, Field::Missing) {
65 Field::Null
66 } else {
67 self.truncated.clone()
68 },
69 verdict: self.verdict,
70 }
71 }
72}
73
74fn take<T: serde::de::DeserializeOwned>(
75 fields: &mut Map<String, Value>,
76 key: &str,
77) -> Result<Field<T>, serde_json::Error> {
78 match fields.remove(key) {
79 None => Ok(Field::Missing),
80 Some(Value::Null) => Ok(Field::Null),
81 Some(value) => serde_json::from_value(value).map(Field::Value),
82 }
83}
84
85fn insert<T: Serialize>(fields: &mut Map<String, Value>, key: &str, value: &Field<T>) {
86 match value {
87 Field::Missing => {}
88 Field::Null => {
89 fields.insert(key.into(), Value::Null);
90 }
91 Field::Value(value) => {
92 fields.insert(
93 key.into(),
94 serde_json::to_value(value).expect("result field is JSON"),
95 );
96 }
97 }
98}
99
100impl ExecutionResult {
101 pub fn verdict(&self) -> Verdict {
102 match self {
103 Self::Owner(result) => result.verdict,
104 Self::Runtime(result) => result.verdict,
105 }
106 }
107
108 pub fn owner_fields(&self) -> &Map<String, Value> {
109 match self {
110 Self::Owner(result) => &result.fields,
111 Self::Runtime(result) => &result.fields,
112 }
113 }
114
115 pub fn to_json(&self) -> Value {
117 let mut fields = self.owner_fields().clone();
118 fields.insert(
119 "verdict".into(),
120 serde_json::to_value(self.verdict()).expect("verdict is JSON"),
121 );
122 if let Self::Runtime(result) = self {
123 insert(&mut fields, "exitCode", &result.exit_code);
124 let duration = match result.duration {
125 Field::Missing => Field::Missing,
126 Field::Null => Field::Null,
127 Field::Value(duration) => Field::Value(
128 u64::try_from(duration.as_millis())
129 .expect("saved runtime duration fits u64 milliseconds"),
130 ),
131 };
132 insert(&mut fields, "durationMs", &duration);
133 insert(&mut fields, "truncated", &result.truncated);
134 insert(&mut fields, "stdout", &result.stdout);
135 insert(&mut fields, "stderr", &result.stderr);
136 }
137 Value::Object(fields)
138 }
139}
140
141impl From<ValidatedResult> for ExecutionResult {
142 fn from(result: ValidatedResult) -> Self {
143 Self::Owner(result)
144 }
145}
146
147impl TryFrom<Value> for ExecutionResult {
148 type Error = serde_json::Error;
149 fn try_from(value: Value) -> Result<Self, Self::Error> {
150 let mut fields: Map<String, Value> = serde_json::from_value(value)?;
151 let verdict = serde_json::from_value(fields.remove("verdict").unwrap_or(Value::Null))?;
152 if ["exitCode", "durationMs", "stdout", "stderr"]
155 .iter()
156 .any(|key| fields.contains_key(*key))
157 {
158 let duration = match take::<u64>(&mut fields, "durationMs")? {
159 Field::Missing => Field::Missing,
160 Field::Null => Field::Null,
161 Field::Value(ms) => Field::Value(Duration::from_millis(ms)),
162 };
163 Ok(Self::Runtime(RuntimeResult {
164 verdict,
165 exit_code: take(&mut fields, "exitCode")?,
166 duration,
167 truncated: take(&mut fields, "truncated")?,
168 stdout: take(&mut fields, "stdout")?,
169 stderr: take(&mut fields, "stderr")?,
170 fields,
171 }))
172 } else {
173 Ok(Self::Owner(ValidatedResult { verdict, fields }))
174 }
175 }
176}
177
178impl Serialize for ExecutionResult {
179 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
180 self.to_json().serialize(serializer)
181 }
182}
183impl<'de> Deserialize<'de> for ExecutionResult {
184 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
185 Self::try_from(Value::deserialize(deserializer)?).map_err(serde::de::Error::custom)
186 }
187}
188
189#[cfg(test)]
190mod tests {
191 use super::*;
192 use serde_json::json;
193
194 #[test]
195 fn saved_envelopes_round_trip_without_tags_or_field_changes() {
196 for value in [
197 json!({"verdict":"GREEN","approved":true,"nested":{"a":[1,null]},"truncated":"owner"}),
198 json!({"verdict":"RED","exitCode":7,"durationMs":123,"stdout":"out","stderr":"err","truncated":true}),
199 json!({"verdict":"RED","exitCode":7,"durationMs":123,"truncated":false}),
200 json!({"verdict":"RED","exitCode":null,"durationMs":null,"truncated":null,"future":[]}),
201 json!({"verdict":"GREEN","durationMs":u64::MAX}),
202 ] {
203 let text = serde_json::to_string(&value).unwrap();
204 let result: ExecutionResult = serde_json::from_str(&text).unwrap();
205 assert_eq!(serde_json::to_string(&result).unwrap(), text);
206 }
207 }
208
209 #[test]
210 fn verdict_and_runtime_metadata_are_parsed_at_the_edge() {
211 for value in [
212 json!({"verdict":"ERROR"}),
213 json!({"verdict":"GREEN","exitCode":"0"}),
214 json!({"verdict":"RED","durationMs":-1}),
215 json!({"verdict":"RED","stdout":[],"truncated":false}),
216 ] {
217 assert!(ExecutionResult::try_from(value).is_err());
218 }
219 let result = ExecutionResult::try_from(json!({"verdict":"RED","exitCode":7,"durationMs":123,"truncated":true,"stdout":"out","stderr":"err"})).unwrap();
220 let ExecutionResult::Runtime(runtime) = result else {
221 panic!("runtime result");
222 };
223 assert_eq!(runtime.duration.value(), Some(&Duration::from_millis(123)));
224 assert_eq!(
225 ExecutionResult::Runtime(runtime.summary()).to_json(),
226 json!({"verdict":"RED","exitCode":7,"durationMs":123,"truncated":true})
227 );
228 }
229}