use std::time::Duration;
use rusqlite::params;
use serde_json::json;
use wiremock::matchers::method;
use wiremock::{Mock, MockServer, ResponseTemplate};
use super::jev::JevClassifier;
use super::jev_obfuscate::{KnownNames, Obfuscator};
use super::jev_patterns::{classify_dotted, Dotted};
use super::jev_tests::{
bodies, categories, classifier, prep, probs, reply, run_pipeline_with, server_with,
spent_tokens, TEST_KEY,
};
use super::llm_prompt::LlmOutcome;
use crate::core::config::JevOptions;
use crate::core::db::Database;
fn run(names: &KnownNames, text: &str) -> String {
let mut o = Obfuscator::new(names).expect("builds");
o.obfuscate(text).expect("obfuscates").as_str().to_string()
}
fn plain(text: &str) -> String {
run(&KnownNames::default(), text)
}
fn assert_absent(out: &str, secrets: &[&str]) {
let lower = out.to_lowercase();
for s in secrets {
assert!(
!lower.contains(&s.to_lowercase()),
"{s:?} survived in {out:?}"
);
}
}
#[tokio::test]
async fn names_from_earlier_runs_are_redacted() {
let server = server_with(reply("CAT_1", probs("CAT_1"), 150)).await;
let setup = |db: &Database| {
let c = db.connection();
c.execute(
"INSERT INTO classifications (category, confidence, method) \
VALUES ('bugfix', 0.9, 'exact')",
[],
)
.expect("classification");
let cid = c.last_insert_rowid();
c.execute(
"INSERT INTO commits (sha, author_name, author_email, timestamp, message, \
repository, classification_id) VALUES ('sha-old', 'Qirin Vossberg', \
'qvossberg@corp.test', '2023-01-01T00:00:00Z', 'fix: cache', 'ledger-core', ?1)",
params![cid],
)
.expect("old commit");
c.execute(
"INSERT INTO authors (canonical_name, canonical_email, aliases) VALUES \
('Tamsin Quell', 'tamsin@corp.test', '[\"tquell\", \"tq.alt@corp.test\"]')",
[],
)
.expect("author");
};
let rows = [(
"Revert qvossberg's cache change; Vossberg, Tamsin, tquell and tq.alt agree",
"b",
"b@x",
false,
)];
let (_, _db) = run_pipeline_with(&server, &rows, setup).await;
let sent = bodies(&server).await;
assert_eq!(sent.len(), 1);
let text = sent[0].to_string();
assert_absent(
&text,
&["qvossberg", "vossberg", "tamsin", "tquell", "tq.alt"],
);
}
#[test]
fn trailer_tokens_any_case_and_spacing() {
let out = plain(
"fix: cache\n\n\
Approved by: qvoss\n\
requested BY: qarn\n\
Paired-with: qbel\n\
Thanks-to: qcor\n\
Reviewer: qdun\n\
Owners: qeli qfen\n\
assignee: qgor\n\
APPROVERS: qhal, qjin\n\
Author: qkap",
);
assert_absent(
&out,
&[
"qvoss", "qarn", "qbel", "qcor", "qdun", "qeli", "qfen", "qgor", "qhal", "qjin", "qkap",
],
);
assert!(out.contains("Approved by: PERSON_1"), "{out}");
}
#[test]
fn ticket_keys_in_branch_names() {
let out = plain("merge PROJ-1234_fix and feature/qproj-4321 into main");
assert_absent(&out, &["proj-1234", "qproj-4321"]);
assert!(out.contains("TICKET_1_fix"), "{out}");
}
#[test]
fn hosts_in_any_case() {
let out = plain("connect SQLPROD01.CORP.QACME.COM and Qacme.com");
assert_absent(&out, &["sqlprod01", "qacme"]);
let out = plain(
"join SQL01.QACME.LOCAL in realm QACME.LOCAL; mount Qfileserver.local, \
Api.Qacme.Dev and QLEDGER.PROD",
);
assert_absent(&out, &["sql01", "qacme", "qfileserver", "qledger"]);
let keep = "read fooBar.baz and obj.fooBar";
assert_eq!(plain(keep), keep);
}
#[test]
fn internal_two_label_hosts() {
let out = plain("mount fileserver.local, ledger.prod, paygate.staging and vpn.qa");
assert_absent(&out, &["fileserver", "ledger", "paygate", "vpn.qa"]);
let keep = "read config.prod and window.app";
assert_eq!(plain(keep), keep);
assert_eq!(classify_dotted("qacme.dev", None), Dotted::Host);
assert_eq!(classify_dotted("qacme.app", None), Dotted::Host);
}
#[test]
fn folded_username_list() {
let out = plain("Summary: sync\n\nSubscribers: qharp qmeyer\n qtan qosei\n\nTest Plan: ran");
assert_absent(&out, &["qharp", "qmeyer", "qtan", "qosei"]);
assert!(out.ends_with("Test Plan: ran"), "{out}");
}
#[test]
fn name_tokens_apostrophes_hyphens_titlecase() {
let names = KnownNames {
people: vec![
"Sean O'Qbrien".into(),
"Mary-Qjane Quillfeather".into(),
"Frank Qolt".into(),
],
..KnownNames::default()
};
let out = run(
&names,
"thanks O\u{2019}Qbrien, O'Qbrien, Mary-Qjane and Qjane; ask Frank for a frank review",
);
assert_absent(&out, &["qbrien", "mary-qjane", "qjane"]);
assert!(!out.contains("Frank"), "{out}");
assert!(out.contains("a frank review"), "{out}");
}
#[tokio::test]
async fn billed_retries_count_against_the_cap() {
for timeout in [true, false] {
let server = MockServer::start().await;
let first = if timeout {
ResponseTemplate::new(200).set_delay(Duration::from_secs(3))
} else {
ResponseTemplate::new(500)
};
Mock::given(method("POST"))
.respond_with(first)
.up_to_n_times(1)
.with_priority(1)
.mount(&server)
.await;
Mock::given(method("POST"))
.respond_with(reply("CAT_1", probs("CAT_1"), 150))
.with_priority(2)
.mount(&server)
.await;
let jev = JevClassifier::from_options(Some(TEST_KEY.into()), &JevOptions::default())
.expect("keyed")
.with_test_endpoint(&format!("{}/v1/systemone", server.uri()))
.with_test_timeout(Duration::from_millis(300))
.with_context(categories(), KnownNames::default())
.expect("context");
prep(&jev);
let call = jev.classify("zzz").await;
assert_eq!(call.outcome, LlmOutcome::Answered, "timeout={timeout}");
let reqs = server.received_requests().await.expect("rec");
assert_eq!(reqs.len(), 2, "timeout={timeout}");
let per_attempt = reqs[0].body.len() as u64 + 1024;
assert_eq!(spent_tokens(&jev), per_attempt + 150, "timeout={timeout}");
}
}
#[tokio::test]
async fn retries_never_pass_the_cap() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.respond_with(ResponseTemplate::new(500))
.mount(&server)
.await;
let probe = classifier(&server, 1.0);
probe.classify("zzz").await;
let body_len = server.received_requests().await.expect("rec")[0].body.len() as u64;
let reservation = (body_len + 1024) * 3;
let cap_usd = (reservation as f64 + 0.5) * super::jev::JEV_INPUT_PRICE_PER_MTOK_USD / 1e6;
server.reset().await;
Mock::given(method("POST"))
.respond_with(ResponseTemplate::new(500))
.mount(&server)
.await;
let jev = classifier(&server, cap_usd);
assert_eq!(jev.classify("zzz").await.outcome, LlmOutcome::Failed);
assert_eq!(jev.classify("zzz").await.outcome, LlmOutcome::Skipped);
assert_eq!(server.received_requests().await.expect("rec").len(), 3);
assert_eq!(spent_tokens(&jev), reservation);
}
#[tokio::test]
async fn reply_without_usage_records_its_model() {
let server = server_with(ResponseTemplate::new(200).set_body_json(json!({
"model": "jev-9.9.9",
"answers": {"category": {
"type": "choice", "choice": "CAT_1", "probabilities": probs("CAT_1")
}}
})))
.await;
let (stats, db) = run_pipeline_with(&server, &[("zzz tidy", "a", "a@x", false)], |_| {}).await;
assert_eq!(stats.llm_usage.failed, 1);
let model: String = db
.connection()
.query_row("SELECT model FROM llm_usage", [], |r| r.get(0))
.expect("row");
assert_eq!(model, "jev-9.9.9");
}