use serde::{Deserialize, Serialize};
use std::sync::Arc;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionSnapshot {
pub session_id: String,
pub pid: u32,
pub cwd: String,
pub project_name: String,
pub status: String,
pub cost_usd: f64,
pub context_tokens: u64,
pub context_max: u64,
pub last_message_ts: u64,
}
pub trait SessionSource: Send + Sync {
fn list(&self) -> Vec<SessionSnapshot>;
fn detail(&self, session_id: &str) -> Option<SessionSnapshot> {
self.list().into_iter().find(|s| s.session_id == session_id)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum BrainGateMode {
On,
Off,
Auto,
}
impl BrainGateMode {
pub fn as_str(&self) -> &'static str {
match self {
Self::On => "on",
Self::Off => "off",
Self::Auto => "auto",
}
}
}
impl std::fmt::Display for BrainGateMode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DecisionSummary {
pub id: String,
pub timestamp: String,
pub action: String,
pub confidence: Option<f64>,
pub project: Option<String>,
pub tool: Option<String>,
#[serde(default)]
pub pid: u32,
#[serde(default)]
pub command: Option<String>,
#[serde(default)]
pub reasoning: Option<String>,
#[serde(default)]
pub user_action: Option<String>,
#[serde(default)]
pub override_reason: Option<String>,
#[serde(default)]
pub brain_decision_ms: Option<u64>,
#[serde(default)]
pub canonical: Option<bool>,
#[serde(default)]
pub cache_hit: Option<bool>,
#[serde(default)]
pub cost_usd: Option<f64>,
#[serde(default)]
pub model: Option<String>,
#[serde(default)]
pub outcome_kind: Option<String>,
#[serde(default)]
pub outcome_detail: Option<String>,
#[serde(default)]
pub suggested_at: Option<u64>,
#[serde(default)]
pub resolved_at: Option<u64>,
#[serde(default)]
pub why: String,
#[serde(default)]
pub decision_source: Option<String>,
#[serde(default)]
pub rule_name: Option<String>,
}
impl DecisionSummary {
pub fn is_positive(&self) -> bool {
matches!(
self.user_action.as_deref(),
Some("accept" | "auto" | "user_approve" | "rule_approve")
)
}
pub fn is_negative(&self) -> bool {
matches!(
self.user_action.as_deref(),
Some("reject" | "deny_rule_override" | "rule_deny" | "conflict_deny")
)
}
}
pub trait BrainView: Send + Sync {
fn gate_mode(&self) -> BrainGateMode;
fn recent_decisions(&self, n: usize) -> Vec<DecisionSummary>;
fn decision_count(&self) -> usize;
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReviewItemSummary {
pub decision: DecisionSummary,
pub reason: String,
pub score: f64,
}
pub trait BrainReviewView: Send + Sync {
fn all_decisions(&self) -> Vec<DecisionSummary>;
fn review_queue(&self) -> Vec<ReviewItemSummary>;
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LeaseSummary {
pub id: String,
pub owner_session_id: String,
pub resource_kind: String,
pub resource_value: String,
pub mode: String,
pub acquired_at: String,
#[serde(default)]
pub expires_at: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HandoffSummary {
pub id: String,
pub from_session_id: String,
pub to_session_id: Option<String>,
pub task_id: String,
pub summary: String,
pub priority: String,
pub created_at: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InterruptSummary {
pub id: String,
pub interrupt_type: String,
pub priority: String,
pub target_session_id: String,
pub reason: String,
pub created_at: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TaskSummary {
pub id: String,
pub name: String,
pub state: String,
pub role: Option<String>,
pub attempts: u32,
pub max_retries: u32,
pub last_session_id: Option<String>,
#[serde(default)]
pub last_verdict: Option<String>,
#[serde(default)]
pub cost_usd: f64,
pub created_at: String,
pub updated_at: String,
}
pub trait CoordView: Send + Sync {
fn active_leases(&self) -> Vec<LeaseSummary>;
fn pending_handoffs(&self) -> Vec<HandoffSummary>;
fn pending_interrupts(&self) -> Vec<InterruptSummary>;
fn tasks(&self) -> Vec<TaskSummary>;
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentDirectoryEntry {
pub session_id: String,
pub pid: u32,
pub cwd: String,
pub project: String,
pub status: String,
pub role: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RoleBinding {
pub name: String,
pub cwd_selector: String,
pub last_session_id: Option<String>,
pub last_seen: String,
pub pid: Option<u32>,
}
pub trait BusView: Send + Sync {
fn list_agents(&self) -> Vec<AgentDirectoryEntry>;
fn list_roles(&self) -> Vec<RoleBinding>;
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ObservationInput {
pub session_pid: u32,
pub project: String,
pub tool: Option<String>,
pub command: Option<String>,
pub observed_action: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum DecisionScope {
Session,
Orchestration,
}
impl DecisionScope {
pub fn as_str(&self) -> &'static str {
match self {
Self::Session => "session",
Self::Orchestration => "orchestration",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LogDecisionInput {
pub session_pid: u32,
pub project: String,
pub tool: Option<String>,
pub command: Option<String>,
pub suggestion: PendingSuggestion,
pub user_action: String,
pub decision_type: DecisionScope,
pub override_reason: Option<String>,
}
pub trait Actions: Send + Sync {
fn terminate_session(&self, pid: u32) -> Result<(), String>;
fn inject_text(&self, session_id: &str, text: &str) -> Result<(), String>;
fn set_gate_mode(&self, mode: BrainGateMode) -> Result<(), String>;
fn log_observation(&self, observation: ObservationInput) -> Result<(), String>;
fn log_decision(&self, input: LogDecisionInput) -> Result<(), String>;
fn mark_canonical(&self, decision_id: &str, note: Option<String>) -> Result<(), String>;
fn bind_bus_role(&self, name: &str, cwd: &str, pid: u32) -> Result<(), String>;
fn cancel_task(&self, task_id: &str) -> Result<(), String>;
fn set_supervisor_drain(&self, draining: bool) -> Result<(), String>;
fn retry_task(&self, task_id: &str) -> Result<(), String>;
fn approve_task(&self, task_id: &str) -> Result<(), String>;
fn send_session_to_peer(
&self,
session_id: &str,
transcript: Option<&std::path::Path>,
cwd: &str,
peer: &str,
) -> Result<String, String>;
}
#[derive(Debug, Clone, Default)]
pub struct HiveViewSnapshot {
pub identity: Option<String>,
pub peers: Vec<(String, Option<String>)>,
}
pub trait HiveActions: Send + Sync {
fn shared_skill_keys(&self) -> std::collections::HashSet<String>;
fn share_skill(&self, skill: &crate::skills::DiscoveredSkill) -> Result<String, String>;
fn hive_view_snapshot(&self) -> HiveViewSnapshot;
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PendingSuggestion {
pub pid: u32,
pub action: String,
pub message: Option<String>,
pub reasoning: String,
pub confidence: f64,
pub suggested_at: u64,
}
pub trait BrainDriver: Send {
fn tick(
&mut self,
sessions: &[SessionSnapshot],
deny_rules: &[crate::rules::AutoRule],
) -> Vec<(u32, String)>;
fn cleanup(&mut self, sessions: &[SessionSnapshot]);
fn accept(&mut self, pid: u32) -> Option<String>;
fn reject(&mut self, pid: u32) -> Option<PendingSuggestion>;
fn pending_for(&self, pid: u32) -> Option<PendingSuggestion>;
fn pending_count(&self) -> usize;
fn clear_pending(&mut self);
fn set_pending(&mut self, suggestion: PendingSuggestion);
}
pub trait Orchestrator: Send + Sync {
fn deliver_mailbox(&self, sessions: &[SessionSnapshot]) -> Vec<(u32, String)>;
fn deliver_interrupts(&self, sessions: &[SessionSnapshot]) -> Vec<(String, String)>;
fn expire_stale(&self, live_session_ids: &[String]);
}
#[derive(Clone)]
pub struct Runtime {
pub sessions: Arc<dyn SessionSource>,
pub brain: Arc<dyn BrainView>,
pub coord: Arc<dyn CoordView>,
pub bus: Arc<dyn BusView>,
pub actions: Arc<dyn Actions>,
pub review: Arc<dyn BrainReviewView>,
pub orchestrator: Arc<dyn Orchestrator>,
pub hive: Arc<dyn HiveActions>,
}
impl Runtime {
#[allow(clippy::too_many_arguments)]
pub fn new(
sessions: Arc<dyn SessionSource>,
brain: Arc<dyn BrainView>,
coord: Arc<dyn CoordView>,
bus: Arc<dyn BusView>,
actions: Arc<dyn Actions>,
review: Arc<dyn BrainReviewView>,
orchestrator: Arc<dyn Orchestrator>,
hive: Arc<dyn HiveActions>,
) -> Self {
Self {
sessions,
brain,
coord,
bus,
actions,
review,
orchestrator,
hive,
}
}
}
#[derive(Default)]
pub struct MockRuntime {
pub sessions: Vec<SessionSnapshot>,
pub gate_mode: std::sync::Mutex<Option<BrainGateMode>>,
pub decisions: Vec<DecisionSummary>,
pub review_queue: Vec<ReviewItemSummary>,
pub leases: Vec<LeaseSummary>,
pub handoffs: Vec<HandoffSummary>,
pub interrupts: Vec<InterruptSummary>,
pub tasks: Vec<TaskSummary>,
pub agents: Vec<AgentDirectoryEntry>,
pub roles: Vec<RoleBinding>,
pub peers: Vec<(String, Option<String>)>,
pub actions_log: std::sync::Mutex<Vec<MockAction>>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MockAction {
Terminate {
pid: u32,
},
InjectText {
session_id: String,
text: String,
},
SetGateMode(BrainGateMode),
LogObservation(ObservationInput),
LogDecision(LogDecisionInput),
MarkCanonical {
decision_id: String,
note: Option<String>,
},
BindBusRole {
name: String,
cwd: String,
pid: u32,
},
CancelTask {
task_id: String,
},
SetSupervisorDrain {
draining: bool,
},
RetryTask {
task_id: String,
},
ApproveTask {
task_id: String,
},
SendSessionToPeer {
session_id: String,
transcript: Option<std::path::PathBuf>,
cwd: String,
peer: String,
},
}
impl PartialEq for ObservationInput {
fn eq(&self, other: &Self) -> bool {
self.session_pid == other.session_pid
&& self.project == other.project
&& self.tool == other.tool
&& self.command == other.command
&& self.observed_action == other.observed_action
}
}
impl Eq for ObservationInput {}
impl PartialEq for LogDecisionInput {
fn eq(&self, other: &Self) -> bool {
self.session_pid == other.session_pid
&& self.project == other.project
&& self.tool == other.tool
&& self.command == other.command
&& self.user_action == other.user_action
&& self.decision_type == other.decision_type
&& self.override_reason == other.override_reason
&& self.suggestion.pid == other.suggestion.pid
&& self.suggestion.action == other.suggestion.action
&& self.suggestion.message == other.suggestion.message
&& self.suggestion.reasoning == other.suggestion.reasoning
}
}
impl Eq for LogDecisionInput {}
impl MockRuntime {
pub fn into_runtime(self) -> Runtime {
let arc = Arc::new(self);
Runtime::new(
arc.clone(),
arc.clone(),
arc.clone(),
arc.clone(),
arc.clone(),
arc.clone(),
arc.clone(),
arc,
)
}
pub fn into_runtime_shared(self) -> (Arc<Self>, Runtime) {
let arc = Arc::new(self);
let runtime = Runtime::new(
arc.clone(),
arc.clone(),
arc.clone(),
arc.clone(),
arc.clone(),
arc.clone(),
arc.clone(),
arc.clone(),
);
(arc, runtime)
}
pub fn actions(&self) -> Vec<MockAction> {
self.actions_log
.lock()
.expect("actions_log poisoned")
.clone()
}
}
impl SessionSource for MockRuntime {
fn list(&self) -> Vec<SessionSnapshot> {
self.sessions.clone()
}
}
impl BrainView for MockRuntime {
fn gate_mode(&self) -> BrainGateMode {
self.gate_mode
.lock()
.expect("gate_mode poisoned")
.unwrap_or(BrainGateMode::On)
}
fn recent_decisions(&self, n: usize) -> Vec<DecisionSummary> {
self.decisions.iter().take(n).cloned().collect()
}
fn decision_count(&self) -> usize {
self.decisions.len()
}
}
impl CoordView for MockRuntime {
fn active_leases(&self) -> Vec<LeaseSummary> {
self.leases.clone()
}
fn pending_handoffs(&self) -> Vec<HandoffSummary> {
self.handoffs.clone()
}
fn pending_interrupts(&self) -> Vec<InterruptSummary> {
self.interrupts.clone()
}
fn tasks(&self) -> Vec<TaskSummary> {
self.tasks.clone()
}
}
impl BusView for MockRuntime {
fn list_agents(&self) -> Vec<AgentDirectoryEntry> {
self.agents.clone()
}
fn list_roles(&self) -> Vec<RoleBinding> {
self.roles.clone()
}
}
impl BrainReviewView for MockRuntime {
fn all_decisions(&self) -> Vec<DecisionSummary> {
self.decisions.clone()
}
fn review_queue(&self) -> Vec<ReviewItemSummary> {
self.review_queue.clone()
}
}
impl Orchestrator for MockRuntime {
fn deliver_mailbox(&self, _sessions: &[SessionSnapshot]) -> Vec<(u32, String)> {
Vec::new()
}
fn deliver_interrupts(&self, _sessions: &[SessionSnapshot]) -> Vec<(String, String)> {
Vec::new()
}
fn expire_stale(&self, _live_session_ids: &[String]) {}
}
impl HiveActions for MockRuntime {
fn shared_skill_keys(&self) -> std::collections::HashSet<String> {
std::collections::HashSet::new()
}
fn share_skill(&self, _skill: &crate::skills::DiscoveredSkill) -> Result<String, String> {
Err("MockRuntime does not implement skill sharing".into())
}
fn hive_view_snapshot(&self) -> HiveViewSnapshot {
HiveViewSnapshot {
identity: Some("mock-local".into()),
peers: self.peers.clone(),
}
}
}
impl Actions for MockRuntime {
fn terminate_session(&self, pid: u32) -> Result<(), String> {
self.actions_log
.lock()
.expect("actions_log poisoned")
.push(MockAction::Terminate { pid });
Ok(())
}
fn inject_text(&self, session_id: &str, text: &str) -> Result<(), String> {
self.actions_log
.lock()
.expect("actions_log poisoned")
.push(MockAction::InjectText {
session_id: session_id.into(),
text: text.into(),
});
Ok(())
}
fn set_gate_mode(&self, mode: BrainGateMode) -> Result<(), String> {
*self.gate_mode.lock().expect("gate_mode poisoned") = Some(mode);
self.actions_log
.lock()
.expect("actions_log poisoned")
.push(MockAction::SetGateMode(mode));
Ok(())
}
fn log_observation(&self, observation: ObservationInput) -> Result<(), String> {
self.actions_log
.lock()
.expect("actions_log poisoned")
.push(MockAction::LogObservation(observation));
Ok(())
}
fn log_decision(&self, input: LogDecisionInput) -> Result<(), String> {
self.actions_log
.lock()
.expect("actions_log poisoned")
.push(MockAction::LogDecision(input));
Ok(())
}
fn mark_canonical(&self, decision_id: &str, note: Option<String>) -> Result<(), String> {
self.actions_log
.lock()
.expect("actions_log poisoned")
.push(MockAction::MarkCanonical {
decision_id: decision_id.into(),
note,
});
Ok(())
}
fn bind_bus_role(&self, name: &str, cwd: &str, pid: u32) -> Result<(), String> {
self.actions_log
.lock()
.expect("actions_log poisoned")
.push(MockAction::BindBusRole {
name: name.into(),
cwd: cwd.into(),
pid,
});
Ok(())
}
fn cancel_task(&self, task_id: &str) -> Result<(), String> {
self.actions_log
.lock()
.expect("actions_log poisoned")
.push(MockAction::CancelTask {
task_id: task_id.into(),
});
Ok(())
}
fn set_supervisor_drain(&self, draining: bool) -> Result<(), String> {
self.actions_log
.lock()
.expect("actions_log poisoned")
.push(MockAction::SetSupervisorDrain { draining });
Ok(())
}
fn retry_task(&self, task_id: &str) -> Result<(), String> {
self.actions_log
.lock()
.expect("actions_log poisoned")
.push(MockAction::RetryTask {
task_id: task_id.into(),
});
Ok(())
}
fn approve_task(&self, task_id: &str) -> Result<(), String> {
self.actions_log
.lock()
.expect("actions_log poisoned")
.push(MockAction::ApproveTask {
task_id: task_id.into(),
});
Ok(())
}
fn send_session_to_peer(
&self,
session_id: &str,
transcript: Option<&std::path::Path>,
cwd: &str,
peer: &str,
) -> Result<String, String> {
self.actions_log.lock().expect("actions_log poisoned").push(
MockAction::SendSessionToPeer {
session_id: session_id.into(),
transcript: transcript.map(|p| p.to_path_buf()),
cwd: cwd.into(),
peer: peer.into(),
},
);
Ok(session_id.to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_session(id: &str) -> SessionSnapshot {
SessionSnapshot {
session_id: id.into(),
pid: 12345,
cwd: "/work/proj".into(),
project_name: "proj".into(),
status: "Processing".into(),
cost_usd: 1.23,
context_tokens: 4000,
context_max: 200_000,
last_message_ts: 1_780_000_000,
}
}
#[test]
fn mock_runtime_assembles_and_lists_sessions() {
let mock = MockRuntime {
sessions: vec![sample_session("sess_a"), sample_session("sess_b")],
..Default::default()
};
let rt = mock.into_runtime();
let listed = rt.sessions.list();
assert_eq!(listed.len(), 2);
assert_eq!(listed[0].session_id, "sess_a");
}
#[test]
fn session_detail_falls_back_to_list_scan() {
let mock = MockRuntime {
sessions: vec![sample_session("sess_a"), sample_session("sess_b")],
..Default::default()
};
let rt = mock.into_runtime();
assert!(rt.sessions.detail("sess_a").is_some());
assert!(rt.sessions.detail("sess_missing").is_none());
}
#[test]
fn brain_view_returns_recent_decisions_with_cap() {
let mock = MockRuntime {
decisions: (0..5)
.map(|i| DecisionSummary {
id: format!("dec_{i}"),
timestamp: format!("2026-06-06T00:00:0{i}Z"),
action: "approve".into(),
confidence: Some(0.9),
project: None,
tool: None,
pid: 0,
command: None,
reasoning: None,
user_action: None,
override_reason: None,
brain_decision_ms: None,
canonical: None,
cache_hit: None,
cost_usd: None,
model: None,
outcome_kind: None,
outcome_detail: None,
suggested_at: None,
resolved_at: None,
why: String::new(),
decision_source: None,
rule_name: None,
})
.collect(),
..Default::default()
};
let rt = mock.into_runtime();
assert_eq!(rt.brain.recent_decisions(3).len(), 3);
assert_eq!(rt.brain.decision_count(), 5);
assert_eq!(rt.brain.gate_mode(), BrainGateMode::On);
}
#[test]
fn coord_view_reports_empty_state_cleanly() {
let rt = MockRuntime::default().into_runtime();
assert!(rt.coord.active_leases().is_empty());
assert!(rt.coord.pending_handoffs().is_empty());
assert!(rt.coord.pending_interrupts().is_empty());
assert!(rt.coord.tasks().is_empty());
}
#[test]
fn coord_view_returns_task_fixture() {
let mock = MockRuntime {
tasks: vec![TaskSummary {
id: "t1".into(),
name: "ship the thing".into(),
state: "running".into(),
role: Some("backend".into()),
attempts: 2,
max_retries: 3,
last_session_id: Some("sess-abcdef12".into()),
last_verdict: Some("PASS".into()),
cost_usd: 1.25,
created_at: "2026-06-25T10:00:00Z".into(),
updated_at: "2026-06-25T10:05:00Z".into(),
}],
..Default::default()
};
let rt = mock.into_runtime();
let tasks = rt.coord.tasks();
assert_eq!(tasks.len(), 1);
assert_eq!(tasks[0].state, "running");
assert_eq!(tasks[0].attempts, 2);
}
#[test]
fn bus_view_reports_empty_state_cleanly() {
let rt = MockRuntime::default().into_runtime();
assert!(rt.bus.list_agents().is_empty());
assert!(rt.bus.list_roles().is_empty());
}
#[test]
fn runtime_implements_clone_and_send() {
fn assert_send<T: Send>() {}
assert_send::<Runtime>();
let rt = MockRuntime::default().into_runtime();
let _rt2 = rt.clone();
}
#[test]
fn actions_terminate_records_in_log() {
let rt = MockRuntime::default().into_runtime();
rt.actions.terminate_session(42).unwrap();
rt.actions.terminate_session(43).unwrap();
}
#[test]
fn actions_task_writes_record_in_log() {
let mock = MockRuntime::default();
mock.cancel_task("t-1").unwrap();
mock.set_supervisor_drain(true).unwrap();
mock.retry_task("t-2").unwrap();
mock.approve_task("t-3").unwrap();
let actions = mock.actions();
assert!(matches!(&actions[0], MockAction::CancelTask { task_id } if task_id == "t-1"));
assert!(matches!(
actions[1],
MockAction::SetSupervisorDrain { draining: true }
));
assert!(matches!(&actions[2], MockAction::RetryTask { task_id } if task_id == "t-2"));
assert!(matches!(&actions[3], MockAction::ApproveTask { task_id } if task_id == "t-3"));
}
#[test]
fn actions_set_gate_mode_round_trips_through_brain_view() {
let mock = MockRuntime::default();
let arc = Arc::new(mock);
let rt = Runtime::new(
arc.clone(),
arc.clone(),
arc.clone(),
arc.clone(),
arc.clone(),
arc.clone(),
arc.clone(),
arc.clone(),
);
assert_eq!(rt.brain.gate_mode(), BrainGateMode::On);
rt.actions.set_gate_mode(BrainGateMode::Off).unwrap();
assert_eq!(rt.brain.gate_mode(), BrainGateMode::Off);
rt.actions.set_gate_mode(BrainGateMode::Auto).unwrap();
assert_eq!(rt.brain.gate_mode(), BrainGateMode::Auto);
let calls = arc.actions();
assert_eq!(
calls,
vec![
MockAction::SetGateMode(BrainGateMode::Off),
MockAction::SetGateMode(BrainGateMode::Auto),
]
);
}
#[test]
fn actions_log_observation_captures_full_input() {
let mock = MockRuntime::default();
let arc = Arc::new(mock);
let rt = Runtime::new(
arc.clone(),
arc.clone(),
arc.clone(),
arc.clone(),
arc.clone(),
arc.clone(),
arc.clone(),
arc.clone(),
);
let obs = ObservationInput {
session_pid: 12345,
project: "claudectl".into(),
tool: Some("Bash".into()),
command: Some("cargo test".into()),
observed_action: "user_approve".into(),
};
rt.actions.log_observation(obs.clone()).unwrap();
let calls = arc.actions();
assert_eq!(calls, vec![MockAction::LogObservation(obs)]);
}
#[test]
fn actions_inject_text_and_mark_canonical_record_inputs() {
let mock = MockRuntime::default();
let arc = Arc::new(mock);
let rt = Runtime::new(
arc.clone(),
arc.clone(),
arc.clone(),
arc.clone(),
arc.clone(),
arc.clone(),
arc.clone(),
arc.clone(),
);
rt.actions.inject_text("sess_a", "/compact\n").unwrap();
rt.actions
.mark_canonical("dec_42", Some("nice catch".into()))
.unwrap();
let calls = arc.actions();
assert_eq!(
calls,
vec![
MockAction::InjectText {
session_id: "sess_a".into(),
text: "/compact\n".into(),
},
MockAction::MarkCanonical {
decision_id: "dec_42".into(),
note: Some("nice catch".into()),
},
]
);
}
}