use super::*;
#[test]
fn projects_bash_command_as_bash_card_not_user_turn() {
let mut state = MissionControlState::default();
state.apply_output_event(&OutputEvent::BashCommand {
command: "printf hi".into(),
});
let cards = project_cards(&state);
assert_eq!(cards.len(), 1);
assert_eq!(cards[0].role, TranscriptCardRole::Bash);
assert_eq!(cards[0].title, "Bash");
assert_eq!(cards[0].body, "printf hi");
}
#[test]
fn bash_mode_tool_card_uses_full_activity_preview_inline() {
let mut state = MissionControlState::default();
let id = ActivityId::new("bash_mode_1");
state.apply_output_event(&OutputEvent::BashCommand {
command: "printf 'one\\ntwo\\nthree\\nfour\\nfive\\n'".into(),
});
state
.transcript
.push_back("tool: ✓ bash • exit 0".to_string());
state.transcript.link_activity(
1,
TranscriptActivityLink {
activity_id: id.clone(),
tool_name: "bash".to_string(),
},
);
state.apply_activity_event(ActivityEvent::Started {
id: id.clone(),
parent_id: None,
kind: ActivityKind::Tool,
status: ActivityStatus::Success,
metadata: ActivityMetadata::new("bash"),
});
state.apply_activity_event(ActivityEvent::Delta {
id,
preview:
"STATUS: success\nCOMMAND: printf\nEXIT_CODE: 0\nOUTPUT:\none\ntwo\nthree\nfour\nfive\n"
.to_string(),
});
let cards = project_cards(&state);
assert_eq!(cards.len(), 2);
assert_eq!(cards[0].role, TranscriptCardRole::Bash);
assert_eq!(cards[1].role, TranscriptCardRole::Tool);
assert_eq!(cards[1].title, BASH_MODE_RESPONSE_TITLE);
assert!(cards[1].body.contains("OUTPUT:"), "{}", cards[1].body);
assert!(cards[1].body.contains("five"), "{}", cards[1].body);
assert!(cards[1].body.lines().count() > TOOL_PREVIEW_LINES);
let visual = state.transcript_visual_text();
assert!(visual.contains("Bash\n printf"), "{visual}");
assert!(
visual.contains("✓ Tool • Bash Mode Response\n STATUS: success"),
"{visual}"
);
assert!(visual.contains("OUTPUT:\n one"), "{visual}");
assert!(visual.contains("five"), "{visual}");
}
#[test]
fn bash_mode_response_cache_rebuilds_after_command_rollover() {
let mut state = MissionControlState::default();
let id = ActivityId::new("bash-rollover");
state.apply_output_event(&OutputEvent::BashCommand {
command: "printf response".into(),
});
state
.transcript
.push_back("tool: ✓ bash • exit 0".to_string());
state.transcript.link_activity(
1,
TranscriptActivityLink {
activity_id: id.clone(),
tool_name: "bash".to_string(),
},
);
state.apply_activity_event(ActivityEvent::ToolResultDetail {
id,
detail: crate::output::ToolActivityDetail {
tool_name: "bash".into(),
label: "bash".into(),
params: serde_json::json!({"command":"printf response"}),
metadata: serde_json::json!({
"exit_code": 0,
"stdout": "one\ntwo\n",
"stderr": ""
}),
status: ActivityStatus::Success,
output: "stdout:\none\ntwo\nstderr:\n".into(),
applied_diff: None,
},
});
state
.transcript
.extend((0..498).map(|index| format!("session: filler {index}")));
assert_eq!(state.transcript.len(), 500);
state.invalidate_transcript_cache();
let warmed = project_cards(&state);
assert_eq!(warmed[1].title, BASH_MODE_RESPONSE_TITLE);
let _ = state.cached_transcript_lines(80);
state.apply_output_event(&OutputEvent::UserPrompt {
text: "after rollover".into(),
});
assert_eq!(state.transcript.index_base(), 1);
assert_eq!(
state.transcript.front().map(String::as_str),
Some("tool: ✓ bash • exit 0")
);
let cached_cards = project_cards(&state);
assert_eq!(cached_cards[0].role, TranscriptCardRole::Bash);
assert_eq!(cached_cards[0].title, "Bash • exit 0");
let cached_lines = state
.cached_transcript_lines(80)
.into_iter()
.map(|line| line_text(&line))
.collect::<Vec<_>>();
state.invalidate_transcript_cache();
let rebuilt_cards = project_cards(&state);
let rebuilt_lines = state
.cached_transcript_lines(80)
.into_iter()
.map(|line| line_text(&line))
.collect::<Vec<_>>();
assert_eq!(cached_cards, rebuilt_cards);
assert_eq!(cached_lines, rebuilt_lines);
}
#[test]
fn normal_bash_tool_card_uses_structured_command_and_result_summary() {
let state = bash_card_state(
"call_bash",
"bash",
ActivityStatus::Success,
serde_json::json!({"command":"printf hi"}),
serde_json::json!({
"exit_code":0,
"timed_out":false,
"stdout":"one\ntwo\nthree\nfour\nfive\n",
"stderr":""
}),
"stdout:\none\ntwo\nthree\nfour\nfive\nstderr:\n",
);
let cards = project_cards(&state);
assert_eq!(cards[0].role, TranscriptCardRole::Bash);
assert_eq!(cards[0].title, "Bash • exit 0");
assert_eq!(cards[0].body, "printf hi\n5 lines");
let visual = state.transcript_visual_text();
assert!(
visual.contains("✓ Bash • exit 0\n printf hi\n 5 lines"),
"{visual}"
);
assert!(!visual.contains("STATUS:"), "{visual}");
assert!(!visual.contains("COMMAND:"), "{visual}");
assert!(!visual.contains("OUTPUT:"), "{visual}");
assert!(!visual.contains("one"), "{visual}");
}
#[test]
fn bash_card_projects_running_failure_timeout_cancel_and_alias() {
let running = bash_card_state(
"bash-running",
"shell",
ActivityStatus::Running,
serde_json::json!({"cmd":"cargo check"}),
serde_json::json!({}),
"",
);
let card = &project_cards(&running)[0];
assert_eq!(card.title, "Bash");
assert_eq!(card.body, "cargo check");
let failed = bash_card_state(
"bash-failed",
"bash",
ActivityStatus::Failed,
serde_json::json!({"command":"cargo test"}),
serde_json::json!({"exit_code":101,"timed_out":false,"stdout":"","stderr":"test parser failed\nmore"}),
"stdout:\n\nstderr:\ntest parser failed\nmore",
);
let card = &project_cards(&failed)[0];
assert_eq!(card.title, "Bash • exit 101");
assert_eq!(card.body, "cargo test\ntest parser failed");
let timed_out = bash_card_state(
"bash-timeout",
"bash",
ActivityStatus::Failed,
serde_json::json!({"command":"sleep 60","timeout":9}),
serde_json::json!({"exit_code":null,"timed_out":true,"stdout":"","stderr":""}),
"stdout:\n\nstderr:\n",
);
let card = &project_cards(&timed_out)[0];
assert_eq!(card.title, "Bash • timed out");
assert_eq!(card.body, "sleep 60\ntimed out after 9s");
let canceled = bash_card_state(
"bash-canceled",
"bash",
ActivityStatus::Canceled,
serde_json::json!({"command":"sleep 60"}),
serde_json::json!({}),
"prompt canceled",
);
let card = &project_cards(&canceled)[0];
assert_eq!(card.title, "Bash • canceled");
assert_eq!(card.body, "sleep 60\ncanceled");
let runtime_canceled = bash_card_state(
"bash-runtime-canceled",
"bash",
ActivityStatus::Failed,
serde_json::json!({"command":"sleep 60"}),
serde_json::json!({}),
"prompt canceled",
);
let card = &project_cards(&runtime_canceled)[0];
assert_eq!(card.status, TranscriptCardStatus::Canceled);
assert_eq!(card.title, "Bash • canceled");
assert_eq!(card.body, "sleep 60\ncanceled");
}
#[test]
fn bash_card_handles_metadata_free_failure_truncation_and_cleanup_warning() {
let failed = bash_card_state(
"bash-preflight",
"bash",
ActivityStatus::Failed,
serde_json::json!({"command":"cat ../outside"}),
serde_json::json!({}),
"cwd-scope preflight rejected parent traversal",
);
assert_eq!(
project_cards(&failed)[0].body,
"cat ../outside\ncwd-scope preflight rejected parent traversal"
);
let truncated = bash_card_state(
"bash-truncated",
"bash",
ActivityStatus::Failed,
serde_json::json!({"command":"yes"}),
serde_json::json!({"exit_code":0,"stdout_truncated":true,"stderr_truncated":false,"stdout":"many","stderr":""}),
"stdout:\nmany\nstderr:\n\n[output truncated]",
);
assert_eq!(project_cards(&truncated)[0].body, "yes\noutput truncated");
let cleanup = bash_card_state(
"bash-cleanup",
"bash",
ActivityStatus::Failed,
serde_json::json!({"command":"sleep 1"}),
serde_json::json!({"cleanup_warning":"process cleanup incomplete"}),
"stdout:\n\nstderr:\n\n[process cleanup incomplete]",
);
assert_eq!(
project_cards(&cleanup)[0].body,
"sleep 1\nprocess cleanup incomplete"
);
}
#[test]
fn bash_card_decodes_fallback_commands_at_ingestion_and_prefers_structured_params() {
let id = ActivityId::new("typed-bash");
let mut state = linked_tool_state(
id.as_str(),
"bash",
"bash printf label",
ActivityStatus::Running,
Vec::new(),
"",
);
assert_eq!(project_cards(&state)[0].body, "printf label");
state.apply_activity_event(ActivityEvent::Delta {
id: id.clone(),
preview: "COMMAND: printf preview\n".into(),
});
let node = state.nodes.get_mut(&id).unwrap();
node.preview = "COMMAND: should not be decoded during rendering".into();
node.metadata.label = "bash also not the command".into();
state.invalidate_transcript_cache();
assert_eq!(project_cards(&state)[0].body, "printf preview");
state.apply_activity_event(ActivityEvent::ToolResultDetail {
id,
detail: crate::output::ToolActivityDetail {
tool_name: "bash".into(),
label: "bash".into(),
params: serde_json::json!({"command": " ", "cmd": "printf structured"}),
metadata: serde_json::json!({"exit_code": 0, "stdout": "done\n"}),
status: ActivityStatus::Success,
output: "done\n".into(),
applied_diff: None,
},
});
assert_eq!(project_cards(&state)[0].body, "printf structured\n1 line");
}