#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
use std::sync::Arc;
use std::sync::Mutex;
use aion_core::{
ActivityEvent, ActivityId, ContentType, InjectPriority, InterventionCapabilities,
InterventionCommand, InterventionKind, InterventionOutcome, InterventionPrimitive, Payload,
WorkflowId,
};
use aion_integrations::contract::{AgentHarness, AgentSession};
use aion_integrations::error::HarnessError;
use aion_integrations::spec::AgentRunSpec;
use async_trait::async_trait;
use chrono::Utc;
use futures::stream::BoxStream;
use tokio::sync::mpsc;
use uuid::Uuid;
use super::{ControlRegistry, SessionKey};
use crate::runtime::agent::spawn_agent;
struct FakeSession {
capabilities: InterventionCapabilities,
applied: Arc<Mutex<Vec<InterventionKind>>>,
end_events: Option<mpsc::UnboundedReceiver<ActivityEvent>>,
}
struct FakeHarness {
session: Mutex<Option<FakeSession>>,
}
#[async_trait]
impl AgentHarness for FakeHarness {
type Session = FakeSession;
async fn start(&self, _spec: AgentRunSpec) -> Result<Self::Session, HarnessError> {
self.session
.lock()
.unwrap()
.take()
.ok_or_else(|| HarnessError::transport("started twice"))
}
}
#[async_trait]
impl AgentSession for FakeSession {
fn capabilities(&self) -> &InterventionCapabilities {
&self.capabilities
}
fn events(&mut self) -> BoxStream<'static, ActivityEvent> {
match self.end_events.take() {
Some(receiver) => Box::pin(tokio_stream::wrappers::UnboundedReceiverStream::new(
receiver,
)),
None => Box::pin(futures::stream::empty()),
}
}
async fn intervene(&self, cmd: InterventionCommand) -> Result<(), HarnessError> {
if !self.capabilities.supports(&cmd.kind) {
return Err(HarnessError::capability_not_supported("gated"));
}
self.applied.lock().unwrap().push(cmd.kind);
Ok(())
}
async fn wait_result(self) -> Result<Payload, HarnessError> {
Ok(Payload::new(ContentType::Json, b"null".to_vec()))
}
}
fn key(attempt: u32) -> SessionKey {
SessionKey::new(
WorkflowId::new(Uuid::nil()),
ActivityId::from_sequence_position(3),
attempt,
)
}
fn command(attempt: u32, kind: InterventionKind) -> InterventionCommand {
InterventionCommand {
workflow_id: WorkflowId::new(Uuid::nil()),
activity_id: ActivityId::from_sequence_position(3),
attempt,
issued_by: Some("operator".to_owned()),
issued_at: Utc::now(),
kind,
}
}
fn inject(attempt: u32) -> InterventionCommand {
command(
attempt,
InterventionKind::InjectMessage {
text: "steer".to_owned(),
priority: InjectPriority::Interrupt,
},
)
}
fn caps_inject() -> InterventionCapabilities {
InterventionCapabilities::from_primitives([InterventionPrimitive::InjectMessage])
}
struct LiveSession {
applied: Arc<Mutex<Vec<InterventionKind>>>,
guard: super::SessionGuard,
end_events: mpsc::UnboundedSender<ActivityEvent>,
driver: tokio::task::JoinHandle<()>,
}
impl LiveSession {
async fn shutdown(self) {
drop(self.end_events);
drop(self.guard);
let _ = self.driver.await;
}
}
fn live_session(
registry: &ControlRegistry,
attempt: u32,
capabilities: InterventionCapabilities,
) -> LiveSession {
let applied = Arc::new(Mutex::new(Vec::new()));
let (end_events, events_rx) = mpsc::unbounded_channel();
let session = FakeSession {
capabilities: capabilities.clone(),
applied: Arc::clone(&applied),
end_events: Some(events_rx),
};
let harness = FakeHarness {
session: Mutex::new(Some(session)),
};
let (event_tx, _event_rx) = mpsc::unbounded_channel();
let (control_tx, control_rx) = mpsc::unbounded_channel();
let guard = registry.register(key(attempt), control_tx, capabilities);
let driver = tokio::spawn(async move {
let _ = spawn_agent(&harness, spec(), event_tx, Some(control_rx)).await;
});
LiveSession {
applied,
guard,
end_events,
driver,
}
}
fn spec() -> AgentRunSpec {
AgentRunSpec::new(
WorkflowId::new(Uuid::nil()),
ActivityId::from_sequence_position(3),
1,
"agent-activity",
Payload::new(ContentType::Json, b"\"in\"".to_vec()),
)
}
#[tokio::test]
async fn delivers_an_advertised_command_to_the_live_session() {
let registry = ControlRegistry::new();
let session = live_session(®istry, 1, caps_inject());
let outcome = registry.deliver(inject(1)).await;
assert_eq!(outcome, InterventionOutcome::Applied);
assert_eq!(session.applied.lock().unwrap().len(), 1);
session.shutdown().await;
}
#[tokio::test]
async fn gates_an_unadvertised_primitive_at_the_worker() {
let registry = ControlRegistry::new();
let session = live_session(®istry, 1, caps_inject());
let gated = command(1, InterventionKind::PauseResume { paused: true });
let outcome = registry.deliver(gated).await;
assert!(matches!(
outcome,
InterventionOutcome::CapabilityNotSupported { .. }
));
assert!(
session.applied.lock().unwrap().is_empty(),
"a gated command must not reach the session"
);
session.shutdown().await;
}
#[tokio::test]
async fn a_command_for_an_unknown_attempt_is_a_stale_target_no_op() {
let registry = ControlRegistry::new();
let session = live_session(®istry, 1, caps_inject());
let outcome = registry.deliver(inject(2)).await;
assert!(matches!(outcome, InterventionOutcome::StaleTarget { .. }));
session.shutdown().await;
}
#[tokio::test]
async fn a_command_to_an_empty_registry_is_a_stale_target_no_op() {
let registry = ControlRegistry::new();
let outcome = registry.deliver(inject(1)).await;
assert!(matches!(outcome, InterventionOutcome::StaleTarget { .. }));
}
#[tokio::test]
async fn a_deregistered_session_no_longer_receives_commands() {
let registry = ControlRegistry::new();
let session = live_session(®istry, 1, caps_inject());
session.shutdown().await;
let outcome = registry.deliver(inject(1)).await;
assert!(matches!(outcome, InterventionOutcome::StaleTarget { .. }));
}