acorn-lib 0.3.2

ACORN library
use super::{Client, EndpointMode, InferenceError, Options};
use crate::{
    agent::{HydratedPrompt, InferenceBackend, InferenceFinishReason, InferencePermissionPolicy, InferenceRequest, InferenceUsage},
    analyzer::fix::{CheckReference, CheckReferences, FixOptions, IntoFixes},
    io::api::Configuration,
    test::server::TestServer,
};
use acorn_schema::agent::{FrontMatter, PromptFileAsset};
use alloc::sync::Arc;
use axum::{extract::State as AxumState, http::StatusCode, routing::post, Json, Router};
use serde_json::{json, Value};
use std::sync::{Mutex, PoisonError};

const FIX_RESPONSE: &str = r#"{"fixes":[{"actions":[{"instructions":"Replace the title with a valid value","kind":"manual"}],"automation":"manual","confidence":0.9,"effects":{"elevation":false,"filesystem_write":false,"network":false,"package_manager":false,"process":false,"restart":false,"reversible":true},"finding_id":"check-0001","id":"fix-0001","rationale":"The title requires human review.","risk":"low","source_fingerprint":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","status":"proposed","summary":"Review the title","target":"activity.json"}],"version":"1"}"#;

#[derive(Clone, Default)]
struct ServerState {
    requests: Arc<Mutex<Vec<(EndpointMode, Value)>>>,
}
async fn chat(AxumState(state): AxumState<ServerState>, Json(body): Json<Value>) -> (StatusCode, Json<Value>) {
    let prompt = body
        .pointer("/messages/0/content")
        .and_then(Value::as_str)
        .unwrap_or_default()
        .to_string();
    state
        .requests
        .lock()
        .unwrap_or_else(PoisonError::into_inner)
        .push((EndpointMode::ChatCompletions, body));
    response_for(EndpointMode::ChatCompletions, &prompt).await
}
fn client(server: &TestServer, mode: EndpointMode) -> Client {
    Client::new(Options::default().with_domain(&server.base_url), mode, "fake-model")
}
fn fix_reference() -> CheckReference {
    CheckReference {
        category: "schema".to_string(),
        context: Some("Old title".to_string()),
        fingerprint: "a".repeat(64),
        id: "check-0001".to_string(),
        locator: Some("title".to_string()),
        message: "Title is invalid".to_string(),
        severity: "error".to_string(),
        standard: "research-activity-data".to_string(),
        uri: Some("activity.json".to_string()),
    }
}
fn request(body: &str) -> InferenceRequest {
    InferenceRequest {
        agent: None,
        model: None,
        permission_policy: InferencePermissionPolicy::DenyMutation,
        prompt: HydratedPrompt {
            asset: PromptFileAsset::Summarize,
            body: body.to_string(),
            metadata: FrontMatter::init().name("test".to_string()).build(),
        },
        response_schema: None,
        timeout_ms: 10_000,
    }
}
async fn response_for(mode: EndpointMode, prompt: &str) -> (StatusCode, Json<Value>) {
    if prompt == "slow" {
        tokio::time::sleep(core::time::Duration::from_millis(100)).await;
    }
    let content = match prompt.contains("You propose reviewable fixes") {
        | true => FIX_RESPONSE,
        | false => match prompt {
            | "invalid-structured" => r#"{"wrong":true}"#,
            | "structured" => r#"{"answer":42}"#,
            | _ => "same response",
        },
    };
    match (prompt, mode) {
        | ("error", _) => (
            StatusCode::TOO_MANY_REQUESTS,
            Json(json!({"error":{"code":"rate_limit","message":"try later"}})),
        ),
        | ("malformed", _) => (StatusCode::OK, Json(json!({"unexpected":true}))),
        | ("refuse", EndpointMode::ChatCompletions) => (
            StatusCode::OK,
            Json(json!({
                "id":"chat-refusal",
                "model":"fake-model",
                "choices":[{"finish_reason":"content_filter","message":{"content":null,"refusal":"request refused"}}],
                "usage":{"prompt_tokens":4,"completion_tokens":2}
            })),
        ),
        | ("refuse", EndpointMode::Responses) => (
            StatusCode::OK,
            Json(json!({
                "id":"resp-refusal",
                "model":"fake-model",
                "status":"completed",
                "error":null,
                "incomplete_details":null,
                "output":[{"type":"message","content":[{"type":"refusal","refusal":"request refused"}]}],
                "usage":{"input_tokens":4,"output_tokens":2}
            })),
        ),
        | (_, EndpointMode::ChatCompletions) => (
            StatusCode::OK,
            Json(json!({
                "id":"chat-test",
                "model":"fake-model",
                "choices":[{"finish_reason":"stop","message":{"content":content,"refusal":null}}],
                "usage":{"prompt_tokens":4,"completion_tokens":2}
            })),
        ),
        | (_, EndpointMode::Responses) => (
            StatusCode::OK,
            Json(json!({
                "id":"resp-test",
                "model":"fake-model",
                "status":"completed",
                "error":null,
                "incomplete_details":null,
                "output":[{"type":"message","content":[{"type":"output_text","text":content}]}],
                "usage":{"input_tokens":4,"output_tokens":2}
            })),
        ),
    }
}
async fn responses(AxumState(state): AxumState<ServerState>, Json(body): Json<Value>) -> (StatusCode, Json<Value>) {
    let prompt = body.get("input").and_then(Value::as_str).unwrap_or_default().to_string();
    state
        .requests
        .lock()
        .unwrap_or_else(PoisonError::into_inner)
        .push((EndpointMode::Responses, body));
    response_for(EndpointMode::Responses, &prompt).await
}
fn schema() -> Value {
    json!({
        "additionalProperties": false,
        "properties": {"answer": {"type": "integer"}},
        "required": ["answer"],
        "type": "object"
    })
}
async fn server() -> (TestServer, ServerState) {
    let state = ServerState::default();
    let router = Router::new()
        .route("/v1/chat/completions", post(chat))
        .route("/v1/responses", post(responses))
        .with_state(state.clone());
    (TestServer::start(router).await.expect("start OpenAI-compatible test server"), state)
}

#[tokio::test]
async fn test_capabilities_and_debug_keep_configuration_explicit_and_secret_free() {
    let (server, _) = server().await;
    let options = Options::with_token("private-token").build().with_domain(&server.base_url);
    let client = Client::new(options, EndpointMode::Responses, "fake-model").with_structured_output(true);
    let capabilities = client.capabilities();
    assert!(capabilities.structured_output);
    assert!(!capabilities.attachments);
    assert!(!capabilities.mutating_tools);
    assert!(!capabilities.streaming);
    assert!(!capabilities.tools);
    assert!(!format!("{client:?}").contains("private-token"));
    server.stop().await.expect("stop test server");
}
#[tokio::test]
async fn test_endpoint_modes_forward_hydrated_prompt_body_verbatim() {
    let (server, state) = server().await;
    let body = "Instructions\n\n# Input Data (ZON)\n\n```zon\ntext:source\n```";
    client(&server, EndpointMode::Responses)
        .infer_checked(&request(body))
        .await
        .expect("responses inference");
    client(&server, EndpointMode::ChatCompletions)
        .infer_checked(&request(body))
        .await
        .expect("chat inference");
    {
        let requests = state.requests.lock().unwrap_or_else(PoisonError::into_inner);
        let responses_body = requests
            .iter()
            .find_map(|(mode, request)| (*mode == EndpointMode::Responses).then_some(request))
            .expect("responses request body");
        let chat_body = requests
            .iter()
            .find_map(|(mode, request)| (*mode == EndpointMode::ChatCompletions).then_some(request))
            .expect("chat request body");
        assert_eq!(responses_body.get("input").and_then(Value::as_str), Some(body));
        assert_eq!(chat_body.pointer("/messages/0/content").and_then(Value::as_str), Some(body));
    }
    server.stop().await.expect("stop test server");
}
#[tokio::test]
async fn test_endpoint_modes_return_equivalent_normalized_results() {
    let (server, _) = server().await;
    let responses_result = client(&server, EndpointMode::Responses)
        .infer_checked(&request("plain"))
        .await
        .expect("responses inference");
    let chat_result = client(&server, EndpointMode::ChatCompletions)
        .infer_checked(&request("plain"))
        .await
        .expect("chat inference");
    assert_eq!(responses_result.text, chat_result.text);
    assert_eq!(responses_result.finish_reason, chat_result.finish_reason);
    assert_eq!(responses_result.structured, chat_result.structured);
    assert_eq!(responses_result.usage, chat_result.usage);
    assert_eq!(responses_result.provenance.backend, "openai");
    assert_eq!(chat_result.provenance.backend, "openai");
    assert_eq!(responses_result.provenance.model, chat_result.provenance.model);
    server.stop().await.expect("stop test server");
}
#[tokio::test]
async fn test_endpoint_modes_return_equivalent_validated_fix_reports() {
    let (server, _) = server().await;
    let reference = fix_reference();
    let options = FixOptions {
        min_confidence: 0.5,
        ..FixOptions::default()
    };
    let responses = vec![reference.clone()]
        .suggest(&client(&server, EndpointMode::Responses).with_structured_output(true), &options)
        .await
        .expect("responses fix suggestions");
    let chat = vec![reference]
        .suggest(&client(&server, EndpointMode::ChatCompletions).with_structured_output(true), &options)
        .await
        .expect("chat fix suggestions");
    assert!(responses.diagnostics.is_empty());
    assert!(chat.diagnostics.is_empty());
    let responses_fixes = responses.fixes();
    let chat_fixes = chat.fixes();
    let responses_fix = responses_fixes.first().expect("one Responses fix proposal");
    let chat_fix = chat_fixes.first().expect("one Chat Completions fix proposal");
    assert_eq!(responses_fix.id, chat_fix.id);
    assert_eq!(responses_fix.actions, chat_fix.actions);
    assert_eq!(responses_fix.status, chat_fix.status);
    server.stop().await.expect("stop test server");
}
#[tokio::test]
async fn test_endpoint_modes_send_distinct_structured_output_shapes() {
    let (server, state) = server().await;
    let structured = InferenceRequest {
        response_schema: Some(schema()),
        ..request("structured")
    };
    let responses_result = client(&server, EndpointMode::Responses)
        .with_structured_output(true)
        .infer_checked(&structured)
        .await
        .expect("responses structured inference");
    let chat_result = client(&server, EndpointMode::ChatCompletions)
        .with_structured_output(true)
        .infer_checked(&structured)
        .await
        .expect("chat structured inference");
    assert_eq!(responses_result.structured, Some(json!({"answer":42})));
    assert_eq!(chat_result.structured, responses_result.structured);
    {
        let requests = state.requests.lock().unwrap_or_else(PoisonError::into_inner);
        let responses_body = requests
            .iter()
            .find_map(|(mode, body)| (*mode == EndpointMode::Responses).then_some(body))
            .expect("responses request body");
        let chat_body = requests
            .iter()
            .find_map(|(mode, body)| (*mode == EndpointMode::ChatCompletions).then_some(body))
            .expect("chat request body");
        assert_eq!(responses_body.pointer("/text/format/type").and_then(Value::as_str), Some("json_schema"));
        assert_eq!(responses_body.pointer("/text/format/schema"), Some(&schema()));
        assert_eq!(chat_body.pointer("/response_format/type").and_then(Value::as_str), Some("json_schema"));
        assert_eq!(chat_body.pointer("/response_format/json_schema/schema"), Some(&schema()));
        assert_eq!(responses_body.get("store"), Some(&Value::Bool(false)));
        assert_eq!(chat_body.get("store"), Some(&Value::Bool(false)));
    }
    server.stop().await.expect("stop test server");
}
#[tokio::test]
async fn test_invalid_structured_output_and_schema_are_rejected_locally() {
    let (server, state) = server().await;
    let invalid_output = InferenceRequest {
        response_schema: Some(schema()),
        ..request("invalid-structured")
    };
    let output_error = client(&server, EndpointMode::Responses)
        .infer_checked(&invalid_output)
        .await
        .expect_err("invalid structured output should fail");
    assert!(matches!(output_error, InferenceError::SchemaViolation(_)));
    let invalid_schema = InferenceRequest {
        response_schema: Some(json!({"type":"invented"})),
        ..request("structured")
    };
    let schema_error = client(&server, EndpointMode::Responses)
        .infer_checked(&invalid_schema)
        .await
        .expect_err("invalid schema should fail");
    assert!(matches!(schema_error, InferenceError::InvalidSchema(_)));
    assert_eq!(state.requests.lock().unwrap_or_else(PoisonError::into_inner).len(), 1);
    server.stop().await.expect("stop test server");
}
#[tokio::test]
async fn test_malformed_success_response_is_rejected() {
    let (server, _) = server().await;
    let result = client(&server, EndpointMode::ChatCompletions).infer_checked(&request("malformed")).await;
    assert!(matches!(result, Err(InferenceError::InvalidResponse(_))));
    server.stop().await.expect("stop test server");
}
#[tokio::test]
async fn test_offline_policy_allows_loopback_and_rejects_remote_endpoints() {
    let (server, _) = server().await;
    let local = client(&server, EndpointMode::Responses).with_offline(true);
    assert!(local.infer_checked(&request("plain")).await.is_ok());
    let remote = Client::new(Options::default(), EndpointMode::Responses, "fake-model").with_offline(true);
    assert!(matches!(remote.infer_checked(&request("plain")).await, Err(InferenceError::Offline(_))));
    server.stop().await.expect("stop test server");
}
#[tokio::test]
async fn test_provider_errors_and_refusals_are_typed() {
    let (server, _) = server().await;
    let client = client(&server, EndpointMode::Responses);
    let provider = client.infer_checked(&request("error")).await.expect_err("provider error");
    assert!(matches!(
        provider,
        InferenceError::Provider {
            code: Some(code),
            status: 429,
            ..
        } if code == "rate_limit"
    ));
    let refusal = client.infer_checked(&request("refuse")).await.expect("typed refusal");
    assert_eq!(refusal.finish_reason, InferenceFinishReason::Refusal);
    assert_eq!(refusal.text, "request refused");
    assert_eq!(
        refusal.usage,
        Some(InferenceUsage {
            input_tokens: Some(4),
            output_tokens: Some(2)
        })
    );
    server.stop().await.expect("stop test server");
}
#[tokio::test]
async fn test_remote_authentication_and_transport_security_are_checked_before_io() {
    let missing = Client::new(Options::default(), EndpointMode::Responses, "fake-model");
    assert!(matches!(
        missing.infer_checked(&request("plain")).await,
        Err(InferenceError::MissingAuthentication)
    ));
    let insecure = Client::new(
        Options::with_token("private-token").build().with_domain("http://example.com"),
        EndpointMode::Responses,
        "fake-model",
    );
    assert!(matches!(
        insecure.infer_checked(&request("plain")).await,
        Err(InferenceError::InsecureEndpoint(_))
    ));
}
#[tokio::test]
async fn test_structured_output_fallback_parses_without_advertised_capability() {
    let (server, state) = server().await;
    let structured = InferenceRequest {
        response_schema: Some(schema()),
        ..request("structured")
    };
    let result = client(&server, EndpointMode::Responses)
        .infer_checked(&structured)
        .await
        .expect("structured fallback");
    assert_eq!(result.structured, Some(json!({"answer":42})));
    {
        let requests = state.requests.lock().unwrap_or_else(PoisonError::into_inner);
        assert_eq!(requests.first().and_then(|(_, body)| body.get("text")), None);
    }
    server.stop().await.expect("stop test server");
}
#[tokio::test]
async fn test_timeout_is_enforced() {
    let (server, _) = server().await;
    let timed = InferenceRequest {
        timeout_ms: 10,
        ..request("slow")
    };
    let result = client(&server, EndpointMode::Responses).infer_checked(&timed).await;
    assert!(matches!(result, Err(InferenceError::Timeout(_))));
    server.stop().await.expect("stop test server");
}