diff --git a/agent-sessions/src/providers/claude/mod.rs b/agent-sessions/src/providers/claude/mod.rs index 0fa3b24..0b73a8a 100644 --- a/agent-sessions/src/providers/claude/mod.rs +++ b/agent-sessions/src/providers/claude/mod.rs @@ -72,7 +72,7 @@ impl Claude { } if cwd.is_none() { - if let Some(value) = raw.cwd { + if let Some(value) = raw.cwd.as_deref() { cwd = Some(box_str(value)); if want_title { if let Some(found) = extract_user_message_title(&raw) { @@ -311,7 +311,7 @@ where .and_then(|message| opt_box_str(message.id)), role: map_role(raw.kind.unwrap_or("unknown")), session_id: opt_box_str(raw.session_id), - cwd: opt_box_str(raw.cwd), + cwd: opt_cow_to_box(raw.cwd.clone()), model: raw .message .as_ref() @@ -356,7 +356,7 @@ where .and_then(|value| opt_box_str(value.attachment_type)), name: attachment .as_ref() - .and_then(|value| opt_box_str(value.name)), + .and_then(|value| opt_cow_to_box(value.name.clone())), species: attachment .as_ref() .and_then(|value| opt_box_str(value.species)), @@ -390,7 +390,7 @@ where if selection.includes_state() { entries.push(Entry::LastPrompt(LastPromptEntry { session_id: opt_box_str(raw.session_id), - last_prompt: opt_box_str(raw.last_prompt), + last_prompt: raw.last_prompt.map(cow_to_box), })); } } @@ -405,19 +405,20 @@ where Some("hook_progress") => { entries.push(Entry::Progress(ProgressEntry::HookProgress { session_id: opt_box_str(raw.session_id), - cwd: opt_box_str(raw.cwd), + cwd: opt_cow_to_box(raw.cwd.clone()), timestamp: opt_timestamp_box_str(raw.timestamp.as_ref()), parent_tool_use_id: opt_box_str(raw.parent_tool_use_id), tool_use_id: opt_box_str(raw.tool_use_id), hook_event: progress.and_then(|value| opt_box_str(value.hook_event)), hook_name: progress.and_then(|value| opt_box_str(value.hook_name)), - command: progress.and_then(|value| opt_box_str(value.command)), + command: progress + .and_then(|value| opt_cow_to_box(value.command.clone())), })) } Some("bash_progress") => { entries.push(Entry::Progress(ProgressEntry::BashProgress { session_id: opt_box_str(raw.session_id), - cwd: opt_box_str(raw.cwd), + cwd: opt_cow_to_box(raw.cwd.clone()), timestamp: opt_timestamp_box_str(raw.timestamp.as_ref()), parent_tool_use_id: opt_box_str(raw.parent_tool_use_id), tool_use_id: opt_box_str(raw.tool_use_id), @@ -432,7 +433,7 @@ where Some("agent_progress") => { entries.push(Entry::Progress(ProgressEntry::AgentProgress { session_id: opt_box_str(raw.session_id), - cwd: opt_box_str(raw.cwd), + cwd: opt_cow_to_box(raw.cwd.clone()), timestamp: opt_timestamp_box_str(raw.timestamp.as_ref()), parent_tool_use_id: opt_box_str(raw.parent_tool_use_id), tool_use_id: opt_box_str(raw.tool_use_id), @@ -444,7 +445,7 @@ where Some("query_update") => { entries.push(Entry::Progress(ProgressEntry::QueryUpdate { session_id: opt_box_str(raw.session_id), - cwd: opt_box_str(raw.cwd), + cwd: opt_cow_to_box(raw.cwd.clone()), timestamp: opt_timestamp_box_str(raw.timestamp.as_ref()), parent_tool_use_id: opt_box_str(raw.parent_tool_use_id), tool_use_id: opt_box_str(raw.tool_use_id), @@ -454,7 +455,7 @@ where Some("search_results_received") => { entries.push(Entry::Progress(ProgressEntry::SearchResultsReceived { session_id: opt_box_str(raw.session_id), - cwd: opt_box_str(raw.cwd), + cwd: opt_cow_to_box(raw.cwd.clone()), timestamp: opt_timestamp_box_str(raw.timestamp.as_ref()), parent_tool_use_id: opt_box_str(raw.parent_tool_use_id), tool_use_id: opt_box_str(raw.tool_use_id), @@ -465,7 +466,7 @@ where Some("mcp_progress") => { entries.push(Entry::Progress(ProgressEntry::McpProgress { session_id: opt_box_str(raw.session_id), - cwd: opt_box_str(raw.cwd), + cwd: opt_cow_to_box(raw.cwd.clone()), timestamp: opt_timestamp_box_str(raw.timestamp.as_ref()), parent_tool_use_id: opt_box_str(raw.parent_tool_use_id), tool_use_id: opt_box_str(raw.tool_use_id), @@ -477,19 +478,19 @@ where Some("waiting_for_task") => { entries.push(Entry::Progress(ProgressEntry::WaitingForTask { session_id: opt_box_str(raw.session_id), - cwd: opt_box_str(raw.cwd), + cwd: opt_cow_to_box(raw.cwd.clone()), timestamp: opt_timestamp_box_str(raw.timestamp.as_ref()), parent_tool_use_id: opt_box_str(raw.parent_tool_use_id), tool_use_id: opt_box_str(raw.tool_use_id), task_description: progress - .and_then(|value| opt_box_str(value.task_description)), + .and_then(|value| opt_cow_to_box(value.task_description.clone())), task_type: progress.and_then(|value| opt_box_str(value.task_type)), })) } _ => entries.push(Entry::Progress(ProgressEntry::Other { kind: progress_kind.map(box_str), session_id: opt_box_str(raw.session_id), - cwd: opt_box_str(raw.cwd), + cwd: opt_cow_to_box(raw.cwd.clone()), timestamp: opt_timestamp_box_str(raw.timestamp.as_ref()), parent_tool_use_id: opt_box_str(raw.parent_tool_use_id), tool_use_id: opt_box_str(raw.tool_use_id), @@ -526,7 +527,7 @@ where entries.push(Entry::InputSnapshot(InputSnapshotEntry { display: raw.display.map(cow_to_box), pasted_contents_json: opt_raw_json_box(raw.pasted_contents), - project: opt_box_str(raw.project), + project: raw.project.map(cow_to_box), session_id: opt_box_str(raw.session_id), timestamp_millis: opt_timestamp_millis(raw.timestamp.as_ref()), })); @@ -592,7 +593,7 @@ where }; } if cwd.is_none() && raw.cwd.is_some() { - cwd = opt_box_str(raw.cwd); + cwd = opt_cow_to_box(raw.cwd.clone()); if timestamp.is_none() { timestamp = opt_timestamp_box_str(raw.timestamp.as_ref()); } @@ -715,8 +716,8 @@ struct RawEntry<'a> { kind: Option<&'a str>, #[serde(default, alias = "sessionId")] session_id: Option<&'a str>, - #[serde(default)] - cwd: Option<&'a str>, + #[serde(default, borrow)] + cwd: Option>, #[serde(default, alias = "isMeta")] is_meta: Option, #[serde(default, deserialize_with = "deserialize_opt_timestamp")] @@ -738,7 +739,7 @@ struct RawEntry<'a> { #[serde(default)] snapshot: Option>, #[serde(default, alias = "lastPrompt")] - last_prompt: Option<&'a str>, + last_prompt: Option>, #[serde(default)] operation: Option<&'a str>, #[serde(default, borrow)] @@ -748,7 +749,7 @@ struct RawEntry<'a> { #[serde(default, alias = "pastedContents", borrow)] pasted_contents: Option<&'a RawValue>, #[serde(default)] - project: Option<&'a str>, + project: Option>, #[serde(default)] subtype: Option<&'a str>, #[serde(default)] @@ -981,7 +982,7 @@ struct AttachmentPayload<'a> { #[serde(default)] attachment_type: Option<&'a str>, #[serde(default)] - name: Option<&'a str>, + name: Option>, #[serde(default)] species: Option<&'a str>, } @@ -1003,7 +1004,7 @@ struct ProgressPayload<'a> { #[serde(default)] hook_name: Option<&'a str>, #[serde(default)] - command: Option<&'a str>, + command: Option>, #[serde(default)] output: Option>, #[serde(default, alias = "fullOutput")] @@ -1029,7 +1030,7 @@ struct ProgressPayload<'a> { #[serde(default, alias = "toolName")] tool_name: Option<&'a str>, #[serde(default, alias = "taskDescription")] - task_description: Option<&'a str>, + task_description: Option>, #[serde(default, alias = "taskType")] task_type: Option<&'a str>, } @@ -1094,6 +1095,69 @@ mod tests { assert_eq!(session_id.as_deref(), Some("sess-1")); } + #[test] + fn reader_accepts_escaped_windows_cwd() { + let bytes = concat!( + r#"{"type":"user","sessionId":"sess-win","cwd":"D:\\Input\\Api\\src\\InputService","timestamp":"2026-05-01T11:09:46.305Z","message":{"id":"msg-win","model":"claude-sonnet-4","content":"hello from windows"}}"#, + "\n", + ); + + let (_version, body) = super::parse_claude_reader( + Cursor::new(bytes.as_bytes()), + crate::ParseSelection::full(), + ) + .unwrap(); + + let [super::Entry::Message(message)] = body.entries.as_ref() else { + panic!("expected one message entry"); + }; + assert_eq!(message.session_id.as_deref(), Some("sess-win")); + assert_eq!( + message.cwd.as_deref(), + Some(r#"D:\Input\Api\src\InputService"#) + ); + } + + #[test] + fn reader_accepts_escaped_windows_progress_command() { + let bytes = concat!( + r#"{"type":"progress","sessionId":"sess-win","cwd":"D:\\Input","timestamp":"2026-05-01T11:09:46.305Z","data":{"type":"hook_progress","command":"cd D:\\Input\\Api\nrun tests"}}"#, + "\n", + ); + + let (_version, body) = super::parse_claude_reader( + Cursor::new(bytes.as_bytes()), + crate::ParseSelection::full(), + ) + .unwrap(); + + let [super::Entry::Progress(super::ProgressEntry::HookProgress { command, .. })] = + body.entries.as_ref() + else { + panic!("expected hook progress entry"); + }; + assert_eq!(command.as_deref(), Some("cd D:\\Input\\Api\nrun tests")); + } + + #[cfg(feature = "agent_session")] + #[test] + fn probe_session_meta_accepts_escaped_windows_cwd() { + let bytes = concat!( + r#"{"type":"user","sessionId":"sess-win","cwd":"D:\\Input\\Api\\src\\InputService","timestamp":"2026-05-01T11:09:46.305Z"}"#, + "\n", + ); + + let meta = super::Claude::probe_session_meta(Cursor::new(bytes.as_bytes())) + .unwrap() + .expect("expected session meta"); + + assert_eq!(meta.session_id.as_deref(), Some("sess-win")); + assert_eq!( + meta.cwd.as_deref(), + Some(r#"D:\Input\Api\src\InputService"#) + ); + } + #[cfg(feature = "agent_session")] #[test] fn probe_session_meta_keeps_reading_until_cwd_seen() { diff --git a/agent-sessions/src/providers/codex/mod.rs b/agent-sessions/src/providers/codex/mod.rs index 3400ddd..06503b7 100644 --- a/agent-sessions/src/providers/codex/mod.rs +++ b/agent-sessions/src/providers/codex/mod.rs @@ -496,7 +496,8 @@ fn parse_codex_response_item_entry<'a>( commit .preexisting_untracked_files .iter() - .map(|path| box_str(path)) + .cloned() + .map(cow_to_box) .collect::>() .into_boxed_slice() }) @@ -508,7 +509,8 @@ fn parse_codex_response_item_entry<'a>( commit .preexisting_untracked_dirs .iter() - .map(|path| box_str(path)) + .cloned() + .map(cow_to_box) .collect::>() .into_boxed_slice() }) @@ -755,8 +757,8 @@ fn map_event_msg_data(payload: &EventMsgPayload<'_>) -> EventMsgData { call_id: opt_box_str(payload.call_id), sender_thread_id: opt_box_str(payload.sender_thread_id), new_thread_id: opt_box_str(payload.new_thread_id), - new_agent_nickname: opt_box_str(payload.new_agent_nickname), - new_agent_role: opt_box_str(payload.new_agent_role), + new_agent_nickname: opt_cow_box_str(payload.new_agent_nickname.clone()), + new_agent_role: opt_cow_box_str(payload.new_agent_role.clone()), prompt: opt_cow_box_str(payload.prompt.clone()), model: opt_box_str(payload.model), reasoning_effort: opt_box_str(payload.reasoning_effort), @@ -767,8 +769,8 @@ fn map_event_msg_data(payload: &EventMsgPayload<'_>) -> EventMsgData { call_id: opt_box_str(payload.call_id), sender_thread_id: opt_box_str(payload.sender_thread_id), receiver_thread_id: opt_box_str(payload.receiver_thread_id), - receiver_agent_nickname: opt_box_str(payload.receiver_agent_nickname), - receiver_agent_role: opt_box_str(payload.receiver_agent_role), + receiver_agent_nickname: opt_cow_box_str(payload.receiver_agent_nickname.clone()), + receiver_agent_role: opt_cow_box_str(payload.receiver_agent_role.clone()), status_json: opt_raw_json_box(payload.status), }), "collab_agent_interaction_end" => { @@ -776,15 +778,15 @@ fn map_event_msg_data(payload: &EventMsgPayload<'_>) -> EventMsgData { call_id: opt_box_str(payload.call_id), sender_thread_id: opt_box_str(payload.sender_thread_id), receiver_thread_id: opt_box_str(payload.receiver_thread_id), - receiver_agent_nickname: opt_box_str(payload.receiver_agent_nickname), - receiver_agent_role: opt_box_str(payload.receiver_agent_role), + receiver_agent_nickname: opt_cow_box_str(payload.receiver_agent_nickname.clone()), + receiver_agent_role: opt_cow_box_str(payload.receiver_agent_role.clone()), prompt: opt_cow_box_str(payload.prompt.clone()), status_json: opt_raw_json_box(payload.status), }) } "error" => EventMsgData::Error(ErrorEventMsg { message: opt_cow_box_str(payload.message.clone()), - codex_error_info: opt_box_str(payload.codex_error_info), + codex_error_info: opt_cow_box_str(payload.codex_error_info.clone()), }), "entered_review_mode" => EventMsgData::EnteredReviewMode(EnteredReviewModeEventMsg { target_json: opt_raw_json_box(payload.target), @@ -1065,9 +1067,9 @@ struct EventMsgPayload<'a> { #[serde(default)] new_thread_id: Option<&'a str>, #[serde(default)] - new_agent_nickname: Option<&'a str>, + new_agent_nickname: Option>, #[serde(default)] - new_agent_role: Option<&'a str>, + new_agent_role: Option>, #[serde(default)] prompt: Option>, #[serde(default)] @@ -1077,11 +1079,11 @@ struct EventMsgPayload<'a> { #[serde(default)] receiver_thread_id: Option<&'a str>, #[serde(default)] - receiver_agent_nickname: Option<&'a str>, + receiver_agent_nickname: Option>, #[serde(default)] - receiver_agent_role: Option<&'a str>, + receiver_agent_role: Option>, #[serde(default)] - codex_error_info: Option<&'a str>, + codex_error_info: Option>, #[serde(default, borrow)] target: Option<&'a RawValue>, #[serde(default)] @@ -1109,9 +1111,9 @@ struct GhostCommit<'a> { #[serde(default)] parent: Option<&'a str>, #[serde(default)] - preexisting_untracked_files: Vec<&'a str>, + preexisting_untracked_files: Vec>, #[serde(default)] - preexisting_untracked_dirs: Vec<&'a str>, + preexisting_untracked_dirs: Vec>, } #[derive(Deserialize)] @@ -1231,6 +1233,29 @@ mod tests { assert_eq!(meta.cwd.as_deref(), Some(r"C:\Users\alice\project")); } + #[test] + fn reader_accepts_escaped_windows_paths_in_ghost_snapshot() { + let bytes = concat!( + r#"{"timestamp":"2026-04-16T00:00:01.000Z","type":"response_item","payload":{"type":"ghost_snapshot","ghost_commit":{"id":"abc123","preexisting_untracked_files":["C:\\Users\\alice\\project\\file.txt"],"preexisting_untracked_dirs":["D:\\work\\scratch"]}}}"#, + "\n", + ); + + let (_version, body) = + super::parse_codex_reader(Cursor::new(bytes), crate::ParseSelection::full()).unwrap(); + + let [super::Entry::GhostSnapshot(snapshot)] = body.entries.as_ref() else { + panic!("expected ghost snapshot entry"); + }; + assert_eq!( + snapshot.preexisting_untracked_files.as_ref(), + &[SmolStr::from(r"C:\Users\alice\project\file.txt")] + ); + assert_eq!( + snapshot.preexisting_untracked_dirs.as_ref(), + &[SmolStr::from(r"D:\work\scratch")] + ); + } + #[cfg(feature = "agent_session")] #[test] fn probe_session_meta_with_title_uses_post_session_user_text() { diff --git a/agent-sessions/src/providers/copilot/mod.rs b/agent-sessions/src/providers/copilot/mod.rs index 9b58267..98ab65a 100644 --- a/agent-sessions/src/providers/copilot/mod.rs +++ b/agent-sessions/src/providers/copilot/mod.rs @@ -98,19 +98,19 @@ fn parse_chat_session_str(text: &str, selection: ParseSelection) -> Result Result Result Result { let raw: RawChatSessionMeta<'_> = serde_json::from_str(text)?; Ok(ChatSessionBody { - session_id: raw.session_id.map(box_str), - workspace_id: raw.workspace_id.map(box_str), + session_id: raw.session_id.map(cow_to_box), + workspace_id: raw.workspace_id.map(cow_to_box), model: raw .selected_model .and_then(|model| model.identifier.map(box_str)), @@ -563,14 +563,14 @@ fn extract_response_text(raw: Option<&RawValue>) -> Option { #[serde(rename_all = "camelCase")] struct RawChatSession<'a> { #[serde(default)] - session_id: Option<&'a str>, + session_id: Option>, #[serde(default)] - workspace_id: Option<&'a str>, + workspace_id: Option>, #[serde(default, borrow)] requests: Vec>, - #[serde(default)] + #[serde(default, borrow)] mode: Option>, - #[serde(default)] + #[serde(default, borrow)] selected_model: Option>, } @@ -578,12 +578,12 @@ struct RawChatSession<'a> { #[serde(rename_all = "camelCase")] struct RawChatSessionMeta<'a> { #[serde(default)] - session_id: Option<&'a str>, - #[serde(default)] - workspace_id: Option<&'a str>, + session_id: Option>, #[serde(default)] + workspace_id: Option>, + #[serde(default, borrow)] mode: Option>, - #[serde(default)] + #[serde(default, borrow)] selected_model: Option>, } @@ -597,7 +597,7 @@ struct RawChatSessionProbe<'a> { #[derive(Deserialize)] #[serde(rename_all = "camelCase")] struct RawChatRequest<'a> { - request_id: &'a str, + request_id: Cow<'a, str>, #[serde(default, borrow)] message: Option>, #[serde(default, borrow)] @@ -605,13 +605,13 @@ struct RawChatRequest<'a> { #[serde(default)] timestamp: Option, #[serde(default)] - model_id: Option<&'a str>, + model_id: Option>, } #[derive(Deserialize)] struct RawChatMessage<'a> { #[serde(default)] - text: &'a str, + text: Option>, #[serde(default, borrow)] parts: Vec>, } @@ -619,7 +619,7 @@ struct RawChatMessage<'a> { #[derive(Deserialize)] struct RawMessagePart<'a> { #[serde(default)] - text: Option<&'a str>, + text: Option>, } #[derive(Deserialize)] @@ -924,6 +924,36 @@ mod tests { assert!(!from_reader.requests.is_empty()); } + #[test] + fn chat_session_reader_accepts_escaped_windows_workspace_and_prompt() { + let bytes = concat!( + r#"{"sessionId":"sess-chat","workspaceId":"C:\\Users\\alice\\project","requests":[{"requestId":"req-1","message":{"text":"open C:\\Users\\alice\\project\nthen inspect"},"response":[]}]}"#, + "\n", + ); + + let (_version, body) = super::parse_copilot_body_reader( + Cursor::new(bytes.as_bytes()), + crate::InputMetadata::default(), + crate::ParseSelection::full(), + ) + .unwrap(); + + let super::Body::ChatSession(body) = body else { + panic!("expected chat session body"); + }; + assert_eq!( + body.workspace_id.as_deref(), + Some(r#"C:\Users\alice\project"#) + ); + let [request] = body.requests.as_ref() else { + panic!("expected one request"); + }; + assert_eq!( + request.prompt.as_deref(), + Some("open C:\\Users\\alice\\project\nthen inspect") + ); + } + #[cfg(feature = "agent_session")] #[test] fn direct_agent_session_reader_parses_chat_fixture() { diff --git a/agent-sessions/src/providers/cursor/mod.rs b/agent-sessions/src/providers/cursor/mod.rs index 27e6703..051a7a2 100644 --- a/agent-sessions/src/providers/cursor/mod.rs +++ b/agent-sessions/src/providers/cursor/mod.rs @@ -245,6 +245,32 @@ mod tests { assert!(body.entries.is_empty()); } + #[test] + fn reader_accepts_escaped_windows_message_text() { + let bytes = concat!( + r#"{"role":"user","timestamp":"2026-05-01T11:09:46.305Z","message":{"content":"open C:\\Users\\alice\\project\nthen inspect"}}"#, + "\n", + ); + + let body = super::parse_cursor_reader( + IoCursor::new(bytes.as_bytes()), + Some("cursor-win".into()), + crate::ParseSelection::full(), + ) + .unwrap(); + + let [entry] = body.entries.as_ref() else { + panic!("expected one cursor entry"); + }; + let [super::ContentBlock::Text(text)] = entry.blocks.as_ref() else { + panic!("expected one text block"); + }; + assert_eq!( + text.text.as_str(), + "open C:\\Users\\alice\\project\nthen inspect" + ); + } + #[cfg(feature = "agent_session")] #[test] fn direct_agent_session_reader_parses_current_fixture() { diff --git a/agent-sessions/src/providers/gemini/mod.rs b/agent-sessions/src/providers/gemini/mod.rs index 4fcebb3..3b5b819 100644 --- a/agent-sessions/src/providers/gemini/mod.rs +++ b/agent-sessions/src/providers/gemini/mod.rs @@ -446,6 +446,32 @@ where mod tests { use std::io::Cursor; + #[test] + fn reader_accepts_escaped_windows_message_text() { + let bytes = concat!( + r#"{"sessionId":"gem-win","messages":[{"type":"user","id":"u1","timestamp":"2026-05-01T11:09:46.305Z","content":"open C:\\Users\\alice\\project\nthen inspect"}]}"#, + "\n", + ); + + let body = super::parse_gemini_body_reader( + Cursor::new(bytes.as_bytes()), + Some(r#"C:\Users\alice\project"#.into()), + crate::ParseSelection::full(), + ) + .unwrap(); + + let [super::Entry::User(message)] = body.entries.as_ref() else { + panic!("expected one user entry"); + }; + let [super::UserContentPart::Text(text)] = message.content.as_ref() else { + panic!("expected one text part"); + }; + assert_eq!( + text.text.as_str(), + "open C:\\Users\\alice\\project\nthen inspect" + ); + } + #[test] fn direct_agent_session_reader_parses_current_fixture() { let bytes = include_bytes!("../../../tests/fixtures/gemini/session-sample.json").as_slice(); diff --git a/agent-sessions/src/providers/pi/mod.rs b/agent-sessions/src/providers/pi/mod.rs index 4874a75..c982c21 100644 --- a/agent-sessions/src/providers/pi/mod.rs +++ b/agent-sessions/src/providers/pi/mod.rs @@ -624,6 +624,32 @@ mod tests { assert_eq!(cwd.as_deref(), Some("/tmp/project")); } + #[test] + fn reader_accepts_escaped_windows_cwd_and_message_text() { + let bytes = concat!( + r#"{"type":"session","version":3,"id":"pi-win","timestamp":"2026-05-03T00:00:00.000Z","cwd":"C:\\Users\\alice\\project"}"#, + "\n", + r#"{"type":"message","id":"u1","timestamp":"2026-05-03T00:00:01.000Z","message":{"role":"user","content":[{"type":"text","text":"open C:\\Users\\alice\\project\nthen inspect"}]}}"#, + "\n", + ); + + let body = + super::parse_pi_reader(Cursor::new(bytes), crate::ParseSelection::full()).unwrap(); + + let [ + super::Entry::SessionInfo { cwd, .. }, + super::Entry::UserMessage { text, .. }, + ] = body.entries.as_ref() + else { + panic!("expected session info and user message"); + }; + assert_eq!(cwd.as_deref(), Some(r#"C:\Users\alice\project"#)); + assert_eq!( + text.as_str(), + "open C:\\Users\\alice\\project\nthen inspect" + ); + } + #[test] fn probe_session_meta_uses_first_post_parent_session_user_text_as_title() { let mut lines = vec![