use crate::harness::*;
const ENC: &str = "-Users-dev-example-project";
const SESS: &str = "9c8b7a6d-5e4f-4321-a098-76543210fedc";
const SUB: &str = "0badc0ffee123456";
fn varint(mut v: u64) -> Vec<u8> {
let mut out = Vec::new();
loop {
let b = (v & 0x7f) as u8;
v >>= 7;
if v == 0 {
out.push(b);
break;
}
out.push(b | 0x80);
}
out
}
fn field(num: u64, payload: &[u8]) -> Vec<u8> {
let mut out = varint((num << 3) | 2);
out.extend(varint(payload.len() as u64));
out.extend_from_slice(payload);
out
}
fn b64(bytes: &[u8]) -> String {
const AL: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
let mut out = String::new();
for chunk in bytes.chunks(3) {
let b = [
chunk[0],
*chunk.get(1).unwrap_or(&0),
*chunk.get(2).unwrap_or(&0),
];
let n = (u32::from(b[0]) << 16) | (u32::from(b[1]) << 8) | u32::from(b[2]);
out.push(AL[(n >> 18) as usize & 63] as char);
out.push(AL[(n >> 12) as usize & 63] as char);
out.push(if chunk.len() > 1 {
AL[(n >> 6) as usize & 63] as char
} else {
'='
});
out.push(if chunk.len() > 2 {
AL[n as usize & 63] as char
} else {
'='
});
}
out
}
fn sig(tag: &str) -> String {
let mut outer = vec![0x08, 0x02];
outer.extend(field(2, &field(1, &field(8, tag.as_bytes()))));
b64(&outer)
}
fn narration_home() -> Home {
let h = Home::new();
let t = sig("thinking");
let n = sig("narration");
h.write(
&format!("{ENC}/{SESS}.jsonl"),
&format!(concat!(
r#"{{"type":"user","uuid":"u1","timestamp":"2026-06-07T05:00:00.000Z","message":{{"role":"user","content":"chart the reef"}}}}"#, "\n",
r#"{{"type":"assistant","uuid":"a1","parentUuid":"u1","timestamp":"2026-06-07T05:00:01.000Z","message":{{"role":"assistant","content":[{{"type":"thinking","thinking":"placeholder reasoning about the reef","signature":"{t}"}}]}}}}"#, "\n",
r#"{{"type":"assistant","uuid":"a2","parentUuid":"a1","timestamp":"2026-06-07T05:00:02.000Z","message":{{"role":"assistant","content":[{{"type":"thinking","thinking":"placeholder narration summary line","signature":"{n}"}}]}}}}"#, "\n",
r#"{{"type":"assistant","uuid":"a3","parentUuid":"a2","timestamp":"2026-06-07T05:00:03.000Z","message":{{"role":"assistant","content":[{{"type":"thinking","thinking":"","signature":"{n}"}}]}}}}"#, "\n",
r#"{{"type":"assistant","uuid":"a4","parentUuid":"a3","timestamp":"2026-06-07T05:00:04.000Z","message":{{"role":"assistant","content":[{{"type":"thinking","thinking":"placeholder torn-signature reasoning","signature":"!!!not-base64!!!"}}]}}}}"#, "\n",
r#"{{"type":"assistant","uuid":"a5","parentUuid":"a4","timestamp":"2026-06-07T05:00:05.000Z","message":{{"role":"assistant","content":[{{"type":"text","text":"the reef is charted"}}]}}}}"#, "\n",
), t = t, n = n),
);
h
}
#[test]
fn probe1_show_labels_the_discriminating_pair() {
let h = narration_home();
let out = h.run(&["show", &at(SESS), "--line", "2..3", "--format", "json"]);
assert!(out.success, "stderr: {}", out.stderr);
let rows: Vec<serde_json::Value> = out
.stdout
.lines()
.map(|l| serde_json::from_str(l).unwrap())
.collect();
let recs: Vec<&serde_json::Value> = rows.iter().filter(|r| r["kind"] == "record").collect();
assert_eq!(recs.len(), 2);
assert_eq!(recs[0]["label"], "agent.thinking", "{}", out.stdout);
assert_eq!(
recs[1]["label"], "agent.thinking.narration",
"{}",
out.stdout
);
for r in recs {
assert_eq!(r["labels"].as_array().unwrap().len(), 1);
}
}
#[test]
fn probe2_census_conservation_and_degradation() {
let h = narration_home();
let out = h.run(&["search", "", &at(SESS), "--count-by", "label"]);
assert!(out.success, "stderr: {}", out.stderr);
assert!(
out.stdout.contains("2 agent.thinking\n") || out.stdout.contains("2 agent.thinking"),
"{}",
out.stdout
);
assert!(
out.stdout.contains("agent.thinking.narration"),
"{}",
out.stdout
);
let json = h.run(&[
"search",
"",
&at(SESS),
"--count-by",
"label",
"--format",
"json",
]);
let mut thinking = 0u64;
let mut narration = 0u64;
for line in json.stdout.lines() {
let v: serde_json::Value = serde_json::from_str(line).unwrap();
if v["kind"] == "census" {
match v["key"].as_str() {
Some("agent.thinking") => thinking = v["records"].as_u64().unwrap(),
Some("agent.thinking.narration") => narration = v["records"].as_u64().unwrap(),
_ => {}
}
}
}
assert_eq!((thinking, narration), (2, 2), "{}", json.stdout);
}
#[test]
fn probe3_prefix_selects_both_and_subtraction_isolates_reasoning() {
let h = narration_home();
let both = h.run(&[
"search",
"",
&at(SESS),
"--count-by",
"label",
"-t",
"agent.thinking",
]);
assert!(both.success, "stderr: {}", both.stderr);
assert!(
both.stdout.contains("agent.thinking") && both.stdout.contains("agent.thinking.narration"),
"prefix selects both leaves:\n{}",
both.stdout
);
let pure = h.run(&[
"search",
"",
&at(SESS),
"--count-by",
"label",
"-t",
"agent.thinking",
"-T",
"agent.thinking.narration",
]);
assert!(pure.success, "stderr: {}", pure.stderr);
assert!(
pure.stdout.contains("agent.thinking") && !pure.stdout.contains("agent.thinking.narration"),
"subtraction isolates pure reasoning:\n{}",
pure.stdout
);
let hit = h.run(&[
"search",
"narration summary line",
&at(SESS),
"-t",
"agent.thinking.narration",
]);
assert!(
hit.stdout
.contains("agent.thinking.narration [narration summary]"),
"label-zone marker:\n{}",
hit.stdout
);
}
#[test]
fn probe4_verbatim_never_replays_narration() {
let h = narration_home();
let out = h.run(&["verbatim", &at(SESS)]);
assert!(out.success, "stderr: {}", out.stderr);
assert!(
!out.stdout.contains("placeholder narration summary line"),
"narration is a UI affordance, never conversation content:\n{}",
out.stdout
);
assert!(
out.stdout.contains("the reef is charted"),
"agent prose still replays:\n{}",
out.stdout
);
}
#[test]
fn probe5_span_counts_subagent_narration() {
let h = narration_home();
let n = sig("narration");
h.write(
&format!("{ENC}/{SESS}/subagents/agent-{SUB}.jsonl"),
&format!(concat!(
r#"{{"type":"user","uuid":"su1","timestamp":"2026-06-07T05:00:06.000Z","message":{{"role":"user","content":"child task"}}}}"#, "\n",
r#"{{"type":"assistant","uuid":"sa1","parentUuid":"su1","timestamp":"2026-06-07T05:00:07.000Z","message":{{"role":"assistant","content":[{{"type":"thinking","thinking":"child narration placeholder","signature":"{n}"}}]}}}}"#, "\n",
), n = n),
);
let spanned = h.run(&[
"search",
"",
&at(SESS),
"--count-by",
"label",
"--format",
"json",
]);
let narration_count = |stdout: &str| -> u64 {
stdout
.lines()
.filter_map(|l| serde_json::from_str::<serde_json::Value>(l).ok())
.find(|v| v["kind"] == "census" && v["key"] == "agent.thinking.narration")
.and_then(|v| v["records"].as_u64())
.unwrap_or(0)
};
assert_eq!(narration_count(&spanned.stdout), 3, "{}", spanned.stdout);
let top = h.run(&[
"search",
"",
&at(SESS),
"--no-subagents",
"--count-by",
"label",
"--format",
"json",
]);
assert_eq!(narration_count(&top.stdout), 2, "{}", top.stdout);
}
#[test]
fn stats_counts_narration_blocks_per_model_and_flags_unknown_tags() {
let h = Home::new();
let t = sig("thinking");
let n = sig("narration");
let x = sig("future-tag");
let stats_sess = "5a4b3c2d-1e0f-4987-b654-3210fedcba98";
h.write(
&format!("{ENC}/{stats_sess}.jsonl"),
&format!(concat!(
r#"{{"type":"assistant","uuid":"s1","timestamp":"2026-06-07T05:00:01.000Z","message":{{"role":"assistant","id":"msg_s1","model":"claude-test-1","content":[{{"type":"thinking","thinking":"placeholder reasoning","signature":"{t}"}}]}}}}"#, "\n",
r#"{{"type":"assistant","uuid":"s2","timestamp":"2026-06-07T05:00:02.000Z","message":{{"role":"assistant","id":"msg_s1","model":"claude-test-1","content":[{{"type":"thinking","thinking":"placeholder summary","signature":"{n}"}}]}}}}"#, "\n",
r#"{{"type":"assistant","uuid":"s3","timestamp":"2026-06-07T05:00:03.000Z","message":{{"role":"assistant","id":"msg_s2","model":"claude-test-2","content":[{{"type":"thinking","thinking":"placeholder summary two","signature":"{n}"}}]}}}}"#, "\n",
r#"{{"type":"assistant","uuid":"s4","timestamp":"2026-06-07T05:00:04.000Z","message":{{"role":"assistant","id":"msg_s3","model":"claude-test-1","content":[{{"type":"thinking","thinking":"placeholder oddity","signature":"{x}"}}]}}}}"#, "\n",
), t = t, n = n, x = x),
);
let out = h.run(&["stats", &format!("@{stats_sess}"), "--format", "json"]);
assert!(out.success, "stderr: {}", out.stderr);
let row: serde_json::Value = out
.stdout
.lines()
.filter_map(|l| serde_json::from_str(l).ok())
.find(|v: &serde_json::Value| v["kind"] == "session")
.expect("session row");
assert_eq!(
row["narration_blocks"]["claude-test-1"], 1,
"{}",
out.stdout
);
assert_eq!(
row["narration_blocks"]["claude-test-2"], 1,
"{}",
out.stdout
);
assert_eq!(
row["unknown_thinking_tags"], 1,
"a future tag value surfaces without a release: {}",
out.stdout
);
let text = h.run(&["stats", &format!("@{stats_sess}")]);
assert!(
text.stdout.contains("narration blocks")
&& text.stdout.contains("claude-test-1×1")
&& text.stdout.contains("unknown thinking-signature tags 1"),
"{}",
text.stdout
);
}
#[test]
fn stats_narration_total_merges_across_sessions_and_absence_prints_nothing() {
let h = Home::new();
let n = sig("narration");
let s1 = "7e6f5a4b-3c2d-4109-8765-43210fedcba9";
let s2 = "8f709a1b-4d3e-4210-9876-543210fedcb0";
for (sess, model) in [(s1, "claude-test-1"), (s2, "claude-test-1")] {
h.write(
&format!("{ENC}/{sess}.jsonl"),
&format!(concat!(
r#"{{"type":"assistant","uuid":"t1","timestamp":"2026-06-07T05:00:01.000Z","message":{{"role":"assistant","id":"msg_t1","model":"{model}","content":[{{"type":"thinking","thinking":"placeholder summary","signature":"{n}"}}]}}}}"#, "\n",
), model = model, n = n),
);
}
let out = h.run(&["stats", &at(s1), &at(s2)]);
assert!(out.success, "stderr: {}", out.stderr);
assert!(
out.stdout.contains("TOTAL") && out.stdout.contains("narration blocks claude-test-1×2"),
"{}",
out.stdout
);
let j = h.run(&["stats", &at(s1), &at(s2), "--format", "json"]);
let summary: serde_json::Value =
serde_json::from_str(j.stdout.lines().last().unwrap()).unwrap();
assert_eq!(
summary["narration_blocks"]["claude-test-1"], 2,
"{}",
j.stdout
);
assert_eq!(summary["unknown_thinking_tags"], 0, "{}", j.stdout);
let s3 = "9a8b7c6d-5e4f-4321-a987-6543210fedcb";
h.write(
&format!("{ENC}/{s3}.jsonl"),
concat!(
r#"{"type":"assistant","uuid":"p1","timestamp":"2026-06-07T05:00:01.000Z","message":{"role":"assistant","id":"msg_p1","model":"claude-test-1","content":[{"type":"thinking","thinking":"plain reasoning, no signature"}]}}"#,
"\n",
),
);
let quiet = h.run(&["stats", &at(s3)]);
assert!(
!quiet.stdout.contains("narration blocks")
&& !quiet.stdout.contains("unknown thinking-signature"),
"{}",
quiet.stdout
);
}
#[test]
fn stats_empty_string_message_id_counts_individually() {
let h = Home::new();
let s = "0b1c2d3e-4f5a-4678-b210-fedcba987654";
h.write(
&format!("{ENC}/{s}.jsonl"),
concat!(
r#"{"type":"assistant","uuid":"e1","timestamp":"2026-06-07T05:00:01.000Z","message":{"role":"assistant","id":"","model":"claude-test-1","usage":{"input_tokens":0,"output_tokens":11,"cache_read_input_tokens":0,"cache_creation_input_tokens":0},"content":[{"type":"text","text":"one"}]}}"#, "\n",
r#"{"type":"assistant","uuid":"e2","timestamp":"2026-06-07T05:00:02.000Z","message":{"role":"assistant","id":"","model":"claude-test-1","usage":{"input_tokens":0,"output_tokens":31,"cache_read_input_tokens":0,"cache_creation_input_tokens":0},"content":[{"type":"text","text":"two"}]}}"#, "\n",
),
);
let out = h.run(&["stats", &at(s), "--format", "json"]);
let row: serde_json::Value = out
.stdout
.lines()
.filter_map(|l| serde_json::from_str(l).ok())
.find(|v: &serde_json::Value| v["kind"] == "session")
.expect("row");
assert_eq!(
row["tokens"]["claude-test-1"]["output"], 42,
"{}",
out.stdout
);
}