unified-agent-api 0.2.3

Agent-agnostic facade and registry for wrapper backends
Documentation
use std::{
    fs,
    path::{Path, PathBuf},
};

use serde_json::json;

use super::super::{normalize_model_id_v1, normalize_request};
use super::support::PolicyFnAdapter;
use crate::backend_harness::BackendDefaults;
use crate::{AgentWrapperError, AgentWrapperRunRequest, EXT_AGENT_API_CONFIG_MODEL_V1};

fn collect_rs_files(dir: &Path, files: &mut Vec<PathBuf>) {
    for entry in fs::read_dir(dir).expect("read source directory") {
        let entry = entry.expect("read source entry");
        let path = entry.path();
        if path.is_dir() {
            collect_rs_files(&path, files);
        } else if path.extension().and_then(|ext| ext.to_str()) == Some("rs") {
            files.push(path);
        }
    }
}

#[test]
fn normalize_model_id_v1_absent_returns_none() {
    let normalized = normalize_model_id_v1(None).expect("absent model id is allowed");
    assert_eq!(normalized, None);
}

#[test]
fn normalize_model_id_v1_rejects_non_string_without_echoing_value() {
    let raw = json!(false);
    let err = normalize_model_id_v1(Some(&raw)).expect_err("expected string parse failure");
    match err {
        AgentWrapperError::InvalidRequest { message } => {
            assert_eq!(message, "invalid agent_api.config.model.v1");
            assert!(!message.contains("false"));
        }
        other => panic!("expected InvalidRequest, got: {other:?}"),
    }
}

#[test]
fn normalize_model_id_v1_rejects_whitespace_only_after_trim() {
    let err = normalize_model_id_v1(Some(&json!("  \t \n  ")))
        .expect_err("expected whitespace-only failure");
    match err {
        AgentWrapperError::InvalidRequest { message } => {
            assert_eq!(message, "invalid agent_api.config.model.v1");
        }
        other => panic!("expected InvalidRequest, got: {other:?}"),
    }
}

#[test]
fn normalize_model_id_v1_rejects_oversize_after_trim_without_echoing_value() {
    let raw = format!("  {}  ", "a".repeat(129));
    let err =
        normalize_model_id_v1(Some(&json!(raw.clone()))).expect_err("expected oversize failure");
    match err {
        AgentWrapperError::InvalidRequest { message } => {
            assert_eq!(message, "invalid agent_api.config.model.v1");
            assert!(!message.contains(&raw));
        }
        other => panic!("expected InvalidRequest, got: {other:?}"),
    }
}

#[test]
fn normalize_model_id_v1_trims_and_returns_success() {
    let normalized =
        normalize_model_id_v1(Some(&json!("  agent-model-1  "))).expect("expected trimmed success");
    assert_eq!(normalized, Some("agent-model-1".to_string()));
}

#[test]
fn bh_c03_agent_api_config_model_v1_invalid_values_use_safe_template_via_normalize_request() {
    const SUPPORTED: [&str; 1] = [EXT_AGENT_API_CONFIG_MODEL_V1];
    let adapter = PolicyFnAdapter::panic_on_policy(&SUPPORTED);
    let defaults = BackendDefaults::default();
    let secret = "SECRET_MODEL_ID_SHOULD_NOT_LEAK";
    let invalid_cases = vec![
        ("null", json!(null), Some("null".to_string())),
        ("bool", json!(false), Some("false".to_string())),
        ("number", json!(123), Some("123".to_string())),
        (
            "object",
            json!({ "model": secret }),
            Some(secret.to_string()),
        ),
        (
            "array",
            json!(["agent-model", secret]),
            Some(secret.to_string()),
        ),
        ("whitespace_only", json!("  \t \n  "), None),
        (
            "oversize_after_trim",
            json!(format!("  {}  ", "x".repeat(129))),
            Some("x".repeat(129)),
        ),
    ];

    for (name, raw, leak_probe) in invalid_cases {
        let mut request = AgentWrapperRunRequest {
            prompt: "hello".to_string(),
            ..Default::default()
        };
        request
            .extensions
            .insert(EXT_AGENT_API_CONFIG_MODEL_V1.to_string(), raw);

        let err = match normalize_request(&adapter, &defaults, request) {
            Ok(_) => panic!("expected invalid model id for case {name}"),
            Err(err) => err,
        };

        match err {
            AgentWrapperError::InvalidRequest { message } => {
                assert_eq!(message, "invalid agent_api.config.model.v1");
                if let Some(leak_probe) = leak_probe {
                    assert!(
                        !message.contains(&leak_probe),
                        "case {name} leaked raw value into InvalidRequest message"
                    );
                }
            }
            other => panic!("expected InvalidRequest for case {name}, got: {other:?}"),
        }
    }
}

#[test]
fn bh_c03_agent_api_config_model_v1_trims_before_mapping_via_normalize_request() {
    const SUPPORTED: [&str; 1] = [EXT_AGENT_API_CONFIG_MODEL_V1];
    let adapter = PolicyFnAdapter::ok_policy(&SUPPORTED);
    let defaults = BackendDefaults::default();
    let mut request = AgentWrapperRunRequest {
        prompt: "hello".to_string(),
        ..Default::default()
    };
    request.extensions.insert(
        EXT_AGENT_API_CONFIG_MODEL_V1.to_string(),
        json!("  agent-model-1  "),
    );

    let normalized =
        normalize_request(&adapter, &defaults, request).expect("expected normalized request");
    assert_eq!(normalized.model_id, Some("agent-model-1".to_string()));
}

#[test]
fn bh_r0_agent_api_config_model_v1_is_rejected_before_value_shape_validation_via_normalize_request()
{
    const SUPPORTED: [&str; 1] = ["backend.toy.example"];
    let adapter = PolicyFnAdapter::panic_on_policy(&SUPPORTED);
    let defaults = BackendDefaults::default();

    for raw in [json!(false), json!("  \t \n  "), json!("x".repeat(256))] {
        let mut request = AgentWrapperRunRequest {
            prompt: "hello".to_string(),
            ..Default::default()
        };
        request
            .extensions
            .insert(EXT_AGENT_API_CONFIG_MODEL_V1.to_string(), raw.clone());

        let err = match normalize_request(&adapter, &defaults, request) {
            Ok(_) => panic!("unsupported model key must fail closed"),
            Err(err) => err,
        };
        match err {
            AgentWrapperError::UnsupportedCapability {
                agent_kind,
                capability,
            } => {
                assert_eq!(agent_kind, "toy");
                assert_eq!(capability, EXT_AGENT_API_CONFIG_MODEL_V1);
            }
            other => panic!("expected UnsupportedCapability, got: {other:?}"),
        }
    }
}

#[test]
fn bh_r0_agent_api_config_model_v1_is_confined_to_normalize_rs_in_production_code() {
    let src_root = Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
    let allowed_normalize = src_root.join("backend_harness/normalize.rs");
    let allowed_constant_home = src_root.join("lib.rs");

    let mut files = Vec::new();
    collect_rs_files(&src_root, &mut files);

    let mut literal_offenders = Vec::new();
    let mut raw_access_offenders = Vec::new();
    for path in files {
        let is_test_source = path
            .components()
            .any(|component| component.as_os_str() == "tests")
            || path.file_name().and_then(|name| name.to_str()) == Some("tests.rs");

        if is_test_source {
            continue;
        }

        let contents = fs::read_to_string(&path).expect("read source file");
        if path != allowed_normalize
            && path != allowed_constant_home
            && contents.contains(EXT_AGENT_API_CONFIG_MODEL_V1)
        {
            literal_offenders.push(
                path.strip_prefix(&src_root)
                    .unwrap_or(&path)
                    .display()
                    .to_string(),
            );
        }

        if path != allowed_normalize
            && (contents.contains("extensions.get(EXT_AGENT_API_CONFIG_MODEL_V1)")
                || contents.contains("extensions.get(crate::EXT_AGENT_API_CONFIG_MODEL_V1)")
                || contents.contains("extensions.contains_key(EXT_AGENT_API_CONFIG_MODEL_V1)")
                || contents
                    .contains("extensions.contains_key(crate::EXT_AGENT_API_CONFIG_MODEL_V1)")
                || contents.contains("[EXT_AGENT_API_CONFIG_MODEL_V1]")
                || contents.contains("[crate::EXT_AGENT_API_CONFIG_MODEL_V1]"))
        {
            raw_access_offenders.push(
                path.strip_prefix(&src_root)
                    .unwrap_or(&path)
                    .display()
                    .to_string(),
            );
        }
    }

    assert!(
        literal_offenders.is_empty(),
        "agent_api.config.model.v1 literal leaked outside normalize.rs/lib.rs and tests: {literal_offenders:?}"
    );
    assert!(
        raw_access_offenders.is_empty(),
        "raw model-selection extension access escaped normalize.rs: {raw_access_offenders:?}"
    );
}