diff --git a/proptest-regressions/event_preview.txt b/proptest-regressions/event_preview.txt new file mode 100644 index 000000000..ff5d33e65 --- /dev/null +++ b/proptest-regressions/event_preview.txt @@ -0,0 +1,7 @@ +# Seeds for failure cases proptest has generated in the past. It is +# automatically read and these particular cases re-run before any +# novel cases are generated. +# +# It is recommended to check this file in to source control so that +# everyone who runs the test benefits from these saved cases. +cc 0c081b28fcee5c8c4b5439561a52585269c1cfb4d75510d9001f584fb8ac3aea # shrinks to body = "", detail = "" diff --git a/specs/task-agent-chat-owner-approval.spec.md b/specs/task-agent-chat-owner-approval.spec.md index a6d6e2339..fb3bc9343 100644 --- a/specs/task-agent-chat-owner-approval.spec.md +++ b/specs/task-agent-chat-owner-approval.spec.md @@ -41,6 +41,8 @@ expiration, and one-shot state before releasing a coding-agent operation. prevents an approval click from becoming undecryptable when the bridge device was registered after an older room session was established. - Keep the existing `org.octos.approval_request` protocol backward compatible. +- Custom room-message previews show only the HTML-escaped fallback body, never + the Rust debug representation or extra protocol fields. ## Boundaries @@ -50,6 +52,8 @@ expiration, and one-shot state before releasing a coding-agent operation. - src/shared/mod.rs - src/home/room_screen.rs - src/sliding_sync.rs +- src/event_preview.rs +- proptest-regressions/event_preview.txt - resources/i18n/en.json - resources/i18n/zh-CN.json @@ -69,6 +73,16 @@ expiration, and one-shot state before releasing a coding-agent operation. ## Completion Criteria +### Rule: custom-preview — preview depends only on escaped fallback body + +Scenario: Custom message summaries do not expose protocol internals + Test: custom_message_preview_shows_escaped_body_without_protocol_details + Test: prop_custom_preview_depends_only_on_escaped_body + Given a custom room message with a fallback body and arbitrary extra fields + When text_preview_of_message creates a room or reply summary + Then the preview is exactly the HTML-escaped fallback body + And protocol fields and Rust debug output do not appear + Scenario: Public project-room status is read-only Test: test_agentchat_public_status_has_no_actions Given a timeline event has msgtype `com.agentchat.approval.status.v1` diff --git a/src/event_preview.rs b/src/event_preview.rs index a80bf5810..4bbb0b721 100644 --- a/src/event_preview.rs +++ b/src/event_preview.rs @@ -478,10 +478,7 @@ fn text_preview_of_message( htmlize::escape_text(&video.body) } ), - MessageType::_Custom(custom) => format!( - "[Custom message]: {:?}", - custom, - ), + MessageType::_Custom(_) => htmlize::escape_text(msg.body()).to_string(), other => format!( "[Unknown message type]: {}", htmlize::escape_text(other.body()), @@ -834,3 +831,34 @@ pub fn text_preview_of_room_membership_change( }; Some(TextPreview::from((text, BeforeText::UsernameWithoutColon))) } + +#[cfg(test)] +mod preview_tests { + use super::*; + use proptest::prelude::*; + + fn custom_message(body: &str, detail: &str) -> MessageType { + serde_json::from_value(serde_json::json!({ + "msgtype": "com.agentchat.approval.status.v1", + "body": body, + "com.agentchat.approval": { "kind": "status", "internal_detail": detail }, + })).expect("valid custom room message") + } + + #[test] + fn custom_message_preview_shows_escaped_body_without_protocol_details() { + let message = custom_message("等待 & approval", "PRIVATE_SENTINEL"); + let preview = text_preview_of_message(&message, "agent").text; + assert_eq!(preview, "等待 <owner> & approval"); + assert!(!preview.contains("PRIVATE_SENTINEL")); + assert!(!preview.contains("CustomEventContent")); + } + + proptest! { + #[test] + fn prop_custom_preview_depends_only_on_escaped_body(body in any::(), detail in any::()) { + let preview = text_preview_of_message(&custom_message(&body, &detail), "agent").text; + prop_assert_eq!(preview, htmlize::escape_text(&body).to_string()); + } + } +}