1use crate::{
5 CompatibleLifecycleOutcome, CompatibleStartOutcome, EmbedderCore, EmbedderError,
6 EmbedderErrorCode, EventCallback, InstanceState, ShutdownOutcome, SubmitOutcome, SubmitStatus,
7 TraverseEmbedderApi,
8};
9use serde_json::{Value, json};
10use std::collections::BTreeMap;
11
12#[derive(Debug, Clone)]
14enum ScriptedResult {
15 Output(Value),
16 Error { code: String, message: String },
17}
18
19pub struct EmbedderTestDouble {
27 core: EmbedderCore,
28 scripted: BTreeMap<String, ScriptedResult>,
29}
30
31impl EmbedderTestDouble {
32 #[must_use]
35 pub fn new(
36 workspace_id: impl Into<String>,
37 app_id: impl Into<String>,
38 app_version: impl Into<String>,
39 platform: impl Into<String>,
40 ) -> Self {
41 Self {
42 core: EmbedderCore::new(
43 workspace_id.into(),
44 app_id.into(),
45 app_version.into(),
46 platform.into(),
47 BTreeMap::new(),
48 ),
49 scripted: BTreeMap::new(),
50 }
51 }
52
53 #[must_use]
55 pub fn with_target_output(mut self, target_id: impl Into<String>, output: Value) -> Self {
56 self.scripted
57 .insert(target_id.into(), ScriptedResult::Output(output));
58 self
59 }
60
61 #[must_use]
63 pub fn with_target_error(
64 mut self,
65 target_id: impl Into<String>,
66 code: impl Into<String>,
67 message: impl Into<String>,
68 ) -> Self {
69 self.scripted.insert(
70 target_id.into(),
71 ScriptedResult::Error {
72 code: code.into(),
73 message: message.into(),
74 },
75 );
76 self
77 }
78
79 #[must_use]
81 pub fn with_compatible_target(
82 mut self,
83 capability_id: impl Into<String>,
84 platforms: Vec<String>,
85 ) -> Self {
86 self.core
87 .compatible_targets
88 .insert(capability_id.into(), platforms);
89 self
90 }
91}
92
93impl TraverseEmbedderApi for EmbedderTestDouble {
94 fn submit(&mut self, target_id: &str, input: &Value) -> SubmitOutcome {
95 let _ = input;
96 if self.core.stopped {
97 let error = crate::runtime_stopped_error();
98 return self.core.rejected_submit(target_id, error);
99 }
100 let Some(result) = self.scripted.get(target_id).cloned() else {
101 let error = EmbedderError::new(
102 EmbedderErrorCode::TargetNotFound,
103 format!("'{target_id}' is neither a bundled workflow nor a bundled capability"),
104 );
105 return self.core.rejected_submit(target_id, error);
106 };
107
108 let session_id = self.core.next_session_id();
109 let request_id = self.core.next_request_id();
110 let execution_id = format!("exec_{request_id}");
111 self.core.emit(
112 "capability_invoked",
113 Some(&session_id),
114 json!({
115 "execution_id": execution_id,
116 "capability_id": target_id,
117 "capability_version": "1.0.0",
118 }),
119 );
120 match result {
121 ScriptedResult::Output(output) => {
122 self.core.emit(
123 "capability_result",
124 Some(&session_id),
125 json!({
126 "execution_id": execution_id,
127 "capability_id": target_id,
128 "status": "completed",
129 "output": output,
130 }),
131 );
132 }
133 ScriptedResult::Error { code, message } => {
134 self.core.emit(
135 "error",
136 Some(&session_id),
137 json!({
138 "execution_id": execution_id,
139 "capability_id": target_id,
140 "status": "error",
141 "error": { "code": code, "message": message, "details": {} },
142 }),
143 );
144 }
145 }
146 SubmitOutcome {
147 session_id: Some(session_id),
148 status: SubmitStatus::Accepted,
149 error: None,
150 }
151 }
152
153 fn subscribe(&mut self, callback: EventCallback) {
154 self.core.subscribe(callback);
155 }
156
157 fn start_compatible(&mut self, capability_id: &str, input: &Value) -> CompatibleStartOutcome {
158 self.core.start_compatible(capability_id, input)
159 }
160
161 fn stop_compatible(
162 &mut self,
163 capability_id: &str,
164 instance_id: Option<&str>,
165 ) -> CompatibleLifecycleOutcome {
166 self.core
167 .transition_compatible(capability_id, instance_id, InstanceState::Stopped)
168 }
169
170 fn kill_compatible(
171 &mut self,
172 capability_id: &str,
173 instance_id: Option<&str>,
174 ) -> CompatibleLifecycleOutcome {
175 self.core
176 .transition_compatible(capability_id, instance_id, InstanceState::Killed)
177 }
178
179 fn shutdown(&mut self) -> ShutdownOutcome {
180 self.core.shutdown()
181 }
182
183 fn release_evidence(&self) -> Value {
184 self.core.evidence("test-double", json!([]))
185 }
186}