Skip to main content

agent_sdk_core/testing/
hooks.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 hooks portion of that
5//! contract.
6//!
7use std::{
8    collections::VecDeque,
9    sync::{Arc, Mutex},
10};
11
12use crate::{
13    domain::AgentError,
14    hook_ports::{HookExecutionOutcome, HookExecutor},
15    package_hooks::{HookExecutorRef, HookInput, HookResponse},
16};
17
18#[derive(Clone, Debug)]
19/// In-memory scripted hook executor fixture for SDK conformance tests.
20/// Use it to script deterministic behavior in memory; any transcript or endpoint mutation is documented on the method that performs it.
21pub struct ScriptedHookExecutor {
22    executor_ref: HookExecutorRef,
23    outcomes: Arc<Mutex<VecDeque<Result<HookExecutionOutcome, AgentError>>>>,
24    invocations: Arc<Mutex<Vec<HookInput>>>,
25}
26
27impl ScriptedHookExecutor {
28    /// Creates a new testing::hooks value with explicit caller-provided
29    /// inputs. This constructor is data-only and performs no I/O or
30    /// external side effects.
31    pub fn new(
32        executor_ref: impl Into<String>,
33        outcomes: impl IntoIterator<Item = Result<HookExecutionOutcome, AgentError>>,
34    ) -> Self {
35        Self {
36            executor_ref: HookExecutorRef::new(executor_ref),
37            outcomes: Arc::new(Mutex::new(outcomes.into_iter().collect())),
38            invocations: Arc::new(Mutex::new(Vec::new())),
39        }
40    }
41
42    /// Builds the once value.
43    /// This is data construction and performs no I/O, journal append, event publication, or
44    /// process work.
45    pub fn once(executor_ref: impl Into<String>, response: HookResponse, elapsed_ms: u64) -> Self {
46        Self::new(
47            executor_ref,
48            [Ok(HookExecutionOutcome::new(response, elapsed_ms))],
49        )
50    }
51
52    /// Returns the invocations currently held by this value.
53    /// This configures deterministic in-memory test state only.
54    pub fn invocations(&self) -> Vec<HookInput> {
55        self.invocations
56            .lock()
57            .expect("hook invocations lock")
58            .clone()
59    }
60}
61
62impl HookExecutor for ScriptedHookExecutor {
63    fn executor_ref(&self) -> &HookExecutorRef {
64        &self.executor_ref
65    }
66
67    fn invoke(&self, input: HookInput) -> Result<HookExecutionOutcome, AgentError> {
68        self.invocations
69            .lock()
70            .expect("hook invocations lock")
71            .push(input);
72        self.outcomes
73            .lock()
74            .expect("hook outcomes lock")
75            .pop_front()
76            .unwrap_or_else(|| Err(AgentError::contract_violation("scripted hook exhausted")))
77    }
78}