first pass of merging in warp (doesn't build)
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
use super::event::{parse_event, CLIAgentEvent, CLIAgentEventPayload, CLIAgentEventType};
|
||||
use super::event::{
|
||||
parse_event, CLIAgentEvent, CLIAgentEventPayload, CLIAgentEventSource, CLIAgentEventType,
|
||||
};
|
||||
use super::{
|
||||
CLIAgentInputEntrypoint, CLIAgentInputState, CLIAgentSession, CLIAgentSessionContext,
|
||||
CLIAgentSessionStatus, CLIAgentSessionsModel,
|
||||
@@ -222,6 +224,34 @@ fn parse_auggie_stop_notification() {
|
||||
assert_eq!(notif.payload.response.as_deref(), Some("Memory is safe"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_pi_stop_notification() {
|
||||
// Mirrors what the community pi-mono plugin emits on the Stop hook —
|
||||
// matches the Auggie shape and uses `"agent":"pi"`, which `resolve_agent`
|
||||
// already maps to `CLIAgent::Pi` via `command_prefix()`.
|
||||
let body = r#"{"v":1,"agent":"pi","event":"stop","session_id":"abc","cwd":"/tmp/proj","project":"proj","query":"write a haiku","response":"Memory is safe"}"#;
|
||||
let notif = parse_event(Some("warp://cli-agent"), body).unwrap();
|
||||
|
||||
assert_eq!(notif.agent, CLIAgent::Pi);
|
||||
assert_eq!(notif.event, CLIAgentEventType::Stop);
|
||||
assert_eq!(notif.payload.query.as_deref(), Some("write a haiku"));
|
||||
assert_eq!(notif.payload.response.as_deref(), Some("Memory is safe"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_droid_stop_notification() {
|
||||
// Droid is already a known CLI agent, so structured OSC 777 events using
|
||||
// `"agent":"droid"` should resolve through the existing command prefix
|
||||
// parser without any Droid-specific parser logic.
|
||||
let body = r#"{"v":1,"agent":"droid","event":"stop","session_id":"abc","cwd":"/tmp/proj","project":"proj","query":"write a haiku","response":"Memory is safe"}"#;
|
||||
let notif = parse_event(Some("warp://cli-agent"), body).unwrap();
|
||||
|
||||
assert_eq!(notif.agent, CLIAgent::Droid);
|
||||
assert_eq!(notif.event, CLIAgentEventType::Stop);
|
||||
assert_eq!(notif.payload.query.as_deref(), Some("write a haiku"));
|
||||
assert_eq!(notif.payload.response.as_deref(), Some("Memory is safe"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apply_event_preserves_input_session() {
|
||||
let input_state = CLIAgentInputState::Open {
|
||||
@@ -243,9 +273,11 @@ fn apply_event_preserves_input_session() {
|
||||
plugin_version: None,
|
||||
draft_text: None,
|
||||
custom_command_prefix: None,
|
||||
received_rich_notification: false,
|
||||
};
|
||||
|
||||
let event = CLIAgentEvent {
|
||||
source: CLIAgentEventSource::RichPlugin,
|
||||
v: 1,
|
||||
agent: CLIAgent::Claude,
|
||||
event: CLIAgentEventType::PermissionRequest,
|
||||
@@ -276,6 +308,7 @@ fn is_remote_returns_true_when_remote_host_is_set() {
|
||||
draft_text: None,
|
||||
remote_host: Some("user@devbox".to_owned()),
|
||||
custom_command_prefix: None,
|
||||
received_rich_notification: false,
|
||||
};
|
||||
assert!(session.is_remote());
|
||||
}
|
||||
@@ -293,6 +326,7 @@ fn is_remote_returns_false_when_remote_host_is_none() {
|
||||
plugin_version: None,
|
||||
draft_text: None,
|
||||
custom_command_prefix: None,
|
||||
received_rich_notification: false,
|
||||
};
|
||||
assert!(!session.is_remote());
|
||||
}
|
||||
@@ -361,9 +395,11 @@ fn session_start_sets_plugin_version() {
|
||||
draft_text: None,
|
||||
remote_host: None,
|
||||
custom_command_prefix: None,
|
||||
received_rich_notification: false,
|
||||
};
|
||||
|
||||
let event = CLIAgentEvent {
|
||||
source: CLIAgentEventSource::RichPlugin,
|
||||
v: 1,
|
||||
agent: CLIAgent::Claude,
|
||||
event: CLIAgentEventType::SessionStart,
|
||||
@@ -393,9 +429,11 @@ fn session_start_without_plugin_version_leaves_none() {
|
||||
draft_text: None,
|
||||
remote_host: None,
|
||||
custom_command_prefix: None,
|
||||
received_rich_notification: false,
|
||||
};
|
||||
|
||||
let event = CLIAgentEvent {
|
||||
source: CLIAgentEventSource::RichPlugin,
|
||||
v: 1,
|
||||
agent: CLIAgent::Claude,
|
||||
event: CLIAgentEventType::SessionStart,
|
||||
@@ -408,3 +446,254 @@ fn session_start_without_plugin_version_leaves_none() {
|
||||
session.apply_event(&event);
|
||||
assert_eq!(session.plugin_version, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn codex_session_not_rich_until_rich_notification() {
|
||||
// Codex's OSC 9 fallback never sets `received_rich_notification`, so the
|
||||
// session must not claim rich status even when a fallback listener exists.
|
||||
let mut session = CLIAgentSession {
|
||||
agent: CLIAgent::Codex,
|
||||
status: CLIAgentSessionStatus::InProgress,
|
||||
session_context: CLIAgentSessionContext::default(),
|
||||
input_state: CLIAgentInputState::Closed,
|
||||
should_auto_toggle_input: false,
|
||||
listener: None,
|
||||
plugin_version: None,
|
||||
remote_host: None,
|
||||
draft_text: None,
|
||||
custom_command_prefix: None,
|
||||
received_rich_notification: false,
|
||||
};
|
||||
assert!(!session.supports_rich_status());
|
||||
|
||||
// A structured OSC 777 notification latches the flag -> rich status.
|
||||
session.received_rich_notification = true;
|
||||
assert!(session.supports_rich_status());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_codex_session_rich_after_rich_notification() {
|
||||
let mut session = CLIAgentSession {
|
||||
agent: CLIAgent::Claude,
|
||||
status: CLIAgentSessionStatus::InProgress,
|
||||
session_context: CLIAgentSessionContext::default(),
|
||||
input_state: CLIAgentInputState::Closed,
|
||||
should_auto_toggle_input: false,
|
||||
listener: None,
|
||||
plugin_version: None,
|
||||
remote_host: None,
|
||||
draft_text: None,
|
||||
custom_command_prefix: None,
|
||||
received_rich_notification: false,
|
||||
};
|
||||
// No listener and no rich notification yet.
|
||||
assert!(!session.supports_rich_status());
|
||||
|
||||
session.received_rich_notification = true;
|
||||
assert!(session.supports_rich_status());
|
||||
}
|
||||
|
||||
/// Constructs a session with permission-scoped state already populated, as if
|
||||
/// a `PermissionRequest` had just been received and the agent is now Blocked.
|
||||
/// Used by the GH-9525 regression tests below.
|
||||
fn blocked_claude_session_with_permission_state() -> CLIAgentSession {
|
||||
CLIAgentSession {
|
||||
agent: CLIAgent::Claude,
|
||||
status: CLIAgentSessionStatus::Blocked {
|
||||
message: Some("Wants to run bash: rm -rf /tmp".to_owned()),
|
||||
},
|
||||
session_context: CLIAgentSessionContext {
|
||||
summary: Some("Wants to run bash: rm -rf /tmp".to_owned()),
|
||||
tool_name: Some("Bash".to_owned()),
|
||||
tool_input_preview: Some("rm -rf /tmp".to_owned()),
|
||||
..Default::default()
|
||||
},
|
||||
input_state: CLIAgentInputState::Closed,
|
||||
should_auto_toggle_input: false,
|
||||
listener: None,
|
||||
plugin_version: None,
|
||||
draft_text: None,
|
||||
remote_host: None,
|
||||
custom_command_prefix: None,
|
||||
received_rich_notification: false,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stop_clears_permission_scoped_state() {
|
||||
// GH-9525: after a PermissionRequest sets `summary`, the Stop event must
|
||||
// clear it. Otherwise the tab title falls back to the stale permission
|
||||
// text instead of reflecting the now-completed session.
|
||||
let mut session = blocked_claude_session_with_permission_state();
|
||||
|
||||
let event = CLIAgentEvent {
|
||||
source: CLIAgentEventSource::RichPlugin,
|
||||
v: 1,
|
||||
agent: CLIAgent::Claude,
|
||||
event: CLIAgentEventType::Stop,
|
||||
session_id: Some("abc".to_owned()),
|
||||
cwd: None,
|
||||
project: None,
|
||||
payload: CLIAgentEventPayload {
|
||||
query: Some("write a haiku".to_owned()),
|
||||
response: Some("Memory is safe".to_owned()),
|
||||
..Default::default()
|
||||
},
|
||||
};
|
||||
|
||||
session.apply_event(&event);
|
||||
|
||||
assert_eq!(session.session_context.summary, None);
|
||||
assert_eq!(session.session_context.tool_name, None);
|
||||
assert_eq!(session.session_context.tool_input_preview, None);
|
||||
assert_eq!(
|
||||
session.session_context.query.as_deref(),
|
||||
Some("write a haiku"),
|
||||
);
|
||||
assert_eq!(
|
||||
session.session_context.response.as_deref(),
|
||||
Some("Memory is safe"),
|
||||
);
|
||||
assert!(matches!(session.status, CLIAgentSessionStatus::Success));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn permission_replied_clears_permission_scoped_state() {
|
||||
// When the user replies to a permission prompt the agent transitions back
|
||||
// to InProgress; the now-stale summary/tool fields must be cleared so they
|
||||
// don't leak into UI surfaces during the next turn.
|
||||
let mut session = blocked_claude_session_with_permission_state();
|
||||
|
||||
let event = CLIAgentEvent {
|
||||
source: CLIAgentEventSource::RichPlugin,
|
||||
v: 1,
|
||||
agent: CLIAgent::Claude,
|
||||
event: CLIAgentEventType::PermissionReplied,
|
||||
session_id: Some("abc".to_owned()),
|
||||
cwd: None,
|
||||
project: None,
|
||||
payload: CLIAgentEventPayload::default(),
|
||||
};
|
||||
|
||||
session.apply_event(&event);
|
||||
|
||||
assert_eq!(session.session_context.summary, None);
|
||||
assert_eq!(session.session_context.tool_name, None);
|
||||
assert_eq!(session.session_context.tool_input_preview, None);
|
||||
assert!(matches!(session.status, CLIAgentSessionStatus::InProgress));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prompt_submit_clears_permission_scoped_state() {
|
||||
// PromptSubmit already clears `response`; clearing the permission-scoped
|
||||
// fields keeps the same hygiene if the user manages to start a new turn
|
||||
// while permission state is still populated (e.g. an abandoned permission
|
||||
// flow that was not closed by an explicit PermissionReplied).
|
||||
let mut session = blocked_claude_session_with_permission_state();
|
||||
session.session_context.response = Some("stale response".to_owned());
|
||||
|
||||
let event = CLIAgentEvent {
|
||||
source: CLIAgentEventSource::RichPlugin,
|
||||
v: 1,
|
||||
agent: CLIAgent::Claude,
|
||||
event: CLIAgentEventType::PromptSubmit,
|
||||
session_id: Some("abc".to_owned()),
|
||||
cwd: None,
|
||||
project: None,
|
||||
payload: CLIAgentEventPayload {
|
||||
query: Some("next prompt".to_owned()),
|
||||
..Default::default()
|
||||
},
|
||||
};
|
||||
|
||||
session.apply_event(&event);
|
||||
|
||||
assert_eq!(session.session_context.summary, None);
|
||||
assert_eq!(session.session_context.tool_name, None);
|
||||
assert_eq!(session.session_context.tool_input_preview, None);
|
||||
assert_eq!(session.session_context.response, None);
|
||||
assert_eq!(
|
||||
session.session_context.query.as_deref(),
|
||||
Some("next prompt")
|
||||
);
|
||||
assert!(matches!(session.status, CLIAgentSessionStatus::InProgress));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_complete_clears_permission_scoped_state() {
|
||||
// GH-11082: answering an AskUserQuestion emits only ToolComplete (the
|
||||
// plugin sends no PermissionReplied for it), so the Blocked -> InProgress
|
||||
// transition here must also clear the stale summary. Otherwise the tab
|
||||
// title keeps showing "Wants to run AskUserQuestion: ..." until the next
|
||||
// prompt or Stop.
|
||||
let mut session = blocked_claude_session_with_permission_state();
|
||||
|
||||
let event = CLIAgentEvent {
|
||||
source: CLIAgentEventSource::RichPlugin,
|
||||
v: 1,
|
||||
agent: CLIAgent::Claude,
|
||||
event: CLIAgentEventType::ToolComplete,
|
||||
session_id: Some("abc".to_owned()),
|
||||
cwd: None,
|
||||
project: None,
|
||||
payload: CLIAgentEventPayload::default(),
|
||||
};
|
||||
|
||||
session.apply_event(&event);
|
||||
|
||||
assert_eq!(session.session_context.summary, None);
|
||||
assert_eq!(session.session_context.tool_name, None);
|
||||
assert_eq!(session.session_context.tool_input_preview, None);
|
||||
assert!(matches!(session.status, CLIAgentSessionStatus::InProgress));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn permission_request_still_populates_summary_and_tool_fields() {
|
||||
// Sanity: clearing permission-scoped state on Stop/Reply/Submit must not
|
||||
// also break the PermissionRequest path that initially populates them.
|
||||
let mut session = CLIAgentSession {
|
||||
agent: CLIAgent::Claude,
|
||||
status: CLIAgentSessionStatus::InProgress,
|
||||
session_context: CLIAgentSessionContext::default(),
|
||||
input_state: CLIAgentInputState::Closed,
|
||||
should_auto_toggle_input: false,
|
||||
listener: None,
|
||||
plugin_version: None,
|
||||
draft_text: None,
|
||||
remote_host: None,
|
||||
custom_command_prefix: None,
|
||||
received_rich_notification: false,
|
||||
};
|
||||
|
||||
let event = CLIAgentEvent {
|
||||
source: CLIAgentEventSource::RichPlugin,
|
||||
v: 1,
|
||||
agent: CLIAgent::Claude,
|
||||
event: CLIAgentEventType::PermissionRequest,
|
||||
session_id: Some("abc".to_owned()),
|
||||
cwd: None,
|
||||
project: None,
|
||||
payload: CLIAgentEventPayload {
|
||||
summary: Some("Wants to run bash: rm -rf /tmp".to_owned()),
|
||||
tool_name: Some("Bash".to_owned()),
|
||||
tool_input_preview: Some("rm -rf /tmp".to_owned()),
|
||||
..Default::default()
|
||||
},
|
||||
};
|
||||
|
||||
session.apply_event(&event);
|
||||
|
||||
assert_eq!(
|
||||
session.session_context.summary.as_deref(),
|
||||
Some("Wants to run bash: rm -rf /tmp"),
|
||||
);
|
||||
assert_eq!(session.session_context.tool_name.as_deref(), Some("Bash"));
|
||||
assert_eq!(
|
||||
session.session_context.tool_input_preview.as_deref(),
|
||||
Some("rm -rf /tmp"),
|
||||
);
|
||||
assert!(matches!(
|
||||
session.status,
|
||||
CLIAgentSessionStatus::Blocked { .. },
|
||||
));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user