1use crate::{
5 CompatibleLifecycleOutcome, CompatibleStartOutcome, EMBEDDED_TRACE_API_VERSION,
6 EmbeddedTraceApi, EmbeddedTraceApiError, EmbeddedTraceDetail, EmbeddedTraceOutcome,
7 EmbeddedTracePage, EmbeddedTracePhase, EmbeddedTraceRecordInput, EmbeddedTraceSelectedTarget,
8 EmbedderCore, EmbedderError, EmbedderErrorCode, EventCallback, InstanceState, ShutdownOutcome,
9 SubmitOutcome, SubmitStatus, TraverseEmbedderApi,
10};
11use serde_json::{Value, json};
12use std::collections::BTreeMap;
13
14#[derive(Debug, Clone)]
16enum ScriptedResult {
17 Output(Value),
18 Error { code: String, message: String },
19}
20
21pub struct EmbedderTestDouble {
29 core: EmbedderCore,
30 scripted: BTreeMap<String, ScriptedResult>,
31}
32
33impl EmbedderTestDouble {
34 #[must_use]
37 pub fn new(
38 workspace_id: impl Into<String>,
39 app_id: impl Into<String>,
40 app_version: impl Into<String>,
41 platform: impl Into<String>,
42 ) -> Self {
43 Self {
44 core: EmbedderCore::new(
45 workspace_id.into(),
46 app_id.into(),
47 app_version.into(),
48 platform.into(),
49 BTreeMap::new(),
50 ),
51 scripted: BTreeMap::new(),
52 }
53 }
54
55 #[must_use]
57 pub fn with_target_output(mut self, target_id: impl Into<String>, output: Value) -> Self {
58 self.scripted
59 .insert(target_id.into(), ScriptedResult::Output(output));
60 self
61 }
62
63 #[must_use]
65 pub fn with_target_error(
66 mut self,
67 target_id: impl Into<String>,
68 code: impl Into<String>,
69 message: impl Into<String>,
70 ) -> Self {
71 self.scripted.insert(
72 target_id.into(),
73 ScriptedResult::Error {
74 code: code.into(),
75 message: message.into(),
76 },
77 );
78 self
79 }
80
81 #[must_use]
83 pub fn with_compatible_target(
84 mut self,
85 capability_id: impl Into<String>,
86 platforms: Vec<String>,
87 ) -> Self {
88 self.core
89 .compatible_targets
90 .insert(capability_id.into(), platforms);
91 self
92 }
93}
94
95impl TraverseEmbedderApi for EmbedderTestDouble {
96 fn submit(&mut self, target_id: &str, input: &Value) -> SubmitOutcome {
97 let _ = input;
98 if self.core.stopped {
99 let error = crate::runtime_stopped_error();
100 return self.core.rejected_submit(target_id, error);
101 }
102 let Some(result) = self.scripted.get(target_id).cloned() else {
103 let error = EmbedderError::new(
104 EmbedderErrorCode::TargetNotFound,
105 format!("'{target_id}' is neither a bundled workflow nor a bundled capability"),
106 );
107 return self.core.rejected_submit(target_id, error);
108 };
109
110 let session_id = self.core.next_session_id();
111 let request_id = self.core.next_request_id();
112 let execution_id = format!("exec_{request_id}");
113 let trace_input = match &result {
114 ScriptedResult::Output(_) => EmbeddedTraceRecordInput {
115 execution_id: execution_id.clone(),
116 target_id: target_id.to_string(),
117 outcome: EmbeddedTraceOutcome::Completed,
118 phases: vec![EmbeddedTracePhase {
119 code: "completed".to_string(),
120 }],
121 selected_target: Some(EmbeddedTraceSelectedTarget {
122 target_id: target_id.to_string(),
123 target_version: Some("1.0.0".to_string()),
124 }),
125 placement: None,
126 failure_code: None,
127 state_machine_valid: Some(true),
128 },
129 ScriptedResult::Error { code, .. } => EmbeddedTraceRecordInput {
130 execution_id: execution_id.clone(),
131 target_id: target_id.to_string(),
132 outcome: EmbeddedTraceOutcome::Error,
133 phases: vec![EmbeddedTracePhase {
134 code: "error".to_string(),
135 }],
136 selected_target: Some(EmbeddedTraceSelectedTarget {
137 target_id: target_id.to_string(),
138 target_version: Some("1.0.0".to_string()),
139 }),
140 placement: None,
141 failure_code: Some(code.clone()),
142 state_machine_valid: Some(true),
143 },
144 };
145 self.core.record_trace(trace_input);
146 self.core.emit(
147 "capability_invoked",
148 Some(&session_id),
149 json!({
150 "execution_id": execution_id,
151 "capability_id": target_id,
152 "capability_version": "1.0.0",
153 }),
154 );
155 match result {
156 ScriptedResult::Output(output) => {
157 self.core.emit(
158 "capability_result",
159 Some(&session_id),
160 json!({
161 "execution_id": execution_id,
162 "capability_id": target_id,
163 "status": "completed",
164 "output": output,
165 }),
166 );
167 }
168 ScriptedResult::Error { code, message } => {
169 self.core.emit(
170 "error",
171 Some(&session_id),
172 json!({
173 "execution_id": execution_id,
174 "capability_id": target_id,
175 "status": "error",
176 "error": { "code": code, "message": message, "details": {} },
177 }),
178 );
179 }
180 }
181 SubmitOutcome {
182 session_id: Some(session_id),
183 status: SubmitStatus::Accepted,
184 error: None,
185 }
186 }
187
188 fn subscribe(&mut self, callback: EventCallback) {
189 self.core.subscribe(callback);
190 }
191
192 fn start_compatible(&mut self, capability_id: &str, input: &Value) -> CompatibleStartOutcome {
193 self.core.start_compatible(capability_id, input)
194 }
195
196 fn stop_compatible(
197 &mut self,
198 capability_id: &str,
199 instance_id: Option<&str>,
200 ) -> CompatibleLifecycleOutcome {
201 self.core
202 .transition_compatible(capability_id, instance_id, InstanceState::Stopped)
203 }
204
205 fn kill_compatible(
206 &mut self,
207 capability_id: &str,
208 instance_id: Option<&str>,
209 ) -> CompatibleLifecycleOutcome {
210 self.core
211 .transition_compatible(capability_id, instance_id, InstanceState::Killed)
212 }
213
214 fn shutdown(&mut self) -> ShutdownOutcome {
215 self.core.shutdown()
216 }
217
218 fn release_evidence(&self) -> Value {
219 self.core.evidence("test-double", json!([]))
220 }
221}
222
223impl EmbeddedTraceApi for EmbedderTestDouble {
224 fn embedded_trace_api_version(&self) -> &'static str {
225 EMBEDDED_TRACE_API_VERSION
226 }
227
228 fn trace_list(
229 &self,
230 requested_version: &str,
231 page_size: usize,
232 cursor: Option<&str>,
233 ) -> Result<EmbeddedTracePage, EmbeddedTraceApiError> {
234 self.core.trace_list(requested_version, page_size, cursor)
235 }
236
237 fn trace_get(
238 &self,
239 requested_version: &str,
240 trace_id: &str,
241 ) -> Result<EmbeddedTraceDetail, EmbeddedTraceApiError> {
242 self.core.trace_get(requested_version, trace_id)
243 }
244}