use std::path::{Path, PathBuf};
use axum::extract::Path as AxumPath;
use axum::extract::Query;
use axum::extract::State;
use axum::http::StatusCode;
use axum::response::Json;
use super::types::*;
use leviath_core::manifest::parse_manifest;
pub(super) fn agents_dir() -> PathBuf {
leviath_core::paths::agents_dir().unwrap_or_default()
}
fn blueprint_dir(name: &str) -> Result<PathBuf, (StatusCode, Json<ErrorResponse>)> {
if !leviath_core::is_safe_path_component(name) {
return Err((
StatusCode::BAD_REQUEST,
Json(ErrorResponse {
error: format!(
"Invalid blueprint name '{name}': names may contain only letters, \
digits, '.', '_' and '-'"
),
}),
));
}
Ok(agents_dir().join(name))
}
fn canonicalize(found: Vec<BlueprintInfo>) -> Vec<BlueprintInfo> {
let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
let mut kept: Vec<BlueprintInfo> = Vec::with_capacity(found.len());
for info in found {
if seen.insert(info.name.clone()) {
kept.push(info);
} else {
tracing::debug!(
name = %info.name,
shadowed = %info.path,
"duplicate blueprint name; keeping the earlier one"
);
}
}
kept.sort_by(|a, b| a.name.cmp(&b.name));
kept
}
pub(super) fn discover_blueprints(config: &crate::config::Config) -> Vec<BlueprintInfo> {
let mut results = Vec::new();
let agents = agents_dir();
let mut dirs_to_scan: Vec<PathBuf> = vec![agents];
dirs_to_scan.extend(config.agent_paths.iter().cloned());
for dir in dirs_to_scan {
if !dir.exists() {
continue;
}
let manifest = dir.join("agent.leviath");
if manifest.exists() {
results.extend(read_blueprint_info(&manifest, &dir));
}
let mut subdirs: Vec<PathBuf> = std::fs::read_dir(&dir)
.into_iter()
.flatten()
.flatten()
.map(|entry| entry.path())
.filter(|p| p.is_dir())
.collect();
subdirs.sort();
for p in subdirs {
let m = p.join("agent.leviath");
if m.exists() {
results.extend(read_blueprint_info(&m, &p));
}
}
}
canonicalize(results)
}
pub(super) fn read_blueprint_info(manifest_path: &Path, dir: &Path) -> Option<BlueprintInfo> {
let content = std::fs::read_to_string(manifest_path).ok()?;
let bp = parse_manifest(&content).ok()?;
Some(BlueprintInfo {
name: bp.name,
version: bp.version,
description: bp.description,
path: dir.to_string_lossy().to_string(),
stages: bp.stages.iter().map(|s| s.name.clone()).collect(),
manifest: content,
})
}
const DEFAULT_LIMIT: usize = 50;
const MAX_LIMIT: usize = 200;
pub(super) async fn list_blueprints(
State(state): State<AppState>,
Query(query): Query<BlueprintsQuery>,
) -> Result<Json<Page<BlueprintInfo>>, (StatusCode, Json<ErrorResponse>)> {
let descending = match query.order.as_deref() {
None | Some("asc") => false,
Some("desc") => true,
Some(other) => {
return Err(err(
StatusCode::BAD_REQUEST,
format!("Unknown order '{other}': expected asc or desc"),
));
}
};
let sort_name = match query.sort.as_deref() {
None | Some("name") => "name",
Some("version") => "version",
Some(other) => {
return Err(err(
StatusCode::BAD_REQUEST,
format!("Unknown sort '{other}': expected name or version"),
));
}
};
let limit = match query.limit {
None => DEFAULT_LIMIT,
Some(0) => {
return Err(err(
StatusCode::BAD_REQUEST,
"`limit` must be at least 1; omit it for the default".to_string(),
));
}
Some(n) => n.min(MAX_LIMIT),
};
let digest = super::cursor::filter_digest(&[query.q.as_deref().unwrap_or("")]);
let order_name = if descending { "desc" } else { "asc" };
let cursor = match query.cursor.as_deref() {
None => None,
Some(raw) => Some(
super::cursor::decode(raw, sort_name, order_name, &digest)
.map_err(|e| err(StatusCode::BAD_REQUEST, e.message()))?,
),
};
let mut found = discover_blueprints(&state.config);
if let Some(needle) = query.q.as_deref().filter(|s| !s.is_empty()) {
found.retain(|bp| {
super::search::find_ignore_ascii_case(&bp.name, needle).is_some()
|| super::search::find_ignore_ascii_case(&bp.description, needle).is_some()
|| bp
.stages
.iter()
.any(|stage| super::search::find_ignore_ascii_case(stage, needle).is_some())
});
}
let key = |bp: &BlueprintInfo| match sort_name {
"version" => (bp.version.clone(), bp.name.clone()),
_ => (bp.name.clone(), String::new()),
};
found.sort_by(|a, b| {
if descending {
key(b).cmp(&key(a))
} else {
key(a).cmp(&key(b))
}
});
let total = found.len();
let mut remaining: Vec<BlueprintInfo> = match cursor {
None => found,
Some(ref cursor) => found
.into_iter()
.filter(|bp| {
cursor.precedes(
&super::cursor::CursorKey::Text(key(bp).0),
&key(bp).1,
descending,
)
})
.collect(),
};
let has_more = remaining.len() > limit;
remaining.truncate(limit);
let next_cursor = has_more.then(|| remaining.last()).flatten().map(|last| {
let (primary, tiebreak) = key(last);
super::cursor::encode(
sort_name,
order_name,
&digest,
super::cursor::CursorKey::Text(primary),
&tiebreak,
)
});
Ok(Json(Page::new(
remaining,
next_cursor,
Some(total),
now_secs(),
)))
}
fn now_secs() -> i64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0)
}
pub(super) async fn get_blueprint(
State(state): State<AppState>,
AxumPath(name): AxumPath<String>,
) -> Result<Json<BlueprintDetail>, StatusCode> {
let blueprints = discover_blueprints(&state.config);
let mut info = blueprints
.into_iter()
.find(|b| b.name == name)
.ok_or(StatusCode::NOT_FOUND)?;
let manifest = std::mem::take(&mut info.manifest);
Ok(Json(BlueprintDetail { info, manifest }))
}
pub(super) async fn create_blueprint(
Json(body): Json<CreateBlueprintReq>,
) -> Result<Json<BlueprintInfo>, (StatusCode, Json<ErrorResponse>)> {
let bp = parse_manifest(&body.manifest).map_err(|e| {
(
StatusCode::BAD_REQUEST,
Json(ErrorResponse {
error: format!("Invalid manifest: {}", e),
}),
)
})?;
let dir = blueprint_dir(&body.name)?;
std::fs::create_dir_all(&dir).map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse {
error: format!("Failed to create directory: {}", e),
}),
)
})?;
let manifest_path = dir.join("agent.leviath");
std::fs::write(&manifest_path, &body.manifest).map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse {
error: format!("Failed to write manifest: {}", e),
}),
)
})?;
Ok(Json(BlueprintInfo {
name: bp.name,
version: bp.version,
description: bp.description,
path: dir.to_string_lossy().to_string(),
stages: bp.stages.iter().map(|s| s.name.clone()).collect(),
manifest: body.manifest,
}))
}
pub(super) async fn update_blueprint(
AxumPath(name): AxumPath<String>,
Json(body): Json<UpdateBlueprintReq>,
) -> Result<Json<BlueprintInfo>, (StatusCode, Json<ErrorResponse>)> {
let bp = parse_manifest(&body.manifest).map_err(|e| {
(
StatusCode::BAD_REQUEST,
Json(ErrorResponse {
error: format!("Invalid manifest: {}", e),
}),
)
})?;
let dir = blueprint_dir(&name)?;
let manifest_path = dir.join("agent.leviath");
if !manifest_path.exists() {
return Err((
StatusCode::NOT_FOUND,
Json(ErrorResponse {
error: format!("Blueprint '{}' not found", name),
}),
));
}
std::fs::write(&manifest_path, &body.manifest).map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse {
error: format!("Failed to write manifest: {}", e),
}),
)
})?;
Ok(Json(BlueprintInfo {
name: bp.name,
version: bp.version,
description: bp.description,
path: dir.to_string_lossy().to_string(),
stages: bp.stages.iter().map(|s| s.name.clone()).collect(),
manifest: body.manifest,
}))
}
pub(super) async fn delete_blueprint(
AxumPath(name): AxumPath<String>,
) -> Result<StatusCode, (StatusCode, Json<ErrorResponse>)> {
let dir = blueprint_dir(&name)?;
if !dir.exists() {
return Err((
StatusCode::NOT_FOUND,
Json(ErrorResponse {
error: format!("Blueprint '{}' not found", name),
}),
));
}
std::fs::remove_dir_all(&dir).map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse {
error: format!("Failed to delete blueprint: {}", e),
}),
)
})?;
Ok(StatusCode::NO_CONTENT)
}
pub(super) async fn validate_blueprint(
Json(body): Json<ValidateBlueprintReq>,
) -> Json<ValidateResponse> {
let dir = body
.name
.as_deref()
.and_then(|name| blueprint_dir(name).ok())
.unwrap_or_else(|| PathBuf::from("."));
Json(validate_manifest_text(&body.manifest, &dir))
}
fn validate_manifest_text(manifest: &str, dir: &Path) -> ValidateResponse {
let bp = match parse_manifest(manifest) {
Ok(bp) => bp,
Err(e) => return ValidateResponse::invalid(vec![e.to_string()]),
};
if let Err(e) = bp.validate() {
return ValidateResponse::invalid(vec![e.to_string()]);
}
let env = crate::lint::LintEnv::offline(dir);
let findings = crate::lint::lint_manifest(manifest, &bp, &env);
let (errors, warnings): (Vec<_>, Vec<_>) = findings
.iter()
.partition(|f| f.severity == crate::lint::LintSeverity::Error);
let render = |f: &&crate::lint::LintFinding| format!("{} [{}]", f.one_line(), f.code);
ValidateResponse {
valid: errors.is_empty(),
errors: (!errors.is_empty()).then(|| errors.iter().map(render).collect()),
warnings: (!warnings.is_empty()).then(|| warnings.iter().map(render).collect()),
}
}
#[cfg(test)]
mod listing_tests {
use super::*;
use axum::Router;
use axum::body::Body;
use axum::http::Request;
use axum::routing::get;
use std::sync::Arc;
use tokio::sync::broadcast;
use tower::ServiceExt;
use crate::config::Config;
fn manifest(name: &str, description: &str) -> String {
format!(
r#"
[agent]
name = "{name}"
version = "1.0.0"
description = "{description}"
[stages.zzstage-work]
system_prompt = "do it"
"#
)
}
fn catalog(entries: &[(&str, &str)]) -> tempfile::TempDir {
let dir = tempfile::tempdir().unwrap();
for (name, description) in entries {
let name = format!("{FIXTURE_PREFIX}{name}");
let sub = dir.path().join(&name);
std::fs::create_dir_all(&sub).unwrap();
std::fs::write(sub.join("agent.leviath"), manifest(&name, description)).unwrap();
}
dir
}
fn fx(name: &str) -> String {
format!("{FIXTURE_PREFIX}{name}")
}
const FIXTURE_PREFIX: &str = "zzfixture-";
async fn page(dir: &tempfile::TempDir, extra: &str) -> (StatusCode, serde_json::Value) {
let (tx, _) = broadcast::channel(64);
let state = AppState {
config: Arc::new(Config {
agent_paths: vec![dir.path().to_path_buf()],
..Default::default()
}),
event_tx: tx,
control: crate::commands::serve::testutil::no_daemon_client(),
mcp: crate::commands::serve::mcp::McpAdmin::default(),
limits: Arc::new(crate::commands::serve::types::ServeLimits::default()),
};
let app = Router::new()
.route("/api/blueprints", get(list_blueprints))
.with_state(state);
let req = Request::builder()
.uri(format!("/api/blueprints{extra}"))
.body(Body::empty())
.unwrap();
let resp = app.oneshot(req).await.unwrap();
let status = resp.status();
let body = axum::body::to_bytes(resp.into_body(), usize::MAX)
.await
.unwrap();
(
status,
serde_json::from_slice(&body).unwrap_or(serde_json::Value::Null),
)
}
fn fixture_names(page: &serde_json::Value) -> Vec<String> {
names(page)
.into_iter()
.filter(|n| n.starts_with(FIXTURE_PREFIX))
.collect()
}
fn names(page: &serde_json::Value) -> Vec<String> {
page["items"]
.as_array()
.unwrap()
.iter()
.map(|b| b["name"].as_str().unwrap().to_string())
.collect()
}
#[tokio::test]
async fn the_catalog_pages_through_every_blueprint_exactly_once() {
let dir = catalog(&[
("alpha", "first"),
("bravo", "second"),
("charlie", "third"),
("delta", "fourth"),
]);
let mut seen: Vec<String> = Vec::new();
let mut cursor: Option<String> = None;
for _ in 0..10 {
let extra = match cursor {
None => "?limit=2".to_string(),
Some(ref c) => format!("?limit=2&cursor={c}"),
};
let (status, body) = page(&dir, &extra).await;
assert_eq!(status, StatusCode::OK);
seen.extend(names(&body));
match body["next_cursor"].as_str() {
Some(c) => cursor = Some(c.to_string()),
None => break,
}
}
let got: Vec<String> = seen
.iter()
.filter(|n| n.starts_with(FIXTURE_PREFIX))
.cloned()
.collect();
assert_eq!(
got,
vec![fx("alpha"), fx("bravo"), fx("charlie"), fx("delta")]
);
let mut unique = seen.clone();
unique.sort();
unique.dedup();
assert_eq!(unique.len(), seen.len(), "a blueprint was returned twice");
}
#[tokio::test]
async fn q_matches_name_description_and_stage_names() {
let dir = catalog(&[
("researcher", "digs through papers"),
("coder", "writes zzrust"),
]);
let (_, by_name) = page(&dir, "?q=ZZFIXTURE-RESEARCH").await;
assert_eq!(names(&by_name), vec![fx("researcher")]);
let (_, by_description) = page(&dir, "?q=writes+zzrust").await;
assert_eq!(names(&by_description), vec![fx("coder")]);
let (_, by_stage) = page(&dir, "?q=zzstage").await;
assert_eq!(fixture_names(&by_stage).len(), 2);
let (_, nothing) = page(&dir, "?q=nothing-like-this-at-all").await;
assert!(names(¬hing).is_empty());
assert_eq!(nothing["total"], 0);
}
#[tokio::test]
async fn the_catalog_can_be_sorted_by_version() {
let dir = catalog(&[("alpha", "a"), ("bravo", "b")]);
let (status, body) = page(&dir, "?sort=version&limit=200").await;
assert_eq!(status, StatusCode::OK);
assert_eq!(fixture_names(&body), vec![fx("alpha"), fx("bravo")]);
}
#[tokio::test]
async fn the_catalog_can_be_ordered_backwards() {
let dir = catalog(&[("alpha", "a"), ("bravo", "b")]);
let (_, body) = page(&dir, "?order=desc&limit=200").await;
assert_eq!(fixture_names(&body), vec![fx("bravo"), fx("alpha")]);
}
#[tokio::test]
async fn a_bad_sort_order_or_limit_is_refused() {
let dir = catalog(&[("alpha", "a")]);
for extra in [
"?sort=whenever",
"?order=sideways",
"?limit=0",
"?cursor=zz",
] {
let (status, _) = page(&dir, extra).await;
assert_eq!(
status,
StatusCode::BAD_REQUEST,
"expected {extra} to be rejected"
);
}
}
#[tokio::test]
async fn a_cursor_is_bound_to_the_query_that_minted_it() {
let dir = catalog(&[("alpha", "a"), ("bravo", "b"), ("charlie", "c")]);
let (_, first) = page(&dir, "?limit=1").await;
let cursor = first["next_cursor"].as_str().unwrap().to_string();
let (ok, _) = page(&dir, &format!("?limit=1&cursor={cursor}")).await;
assert_eq!(ok, StatusCode::OK);
let (changed, _) = page(&dir, &format!("?limit=1&q=alpha&cursor={cursor}")).await;
assert_eq!(changed, StatusCode::BAD_REQUEST);
}
}
#[cfg(test)]
mod canonicalize_tests {
use super::*;
fn info(name: &str, path: &str) -> BlueprintInfo {
BlueprintInfo {
name: name.to_string(),
version: "1".to_string(),
description: String::new(),
path: path.to_string(),
stages: vec![],
manifest: String::new(),
}
}
#[test]
fn duplicate_names_keep_the_first_scanned_not_the_first_sorted() {
let out = canonicalize(vec![
info("coder", "/zzz/installed"),
info("coder", "/aaa/custom"),
]);
assert_eq!(out.len(), 1);
assert_eq!(out[0].path, "/zzz/installed");
}
#[test]
fn output_is_name_sorted_with_path_as_the_tie_break() {
let out = canonicalize(vec![
info("zebra", "/b"),
info("alpha", "/z"),
info("alpha2", "/a"),
]);
let names: Vec<&str> = out.iter().map(|b| b.name.as_str()).collect();
assert_eq!(names, vec!["alpha", "alpha2", "zebra"]);
}
#[test]
fn distinct_names_are_all_kept() {
let out = canonicalize(vec![info("a", "/1"), info("b", "/2"), info("c", "/3")]);
assert_eq!(out.len(), 3);
}
#[test]
fn an_empty_scan_canonicalizes_to_an_empty_catalog() {
assert!(canonicalize(vec![]).is_empty());
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_support::write_test_agent;
use axum::Router;
use axum::body::Body;
use axum::http::Request;
use axum::routing::{get, post};
use std::sync::Arc;
use tokio::sync::broadcast;
use tower::ServiceExt;
use crate::config::Config;
fn test_state_with_path(path: PathBuf) -> AppState {
let (tx, _) = broadcast::channel(64);
AppState {
config: Arc::new(Config {
agent_paths: vec![path],
..Default::default()
}),
event_tx: tx,
control: crate::commands::serve::testutil::no_daemon_client(),
mcp: crate::commands::serve::mcp::McpAdmin::default(),
limits: Default::default(),
}
}
fn test_manifest() -> &'static str {
r#"
[agent]
name = "test-bp"
version = "1.0.0"
description = "A test blueprint"
[stages.plan]
system_prompt = "Plan the work"
"#
}
#[tokio::test]
async fn list_blueprints_empty_path_returns_ok() {
let dir = tempfile::tempdir().unwrap();
let state = test_state_with_path(dir.path().to_path_buf());
let app = Router::new()
.route("/api/blueprints", get(list_blueprints))
.with_state(state);
let req = Request::builder()
.uri("/api/blueprints")
.body(Body::empty())
.unwrap();
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status(), axum::http::StatusCode::OK);
let body = axum::body::to_bytes(resp.into_body(), usize::MAX)
.await
.unwrap();
let page: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert!(page["items"].is_array());
assert!(page["total"].is_number());
assert!(page["next_cursor"].is_null());
}
#[tokio::test]
async fn list_blueprints_with_agent_returns_it() {
let dir = tempfile::tempdir().unwrap();
let agent_dir = dir.path().join("my-agent");
std::fs::create_dir_all(&agent_dir).unwrap();
std::fs::write(agent_dir.join("agent.leviath"), test_manifest()).unwrap();
let state = test_state_with_path(dir.path().to_path_buf());
let app = Router::new()
.route("/api/blueprints", get(list_blueprints))
.with_state(state);
let req = Request::builder()
.uri("/api/blueprints")
.body(Body::empty())
.unwrap();
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status(), axum::http::StatusCode::OK);
let body = axum::body::to_bytes(resp.into_body(), usize::MAX)
.await
.unwrap();
let page: serde_json::Value = serde_json::from_slice(&body).unwrap();
let blueprints = page["items"].as_array().unwrap().clone();
assert_test_bp_listed(&blueprints);
}
fn assert_test_bp_listed(blueprints: &[serde_json::Value]) {
assert!(
blueprints
.iter()
.any(|b| b["name"].as_str() == Some("test-bp")),
"test-bp should be listed"
);
}
#[test]
#[should_panic(expected = "test-bp should be listed")]
fn assert_test_bp_listed_panics_when_missing() {
assert_test_bp_listed(&[]);
}
#[tokio::test]
async fn get_blueprint_existing_returns_ok() {
let dir = tempfile::tempdir().unwrap();
let agent_dir = dir.path().join("test-bp");
std::fs::create_dir_all(&agent_dir).unwrap();
std::fs::write(agent_dir.join("agent.leviath"), test_manifest()).unwrap();
let state = test_state_with_path(dir.path().to_path_buf());
let app = Router::new()
.route("/api/blueprints/{name}", get(get_blueprint))
.with_state(state);
let req = Request::builder()
.uri("/api/blueprints/test-bp")
.body(Body::empty())
.unwrap();
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status(), axum::http::StatusCode::OK);
let body = axum::body::to_bytes(resp.into_body(), usize::MAX)
.await
.unwrap();
let bp: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert_eq!(bp["name"].as_str().unwrap(), "test-bp");
assert_eq!(bp["version"].as_str().unwrap(), "1.0.0");
assert_eq!(bp["manifest"].as_str().unwrap(), test_manifest());
}
#[tokio::test]
async fn the_listing_does_not_carry_manifests() {
let dir = tempfile::tempdir().unwrap();
let agent_dir = dir.path().join("test-bp");
std::fs::create_dir_all(&agent_dir).unwrap();
std::fs::write(agent_dir.join("agent.leviath"), test_manifest()).unwrap();
let state = test_state_with_path(dir.path().to_path_buf());
let app = Router::new()
.route("/api/blueprints", get(list_blueprints))
.with_state(state);
let req = Request::builder()
.uri("/api/blueprints")
.body(Body::empty())
.unwrap();
let resp = app.oneshot(req).await.unwrap();
let body = axum::body::to_bytes(resp.into_body(), usize::MAX)
.await
.unwrap();
let page: serde_json::Value = serde_json::from_slice(&body).unwrap();
let items = page["items"].as_array().expect("an items array");
assert!(!items.is_empty(), "the agent is listed");
assert!(
items.iter().all(|b| b.get("manifest").is_none()),
"the listing stays a catalog: {items:?}"
);
}
#[tokio::test]
async fn a_blueprint_whose_manifest_cannot_be_read_is_not_found() {
let dir = tempfile::tempdir().unwrap();
let agent_dir = dir.path().join("test-bp");
std::fs::create_dir_all(&agent_dir).unwrap();
std::fs::create_dir_all(agent_dir.join("agent.leviath")).unwrap();
let state = test_state_with_path(dir.path().to_path_buf());
let app = Router::new()
.route("/api/blueprints/{name}", get(get_blueprint))
.with_state(state);
let req = Request::builder()
.uri("/api/blueprints/test-bp")
.body(Body::empty())
.unwrap();
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status(), axum::http::StatusCode::NOT_FOUND);
}
#[tokio::test]
async fn get_blueprint_not_found_returns_404() {
let dir = tempfile::tempdir().unwrap();
let state = test_state_with_path(dir.path().to_path_buf());
let app = Router::new()
.route("/api/blueprints/{name}", get(get_blueprint))
.with_state(state);
let req = Request::builder()
.uri("/api/blueprints/does-not-exist-xyz")
.body(Body::empty())
.unwrap();
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status(), axum::http::StatusCode::NOT_FOUND);
}
fn unique_bp_name(prefix: &str) -> String {
use std::time::{SystemTime, UNIX_EPOCH};
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.subsec_nanos();
format!("test-bp-{}-{}-{}", prefix, std::process::id(), nanos)
}
#[tokio::test]
async fn create_blueprint_valid_manifest_returns_ok() {
let name = unique_bp_name("create");
let manifest = format!(
r#"
[agent]
name = "{name}"
version = "1.0.0"
description = "Created via API"
[stages.plan]
system_prompt = "Plan the work"
"#
);
let app = Router::new().route("/api/blueprints", post(create_blueprint));
let body = serde_json::json!({ "name": name, "manifest": manifest });
let req = Request::builder()
.method("POST")
.uri("/api/blueprints")
.header("content-type", "application/json")
.body(Body::from(serde_json::to_string(&body).unwrap()))
.unwrap();
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status(), axum::http::StatusCode::OK);
let bytes = axum::body::to_bytes(resp.into_body(), usize::MAX)
.await
.unwrap();
let info: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
assert_eq!(info["name"].as_str().unwrap(), name);
assert_eq!(info["stages"].as_array().unwrap().len(), 1);
let _ = std::fs::remove_dir_all(agents_dir().join(&name));
}
#[tokio::test]
async fn create_blueprint_rejects_traversing_names() {
let manifest = r#"
[agent]
name = "x"
version = "1.0.0"
description = "d"
[stages.plan]
system_prompt = "p"
"#;
for name in [
"../../../../tmp/leviath-traversal-probe",
"/tmp/leviath-traversal-probe",
"..",
"a/b",
] {
let app = Router::new().route("/api/blueprints", post(create_blueprint));
let body = serde_json::json!({ "name": name, "manifest": manifest });
let req = Request::builder()
.method("POST")
.uri("/api/blueprints")
.header("content-type", "application/json")
.body(Body::from(serde_json::to_string(&body).unwrap()))
.unwrap();
let resp = app.oneshot(req).await.unwrap();
assert_eq!(
resp.status(),
axum::http::StatusCode::BAD_REQUEST,
"name {name:?} should be refused"
);
}
assert!(
!std::path::Path::new("/tmp/leviath-traversal-probe").exists(),
"nothing may be created outside the agents directory"
);
}
#[tokio::test]
async fn delete_blueprint_rejects_traversing_names() {
let victim = std::env::temp_dir().join("leviath-delete-probe");
std::fs::create_dir_all(&victim).unwrap();
let app = Router::new().route(
"/api/blueprints/{name}",
axum::routing::delete(delete_blueprint),
);
let req = Request::builder()
.method("DELETE")
.uri("/api/blueprints/..%2f..%2f..%2f..%2ftmp%2fleviath-delete-probe")
.body(Body::empty())
.unwrap();
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status(), axum::http::StatusCode::BAD_REQUEST);
assert!(victim.exists(), "the directory must not have been deleted");
let _ = std::fs::remove_dir_all(&victim);
}
#[tokio::test]
async fn create_blueprint_dir_creation_failure_returns_500() {
let name = unique_bp_name("create-fail");
let dir = agents_dir().join(&name);
std::fs::create_dir_all(agents_dir()).unwrap();
std::fs::write(&dir, b"blocking file").unwrap();
let app = Router::new().route("/api/blueprints", post(create_blueprint));
let manifest = format!(
"\n[agent]\nname = \"{name}\"\nversion = \"1.0.0\"\ndescription = \"d\"\n\n[stages.plan]\nsystem_prompt = \"p\"\n"
);
let body = serde_json::json!({ "name": name, "manifest": manifest });
let req = Request::builder()
.method("POST")
.uri("/api/blueprints")
.header("content-type", "application/json")
.body(Body::from(serde_json::to_string(&body).unwrap()))
.unwrap();
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status(), axum::http::StatusCode::INTERNAL_SERVER_ERROR);
let _ = std::fs::remove_file(&dir);
}
#[tokio::test]
async fn create_blueprint_invalid_manifest_returns_400() {
let app = Router::new().route("/api/blueprints", post(create_blueprint));
let body = serde_json::json!({
"name": "bad-agent",
"manifest": "not valid toml [[[{"
});
let req = Request::builder()
.method("POST")
.uri("/api/blueprints")
.header("content-type", "application/json")
.body(Body::from(serde_json::to_string(&body).unwrap()))
.unwrap();
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status(), axum::http::StatusCode::BAD_REQUEST);
}
#[tokio::test]
async fn create_blueprint_manifest_write_failure_returns_500() {
let name = unique_bp_name("create-manifest-write-fail");
let dir = agents_dir().join(&name);
std::fs::create_dir_all(dir.join("agent.leviath")).unwrap();
let app = Router::new().route("/api/blueprints", post(create_blueprint));
let manifest = format!(
"\n[agent]\nname = \"{name}\"\nversion = \"1.0.0\"\ndescription = \"d\"\n\n[stages.plan]\nsystem_prompt = \"p\"\n"
);
let body = serde_json::json!({ "name": name, "manifest": manifest });
let req = Request::builder()
.method("POST")
.uri("/api/blueprints")
.header("content-type", "application/json")
.body(Body::from(serde_json::to_string(&body).unwrap()))
.unwrap();
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status(), axum::http::StatusCode::INTERNAL_SERVER_ERROR);
let _ = std::fs::remove_dir_all(&dir);
}
#[tokio::test]
async fn update_blueprint_write_failure_returns_500() {
use axum::routing::put;
let name = unique_bp_name("update-fail");
let dir = agents_dir().join(&name);
std::fs::create_dir_all(&dir).unwrap();
let manifest_path = dir.join("agent.leviath");
std::fs::write(&manifest_path, test_manifest()).unwrap();
let original = std::fs::metadata(&manifest_path).unwrap().permissions();
let mut perms = original.clone();
perms.set_readonly(true);
std::fs::set_permissions(&manifest_path, perms).unwrap();
let app = Router::new().route("/api/blueprints/{name}", put(update_blueprint));
let body = serde_json::json!({ "manifest": test_manifest() });
let req = Request::builder()
.method("PUT")
.uri(format!("/api/blueprints/{}", name))
.header("content-type", "application/json")
.body(Body::from(serde_json::to_string(&body).unwrap()))
.unwrap();
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status(), axum::http::StatusCode::INTERNAL_SERVER_ERROR);
let _ = std::fs::set_permissions(&manifest_path, original);
let _ = std::fs::remove_dir_all(&dir);
}
#[tokio::test]
async fn update_blueprint_existing_returns_ok() {
use axum::routing::put;
let name = unique_bp_name("update");
let dir = agents_dir().join(&name);
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(
dir.join("agent.leviath"),
format!(
r#"
[agent]
name = "{name}"
version = "1.0.0"
description = "Original"
[stages.plan]
system_prompt = "Plan"
"#
),
)
.unwrap();
let app = Router::new().route("/api/blueprints/{name}", put(update_blueprint));
let updated_manifest = format!(
r#"
[agent]
name = "{name}"
version = "2.0.0"
description = "Updated"
[stages.plan]
system_prompt = "Plan"
[stages.implement]
system_prompt = "Implement"
"#
);
let body = serde_json::json!({ "manifest": updated_manifest });
let req = Request::builder()
.method("PUT")
.uri(format!("/api/blueprints/{}", name))
.header("content-type", "application/json")
.body(Body::from(serde_json::to_string(&body).unwrap()))
.unwrap();
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status(), axum::http::StatusCode::OK);
let bytes = axum::body::to_bytes(resp.into_body(), usize::MAX)
.await
.unwrap();
let info: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
assert_eq!(info["version"].as_str().unwrap(), "2.0.0");
assert_eq!(info["stages"].as_array().unwrap().len(), 2);
let _ = std::fs::remove_dir_all(&dir);
}
#[tokio::test]
async fn update_blueprint_invalid_manifest_returns_400() {
use axum::routing::put;
let app = Router::new().route("/api/blueprints/{name}", put(update_blueprint));
let body = serde_json::json!({
"manifest": "not valid toml {{{"
});
let req = Request::builder()
.method("PUT")
.uri("/api/blueprints/my-agent")
.header("content-type", "application/json")
.body(Body::from(serde_json::to_string(&body).unwrap()))
.unwrap();
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status(), axum::http::StatusCode::BAD_REQUEST);
}
#[tokio::test]
async fn update_blueprint_rejects_traversing_names() {
use axum::routing::put;
let manifest = r#"
[agent]
name = "x"
version = "1.0.0"
description = "d"
[stages.plan]
system_prompt = "p"
"#;
for name in ["..", "%2e%2e", "."] {
let app = Router::new().route("/api/blueprints/{name}", put(update_blueprint));
let body = serde_json::json!({ "manifest": manifest });
let req = Request::builder()
.method("PUT")
.uri(format!("/api/blueprints/{name}"))
.header("content-type", "application/json")
.body(Body::from(serde_json::to_string(&body).unwrap()))
.unwrap();
let resp = app.oneshot(req).await.unwrap();
assert_eq!(
resp.status(),
axum::http::StatusCode::BAD_REQUEST,
"name {name:?} should be refused"
);
}
}
#[tokio::test]
async fn update_blueprint_not_found_returns_404() {
use axum::routing::put;
let app = Router::new().route("/api/blueprints/{name}", put(update_blueprint));
let body = serde_json::json!({
"manifest": r#"
[agent]
name = "no-such-agent"
version = "1.0.0"
description = "Missing"
[stages.run]
system_prompt = "Run"
"#
});
let req = Request::builder()
.method("PUT")
.uri("/api/blueprints/no-such-agent-xyz-99999")
.header("content-type", "application/json")
.body(Body::from(serde_json::to_string(&body).unwrap()))
.unwrap();
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status(), axum::http::StatusCode::NOT_FOUND);
}
#[cfg(unix)]
#[tokio::test]
async fn delete_blueprint_removal_failure_returns_500() {
use axum::routing::delete;
use std::os::unix::fs::PermissionsExt;
let name = unique_bp_name("delete-fail");
let dir = agents_dir().join(&name);
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("agent.leviath"), test_manifest()).unwrap();
std::fs::set_permissions(&dir, std::fs::Permissions::from_mode(0o555)).unwrap();
let app = Router::new().route("/api/blueprints/{name}", delete(delete_blueprint));
let req = Request::builder()
.method("DELETE")
.uri(format!("/api/blueprints/{}", name))
.body(Body::empty())
.unwrap();
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status(), axum::http::StatusCode::INTERNAL_SERVER_ERROR);
let _ = std::fs::set_permissions(&dir, std::fs::Permissions::from_mode(0o755));
let _ = std::fs::remove_dir_all(&dir);
}
#[cfg(windows)]
#[tokio::test]
async fn delete_blueprint_removal_failure_returns_500_windows() {
use axum::routing::delete;
use std::fs::OpenOptions;
use std::os::windows::fs::OpenOptionsExt;
let name = unique_bp_name("delete-fail-win");
let dir = agents_dir().join(&name);
std::fs::create_dir_all(&dir).unwrap();
let manifest_path = dir.join("agent.leviath");
let mut locked = OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.share_mode(0)
.open(&manifest_path)
.unwrap();
std::io::Write::write_all(&mut locked, test_manifest().as_bytes()).unwrap();
let _locked = locked;
let app = Router::new().route("/api/blueprints/{name}", delete(delete_blueprint));
let req = Request::builder()
.method("DELETE")
.uri(format!("/api/blueprints/{}", name))
.body(Body::empty())
.unwrap();
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status(), axum::http::StatusCode::INTERNAL_SERVER_ERROR);
drop(_locked);
let _ = std::fs::remove_dir_all(&dir);
}
#[tokio::test]
async fn delete_blueprint_existing_returns_no_content() {
use axum::routing::delete;
let name = unique_bp_name("delete");
let dir = agents_dir().join(&name);
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("agent.leviath"), test_manifest()).unwrap();
assert!(dir.exists());
let app = Router::new().route("/api/blueprints/{name}", delete(delete_blueprint));
let req = Request::builder()
.method("DELETE")
.uri(format!("/api/blueprints/{}", name))
.body(Body::empty())
.unwrap();
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status(), axum::http::StatusCode::NO_CONTENT);
assert_dir_removed(&dir);
}
fn assert_dir_removed(dir: &std::path::Path) {
assert!(!dir.exists(), "directory should be removed");
}
#[test]
#[should_panic(expected = "directory should be removed")]
fn assert_dir_removed_panics_when_still_present() {
assert_dir_removed(std::path::Path::new("."));
}
#[tokio::test]
async fn delete_blueprint_not_found_returns_404() {
use axum::routing::delete;
let app = Router::new().route("/api/blueprints/{name}", delete(delete_blueprint));
let req = Request::builder()
.method("DELETE")
.uri("/api/blueprints/nonexistent-xyz")
.body(Body::empty())
.unwrap();
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status(), axum::http::StatusCode::NOT_FOUND);
}
#[tokio::test]
async fn validate_blueprint_valid_manifest_returns_ok_valid_true() {
let app = Router::new().route("/api/blueprints/validate", post(validate_blueprint));
let body = serde_json::json!({"manifest": test_manifest()});
let req = Request::builder()
.method("POST")
.uri("/api/blueprints/validate")
.header("content-type", "application/json")
.body(Body::from(serde_json::to_string(&body).unwrap()))
.unwrap();
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status(), axum::http::StatusCode::OK);
let bytes = axum::body::to_bytes(resp.into_body(), usize::MAX)
.await
.unwrap();
let result: ValidateResponse = serde_json::from_slice(&bytes).unwrap();
assert!(result.valid);
assert!(result.errors.is_none());
}
#[tokio::test]
async fn validate_blueprint_reports_lint_errors_and_warnings_separately() {
let manifest = r#"
[agent]
name = "linty"
version = "0.1.0"
[stages.main]
mode = "autonomous"
model = { models = [{ provider = "anthropic", model = "claude-sonnet-5" }] }
available_tools = ["read_file", "raed_file", "ask_user_text"]
[context.regions]
system = { kind = "pinned", max_tokens = 1000 }
"#;
let result = validate_manifest_text(manifest, Path::new("."));
assert!(!result.valid);
let errors = result.errors.expect("the typo is an error");
assert_eq!(errors.len(), 1);
assert!(errors[0].contains("unknown-tool"), "{errors:?}");
let warnings = result.warnings.expect("the defaults are warnings");
assert!(
warnings
.iter()
.any(|w| w.contains("stage-missing-max-iterations")),
"{warnings:?}"
);
assert!(
warnings
.iter()
.any(|w| w.contains("blocking-tool-in-autonomous-stage")),
"{warnings:?}"
);
}
#[tokio::test]
async fn a_manifest_naming_its_agent_resolves_that_agents_own_tools() {
let dir = tempfile::tempdir().unwrap();
let tools = dir.path().join("tools");
std::fs::create_dir_all(&tools).unwrap();
std::fs::write(
tools.join("web_search.rhai"),
"// @tool web_search\n// @description searches\n\"found\"",
)
.unwrap();
let manifest = r#"
[agent]
name = "toolful"
version = "0.1.0"
description = "d"
[stages.main]
mode = "autonomous"
model = { models = [{ provider = "anthropic", model = "claude-sonnet-5" }] }
description = "Main"
max_iterations = 5
available_tools = ["web_search"]
[context.regions]
system = { kind = "pinned", max_tokens = 1000 }
"#;
let rooted = validate_manifest_text(manifest, dir.path());
assert!(
rooted.valid,
"the agent's own tool resolves: {:?}",
rooted.errors
);
let elsewhere = tempfile::tempdir().unwrap();
let unrooted = validate_manifest_text(manifest, elsewhere.path());
assert!(!unrooted.valid);
let errors = unrooted.errors.expect("the grant resolves to nothing");
assert!(errors[0].contains("unknown-tool"), "{errors:?}");
}
#[tokio::test]
async fn validate_accepts_a_blueprint_name_and_ignores_an_unusable_one() {
let app = Router::new().route("/api/blueprints/validate", post(validate_blueprint));
let manifest = r#"
[agent]
name = "plain"
version = "0.1.0"
description = "d"
[stages.main]
mode = "autonomous"
model = { models = [{ provider = "anthropic", model = "claude-sonnet-5" }] }
description = "Main"
max_iterations = 5
[context.regions]
system = { kind = "pinned", max_tokens = 1000 }
"#;
for name in [
serde_json::json!("../../etc"),
serde_json::json!("no-such-agent"),
serde_json::Value::Null,
] {
let body = serde_json::json!({ "manifest": manifest, "name": name }).to_string();
let req = Request::builder()
.method("POST")
.uri("/api/blueprints/validate")
.header("content-type", "application/json")
.body(Body::from(body))
.unwrap();
let resp = app.clone().oneshot(req).await.unwrap();
assert_eq!(resp.status(), axum::http::StatusCode::OK, "{name}");
let bytes = axum::body::to_bytes(resp.into_body(), usize::MAX)
.await
.unwrap();
let out: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
assert_eq!(out["valid"], serde_json::json!(true), "{name}");
}
}
#[tokio::test]
async fn validate_blueprint_with_only_warnings_stays_valid() {
let manifest = r#"
[agent]
name = "warny"
version = "0.1.0"
[stages.main]
mode = "autonomous"
model = { models = [{ provider = "anthropic", model = "claude-sonnet-5" }] }
[context.regions]
system = { kind = "pinned", max_tokens = 1000 }
"#;
let result = validate_manifest_text(manifest, Path::new("."));
assert!(result.valid);
assert!(result.errors.is_none());
assert_eq!(result.warnings.expect("no max_iterations").len(), 1);
}
#[tokio::test]
async fn validate_blueprint_invalid_manifest_returns_ok_valid_false() {
let app = Router::new().route("/api/blueprints/validate", post(validate_blueprint));
let body = serde_json::json!({"manifest": "not toml at all [[[{"});
let req = Request::builder()
.method("POST")
.uri("/api/blueprints/validate")
.header("content-type", "application/json")
.body(Body::from(serde_json::to_string(&body).unwrap()))
.unwrap();
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status(), axum::http::StatusCode::OK);
let bytes = axum::body::to_bytes(resp.into_body(), usize::MAX)
.await
.unwrap();
let result: ValidateResponse = serde_json::from_slice(&bytes).unwrap();
assert!(!result.valid);
assert!(result.errors.is_some());
}
#[tokio::test]
async fn validate_blueprint_parses_but_fails_structural_validation_returns_ok_valid_false() {
let app = Router::new().route("/api/blueprints/validate", post(validate_blueprint));
let manifest = r#"
[agent]
name = "bad-entry-stage"
version = "1.0.0"
description = "Entry stage doesn't exist"
entry_stage = "does-not-exist"
[stages.plan]
system_prompt = "Plan"
"#;
let body = serde_json::json!({"manifest": manifest});
let req = Request::builder()
.method("POST")
.uri("/api/blueprints/validate")
.header("content-type", "application/json")
.body(Body::from(serde_json::to_string(&body).unwrap()))
.unwrap();
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status(), axum::http::StatusCode::OK);
let bytes = axum::body::to_bytes(resp.into_body(), usize::MAX)
.await
.unwrap();
let result: ValidateResponse = serde_json::from_slice(&bytes).unwrap();
assert!(!result.valid);
assert!(
result
.errors
.unwrap()
.iter()
.any(|e| e.contains("entry_stage"))
);
}
#[test]
fn agents_dir_is_under_home() {
let dir = agents_dir();
let path_str = dir.to_string_lossy();
assert!(path_str.contains(".leviath"));
assert!(path_str.ends_with("agents"));
}
#[test]
fn read_blueprint_info_from_valid_manifest() {
let dir = tempfile::tempdir().unwrap();
let manifest_path = dir.path().join("agent.leviath");
let content = r#"
[agent]
name = "test-bp"
version = "1.0.0"
description = "A test blueprint"
[stages.plan]
system_prompt = "Plan the work"
"#;
std::fs::write(&manifest_path, content).unwrap();
let info = read_blueprint_info(&manifest_path, dir.path()).unwrap();
assert_eq!(info.name, "test-bp");
assert_eq!(info.version, "1.0.0");
assert_eq!(info.description, "A test blueprint");
assert_eq!(info.stages, vec!["plan"]);
assert_eq!(info.path, dir.path().to_string_lossy());
}
#[test]
fn read_blueprint_info_nonexistent_file_returns_none() {
let dir = tempfile::tempdir().unwrap();
let manifest_path = dir.path().join("nonexistent.leviath");
let result = read_blueprint_info(&manifest_path, dir.path());
assert!(result.is_none());
}
#[test]
fn read_blueprint_info_invalid_toml_returns_none() {
let dir = tempfile::tempdir().unwrap();
let manifest_path = dir.path().join("agent.leviath");
std::fs::write(&manifest_path, "not valid toml [[[").unwrap();
let result = read_blueprint_info(&manifest_path, dir.path());
assert!(result.is_none());
}
#[test]
fn read_blueprint_info_multiple_stages() {
let dir = tempfile::tempdir().unwrap();
let manifest_path = dir.path().join("agent.leviath");
let content = r#"
[agent]
name = "multi-stage"
version = "0.2.0"
description = "Multi-stage"
[stages.plan]
system_prompt = "Plan"
[stages.implement]
system_prompt = "Implement"
[stages.review]
system_prompt = "Review"
"#;
std::fs::write(&manifest_path, content).unwrap();
let info = read_blueprint_info(&manifest_path, dir.path()).unwrap();
assert_eq!(info.name, "multi-stage");
assert_eq!(info.stages.len(), 3);
}
#[test]
fn discover_blueprints_with_custom_path() {
let dir = tempfile::tempdir().unwrap();
let agent_dir = dir.path().join("my-agent");
std::fs::create_dir_all(&agent_dir).unwrap();
let content = r#"
[agent]
name = "discovered"
version = "1.0.0"
description = "Should be discovered"
[stages.work]
system_prompt = "Do work"
"#;
write_test_agent(agent_dir, content);
let config = crate::config::Config {
agent_paths: vec![dir.path().to_path_buf()],
..Default::default()
};
let blueprints = discover_blueprints(&config);
let found = blueprints.iter().find(|b| b.name == "discovered");
assert_discovered_in_custom_path(found.is_some());
}
fn assert_discovered_in_custom_path(found: bool) {
assert!(found, "should discover agent in custom path");
}
#[test]
#[should_panic(expected = "should discover agent in custom path")]
fn assert_discovered_in_custom_path_panics_when_not_found() {
assert_discovered_in_custom_path(false);
}
#[test]
fn discover_blueprints_empty_dir() {
let dir = tempfile::tempdir().unwrap();
let config = crate::config::Config {
agent_paths: vec![dir.path().to_path_buf()],
..Default::default()
};
let blueprints = discover_blueprints(&config);
let _ = blueprints;
}
#[test]
fn discover_blueprints_nonexistent_path_is_skipped() {
let config = crate::config::Config {
agent_paths: vec![PathBuf::from("/nonexistent/path/unlikely_to_exist_12345")],
..Default::default()
};
let _ = discover_blueprints(&config);
}
#[test]
fn discover_blueprints_direct_manifest_in_dir() {
let dir = tempfile::tempdir().unwrap();
let content = r#"
[agent]
name = "direct"
version = "0.1.0"
description = "Directly in scan dir"
[stages.run]
system_prompt = "Run"
"#;
write_test_agent(dir.path(), content);
let config = crate::config::Config {
agent_paths: vec![dir.path().to_path_buf()],
..Default::default()
};
let blueprints = discover_blueprints(&config);
let found = blueprints.iter().find(|b| b.name == "direct");
assert_discovered_directly_in_scan_dir(found.is_some());
}
fn assert_discovered_directly_in_scan_dir(found: bool) {
assert!(found, "should discover agent.leviath directly in scan dir");
}
#[test]
#[should_panic(expected = "should discover agent.leviath directly in scan dir")]
fn assert_discovered_directly_in_scan_dir_panics_when_not_found() {
assert_discovered_directly_in_scan_dir(false);
}
}