use super::*;
#[test]
fn ast_grep_card_projects_params_counts_preview_and_truncation() {
let state = ast_grep_card_state(
"ast-grep-full",
ActivityStatus::Success,
serde_json::json!({
"pattern":"$F($$$)", "path":"src", "language":"rust",
"rewrite":"$F($$$)", "limit":7
}),
serde_json::json!({"matches_returned":1,"truncated":true,"stdout_truncated":true}),
"one\ntwo\nthree\nfour\nfive",
);
let card = &project_cards(&state)[0];
assert_eq!(card.role, TranscriptCardRole::AstGrep);
assert_eq!(card.title, "AST Grep • 1 match returned");
assert_eq!(
card.body,
"\"$F($$$)\"\nin src\nlanguage rust\nrewrite \"$F($$$)\"\nlimit 7\none\ntwo\nthree\nfour\n… preview limited to 4 lines\n… output truncated"
);
for (count, title) in [
(0, "AST Grep • 0 matches"),
(1, "AST Grep • 1 match"),
(2, "AST Grep • 2 matches"),
] {
let state = ast_grep_card_state(
&format!("ast-grep-count-{count}"),
ActivityStatus::Success,
serde_json::json!({"pattern":"x"}),
serde_json::json!({"matches_returned":count}),
"",
);
assert_eq!(project_cards(&state)[0].title, title);
}
let missing = ast_grep_card_state(
"ast-grep-missing-count",
ActivityStatus::Success,
serde_json::json!({"pattern":"x"}),
serde_json::json!({}),
"",
);
assert_eq!(project_cards(&missing)[0].title, "AST Grep");
let generic_truncation = ast_grep_card_state(
"ast-grep-generic-truncation",
ActivityStatus::Success,
serde_json::json!({"pattern":"x"}),
serde_json::json!({"matches_returned":2,"truncated":true}),
"one\ntwo",
);
let card = &project_cards(&generic_truncation)[0];
assert_eq!(card.title, "AST Grep • 2 matches returned");
assert!(card.body.ends_with("… search incomplete"));
let output_only_truncation = ast_grep_card_state(
"ast-grep-output-only-truncation",
ActivityStatus::Success,
serde_json::json!({"pattern":"x"}),
serde_json::json!({"matches_returned":1,"stdout_truncated":true}),
"one",
);
assert!(
project_cards(&output_only_truncation)[0]
.body
.ends_with("… output truncated")
);
}
#[test]
fn ast_grep_card_uses_path_and_language_metadata_and_preserves_failure_detail() {
let state = ast_grep_card_state(
"ast-grep-meta",
ActivityStatus::Running,
serde_json::json!({"pattern":"node"}),
serde_json::json!({"path":"resolved/src","language":"typescript"}),
"",
);
assert_eq!(
project_cards(&state)[0].body,
"\"node\"\nin resolved/src\nlanguage typescript"
);
let failed = ast_grep_card_state(
"ast-grep-install",
ActivityStatus::Failed,
serde_json::json!({"pattern":"node","path":"src"}),
serde_json::json!({}),
"ast-grep binary not found; install ast-grep with cargo install ast-grep",
);
assert_eq!(
project_cards(&failed)[0].body,
"\"node\"\nin src\nast-grep binary not found; install ast-grep with cargo install ast-grep"
);
let cleanup = ast_grep_card_state(
"ast-grep-cleanup",
ActivityStatus::Failed,
serde_json::json!({"pattern":"x"}),
serde_json::json!({"cleanup_warning":"cleanup warning\nsecondary"}),
"real error",
);
assert!(project_cards(&cleanup)[0].body.ends_with("cleanup warning"));
}
#[test]
fn ast_grep_card_timeout_cancellation_and_failure_status_are_exact() {
let timeout = ast_grep_card_state(
"ast-grep-timeout",
ActivityStatus::Failed,
serde_json::json!({"pattern":"x","path":"."}),
serde_json::json!({"timed_out":true}),
"partial",
);
assert_eq!(project_cards(&timeout)[0].title, "AST Grep • timed out");
assert!(
project_cards(&timeout)[0]
.body
.ends_with("search timed out")
);
let canceled = ast_grep_card_state(
"ast-grep-canceled",
ActivityStatus::Canceled,
serde_json::json!({"pattern":"x"}),
serde_json::json!({}),
"partial",
);
assert_eq!(
project_cards(&canceled)[0].body,
"\"x\"\nin .\nCanceled before completion"
);
let runtime_canceled = ast_grep_card_state(
"ast-grep-runtime-canceled",
ActivityStatus::Failed,
serde_json::json!({"pattern":"x"}),
serde_json::json!({}),
"prompt canceled",
);
assert_eq!(
project_cards(&runtime_canceled)[0].status,
TranscriptCardStatus::Canceled
);
let failed = ast_grep_card_state(
"ast-grep-negative-canceled",
ActivityStatus::Failed,
serde_json::json!({"pattern":"x"}),
serde_json::json!({}),
"real failure\nprompt canceled",
);
assert_eq!(
project_cards(&failed)[0].status,
TranscriptCardStatus::Failed
);
assert_eq!(project_cards(&failed)[0].body, "\"x\"\nin .\nreal failure");
}
#[test]
fn ast_grep_card_sanitizes_unicode_and_keeps_dedicated_copy_at_tiny_widths() {
let secret = "ast-grep-secret";
let long = "界".repeat(PREVIEW_LINE_MAX_DISPLAY_WIDTH);
let state = ast_grep_card_state(
"ast-grep-safe",
ActivityStatus::Success,
serde_json::json!({
"pattern":format!("authorization: Bearer {secret}\u{1b}[31m\nforged"),
"path":format!("src/authorization: Bearer {secret}\u{1b}]0;hidden\u{7}"), "language":long,
"rewrite":format!("token={secret}"), "limit":4
}),
serde_json::json!({"matches_returned":1}),
&format!("{long}\nauthorization: Bearer {secret}\u{1b}[2J"),
);
let card = &project_cards(&state)[0];
assert!(!card.body.contains(secret));
assert!(!card.body.contains('\u{1b}'));
assert!(
card.body
.lines()
.all(|line| UnicodeWidthStr::width(line) <= PREVIEW_LINE_MAX_DISPLAY_WIDTH)
);
let visual = state.transcript_visual_text();
let tiny_state = ast_grep_card_state(
"ast-grep-tiny",
ActivityStatus::Success,
serde_json::json!({"pattern":"x","path":"src","language":"rust","rewrite":"y","limit":4}),
serde_json::json!({"matches_returned":1}),
"safe output",
);
assert!(!visual.contains("Tool:"));
for width in [1, 2, 3, 12, CARD_LAYOUT_MIN_WIDTH, 80] {
let lines = visual_lines(&tiny_state, width);
assert!(lines.iter().all(
|line| UnicodeWidthStr::width(line_text(&line.line).as_str()) == usize::from(width)
));
let copied = visual_text_for_width(&tiny_state, width);
assert!(copied.contains("AST Grep"));
assert!(!copied.contains(secret));
}
let empty = ast_grep_card_state(
"ast-grep-empty",
ActivityStatus::Running,
serde_json::json!({}),
serde_json::json!({}),
"",
);
assert_eq!(project_cards(&empty)[0].body, "in .");
}