pointlock-vision 0.1.10

Pointlock's Anthropic-backed visual verifier (downgrade-only, evidence-backed).
Documentation
//! `OpenAiCompatVisionVerifier` against a local canned-response server
//! (the `base_url` override): the chat-completions request shape
//! (data-URL image part, bearer auth present exactly when a key is
//! configured), answer parsing under the shared look-then-judge
//! protocol, and the fail-closed folds. No API key, no network.

use std::sync::Arc;
use std::sync::Mutex;

use pointlock_ir::VerdictStatus;
use pointlock_vision::{OpenAiCompatVisionVerifier, VisionJudge, VisionRequest, VisionVerifier};

/// One request as the server saw it.
struct SeenRequest {
    authorization: Option<String>,
    url: String,
    body: String,
}

/// A one-shot local chat-completions endpoint: answers `body` with
/// `status`, records each request's Authorization header, URL, and body.
struct CannedServer {
    server: Arc<tiny_http::Server>,
    seen: Arc<Mutex<Vec<SeenRequest>>>,
    url: String,
}

impl CannedServer {
    fn start(status: u16, body: impl Into<String>) -> CannedServer {
        let body: String = body.into();
        let server = Arc::new(tiny_http::Server::http("127.0.0.1:0").expect("bind canned server"));
        // The configured base includes the ecosystem-conventional `/v1`.
        let url = format!(
            "http://127.0.0.1:{}/v1",
            server.server_addr().to_ip().expect("ip").port()
        );
        let seen = Arc::new(Mutex::new(Vec::new()));
        {
            let server = Arc::clone(&server);
            let seen = Arc::clone(&seen);
            std::thread::spawn(move || {
                while let Ok(mut request) = server.recv() {
                    let authorization = request
                        .headers()
                        .iter()
                        .find(|header| header.field.equiv("Authorization"))
                        .map(|header| header.value.as_str().to_owned());
                    let url = request.url().to_owned();
                    let mut raw = String::new();
                    let _ = request.as_reader().read_to_string(&mut raw);
                    seen.lock().expect("seen").push(SeenRequest {
                        authorization,
                        url,
                        body: raw,
                    });
                    let response = tiny_http::Response::from_string(body.clone())
                        .with_status_code(tiny_http::StatusCode(status));
                    let _ = request.respond(response);
                }
            });
        }
        CannedServer { server, seen, url }
    }

    fn verifier(&self, api_key: Option<&str>) -> OpenAiCompatVisionVerifier {
        OpenAiCompatVisionVerifier::new(api_key.map(str::to_owned), "qwen2.5-vl-32b", &self.url)
    }
}

impl Drop for CannedServer {
    fn drop(&mut self) {
        self.server.unblock();
    }
}

fn request<'a>() -> VisionRequest<'a> {
    VisionRequest {
        prompt: "the SSID field visibly contains the requested name",
        region: None,
        screenshot: b"fake-png-bytes",
        media_type: "image/png",
    }
}

fn completions_body(content: serde_json::Value) -> String {
    serde_json::json!({
        "id": "chatcmpl-test",
        "object": "chat.completion",
        "choices": [{
            "index": 0,
            "message": { "role": "assistant", "content": content },
            "finish_reason": "stop",
        }],
    })
    .to_string()
}

#[tokio::test]
async fn parses_the_verdict_with_judge_identity_and_observations() {
    let server = CannedServer::start(
        200,
        completions_body(serde_json::json!(
            "OBSERVED: the SSID field shows HomeWifi\n\
             PASS: the field shows the requested name"
        )),
    );
    let verdict = server.verifier(None).verify(request()).await;
    assert_eq!(verdict.status, VerdictStatus::Pass);
    assert!(verdict.reason.starts_with("vision: "), "{}", verdict.reason);
    assert_eq!(
        verdict.judge,
        Some(VisionJudge {
            provider: "openai-compat".to_owned(),
            model: Some("qwen2.5-vl-32b".to_owned()),
        })
    );
    assert_eq!(
        verdict.observations,
        vec!["the SSID field shows HomeWifi".to_owned()]
    );
}

#[tokio::test]
async fn array_content_parts_are_concatenated() {
    let server = CannedServer::start(
        200,
        completions_body(serde_json::json!([
            { "type": "text", "text": "FAIL: the field " },
            { "type": "text", "text": "is empty" },
        ])),
    );
    let verdict = server.verifier(None).verify(request()).await;
    assert_eq!(verdict.status, VerdictStatus::Fail);
    assert_eq!(verdict.reason, "vision: the field is empty");
}

#[tokio::test]
async fn oversized_response_bodies_fold_to_unknown() {
    let padded = format!("PASS: ok{}", " ".repeat(1024 * 1024 + 64));
    let server = CannedServer::start(200, completions_body(serde_json::json!(padded)));
    let verdict = server.verifier(None).verify(request()).await;
    assert_eq!(verdict.status, VerdictStatus::Unknown);
    assert!(verdict.reason.contains("exceeds"), "{}", verdict.reason);
}

#[tokio::test]
async fn chatty_answers_fold_to_unknown() {
    let server = CannedServer::start(
        200,
        completions_body(serde_json::json!(
            "Sure! Looking at the screenshot, I believe it passes."
        )),
    );
    let verdict = server.verifier(None).verify(request()).await;
    assert_eq!(verdict.status, VerdictStatus::Unknown);
    assert!(verdict.reason.contains("unparseable"), "{}", verdict.reason);
}

#[tokio::test]
async fn non_success_status_folds_to_unknown() {
    let server = CannedServer::start(429, r#"{"error":{"type":"rate_limit"}}"#);
    let verdict = server.verifier(None).verify(request()).await;
    assert_eq!(verdict.status, VerdictStatus::Unknown);
    assert!(verdict.reason.contains("429"), "{}", verdict.reason);
}

#[tokio::test]
async fn transport_failure_folds_to_unknown() {
    // A port nothing listens on.
    let verifier = OpenAiCompatVisionVerifier::new(None, "m", "http://127.0.0.1:9/v1");
    let verdict = verifier.verify(request()).await;
    assert_eq!(verdict.status, VerdictStatus::Unknown);
    assert!(verdict.reason.contains("transport"), "{}", verdict.reason);
}

#[tokio::test]
async fn the_request_carries_data_url_model_instruction_and_bearer_auth() {
    let server = CannedServer::start(200, completions_body(serde_json::json!("PASS: ok")));
    let _ = server.verifier(Some("test-key")).verify(request()).await;
    let seen = server.seen.lock().expect("seen");
    assert_eq!(seen[0].authorization.as_deref(), Some("Bearer test-key"));
    assert_eq!(seen[0].url, "/v1/chat/completions");
    let sent: serde_json::Value = serde_json::from_str(&seen[0].body).expect("request JSON");
    assert_eq!(sent["model"], "qwen2.5-vl-32b");
    let content = sent["messages"][0]["content"].as_array().expect("content");
    assert_eq!(content[0]["type"], "image_url");
    let url = content[0]["image_url"]["url"].as_str().expect("data url");
    assert!(url.starts_with("data:image/png;base64,"), "{url}");
    let text = content[1]["text"].as_str().expect("instruction");
    assert!(text.contains("the SSID field visibly contains"), "{text}");
    assert!(text.contains("OBSERVED:"), "{text}");
    assert!(text.contains("Answer UNKNOWN unless"), "{text}");
}

#[tokio::test]
async fn keyless_requests_send_no_authorization_header() {
    let server = CannedServer::start(200, completions_body(serde_json::json!("PASS: ok")));
    let _ = server.verifier(None).verify(request()).await;
    let seen = server.seen.lock().expect("seen");
    assert_eq!(seen[0].authorization, None);
}