use rusqlite::params;
use super::jev_tests::{
bodies, classifier, probs, reply, run_pipeline_cfg, run_pipeline_with, server_with, RULES,
};
use super::llm_prompt::LlmOutcome;
use crate::core::db::Database;
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:?}"
);
}
}
fn classified_commit(db: &Database, sha: &str, message: &str, repository: &str) {
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 (?1, 'Old Author', 'old@corp.test', \
'2023-01-01T00:00:00Z', ?2, ?3, ?4)",
params![sha, message, repository, cid],
)
.expect("old commit");
}
#[tokio::test]
async fn trailer_names_from_stored_commits_are_redacted() {
let server = server_with(reply("CAT_1", probs("CAT_1"), 150)).await;
let setup = |db: &Database| {
classified_commit(
db,
"sha-old",
"fix: cache\n\nReviewers: qzelvin, qmarrow\n\
Co-authored-by: Quorra Bexley <qbexley@corp.test>",
"ledger-core",
);
};
let rows = [(
"zzz follow-up for qzelvin and qmarrow; Quorra Bexley and qbexley 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 message = sent[0]["state"]["commit"]["message"]
.as_str()
.expect("message");
assert_absent(message, &["qzelvin", "qmarrow", "quorra", "bexley"]);
assert!(message.contains("PERSON_"), "{message}");
}
#[tokio::test]
async fn org_and_repo_names_from_every_source_are_redacted() {
let server = server_with(reply("CAT_1", probs("CAT_1"), 150)).await;
let setup = |db: &Database| {
classified_commit(db, "sha-old", "fix: cache", "qdb-repo");
db.connection()
.execute(
"INSERT INTO pull_requests (pr_number, title, author, state, created_at, \
repository) VALUES (1, 't', 'qprauthor', 'merged', '2023-01-01', 'qpr-repo')",
[],
)
.expect("pull request");
};
let tweak = |c: &mut crate::core::config::Config| {
c.repositories[0].org = None;
c.github = Some(
serde_yaml::from_str(
"org: acme-fin\norgs: [qorvex-ops]\nrepo: acme-fin/invoicing-api\n",
)
.expect("github"),
);
c.bitbucket = Some(
serde_yaml::from_str(
"workspace: qbit-team\nworkspaces: [qbit-two]\nrepo_slug: qslug-svc\n",
)
.expect("bitbucket"),
);
c.pm = Some(
serde_yaml::from_str(
"azure_devops:\n organization_url: https://dev.azure.com/qazorg\n \
pat: not-a-real-pat\n projects: [qproj-alpha]\n", )
.expect("pm"),
);
c.jira =
Some(serde_yaml::from_str("url: https://qjirasite.atlassian.net\n").expect("jira"));
let cls = c.classification.as_mut().expect("section");
cls.repo_categories
.insert("qcat-repo".into(), "platform".into());
};
let names = [
"acme-fin",
"invoicing-api",
"qorvex-ops",
"qbit-team",
"qbit-two",
"qslug-svc",
"qazorg",
"qproj-alpha",
"qjirasite",
"qcat-repo",
"qdb-repo",
"qpr-repo",
];
let text = format!(
"port acme-fin invoicing-api client; ping {}",
names[2..].join(", ")
);
let rows = [(text.as_str(), "b", "b@x", false)];
let (_, _db) = run_pipeline_cfg(&server, &rows, RULES, setup, tweak).await;
let sent = bodies(&server).await;
assert_eq!(sent.len(), 1);
let message = sent[0]["state"]["commit"]["message"]
.as_str()
.expect("message");
assert_absent(message, &names);
assert!(message.starts_with("port REPO_"), "{message}");
assert!(message.contains(" client; ping "), "{message}");
}
#[tokio::test]
async fn poisoned_obfuscator_lock_sends_nothing() {
let server = server_with(reply("CAT_1", probs("CAT_1"), 150)).await;
let jev = classifier(&server, 1.0);
jev.poison_obfuscator_lock();
let call = jev.classify("zzz tidy").await;
assert_eq!(call.outcome, LlmOutcome::Failed);
assert!(call.verdict.is_none());
assert!(bodies(&server).await.is_empty(), "a request was sent");
assert!(
jev.prepare(&["zzz"], &[]).is_err(),
"prepare ran on a poisoned lock"
);
}