use serde_json::Value;
use std::path::PathBuf;
fn fixtures_dir() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests")
.join("fixtures")
}
fn load_last_event(name: &str) -> Value {
let path = fixtures_dir().join(name);
let text =
std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("read {}: {}", path.display(), e));
let last = text
.lines()
.filter(|l| !l.trim().is_empty())
.last()
.unwrap_or_else(|| panic!("fixture {} is empty", name));
serde_json::from_str(last).unwrap_or_else(|e| panic!("parse {}: {}", path.display(), e))
}
fn collect_agent_output(event: &Value) -> String {
let mut out = String::new();
let messages = event
.get("messages")
.and_then(|v| v.as_array())
.expect("messages array");
for m in messages {
if let Some(c) = m.get("content") {
if let Some(a) = c.get("AssistantWithToolCalls") {
if let Some(t) = a.get("text").and_then(|v| v.as_str()) {
out.push_str(t);
out.push('\n');
}
}
if let Some(t) = c.get("Text").and_then(|v| v.as_str()) {
out.push_str(t);
out.push('\n');
}
if let Some(tr) = c.get("ToolResult") {
if let Some(o) = tr.get("output").and_then(|v| v.as_str()) {
out.push_str(o);
out.push('\n');
}
}
}
}
out
}
fn assert_no_removed_patterns(fixture_name: &str) {
let ev = load_last_event(fixture_name);
let all = collect_agent_output(&ev);
let removed_markers: &[(&str, &str)] = &[
(
"(continuing...)",
"framework injected `(continuing...)` placeholder after empty \
model response — removed in commit 804fd31 (continuation recovery removal)",
),
(
"(completed)",
"framework injected `(completed)` placeholder on slow-empty — \
removed in commit 804fd31",
),
(
"summarize and stop instead of continuing",
"old #5 Auto-STOP nudge was command-style and caused weak models \
to skip user-requested steps — replaced with `key diagnostic lines` \
wording in commit 389d604",
),
];
for (pat, reason) in removed_markers {
assert!(
!all.contains(pat),
"fixture {} contains `{}` — {}",
fixture_name,
pat,
reason,
);
}
}
fn collect_tool_call_args(event: &Value) -> Vec<String> {
let mut out = Vec::new();
let messages = event
.get("messages")
.and_then(|v| v.as_array())
.expect("messages array");
for m in messages {
if let Some(a) = m
.get("content")
.and_then(|c| c.get("AssistantWithToolCalls"))
{
if let Some(tcs) = a.get("tool_calls").and_then(|v| v.as_array()) {
for tc in tcs {
if let Some(args) = tc.get("arguments").and_then(|v| v.as_str()) {
out.push(args.to_string());
}
}
}
}
}
out
}
fn assert_no_shell_workaround_calls(fixture_name: &str) {
let ev = load_last_event(fixture_name);
let args_list = collect_tool_call_args(&ev);
let bypass_markers = &["sed -i", "perl -pi", "awk -i inplace"];
for args in &args_list {
for bad in bypass_markers {
assert!(
!args.contains(bad),
"fixture {} has a tool call with `{}` — shell workaround \
pattern (see 426-atom 2026-04-21 session regression). \
args: {}",
fixture_name,
bad,
args,
);
}
}
}
fn assert_bash_has_exit_markers(fixture_name: &str) {
let ev = load_last_event(fixture_name);
let messages = ev
.get("messages")
.and_then(|v| v.as_array())
.expect("messages");
let mut tool_name_of: std::collections::HashMap<usize, String> =
std::collections::HashMap::new();
for (i, m) in messages.iter().enumerate() {
if let Some(a) = m
.get("content")
.and_then(|c| c.get("AssistantWithToolCalls"))
{
if let Some(tcs) = a.get("tool_calls").and_then(|v| v.as_array()) {
for (k, tc) in tcs.iter().enumerate() {
if let Some(name) = tc.get("name").and_then(|v| v.as_str()) {
tool_name_of.insert(i + 1 + k, name.to_string());
}
}
}
}
}
let mut bash_results_checked = 0;
for (i, m) in messages.iter().enumerate() {
if tool_name_of.get(&i).map(|n| n == "bash").unwrap_or(false) {
if let Some(out) = m
.get("content")
.and_then(|c| c.get("ToolResult"))
.and_then(|tr| tr.get("output"))
.and_then(|v| v.as_str())
{
let has_exit = out.contains("exit: ") || out.contains("killed:");
assert!(
has_exit,
"fixture {} bash ToolResult at msg[{}] missing `exit: N` / `killed:` marker — \
check that P0 #3 (exit code in bash marker) is still in place. output:\n{}",
fixture_name, i, out
);
bash_results_checked += 1;
}
}
}
assert!(
bash_results_checked > 0,
"fixture {} had no bash ToolResults to check — either fixture is \
not bash-exercising or tool_name tracking broke",
fixture_name,
);
}
#[test]
fn p0_sprint_clean_session_is_free_of_removed_patterns() {
assert_no_removed_patterns("session_p0_sprint_clean.jsonl");
}
#[test]
fn p0_sprint_clean_session_has_no_shell_workarounds() {
assert_no_shell_workaround_calls("session_p0_sprint_clean.jsonl");
}
#[test]
fn p0_sprint_clean_session_bash_has_exit_markers() {
assert_bash_has_exit_markers("session_p0_sprint_clean.jsonl");
}
#[test]
fn p0_sprint_clean_session_tool_call_result_parity() {
assert_tool_call_result_parity("session_p0_sprint_clean.jsonl");
}
#[test]
fn path_404_recovery_session_tool_call_result_parity() {
assert_tool_call_result_parity("session_404_recovery.jsonl");
}
#[test]
fn path_404_recovery_session_is_free_of_removed_patterns() {
assert_no_removed_patterns("session_404_recovery.jsonl");
}
#[test]
fn path_404_recovery_session_has_no_shell_workarounds() {
assert_no_shell_workaround_calls("session_404_recovery.jsonl");
}
fn assert_tool_call_result_parity(fixture_name: &str) {
let ev = load_last_event(fixture_name);
let messages = ev
.get("messages")
.and_then(|v| v.as_array())
.expect("messages array");
let mut i = 0;
while i < messages.len() {
if let Some(a) = messages[i]
.get("content")
.and_then(|c| c.get("AssistantWithToolCalls"))
{
if let Some(tcs) = a.get("tool_calls").and_then(|v| v.as_array()) {
let expected = tcs.len();
let mut actual = 0;
let mut j = i + 1;
while j < messages.len() {
if messages[j]
.get("content")
.and_then(|c| c.get("ToolResult"))
.is_some()
{
actual += 1;
j += 1;
} else {
break;
}
}
assert_eq!(
expected, actual,
"fixture {} msg[{}]: assistant declares {} tool calls but \
followed by {} ToolResult messages — tool_call/tool_result \
mismatch poisons the next provider request",
fixture_name, i, expected, actual,
);
}
}
i += 1;
}
}
#[test]
fn fixture_collector_sees_assistant_content() {
let ev = load_last_event("session_p0_sprint_clean.jsonl");
let all = collect_agent_output(&ev);
assert!(
all.len() > 1000,
"collected agent output suspiciously short ({} bytes) — jsonl schema may have changed, \
collect_agent_output needs updating",
all.len()
);
assert!(
all.contains("任务总结") || all.contains("任务结果") || all.contains("## "),
"expected a markdown summary header in final Assistant text; got first 300 chars:\n{}",
all.chars().take(300).collect::<String>()
);
}