use crate::{
CompatibleLifecycleOutcome, CompatibleStartOutcome, EMBEDDED_TRACE_API_VERSION,
EmbeddedTraceApi, EmbeddedTraceApiError, EmbeddedTraceDetail, EmbeddedTraceOutcome,
EmbeddedTracePage, EmbeddedTracePhase, EmbeddedTraceRecordInput, EmbeddedTraceSelectedTarget,
EmbedderCore, EmbedderError, EmbedderErrorCode, EventCallback, InstanceState, ShutdownOutcome,
SubmitOutcome, SubmitStatus, TraverseEmbedderApi,
};
use serde_json::{Value, json};
use std::collections::BTreeMap;
#[derive(Debug, Clone)]
enum ScriptedResult {
Output(Value),
Error { code: String, message: String },
}
pub struct EmbedderTestDouble {
core: EmbedderCore,
scripted: BTreeMap<String, ScriptedResult>,
}
impl EmbedderTestDouble {
#[must_use]
pub fn new(
workspace_id: impl Into<String>,
app_id: impl Into<String>,
app_version: impl Into<String>,
platform: impl Into<String>,
) -> Self {
Self {
core: EmbedderCore::new(
workspace_id.into(),
app_id.into(),
app_version.into(),
platform.into(),
BTreeMap::new(),
),
scripted: BTreeMap::new(),
}
}
#[must_use]
pub fn with_target_output(mut self, target_id: impl Into<String>, output: Value) -> Self {
self.scripted
.insert(target_id.into(), ScriptedResult::Output(output));
self
}
#[must_use]
pub fn with_target_error(
mut self,
target_id: impl Into<String>,
code: impl Into<String>,
message: impl Into<String>,
) -> Self {
self.scripted.insert(
target_id.into(),
ScriptedResult::Error {
code: code.into(),
message: message.into(),
},
);
self
}
#[must_use]
pub fn with_compatible_target(
mut self,
capability_id: impl Into<String>,
platforms: Vec<String>,
) -> Self {
self.core
.compatible_targets
.insert(capability_id.into(), platforms);
self
}
}
impl TraverseEmbedderApi for EmbedderTestDouble {
fn submit(&mut self, target_id: &str, input: &Value) -> SubmitOutcome {
let _ = input;
if self.core.stopped {
let error = crate::runtime_stopped_error();
return self.core.rejected_submit(target_id, error);
}
let Some(result) = self.scripted.get(target_id).cloned() else {
let error = EmbedderError::new(
EmbedderErrorCode::TargetNotFound,
format!("'{target_id}' is neither a bundled workflow nor a bundled capability"),
);
return self.core.rejected_submit(target_id, error);
};
let session_id = self.core.next_session_id();
let request_id = self.core.next_request_id();
let execution_id = format!("exec_{request_id}");
let trace_input = match &result {
ScriptedResult::Output(_) => EmbeddedTraceRecordInput {
execution_id: execution_id.clone(),
target_id: target_id.to_string(),
outcome: EmbeddedTraceOutcome::Completed,
phases: vec![EmbeddedTracePhase {
code: "completed".to_string(),
}],
selected_target: Some(EmbeddedTraceSelectedTarget {
target_id: target_id.to_string(),
target_version: Some("1.0.0".to_string()),
}),
placement: None,
failure_code: None,
state_machine_valid: Some(true),
},
ScriptedResult::Error { code, .. } => EmbeddedTraceRecordInput {
execution_id: execution_id.clone(),
target_id: target_id.to_string(),
outcome: EmbeddedTraceOutcome::Error,
phases: vec![EmbeddedTracePhase {
code: "error".to_string(),
}],
selected_target: Some(EmbeddedTraceSelectedTarget {
target_id: target_id.to_string(),
target_version: Some("1.0.0".to_string()),
}),
placement: None,
failure_code: Some(code.clone()),
state_machine_valid: Some(true),
},
};
self.core.record_trace(trace_input);
self.core.emit(
"capability_invoked",
Some(&session_id),
json!({
"execution_id": execution_id,
"capability_id": target_id,
"capability_version": "1.0.0",
}),
);
match result {
ScriptedResult::Output(output) => {
self.core.emit(
"capability_result",
Some(&session_id),
json!({
"execution_id": execution_id,
"capability_id": target_id,
"status": "completed",
"output": output,
}),
);
}
ScriptedResult::Error { code, message } => {
self.core.emit(
"error",
Some(&session_id),
json!({
"execution_id": execution_id,
"capability_id": target_id,
"status": "error",
"error": { "code": code, "message": message, "details": {} },
}),
);
}
}
SubmitOutcome {
session_id: Some(session_id),
status: SubmitStatus::Accepted,
error: None,
}
}
fn subscribe(&mut self, callback: EventCallback) {
self.core.subscribe(callback);
}
fn start_compatible(&mut self, capability_id: &str, input: &Value) -> CompatibleStartOutcome {
self.core.start_compatible(capability_id, input)
}
fn stop_compatible(
&mut self,
capability_id: &str,
instance_id: Option<&str>,
) -> CompatibleLifecycleOutcome {
self.core
.transition_compatible(capability_id, instance_id, InstanceState::Stopped)
}
fn kill_compatible(
&mut self,
capability_id: &str,
instance_id: Option<&str>,
) -> CompatibleLifecycleOutcome {
self.core
.transition_compatible(capability_id, instance_id, InstanceState::Killed)
}
fn shutdown(&mut self) -> ShutdownOutcome {
self.core.shutdown()
}
fn release_evidence(&self) -> Value {
self.core.evidence("test-double", json!([]))
}
}
impl EmbeddedTraceApi for EmbedderTestDouble {
fn embedded_trace_api_version(&self) -> &'static str {
EMBEDDED_TRACE_API_VERSION
}
fn trace_list(
&self,
requested_version: &str,
page_size: usize,
cursor: Option<&str>,
) -> Result<EmbeddedTracePage, EmbeddedTraceApiError> {
self.core.trace_list(requested_version, page_size, cursor)
}
fn trace_get(
&self,
requested_version: &str,
trace_id: &str,
) -> Result<EmbeddedTraceDetail, EmbeddedTraceApiError> {
self.core.trace_get(requested_version, trace_id)
}
}