use super::*;
#[test]
fn smart_case_lowercase_is_insensitive() {
let m = build_matcher(&args("carry")).unwrap();
assert!(m.is_match("the CARRY logic"));
assert!(m.is_match("the carry logic"));
}
#[test]
fn smart_case_uppercase_is_sensitive() {
let m = build_matcher(&args("Carry")).unwrap();
assert!(m.is_match("the Carry logic"));
assert!(!m.is_match("the carry logic"));
}
#[test]
fn ignore_case_overrides_smart_case() {
let mut a = args("Carry");
a.ignore_case = true;
let m = build_matcher(&a).unwrap();
assert!(m.is_match("the carry logic"));
}
#[test]
fn empty_pattern_is_pure_filter_matches_all() {
let m = build_matcher(&args("")).unwrap();
assert!(m.is_pure_filter());
assert!(m.is_match("literally anything"));
}
#[test]
fn multiline_dot_crosses_newline() {
let mut a = args("foo.*bar");
a.multiline = true;
let m = build_matcher(&a).unwrap();
assert!(m.is_match("foo\nmiddle\nbar"));
let m2 = build_matcher(&args("foo.*bar")).unwrap();
assert!(!m2.is_match("foo\nbar"));
}
#[test]
fn required_literal_only_for_plain_patterns() {
assert_eq!(required_literal("carry"), Some(b"carry".to_vec()));
assert!(required_literal("ca.ry").is_none());
assert!(required_literal("a|b").is_none());
}
#[test]
fn required_literal_rejects_json_escaped_chars() {
assert!(
required_literal("Say\"Xello").is_none(),
"a quote-containing literal must not be prefiltered"
);
assert!(required_literal("a\tb").is_none(), "tab is JSON-escaped");
assert!(
required_literal("a\nb").is_none(),
"newline is JSON-escaped"
);
assert_eq!(required_literal("café🛠"), Some("café🛠".as_bytes().to_vec()));
}
#[test]
fn quote_pattern_no_silent_drop_case_sensitive() {
let m = build_matcher(&args("Say\"Xello")).unwrap();
assert!(
matches!(m.prefilter, Some(Prefilter::Literal(_))),
"the quote-free run anchors; the full literal never does"
);
assert!(m.is_match("Say\"Xello there"));
let raw = br#"{"type":"user","message":{"role":"user","content":"Say\"Xello there"}}"#;
assert!(
m.line_may_match(raw),
"without a literal prefilter the line passes to the regex stage"
);
}
#[test]
fn prefilter_drops_lines_without_literal() {
let m = build_matcher(&args("carry")).unwrap();
assert!(matches!(m.prefilter, Some(Prefilter::CaselessLiteral(_))));
assert!(m.line_may_match(b"...the CARRY logic..."));
assert!(m.line_may_match(b"...the carry logic..."));
assert!(!m.line_may_match(b"...nothing relevant..."));
assert!(!m.file_may_match(b"a whole file without the needle"));
assert!(m.file_may_match(b"prefix bytes then Carry appears"));
let m2 = build_matcher(&args("Carry")).unwrap();
assert!(matches!(m2.prefilter, Some(Prefilter::Literal(_))));
assert!(m2.line_may_match(b"...the Carry logic..."));
assert!(!m2.line_may_match(b"...the CARRY logic..."));
assert!(!m2.line_may_match(b"...nothing relevant..."));
}
#[test]
fn prefilter_space_pattern_anchors_its_longest_safe_run() {
let m = build_matcher(&args("hello world")).unwrap();
assert!(matches!(m.prefilter, Some(Prefilter::CaselessLiteral(_))));
assert!(m.line_may_match(br"prefix hello\nworld suffix"));
assert!(!m.line_may_match(b"unrelated bytes"));
let m2 = build_matcher(&args("Hello World")).unwrap();
assert!(
matches!(m2.prefilter, Some(Prefilter::Literal(_))),
"case-sensitive: byte-exact run"
);
assert!(m2.line_may_match(br"say Hello\nWorld now"));
assert!(!m2.line_may_match(b"unrelated bytes"));
}
#[test]
fn required_needles_alternation_and_necessity() {
assert_eq!(
required_needles("TodoWrite.*legacy|legacy.*TodoWrite"),
Some(vec!["TodoWrite".to_string()])
);
assert_eq!(
required_needles("alphax.*[0-9]|betay+z"),
Some(vec!["alphax".to_string(), "beta".to_string()])
);
assert!(required_needles("alphax|[0-9]+").is_none());
assert!(required_needles("[a-z]+").is_none());
assert!(required_needles("ca.ry").is_none(), "runs under 3 bytes");
assert_eq!(required_needles("carry"), Some(vec!["carry".to_string()]));
}
#[test]
fn required_needles_concat_repetition_and_runs() {
assert_eq!(
required_needles("abc.*defgh"),
Some(vec!["defgh".to_string()])
);
assert_eq!(
required_needles("(foo)*barbaz"),
Some(vec!["barbaz".to_string()])
);
assert_eq!(
required_needles("(carry)+"),
Some(vec!["carry".to_string()])
);
assert_eq!(
required_needles("^\\bTodoWrite\\b$"),
Some(vec!["TodoWrite".to_string()])
);
assert_eq!(
required_needles("say\"hopeful.*"),
Some(vec!["hopeful".to_string()])
);
}
#[test]
fn metachar_pattern_gate_composes_with_synth_markers() {
let m = build_matcher(&args("subagent.*probe|probe.*subagent")).unwrap();
assert!(m.prefilter.is_some());
let line = br#"{"type":"user","message":{"role":"user","content":"<task-notification><task-id>t1</task-id><summary>Agent \"probe\" completed</summary></task-notification>"}}"#;
assert!(m.line_may_match(line));
assert!(
!m.line_may_match(b"{\"type\":\"user\"} nothing relevant"),
"no needle, no marker: provably a miss"
);
}
#[test]
fn synth_marker_keeps_line_and_file_matchable_without_literal() {
let m = build_matcher(&args("subagent")).unwrap();
assert!(m.prefilter.is_some());
let line = br#"{"type":"user","message":{"role":"user","content":"<task-notification><task-id>t1</task-id><summary>Agent \"probe\" completed</summary></task-notification>"}}"#;
assert!(m.line_may_match(line));
assert!(m.file_may_match(line));
let rej = br#"{"type":"user","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"x","content":"To tell you how to proceed, the user said:\ngo"}]}}"#;
let m_plan = build_matcher(&args("plan")).unwrap();
assert!(m_plan.line_may_match(rej));
assert!(!m.line_may_match(b"{\"type\":\"user\"} nothing relevant"));
assert!(!m.file_may_match(b"a whole file with nothing relevant"));
}
#[test]
fn resolve_persisted_flag_adds_pointer_markers() {
let mut a = args("zzguarded");
a.resolve_persisted = true;
let m = build_matcher(&a).unwrap();
let line = br#"{"toolUseResult":{"persistedOutputPath":"/tmp/x.txt"},"message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"t","content":"Full output saved to: /tmp/x.txt"}]}}"#;
assert!(m.line_may_match(line));
let m2 = build_matcher(&args("zzguarded")).unwrap();
assert!(!m2.line_may_match(line));
}
#[test]
fn resolve_persisted_text_reads_file_or_notes_failure() {
let dir = std::env::temp_dir();
let p = dir.join(format!("csift-persist-test-{}.txt", std::process::id()));
std::fs::write(&p, "THE REAL PERSISTED BODY with a deep token zzqqxx").unwrap();
let resolved = resolve_persisted_text(&p.to_string_lossy(), "<persisted-output> pointer");
assert!(resolved.contains("zzqqxx"), "got: {resolved}");
assert!(
!resolved.contains("pointer"),
"inline pointer should be replaced"
);
std::fs::remove_file(&p).ok();
let missing = resolve_persisted_text("/no/such/csift/file.txt", "inline preview text");
assert!(missing.contains("inline preview text"));
assert!(missing.contains("could not resolve persisted output"));
}
#[test]
fn resolve_persisted_end_to_end_matches_deep_token() {
let dir = std::env::temp_dir();
let p = dir.join(format!("csift-e2e-persist-{}.txt", std::process::id()));
std::fs::write(&p, "deep file body containing the token wibblewobble here").unwrap();
let line = format!(
r#"{{"type":"user","uuid":"u0","message":{{"role":"user","content":[{{"type":"tool_result","tool_use_id":"x","content":"<persisted-output>\nOutput too large (1 KB). Full output saved to: {}\n\nPreview (first 2KB):\n(no token here)\n</persisted-output>"}}]}}}}"#,
p.to_string_lossy().replace('\\', "\\\\")
);
let r: Record = serde_json::from_str(&line).expect("valid record");
let m = build_matcher(&args("wibblewobble")).unwrap();
let mut no_resolve = Vec::new();
collect_record_hits(
&r,
false,
LabelFilter::new(&["agent.tool.result".to_string()], &[]),
&m,
false,
EXCERPT_MAX,
&PlanIndex::default(),
&HashMap::new(),
&ClassifyCtx::top_level(),
&mut no_resolve,
);
assert!(no_resolve.is_empty(), "deep token must NOT match inline");
let mut with_resolve = Vec::new();
collect_record_hits(
&r,
false,
LabelFilter::new(&["agent.tool.result".to_string()], &[]),
&m,
true,
EXCERPT_MAX,
&PlanIndex::default(),
&HashMap::new(),
&ClassifyCtx::top_level(),
&mut with_resolve,
);
assert_eq!(with_resolve.len(), 1, "deep token matches after resolution");
assert_eq!(with_resolve[0].class, Class::AgentToolResult);
std::fs::remove_file(&p).ok();
}
#[test]
fn any_literal_prefilter_case_sensitive_multi_needle() {
let m = build_matcher(&args("Alphax.*probe|Betay.*probe")).unwrap();
assert!(matches!(m.prefilter, Some(Prefilter::AnyLiteral(_))));
assert!(m.line_may_match(b"... Alphax then something ..."));
assert!(m.line_may_match(b"... Betay probe something ..."));
assert!(!m.line_may_match(b"... alphax lowercase misses ..."));
assert!(!m.line_may_match(b"... nothing relevant ..."));
assert!(m.file_may_match(b"a whole file where Alphax appears"));
assert!(!m.file_may_match(b"a whole file with neither"));
assert!(m.line_prefilter_hits(b"probe present"));
assert!(!m.line_prefilter_hits(b"nothing"));
let wide = "aaax1|bbbx2|cccx3|dddx4|eeex5|fffx6|gggx7|hhhx8|iiix9";
let m2 = build_matcher(&args(wide)).unwrap();
assert!(m2.prefilter.is_none(), "over-cap union anchors nothing");
assert!(m2.line_prefilter_hits(b"anything"));
assert!(required_needles("").is_none());
}
#[test]
fn needle_extraction_mutation_pins() {
assert_eq!(
required_needles("(alphaxx)*barb"),
Some(vec!["barb".to_string()])
);
assert!(required_needles("ab\u{01}cd").is_none());
}