use crate::harness::*;
#[test]
fn search_header_tokens_are_stable_across_invocations() {
let h = populated_home();
let a = h.run(&["search", "carry"]);
let b = h.run(&["search", "carry"]);
assert!(
a.success && b.success,
"stderr: {} / {}",
a.stderr,
b.stderr
);
assert_eq!(
a.stdout, b.stdout,
"byte-identical output across identical invocations"
);
}
#[test]
fn search_header_token_collision_lengthens_the_group_only() {
let h = Home::new();
let _ = header_collision_scenario(&h);
let out = h.run(&["search", "SEEDWORD"]);
assert!(out.success, "stderr: {}", out.stderr);
assert!(
out.stdout.contains("aaaabbbb-111\u{b7}t"),
"colliding id 1 lengthens to 12: {}",
out.stdout
);
assert!(
out.stdout.contains("aaaabbbb-222\u{b7}t"),
"colliding id 2 lengthens to 12: {}",
out.stdout
);
assert!(
out.stdout.contains("ccccdddd\u{b7}t"),
"non-collider stays at 8: {}",
out.stdout
);
assert!(
!out.stdout.contains("aaaabbbb\u{b7}t"),
"the collided bare 8-prefix must not appear as a token: {}",
out.stdout
);
}
#[test]
fn search_lengthened_uuid_token_resolves_and_short_prefix_fails_loud() {
let h = Home::new();
let (c1, c2, _) = header_collision_scenario(&h);
let one = h.run(&["search", "SEEDWORD", "@aaaabbbb-111"]);
assert!(one.success, "stderr: {}", one.stderr);
assert!(one.stdout.contains("COLLIDEONE"), "got: {}", one.stdout);
assert!(
!one.stdout.contains("COLLIDETWO"),
"the sibling session must be out of scope: {}",
one.stdout
);
let ambi = h.run(&["search", "SEEDWORD", "@aaaabbbb"]);
assert!(!ambi.success, "an ambiguous prefix must error");
assert!(
ambi.stderr.contains("AMBIGUOUS") && ambi.stderr.contains(c1) && ambi.stderr.contains(c2),
"the error names both candidates: {}",
ambi.stderr
);
}
#[test]
fn search_subagent_header_carries_parent_token_on_every_exchange() {
let h = Home::new();
subagents_only_scenario(&h);
let out = h.run(&["search", "", at(SESS).as_str()]);
assert!(out.success, "stderr: {}", out.stderr);
assert!(
out.stdout
.contains(&format!("sub111\u{b7}t0 (parent {})", &SESS[..8])),
"subagent header carries the parent token: {}",
out.stdout
);
for line in out.stdout.lines() {
if line.starts_with(&format!("{}\u{b7}t", &SESS[..8])) {
assert!(
!line.contains("(parent"),
"a top-level header must not carry a parent: {line}"
);
}
}
}
#[test]
fn search_and_show_resolve_an_agent_id_prefix_token() {
let h = Home::new();
let (_, _, gamma) = agent_prefix_scenario(&h);
let out = h.run(&["search", "AGENTGAMMA", &format!("@{}", &gamma[..8])]);
assert!(out.success, "stderr: {}", out.stderr);
assert!(out.stdout.contains("AGENTGAMMA"), "got: {}", out.stdout);
let s = h.run(&["show", &format!("@{}", &gamma[..8]), "--line", "1"]);
assert!(s.success, "show by agent prefix: {}", s.stderr);
assert!(s.stdout.contains("AGENTGAMMA"), "got: {}", s.stdout);
}
#[test]
fn search_agent_twelve_hex_token_falls_back_to_unique_prefix() {
let h = Home::new();
let (alpha, beta, _) = agent_prefix_scenario(&h);
let out = h.run(&["search", "seed"]);
assert!(out.success, "stderr: {}", out.stderr);
assert!(
out.stdout.contains(&format!("{}\u{b7}t", &alpha[..12]))
&& out.stdout.contains(&format!("{}\u{b7}t", &beta[..12])),
"colliding agent tokens lengthen to 12: {}",
out.stdout
);
let one = h.run(&["search", "AGENT", &format!("@{}", &alpha[..12])]);
assert!(one.success, "stderr: {}", one.stderr);
assert!(one.stdout.contains("AGENTALPHA"), "got: {}", one.stdout);
assert!(
!one.stdout.contains("AGENTBETA"),
"the sibling agent must be out of scope: {}",
one.stdout
);
let ambi = h.run(&["search", "AGENT", &format!("@{}", &alpha[..8])]);
assert!(!ambi.success, "an ambiguous agent prefix must error");
assert!(
ambi.stderr.contains("AMBIGUOUS")
&& ambi.stderr.contains(alpha)
&& ambi.stderr.contains(beta),
"the error names both agent ids: {}",
ambi.stderr
);
}
#[test]
fn search_match_banner_at_head_mirrors_footer_and_json() {
let h = Home::new();
let _ = header_collision_scenario(&h);
let out = h.run(&["search", "SEEDWORD"]);
assert!(out.success, "stderr: {}", out.stderr);
assert_eq!(
out.stdout.lines().next(),
Some("matches 3 exchanges · 3 sessions · oldest first"),
"head banner: {}",
out.stdout
);
assert!(
out.stdout
.contains("matched 3 exchanges · 3 sessions · label=all"),
"footer repeats the totals: {}",
out.stdout
);
assert!(
!out.stdout.contains("malformed") && !out.stdout.contains("sidecar"),
"no zero-count footer notes on a clean run: {}",
out.stdout
);
let js = h.run(&["search", "SEEDWORD", "--format", "json"]);
assert!(js.success, "stderr: {}", js.stderr);
assert!(
!js.stdout.contains("matches "),
"no banner in JSON mode: {}",
js.stdout
);
let last: serde_json::Value =
serde_json::from_str(js.stdout.lines().next_back().unwrap()).unwrap();
assert_eq!(last["matched"], serde_json::json!(3));
assert_eq!(last["sessions"], serde_json::json!(3));
let cap = h.run(&["search", "SEEDWORD", "--max-count", "2"]);
assert!(cap.success, "stderr: {}", cap.stderr);
assert_eq!(
cap.stdout.lines().next(),
Some("matches 3 exchanges · 3 sessions · oldest first · showing earliest 2"),
"capped head banner: {}",
cap.stdout
);
assert!(
cap.stdout.contains("matched 3 exchanges · 3 sessions")
&& cap.stdout.contains("1 later dropped by --max-count"),
"capped footer: {}",
cap.stdout
);
let capjs = h.run(&["search", "SEEDWORD", "--max-count", "2", "--format", "json"]);
let last: serde_json::Value =
serde_json::from_str(capjs.stdout.lines().next_back().unwrap()).unwrap();
assert_eq!(
last["matched"].as_u64().unwrap() + last["dropped_by_cap"].as_u64().unwrap(),
3,
"JSON post-cap matched + dropped reconcile to the banner total"
);
let c = h.run(&["search", "SEEDWORD", "-c"]);
assert!(
!c.stdout.contains("matches "),
"no banner under -c: {}",
c.stdout
);
let l = h.run(&["search", "SEEDWORD", "-l"]);
assert!(
!l.stdout.contains("matches "),
"no banner under -l: {}",
l.stdout
);
let cb = h.run(&["search", "SEEDWORD", "--count-by", "label"]);
assert!(
!cb.stdout.contains("matches "),
"no banner under --count-by: {}",
cb.stdout
);
}