sniff-cli 0.1.3

An exhaustive LLM-backed slop finder for codebases
Documentation
#[path = "llm_call_policy.rs"]
mod llm_call_policy;
#[path = "llm_call_state.rs"]
mod llm_call_state;

use super::{LLMClient, ResponseSchema};

pub(super) async fn execute_call(
    client: &LLMClient,
    prompt: &str,
    schema: ResponseSchema,
) -> Result<(Option<serde_json::Value>, usize, usize), String> {
    llm_call_state::execute_call(client, prompt, schema).await
}

#[cfg(test)]
mod tests {
    use crate::config::{LLMConfig, ResolvedConfig, ThresholdsConfig};
    use crate::llm::{LLMClient, ResponseSchema};
    use std::io::{Read, Write};
    use std::net::{Shutdown, TcpListener};
    use std::sync::Arc;
    use std::sync::atomic::{AtomicUsize, Ordering};
    use std::sync::mpsc;
    use std::thread;

    fn cfg(endpoint: &str) -> ResolvedConfig {
        ResolvedConfig {
            thresholds: ThresholdsConfig::default(),
            ignore: vec![],
            generic_names: vec![],
            generic_file_names: vec![],
            model: "test-model".to_string(),
            llm: LLMConfig {
                system_context: String::new(),
                endpoint: endpoint.to_string(),
            },
        }
    }

    fn spawn_retry_server() -> (String, Arc<AtomicUsize>) {
        let listener = TcpListener::bind("127.0.0.1:0").unwrap();
        let addr = listener.local_addr().unwrap();
        let hits = Arc::new(AtomicUsize::new(0));
        let hits_clone = Arc::clone(&hits);
        let (ready_tx, ready_rx) = mpsc::channel();

        thread::spawn(move || {
            let _ = ready_tx.send(());
            loop {
                let Ok((mut stream, _)) = listener.accept() else {
                    return;
                };
                let hit = hits_clone.fetch_add(1, Ordering::SeqCst);
                let mut buf = [0u8; 4096];
                let _ = stream.read(&mut buf);
                let body = if hit == 0 {
                    r#"{"choices":[{"message":{"content":"plain text no json here"}}]}"#
                } else {
                    r#"{"choices":[{"message":{"content":"{\"smelly\":false,\"tier\":\"clean\",\"evidence\":\"\",\"reason\":\"clean\",\"cohesive\":true,\"name_accurate\":true}"}}]}"#
                };
                let response = format!(
                    "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
                    body.len(),
                    body
                );
                let _ = stream.write_all(response.as_bytes());
                let _ = stream.flush();
                let _ = stream.shutdown(Shutdown::Both);
            }
        });
        let _ = ready_rx.recv();

        (format!("http://{}", addr), hits)
    }

    #[test]
    fn retry_same_prompt_includes_decode_failures() {
        assert!(super::super::llm_retry::retry_same_prompt_without_repair(
            "error decoding response body"
        ));
        assert!(super::super::llm_retry::retry_same_prompt_without_repair(
            "No JSON object found in response."
        ));
        assert!(super::super::llm_retry::retry_same_prompt_without_repair(
            "Timed out reading response body from Anthropic endpoint"
        ));
        assert!(super::super::llm_retry::retry_same_prompt_without_repair(
            "Invalid JSON response from OpenAI-style endpoint"
        ));
        assert!(super::super::llm_retry::retry_same_prompt_without_repair(
            "Empty assistant content"
        ));
        assert!(!super::super::llm_retry::retry_same_prompt_without_repair(
            "HTTP 500 from endpoint"
        ));
    }

    #[tokio::test]
    async fn no_json_object_retries_the_same_prompt_before_repair() {
        let (endpoint, hits) = spawn_retry_server();
        let client = LLMClient::new(cfg(&endpoint), Some("test-key".to_string()));

        let (result, _, _) = client
            .call_once("return exactly one JSON object", ResponseSchema::FileReview)
            .await
            .expect("expected retry path to recover");

        assert!(result.is_some());
        assert!(hits.load(Ordering::SeqCst) >= 2);
    }
}