use super::*;
#[test]
fn hash_edit_card_projects_running_operations_without_patch_payload() {
let input = "[a.rs#A1B2]\nSWAP 1.=1:\n+secret payload:\n[b.rs#B2C3]\nMV moved.rs\n[c.rs#C3D4]\nREM\n[d.rs#D4E5]\nINS.TAIL:\n+x\n[e.rs#E5F6]\nDEL 1";
let state = hash_edit_card_state(
"hash-edit-running",
ActivityStatus::Running,
input,
serde_json::json!({}),
"",
None,
);
let card = &project_cards(&state)[0];
assert_eq!(card.role, TranscriptCardRole::HashEdit);
assert_eq!(card.title, "Hash Edit");
assert_eq!(
card.body,
"Update a.rs\nMove b.rs → moved.rs\nDelete c.rs\nUpdate d.rs\n… 1 more files"
);
for hidden in ["A1B2", "SWAP", "secret payload", "input_bytes", "Tool:"] {
assert!(!state.transcript_visual_text().contains(hidden));
}
}
#[test]
fn hash_edit_card_projects_success_titles_rows_and_diff_preview() {
let update = hash_edit_card_state(
"hash-edit-update",
ActivityStatus::Success,
"patch payload",
serde_json::json!({
"outcome":"success", "added":3, "removed":3, "diff_truncated":false,
"files":[{"path":"src/a.rs","destination":null,"operation":"update","status":"committed"}]
}),
"applied",
Some(
"diff --git a/src/a.rs b/src/a.rs\n--- a/src/a.rs\n+++ b/src/a.rs\n@@ -1,3 +1,3 @@\n-old1\n+new1\n-old2\n+new2\n-old3\n+new3",
),
);
let card = &project_cards(&update)[0];
assert_eq!(card.title, "Hash Edit • +3 −3");
assert_eq!(
card.body,
"Updated src/a.rs\n-old1\n+new1\n-old2\n+new2\n… diff preview limited to 4 changed lines"
);
assert!(!card.body.contains("+++"));
assert!(!card.body.contains("@@"));
let prefix_payload = hash_edit_card_state(
"hash-edit-prefix-payload",
ActivityStatus::Success,
"raw patch",
serde_json::json!({
"outcome":"success", "added":1, "removed":1, "diff_truncated":false,
"files":[{"path":"src/a.rs","operation":"update","status":"committed"}]
}),
"applied",
Some(
"diff --git a/src/a.rs b/src/a.rs\n--- a/src/a.rs\n+++ b/src/a.rs\n@@ -1,1 +1,1 @@\n--- value\n+++ value",
),
);
let prefix_card = &project_cards(&prefix_payload)[0];
assert!(
prefix_card.body.contains("--- value"),
"{}",
prefix_card.body
);
assert!(
prefix_card.body.contains("+++ value"),
"{}",
prefix_card.body
);
for (id, metadata, expected_title, expected_body) in [
(
"move",
serde_json::json!({"outcome":"success","added":0,"removed":0,"files":[{"path":"a.rs","destination":"b.rs","operation":"move","status":"committed"}]}),
"Hash Edit • moved",
"Moved a.rs → b.rs",
),
(
"move-edit",
serde_json::json!({"outcome":"success","added":2,"removed":1,"files":[{"path":"a.rs","destination":"b.rs","operation":"move","status":"committed"}]}),
"Hash Edit • moved • +2 −1",
"Moved a.rs → b.rs",
),
(
"delete",
serde_json::json!({"outcome":"success","added":0,"removed":3,"files":[{"path":"old.rs","destination":null,"operation":"delete","status":"committed"}]}),
"Hash Edit • deleted • −3",
"Deleted old.rs",
),
(
"noop",
serde_json::json!({"outcome":"success","added":0,"removed":0,"files":[{"path":"same.rs","destination":null,"operation":"noop","status":"committed"}]}),
"Hash Edit • no changes",
"Unchanged same.rs",
),
] {
let state = hash_edit_card_state(
id,
ActivityStatus::Success,
"raw patch",
metadata,
"applied",
Some("--- a/old.rs\n+++ /dev/null\n-secret deleted content"),
);
let card = &project_cards(&state)[0];
assert_eq!(card.title, expected_title);
assert!(card.body.starts_with(expected_body), "{}", card.body);
if id == "delete" {
assert!(!card.body.contains("secret deleted content"));
}
}
let multi_noop = hash_edit_card_state(
"hash-edit-multi-noop",
ActivityStatus::Success,
"raw patch",
serde_json::json!({
"outcome":"success","added":0,"removed":0,
"files":[
{"path":"a","operation":"noop","status":"committed"},
{"path":"b","operation":"noop","status":"committed"}
]
}),
"applied",
None,
);
assert_eq!(
project_cards(&multi_noop)[0].title,
"Hash Edit • 2 files • no changes"
);
let mixed_delete = hash_edit_card_state(
"hash-edit-mixed-delete",
ActivityStatus::Success,
"raw patch",
serde_json::json!({
"outcome":"success","added":1,"removed":2,
"files":[
{"path":"a","operation":"update","status":"committed"},
{"path":"b","operation":"delete","status":"committed"}
]
}),
"applied",
Some(
"diff --git a/a b/a\n--- a/a\n+++ b/a\n@@ -1,1 +1,1 @@\n-old\n+new\n-secret deleted content",
),
);
let mixed_card = &project_cards(&mixed_delete)[0];
assert!(!mixed_card.body.contains("secret deleted content"));
assert!(!mixed_card.body.contains("+new"));
}
#[test]
fn hash_edit_card_projects_stale_partial_canceled_and_generic_failures() {
let stale = hash_edit_card_state(
"hash-edit-stale",
ActivityStatus::Failed,
"[src/a.rs#A1B2]\nDEL 1",
serde_json::json!({
"outcome":"stale_tag","error_kind":"stale_tag","path":"src/a.rs",
"expected_tag":"A1B2","current_tag":"C3D4","sections":1,"committed":0
}),
"raw recovery context that must not render",
None,
);
let card = &project_cards(&stale)[0];
assert_eq!(card.title, "Hash Edit • stale tag");
assert_eq!(
card.body,
"src/a.rs\nExpected #A1B2 · current #C3D4\nRe-read file, then retry."
);
let partial = hash_edit_card_state(
"hash-edit-partial",
ActivityStatus::Failed,
"raw patch",
serde_json::json!({
"outcome":"partial","error_kind":"write","sections":3,"committed":1,
"files":[
{"path":"a.rs","operation":"update","status":"committed"},
{"path":"b.rs","operation":"update","status":"failed","error":"permission denied\ntrace"},
{"path":"c.rs","operation":"update","status":"not_written"}
]
}),
"failure detail",
None,
);
let card = &project_cards(&partial)[0];
assert_eq!(card.title, "Hash Edit • partial 1/3");
assert_eq!(
card.body,
"✓ Updated a.rs\n✗ b.rs · permission denied\n○ c.rs · not written\nChanges to 1 file remain on disk."
);
let partial_canceled = hash_edit_card_state(
"hash-edit-partial-canceled",
ActivityStatus::Failed,
"raw patch",
serde_json::json!({
"outcome":"partial","error_kind":"canceled","sections":2,"committed":1,"changed":1,
"files":[
{"path":"a.rs","operation":"update","status":"committed"},
{"path":"b.rs","operation":"update","status":"canceled"}
]
}),
"prompt canceled",
None,
);
let card = &project_cards(&partial_canceled)[0];
assert_eq!(card.title, "Hash Edit • partial 1/2");
assert_eq!(
card.body,
"✓ Updated a.rs\n○ b.rs · not written\nChanges to 1 file remain on disk."
);
let canceled = hash_edit_card_state(
"hash-edit-canceled",
ActivityStatus::Failed,
"raw patch",
serde_json::json!({"outcome":"canceled","error_kind":"canceled","sections":0,"committed":0,"changed":0}),
"prompt canceled",
None,
);
let card = &project_cards(&canceled)[0];
assert_eq!(card.status, TranscriptCardStatus::Canceled);
assert_eq!(card.title, "Hash Edit • canceled");
assert_eq!(card.body, "Canceled before completion");
let secret = "generic-failure-secret";
let failed = hash_edit_card_state(
"hash-edit-failed",
ActivityStatus::Failed,
"[known.rs#A1B2]\nDEL 1",
serde_json::json!({"path":"known.rs"}),
&format!("authorization: Bearer {secret}\ntrace"),
None,
);
let card = &project_cards(&failed)[0];
assert_eq!(card.title, "Hash Edit");
assert_eq!(card.body, "known.rs\nauthorization: <redacted> <redacted>");
assert!(!card.body.contains(secret));
}
#[test]
fn hash_edit_card_sanitizes_and_preserves_copy_at_tiny_widths() {
let path_secret = "path-secret-value";
let diff_secret = "diff-secret-value";
let state = hash_edit_card_state(
"hash-edit-safe",
ActivityStatus::Success,
"[raw-secret.rs#A1B2]\nSWAP 1.=1:\n+raw payload",
serde_json::json!({
"outcome":"success","added":1,"removed":1,"diff_truncated":true,
"files":[{"path":format!("src/token={path_secret}\u{1b}[31m.rs"),"operation":"update","status":"committed"}]
}),
"applied",
Some(&format!(
"--- a/x\n+++ b/x\n-old authorization: Bearer {diff_secret}\n+new\u{1b}[2J"
)),
);
let card = &project_cards(&state)[0];
assert!(card.body.contains("<redacted>"), "{}", card.body);
assert!(!card.body.contains(path_secret));
assert!(!card.body.contains(diff_secret));
assert!(!card.body.contains("raw payload"));
assert!(!card.body.contains('\u{1b}'));
assert!(
card.body
.ends_with("… diff preview limited to 4 changed lines")
);
for width in [1, 2, 3, 12, CARD_LAYOUT_MIN_WIDTH, 80] {
let lines = visual_lines(&state, width);
assert!(!lines.is_empty());
assert!(lines.iter().all(|line| {
UnicodeWidthStr::width(line_text(&line.line).as_str()) == usize::from(width)
}));
let copied = visual_text_for_width(&state, width);
assert!(copied.contains("Hash Edit"), "width {width}: {copied:?}");
assert!(copied.contains("Updated src/token=<redacted>"));
assert!(!copied.contains(path_secret));
assert!(!copied.contains(diff_secret));
assert!(!copied.contains("raw payload"));
assert!(!copied.contains("Tool:"));
}
}
#[test]
fn activity_viewer_matches_canonical_hash_edit_card() {
let state = hash_edit_card_state(
"hash-edit-viewer-parity",
ActivityStatus::Success,
"raw patch",
serde_json::json!({
"outcome":"success", "added":2, "removed":1, "diff_truncated":false,
"files":[{"path":"src/a.rs","destination":null,"operation":"update","status":"committed"}]
}),
"applied",
Some("--- a/src/a.rs\n+++ b/src/a.rs\n-old\n+new"),
);
let id = ActivityId::new("hash-edit-viewer-parity");
let transcript = visual_lines(&state, 80);
let activity = activity_card_visual_lines(
&state,
state.nodes.get(&id).expect("hash-edit activity"),
80,
false,
);
assert_eq!(transcript.len(), activity.len() + 1);
assert_eq!(
transcript[..transcript.len() - 1]
.iter()
.map(|line| line_text(&line.line))
.collect::<Vec<_>>(),
activity
.iter()
.map(|line| line_text(&line.line))
.collect::<Vec<_>>()
);
}