Skip to main content

agent_sdk_core/testing/
output_delivery.rs

1//! Deterministic test-kit helpers for SDK consumers. Use these fakes and harnesses to
2//! exercise public contracts without live providers, real stores, product UI, network
3//! telemetry, or wall-clock-dependent infrastructure. They mutate only their
4//! in-memory state unless noted. This file contains the output delivery portion of
5//! that contract.
6//!
7use std::sync::{Arc, Mutex};
8
9use crate::{
10    domain::{AgentError, AgentErrorKind, RetryClassification},
11    output_delivery::{OutputDeliveryReceipt, OutputDeliveryRequest, OutputSinkRef},
12    output_delivery_port::{OutputSink, OutputSinkCapabilities},
13};
14
15#[derive(Clone)]
16/// In-memory scripted output sink fixture for SDK conformance tests.
17/// Use it to script deterministic behavior in memory; any transcript or endpoint mutation is documented on the method that performs it.
18pub struct ScriptedOutputSink {
19    sink_ref: OutputSinkRef,
20    capabilities: OutputSinkCapabilities,
21    calls: Arc<Mutex<Vec<OutputDeliveryRequest>>>,
22    next_receipts: Arc<Mutex<Vec<Result<OutputDeliveryReceipt, AgentError>>>>,
23}
24
25impl ScriptedOutputSink {
26    /// Creates a new testing::output_delivery value with explicit
27    /// caller-provided inputs. This constructor is data-only and
28    /// performs no I/O or external side effects.
29    ///
30    /// # Panics
31    ///
32    /// Panics if constructor invariants fail, such as invalid identifier
33    /// text or constructor-specific bounds. Use a fallible constructor such as
34    /// `try_new` when one is available for untrusted input.
35    pub fn new(sink_ref: OutputSinkRef, capabilities: OutputSinkCapabilities) -> Self {
36        Self {
37            sink_ref,
38            capabilities,
39            calls: Arc::new(Mutex::new(Vec::new())),
40            next_receipts: Arc::new(Mutex::new(Vec::new())),
41        }
42    }
43
44    /// Push receipt.
45    /// This reads or mutates deterministic in-memory test state unless the method explicitly
46    /// names a fixture file.
47    pub fn push_receipt(&self, receipt: Result<OutputDeliveryReceipt, AgentError>) {
48        self.next_receipts
49            .lock()
50            .expect("output sink receipt lock")
51            .push(receipt);
52    }
53
54    /// Operates on in-memory or journal-derived testing::output_delivery
55    /// state for diagnostics and repair evidence. It does not create a second
56    /// run loop or product workflow owner.
57    pub fn calls(&self) -> Vec<OutputDeliveryRequest> {
58        self.calls.lock().expect("output sink calls lock").clone()
59    }
60
61    fn next_receipt(
62        &self,
63        request: OutputDeliveryRequest,
64    ) -> Result<OutputDeliveryReceipt, AgentError> {
65        self.calls
66            .lock()
67            .expect("output sink calls lock")
68            .push(request.clone());
69        self.next_receipts
70            .lock()
71            .expect("output sink receipt lock")
72            .pop()
73            .unwrap_or_else(|| {
74                Ok(OutputDeliveryReceipt::completed(
75                    request.delivery_id,
76                    "ack.output_delivery.fake",
77                ))
78            })
79    }
80}
81
82impl OutputSink for ScriptedOutputSink {
83    fn sink_ref(&self) -> OutputSinkRef {
84        self.sink_ref.clone()
85    }
86
87    fn capabilities(&self) -> OutputSinkCapabilities {
88        self.capabilities.clone()
89    }
90
91    fn send_chunk(
92        &self,
93        request: OutputDeliveryRequest,
94    ) -> Result<OutputDeliveryReceipt, AgentError> {
95        if !self.capabilities.supports_chunks {
96            return Err(AgentError::new(
97                AgentErrorKind::HostConfigurationNeeded,
98                RetryClassification::HostConfigurationNeeded,
99                "output sink does not support chunks",
100            ));
101        }
102        self.next_receipt(request)
103    }
104
105    fn send_final(
106        &self,
107        request: OutputDeliveryRequest,
108    ) -> Result<OutputDeliveryReceipt, AgentError> {
109        self.next_receipt(request)
110    }
111
112    fn reconcile(
113        &self,
114        request: OutputDeliveryRequest,
115    ) -> Result<OutputDeliveryReceipt, AgentError> {
116        self.next_receipt(request)
117    }
118}