use super::*;
use crate::claude_runtime::stream::{TerminalClassification, TerminalResult};
enum ExitResult {
Success,
Failure,
}
fn exit_status(result: ExitResult) -> std::process::ExitStatus {
#[cfg(unix)]
{
let program = match result {
ExitResult::Success => "true",
ExitResult::Failure => "false",
};
std::process::Command::new(program).status().unwrap()
}
#[cfg(windows)]
{
let code = match result {
ExitResult::Success => "0",
ExitResult::Failure => "1",
};
std::process::Command::new("cmd")
.args(["/C", &format!("exit {code}")])
.status()
.unwrap()
}
}
#[test]
fn terminal_assessment_matrix() {
let success = TerminalResult {
classification: TerminalClassification::Success {
subtype: "success".into(),
},
result_text: Some("ok".into()),
error: None,
session_id: Some("s".into()),
num_turns: Some(1),
usage: None,
context: None,
total_cost_usd: None,
permission_denials: Vec::new(),
stop_reason: None,
};
let failure = TerminalResult {
classification: TerminalClassification::Failure {
subtype: "error_max_turns".into(),
is_error: true,
},
result_text: Some("hit max".into()),
error: Some("hit max".into()),
session_id: None,
num_turns: Some(3),
usage: None,
context: None,
total_cost_usd: None,
permission_denials: Vec::new(),
stop_reason: None,
};
let invalid = TerminalResult {
classification: TerminalClassification::Invalid {
reason: "missing subtype".into(),
},
result_text: None,
error: Some("missing subtype".into()),
session_id: None,
num_turns: None,
usage: None,
context: None,
total_cost_usd: None,
permission_denials: Vec::new(),
stop_reason: None,
};
let ok_status = exit_status(ExitResult::Success);
let err_status = exit_status(ExitResult::Failure);
assert!(matches!(
assess_terminal(Some(success.clone()), ok_status, ""),
TerminalOutcome::Success(_)
));
assert!(matches!(
assess_terminal(Some(success), err_status, ""),
TerminalOutcome::Failure { .. }
));
assert!(matches!(
assess_terminal(Some(failure), ok_status, ""),
TerminalOutcome::Failure {
prefer_detail: false,
..
}
));
assert!(matches!(
assess_terminal(Some(invalid), ok_status, ""),
TerminalOutcome::Failure { .. }
));
assert!(matches!(
assess_terminal(None, ok_status, ""),
TerminalOutcome::Failure { terminal: None, .. }
));
let TerminalOutcome::Failure { detail, .. } =
assess_terminal(None, err_status, "error: unknown option '--max-turns'")
else {
panic!("unsupported --max-turns must fail");
};
assert_eq!(
detail,
"claude code: this claude binary rejected --max-turns; upgrade Claude Code or remove the turn cap"
);
let safeguard = TerminalResult {
classification: TerminalClassification::Failure {
subtype: "success".into(),
is_error: true,
},
result_text: Some(
"API Error: Fable 5's safeguards flagged this message (https://www.anthropic.com/legal/aup)."
.into(),
),
error: Some(
"API Error: Fable 5's safeguards flagged this message (https://www.anthropic.com/legal/aup)."
.into(),
),
session_id: None,
num_turns: Some(1),
usage: None,
context: None,
total_cost_usd: None,
permission_denials: Vec::new(),
stop_reason: None,
};
let TerminalOutcome::Failure { detail, .. } = assess_terminal(Some(safeguard), err_status, "")
else {
panic!("safeguard stream failure must fail");
};
assert!(
detail.contains("safeguards flagged"),
"stream failure text must surface, got: {detail}"
);
assert!(
!detail.contains("process exited"),
"empty-stderr protocol failures should not bury the stream reason under exit code: {detail}"
);
let success_with_text = TerminalResult {
classification: TerminalClassification::Success {
subtype: "success".into(),
},
result_text: Some("ok".into()),
error: None,
session_id: None,
num_turns: Some(1),
usage: None,
context: None,
total_cost_usd: None,
permission_denials: Vec::new(),
stop_reason: None,
};
let TerminalOutcome::Failure { detail, .. } =
assess_terminal(Some(success_with_text), err_status, "")
else {
panic!("success stream with non-zero exit must fail");
};
assert!(
detail.contains("process exited"),
"success stream text must not replace exit diagnosis: {detail}"
);
assert!(
!detail.contains("ok"),
"success answer must not be reported as the failure detail: {detail}"
);
let failed_with_stderr = TerminalResult {
classification: TerminalClassification::Failure {
subtype: "error_during_execution".into(),
is_error: true,
},
result_text: Some("stream said boom".into()),
error: Some("stream said boom".into()),
session_id: None,
num_turns: None,
usage: None,
context: None,
total_cost_usd: None,
permission_denials: Vec::new(),
stop_reason: None,
};
let TerminalOutcome::Failure { detail, .. } =
assess_terminal(Some(failed_with_stderr), err_status, "segfault nearby")
else {
panic!("failure with stderr must fail");
};
assert!(
detail.contains("segfault nearby") && detail.contains("stream said boom"),
"stderr and stream failure should both surface: {detail}"
);
}