use assay_core::model::{TestResultRow, TestStatus};
use assay_core::render_safety::{render_truncate_first_unsafe, Sink};
use assay_core::report::console::console_prompt_preview;
use assay_core::report::json::render_json;
use assay_core::report::junit::write_junit;
use assay_core::report::sarif::write_sarif;
use assay_core::report::RunArtifacts;
const EMAIL: &str = "alice@example.com";
const BENIGN: &str = "uuid 123e4567-e89b-12d3-a456-426614174000";
const ANSI: &str = "\u{1b}[31mRED\u{1b}[0m";
const OWNED_FP: &str = "ownedfp123";
const SUITE: &str = "owned-suite";
fn secret() -> String {
format!("ghp_{}", "A".repeat(36))
}
fn secret_control_glued() -> String {
format!("ghp\u{1b}[0m_{}", "A".repeat(36))
}
fn straddling_prompt() -> String {
format!("{} {} tail", "x".repeat(90), secret())
}
fn row(
test_id: &str,
status: TestStatus,
message: String,
details: serde_json::Value,
) -> TestResultRow {
TestResultRow {
test_id: test_id.to_string(),
status,
score: Some(0.0),
cached: false,
message,
details,
duration_ms: Some(5),
fingerprint: Some(OWNED_FP.to_string()),
skip_reason: None,
attempts: None,
error_policy_applied: None,
}
}
fn hostile_artifacts() -> RunArtifacts {
let fail = row(
"t_fail",
TestStatus::Fail,
format!("fail {ANSI} {EMAIL}"),
serde_json::json!({
"prompt": straddling_prompt(),
"response": secret_control_glued(),
"assertions": [{ "message": format!("got {}", secret()) }, { "passed": true }],
"expected": BENIGN,
"actual": format!("val {EMAIL}"),
"skip": { "fingerprint": OWNED_FP, "reason": "fingerprint_match" },
"owned_count": 7,
}),
);
let error = row(
"t_error",
TestStatus::Error,
format!("boom {}", secret()),
serde_json::json!({}),
);
let pass = row(
"t_pass",
TestStatus::Pass,
"ok".to_string(),
serde_json::json!({ "prompt": "benign prompt", "response": "all good" }),
);
let allowed = row(
"t_allowed",
TestStatus::AllowedOnError,
format!("allowed by error policy: provider 500 {}", secret()),
serde_json::json!({}),
);
RunArtifacts {
run_id: 1,
suite: SUITE.to_string(),
results: vec![fail, error, pass, allowed],
order_seed: None,
runner_clone_ms: None,
}
}
fn assert_no_hostile_values(haystack: &str, ctx: &str) {
assert!(!haystack.contains(&secret()), "{ctx}: raw secret leaked");
assert!(!haystack.contains("ghp_"), "{ctx}: raw token prefix leaked");
assert!(!haystack.contains(EMAIL), "{ctx}: raw pii leaked");
assert!(
!haystack.contains('\u{1b}'),
"{ctx}: raw terminal control leaked"
);
}
#[test]
fn run_json_record_sink_is_render_safe() {
let artifacts = hostile_artifacts();
let out = render_json(&artifacts).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&out).unwrap();
assert_no_hostile_values(&out, "run.json");
assert!(out.contains("<redacted:"), "run.json fired no redaction");
assert!(out.contains(SUITE), "suite mutated");
assert!(out.contains(OWNED_FP), "fingerprint mutated");
assert_eq!(
parsed["results"][0]["details"]["skip"]["fingerprint"],
OWNED_FP
);
assert_eq!(
parsed["results"][0]["details"]["skip"]["reason"],
"fingerprint_match"
);
assert_eq!(parsed["results"][0]["details"]["owned_count"], 7);
assert_eq!(parsed["results"][0]["test_id"], "t_fail");
assert_eq!(parsed["results"][0]["details"]["expected"], BENIGN);
let allowed_msg = parsed["results"][3]["message"].as_str().unwrap();
assert_eq!(parsed["results"][3]["test_id"], "t_allowed");
assert!(
!allowed_msg.contains("ghp_"),
"AllowedOnError message leaked secret"
);
assert!(allowed_msg.contains("<redacted:github-token>"));
assert_eq!(
parsed["results"][0]["details"]["response"],
"<redacted:github-token>"
);
let prompt = parsed["results"][0]["details"]["prompt"].as_str().unwrap();
assert!(
prompt.contains("<redacted:github-token>"),
"deep prompt secret not redacted"
);
assert!(!prompt.contains("ghp_"));
assert!(
!prompt.contains("(truncated)"),
"record sink must not truncate"
);
}
#[test]
fn console_prompt_preview_redacts_before_truncating() {
let prompt = straddling_prompt();
let preview = console_prompt_preview(&prompt);
assert!(!preview.contains("ghp_"), "console preview leaked secret");
assert!(
preview.contains("<redacted"),
"console preview did not redact"
);
assert!(
preview.contains("(truncated)"),
"console preview did not bound"
);
let unsafe_preview = render_truncate_first_unsafe(Sink::Stdout, &prompt, 100);
assert!(
unsafe_preview.contains("ghp_"),
"truncate-first is expected to leak (proves the order matters)"
);
}
#[test]
fn sarif_report_sink_is_render_safe() {
let artifacts = hostile_artifacts();
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("out.sarif");
write_sarif("assay", &artifacts.results, &path).unwrap();
let s = std::fs::read_to_string(&path).unwrap();
let _: serde_json::Value = serde_json::from_str(&s).unwrap();
assert_no_hostile_values(&s, "sarif");
assert!(s.contains("redacted"), "sarif fired no redaction");
assert!(s.contains("t_fail"), "sarif dropped assay-owned test_id");
}
#[test]
fn junit_report_sink_is_render_safe_without_double_escape() {
let artifacts = hostile_artifacts();
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("out.xml");
write_junit(SUITE, &artifacts.results, &path).unwrap();
let j = std::fs::read_to_string(&path).unwrap();
assert_no_hostile_values(&j, "junit");
assert!(
j.contains("<redacted"),
"junit redaction marker not xml-escaped"
);
assert!(
!j.contains("&lt;redacted"),
"junit double-escaped the marker"
);
assert!(j.contains(r#"name="owned-suite""#), "junit mutated suite");
assert!(j.contains(r#"name="t_fail""#), "junit mutated test_id");
}
#[test]
fn benign_only_run_introduces_no_redaction_markers() {
let clean = RunArtifacts {
run_id: 2,
suite: SUITE.to_string(),
results: vec![row(
"t_clean",
TestStatus::Fail,
"expected greeting, got farewell".to_string(),
serde_json::json!({ "prompt": "say hello", "response": "goodbye" }),
)],
order_seed: None,
runner_clone_ms: None,
};
let out = render_json(&clean).unwrap();
assert!(
!out.contains("<redacted:"),
"benign run over-redacted: {out}"
);
assert!(out.contains("say hello") && out.contains("goodbye"));
}