use serde::{Deserialize, Serialize};
use super::{
session::{FileDiff, Message, Part, Session},
shared::SessionError,
};
use crate::client::Opencode;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "type")]
pub enum EventListResponse {
#[serde(rename = "installation.updated")]
InstallationUpdated {
properties: InstallationUpdatedProps,
},
#[serde(rename = "installation.update-available")]
InstallationUpdateAvailable {
properties: InstallationUpdateAvailableProps,
},
#[serde(rename = "project.updated")]
ProjectUpdated {
properties: ProjectUpdatedProps,
},
#[serde(rename = "server.instance.disposed")]
ServerInstanceDisposed {
properties: ServerInstanceDisposedProps,
},
#[serde(rename = "server.connected")]
ServerConnected {
properties: EmptyProps,
},
#[serde(rename = "global.disposed")]
GlobalDisposed {
properties: EmptyProps,
},
#[serde(rename = "lsp.client.diagnostics")]
LspClientDiagnostics {
properties: LspClientDiagnosticsProps,
},
#[serde(rename = "lsp.updated")]
LspUpdated {
properties: EmptyProps,
},
#[serde(rename = "file.edited")]
FileEdited {
properties: FileEditedProps,
},
#[serde(rename = "file.watcher.updated")]
FileWatcherUpdated {
properties: FileWatcherUpdatedProps,
},
#[serde(rename = "message.updated")]
MessageUpdated {
properties: MessageUpdatedProps,
},
#[serde(rename = "message.removed")]
MessageRemoved {
properties: MessageRemovedProps,
},
#[serde(rename = "message.part.updated")]
MessagePartUpdated {
properties: MessagePartUpdatedProps,
},
#[serde(rename = "message.part.delta")]
MessagePartDelta {
properties: MessagePartDeltaProps,
},
#[serde(rename = "message.part.removed")]
MessagePartRemoved {
properties: MessagePartRemovedProps,
},
#[serde(rename = "permission.asked")]
PermissionAsked {
properties: serde_json::Value,
},
#[serde(rename = "permission.replied")]
PermissionReplied {
properties: PermissionRepliedProps,
},
#[serde(rename = "session.created")]
SessionCreated {
properties: SessionCreatedProps,
},
#[serde(rename = "session.updated")]
SessionUpdated {
properties: SessionUpdatedProps,
},
#[serde(rename = "session.deleted")]
SessionDeleted {
properties: SessionDeletedProps,
},
#[serde(rename = "session.status")]
SessionStatus {
properties: SessionStatusProps,
},
#[serde(rename = "session.idle")]
SessionIdle {
properties: SessionIdleProps,
},
#[serde(rename = "session.diff")]
SessionDiff {
properties: SessionDiffProps,
},
#[serde(rename = "session.compacted")]
SessionCompacted {
properties: SessionCompactedProps,
},
#[serde(rename = "session.error")]
SessionError {
properties: SessionErrorProps,
},
#[serde(rename = "question.asked")]
QuestionAsked {
properties: serde_json::Value,
},
#[serde(rename = "question.replied")]
QuestionReplied {
properties: QuestionRepliedProps,
},
#[serde(rename = "question.rejected")]
QuestionRejected {
properties: QuestionRejectedProps,
},
#[serde(rename = "todo.updated")]
TodoUpdated {
properties: TodoUpdatedProps,
},
#[serde(rename = "tui.prompt.append")]
TuiPromptAppend {
properties: TuiPromptAppendProps,
},
#[serde(rename = "tui.command.execute")]
TuiCommandExecute {
properties: TuiCommandExecuteProps,
},
#[serde(rename = "tui.toast.show")]
TuiToastShow {
properties: TuiToastShowProps,
},
#[serde(rename = "tui.session.select")]
TuiSessionSelect {
properties: TuiSessionSelectProps,
},
#[serde(rename = "mcp.tools.changed")]
McpToolsChanged {
properties: McpToolsChangedProps,
},
#[serde(rename = "mcp.browser.open.failed")]
McpBrowserOpenFailed {
properties: McpBrowserOpenFailedProps,
},
#[serde(rename = "command.executed")]
CommandExecuted {
properties: CommandExecutedProps,
},
#[serde(rename = "vcs.branch.updated")]
VcsBranchUpdated {
properties: VcsBranchUpdatedProps,
},
#[serde(rename = "pty.created")]
PtyCreated {
properties: PtyCreatedProps,
},
#[serde(rename = "pty.updated")]
PtyUpdated {
properties: PtyUpdatedProps,
},
#[serde(rename = "pty.exited")]
PtyExited {
properties: PtyExitedProps,
},
#[serde(rename = "pty.deleted")]
PtyDeleted {
properties: PtyDeletedProps,
},
#[serde(rename = "worktree.ready")]
WorktreeReady {
properties: WorktreeReadyProps,
},
#[serde(rename = "worktree.failed")]
WorktreeFailed {
properties: WorktreeFailedProps,
},
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct EmptyProps {}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct InstallationUpdatedProps {
pub version: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct InstallationUpdateAvailableProps {
pub version: String,
}
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ProjectUpdatedProps {
pub properties: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ServerInstanceDisposedProps {
pub directory: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct LspClientDiagnosticsProps {
pub path: String,
#[serde(rename = "serverID")]
pub server_id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct MessageUpdatedProps {
pub info: Message,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct MessageRemovedProps {
#[serde(rename = "messageID")]
pub message_id: String,
#[serde(rename = "sessionID")]
pub session_id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct MessagePartUpdatedProps {
pub part: Part,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct MessagePartDeltaProps {
#[serde(rename = "sessionID")]
pub session_id: String,
#[serde(rename = "messageID")]
pub message_id: String,
#[serde(rename = "partID")]
pub part_id: String,
pub field: String,
pub delta: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct MessagePartRemovedProps {
#[serde(rename = "sessionID")]
pub session_id: String,
#[serde(rename = "messageID")]
pub message_id: String,
#[serde(rename = "partID")]
pub part_id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum PermissionReply {
#[serde(rename = "once")]
Once,
#[serde(rename = "always")]
Always,
#[serde(rename = "reject")]
Reject,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct PermissionRepliedProps {
#[serde(rename = "sessionID")]
pub session_id: String,
#[serde(rename = "requestID")]
pub request_id: String,
pub reply: PermissionReply,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct SessionCreatedProps {
pub info: Session,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct SessionUpdatedProps {
pub info: Session,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct SessionDeletedProps {
pub info: Session,
}
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct SessionStatusProps {
#[serde(rename = "sessionID")]
pub session_id: String,
pub status: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct SessionIdleProps {
#[serde(rename = "sessionID")]
pub session_id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct SessionDiffProps {
#[serde(rename = "sessionID")]
pub session_id: String,
pub diff: Vec<FileDiff>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct SessionCompactedProps {
#[serde(rename = "sessionID")]
pub session_id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct SessionErrorProps {
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<SessionError>,
#[serde(rename = "sessionID")]
#[serde(skip_serializing_if = "Option::is_none")]
pub session_id: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct QuestionRepliedProps {
#[serde(rename = "sessionID")]
pub session_id: String,
#[serde(rename = "requestID")]
pub request_id: String,
pub answers: Vec<Vec<String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct QuestionRejectedProps {
#[serde(rename = "sessionID")]
pub session_id: String,
#[serde(rename = "requestID")]
pub request_id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct Todo {
pub content: String,
pub status: String,
pub priority: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct TodoUpdatedProps {
#[serde(rename = "sessionID")]
pub session_id: String,
pub todos: Vec<Todo>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct FileEditedProps {
pub file: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum FileWatcherEvent {
#[serde(rename = "add")]
Add,
#[serde(rename = "change")]
Change,
#[serde(rename = "unlink")]
Unlink,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct FileWatcherUpdatedProps {
pub event: FileWatcherEvent,
pub file: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct TuiPromptAppendProps {
pub text: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct TuiCommandExecuteProps {
pub command: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum ToastVariant {
#[serde(rename = "info")]
Info,
#[serde(rename = "success")]
Success,
#[serde(rename = "warning")]
Warning,
#[serde(rename = "error")]
Error,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct TuiToastShowProps {
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
pub message: String,
pub variant: ToastVariant,
#[serde(skip_serializing_if = "Option::is_none")]
pub duration: Option<f64>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct TuiSessionSelectProps {
#[serde(rename = "sessionID")]
pub session_id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct McpToolsChangedProps {
pub server: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct McpBrowserOpenFailedProps {
#[serde(rename = "mcpName")]
pub mcp_name: String,
pub url: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct CommandExecutedProps {
pub name: String,
#[serde(rename = "sessionID")]
pub session_id: String,
pub arguments: String,
#[serde(rename = "messageID")]
pub message_id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct VcsBranchUpdatedProps {
#[serde(skip_serializing_if = "Option::is_none")]
pub branch: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum PtyStatus {
#[serde(rename = "running")]
Running,
#[serde(rename = "exited")]
Exited,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Pty {
pub id: String,
pub title: String,
pub command: String,
pub args: Vec<String>,
pub cwd: String,
pub status: PtyStatus,
pub pid: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct PtyCreatedProps {
pub info: Pty,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct PtyUpdatedProps {
pub info: Pty,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct PtyExitedProps {
pub id: String,
#[serde(rename = "exitCode")]
pub exit_code: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct PtyDeletedProps {
pub id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct WorktreeReadyProps {
pub name: String,
pub branch: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct WorktreeFailedProps {
pub message: String,
}
pub struct EventResource<'a> {
client: &'a Opencode,
}
impl<'a> EventResource<'a> {
pub(crate) const fn new(client: &'a Opencode) -> Self {
Self { client }
}
pub async fn list(
&self,
) -> Result<crate::streaming::SseStream<EventListResponse>, crate::error::OpencodeError> {
self.client.get_stream("/event").await
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::resources::session::{UserMessage, UserMessageModel, UserMessageTime};
#[test]
fn installation_updated_round_trip() {
let event = EventListResponse::InstallationUpdated {
properties: InstallationUpdatedProps { version: "1.2.3".into() },
};
let json_str = serde_json::to_string(&event).unwrap();
assert!(json_str.contains(r#""type":"installation.updated"#));
assert!(json_str.contains(r#""version":"1.2.3"#));
let back: EventListResponse = serde_json::from_str(&json_str).unwrap();
assert_eq!(event, back);
}
#[test]
fn message_updated_round_trip() {
let msg = Message::User(Box::new(UserMessage {
id: "msg_u001".into(),
session_id: "sess_001".into(),
time: UserMessageTime { created: 1_700_000_100.0 },
agent: "coder".into(),
model: UserMessageModel { provider_id: "openai".into(), model_id: "gpt-4o".into() },
format: None,
summary: None,
system: None,
tools: None,
variant: None,
}));
let event = EventListResponse::MessageUpdated {
properties: MessageUpdatedProps { info: msg.clone() },
};
let json_str = serde_json::to_string(&event).unwrap();
assert!(json_str.contains(r#""type":"message.updated"#));
assert!(json_str.contains(r#""role":"user"#));
let back: EventListResponse = serde_json::from_str(&json_str).unwrap();
assert_eq!(event, back);
}
#[test]
fn session_error_round_trip() {
use crate::resources::shared::{SessionError as SE, UnknownErrorData};
let event = EventListResponse::SessionError {
properties: SessionErrorProps {
error: Some(SE::UnknownError {
data: UnknownErrorData { message: "something broke".into() },
}),
session_id: Some("sess_err_001".into()),
},
};
let json_str = serde_json::to_string(&event).unwrap();
assert!(json_str.contains(r#""type":"session.error"#));
assert!(json_str.contains(r#""name":"UnknownError"#));
assert!(json_str.contains("something broke"));
let back: EventListResponse = serde_json::from_str(&json_str).unwrap();
assert_eq!(event, back);
}
#[test]
fn session_error_empty_round_trip() {
let event = EventListResponse::SessionError {
properties: SessionErrorProps { error: None, session_id: None },
};
let json_str = serde_json::to_string(&event).unwrap();
assert!(!json_str.contains(r#""error""#));
assert!(!json_str.contains(r#""sessionID""#));
let back: EventListResponse = serde_json::from_str(&json_str).unwrap();
assert_eq!(event, back);
}
#[test]
fn file_watcher_updated_round_trip() {
let event = EventListResponse::FileWatcherUpdated {
properties: FileWatcherUpdatedProps {
event: FileWatcherEvent::Add,
file: "src/main.rs".into(),
},
};
let json_str = serde_json::to_string(&event).unwrap();
assert!(json_str.contains(r#""type":"file.watcher.updated"#));
assert!(json_str.contains(r#""event":"add"#));
let back: EventListResponse = serde_json::from_str(&json_str).unwrap();
assert_eq!(event, back);
let event2 = EventListResponse::FileWatcherUpdated {
properties: FileWatcherUpdatedProps {
event: FileWatcherEvent::Change,
file: "Cargo.toml".into(),
},
};
let json_str2 = serde_json::to_string(&event2).unwrap();
assert!(json_str2.contains(r#""event":"change"#));
let back2: EventListResponse = serde_json::from_str(&json_str2).unwrap();
assert_eq!(event2, back2);
let event3 = EventListResponse::FileWatcherUpdated {
properties: FileWatcherUpdatedProps {
event: FileWatcherEvent::Unlink,
file: "old_file.rs".into(),
},
};
let json_str3 = serde_json::to_string(&event3).unwrap();
assert!(json_str3.contains(r#""event":"unlink"#));
let back3: EventListResponse = serde_json::from_str(&json_str3).unwrap();
assert_eq!(event3, back3);
}
#[test]
fn deserialize_permission_asked() {
let raw = r#"{
"type": "permission.asked",
"properties": {
"id": "perm_001",
"sessionID": "sess_001",
"title": "Run bash command"
}
}"#;
let event: EventListResponse = serde_json::from_str(raw).unwrap();
match &event {
EventListResponse::PermissionAsked { properties } => {
assert_eq!(properties["id"], "perm_001");
assert_eq!(properties["sessionID"], "sess_001");
}
other => panic!("expected PermissionAsked, got {other:?}"),
}
let json_str = serde_json::to_string(&event).unwrap();
let back: EventListResponse = serde_json::from_str(&json_str).unwrap();
assert_eq!(event, back);
}
#[test]
fn deserialize_permission_replied() {
let raw = r#"{
"type": "permission.replied",
"properties": {
"sessionID": "sess_001",
"requestID": "req_001",
"reply": "always"
}
}"#;
let event: EventListResponse = serde_json::from_str(raw).unwrap();
match &event {
EventListResponse::PermissionReplied { properties } => {
assert_eq!(properties.session_id, "sess_001");
assert_eq!(properties.request_id, "req_001");
assert_eq!(properties.reply, PermissionReply::Always);
}
other => panic!("expected PermissionReplied, got {other:?}"),
}
}
#[test]
fn lsp_client_diagnostics_round_trip() {
let event = EventListResponse::LspClientDiagnostics {
properties: LspClientDiagnosticsProps {
path: "src/main.rs".into(),
server_id: "rust-analyzer".into(),
},
};
let json_str = serde_json::to_string(&event).unwrap();
assert!(json_str.contains(r#""type":"lsp.client.diagnostics"#));
assert!(json_str.contains(r#""serverID":"rust-analyzer"#));
let back: EventListResponse = serde_json::from_str(&json_str).unwrap();
assert_eq!(event, back);
}
#[test]
fn message_removed_round_trip() {
let event = EventListResponse::MessageRemoved {
properties: MessageRemovedProps {
message_id: "msg_del_001".into(),
session_id: "sess_001".into(),
},
};
let json_str = serde_json::to_string(&event).unwrap();
assert!(json_str.contains(r#""type":"message.removed"#));
assert!(json_str.contains("messageID"));
assert!(json_str.contains("sessionID"));
let back: EventListResponse = serde_json::from_str(&json_str).unwrap();
assert_eq!(event, back);
}
#[test]
fn message_part_updated_round_trip() {
use crate::resources::session::{Part, TextPart};
let event = EventListResponse::MessagePartUpdated {
properties: MessagePartUpdatedProps {
part: Part::Text(TextPart {
id: "p_upd_001".into(),
message_id: "msg_001".into(),
session_id: "sess_001".into(),
text: "updated text".into(),
synthetic: None,
time: None,
}),
},
};
let json_str = serde_json::to_string(&event).unwrap();
assert!(json_str.contains(r#""type":"message.part.updated"#));
let back: EventListResponse = serde_json::from_str(&json_str).unwrap();
assert_eq!(event, back);
}
#[test]
fn message_part_removed_round_trip() {
let event = EventListResponse::MessagePartRemoved {
properties: MessagePartRemovedProps {
session_id: "sess_001".into(),
message_id: "msg_001".into(),
part_id: "p_del_001".into(),
},
};
let json_str = serde_json::to_string(&event).unwrap();
assert!(json_str.contains(r#""type":"message.part.removed"#));
assert!(json_str.contains("sessionID"));
assert!(json_str.contains("messageID"));
assert!(json_str.contains("partID"));
let back: EventListResponse = serde_json::from_str(&json_str).unwrap();
assert_eq!(event, back);
}
#[test]
fn file_edited_round_trip() {
let event = EventListResponse::FileEdited {
properties: FileEditedProps { file: "src/lib.rs".into() },
};
let json_str = serde_json::to_string(&event).unwrap();
assert!(json_str.contains(r#""type":"file.edited"#));
let back: EventListResponse = serde_json::from_str(&json_str).unwrap();
assert_eq!(event, back);
}
#[test]
fn session_updated_round_trip() {
let event = EventListResponse::SessionUpdated {
properties: SessionUpdatedProps {
info: Session {
id: "sess_upd".into(),
slug: String::new(),
project_id: String::new(),
directory: String::new(),
time: crate::resources::session::SessionTime {
created: 1_700_000_000.0,
updated: 1_700_001_000.0,
compacting: None,
archived: None,
},
title: "Updated".into(),
version: "1".into(),
parent_id: None,
revert: None,
share: None,
summary: None,
permission: None,
},
},
};
let json_str = serde_json::to_string(&event).unwrap();
assert!(json_str.contains(r#""type":"session.updated"#));
let back: EventListResponse = serde_json::from_str(&json_str).unwrap();
assert_eq!(event, back);
}
#[test]
fn session_deleted_round_trip() {
let event = EventListResponse::SessionDeleted {
properties: SessionDeletedProps {
info: Session {
id: "sess_del".into(),
slug: String::new(),
project_id: String::new(),
directory: String::new(),
time: crate::resources::session::SessionTime {
created: 1_700_000_000.0,
updated: 1_700_000_000.0,
compacting: None,
archived: None,
},
title: "Deleted".into(),
version: "1".into(),
parent_id: None,
revert: None,
share: None,
summary: None,
permission: None,
},
},
};
let json_str = serde_json::to_string(&event).unwrap();
assert!(json_str.contains(r#""type":"session.deleted"#));
let back: EventListResponse = serde_json::from_str(&json_str).unwrap();
assert_eq!(event, back);
}
#[test]
fn session_idle_round_trip() {
let event = EventListResponse::SessionIdle {
properties: SessionIdleProps { session_id: "sess_idle_001".into() },
};
let json_str = serde_json::to_string(&event).unwrap();
assert!(json_str.contains(r#""type":"session.idle"#));
assert!(json_str.contains("sessionID"));
let back: EventListResponse = serde_json::from_str(&json_str).unwrap();
assert_eq!(event, back);
}
#[test]
fn session_error_both_fields_null() {
let raw = r#"{
"type": "session.error",
"properties": { "error": null, "sessionID": null }
}"#;
let event: EventListResponse = serde_json::from_str(raw).unwrap();
match &event {
EventListResponse::SessionError { properties } => {
assert_eq!(properties.error, None);
assert_eq!(properties.session_id, None);
}
other => panic!("expected SessionError, got {other:?}"),
}
}
#[test]
fn installation_update_available_round_trip() {
let event = EventListResponse::InstallationUpdateAvailable {
properties: InstallationUpdateAvailableProps { version: "2.0.0".into() },
};
let json_str = serde_json::to_string(&event).unwrap();
assert!(json_str.contains(r#""type":"installation.update-available"#));
assert!(json_str.contains(r#""version":"2.0.0"#));
let back: EventListResponse = serde_json::from_str(&json_str).unwrap();
assert_eq!(event, back);
}
#[test]
fn message_part_delta_round_trip() {
let event = EventListResponse::MessagePartDelta {
properties: MessagePartDeltaProps {
session_id: "sess_001".into(),
message_id: "msg_001".into(),
part_id: "part_001".into(),
field: "text".into(),
delta: "hello ".into(),
},
};
let json_str = serde_json::to_string(&event).unwrap();
assert!(json_str.contains(r#""type":"message.part.delta"#));
assert!(json_str.contains(r#""sessionID":"sess_001"#));
assert!(json_str.contains(r#""delta":"hello "#));
let back: EventListResponse = serde_json::from_str(&json_str).unwrap();
assert_eq!(event, back);
}
#[test]
fn server_connected_round_trip() {
let event = EventListResponse::ServerConnected { properties: EmptyProps {} };
let json_str = serde_json::to_string(&event).unwrap();
assert!(json_str.contains(r#""type":"server.connected"#));
let back: EventListResponse = serde_json::from_str(&json_str).unwrap();
assert_eq!(event, back);
}
#[test]
fn tui_toast_show_round_trip() {
let event = EventListResponse::TuiToastShow {
properties: TuiToastShowProps {
title: Some("Heads up".into()),
message: "Build succeeded".into(),
variant: ToastVariant::Success,
duration: Some(5.0),
},
};
let json_str = serde_json::to_string(&event).unwrap();
assert!(json_str.contains(r#""type":"tui.toast.show"#));
assert!(json_str.contains(r#""variant":"success"#));
assert!(json_str.contains(r#""title":"Heads up"#));
let back: EventListResponse = serde_json::from_str(&json_str).unwrap();
assert_eq!(event, back);
let event2 = EventListResponse::TuiToastShow {
properties: TuiToastShowProps {
title: None,
message: "Error occurred".into(),
variant: ToastVariant::Error,
duration: None,
},
};
let json_str2 = serde_json::to_string(&event2).unwrap();
assert!(!json_str2.contains(r#""title""#));
assert!(!json_str2.contains(r#""duration""#));
let back2: EventListResponse = serde_json::from_str(&json_str2).unwrap();
assert_eq!(event2, back2);
}
#[test]
fn todo_updated_round_trip() {
let event = EventListResponse::TodoUpdated {
properties: TodoUpdatedProps {
session_id: "sess_001".into(),
todos: vec![
Todo {
content: "Fix bug".into(),
status: "pending".into(),
priority: "high".into(),
},
Todo {
content: "Write docs".into(),
status: "done".into(),
priority: "low".into(),
},
],
},
};
let json_str = serde_json::to_string(&event).unwrap();
assert!(json_str.contains(r#""type":"todo.updated"#));
assert!(json_str.contains("Fix bug"));
let back: EventListResponse = serde_json::from_str(&json_str).unwrap();
assert_eq!(event, back);
}
#[test]
fn worktree_ready_round_trip() {
let event = EventListResponse::WorktreeReady {
properties: WorktreeReadyProps {
name: "feature-branch".into(),
branch: "feat/new-feature".into(),
},
};
let json_str = serde_json::to_string(&event).unwrap();
assert!(json_str.contains(r#""type":"worktree.ready"#));
assert!(json_str.contains(r#""name":"feature-branch"#));
let back: EventListResponse = serde_json::from_str(&json_str).unwrap();
assert_eq!(event, back);
}
#[test]
fn question_replied_round_trip() {
let event = EventListResponse::QuestionReplied {
properties: QuestionRepliedProps {
session_id: "sess_001".into(),
request_id: "req_001".into(),
answers: vec![vec!["yes".into(), "no".into()]],
},
};
let json_str = serde_json::to_string(&event).unwrap();
assert!(json_str.contains(r#""type":"question.replied"#));
assert!(json_str.contains(r#""sessionID":"sess_001"#));
let back: EventListResponse = serde_json::from_str(&json_str).unwrap();
assert_eq!(event, back);
}
#[test]
fn mcp_tools_changed_round_trip() {
let event = EventListResponse::McpToolsChanged {
properties: McpToolsChangedProps { server: "my-mcp-server".into() },
};
let json_str = serde_json::to_string(&event).unwrap();
assert!(json_str.contains(r#""type":"mcp.tools.changed"#));
assert!(json_str.contains(r#""server":"my-mcp-server"#));
let back: EventListResponse = serde_json::from_str(&json_str).unwrap();
assert_eq!(event, back);
}
#[test]
fn pty_created_round_trip() {
let event = EventListResponse::PtyCreated {
properties: PtyCreatedProps {
info: Pty {
id: "pty_001".into(),
title: "shell".into(),
command: "/bin/zsh".into(),
args: vec!["-l".into()],
cwd: "/home/user".into(),
status: PtyStatus::Running,
pid: 12345.0,
},
},
};
let json_str = serde_json::to_string(&event).unwrap();
assert!(json_str.contains(r#""type":"pty.created"#));
assert!(json_str.contains(r#""status":"running"#));
let back: EventListResponse = serde_json::from_str(&json_str).unwrap();
assert_eq!(event, back);
}
#[test]
fn vcs_branch_updated_round_trip() {
let event = EventListResponse::VcsBranchUpdated {
properties: VcsBranchUpdatedProps { branch: Some("main".into()) },
};
let json_str = serde_json::to_string(&event).unwrap();
assert!(json_str.contains(r#""type":"vcs.branch.updated"#));
assert!(json_str.contains(r#""branch":"main"#));
let back: EventListResponse = serde_json::from_str(&json_str).unwrap();
assert_eq!(event, back);
let event2 = EventListResponse::VcsBranchUpdated {
properties: VcsBranchUpdatedProps { branch: None },
};
let json_str2 = serde_json::to_string(&event2).unwrap();
assert!(!json_str2.contains(r#""branch""#));
let back2: EventListResponse = serde_json::from_str(&json_str2).unwrap();
assert_eq!(event2, back2);
}
#[test]
fn command_executed_round_trip() {
let event = EventListResponse::CommandExecuted {
properties: CommandExecutedProps {
name: "test-cmd".into(),
session_id: "sess_001".into(),
arguments: "{}".into(),
message_id: "msg_001".into(),
},
};
let json_str = serde_json::to_string(&event).unwrap();
assert!(json_str.contains(r#""type":"command.executed"#));
assert!(json_str.contains(r#""sessionID":"sess_001"#));
assert!(json_str.contains(r#""messageID":"msg_001"#));
let back: EventListResponse = serde_json::from_str(&json_str).unwrap();
assert_eq!(event, back);
}
#[test]
fn deserialize_project_updated_from_raw() {
let raw = r#"{
"type": "project.updated",
"properties": {
"properties": { "name": "my-project", "path": "/tmp/proj" }
}
}"#;
let event: EventListResponse = serde_json::from_str(raw).unwrap();
match &event {
EventListResponse::ProjectUpdated { properties } => {
assert_eq!(properties.properties["name"], "my-project");
}
other => panic!("expected ProjectUpdated, got {other:?}"),
}
}
#[test]
fn deserialize_session_status_from_raw() {
let raw = r#"{
"type": "session.status",
"properties": {
"sessionID": "sess_001",
"status": { "type": "running", "tool": "bash" }
}
}"#;
let event: EventListResponse = serde_json::from_str(raw).unwrap();
match &event {
EventListResponse::SessionStatus { properties } => {
assert_eq!(properties.session_id, "sess_001");
assert_eq!(properties.status["type"], "running");
}
other => panic!("expected SessionStatus, got {other:?}"),
}
}
}