#![allow(dead_code)]
use std::collections::HashSet;
use std::sync::Arc;
use std::time::Duration;
use axum::body::Body;
use axum::http::{Request, Response};
use http_body_util::BodyExt;
use klieo::App;
use klieo_auth_common::{Authenticator, BearerTokenAuthenticator, Identity};
use klieo_bus_memory::MemoryBus;
use klieo_core::agent::{AgentContext, AgentEvent};
use klieo_core::test_utils::{
FakeToolInvoker, InMemoryEpisodic, InMemoryLongTerm, InMemoryShortTerm,
};
use klieo_core::MemoryHandles;
use klieo_flows::{Flow, FlowError};
use klieo_provenance::{ProvenanceRepository, SqliteProvenanceRepository};
use klieo_workflow::test_support::stub_llm;
use klieo_workflow::Registry;
use klieo_workflow_api::{
RunRecordView, RunStatus, RunStore, WorkflowRunRouterBuilder, WORKFLOW_READ_ALL_SCOPE,
WORKFLOW_READ_SCOPE, WORKFLOW_RUN_SCOPE,
};
use serde_json::Value;
pub const TEST_MODEL: &str = "default";
pub const TEST_TOKEN: &str = "test-token";
pub const OTHER_TOKEN: &str = "other-token";
pub const READ_ALL_TOKEN: &str = "read-all-token";
pub async fn test_app() -> Arc<App> {
let app = App::builder()
.llm(stub_llm("STUB REPLY"))
.memory(MemoryHandles::new(
Arc::new(InMemoryShortTerm::default()),
Arc::new(InMemoryLongTerm::default()),
Arc::new(InMemoryEpisodic::default()),
))
.bus(MemoryBus::new())
.tools_invoker(Arc::new(FakeToolInvoker::new()))
.build()
.await
.expect("test app builds");
Arc::new(app)
}
pub fn test_registry() -> Registry {
Registry::new().with_model(TEST_MODEL, stub_llm("STUB REPLY"))
}
pub fn test_provenance() -> Arc<dyn ProvenanceRepository> {
Arc::new(SqliteProvenanceRepository::open_in_memory().expect("in-memory provenance opens"))
}
fn authenticator_granting(scopes: &'static [&'static str]) -> Arc<dyn Authenticator> {
Arc::new(BearerTokenAuthenticator::new(move |token: &str| {
let scopes = scopes.iter().map(|s| s.to_string()).collect();
Ok(Identity::with_scopes(token, scopes))
}))
}
pub fn test_authenticator() -> Arc<dyn Authenticator> {
authenticator_granting(&[WORKFLOW_RUN_SCOPE, WORKFLOW_READ_SCOPE])
}
pub fn read_only_authenticator() -> Arc<dyn Authenticator> {
authenticator_granting(&[WORKFLOW_READ_SCOPE])
}
pub fn run_only_authenticator() -> Arc<dyn Authenticator> {
authenticator_granting(&[WORKFLOW_RUN_SCOPE])
}
pub fn distinct_author_authenticator() -> Arc<dyn Authenticator> {
Arc::new(BearerTokenAuthenticator::new(|token: &str| {
let mut scopes: HashSet<String> = [WORKFLOW_RUN_SCOPE, WORKFLOW_READ_SCOPE]
.into_iter()
.map(String::from)
.collect();
if token == READ_ALL_TOKEN {
scopes.insert(WORKFLOW_READ_ALL_SCOPE.to_string());
}
Ok(Identity::with_scopes(token, scopes))
}))
}
pub async fn build_router(
registry: Registry,
run_timeout: Duration,
max_concurrent: usize,
) -> axum::Router {
build_router_with_auth(test_authenticator(), registry, run_timeout, max_concurrent).await
}
pub async fn build_router_with_auth(
authenticator: Arc<dyn Authenticator>,
registry: Registry,
run_timeout: Duration,
max_concurrent: usize,
) -> axum::Router {
WorkflowRunRouterBuilder::new()
.with_authenticator(authenticator)
.with_app(test_app().await)
.with_registry(registry)
.with_run_store(RunStore::new(MemoryBus::new().kv))
.with_provenance(test_provenance())
.with_run_timeout(run_timeout)
.with_max_concurrent_runs(max_concurrent)
.build()
.expect("router builds")
}
pub async fn build_router_with_provenance(
registry: Registry,
run_store: RunStore,
provenance: Arc<dyn ProvenanceRepository>,
run_timeout: Duration,
max_concurrent: usize,
) -> axum::Router {
WorkflowRunRouterBuilder::new()
.with_authenticator(test_authenticator())
.with_app(test_app().await)
.with_registry(registry)
.with_run_store(run_store)
.with_provenance(provenance)
.with_run_timeout(run_timeout)
.with_max_concurrent_runs(max_concurrent)
.build()
.expect("router builds")
}
pub async fn chain_entries(
provenance: &Arc<dyn ProvenanceRepository>,
scope: &str,
) -> Vec<klieo_provenance::ChainEntry> {
provenance
.list_range(scope, 0, u64::MAX)
.await
.expect("chain range query succeeds")
}
pub fn panicking_subflow() -> Arc<dyn Flow> {
struct Boom;
#[async_trait::async_trait]
impl Flow for Boom {
fn name(&self) -> &str {
"boom"
}
async fn run(&self, _ctx: AgentContext, _input: Value) -> Result<Value, FlowError> {
panic!("intentional test panic");
}
}
Arc::new(Boom)
}
pub fn progress_emitting_subflow(tag: &'static str, delay: Duration) -> Arc<dyn Flow> {
struct Emit(&'static str, Duration);
#[async_trait::async_trait]
impl Flow for Emit {
fn name(&self) -> &str {
self.0
}
async fn run(&self, ctx: AgentContext, input: Value) -> Result<Value, FlowError> {
tokio::time::sleep(self.1).await;
if let Some(progress) = &ctx.progress {
let _ = progress.send(AgentEvent::ToolCallStarted {
name: self.0.to_string(),
});
let _ = progress.send(AgentEvent::Completed);
}
Ok(input)
}
}
Arc::new(Emit(tag, delay))
}
pub fn slow_subflow(delay: Duration) -> Arc<dyn Flow> {
struct Slow(Duration);
#[async_trait::async_trait]
impl Flow for Slow {
fn name(&self) -> &str {
"slow"
}
async fn run(&self, _ctx: AgentContext, input: Value) -> Result<Value, FlowError> {
tokio::time::sleep(self.0).await;
Ok(input)
}
}
Arc::new(Slow(delay))
}
pub fn single_subflow_def(subflow_ref: &str) -> Value {
serde_json::json!({
"id": "test-workflow",
"entry": "n",
"nodes": [{ "id": "n", "kind": "subflow", "ref": subflow_ref }],
"edges": []
})
}
pub fn submit_request(definition: Value, input: Value) -> Request<Body> {
let body = serde_json::json!({ "definition": definition, "input": input });
Request::builder()
.method("POST")
.uri("/runs")
.header("authorization", format!("Bearer {TEST_TOKEN}"))
.header("content-type", "application/json")
.body(Body::from(serde_json::to_vec(&body).unwrap()))
.unwrap()
}
pub fn authed_get_as(uri: &str, token: &str) -> Request<Body> {
Request::builder()
.method("GET")
.uri(uri)
.header("authorization", format!("Bearer {token}"))
.body(Body::empty())
.unwrap()
}
pub fn authed_get(uri: &str) -> Request<Body> {
authed_get_as(uri, TEST_TOKEN)
}
pub fn get_request(run_id: &str) -> Request<Body> {
authed_get(&format!("/runs/{run_id}"))
}
pub fn get_request_as(run_id: &str, token: &str) -> Request<Body> {
authed_get_as(&format!("/runs/{run_id}"), token)
}
pub async fn body_json(response: Response<Body>) -> Value {
let bytes = response.into_body().collect().await.unwrap().to_bytes();
serde_json::from_slice(&bytes).unwrap_or(Value::Null)
}
pub async fn body_text(response: Response<Body>) -> String {
let bytes = response.into_body().collect().await.unwrap().to_bytes();
String::from_utf8(bytes.to_vec()).expect("body is valid UTF-8")
}
pub async fn run_id_of(response: Response<Body>) -> String {
body_json(response).await["run_id"]
.as_str()
.expect("202 body carries run_id")
.to_string()
}
fn is_terminal(status: RunStatus) -> bool {
!matches!(status, RunStatus::Pending | RunStatus::Running)
}
pub async fn poll_until_terminal(router: axum::Router, run_id: &str) -> RunRecordView {
use tower::ServiceExt;
const MAX_POLLS: u32 = 100;
const POLL_INTERVAL: Duration = Duration::from_millis(20);
for _ in 0..MAX_POLLS {
let response = router.clone().oneshot(get_request(run_id)).await.unwrap();
let view: RunRecordView = serde_json::from_value(body_json(response).await).unwrap();
if is_terminal(view.status) {
return view;
}
tokio::time::sleep(POLL_INTERVAL).await;
}
panic!("run {run_id} did not reach a terminal status in time");
}