mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
unified-exec: retain PathUri in command events (#28780)
## Why App-server must report command events containing foreign-platform paths without changing existing client or rollout path-string formats. ## What changed - retain `PathUri` through exec command begin/end events - convert cwd values to `LegacyAppPathString` at the app-server compatibility boundary - drop command actions with foreign paths and log them - serialize rollout-trace cwd values using their inferred native path representation - restore Wine coverage for retained Windows cwd values and successful completion
This commit is contained in:
committed by
GitHub
Unverified
parent
285eff6c3e
commit
3931bc2bde
@@ -831,7 +831,7 @@ fn sample_command_execution_item_with_id(
|
||||
ThreadItem::CommandExecution {
|
||||
id: id.to_string(),
|
||||
command: "echo hi".to_string(),
|
||||
cwd: test_path_buf("/tmp").abs(),
|
||||
cwd: test_path_buf("/tmp").abs().into(),
|
||||
process_id: Some("pid-1".to_string()),
|
||||
source: CommandExecutionSource::Agent,
|
||||
status,
|
||||
|
||||
+1
-1
@@ -547,7 +547,7 @@
|
||||
"cwd": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
"$ref": "#/definitions/LegacyAppPathString"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
|
||||
@@ -3972,7 +3972,7 @@
|
||||
"cwd": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
"$ref": "#/definitions/LegacyAppPathString"
|
||||
}
|
||||
],
|
||||
"description": "The command's working directory."
|
||||
|
||||
+1
-1
@@ -371,7 +371,7 @@
|
||||
"cwd": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
"$ref": "#/definitions/LegacyAppPathString"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
|
||||
+2
-2
@@ -2378,7 +2378,7 @@
|
||||
"cwd": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/v2/AbsolutePathBuf"
|
||||
"$ref": "#/definitions/v2/LegacyAppPathString"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
@@ -17392,7 +17392,7 @@
|
||||
"cwd": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/v2/AbsolutePathBuf"
|
||||
"$ref": "#/definitions/v2/LegacyAppPathString"
|
||||
}
|
||||
],
|
||||
"description": "The command's working directory."
|
||||
|
||||
+1
-1
@@ -15192,7 +15192,7 @@
|
||||
"cwd": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
"$ref": "#/definitions/LegacyAppPathString"
|
||||
}
|
||||
],
|
||||
"description": "The command's working directory."
|
||||
|
||||
+4
-1
@@ -294,6 +294,9 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"LegacyAppPathString": {
|
||||
"type": "string"
|
||||
},
|
||||
"McpToolCallError": {
|
||||
"properties": {
|
||||
"message": {
|
||||
@@ -687,7 +690,7 @@
|
||||
"cwd": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
"$ref": "#/definitions/LegacyAppPathString"
|
||||
}
|
||||
],
|
||||
"description": "The command's working directory."
|
||||
|
||||
@@ -294,6 +294,9 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"LegacyAppPathString": {
|
||||
"type": "string"
|
||||
},
|
||||
"McpToolCallError": {
|
||||
"properties": {
|
||||
"message": {
|
||||
@@ -687,7 +690,7 @@
|
||||
"cwd": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
"$ref": "#/definitions/LegacyAppPathString"
|
||||
}
|
||||
],
|
||||
"description": "The command's working directory."
|
||||
|
||||
@@ -431,6 +431,9 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"LegacyAppPathString": {
|
||||
"type": "string"
|
||||
},
|
||||
"McpToolCallError": {
|
||||
"properties": {
|
||||
"message": {
|
||||
@@ -831,7 +834,7 @@
|
||||
"cwd": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
"$ref": "#/definitions/LegacyAppPathString"
|
||||
}
|
||||
],
|
||||
"description": "The command's working directory."
|
||||
|
||||
@@ -536,6 +536,9 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"LegacyAppPathString": {
|
||||
"type": "string"
|
||||
},
|
||||
"McpToolCallError": {
|
||||
"properties": {
|
||||
"message": {
|
||||
@@ -1323,7 +1326,7 @@
|
||||
"cwd": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
"$ref": "#/definitions/LegacyAppPathString"
|
||||
}
|
||||
],
|
||||
"description": "The command's working directory."
|
||||
|
||||
@@ -457,6 +457,9 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"LegacyAppPathString": {
|
||||
"type": "string"
|
||||
},
|
||||
"McpToolCallError": {
|
||||
"properties": {
|
||||
"message": {
|
||||
@@ -1138,7 +1141,7 @@
|
||||
"cwd": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
"$ref": "#/definitions/LegacyAppPathString"
|
||||
}
|
||||
],
|
||||
"description": "The command's working directory."
|
||||
|
||||
+4
-1
@@ -457,6 +457,9 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"LegacyAppPathString": {
|
||||
"type": "string"
|
||||
},
|
||||
"McpToolCallError": {
|
||||
"properties": {
|
||||
"message": {
|
||||
@@ -1138,7 +1141,7 @@
|
||||
"cwd": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
"$ref": "#/definitions/LegacyAppPathString"
|
||||
}
|
||||
],
|
||||
"description": "The command's working directory."
|
||||
|
||||
@@ -457,6 +457,9 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"LegacyAppPathString": {
|
||||
"type": "string"
|
||||
},
|
||||
"McpToolCallError": {
|
||||
"properties": {
|
||||
"message": {
|
||||
@@ -1138,7 +1141,7 @@
|
||||
"cwd": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
"$ref": "#/definitions/LegacyAppPathString"
|
||||
}
|
||||
],
|
||||
"description": "The command's working directory."
|
||||
|
||||
@@ -536,6 +536,9 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"LegacyAppPathString": {
|
||||
"type": "string"
|
||||
},
|
||||
"McpToolCallError": {
|
||||
"properties": {
|
||||
"message": {
|
||||
@@ -1323,7 +1326,7 @@
|
||||
"cwd": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
"$ref": "#/definitions/LegacyAppPathString"
|
||||
}
|
||||
],
|
||||
"description": "The command's working directory."
|
||||
|
||||
@@ -457,6 +457,9 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"LegacyAppPathString": {
|
||||
"type": "string"
|
||||
},
|
||||
"McpToolCallError": {
|
||||
"properties": {
|
||||
"message": {
|
||||
@@ -1138,7 +1141,7 @@
|
||||
"cwd": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
"$ref": "#/definitions/LegacyAppPathString"
|
||||
}
|
||||
],
|
||||
"description": "The command's working directory."
|
||||
|
||||
@@ -536,6 +536,9 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"LegacyAppPathString": {
|
||||
"type": "string"
|
||||
},
|
||||
"McpToolCallError": {
|
||||
"properties": {
|
||||
"message": {
|
||||
@@ -1323,7 +1326,7 @@
|
||||
"cwd": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
"$ref": "#/definitions/LegacyAppPathString"
|
||||
}
|
||||
],
|
||||
"description": "The command's working directory."
|
||||
|
||||
+4
-1
@@ -457,6 +457,9 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"LegacyAppPathString": {
|
||||
"type": "string"
|
||||
},
|
||||
"McpToolCallError": {
|
||||
"properties": {
|
||||
"message": {
|
||||
@@ -1138,7 +1141,7 @@
|
||||
"cwd": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
"$ref": "#/definitions/LegacyAppPathString"
|
||||
}
|
||||
],
|
||||
"description": "The command's working directory."
|
||||
|
||||
@@ -457,6 +457,9 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"LegacyAppPathString": {
|
||||
"type": "string"
|
||||
},
|
||||
"McpToolCallError": {
|
||||
"properties": {
|
||||
"message": {
|
||||
@@ -1138,7 +1141,7 @@
|
||||
"cwd": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
"$ref": "#/definitions/LegacyAppPathString"
|
||||
}
|
||||
],
|
||||
"description": "The command's working directory."
|
||||
|
||||
+4
-1
@@ -431,6 +431,9 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"LegacyAppPathString": {
|
||||
"type": "string"
|
||||
},
|
||||
"McpToolCallError": {
|
||||
"properties": {
|
||||
"message": {
|
||||
@@ -831,7 +834,7 @@
|
||||
"cwd": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
"$ref": "#/definitions/LegacyAppPathString"
|
||||
}
|
||||
],
|
||||
"description": "The command's working directory."
|
||||
|
||||
@@ -431,6 +431,9 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"LegacyAppPathString": {
|
||||
"type": "string"
|
||||
},
|
||||
"McpToolCallError": {
|
||||
"properties": {
|
||||
"message": {
|
||||
@@ -831,7 +834,7 @@
|
||||
"cwd": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
"$ref": "#/definitions/LegacyAppPathString"
|
||||
}
|
||||
],
|
||||
"description": "The command's working directory."
|
||||
|
||||
@@ -431,6 +431,9 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"LegacyAppPathString": {
|
||||
"type": "string"
|
||||
},
|
||||
"McpToolCallError": {
|
||||
"properties": {
|
||||
"message": {
|
||||
@@ -831,7 +834,7 @@
|
||||
"cwd": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
"$ref": "#/definitions/LegacyAppPathString"
|
||||
}
|
||||
],
|
||||
"description": "The command's working directory."
|
||||
|
||||
Generated
+2
-2
@@ -1,7 +1,7 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { AbsolutePathBuf } from "../AbsolutePathBuf";
|
||||
import type { LegacyAppPathString } from "../LegacyAppPathString";
|
||||
import type { CommandAction } from "./CommandAction";
|
||||
import type { ExecPolicyAmendment } from "./ExecPolicyAmendment";
|
||||
import type { NetworkApprovalContext } from "./NetworkApprovalContext";
|
||||
@@ -34,7 +34,7 @@ networkApprovalContext?: NetworkApprovalContext | null, /**
|
||||
command?: string | null, /**
|
||||
* The command's working directory.
|
||||
*/
|
||||
cwd?: AbsolutePathBuf | null, /**
|
||||
cwd?: LegacyAppPathString | null, /**
|
||||
* Best-effort parsed command actions for friendly display.
|
||||
*/
|
||||
commandActions?: Array<CommandAction> | null, /**
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { AbsolutePathBuf } from "../AbsolutePathBuf";
|
||||
import type { LegacyAppPathString } from "../LegacyAppPathString";
|
||||
import type { MessagePhase } from "../MessagePhase";
|
||||
import type { ReasoningEffort } from "../ReasoningEffort";
|
||||
import type { JsonValue } from "../serde_json/JsonValue";
|
||||
@@ -32,7 +33,7 @@ command: string,
|
||||
/**
|
||||
* The command's working directory.
|
||||
*/
|
||||
cwd: AbsolutePathBuf,
|
||||
cwd: LegacyAppPathString,
|
||||
/**
|
||||
* Identifier for the underlying PTY process (when available).
|
||||
*/
|
||||
|
||||
@@ -23,6 +23,7 @@ use crate::protocol::v2::PatchApplyStatus;
|
||||
use crate::protocol::v2::PatchChangeKind;
|
||||
use crate::protocol::v2::ThreadItem;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::parse_command::ParsedCommand;
|
||||
use codex_protocol::protocol::ApplyPatchApprovalRequestEvent;
|
||||
use codex_protocol::protocol::ExecApprovalRequestEvent;
|
||||
use codex_protocol::protocol::ExecCommandBeginEvent;
|
||||
@@ -34,8 +35,11 @@ use codex_protocol::protocol::PatchApplyBeginEvent;
|
||||
use codex_protocol::protocol::PatchApplyEndEvent;
|
||||
use codex_shell_command::parse_command::parse_command;
|
||||
use codex_shell_command::parse_command::shlex_join;
|
||||
use codex_utils_path_uri::PathConvention;
|
||||
use codex_utils_path_uri::PathUri;
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use tracing::warn;
|
||||
|
||||
pub fn build_file_change_approval_request_item(
|
||||
payload: &ApplyPatchApprovalRequestEvent,
|
||||
@@ -69,7 +73,7 @@ pub fn build_command_execution_approval_request_item(
|
||||
ThreadItem::CommandExecution {
|
||||
id: payload.call_id.clone(),
|
||||
command: shlex_join(&payload.command),
|
||||
cwd: payload.cwd.clone(),
|
||||
cwd: payload.cwd.clone().into(),
|
||||
process_id: None,
|
||||
source: CommandExecutionSource::Agent,
|
||||
status: CommandExecutionStatus::InProgress,
|
||||
@@ -86,19 +90,15 @@ pub fn build_command_execution_approval_request_item(
|
||||
}
|
||||
|
||||
pub fn build_command_execution_begin_item(payload: &ExecCommandBeginEvent) -> ThreadItem {
|
||||
let command_actions = command_actions_for_path_uri(&payload.parsed_cmd, &payload.cwd);
|
||||
ThreadItem::CommandExecution {
|
||||
id: payload.call_id.clone(),
|
||||
command: shlex_join(&payload.command),
|
||||
cwd: payload.cwd.clone(),
|
||||
cwd: payload.cwd.clone().into(),
|
||||
process_id: payload.process_id.clone(),
|
||||
source: payload.source.into(),
|
||||
status: CommandExecutionStatus::InProgress,
|
||||
command_actions: payload
|
||||
.parsed_cmd
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|parsed| CommandAction::from_core_with_cwd(parsed, &payload.cwd))
|
||||
.collect(),
|
||||
command_actions,
|
||||
aggregated_output: None,
|
||||
exit_code: None,
|
||||
duration_ms: None,
|
||||
@@ -112,26 +112,63 @@ pub fn build_command_execution_end_item(payload: &ExecCommandEndEvent) -> Thread
|
||||
Some(payload.aggregated_output.clone())
|
||||
};
|
||||
let duration_ms = i64::try_from(payload.duration.as_millis()).unwrap_or(i64::MAX);
|
||||
let command_actions = command_actions_for_path_uri(&payload.parsed_cmd, &payload.cwd);
|
||||
|
||||
ThreadItem::CommandExecution {
|
||||
id: payload.call_id.clone(),
|
||||
command: shlex_join(&payload.command),
|
||||
cwd: payload.cwd.clone(),
|
||||
cwd: payload.cwd.clone().into(),
|
||||
process_id: payload.process_id.clone(),
|
||||
source: payload.source.into(),
|
||||
status: (&payload.status).into(),
|
||||
command_actions: payload
|
||||
.parsed_cmd
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|parsed| CommandAction::from_core_with_cwd(parsed, &payload.cwd))
|
||||
.collect(),
|
||||
command_actions,
|
||||
aggregated_output,
|
||||
exit_code: Some(payload.exit_code),
|
||||
duration_ms: Some(duration_ms),
|
||||
}
|
||||
}
|
||||
|
||||
fn command_actions_for_path_uri(parsed_cmd: &[ParsedCommand], cwd: &PathUri) -> Vec<CommandAction> {
|
||||
// TODO(anp): Carry PathUri into CommandAction so foreign Read actions retain resolved paths.
|
||||
// Until then, omit those actions rather than project a foreign cwd onto the host.
|
||||
let native_cwd = if cwd.infer_path_convention() == Some(PathConvention::native()) {
|
||||
cwd.to_abs_path().ok()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
parsed_cmd
|
||||
.iter()
|
||||
.cloned()
|
||||
.filter_map(|parsed| match parsed {
|
||||
ParsedCommand::Read { cmd, name, path } => match native_cwd.as_ref() {
|
||||
Some(native_cwd) => Some(CommandAction::Read {
|
||||
command: cmd,
|
||||
name,
|
||||
path: native_cwd.join(path),
|
||||
}),
|
||||
None => {
|
||||
warn!(
|
||||
command = cmd,
|
||||
%cwd,
|
||||
"omitting read command action whose path cannot be resolved against a foreign cwd"
|
||||
);
|
||||
None
|
||||
}
|
||||
},
|
||||
ParsedCommand::ListFiles { cmd, path } => {
|
||||
Some(CommandAction::ListFiles { command: cmd, path })
|
||||
}
|
||||
ParsedCommand::Search { cmd, query, path } => Some(CommandAction::Search {
|
||||
command: cmd,
|
||||
query,
|
||||
path,
|
||||
}),
|
||||
ParsedCommand::Unknown { cmd } => Some(CommandAction::Unknown { command: cmd }),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Build a guardian-derived [`ThreadItem`].
|
||||
///
|
||||
/// Currently this only synthesizes [`ThreadItem::CommandExecution`] for
|
||||
@@ -150,7 +187,7 @@ pub fn build_item_from_guardian_event(
|
||||
Some(ThreadItem::CommandExecution {
|
||||
id: id.clone(),
|
||||
command,
|
||||
cwd: cwd.clone(),
|
||||
cwd: cwd.clone().into(),
|
||||
process_id: None,
|
||||
source: CommandExecutionSource::Agent,
|
||||
status,
|
||||
@@ -186,7 +223,7 @@ pub fn build_item_from_guardian_event(
|
||||
Some(ThreadItem::CommandExecution {
|
||||
id: id.clone(),
|
||||
command,
|
||||
cwd: cwd.clone(),
|
||||
cwd: cwd.clone().into(),
|
||||
process_id: None,
|
||||
source: CommandExecutionSource::Agent,
|
||||
status,
|
||||
@@ -315,3 +352,7 @@ fn format_file_change_diff(change: &FileChange) -> String {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "item_builders_tests.rs"]
|
||||
mod tests;
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn foreign_read_is_omitted_without_dropping_other_command_actions() {
|
||||
#[cfg(windows)]
|
||||
let cwd = PathUri::parse("file:///usr/local/src").expect("valid foreign POSIX cwd");
|
||||
#[cfg(not(windows))]
|
||||
let cwd = PathUri::parse("file:///C:/src").expect("valid foreign Windows cwd");
|
||||
let parsed_cmd = vec![
|
||||
ParsedCommand::Read {
|
||||
cmd: "cat file.txt".to_string(),
|
||||
name: "file.txt".to_string(),
|
||||
path: PathBuf::from("file.txt"),
|
||||
},
|
||||
ParsedCommand::ListFiles {
|
||||
cmd: "ls".to_string(),
|
||||
path: Some("subdir".to_string()),
|
||||
},
|
||||
ParsedCommand::Search {
|
||||
cmd: "rg needle".to_string(),
|
||||
query: Some("needle".to_string()),
|
||||
path: Some("src".to_string()),
|
||||
},
|
||||
];
|
||||
|
||||
assert_eq!(
|
||||
command_actions_for_path_uri(&parsed_cmd, &cwd),
|
||||
vec![
|
||||
CommandAction::ListFiles {
|
||||
command: "ls".to_string(),
|
||||
path: Some("subdir".to_string()),
|
||||
},
|
||||
CommandAction::Search {
|
||||
command: "rg needle".to_string(),
|
||||
query: Some("needle".to_string()),
|
||||
path: Some("src".to_string()),
|
||||
},
|
||||
]
|
||||
);
|
||||
}
|
||||
@@ -2376,7 +2376,7 @@ mod tests {
|
||||
turn_id: "turn-1".into(),
|
||||
completed_at_ms: 0,
|
||||
command: vec!["echo".into(), "hello world".into()],
|
||||
cwd: test_path_buf("/tmp").abs(),
|
||||
cwd: test_path_buf("/tmp").abs().into(),
|
||||
parsed_cmd: vec![ParsedCommand::Unknown {
|
||||
cmd: "echo hello world".into(),
|
||||
}],
|
||||
@@ -2427,7 +2427,7 @@ mod tests {
|
||||
ThreadItem::CommandExecution {
|
||||
id: "exec-1".into(),
|
||||
command: "echo 'hello world'".into(),
|
||||
cwd: test_path_buf("/tmp").abs(),
|
||||
cwd: test_path_buf("/tmp").abs().into(),
|
||||
process_id: Some("pid-1".into()),
|
||||
source: CommandExecutionSource::Agent,
|
||||
status: CommandExecutionStatus::Completed,
|
||||
@@ -2616,7 +2616,7 @@ mod tests {
|
||||
turn_id: "turn-1".into(),
|
||||
completed_at_ms: 0,
|
||||
command: vec!["ls".into()],
|
||||
cwd: test_path_buf("/tmp").abs(),
|
||||
cwd: test_path_buf("/tmp").abs().into(),
|
||||
parsed_cmd: vec![ParsedCommand::Unknown { cmd: "ls".into() }],
|
||||
source: ExecCommandSource::Agent,
|
||||
interaction_input: None,
|
||||
@@ -2658,7 +2658,7 @@ mod tests {
|
||||
ThreadItem::CommandExecution {
|
||||
id: "exec-declined".into(),
|
||||
command: "ls".into(),
|
||||
cwd: test_path_buf("/tmp").abs(),
|
||||
cwd: test_path_buf("/tmp").abs().into(),
|
||||
process_id: Some("pid-2".into()),
|
||||
source: CommandExecutionSource::Agent,
|
||||
status: CommandExecutionStatus::Declined,
|
||||
@@ -2756,7 +2756,7 @@ mod tests {
|
||||
ThreadItem::CommandExecution {
|
||||
id: "guardian-exec".into(),
|
||||
command: "rm -rf /tmp/guardian".into(),
|
||||
cwd: test_path_buf("/tmp").abs(),
|
||||
cwd: test_path_buf("/tmp").abs().into(),
|
||||
process_id: None,
|
||||
source: CommandExecutionSource::Agent,
|
||||
status: CommandExecutionStatus::Declined,
|
||||
@@ -2822,7 +2822,7 @@ mod tests {
|
||||
ThreadItem::CommandExecution {
|
||||
id: "guardian-execve".into(),
|
||||
command: "/bin/rm -f /tmp/file.sqlite".into(),
|
||||
cwd: test_path_buf("/tmp").abs(),
|
||||
cwd: test_path_buf("/tmp").abs().into(),
|
||||
process_id: None,
|
||||
source: CommandExecutionSource::Agent,
|
||||
status: CommandExecutionStatus::InProgress,
|
||||
@@ -2882,7 +2882,7 @@ mod tests {
|
||||
turn_id: "turn-a".into(),
|
||||
completed_at_ms: 0,
|
||||
command: vec!["echo".into(), "done".into()],
|
||||
cwd: test_path_buf("/tmp").abs(),
|
||||
cwd: test_path_buf("/tmp").abs().into(),
|
||||
parsed_cmd: vec![ParsedCommand::Unknown {
|
||||
cmd: "echo done".into(),
|
||||
}],
|
||||
@@ -2920,7 +2920,7 @@ mod tests {
|
||||
ThreadItem::CommandExecution {
|
||||
id: "exec-late".into(),
|
||||
command: "echo done".into(),
|
||||
cwd: test_path_buf("/tmp").abs(),
|
||||
cwd: test_path_buf("/tmp").abs().into(),
|
||||
process_id: Some("pid-42".into()),
|
||||
source: CommandExecutionSource::Agent,
|
||||
status: CommandExecutionStatus::Completed,
|
||||
@@ -2980,7 +2980,7 @@ mod tests {
|
||||
turn_id: "turn-missing".into(),
|
||||
completed_at_ms: 0,
|
||||
command: vec!["echo".into(), "done".into()],
|
||||
cwd: test_path_buf("/tmp").abs(),
|
||||
cwd: test_path_buf("/tmp").abs().into(),
|
||||
parsed_cmd: vec![ParsedCommand::Unknown {
|
||||
cmd: "echo done".into(),
|
||||
}],
|
||||
|
||||
@@ -31,6 +31,7 @@ use codex_protocol::protocol::PatchApplyStatus as CorePatchApplyStatus;
|
||||
use codex_protocol::protocol::ReviewDecision as CoreReviewDecision;
|
||||
use codex_protocol::protocol::SubAgentActivityKind as CoreSubAgentActivityKind;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_path_uri::LegacyAppPathString;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
@@ -256,7 +257,7 @@ pub enum ThreadItem {
|
||||
/// The command to be executed.
|
||||
command: String,
|
||||
/// The command's working directory.
|
||||
cwd: AbsolutePathBuf,
|
||||
cwd: LegacyAppPathString,
|
||||
/// Identifier for the underlying PTY process (when available).
|
||||
process_id: Option<String>,
|
||||
#[serde(default)]
|
||||
@@ -1339,7 +1340,7 @@ pub struct CommandExecutionRequestApprovalParams {
|
||||
/// The command's working directory.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional = nullable)]
|
||||
pub cwd: Option<AbsolutePathBuf>,
|
||||
pub cwd: Option<LegacyAppPathString>,
|
||||
/// Best-effort parsed command actions for friendly display.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional = nullable)]
|
||||
|
||||
@@ -2092,7 +2092,7 @@ impl CodexClient {
|
||||
println!("< command: {command}");
|
||||
}
|
||||
if let Some(cwd) = cwd.as_ref() {
|
||||
println!("< cwd: {}", cwd.display());
|
||||
println!("< cwd: {cwd}");
|
||||
}
|
||||
if let Some(command_actions) = command_actions.as_ref()
|
||||
&& !command_actions.is_empty()
|
||||
|
||||
@@ -113,6 +113,7 @@ use codex_protocol::request_user_input::RequestUserInputResponse as CoreRequestU
|
||||
use codex_sandboxing::policy_transforms::intersect_permission_profiles;
|
||||
use codex_shell_command::parse_command::shlex_join;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_path_uri::LegacyAppPathString;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::time::SystemTime;
|
||||
@@ -129,7 +130,7 @@ enum CommandExecutionApprovalPresentation {
|
||||
#[derive(Debug, PartialEq)]
|
||||
struct CommandExecutionCompletionItem {
|
||||
command: String,
|
||||
cwd: AbsolutePathBuf,
|
||||
cwd: LegacyAppPathString,
|
||||
command_actions: Vec<V2ParsedCommand>,
|
||||
}
|
||||
|
||||
@@ -575,7 +576,7 @@ pub(crate) async fn apply_bespoke_event_handling(
|
||||
let command_string = shlex_join(&command);
|
||||
let completion_item = CommandExecutionCompletionItem {
|
||||
command: command_string,
|
||||
cwd: cwd.clone(),
|
||||
cwd: cwd.clone().into(),
|
||||
command_actions: command_actions.clone(),
|
||||
};
|
||||
CommandExecutionApprovalPresentation::Command(completion_item)
|
||||
@@ -1354,7 +1355,7 @@ async fn start_command_execution_item(
|
||||
turn_id: String,
|
||||
item_id: String,
|
||||
command: String,
|
||||
cwd: AbsolutePathBuf,
|
||||
cwd: LegacyAppPathString,
|
||||
command_actions: Vec<V2ParsedCommand>,
|
||||
source: CommandExecutionSource,
|
||||
outgoing: &ThreadScopedOutgoingMessageSender,
|
||||
@@ -1398,7 +1399,7 @@ async fn complete_command_execution_item(
|
||||
turn_id: String,
|
||||
item_id: String,
|
||||
command: String,
|
||||
cwd: AbsolutePathBuf,
|
||||
cwd: LegacyAppPathString,
|
||||
process_id: Option<String>,
|
||||
source: CommandExecutionSource,
|
||||
command_actions: Vec<V2ParsedCommand>,
|
||||
@@ -2323,7 +2324,7 @@ mod tests {
|
||||
fn command_execution_completion_item(command: &str) -> CommandExecutionCompletionItem {
|
||||
CommandExecutionCompletionItem {
|
||||
command: command.to_string(),
|
||||
cwd: test_path_buf("/tmp").abs(),
|
||||
cwd: test_path_buf("/tmp").abs().into(),
|
||||
command_actions: vec![V2ParsedCommand::Unknown {
|
||||
command: command.to_string(),
|
||||
}],
|
||||
|
||||
@@ -553,8 +553,7 @@ fn resolve_turn_environment_selections(
|
||||
let environment_id = environment.environment_id;
|
||||
let cwd = environment
|
||||
.cwd
|
||||
.infer_absolute_path_convention()
|
||||
.and_then(|convention| environment.cwd.to_path_uri(convention).ok())
|
||||
.to_inferred_path_uri()
|
||||
.ok_or_else(|| {
|
||||
invalid_request(format!(
|
||||
"invalid cwd for environment `{environment_id}`: path `{}` does not use absolute POSIX or Windows path syntax",
|
||||
|
||||
@@ -254,7 +254,7 @@ async fn command_execution_request_approval_strips_additional_permissions_withou
|
||||
reason: Some("Need extra read access".to_string()),
|
||||
network_approval_context: None,
|
||||
command: Some("cat file".to_string()),
|
||||
cwd: Some(absolute_path("/tmp")),
|
||||
cwd: Some(absolute_path("/tmp").into()),
|
||||
command_actions: None,
|
||||
additional_permissions: Some(
|
||||
codex_app_server_protocol::AdditionalPermissionProfile {
|
||||
@@ -320,7 +320,7 @@ async fn command_execution_request_approval_keeps_additional_permissions_with_ca
|
||||
reason: Some("Need extra read access".to_string()),
|
||||
network_approval_context: None,
|
||||
command: Some("cat file".to_string()),
|
||||
cwd: Some(absolute_path("/tmp")),
|
||||
cwd: Some(absolute_path("/tmp").into()),
|
||||
command_actions: None,
|
||||
additional_permissions: Some(
|
||||
codex_app_server_protocol::AdditionalPermissionProfile {
|
||||
|
||||
@@ -2440,7 +2440,7 @@ async fn turn_start_updates_sandbox_and_cwd_between_turns_v2() -> Result<()> {
|
||||
else {
|
||||
unreachable!("loop ensures we break on command execution items");
|
||||
};
|
||||
assert_eq!(cwd.as_path(), second_cwd.as_path());
|
||||
assert_eq!(cwd.as_str(), second_cwd.to_string_lossy().as_ref());
|
||||
let expected_command = format_with_current_shell_display("echo second turn");
|
||||
assert_eq!(command, expected_command);
|
||||
assert_eq!(status, CommandExecutionStatus::InProgress);
|
||||
|
||||
@@ -167,7 +167,7 @@ async fn turn_start_shell_zsh_fork_executes_command_v2() -> Result<()> {
|
||||
assert!(command.contains("/bin/sh -c"));
|
||||
assert!(command.contains("sleep 0.01"));
|
||||
assert!(command.contains(&release_marker.display().to_string()));
|
||||
assert_eq!(cwd.as_path(), workspace.as_path());
|
||||
assert_eq!(cwd.as_str(), workspace.to_string_lossy().as_ref());
|
||||
|
||||
mcp.interrupt_turn_and_wait_for_aborted(thread.id, turn.id, DEFAULT_READ_TIMEOUT)
|
||||
.await?;
|
||||
|
||||
@@ -188,7 +188,7 @@ pub(crate) async fn execute_user_shell_command(
|
||||
turn_id: turn_context.sub_id.clone(),
|
||||
started_at_ms: now_unix_timestamp_ms(),
|
||||
command: display_command.clone(),
|
||||
cwd: cwd.clone(),
|
||||
cwd: cwd.clone().into(),
|
||||
parsed_cmd: parsed_cmd.clone(),
|
||||
source: ExecCommandSource::UserShell,
|
||||
interaction_input: None,
|
||||
@@ -262,7 +262,7 @@ pub(crate) async fn execute_user_shell_command(
|
||||
turn_id: turn_context.sub_id.clone(),
|
||||
completed_at_ms: now_unix_timestamp_ms(),
|
||||
command: display_command.clone(),
|
||||
cwd: cwd.clone(),
|
||||
cwd: cwd.clone().into(),
|
||||
parsed_cmd: parsed_cmd.clone(),
|
||||
source: ExecCommandSource::UserShell,
|
||||
interaction_input: None,
|
||||
@@ -287,7 +287,7 @@ pub(crate) async fn execute_user_shell_command(
|
||||
turn_id: turn_context.sub_id.clone(),
|
||||
completed_at_ms: now_unix_timestamp_ms(),
|
||||
command: display_command.clone(),
|
||||
cwd: cwd.clone(),
|
||||
cwd: cwd.clone().into(),
|
||||
parsed_cmd: parsed_cmd.clone(),
|
||||
source: ExecCommandSource::UserShell,
|
||||
interaction_input: None,
|
||||
@@ -332,7 +332,7 @@ pub(crate) async fn execute_user_shell_command(
|
||||
turn_id: turn_context.sub_id.clone(),
|
||||
completed_at_ms: now_unix_timestamp_ms(),
|
||||
command: display_command,
|
||||
cwd,
|
||||
cwd: cwd.into(),
|
||||
parsed_cmd,
|
||||
source: ExecCommandSource::UserShell,
|
||||
interaction_input: None,
|
||||
|
||||
@@ -96,7 +96,7 @@ fn tracker_update_for_known_delta<'a>(
|
||||
pub(crate) async fn emit_exec_command_begin(
|
||||
ctx: ToolEventCtx<'_>,
|
||||
command: &[String],
|
||||
cwd: &AbsolutePathBuf,
|
||||
cwd: &PathUri,
|
||||
parsed_cmd: &[ParsedCommand],
|
||||
source: ExecCommandSource,
|
||||
interaction_input: Option<String>,
|
||||
@@ -123,7 +123,7 @@ pub(crate) async fn emit_exec_command_begin(
|
||||
pub(crate) enum ToolEmitter {
|
||||
Shell {
|
||||
command: Vec<String>,
|
||||
cwd: AbsolutePathBuf,
|
||||
cwd: PathUri,
|
||||
source: ExecCommandSource,
|
||||
parsed_cmd: Vec<ParsedCommand>,
|
||||
},
|
||||
@@ -146,7 +146,7 @@ impl ToolEmitter {
|
||||
let parsed_cmd = parse_command(&command);
|
||||
Self::Shell {
|
||||
command,
|
||||
cwd,
|
||||
cwd: PathUri::from_abs_path(&cwd),
|
||||
source,
|
||||
parsed_cmd,
|
||||
}
|
||||
@@ -321,15 +321,11 @@ impl ToolEmitter {
|
||||
},
|
||||
stage,
|
||||
) => {
|
||||
// TODO(anp): Migrate exec command protocol events to PathUri.
|
||||
let Ok(cwd) = cwd.to_abs_path() else {
|
||||
return;
|
||||
};
|
||||
emit_exec_stage(
|
||||
ctx,
|
||||
ExecCommandInput::new(
|
||||
command,
|
||||
&cwd,
|
||||
cwd,
|
||||
parsed_cmd,
|
||||
*source,
|
||||
/*interaction_input*/ None,
|
||||
@@ -437,7 +433,7 @@ impl ToolEmitter {
|
||||
|
||||
struct ExecCommandInput<'a> {
|
||||
command: &'a [String],
|
||||
cwd: &'a AbsolutePathBuf,
|
||||
cwd: &'a PathUri,
|
||||
parsed_cmd: &'a [ParsedCommand],
|
||||
source: ExecCommandSource,
|
||||
interaction_input: Option<&'a str>,
|
||||
@@ -447,7 +443,7 @@ struct ExecCommandInput<'a> {
|
||||
impl<'a> ExecCommandInput<'a> {
|
||||
fn new(
|
||||
command: &'a [String],
|
||||
cwd: &'a AbsolutePathBuf,
|
||||
cwd: &'a PathUri,
|
||||
parsed_cmd: &'a [ParsedCommand],
|
||||
source: ExecCommandSource,
|
||||
interaction_input: Option<&'a str>,
|
||||
|
||||
@@ -20,6 +20,7 @@ use codex_features::Feature;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
use codex_protocol::protocol::EventMsg;
|
||||
use codex_protocol::protocol::ExecCommandStatus;
|
||||
use codex_protocol::protocol::Op;
|
||||
use codex_protocol::protocol::TurnEnvironmentSelection;
|
||||
use codex_protocol::protocol::TurnEnvironmentSelections;
|
||||
@@ -127,10 +128,6 @@ async fn windows_exec_server_runs_with_native_shell_and_cwd() -> Result<()> {
|
||||
})
|
||||
.await?;
|
||||
|
||||
// TODO(anp): Re-enable these event assertions once exec command events retain a
|
||||
// PathUri cwd. Today the host-native event conversion drops begin/end events for a
|
||||
// foreign cwd.
|
||||
/*
|
||||
let mut begin = None;
|
||||
let mut end = None;
|
||||
let mut turn_complete = false;
|
||||
@@ -161,13 +158,9 @@ async fn windows_exec_server_runs_with_native_shell_and_cwd() -> Result<()> {
|
||||
assert_eq!(&begin.command[1..], ["-NoProfile", "-Command", COMMAND]);
|
||||
|
||||
let end = end.context("exec_command should emit an end event")?;
|
||||
let expected_cwd = PathUri::parse("file:///C:/windows")?;
|
||||
assert_eq!((&begin.cwd, &end.cwd), (&expected_cwd, &expected_cwd));
|
||||
assert_eq!((end.exit_code, end.status), (0, ExecCommandStatus::Completed));
|
||||
*/
|
||||
|
||||
wait_for_event(&test.codex, |event| {
|
||||
matches!(event, EventMsg::TurnComplete(_))
|
||||
})
|
||||
.await;
|
||||
|
||||
let request = response_mock
|
||||
.last_request()
|
||||
|
||||
@@ -437,7 +437,7 @@ async fn unified_exec_emits_exec_command_begin_event() -> Result<()> {
|
||||
|
||||
assert_command(&begin_event.command, "-lc", "/bin/echo hello unified exec");
|
||||
|
||||
assert_eq!(begin_event.cwd.as_path(), cwd.as_path());
|
||||
assert_eq!(begin_event.cwd, PathUri::from_path(&cwd)?);
|
||||
|
||||
wait_for_event(&test.codex, |event| {
|
||||
matches!(event, EventMsg::TurnComplete(_))
|
||||
@@ -507,8 +507,8 @@ async fn unified_exec_resolves_relative_workdir() -> Result<()> {
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
begin_event.cwd.as_path(),
|
||||
workdir.as_path(),
|
||||
begin_event.cwd,
|
||||
PathUri::from_path(&workdir)?,
|
||||
"exec_command cwd should resolve relative workdir against turn cwd",
|
||||
);
|
||||
|
||||
@@ -569,8 +569,8 @@ async fn unified_exec_respects_workdir_override() -> Result<()> {
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
begin_event.cwd.as_path(),
|
||||
workdir.as_path(),
|
||||
begin_event.cwd,
|
||||
PathUri::from_path(&workdir)?,
|
||||
"exec_command cwd should reflect the requested workdir override"
|
||||
);
|
||||
|
||||
|
||||
@@ -68,10 +68,9 @@ impl EventProcessorWithHumanOutput {
|
||||
match item {
|
||||
ThreadItem::CommandExecution { command, cwd, .. } => {
|
||||
eprintln!(
|
||||
"{}\n{} in {}",
|
||||
"{}\n{} in {cwd}",
|
||||
"exec".style(self.italic).style(self.magenta),
|
||||
command.style(self.bold),
|
||||
cwd.display()
|
||||
);
|
||||
}
|
||||
ThreadItem::McpToolCall { server, tool, .. } => {
|
||||
|
||||
@@ -170,7 +170,7 @@ fn command_execution_started_and_completed_translate_to_thread_events() {
|
||||
let command_item = ThreadItem::CommandExecution {
|
||||
id: "cmd-1".to_string(),
|
||||
command: "ls".to_string(),
|
||||
cwd: test_path_buf("/tmp/project").abs(),
|
||||
cwd: test_path_buf("/tmp/project").abs().into(),
|
||||
process_id: Some("123".to_string()),
|
||||
source: CommandExecutionSource::UserShell,
|
||||
status: ApiCommandExecutionStatus::InProgress,
|
||||
@@ -210,7 +210,7 @@ fn command_execution_started_and_completed_translate_to_thread_events() {
|
||||
item: ThreadItem::CommandExecution {
|
||||
id: "cmd-1".to_string(),
|
||||
command: "ls".to_string(),
|
||||
cwd: test_path_buf("/tmp/project").abs(),
|
||||
cwd: test_path_buf("/tmp/project").abs().into(),
|
||||
process_id: Some("123".to_string()),
|
||||
source: CommandExecutionSource::UserShell,
|
||||
status: ApiCommandExecutionStatus::Completed,
|
||||
@@ -1321,7 +1321,7 @@ fn turn_completion_reconciles_started_items_from_turn_items() {
|
||||
item: ThreadItem::CommandExecution {
|
||||
id: "cmd-1".to_string(),
|
||||
command: "ls".to_string(),
|
||||
cwd: test_path_buf("/tmp/project").abs(),
|
||||
cwd: test_path_buf("/tmp/project").abs().into(),
|
||||
process_id: Some("123".to_string()),
|
||||
source: CommandExecutionSource::UserShell,
|
||||
status: ApiCommandExecutionStatus::InProgress,
|
||||
@@ -1361,7 +1361,7 @@ fn turn_completion_reconciles_started_items_from_turn_items() {
|
||||
items: vec![ThreadItem::CommandExecution {
|
||||
id: "cmd-1".to_string(),
|
||||
command: "ls".to_string(),
|
||||
cwd: test_path_buf("/tmp/project").abs(),
|
||||
cwd: test_path_buf("/tmp/project").abs().into(),
|
||||
process_id: Some("123".to_string()),
|
||||
source: CommandExecutionSource::UserShell,
|
||||
status: ApiCommandExecutionStatus::Completed,
|
||||
|
||||
@@ -3246,7 +3246,7 @@ pub struct ExecCommandBeginEvent {
|
||||
/// The command to be executed.
|
||||
pub command: Vec<String>,
|
||||
/// The command's working directory if not the default cwd for the agent.
|
||||
pub cwd: AbsolutePathBuf,
|
||||
pub cwd: PathUri,
|
||||
pub parsed_cmd: Vec<ParsedCommand>,
|
||||
/// Where the command originated. Defaults to Agent for backward compatibility.
|
||||
#[serde(default)]
|
||||
@@ -3272,7 +3272,7 @@ pub struct ExecCommandEndEvent {
|
||||
/// The command that was executed.
|
||||
pub command: Vec<String>,
|
||||
/// The command's working directory if not the default cwd for the agent.
|
||||
pub cwd: AbsolutePathBuf,
|
||||
pub cwd: PathUri,
|
||||
pub parsed_cmd: Vec<ParsedCommand>,
|
||||
/// Where the command originated. Defaults to Agent for backward compatibility.
|
||||
#[serde(default)]
|
||||
|
||||
@@ -24,6 +24,7 @@ use codex_protocol::protocol::PatchApplyStatus;
|
||||
use codex_protocol::protocol::SubAgentActivityEvent;
|
||||
use codex_protocol::protocol::TurnAbortReason;
|
||||
use serde::Serialize;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::AgentThreadId;
|
||||
use crate::CodexTurnId;
|
||||
@@ -120,8 +121,12 @@ impl Serialize for ToolRuntimePayload<'_> {
|
||||
S: serde::Serializer,
|
||||
{
|
||||
match self {
|
||||
ToolRuntimePayload::ExecCommandBegin(event) => event.serialize(serializer),
|
||||
ToolRuntimePayload::ExecCommandEnd(event) => event.serialize(serializer),
|
||||
ToolRuntimePayload::ExecCommandBegin(event) => {
|
||||
ExecCommandBeginTracePayload::from(*event).serialize(serializer)
|
||||
}
|
||||
ToolRuntimePayload::ExecCommandEnd(event) => {
|
||||
ExecCommandEndTracePayload::from(*event).serialize(serializer)
|
||||
}
|
||||
ToolRuntimePayload::PatchApplyBegin(event) => event.serialize(serializer),
|
||||
ToolRuntimePayload::PatchApplyEnd(event) => event.serialize(serializer),
|
||||
ToolRuntimePayload::McpToolCallBegin(event) => event.serialize(serializer),
|
||||
@@ -139,6 +144,119 @@ impl Serialize for ToolRuntimePayload<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Rollout-trace representation of an exec begin event.
|
||||
///
|
||||
/// Rollout traces share the rollout compatibility requirement that paths remain path-flavored
|
||||
/// strings on disk, even though live events carry `PathUri` internally.
|
||||
#[derive(Serialize)]
|
||||
struct ExecCommandBeginTracePayload<'a> {
|
||||
call_id: &'a str,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
process_id: Option<&'a str>,
|
||||
turn_id: &'a str,
|
||||
started_at_ms: i64,
|
||||
command: &'a [String],
|
||||
cwd: String,
|
||||
parsed_cmd: &'a [codex_protocol::parse_command::ParsedCommand],
|
||||
source: ExecCommandSource,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
interaction_input: Option<&'a str>,
|
||||
}
|
||||
|
||||
impl<'a> From<&'a ExecCommandBeginEvent> for ExecCommandBeginTracePayload<'a> {
|
||||
fn from(event: &'a ExecCommandBeginEvent) -> Self {
|
||||
let ExecCommandBeginEvent {
|
||||
call_id,
|
||||
process_id,
|
||||
turn_id,
|
||||
started_at_ms,
|
||||
command,
|
||||
cwd,
|
||||
parsed_cmd,
|
||||
source,
|
||||
interaction_input,
|
||||
} = event;
|
||||
Self {
|
||||
call_id,
|
||||
process_id: process_id.as_deref(),
|
||||
turn_id,
|
||||
started_at_ms: *started_at_ms,
|
||||
command,
|
||||
cwd: cwd.inferred_native_path_string(),
|
||||
parsed_cmd,
|
||||
source: *source,
|
||||
interaction_input: interaction_input.as_deref(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Rollout-trace representation of an exec end event.
|
||||
///
|
||||
/// Like [`ExecCommandBeginTracePayload`], this renders `cwd` as an inferred native path to preserve
|
||||
/// the on-disk format rather than serializing the internal `PathUri`.
|
||||
#[derive(Serialize)]
|
||||
struct ExecCommandEndTracePayload<'a> {
|
||||
call_id: &'a str,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
process_id: Option<&'a str>,
|
||||
turn_id: &'a str,
|
||||
completed_at_ms: i64,
|
||||
command: &'a [String],
|
||||
cwd: String,
|
||||
parsed_cmd: &'a [codex_protocol::parse_command::ParsedCommand],
|
||||
source: ExecCommandSource,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
interaction_input: Option<&'a str>,
|
||||
stdout: &'a str,
|
||||
stderr: &'a str,
|
||||
aggregated_output: &'a str,
|
||||
exit_code: i32,
|
||||
duration: Duration,
|
||||
formatted_output: &'a str,
|
||||
status: &'a ExecCommandStatus,
|
||||
}
|
||||
|
||||
impl<'a> From<&'a ExecCommandEndEvent> for ExecCommandEndTracePayload<'a> {
|
||||
fn from(event: &'a ExecCommandEndEvent) -> Self {
|
||||
let ExecCommandEndEvent {
|
||||
call_id,
|
||||
process_id,
|
||||
turn_id,
|
||||
completed_at_ms,
|
||||
command,
|
||||
cwd,
|
||||
parsed_cmd,
|
||||
source,
|
||||
interaction_input,
|
||||
stdout,
|
||||
stderr,
|
||||
aggregated_output,
|
||||
exit_code,
|
||||
duration,
|
||||
formatted_output,
|
||||
status,
|
||||
} = event;
|
||||
Self {
|
||||
call_id,
|
||||
process_id: process_id.as_deref(),
|
||||
turn_id,
|
||||
completed_at_ms: *completed_at_ms,
|
||||
command,
|
||||
cwd: cwd.inferred_native_path_string(),
|
||||
parsed_cmd,
|
||||
source: *source,
|
||||
interaction_input: interaction_input.as_deref(),
|
||||
stdout,
|
||||
stderr,
|
||||
aggregated_output,
|
||||
exit_code: *exit_code,
|
||||
duration: *duration,
|
||||
formatted_output,
|
||||
status,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn tool_runtime_trace_event(event: &EventMsg) -> Option<ToolRuntimeTraceEvent<'_>> {
|
||||
match event {
|
||||
EventMsg::ExecCommandBegin(event) if event.source != ExecCommandSource::UserShell => {
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
use codex_protocol::AgentPath;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::protocol::EventMsg;
|
||||
use codex_protocol::protocol::ExecCommandBeginEvent;
|
||||
use codex_protocol::protocol::ExecCommandEndEvent;
|
||||
use codex_protocol::protocol::ExecCommandSource;
|
||||
use codex_protocol::protocol::ExecCommandStatus;
|
||||
use codex_protocol::protocol::SubAgentActivityEvent;
|
||||
use codex_protocol::protocol::SubAgentActivityKind;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::json;
|
||||
use std::time::Duration;
|
||||
|
||||
use super::ToolRuntimeTraceEvent;
|
||||
use super::tool_runtime_trace_event;
|
||||
@@ -44,3 +49,81 @@ fn sub_agent_activity_is_a_terminal_tool_runtime_event() -> anyhow::Result<()> {
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exec_command_trace_payloads_use_inferred_native_cwd() -> anyhow::Result<()> {
|
||||
// Convention inference depends on the URI spelling, not the test host, so exercise both
|
||||
// Windows and POSIX paths on every platform.
|
||||
let begin = EventMsg::ExecCommandBegin(ExecCommandBeginEvent {
|
||||
call_id: "call-begin".to_string(),
|
||||
process_id: Some("process-1".to_string()),
|
||||
turn_id: "turn-1".to_string(),
|
||||
started_at_ms: 1234,
|
||||
command: vec!["pwd".to_string()],
|
||||
cwd: "file:///C:/windows".parse()?,
|
||||
parsed_cmd: Vec::new(),
|
||||
source: ExecCommandSource::Agent,
|
||||
interaction_input: None,
|
||||
});
|
||||
let end = EventMsg::ExecCommandEnd(ExecCommandEndEvent {
|
||||
call_id: "call-end".to_string(),
|
||||
process_id: None,
|
||||
turn_id: "turn-1".to_string(),
|
||||
completed_at_ms: 2345,
|
||||
command: vec!["pwd".to_string()],
|
||||
cwd: "file:///workspace/project".parse()?,
|
||||
parsed_cmd: Vec::new(),
|
||||
source: ExecCommandSource::UnifiedExecInteraction,
|
||||
interaction_input: Some("input".to_string()),
|
||||
stdout: "output".to_string(),
|
||||
stderr: String::new(),
|
||||
aggregated_output: "output".to_string(),
|
||||
exit_code: 0,
|
||||
duration: Duration::from_millis(250),
|
||||
formatted_output: "output".to_string(),
|
||||
status: ExecCommandStatus::Completed,
|
||||
});
|
||||
|
||||
let Some(ToolRuntimeTraceEvent::Started { payload, .. }) = tool_runtime_trace_event(&begin)
|
||||
else {
|
||||
panic!("expected started tool runtime event");
|
||||
};
|
||||
assert_eq!(
|
||||
serde_json::to_value(payload)?,
|
||||
json!({
|
||||
"call_id": "call-begin",
|
||||
"process_id": "process-1",
|
||||
"turn_id": "turn-1",
|
||||
"started_at_ms": 1234,
|
||||
"command": ["pwd"],
|
||||
"cwd": r"C:\windows",
|
||||
"parsed_cmd": [],
|
||||
"source": "agent"
|
||||
})
|
||||
);
|
||||
|
||||
let Some(ToolRuntimeTraceEvent::Ended { payload, .. }) = tool_runtime_trace_event(&end) else {
|
||||
panic!("expected ended tool runtime event");
|
||||
};
|
||||
assert_eq!(
|
||||
serde_json::to_value(payload)?,
|
||||
json!({
|
||||
"call_id": "call-end",
|
||||
"turn_id": "turn-1",
|
||||
"completed_at_ms": 2345,
|
||||
"command": ["pwd"],
|
||||
"cwd": "/workspace/project",
|
||||
"parsed_cmd": [],
|
||||
"source": "unified_exec_interaction",
|
||||
"interaction_input": "input",
|
||||
"stdout": "output",
|
||||
"stderr": "",
|
||||
"aggregated_output": "output",
|
||||
"exit_code": 0,
|
||||
"duration": {"secs": 0, "nanos": 250000000},
|
||||
"formatted_output": "output",
|
||||
"status": "completed"
|
||||
})
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -12,7 +12,9 @@ fn agent_status_uses_bounded_buffered_activity() {
|
||||
item: ThreadItem::CommandExecution {
|
||||
id: "command-1".to_string(),
|
||||
command: "cargo test -p codex-tui".to_string(),
|
||||
cwd: AbsolutePathBuf::try_from("/workspace").expect("absolute path"),
|
||||
cwd: AbsolutePathBuf::try_from("/workspace")
|
||||
.expect("absolute path")
|
||||
.into(),
|
||||
process_id: None,
|
||||
source: CommandExecutionSource::Agent,
|
||||
status: CommandExecutionStatus::Completed,
|
||||
|
||||
@@ -619,7 +619,7 @@ mod tests {
|
||||
reason: None,
|
||||
network_approval_context: None,
|
||||
command: Some("echo hi".to_string()),
|
||||
cwd: Some(test_path_buf("/tmp").abs()),
|
||||
cwd: Some(test_path_buf("/tmp").abs().into()),
|
||||
command_actions: None,
|
||||
additional_permissions: None,
|
||||
proposed_execpolicy_amendment: None,
|
||||
|
||||
@@ -4697,7 +4697,7 @@ fn exec_approval_request(
|
||||
reason: Some("needs approval".to_string()),
|
||||
network_approval_context: None,
|
||||
command: Some("echo hello".to_string()),
|
||||
cwd: Some(test_path_buf("/tmp/project").abs()),
|
||||
cwd: Some(test_path_buf("/tmp/project").abs().into()),
|
||||
command_actions: None,
|
||||
additional_permissions: None,
|
||||
proposed_execpolicy_amendment: None,
|
||||
|
||||
@@ -492,7 +492,7 @@ mod tests {
|
||||
reason: Some("needs approval".to_string()),
|
||||
network_approval_context: None,
|
||||
command: Some("echo hello".to_string()),
|
||||
cwd: Some(test_path_buf("/tmp/project").abs()),
|
||||
cwd: Some(test_path_buf("/tmp/project").abs().into()),
|
||||
command_actions: None,
|
||||
additional_permissions: None,
|
||||
proposed_execpolicy_amendment: None,
|
||||
|
||||
@@ -840,6 +840,12 @@ fn exec_approval_request_from_params(
|
||||
params: CommandExecutionRequestApprovalParams,
|
||||
fallback_cwd: &AbsolutePathBuf,
|
||||
) -> ExecApprovalRequestEvent {
|
||||
// TODO(anp): Keep this as PathUri once `tui::approval_events::ExecApprovalRequestEvent` and
|
||||
// approval rendering support foreign paths.
|
||||
let cwd = params
|
||||
.cwd
|
||||
.and_then(|cwd| cwd.to_inferred_abs_path())
|
||||
.unwrap_or_else(|| fallback_cwd.clone());
|
||||
ExecApprovalRequestEvent {
|
||||
call_id: params.item_id,
|
||||
command: params
|
||||
@@ -847,7 +853,7 @@ fn exec_approval_request_from_params(
|
||||
.as_deref()
|
||||
.map(split_command_string)
|
||||
.unwrap_or_default(),
|
||||
cwd: params.cwd.unwrap_or_else(|| fallback_cwd.clone()),
|
||||
cwd,
|
||||
reason: params.reason,
|
||||
network_approval_context: params.network_approval_context,
|
||||
additional_permissions: params.additional_permissions,
|
||||
|
||||
@@ -177,7 +177,7 @@ mod tests {
|
||||
ThreadItem::CommandExecution {
|
||||
id: call_id.to_string(),
|
||||
command: "true".to_string(),
|
||||
cwd: AbsolutePathBuf::current_dir().expect("current dir"),
|
||||
cwd: AbsolutePathBuf::current_dir().expect("current dir").into(),
|
||||
process_id: None,
|
||||
source: CommandExecutionSource::Agent,
|
||||
status: CommandExecutionStatus::InProgress,
|
||||
|
||||
@@ -521,7 +521,7 @@ async fn live_app_server_command_execution_strips_shell_wrapper() {
|
||||
item: AppServerThreadItem::CommandExecution {
|
||||
id: "cmd-1".to_string(),
|
||||
command: command.clone(),
|
||||
cwd: test_path_buf("/tmp").abs(),
|
||||
cwd: test_path_buf("/tmp").abs().into(),
|
||||
process_id: None,
|
||||
source: AppServerCommandExecutionSource::UserShell,
|
||||
status: AppServerCommandExecutionStatus::InProgress,
|
||||
@@ -543,7 +543,7 @@ async fn live_app_server_command_execution_strips_shell_wrapper() {
|
||||
item: AppServerThreadItem::CommandExecution {
|
||||
id: "cmd-1".to_string(),
|
||||
command,
|
||||
cwd: test_path_buf("/tmp").abs(),
|
||||
cwd: test_path_buf("/tmp").abs().into(),
|
||||
process_id: None,
|
||||
source: AppServerCommandExecutionSource::UserShell,
|
||||
status: AppServerCommandExecutionStatus::Completed,
|
||||
|
||||
@@ -65,7 +65,7 @@ fn app_server_exec_approval_request_splits_shell_wrapped_command() {
|
||||
shlex::try_join(["/bin/zsh", "-lc", script])
|
||||
.expect("round-trippable shell wrapper"),
|
||||
),
|
||||
cwd: Some(test_path_buf("/tmp").abs()),
|
||||
cwd: Some(test_path_buf("/tmp").abs().into()),
|
||||
command_actions: None,
|
||||
additional_permissions: None,
|
||||
proposed_execpolicy_amendment: None,
|
||||
@@ -107,7 +107,7 @@ fn app_server_exec_approval_request_preserves_permissions_context() {
|
||||
protocol: codex_app_server_protocol::NetworkApprovalProtocol::Socks5Tcp,
|
||||
}),
|
||||
command: Some("ls".to_string()),
|
||||
cwd: Some(test_path_buf("/tmp").abs()),
|
||||
cwd: Some(test_path_buf("/tmp").abs().into()),
|
||||
command_actions: None,
|
||||
additional_permissions: Some(AppServerAdditionalPermissionProfile {
|
||||
network: Some(AppServerAdditionalNetworkPermissions {
|
||||
|
||||
@@ -64,7 +64,7 @@ fn app_server_exec_approval_request_splits_shell_wrapped_command() {
|
||||
shlex::try_join(["/bin/zsh", "-lc", script])
|
||||
.expect("round-trippable shell wrapper"),
|
||||
),
|
||||
cwd: Some(test_path_buf("/tmp").abs()),
|
||||
cwd: Some(test_path_buf("/tmp").abs().into()),
|
||||
command_actions: None,
|
||||
additional_permissions: None,
|
||||
proposed_execpolicy_amendment: None,
|
||||
@@ -363,7 +363,7 @@ async fn exec_end_without_begin_uses_event_command() {
|
||||
AppServerThreadItem::CommandExecution {
|
||||
id: "call-orphan".to_string(),
|
||||
command: codex_shell_command::parse_command::shlex_join(&command),
|
||||
cwd,
|
||||
cwd: cwd.into(),
|
||||
process_id: None,
|
||||
source: ExecCommandSource::Agent,
|
||||
status: AppServerCommandExecutionStatus::Completed,
|
||||
|
||||
@@ -821,7 +821,7 @@ pub(super) fn begin_exec_with_source(
|
||||
let item = AppServerThreadItem::CommandExecution {
|
||||
id: call_id.to_string(),
|
||||
command: codex_shell_command::parse_command::shlex_join(&command),
|
||||
cwd: chat.config.cwd.clone(),
|
||||
cwd: chat.config.cwd.clone().into(),
|
||||
process_id: None,
|
||||
source,
|
||||
status: AppServerCommandExecutionStatus::InProgress,
|
||||
@@ -844,7 +844,7 @@ pub(super) fn begin_unified_exec_startup(
|
||||
let item = AppServerThreadItem::CommandExecution {
|
||||
id: call_id.to_string(),
|
||||
command: codex_shell_command::parse_command::shlex_join(&command),
|
||||
cwd: chat.config.cwd.clone(),
|
||||
cwd: chat.config.cwd.clone().into(),
|
||||
process_id: Some(process_id.to_string()),
|
||||
source: ExecCommandSource::UnifiedExecStartup,
|
||||
status: AppServerCommandExecutionStatus::InProgress,
|
||||
|
||||
@@ -3614,7 +3614,7 @@ async fn chatwidget_exec_and_status_layout_vt100_snapshot() {
|
||||
AppServerThreadItem::CommandExecution {
|
||||
id: "c1".into(),
|
||||
command: codex_shell_command::parse_command::shlex_join(&command),
|
||||
cwd: cwd.clone(),
|
||||
cwd: cwd.clone().into(),
|
||||
process_id: None,
|
||||
source: ExecCommandSource::Agent,
|
||||
status: AppServerCommandExecutionStatus::InProgress,
|
||||
@@ -3629,7 +3629,7 @@ async fn chatwidget_exec_and_status_layout_vt100_snapshot() {
|
||||
AppServerThreadItem::CommandExecution {
|
||||
id: "c1".into(),
|
||||
command: codex_shell_command::parse_command::shlex_join(&command),
|
||||
cwd,
|
||||
cwd: cwd.into(),
|
||||
process_id: None,
|
||||
source: ExecCommandSource::Agent,
|
||||
status: AppServerCommandExecutionStatus::Completed,
|
||||
|
||||
@@ -70,11 +70,21 @@ impl LegacyAppPathString {
|
||||
PathUri::from_absolute_native_path(&self.0, convention).ok_or_else(|| {
|
||||
LegacyAppPathStringError::InvalidNativePath {
|
||||
path: self.0.clone(),
|
||||
convention,
|
||||
convention: Some(convention),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Parses this API string as an absolute path using the convention inferred from its spelling.
|
||||
pub fn to_inferred_path_uri(&self) -> Option<PathUri> {
|
||||
PathUri::try_from(self.clone()).ok()
|
||||
}
|
||||
|
||||
/// Parses this API string as a host-native absolute path.
|
||||
pub fn to_inferred_abs_path(&self) -> Option<AbsolutePathBuf> {
|
||||
AbsolutePathBuf::try_from(self.clone()).ok()
|
||||
}
|
||||
|
||||
/// Infers the path convention of an absolute API path from its spelling.
|
||||
///
|
||||
/// Relative paths and ambiguous spellings return `None`. In particular,
|
||||
@@ -111,6 +121,44 @@ impl From<AbsolutePathBuf> for LegacyAppPathString {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PathUri> for LegacyAppPathString {
|
||||
fn from(path: PathUri) -> Self {
|
||||
Self(path.inferred_native_path_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<LegacyAppPathString> for PathUri {
|
||||
type Error = LegacyAppPathStringError;
|
||||
|
||||
fn try_from(path: LegacyAppPathString) -> Result<Self, Self::Error> {
|
||||
let Some(convention) = path.infer_absolute_path_convention() else {
|
||||
return Err(LegacyAppPathStringError::InvalidNativePath {
|
||||
path: path.0,
|
||||
convention: None,
|
||||
});
|
||||
};
|
||||
PathUri::from_absolute_native_path(path.as_str(), convention).ok_or({
|
||||
LegacyAppPathStringError::InvalidNativePath {
|
||||
path: path.0,
|
||||
convention: Some(convention),
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<LegacyAppPathString> for AbsolutePathBuf {
|
||||
type Error = LegacyAppPathStringError;
|
||||
|
||||
fn try_from(path: LegacyAppPathString) -> Result<Self, Self::Error> {
|
||||
AbsolutePathBuf::from_absolute_path_checked(path.as_str()).map_err(|_| {
|
||||
LegacyAppPathStringError::InvalidNativePath {
|
||||
path: path.0,
|
||||
convention: None,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn render_opaque_fallback(
|
||||
path: &PathUri,
|
||||
path_bytes: &[u8],
|
||||
@@ -279,10 +327,13 @@ pub enum LegacyAppPathStringError {
|
||||
path: String,
|
||||
convention: PathConvention,
|
||||
},
|
||||
#[error("path `{path}` is not absolute using {convention} path syntax")]
|
||||
#[error(
|
||||
"path `{path}` is not absolute{convention}",
|
||||
convention = .convention.map(|convention| format!(" using {convention} path syntax")).unwrap_or_default()
|
||||
)]
|
||||
InvalidNativePath {
|
||||
path: String,
|
||||
convention: PathConvention,
|
||||
convention: Option<PathConvention>,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -369,7 +369,21 @@ fn relative_api_path_is_invalid_when_converted_to_a_path_uri() {
|
||||
path.to_path_uri(PathConvention::Posix),
|
||||
Err(LegacyAppPathStringError::InvalidNativePath {
|
||||
path: raw_path.to_string(),
|
||||
convention: PathConvention::Posix,
|
||||
convention: Some(PathConvention::Posix),
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
PathUri::try_from(path.clone()),
|
||||
Err(LegacyAppPathStringError::InvalidNativePath {
|
||||
path: raw_path.to_string(),
|
||||
convention: None,
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
AbsolutePathBuf::try_from(path),
|
||||
Err(LegacyAppPathStringError::InvalidNativePath {
|
||||
path: raw_path.to_string(),
|
||||
convention: None,
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -388,7 +402,7 @@ fn other_non_absolute_api_paths_cannot_be_converted_to_path_uris() {
|
||||
path.to_path_uri(convention),
|
||||
Err(LegacyAppPathStringError::InvalidNativePath {
|
||||
path: raw_path.to_string(),
|
||||
convention,
|
||||
convention: Some(convention),
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -423,6 +437,51 @@ fn infers_absolute_path_conventions_from_api_text() {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn converts_absolute_api_paths_using_the_inferred_convention() {
|
||||
for (raw_path, convention, expected_uri) in [
|
||||
(
|
||||
r"C:\workspace\file.rs",
|
||||
PathConvention::Windows,
|
||||
"file:///C:/workspace/file.rs",
|
||||
),
|
||||
(
|
||||
"/workspace/file.rs",
|
||||
PathConvention::Posix,
|
||||
"file:///workspace/file.rs",
|
||||
),
|
||||
] {
|
||||
let path = serde_json::from_value::<LegacyAppPathString>(serde_json::json!(raw_path))
|
||||
.expect("absolute API path should deserialize");
|
||||
|
||||
assert_eq!(
|
||||
path.to_inferred_path_uri(),
|
||||
Some(PathUri::parse(expected_uri).expect("expected URI should parse")),
|
||||
);
|
||||
assert_eq!(
|
||||
PathUri::try_from(path.clone()),
|
||||
path.to_path_uri(convention)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn converts_native_api_path_to_inferred_absolute_path() {
|
||||
#[cfg(windows)]
|
||||
let raw_path = r"C:\workspace\file.rs";
|
||||
#[cfg(not(windows))]
|
||||
let raw_path = "/workspace/file.rs";
|
||||
let path = serde_json::from_value::<LegacyAppPathString>(serde_json::json!(raw_path))
|
||||
.expect("absolute API path should deserialize");
|
||||
let expected = AbsolutePathBuf::try_from(raw_path).expect("native absolute path should parse");
|
||||
|
||||
assert_eq!(
|
||||
AbsolutePathBuf::try_from(path.clone()),
|
||||
Ok(expected.clone())
|
||||
);
|
||||
assert_eq!(path.to_inferred_abs_path(), Some(expected));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn foreign_absolute_syntax_deserializes_without_host_interpretation() {
|
||||
for (raw_path, convention) in [
|
||||
|
||||
@@ -119,6 +119,11 @@ fn inferred_native_path_string_uses_the_inferred_convention() {
|
||||
expected,
|
||||
"rendering {uri}"
|
||||
);
|
||||
assert_eq!(
|
||||
LegacyAppPathString::from(path).as_str(),
|
||||
expected,
|
||||
"rendering typed API path {uri}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user