1use serde::{Deserialize, Serialize};
3use serde_json::{json, Value};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
6#[serde(rename_all = "snake_case")]
7pub enum ExecutionStatus {
8 NotDispatched,
9 Completed,
10 Failed,
11 OutcomeUnknown,
12}
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
14#[serde(rename_all = "snake_case")]
15pub enum VerificationStatus {
16 NotRequested,
17 Passed,
18 Failed,
19 Inconclusive,
20}
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
22#[serde(rename_all = "snake_case")]
23pub enum EffectStatus {
24 None,
25 Possible,
26 Confirmed,
27}
28
29#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
30#[serde(rename_all = "camelCase", deny_unknown_fields)]
31pub struct WorkflowError {
32 pub code: String,
33 pub stage: String,
34 pub message: String,
35 #[serde(default)]
36 pub retryable_before_dispatch: bool,
37}
38impl WorkflowError {
39 pub fn new(
40 code: impl Into<String>,
41 stage: impl Into<String>,
42 message: impl Into<String>,
43 ) -> Self {
44 Self {
45 code: code.into(),
46 stage: stage.into(),
47 message: message.into(),
48 retryable_before_dispatch: false,
49 }
50 }
51}
52impl std::fmt::Display for WorkflowError {
53 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54 write!(f, "{}: {}", self.code, self.message)
55 }
56}
57impl std::error::Error for WorkflowError {}
58
59#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
60#[serde(rename_all = "camelCase")]
61pub struct ExecutionOutcome {
62 pub execution: ExecutionStatus,
63 pub verification: VerificationStatus,
64 pub effect: EffectStatus,
65 #[serde(default, skip_serializing_if = "Option::is_none")]
66 pub effect_scope: Option<String>,
67 #[serde(default)]
68 pub data: Value,
69 #[serde(default, skip_serializing_if = "Option::is_none")]
70 pub error: Option<WorkflowError>,
71 #[serde(default)]
72 pub timing: Value,
73 #[serde(default)]
74 pub dispatch: Value,
75 #[serde(default)]
76 pub evidence_refs: Vec<String>,
77 #[serde(default)]
78 pub coverage: Value,
79 #[serde(default)]
80 pub warnings: Vec<String>,
81}
82impl ExecutionOutcome {
83 pub fn completed(data: Value, effect: EffectStatus) -> Self {
84 Self {
85 execution: ExecutionStatus::Completed,
86 verification: VerificationStatus::NotRequested,
87 effect,
88 effect_scope: None,
89 data,
90 error: None,
91 timing: json!({}),
92 dispatch: json!({}),
93 evidence_refs: Vec::new(),
94 coverage: json!({"businessPersistence":"unobserved"}),
95 warnings: Vec::new(),
96 }
97 }
98 pub fn not_dispatched(error: WorkflowError) -> Self {
99 Self {
100 execution: ExecutionStatus::NotDispatched,
101 error: Some(error),
102 ..Self::completed(Value::Null, EffectStatus::None)
103 }
104 }
105 pub fn failed(error: WorkflowError, effect: EffectStatus) -> Self {
106 Self {
107 execution: ExecutionStatus::Failed,
108 error: Some(error),
109 ..Self::completed(Value::Null, effect)
110 }
111 }
112 pub fn unknown(error: WorkflowError) -> Self {
113 Self {
114 execution: ExecutionStatus::OutcomeUnknown,
115 error: Some(error),
116 ..Self::completed(Value::Null, EffectStatus::Possible)
117 }
118 }
119 pub fn is_success(&self, expect: bool) -> bool {
121 self.execution == ExecutionStatus::Completed
122 && self.error.is_none()
123 && if expect {
124 self.verification == VerificationStatus::Passed
125 } else {
126 matches!(
127 self.verification,
128 VerificationStatus::NotRequested | VerificationStatus::Passed
129 )
130 }
131 }
132}
133
134pub const ERROR_CODES: &[&str] = &[
136 "invalid_spec",
137 "capability_unavailable",
138 "unsupported_feature",
139 "unsupported_condition",
140 "target_not_found",
141 "ambiguous_target",
142 "target_changed",
143 "stale_ref",
144 "not_actionable",
145 "binding_missing",
146 "binding_type_mismatch",
147 "precondition_failed",
148 "postcondition_failed",
149 "condition_timeout",
150 "execution_failed",
151 "outcome_unknown",
152 "run_key_conflict",
153 "run_not_found",
154 "run_expired",
155 "stale_checkpoint",
156 "resume_not_safe",
157 "app_instance_changed",
158 "resume_requires_inputs",
159 "observation_failed",
160 "cancel_requested",
161 "cancelled_before_dispatch",
162 "persistence_unavailable",
163 "evidence_persistence_failed",
164 "resource_busy",
165 "capture_incomplete",
166 "protocol_mismatch",
167 "unauthorized",
168];
169
170pub fn legacy_outcome(tool: &str, value: Value) -> ExecutionOutcome {
173 let canonical = tool.strip_prefix("webview_").unwrap_or(tool);
174 let mut out = ExecutionOutcome::completed(value, EffectStatus::Possible);
175 if matches!(canonical, "wait_for" | "wait") {
176 out.effect = EffectStatus::None;
177 if out.data.get("timeout").and_then(Value::as_bool) == Some(true)
178 && out.data.get("found").and_then(Value::as_bool) == Some(false)
179 {
180 out.execution = ExecutionStatus::Failed;
181 out.error = Some(WorkflowError::new(
182 "condition_timeout",
183 "waiting",
184 "Wait condition timed out",
185 ));
186 }
187 }
188 if canonical == "act_and_verify" {
189 match out.data.get("verdict").and_then(Value::as_str) {
190 Some("failed") => {
191 out.verification = VerificationStatus::Failed;
192 out.error = Some(WorkflowError::new(
193 "postcondition_failed",
194 "verifying",
195 "Action verification failed",
196 ));
197 }
198 Some("passed") => out.verification = VerificationStatus::Passed,
199 Some("inconclusive") => out.verification = VerificationStatus::Inconclusive,
200 _ => {}
201 }
202 }
203 out
204}
205
206#[cfg(test)]
207mod tests {
208 use super::*;
209 #[test]
210 fn business_json_does_not_decide_success() {
211 for data in [
212 json!({"error":"saved text"}),
213 json!({"found":false,"timeout":true}),
214 json!({"verdict":"failed"}),
215 json!({"ok":false}),
216 ] {
217 assert!(legacy_outcome("query", data).is_success(false));
218 }
219 }
220 #[test]
221 fn known_wait_and_verification_failures_propagate() {
222 assert!(
223 !legacy_outcome("webview_wait_for", json!({"found":false,"timeout":true}))
224 .is_success(false)
225 );
226 let outcome = legacy_outcome("webview_act_and_verify", json!({"verdict":"failed"}));
227 assert_eq!(outcome.execution, ExecutionStatus::Completed);
228 assert_eq!(outcome.verification, VerificationStatus::Failed);
229 assert!(!outcome.is_success(false));
230 }
231 #[test]
232 fn undispatched_verified_state_is_not_success() {
233 let mut out = ExecutionOutcome::not_dispatched(WorkflowError::new(
234 "not_actionable",
235 "preparing",
236 "disabled",
237 ));
238 out.verification = VerificationStatus::Passed;
239 assert!(!out.is_success(true));
240 }
241 #[test]
242 fn error_codes_are_stable_and_unique() {
243 let unique: std::collections::HashSet<_> = ERROR_CODES.iter().collect();
244 assert_eq!(unique.len(), ERROR_CODES.len());
245 assert!(ERROR_CODES.contains(&"outcome_unknown"));
246 assert_eq!(
247 serde_json::to_value(ExecutionStatus::OutcomeUnknown).unwrap(),
248 json!("outcome_unknown")
249 );
250 }
251}