mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
core: load AGENTS.md from foreign environments (#28958)
## Why Make it possible to load AGENTS.md from remote exec-servers whose OS is different than app-server. ## What - keep `AGENTS.md` discovery and provenance as `PathUri`, with root-aware parent and ancestor traversal - expose lifecycle instruction sources as legacy app-server path strings in events while retaining `PathUri` internally - preserve and test mixed POSIX and Windows paths in model context and TUI status output - cover remote Windows loading end to end by seeding the Wine prefix through host filesystem APIs - fix bug in `PathUri`'s parent() implementation that would erase Windows drive letters
This commit is contained in:
committed by
GitHub
Unverified
parent
406062c3af
commit
dce673905a
@@ -105,6 +105,14 @@ impl WineTestCommand {
|
||||
}
|
||||
|
||||
impl WineTestProcess {
|
||||
/// Returns the host path to this process's isolated Wine prefix.
|
||||
pub fn prefix_path(&self) -> &Path {
|
||||
let Some(processes) = self.processes.as_ref() else {
|
||||
panic!("Wine process guard is missing");
|
||||
};
|
||||
processes.prefix.path()
|
||||
}
|
||||
|
||||
/// Takes the piped standard output of the Wine process.
|
||||
///
|
||||
/// This may only be called once for a process created by
|
||||
|
||||
+6
-6
@@ -16973,9 +16973,9 @@
|
||||
},
|
||||
"instructionSources": {
|
||||
"default": [],
|
||||
"description": "Instruction source files currently loaded for this thread.",
|
||||
"description": "Environment-native paths to instruction source files currently loaded for this thread.",
|
||||
"items": {
|
||||
"$ref": "#/definitions/v2/AbsolutePathBuf"
|
||||
"$ref": "#/definitions/v2/LegacyAppPathString"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
@@ -18688,9 +18688,9 @@
|
||||
},
|
||||
"instructionSources": {
|
||||
"default": [],
|
||||
"description": "Instruction source files currently loaded for this thread.",
|
||||
"description": "Environment-native paths to instruction source files currently loaded for this thread.",
|
||||
"items": {
|
||||
"$ref": "#/definitions/v2/AbsolutePathBuf"
|
||||
"$ref": "#/definitions/v2/LegacyAppPathString"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
@@ -19105,9 +19105,9 @@
|
||||
},
|
||||
"instructionSources": {
|
||||
"default": [],
|
||||
"description": "Instruction source files currently loaded for this thread.",
|
||||
"description": "Environment-native paths to instruction source files currently loaded for this thread.",
|
||||
"items": {
|
||||
"$ref": "#/definitions/v2/AbsolutePathBuf"
|
||||
"$ref": "#/definitions/v2/LegacyAppPathString"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
|
||||
+6
-6
@@ -14752,9 +14752,9 @@
|
||||
},
|
||||
"instructionSources": {
|
||||
"default": [],
|
||||
"description": "Instruction source files currently loaded for this thread.",
|
||||
"description": "Environment-native paths to instruction source files currently loaded for this thread.",
|
||||
"items": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
"$ref": "#/definitions/LegacyAppPathString"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
@@ -16467,9 +16467,9 @@
|
||||
},
|
||||
"instructionSources": {
|
||||
"default": [],
|
||||
"description": "Instruction source files currently loaded for this thread.",
|
||||
"description": "Environment-native paths to instruction source files currently loaded for this thread.",
|
||||
"items": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
"$ref": "#/definitions/LegacyAppPathString"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
@@ -16884,9 +16884,9 @@
|
||||
},
|
||||
"instructionSources": {
|
||||
"default": [],
|
||||
"description": "Instruction source files currently loaded for this thread.",
|
||||
"description": "Environment-native paths to instruction source files currently loaded for this thread.",
|
||||
"items": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
"$ref": "#/definitions/LegacyAppPathString"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
|
||||
@@ -2368,9 +2368,9 @@
|
||||
},
|
||||
"instructionSources": {
|
||||
"default": [],
|
||||
"description": "Instruction source files currently loaded for this thread.",
|
||||
"description": "Environment-native paths to instruction source files currently loaded for this thread.",
|
||||
"items": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
"$ref": "#/definitions/LegacyAppPathString"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
|
||||
@@ -2394,9 +2394,9 @@
|
||||
},
|
||||
"instructionSources": {
|
||||
"default": [],
|
||||
"description": "Instruction source files currently loaded for this thread.",
|
||||
"description": "Environment-native paths to instruction source files currently loaded for this thread.",
|
||||
"items": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
"$ref": "#/definitions/LegacyAppPathString"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
|
||||
@@ -2368,9 +2368,9 @@
|
||||
},
|
||||
"instructionSources": {
|
||||
"default": [],
|
||||
"description": "Instruction source files currently loaded for this thread.",
|
||||
"description": "Environment-native paths to instruction source files currently loaded for this thread.",
|
||||
"items": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
"$ref": "#/definitions/LegacyAppPathString"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
|
||||
@@ -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 { ReasoningEffort } from "../ReasoningEffort";
|
||||
import type { ApprovalsReviewer } from "./ApprovalsReviewer";
|
||||
import type { AskForApproval } from "./AskForApproval";
|
||||
@@ -9,9 +10,9 @@ import type { SandboxPolicy } from "./SandboxPolicy";
|
||||
import type { Thread } from "./Thread";
|
||||
|
||||
export type ThreadForkResponse = {thread: Thread, model: string, modelProvider: string, serviceTier: string | null, cwd: AbsolutePathBuf, /**
|
||||
* Instruction source files currently loaded for this thread.
|
||||
* Environment-native paths to instruction source files currently loaded for this thread.
|
||||
*/
|
||||
instructionSources: Array<AbsolutePathBuf>, approvalPolicy: AskForApproval, /**
|
||||
instructionSources: Array<LegacyAppPathString>, approvalPolicy: AskForApproval, /**
|
||||
* Reviewer currently used for approval requests on this thread.
|
||||
*/
|
||||
approvalsReviewer: ApprovalsReviewer, /**
|
||||
|
||||
@@ -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 { ReasoningEffort } from "../ReasoningEffort";
|
||||
import type { ApprovalsReviewer } from "./ApprovalsReviewer";
|
||||
import type { AskForApproval } from "./AskForApproval";
|
||||
@@ -9,9 +10,9 @@ import type { SandboxPolicy } from "./SandboxPolicy";
|
||||
import type { Thread } from "./Thread";
|
||||
|
||||
export type ThreadResumeResponse = {thread: Thread, model: string, modelProvider: string, serviceTier: string | null, cwd: AbsolutePathBuf, /**
|
||||
* Instruction source files currently loaded for this thread.
|
||||
* Environment-native paths to instruction source files currently loaded for this thread.
|
||||
*/
|
||||
instructionSources: Array<AbsolutePathBuf>, approvalPolicy: AskForApproval, /**
|
||||
instructionSources: Array<LegacyAppPathString>, approvalPolicy: AskForApproval, /**
|
||||
* Reviewer currently used for approval requests on this thread.
|
||||
*/
|
||||
approvalsReviewer: ApprovalsReviewer, /**
|
||||
|
||||
@@ -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 { ReasoningEffort } from "../ReasoningEffort";
|
||||
import type { ApprovalsReviewer } from "./ApprovalsReviewer";
|
||||
import type { AskForApproval } from "./AskForApproval";
|
||||
@@ -9,9 +10,9 @@ import type { SandboxPolicy } from "./SandboxPolicy";
|
||||
import type { Thread } from "./Thread";
|
||||
|
||||
export type ThreadStartResponse = {thread: Thread, model: string, modelProvider: string, serviceTier: string | null, cwd: AbsolutePathBuf, /**
|
||||
* Instruction source files currently loaded for this thread.
|
||||
* Environment-native paths to instruction source files currently loaded for this thread.
|
||||
*/
|
||||
instructionSources: Array<AbsolutePathBuf>, approvalPolicy: AskForApproval, /**
|
||||
instructionSources: Array<LegacyAppPathString>, approvalPolicy: AskForApproval, /**
|
||||
* Reviewer currently used for approval requests on this thread.
|
||||
*/
|
||||
approvalsReviewer: ApprovalsReviewer, /**
|
||||
|
||||
@@ -2573,7 +2573,11 @@ mod tests {
|
||||
service_tier: None,
|
||||
cwd,
|
||||
runtime_workspace_roots: Vec::new(),
|
||||
instruction_sources: vec![absolute_path("/tmp/AGENTS.md")],
|
||||
instruction_sources: vec![
|
||||
codex_utils_path_uri::LegacyAppPathString::from_abs_path(&absolute_path(
|
||||
"/tmp/AGENTS.md",
|
||||
)),
|
||||
],
|
||||
approval_policy: v2::AskForApproval::OnFailure,
|
||||
approvals_reviewer: v2::ApprovalsReviewer::User,
|
||||
sandbox: v2::SandboxPolicy::DangerFullAccess,
|
||||
|
||||
@@ -36,6 +36,7 @@ use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_absolute_path::test_support::PathBufExt;
|
||||
use codex_utils_absolute_path::test_support::test_path_buf;
|
||||
use codex_utils_path_uri::LegacyAppPathString;
|
||||
use codex_utils_path_uri::PathUri;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::Value as JsonValue;
|
||||
use serde_json::json;
|
||||
@@ -3685,17 +3686,49 @@ fn thread_lifecycle_responses_default_missing_optional_fields() {
|
||||
serde_json::from_value(response.clone()).expect("thread/start response");
|
||||
let resume: ThreadResumeResponse =
|
||||
serde_json::from_value(response.clone()).expect("thread/resume response");
|
||||
let fork: ThreadForkResponse = serde_json::from_value(response).expect("thread/fork response");
|
||||
let fork: ThreadForkResponse =
|
||||
serde_json::from_value(response.clone()).expect("thread/fork response");
|
||||
|
||||
assert_eq!(start.instruction_sources, Vec::<AbsolutePathBuf>::new());
|
||||
assert_eq!(start.instruction_sources, Vec::<LegacyAppPathString>::new());
|
||||
assert_eq!(start.thread.parent_thread_id, None);
|
||||
assert_eq!(start.thread.recency_at, None);
|
||||
assert_eq!(resume.instruction_sources, Vec::<AbsolutePathBuf>::new());
|
||||
assert_eq!(fork.instruction_sources, Vec::<AbsolutePathBuf>::new());
|
||||
assert_eq!(
|
||||
resume.instruction_sources,
|
||||
Vec::<LegacyAppPathString>::new()
|
||||
);
|
||||
assert_eq!(fork.instruction_sources, Vec::<LegacyAppPathString>::new());
|
||||
assert_eq!(start.active_permission_profile, None);
|
||||
assert_eq!(resume.active_permission_profile, None);
|
||||
assert_eq!(resume.initial_turns_page, None);
|
||||
assert_eq!(fork.active_permission_profile, None);
|
||||
|
||||
let foreign_source: LegacyAppPathString =
|
||||
serde_json::from_value(json!(r"C:\workspace\AGENTS.md")).expect("foreign source");
|
||||
let mut response_with_foreign_source = response;
|
||||
response_with_foreign_source["instructionSources"] = json!([foreign_source.as_str()]);
|
||||
let start: ThreadStartResponse = serde_json::from_value(response_with_foreign_source.clone())
|
||||
.expect("thread/start response with foreign source");
|
||||
let resume: ThreadResumeResponse = serde_json::from_value(response_with_foreign_source.clone())
|
||||
.expect("thread/resume response with foreign source");
|
||||
let fork: ThreadForkResponse = serde_json::from_value(response_with_foreign_source)
|
||||
.expect("thread/fork response with foreign source");
|
||||
assert_eq!(start.instruction_sources, vec![foreign_source.clone()]);
|
||||
assert_eq!(resume.instruction_sources, vec![foreign_source.clone()]);
|
||||
assert_eq!(fork.instruction_sources, vec![foreign_source]);
|
||||
let foreign_source_uri =
|
||||
PathUri::parse("file:///C:/workspace/AGENTS.md").expect("foreign source URI");
|
||||
assert_eq!(
|
||||
start.instruction_source_path_uris(),
|
||||
vec![foreign_source_uri.clone()]
|
||||
);
|
||||
assert_eq!(
|
||||
resume.instruction_source_path_uris(),
|
||||
vec![foreign_source_uri.clone()]
|
||||
);
|
||||
assert_eq!(
|
||||
fork.instruction_source_path_uris(),
|
||||
vec![foreign_source_uri]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -26,6 +26,8 @@ use codex_protocol::protocol::ThreadGoalStatus as CoreThreadGoalStatus;
|
||||
use codex_protocol::protocol::TokenUsage as CoreTokenUsage;
|
||||
use codex_protocol::protocol::TokenUsageInfo as CoreTokenUsageInfo;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_path_uri::LegacyAppPathString;
|
||||
use codex_utils_path_uri::PathUri;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
@@ -161,9 +163,9 @@ pub struct ThreadStartResponse {
|
||||
#[experimental("thread/start.runtimeWorkspaceRoots")]
|
||||
#[serde(default)]
|
||||
pub runtime_workspace_roots: Vec<AbsolutePathBuf>,
|
||||
/// Instruction source files currently loaded for this thread.
|
||||
/// Environment-native paths to instruction source files currently loaded for this thread.
|
||||
#[serde(default)]
|
||||
pub instruction_sources: Vec<AbsolutePathBuf>,
|
||||
pub instruction_sources: Vec<LegacyAppPathString>,
|
||||
#[experimental(nested)]
|
||||
pub approval_policy: AskForApproval,
|
||||
/// Reviewer currently used for approval requests on this thread.
|
||||
@@ -179,6 +181,13 @@ pub struct ThreadStartResponse {
|
||||
pub reasoning_effort: Option<ReasoningEffort>,
|
||||
}
|
||||
|
||||
impl ThreadStartResponse {
|
||||
/// Parses valid absolute instruction source paths and omits malformed legacy values.
|
||||
pub fn instruction_source_path_uris(&self) -> Vec<PathUri> {
|
||||
instruction_source_path_uris(&self.instruction_sources)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Serialize, Deserialize, Debug, Default, Clone, PartialEq, JsonSchema, TS, ExperimentalApi,
|
||||
)]
|
||||
@@ -375,9 +384,9 @@ pub struct ThreadResumeResponse {
|
||||
#[experimental("thread/resume.runtimeWorkspaceRoots")]
|
||||
#[serde(default)]
|
||||
pub runtime_workspace_roots: Vec<AbsolutePathBuf>,
|
||||
/// Instruction source files currently loaded for this thread.
|
||||
/// Environment-native paths to instruction source files currently loaded for this thread.
|
||||
#[serde(default)]
|
||||
pub instruction_sources: Vec<AbsolutePathBuf>,
|
||||
pub instruction_sources: Vec<LegacyAppPathString>,
|
||||
#[experimental(nested)]
|
||||
pub approval_policy: AskForApproval,
|
||||
/// Reviewer currently used for approval requests on this thread.
|
||||
@@ -397,6 +406,13 @@ pub struct ThreadResumeResponse {
|
||||
pub initial_turns_page: Option<TurnsPage>,
|
||||
}
|
||||
|
||||
impl ThreadResumeResponse {
|
||||
/// Parses valid absolute instruction source paths and omits malformed legacy values.
|
||||
pub fn instruction_source_path_uris(&self) -> Vec<PathUri> {
|
||||
instruction_source_path_uris(&self.instruction_sources)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
@@ -523,9 +539,9 @@ pub struct ThreadForkResponse {
|
||||
#[experimental("thread/fork.runtimeWorkspaceRoots")]
|
||||
#[serde(default)]
|
||||
pub runtime_workspace_roots: Vec<AbsolutePathBuf>,
|
||||
/// Instruction source files currently loaded for this thread.
|
||||
/// Environment-native paths to instruction source files currently loaded for this thread.
|
||||
#[serde(default)]
|
||||
pub instruction_sources: Vec<AbsolutePathBuf>,
|
||||
pub instruction_sources: Vec<LegacyAppPathString>,
|
||||
#[experimental(nested)]
|
||||
pub approval_policy: AskForApproval,
|
||||
/// Reviewer currently used for approval requests on this thread.
|
||||
@@ -541,6 +557,30 @@ pub struct ThreadForkResponse {
|
||||
pub reasoning_effort: Option<ReasoningEffort>,
|
||||
}
|
||||
|
||||
impl ThreadForkResponse {
|
||||
/// Parses valid absolute instruction source paths and omits malformed legacy values.
|
||||
pub fn instruction_source_path_uris(&self) -> Vec<PathUri> {
|
||||
instruction_source_path_uris(&self.instruction_sources)
|
||||
}
|
||||
}
|
||||
|
||||
fn instruction_source_path_uris(sources: &[LegacyAppPathString]) -> Vec<PathUri> {
|
||||
// Instruction sources are advisory diagnostics. Warn and fail open so a malformed legacy
|
||||
// path cannot fail thread start, resume, or fork.
|
||||
sources
|
||||
.iter()
|
||||
.filter_map(|source| {
|
||||
source.to_inferred_path_uri().or_else(|| {
|
||||
tracing::warn!(
|
||||
path = source.as_str(),
|
||||
"ignoring invalid instruction source path from app-server"
|
||||
);
|
||||
None
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
|
||||
@@ -140,7 +140,7 @@ Example with notification opt-out:
|
||||
- `thread/start` — create a new thread; emits `thread/started` (including the current `thread.status`) and auto-subscribes you to turn/item events for that thread. When the request includes a `cwd` and the resolved sandbox is `workspace-write` or full access, app-server also marks that project as trusted in the user `config.toml`. Pass `sessionStartSource: "clear"` when starting a replacement thread after clearing the current session so `SessionStart` hooks receive `source: "clear"` instead of the default `"startup"`. Experimental `runtimeWorkspaceRoots` replaces the thread-scoped runtime workspace roots used to materialize `:workspace_roots`; paths must be absolute. For permissions, prefer experimental `permissions` profile selection by id; the legacy `sandbox` shorthand is still accepted but cannot be combined with `permissions`. Experimental `environments` selects the sticky execution environments for turns on the thread; omit it to use the server default, pass `[]` to disable environments, or pass explicit environment ids with per-environment `cwd`. Experimental `selectedCapabilityRoots` selects environment-owned plugin or standalone-skill roots. Skills found below those roots are listed and read through the owning environment. Stdio MCP servers declared by selected plugins are also started in that environment; HTTP MCP declarations remain inactive.
|
||||
- `thread/resume` — reopen an existing thread by id so subsequent `turn/start` calls append to it. Accepts the same permission override rules as `thread/start`.
|
||||
- `thread/fork` — fork an existing thread into a new thread id by copying the stored history; if the source thread is currently mid-turn, the fork records the same interruption marker as `turn/interrupt` instead of inheriting an unmarked partial turn suffix. The returned `thread.forkedFromId` points at the source thread when known. Accepts `ephemeral: true` for an in-memory temporary fork, emits `thread/started` (including the current `thread.status`), and auto-subscribes you to turn/item events for the new thread. Experimental clients can pass `excludeTurns: true` when they plan to page fork history via `thread/turns/list` instead of receiving the full turn array immediately. Accepts the same permission override rules as `thread/start`.
|
||||
- `thread/start`, `thread/resume`, and `thread/fork` responses include the legacy `sandbox` compatibility projection. Experimental clients can read `runtimeWorkspaceRoots` for the thread-scoped runtime roots and `activePermissionProfile` for the named or implicit built-in profile identity/provenance when known.
|
||||
- `thread/start`, `thread/resume`, and `thread/fork` responses include the legacy `sandbox` compatibility projection. `instructionSources` lists loaded instruction files using each source environment's native absolute path syntax, including files loaded from remote environments. Experimental clients can read `runtimeWorkspaceRoots` for the thread-scoped runtime roots and `activePermissionProfile` for the named or implicit built-in profile identity/provenance when known.
|
||||
- `thread/list` — page through stored threads; supports cursor-based pagination and optional `modelProviders`, `sourceKinds`, `archived`, `cwd`, and `searchTerm` filters. Experimental clients can use `parentThreadId` to filter direct spawned children represented by persisted spawn-edge state. Review and Guardian threads are not included because they do not participate in that spawn-edge lifecycle. Each returned `thread` includes `status` (`ThreadStatus`), defaulting to `notLoaded` when the thread is not currently loaded. Subagent threads also include `parentThreadId` when the immediate parent is known.
|
||||
- `thread/loaded/list` — list the thread ids currently loaded in memory.
|
||||
- `thread/read` — read a stored thread by id without resuming it; optionally include turns via `includeTurns`. The returned `thread` includes `status` (`ThreadStatus`), defaulting to `notLoaded` when the thread is not currently loaded.
|
||||
|
||||
@@ -1181,7 +1181,7 @@ impl ThreadRequestProcessor {
|
||||
)
|
||||
.await?;
|
||||
|
||||
let instruction_sources = thread.instruction_sources().await;
|
||||
let instruction_sources = thread.legacy_instruction_sources().await;
|
||||
let config_snapshot = thread
|
||||
.config_snapshot()
|
||||
.instrument(tracing::info_span!(
|
||||
@@ -2681,7 +2681,7 @@ impl ThreadRequestProcessor {
|
||||
self.outgoing.send_error(request_id, err).await;
|
||||
return Ok(());
|
||||
}
|
||||
let instruction_sources = codex_thread.instruction_sources().await;
|
||||
let instruction_sources = codex_thread.legacy_instruction_sources().await;
|
||||
let SessionConfiguredEvent { rollout_path, .. } = session_configured;
|
||||
let Some(rollout_path) = rollout_path else {
|
||||
let error =
|
||||
@@ -2989,7 +2989,7 @@ impl ThreadRequestProcessor {
|
||||
/*include_turns*/ false,
|
||||
);
|
||||
thread_summary.session_id = existing_thread.session_configured().session_id.to_string();
|
||||
let instruction_sources = existing_thread.instruction_sources().await;
|
||||
let instruction_sources = existing_thread.legacy_instruction_sources().await;
|
||||
|
||||
let listener_command_tx = {
|
||||
let thread_state = thread_state.lock().await;
|
||||
@@ -3422,7 +3422,7 @@ impl ThreadRequestProcessor {
|
||||
.map_err(|err| core_thread_write_error("inherit source thread name", err))?;
|
||||
}
|
||||
|
||||
let instruction_sources = forked_thread.instruction_sources().await;
|
||||
let instruction_sources = forked_thread.legacy_instruction_sources().await;
|
||||
|
||||
// Auto-attach a conversation listener when forking a thread.
|
||||
log_listener_attach_result(
|
||||
|
||||
@@ -13,7 +13,7 @@ use codex_protocol::ThreadId;
|
||||
use codex_protocol::protocol::EventMsg;
|
||||
use codex_protocol::protocol::RolloutItem;
|
||||
use codex_rollout::state_db::StateDbHandle;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_path_uri::LegacyAppPathString;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::HashSet;
|
||||
use std::sync::Arc;
|
||||
@@ -31,7 +31,7 @@ pub(crate) struct PendingThreadResumeRequest {
|
||||
pub(crate) request_id: ConnectionRequestId,
|
||||
pub(crate) history_items: Vec<RolloutItem>,
|
||||
pub(crate) config_snapshot: ThreadConfigSnapshot,
|
||||
pub(crate) instruction_sources: Vec<AbsolutePathBuf>,
|
||||
pub(crate) instruction_sources: Vec<LegacyAppPathString>,
|
||||
pub(crate) thread_summary: codex_app_server_protocol::Thread,
|
||||
pub(crate) emit_thread_goal_update: bool,
|
||||
pub(crate) thread_goal_state_db: Option<StateDbHandle>,
|
||||
@@ -200,6 +200,7 @@ mod tests {
|
||||
use codex_protocol::config_types::CollaborationMode;
|
||||
use codex_protocol::config_types::ModeKind;
|
||||
use codex_protocol::config_types::Settings;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -82,6 +82,7 @@ use codex_rollout::append_rollout_item_to_path;
|
||||
use codex_rollout::read_session_meta_line;
|
||||
use codex_state::StateRuntime;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_path_uri::LegacyAppPathString;
|
||||
use core_test_support::responses;
|
||||
use core_test_support::skip_if_no_network;
|
||||
use pretty_assertions::assert_eq;
|
||||
@@ -288,7 +289,8 @@ async fn thread_resume_running_thread_uses_cached_instruction_sources() -> Resul
|
||||
..
|
||||
} = to_response::<ThreadStartResponse>(start_resp)?;
|
||||
let project_agents = AbsolutePathBuf::try_from(project_agents)?;
|
||||
assert_eq!(instruction_sources, vec![project_agents.clone()]);
|
||||
let project_agents_source = LegacyAppPathString::from_abs_path(&project_agents);
|
||||
assert_eq!(instruction_sources, vec![project_agents_source.clone()]);
|
||||
|
||||
let turn_id = mcp
|
||||
.send_turn_start_request(TurnStartParams {
|
||||
@@ -330,7 +332,7 @@ async fn thread_resume_running_thread_uses_cached_instruction_sources() -> Resul
|
||||
..
|
||||
} = to_response::<ThreadResumeResponse>(resume_resp)?;
|
||||
|
||||
assert_eq!(instruction_sources, vec![project_agents]);
|
||||
assert_eq!(instruction_sources, vec![project_agents_source]);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -381,7 +381,7 @@ async fn thread_start_response_includes_loaded_instruction_sources() -> Result<(
|
||||
|
||||
let instruction_sources = instruction_sources
|
||||
.into_iter()
|
||||
.map(normalize_path_for_comparison)
|
||||
.map(|path| normalize_path_for_comparison(path.as_str()))
|
||||
.collect::<Vec<_>>();
|
||||
let expected_instruction_sources = vec![
|
||||
std::fs::canonicalize(global_agents_path)?,
|
||||
@@ -428,7 +428,7 @@ async fn thread_start_response_excludes_empty_project_instruction_source() -> Re
|
||||
|
||||
let instruction_sources = instruction_sources
|
||||
.into_iter()
|
||||
.map(normalize_path_for_comparison)
|
||||
.map(|path| normalize_path_for_comparison(path.as_str()))
|
||||
.collect::<Vec<_>>();
|
||||
let expected_instruction_sources = vec![normalize_path_for_comparison(std::fs::canonicalize(
|
||||
global_agents_path,
|
||||
@@ -474,7 +474,7 @@ async fn thread_start_without_selected_environment_includes_only_global_instruct
|
||||
assert_eq!(
|
||||
instruction_sources
|
||||
.into_iter()
|
||||
.map(normalize_path_for_comparison)
|
||||
.map(|path| normalize_path_for_comparison(path.as_str()))
|
||||
.collect::<Vec<_>>(),
|
||||
vec![normalize_path_for_comparison(std::fs::canonicalize(
|
||||
global_agents_path,
|
||||
|
||||
@@ -53,16 +53,11 @@ pub(crate) async fn load_project_instructions(
|
||||
let mut loaded = LoadedAgentsMd::from_user_instructions(user_instructions);
|
||||
for turn_environment in &environments.turn_environments {
|
||||
let filesystem = turn_environment.environment.get_filesystem();
|
||||
// TODO(anp): Migrate AGENTS.md discovery to PathUri so instructions can be loaded from
|
||||
// environment-native foreign working directories.
|
||||
let Ok(cwd) = turn_environment.cwd().to_abs_path() else {
|
||||
continue;
|
||||
};
|
||||
match read_agents_md(
|
||||
config,
|
||||
filesystem.as_ref(),
|
||||
&turn_environment.environment_id,
|
||||
&cwd,
|
||||
turn_environment.cwd(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -97,7 +92,7 @@ async fn read_agents_md(
|
||||
config: &Config,
|
||||
fs: &dyn ExecutorFileSystem,
|
||||
environment_id: &str,
|
||||
cwd: &AbsolutePathBuf,
|
||||
cwd: &PathUri,
|
||||
) -> io::Result<Option<LoadedAgentsMd>> {
|
||||
let max_total = config.project_doc_max_bytes;
|
||||
|
||||
@@ -118,15 +113,14 @@ async fn read_agents_md(
|
||||
break;
|
||||
}
|
||||
|
||||
let path_uri = PathUri::from_abs_path(&p);
|
||||
match fs.get_metadata(&path_uri, /*sandbox*/ None).await {
|
||||
match fs.get_metadata(&p, /*sandbox*/ None).await {
|
||||
Ok(metadata) if !metadata.is_file => continue,
|
||||
Ok(_) => {}
|
||||
Err(err) if err.kind() == io::ErrorKind::NotFound => continue,
|
||||
Err(err) => return Err(err),
|
||||
}
|
||||
|
||||
let mut data = match fs.read_file(&path_uri, /*sandbox*/ None).await {
|
||||
let mut data = match fs.read_file(&p, /*sandbox*/ None).await {
|
||||
Ok(data) => data,
|
||||
Err(err) if err.kind() == io::ErrorKind::NotFound => continue,
|
||||
Err(err) => return Err(err),
|
||||
@@ -139,7 +133,7 @@ async fn read_agents_md(
|
||||
if size > remaining {
|
||||
tracing::warn!(
|
||||
"Project doc `{}` exceeds remaining budget ({} bytes) - truncating.",
|
||||
p.display(),
|
||||
p.inferred_native_path_string(),
|
||||
remaining,
|
||||
);
|
||||
}
|
||||
@@ -169,9 +163,9 @@ async fn read_agents_md(
|
||||
/// directory, inclusive. Symlinks are allowed.
|
||||
async fn agents_md_paths(
|
||||
config: &Config,
|
||||
cwd: &AbsolutePathBuf,
|
||||
cwd: &PathUri,
|
||||
fs: &dyn ExecutorFileSystem,
|
||||
) -> io::Result<Vec<AbsolutePathBuf>> {
|
||||
) -> io::Result<Vec<PathUri>> {
|
||||
let dir = cwd.clone();
|
||||
|
||||
let mut merged = TomlValue::Table(toml::map::Map::new());
|
||||
@@ -194,18 +188,18 @@ async fn agents_md_paths(
|
||||
};
|
||||
let mut project_root = None;
|
||||
if !project_root_markers.is_empty() {
|
||||
for ancestor in dir.ancestors() {
|
||||
for current in dir.ancestors() {
|
||||
for marker in &project_root_markers {
|
||||
let marker_path = ancestor.join(marker);
|
||||
let marker_path_uri = PathUri::from_abs_path(&marker_path);
|
||||
let marker_exists = match fs.get_metadata(&marker_path_uri, /*sandbox*/ None).await
|
||||
{
|
||||
let marker_path = current
|
||||
.join(marker)
|
||||
.map_err(|err| io::Error::new(io::ErrorKind::InvalidInput, err))?;
|
||||
let marker_exists = match fs.get_metadata(&marker_path, /*sandbox*/ None).await {
|
||||
Ok(_) => true,
|
||||
Err(err) if err.kind() == io::ErrorKind::NotFound => false,
|
||||
Err(err) => return Err(err),
|
||||
};
|
||||
if marker_exists {
|
||||
project_root = Some(ancestor.clone());
|
||||
project_root = Some(current.clone());
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -215,7 +209,7 @@ async fn agents_md_paths(
|
||||
}
|
||||
}
|
||||
|
||||
let search_dirs: Vec<AbsolutePathBuf> = if let Some(root) = project_root {
|
||||
let search_dirs: Vec<PathUri> = if let Some(root) = project_root {
|
||||
let mut dirs = Vec::new();
|
||||
let mut cursor = dir.clone();
|
||||
loop {
|
||||
@@ -234,13 +228,14 @@ async fn agents_md_paths(
|
||||
vec![dir]
|
||||
};
|
||||
|
||||
let mut found: Vec<AbsolutePathBuf> = Vec::new();
|
||||
let mut found: Vec<PathUri> = Vec::new();
|
||||
let candidate_filenames = candidate_filenames(config);
|
||||
for d in search_dirs {
|
||||
for name in &candidate_filenames {
|
||||
let candidate = d.join(name);
|
||||
let candidate_uri = PathUri::from_abs_path(&candidate);
|
||||
match fs.get_metadata(&candidate_uri, /*sandbox*/ None).await {
|
||||
let candidate = d
|
||||
.join(name)
|
||||
.map_err(|err| io::Error::new(io::ErrorKind::InvalidInput, err))?;
|
||||
match fs.get_metadata(&candidate, /*sandbox*/ None).await {
|
||||
Ok(md) if md.is_file => {
|
||||
found.push(candidate);
|
||||
break;
|
||||
@@ -371,7 +366,7 @@ impl LoadedAgentsMd {
|
||||
fn environment_labeled_text(&self) -> String {
|
||||
let mut output = String::new();
|
||||
let mut has_previous = false;
|
||||
let mut previous_environment: Option<(&str, &AbsolutePathBuf)> = None;
|
||||
let mut previous_environment: Option<(&str, &PathUri)> = None;
|
||||
if let Some(instructions) = &self.user_instructions {
|
||||
output.push_str(&instructions.text);
|
||||
has_previous = true;
|
||||
@@ -394,7 +389,7 @@ impl LoadedAgentsMd {
|
||||
output.push_str(&format!(
|
||||
"for `{}` with root {}\n\n",
|
||||
environment_id,
|
||||
cwd.display()
|
||||
cwd.inferred_native_path_string()
|
||||
));
|
||||
}
|
||||
output.push_str(&entry.contents);
|
||||
@@ -421,7 +416,7 @@ impl LoadedAgentsMd {
|
||||
None
|
||||
} else {
|
||||
self.single_project_cwd()
|
||||
.map(|cwd| cwd.to_string_lossy().into_owned())
|
||||
.map(PathUri::inferred_native_path_string)
|
||||
};
|
||||
ContextUserInstructions {
|
||||
directory,
|
||||
@@ -436,10 +431,10 @@ impl LoadedAgentsMd {
|
||||
}
|
||||
|
||||
/// Returns the AGENTS.md files that supplied instruction entries.
|
||||
pub fn sources(&self) -> impl Iterator<Item = &AbsolutePathBuf> {
|
||||
pub fn sources(&self) -> impl Iterator<Item = PathUri> + '_ {
|
||||
self.user_instructions
|
||||
.iter()
|
||||
.map(|instructions| &instructions.source)
|
||||
.map(|instructions| PathUri::from_abs_path(&instructions.source))
|
||||
.chain(
|
||||
self.entries
|
||||
.iter()
|
||||
@@ -463,7 +458,7 @@ impl LoadedAgentsMd {
|
||||
})
|
||||
}
|
||||
|
||||
fn single_project_cwd(&self) -> Option<&AbsolutePathBuf> {
|
||||
fn single_project_cwd(&self) -> Option<&PathUri> {
|
||||
self.entries
|
||||
.iter()
|
||||
.find_map(|entry| match &entry.provenance {
|
||||
@@ -488,9 +483,9 @@ enum InstructionProvenance {
|
||||
/// Workspace instructions discovered from project AGENTS.md files.
|
||||
Project {
|
||||
/// Exact AGENTS.md file, distinct from the environment's selected cwd.
|
||||
source_path: AbsolutePathBuf,
|
||||
source_path: PathUri,
|
||||
environment_id: String,
|
||||
cwd: AbsolutePathBuf,
|
||||
cwd: PathUri,
|
||||
},
|
||||
|
||||
/// Instructions without a file source, including internally defined guidance.
|
||||
@@ -498,9 +493,9 @@ enum InstructionProvenance {
|
||||
}
|
||||
|
||||
impl InstructionProvenance {
|
||||
fn path(&self) -> Option<&AbsolutePathBuf> {
|
||||
fn path(&self) -> Option<PathUri> {
|
||||
match self {
|
||||
Self::Project { source_path, .. } => Some(source_path),
|
||||
Self::Project { source_path, .. } => Some(source_path.clone()),
|
||||
Self::Internal => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -250,8 +250,13 @@ async fn load_agents_md(config: &TestConfig) -> Option<LoadedAgentsMd> {
|
||||
.await
|
||||
}
|
||||
|
||||
async fn agents_md_paths(config: &TestConfig) -> std::io::Result<Vec<AbsolutePathBuf>> {
|
||||
super::agents_md_paths(&config.config, &config.cwd, LOCAL_FS.as_ref()).await
|
||||
async fn agents_md_paths(config: &TestConfig) -> std::io::Result<Vec<PathUri>> {
|
||||
super::agents_md_paths(
|
||||
&config.config,
|
||||
&PathUri::from_abs_path(&config.cwd),
|
||||
LOCAL_FS.as_ref(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
fn resolved_local_environments<const N: usize>(
|
||||
@@ -277,12 +282,101 @@ fn resolved_local_environments<const N: usize>(
|
||||
|
||||
fn project_provenance(path: AbsolutePathBuf, cwd: AbsolutePathBuf) -> InstructionProvenance {
|
||||
InstructionProvenance::Project {
|
||||
source_path: path,
|
||||
source_path: PathUri::from_abs_path(&path),
|
||||
environment_id: "local".to_string(),
|
||||
cwd,
|
||||
cwd: PathUri::from_abs_path(&cwd),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn foreign_agents_md_uses_environment_native_paths() {
|
||||
let (cwd, rendered_cwd) = if cfg!(windows) {
|
||||
(
|
||||
PathUri::parse("file:///codex%20runtime").expect("POSIX cwd URI"),
|
||||
"/codex runtime",
|
||||
)
|
||||
} else {
|
||||
(
|
||||
PathUri::parse("file:///C:/codex%20runtime").expect("Windows cwd URI"),
|
||||
r"C:\codex runtime",
|
||||
)
|
||||
};
|
||||
let source_path = cwd.join("AGENTS.md").expect("AGENTS.md URI");
|
||||
let loaded = LoadedAgentsMd {
|
||||
user_instructions: None,
|
||||
entries: vec![InstructionEntry {
|
||||
contents: "remote instructions".to_string(),
|
||||
provenance: InstructionProvenance::Project {
|
||||
source_path: source_path.clone(),
|
||||
environment_id: "remote".to_string(),
|
||||
cwd,
|
||||
},
|
||||
}],
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
loaded.render(),
|
||||
format!(
|
||||
"# AGENTS.md instructions for {rendered_cwd}
|
||||
|
||||
<INSTRUCTIONS>
|
||||
remote instructions
|
||||
</INSTRUCTIONS>"
|
||||
)
|
||||
);
|
||||
assert_eq!(loaded.sources().collect::<Vec<_>>(), vec![source_path]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multi_environment_agents_md_renders_mixed_path_conventions() {
|
||||
let posix_cwd = PathUri::parse("file:///srv/project").expect("POSIX cwd URI");
|
||||
let windows_cwd = PathUri::parse("file:///C:/workspace").expect("Windows cwd URI");
|
||||
let posix_source = posix_cwd.join("AGENTS.md").expect("POSIX AGENTS.md URI");
|
||||
let windows_source = windows_cwd
|
||||
.join("AGENTS.md")
|
||||
.expect("Windows AGENTS.md URI");
|
||||
let loaded = LoadedAgentsMd {
|
||||
user_instructions: None,
|
||||
entries: vec![
|
||||
InstructionEntry {
|
||||
contents: "POSIX instructions".to_string(),
|
||||
provenance: InstructionProvenance::Project {
|
||||
source_path: posix_source.clone(),
|
||||
environment_id: "posix".to_string(),
|
||||
cwd: posix_cwd,
|
||||
},
|
||||
},
|
||||
InstructionEntry {
|
||||
contents: "Windows instructions".to_string(),
|
||||
provenance: InstructionProvenance::Project {
|
||||
source_path: windows_source.clone(),
|
||||
environment_id: "windows".to_string(),
|
||||
cwd: windows_cwd,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
loaded.render(),
|
||||
r#"# AGENTS.md instructions
|
||||
|
||||
<INSTRUCTIONS>
|
||||
for `posix` with root /srv/project
|
||||
|
||||
POSIX instructions
|
||||
|
||||
for `windows` with root C:\workspace
|
||||
|
||||
Windows instructions
|
||||
</INSTRUCTIONS>"#
|
||||
);
|
||||
assert_eq!(
|
||||
loaded.sources().collect::<Vec<_>>(),
|
||||
vec![posix_source, windows_source]
|
||||
);
|
||||
}
|
||||
|
||||
/// Helper that returns a `Config` pointing at `root` and using `limit` as
|
||||
/// the maximum number of bytes to embed from AGENTS.md. The caller can
|
||||
/// optionally specify a custom `instructions` string – when `None` the
|
||||
@@ -508,7 +602,7 @@ async fn read_agents_md_propagates_metadata_errors() {
|
||||
};
|
||||
|
||||
let cwd = config.cwd.clone();
|
||||
let err = read_agents_md(&config.config, &fs, "local", &cwd)
|
||||
let err = read_agents_md(&config.config, &fs, "local", &PathUri::from_abs_path(&cwd))
|
||||
.await
|
||||
.expect_err("metadata error");
|
||||
|
||||
@@ -526,7 +620,7 @@ async fn read_agents_md_propagates_read_errors() {
|
||||
};
|
||||
|
||||
let cwd = config.cwd.clone();
|
||||
let err = read_agents_md(&config.config, &fs, "local", &cwd)
|
||||
let err = read_agents_md(&config.config, &fs, "local", &PathUri::from_abs_path(&cwd))
|
||||
.await
|
||||
.expect_err("read error");
|
||||
|
||||
@@ -544,7 +638,7 @@ async fn read_agents_md_ignores_files_removed_after_discovery() {
|
||||
};
|
||||
|
||||
let cwd = config.cwd.clone();
|
||||
let loaded = read_agents_md(&config.config, &fs, "local", &cwd)
|
||||
let loaded = read_agents_md(&config.config, &fs, "local", &PathUri::from_abs_path(&cwd))
|
||||
.await
|
||||
.expect("removed file is recoverable");
|
||||
|
||||
@@ -659,17 +753,18 @@ secondary doc"#,
|
||||
);
|
||||
assert_eq!(loaded.render(), expected_fragment);
|
||||
assert_eq!(
|
||||
loaded.sources().cloned().collect::<Vec<_>>(),
|
||||
loaded.sources().collect::<Vec<_>>(),
|
||||
vec![
|
||||
config
|
||||
.user_instructions
|
||||
.as_ref()
|
||||
.expect("global instructions")
|
||||
.source
|
||||
.clone(),
|
||||
primary.path().join("AGENTS.md").abs(),
|
||||
primary_nested.join("AGENTS.md").abs(),
|
||||
secondary.path().join("AGENTS.md").abs(),
|
||||
PathUri::from_abs_path(
|
||||
&config
|
||||
.user_instructions
|
||||
.as_ref()
|
||||
.expect("global instructions")
|
||||
.source,
|
||||
),
|
||||
PathUri::from_abs_path(&primary.path().join("AGENTS.md").abs()),
|
||||
PathUri::from_abs_path(&primary_nested.join("AGENTS.md").abs()),
|
||||
PathUri::from_abs_path(&secondary.path().join("AGENTS.md").abs()),
|
||||
]
|
||||
);
|
||||
}
|
||||
@@ -898,7 +993,10 @@ async fn concatenates_root_and_cwd_docs() {
|
||||
assert_eq!(loaded.text(), "root doc\n\ncrate doc");
|
||||
assert_eq!(
|
||||
loaded.sources().collect::<Vec<_>>(),
|
||||
vec![&root_agents, &crate_agents]
|
||||
vec![
|
||||
PathUri::from_abs_path(&root_agents),
|
||||
PathUri::from_abs_path(&crate_agents),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -925,8 +1023,8 @@ async fn project_root_markers_are_honored_for_agents_discovery() {
|
||||
let expected_parent = root.path().join("AGENTS.md").abs();
|
||||
let expected_child = cfg.cwd.join("AGENTS.md");
|
||||
assert_eq!(discovery.len(), 2);
|
||||
assert_eq!(discovery[0], expected_parent);
|
||||
assert_eq!(discovery[1], expected_child);
|
||||
assert_eq!(discovery[0], PathUri::from_abs_path(&expected_parent));
|
||||
assert_eq!(discovery[1], PathUri::from_abs_path(&expected_child));
|
||||
|
||||
let res = get_user_instructions(&cfg).await.expect("doc expected");
|
||||
assert_eq!(res, "parent doc\n\nchild doc");
|
||||
@@ -971,8 +1069,8 @@ async fn project_layers_do_not_override_project_root_markers() {
|
||||
assert_eq!(
|
||||
discovery,
|
||||
vec![
|
||||
root.path().join("AGENTS.md").abs(),
|
||||
config.cwd.join("AGENTS.md"),
|
||||
PathUri::from_abs_path(&root.path().join("AGENTS.md").abs()),
|
||||
PathUri::from_abs_path(&config.cwd.join("AGENTS.md")),
|
||||
]
|
||||
);
|
||||
}
|
||||
@@ -991,7 +1089,10 @@ async fn agents_md_paths_preserve_symlinked_cwd() {
|
||||
cfg.cwd = linked_cwd.abs();
|
||||
|
||||
let discovery = agents_md_paths(&cfg).await.expect("discover paths");
|
||||
assert_eq!(discovery, vec![cfg.cwd.join("AGENTS.md")]);
|
||||
assert_eq!(
|
||||
discovery,
|
||||
vec![PathUri::from_abs_path(&cfg.cwd.join("AGENTS.md"))]
|
||||
);
|
||||
|
||||
let res = get_user_instructions(&cfg).await.expect("doc expected");
|
||||
assert_eq!(res, "project doc");
|
||||
@@ -1050,7 +1151,10 @@ async fn instruction_sources_include_global_before_agents_md_docs() {
|
||||
assert_eq!(loaded.user_instructions(), cfg.user_instructions.as_ref());
|
||||
assert_eq!(
|
||||
loaded.sources().collect::<Vec<_>>(),
|
||||
vec![&global_agents, &project_agents]
|
||||
vec![
|
||||
PathUri::from_abs_path(&global_agents),
|
||||
PathUri::from_abs_path(&project_agents),
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
loaded.text(),
|
||||
@@ -1091,7 +1195,10 @@ async fn child_agents_message_after_project_docs_is_not_an_instruction_source()
|
||||
assert_eq!(loaded, expected);
|
||||
assert_eq!(
|
||||
loaded.sources().collect::<Vec<_>>(),
|
||||
vec![&global_agents, &project_agents]
|
||||
vec![
|
||||
PathUri::from_abs_path(&global_agents),
|
||||
PathUri::from_abs_path(&project_agents),
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
loaded.text(),
|
||||
@@ -1117,8 +1224,8 @@ async fn agents_local_md_preferred() {
|
||||
let discovery = agents_md_paths(&cfg).await.expect("discover paths");
|
||||
assert_eq!(discovery.len(), 1);
|
||||
assert_eq!(
|
||||
discovery[0].file_name().unwrap().to_string_lossy(),
|
||||
LOCAL_AGENTS_MD_FILENAME
|
||||
discovery[0].basename().as_deref(),
|
||||
Some(LOCAL_AGENTS_MD_FILENAME)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1166,12 +1273,9 @@ async fn agents_md_preferred_over_fallbacks() {
|
||||
|
||||
let discovery = agents_md_paths(&cfg).await.expect("discover paths");
|
||||
assert_eq!(discovery.len(), 1);
|
||||
assert!(
|
||||
discovery[0]
|
||||
.file_name()
|
||||
.unwrap()
|
||||
.to_string_lossy()
|
||||
.eq(DEFAULT_AGENTS_MD_FILENAME)
|
||||
assert_eq!(
|
||||
discovery[0].basename().as_deref(),
|
||||
Some(DEFAULT_AGENTS_MD_FILENAME)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1186,7 +1290,7 @@ async fn agents_md_directory_is_ignored() {
|
||||
assert_eq!(res, None);
|
||||
|
||||
let discovery = agents_md_paths(&cfg).await.expect("discover paths");
|
||||
assert_eq!(discovery, Vec::<AbsolutePathBuf>::new());
|
||||
assert_eq!(discovery, Vec::<PathUri>::new());
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
@@ -1209,7 +1313,7 @@ async fn agents_md_special_file_is_ignored() {
|
||||
assert_eq!(res, None);
|
||||
|
||||
let discovery = agents_md_paths(&cfg).await.expect("discover paths");
|
||||
assert_eq!(discovery, Vec::<AbsolutePathBuf>::new());
|
||||
assert_eq!(discovery, Vec::<PathUri>::new());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -1228,11 +1332,8 @@ async fn override_directory_falls_back_to_agents_md_file() {
|
||||
let discovery = agents_md_paths(&cfg).await.expect("discover paths");
|
||||
assert_eq!(discovery.len(), 1);
|
||||
assert_eq!(
|
||||
discovery[0]
|
||||
.file_name()
|
||||
.expect("file name")
|
||||
.to_string_lossy(),
|
||||
DEFAULT_AGENTS_MD_FILENAME
|
||||
discovery[0].basename().as_deref(),
|
||||
Some(DEFAULT_AGENTS_MD_FILENAME)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@ use codex_thread_store::ThreadMetadataPatch;
|
||||
use codex_thread_store::ThreadStoreError;
|
||||
use codex_thread_store::ThreadStoreResult;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_path_uri::LegacyAppPathString;
|
||||
use codex_utils_path_uri::PathUri;
|
||||
use rmcp::model::ReadResourceRequestParams;
|
||||
use std::collections::BTreeMap;
|
||||
@@ -564,10 +565,19 @@ impl CodexThread {
|
||||
}
|
||||
|
||||
/// Returns the files that supplied the thread's loaded model instructions.
|
||||
pub async fn instruction_sources(&self) -> Vec<AbsolutePathBuf> {
|
||||
pub async fn instruction_sources(&self) -> Vec<PathUri> {
|
||||
self.codex.instruction_sources().await
|
||||
}
|
||||
|
||||
/// Returns loaded instruction sources rendered as legacy app-server path strings.
|
||||
pub async fn legacy_instruction_sources(&self) -> Vec<LegacyAppPathString> {
|
||||
self.instruction_sources()
|
||||
.await
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub async fn config(&self) -> Arc<crate::config::Config> {
|
||||
self.codex.session.get_config().await
|
||||
}
|
||||
|
||||
@@ -830,15 +830,13 @@ impl Codex {
|
||||
state.session_configuration.thread_config_snapshot()
|
||||
}
|
||||
|
||||
pub(crate) async fn instruction_sources(&self) -> Vec<AbsolutePathBuf> {
|
||||
pub(crate) async fn instruction_sources(&self) -> Vec<PathUri> {
|
||||
let state = self.session.state.lock().await;
|
||||
state
|
||||
.session_configuration
|
||||
.loaded_agents_md
|
||||
.as_ref()
|
||||
.map_or_else(Vec::new, |instructions| {
|
||||
instructions.sources().cloned().collect()
|
||||
})
|
||||
.map_or_else(Vec::new, |instructions| instructions.sources().collect())
|
||||
}
|
||||
|
||||
pub(crate) async fn thread_environment_selections(&self) -> Vec<TurnEnvironmentSelection> {
|
||||
|
||||
@@ -58,6 +58,7 @@ use tempfile::TempDir;
|
||||
use wiremock::MockServer;
|
||||
|
||||
use crate::TempDirExt;
|
||||
use crate::TestEnvironment;
|
||||
use crate::get_remote_test_env;
|
||||
use crate::load_default_config_for_test;
|
||||
use crate::load_default_config_for_test_with_cloud_config_bundle;
|
||||
@@ -179,7 +180,20 @@ pub async fn test_env() -> Result<TestEnv> {
|
||||
/*sandbox*/ None,
|
||||
)
|
||||
.await?;
|
||||
let cwd = cwd_uri.to_abs_path()?;
|
||||
let cwd = if remote_env == TestEnvironment::WineExec {
|
||||
// TODO(anp): Convert `Config::cwd` to `LegacyAppPathString` and remove this
|
||||
// compatibility projection.
|
||||
// `Config::cwd` still requires `AbsolutePathBuf`. Preserve the test harness's
|
||||
// Linux-absolute `/C:/...` compatibility spelling so converting it back to a
|
||||
// `PathUri` recovers the remote Windows convention. Production conversions stay
|
||||
// strict: `PathUri::to_abs_path` intentionally rejects foreign paths.
|
||||
let path = cwd_uri.to_url().to_file_path().map_err(|()| {
|
||||
anyhow!("remote test cwd URI cannot be projected onto the host: {cwd_uri}")
|
||||
})?;
|
||||
AbsolutePathBuf::try_from(path)?
|
||||
} else {
|
||||
cwd_uri.to_abs_path()?
|
||||
};
|
||||
Ok(TestEnv {
|
||||
environment,
|
||||
exec_server_url: Some(websocket_url),
|
||||
|
||||
@@ -22,7 +22,6 @@ wine_rust_test(
|
||||
"//codex-rs/protocol",
|
||||
"//codex-rs/utils/path-uri",
|
||||
"@crates//:anyhow",
|
||||
"@crates//:base64",
|
||||
"@crates//:pretty_assertions",
|
||||
"@crates//:serde_json",
|
||||
"@crates//:tempfile",
|
||||
|
||||
@@ -36,11 +36,13 @@ use core_test_support::test_codex::test_codex;
|
||||
use core_test_support::test_codex::turn_permission_fields;
|
||||
use core_test_support::wait_for_event;
|
||||
use codex_utils_path_uri::LegacyAppPathString;
|
||||
use codex_utils_path_uri::PathConvention;
|
||||
use codex_utils_path_uri::PathUri;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::Value;
|
||||
use serde_json::json;
|
||||
use std::collections::BTreeMap;
|
||||
use std::fs;
|
||||
use tempfile::TempDir;
|
||||
use tokio::time::timeout;
|
||||
use wine_exec_server_test_support::WineExecServer;
|
||||
@@ -57,7 +59,7 @@ async fn windows_exec_server_runs_with_native_shell_and_cwd() -> Result<()> {
|
||||
const VERIFY_COMMAND: &str = r#"$path = Join-Path (Get-Location) 'codex-apply-patch-smoke.txt'; if (-not (Test-Path $path)) { exit 1 }; if ([IO.File]::ReadAllText($path) -ne "patched through unified exec`n") { exit 2 }; Remove-Item $path; Write-Output 'PATCH_VERIFIED'"#;
|
||||
|
||||
WineExecServer
|
||||
.scope(|exec_server_url| async move {
|
||||
.scope(|exec_server_url, _wine_prefix| async move {
|
||||
let server = start_mock_server().await;
|
||||
let arguments = serde_json::to_string(&json!({
|
||||
"cmd": COMMAND,
|
||||
@@ -245,8 +247,17 @@ async fn windows_exec_server_runs_with_native_shell_and_cwd() -> Result<()> {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn app_server_starts_thread_with_windows_environment_native_cwd() -> Result<()> {
|
||||
const AGENTS_INSTRUCTIONS: &str = "remote Windows workspace instructions";
|
||||
const NATIVE_CWD: &str = r"C:\windows";
|
||||
|
||||
WineExecServer
|
||||
.scope(|exec_server_url| async move {
|
||||
.scope(|exec_server_url, wine_prefix| async move {
|
||||
let agents_path = PathUri::parse("file:///C:/windows/AGENTS.md")?;
|
||||
fs::write(
|
||||
wine_prefix.join("drive_c").join("windows").join("AGENTS.md"),
|
||||
AGENTS_INSTRUCTIONS,
|
||||
)?;
|
||||
|
||||
let codex_home = TempDir::new()?;
|
||||
let server = create_mock_responses_server_repeating_assistant("done").await;
|
||||
write_mock_responses_config_toml(
|
||||
@@ -272,7 +283,7 @@ async fn app_server_starts_thread_with_windows_environment_native_cwd() -> Resul
|
||||
.send_thread_start_request(ThreadStartParams {
|
||||
environments: Some(vec![TurnEnvironmentParams {
|
||||
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
|
||||
cwd: serde_json::from_value::<LegacyAppPathString>(json!(r"C:\windows"))?,
|
||||
cwd: serde_json::from_value::<LegacyAppPathString>(json!(NATIVE_CWD))?,
|
||||
}]),
|
||||
..Default::default()
|
||||
})
|
||||
@@ -289,8 +300,13 @@ async fn app_server_starts_thread_with_windows_environment_native_cwd() -> Resul
|
||||
assert_eq!(response.cwd, host_cwd);
|
||||
// TODO(anp): Derive runtime workspace roots from the selected remote environment.
|
||||
assert_eq!(response.runtime_workspace_roots, vec![host_cwd]);
|
||||
// TODO(anp): Discover and report instruction sources from the remote filesystem.
|
||||
assert_eq!(response.instruction_sources, Vec::new());
|
||||
assert_eq!(
|
||||
response.instruction_sources,
|
||||
vec![LegacyAppPathString::from_path_uri(
|
||||
&agents_path,
|
||||
PathConvention::Windows,
|
||||
)?]
|
||||
);
|
||||
// TODO(anp): Report the implicit built-in permission profile instead of None.
|
||||
assert_eq!(response.active_permission_profile, None);
|
||||
|
||||
@@ -326,6 +342,17 @@ async fn app_server_starts_thread_with_windows_environment_native_cwd() -> Resul
|
||||
.find(|request| request.url.path().ends_with("/responses"))
|
||||
.context("turn should send a Responses request")?;
|
||||
let body = first_request.body_json::<Value>()?;
|
||||
let remote_instructions = body["input"]
|
||||
.as_array()
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.filter(|item| item.get("role").and_then(Value::as_str) == Some("user"))
|
||||
.filter_map(|item| item.get("content").and_then(Value::as_array))
|
||||
.flatten()
|
||||
.filter_map(|content| content.get("text").and_then(Value::as_str))
|
||||
.find(|text| text.contains(AGENTS_INSTRUCTIONS))
|
||||
.context("remote workspace instructions should be model visible")?;
|
||||
assert!(remote_instructions.contains(r"# AGENTS.md instructions for C:\windows"));
|
||||
let environment_context = body["input"]
|
||||
.as_array()
|
||||
.into_iter()
|
||||
|
||||
@@ -25,7 +25,6 @@ use core_test_support::responses::mount_sse_once;
|
||||
use core_test_support::responses::sse;
|
||||
use core_test_support::responses::start_mock_server;
|
||||
use core_test_support::skip_if_no_network;
|
||||
use core_test_support::skip_if_wine_exec;
|
||||
use core_test_support::test_codex::RecordingUserInstructionsProvider;
|
||||
use core_test_support::test_codex::TestCodexBuilder;
|
||||
use core_test_support::test_codex::test_codex;
|
||||
@@ -88,7 +87,7 @@ fn instruction_fragments(request: &responses::ResponsesRequest) -> Vec<String> {
|
||||
}
|
||||
|
||||
fn expected_instruction_fragment(cwd: &AbsolutePathBuf, contents: &str) -> String {
|
||||
let cwd = cwd.as_path().display();
|
||||
let cwd = PathUri::from_abs_path(cwd).inferred_native_path_string();
|
||||
format!("# AGENTS.md instructions for {cwd}\n\n<INSTRUCTIONS>\n{contents}\n</INSTRUCTIONS>")
|
||||
}
|
||||
|
||||
@@ -138,8 +137,6 @@ fn request_body_contains(request: &wiremock::Request, text: &str) -> bool {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn agents_override_is_preferred_over_agents_md() -> Result<()> {
|
||||
// TODO(anp): Remove after instruction-source helpers use target-native paths.
|
||||
skip_if_wine_exec!(Ok(()), "requires native cross-OS instruction-source paths");
|
||||
let instructions =
|
||||
agents_instructions(test_codex().with_workspace_setup(|cwd, fs| async move {
|
||||
let agents_md = cwd.join("AGENTS.md");
|
||||
@@ -172,8 +169,6 @@ async fn agents_override_is_preferred_over_agents_md() -> Result<()> {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn configured_fallback_is_used_when_agents_candidate_is_directory() -> Result<()> {
|
||||
// TODO(anp): Remove after instruction-source helpers use target-native paths.
|
||||
skip_if_wine_exec!(Ok(()), "requires native cross-OS instruction-source paths");
|
||||
let instructions = agents_instructions(
|
||||
test_codex()
|
||||
.with_config(|config| {
|
||||
@@ -334,8 +329,8 @@ async fn symlinked_cwd_uses_logical_parent_for_agents_discovery() -> Result<()>
|
||||
assert_eq!(
|
||||
test.codex.instruction_sources().await,
|
||||
vec![
|
||||
logical_root.join("AGENTS.md"),
|
||||
test.config.cwd.join("AGENTS.md")
|
||||
PathUri::from_abs_path(&logical_root.join("AGENTS.md")),
|
||||
PathUri::from_abs_path(&test.config.cwd.join("AGENTS.md"))
|
||||
]
|
||||
);
|
||||
|
||||
@@ -355,8 +350,6 @@ async fn symlinked_cwd_uses_logical_parent_for_agents_discovery() -> Result<()>
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn selected_environment_sources_match_model_visible_instructions() -> Result<()> {
|
||||
// TODO(anp): Remove after instruction-source helpers use target-native paths.
|
||||
skip_if_wine_exec!(Ok(()), "requires native cross-OS instruction-source paths");
|
||||
let server = start_mock_server().await;
|
||||
let resp_mock = mount_sse_once(
|
||||
&server,
|
||||
@@ -385,7 +378,10 @@ async fn selected_environment_sources_match_model_visible_instructions() -> Resu
|
||||
|
||||
assert_eq!(
|
||||
test.codex.instruction_sources().await,
|
||||
vec![global_agents, project_agents]
|
||||
vec![
|
||||
PathUri::from_abs_path(&global_agents),
|
||||
PathUri::from_abs_path(&project_agents),
|
||||
]
|
||||
);
|
||||
|
||||
test.submit_turn("hello").await?;
|
||||
@@ -454,7 +450,7 @@ async fn loads_user_instructions_without_a_primary_environment() -> Result<()> {
|
||||
assert_eq!(provider.load_count(), 2);
|
||||
assert_eq!(
|
||||
no_environment_thread.thread.instruction_sources().await,
|
||||
vec![global_source]
|
||||
vec![PathUri::from_abs_path(&global_source)]
|
||||
);
|
||||
|
||||
no_environment_thread
|
||||
@@ -485,8 +481,6 @@ async fn loads_user_instructions_without_a_primary_environment() -> Result<()> {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn fresh_thread_composes_global_before_project_and_reports_sources() -> Result<()> {
|
||||
// TODO(anp): Remove after instruction-source helpers use target-native paths.
|
||||
skip_if_wine_exec!(Ok(()), "requires native cross-OS instruction-source paths");
|
||||
// Set up one global source, one project source, and two ordinary model turns.
|
||||
let server = responses::start_mock_server().await;
|
||||
let response_mock = responses::mount_sse_sequence(
|
||||
@@ -521,7 +515,10 @@ async fn fresh_thread_composes_global_before_project_and_reports_sources() -> Re
|
||||
});
|
||||
let test = builder.build_with_remote_env(&server).await?;
|
||||
let project_source = test.config.cwd.join(GLOBAL_AGENTS_FILENAME);
|
||||
let creation_sources = vec![global_source.clone(), project_source.clone()];
|
||||
let creation_sources = vec![
|
||||
PathUri::from_abs_path(&global_source),
|
||||
PathUri::from_abs_path(&project_source),
|
||||
];
|
||||
|
||||
// Confirm the thread records both creation-time sources in composition order.
|
||||
assert_eq!(test.codex.instruction_sources().await, creation_sources);
|
||||
@@ -597,8 +594,6 @@ async fn fresh_thread_composes_global_before_project_and_reports_sources() -> Re
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn multi_environment_thread_loads_every_project_and_keeps_creation_snapshot() -> Result<()> {
|
||||
// TODO(anp): Remove after instruction-source helpers use target-native paths.
|
||||
skip_if_wine_exec!(Ok(()), "requires native cross-OS instruction-source paths");
|
||||
skip_if_no_network!(Ok(()));
|
||||
let Some(_remote_env) = get_remote_test_env() else {
|
||||
return Ok(());
|
||||
@@ -672,9 +667,9 @@ async fn multi_environment_thread_loads_every_project_and_keeps_creation_snapsho
|
||||
assert_eq!(
|
||||
thread.thread.instruction_sources().await,
|
||||
vec![
|
||||
global_source.clone(),
|
||||
remote_source.clone(),
|
||||
local_source.clone().try_into()?,
|
||||
PathUri::from_abs_path(&global_source),
|
||||
PathUri::from_abs_path(&remote_source),
|
||||
PathUri::from_path(&local_source)?,
|
||||
]
|
||||
);
|
||||
|
||||
@@ -700,7 +695,7 @@ async fn multi_environment_thread_loads_every_project_and_keeps_creation_snapsho
|
||||
|
||||
let contents = format!(
|
||||
"{GLOBAL_INSTRUCTIONS}\n\nfor `{REMOTE_ENVIRONMENT_ID}` with root {}\n\nremote project instructions\n\nfor `{LOCAL_ENVIRONMENT_ID}` with root {}\n\nlocal project instructions",
|
||||
test.config.cwd.display(),
|
||||
PathUri::from_abs_path(&test.config.cwd).inferred_native_path_string(),
|
||||
local_root.path().display(),
|
||||
);
|
||||
let expected =
|
||||
@@ -712,7 +707,11 @@ async fn multi_environment_thread_loads_every_project_and_keeps_creation_snapsho
|
||||
assert_eq!(provider.load_count(), 2);
|
||||
assert_eq!(
|
||||
thread.thread.instruction_sources().await,
|
||||
vec![global_source, remote_source, local_source.try_into()?]
|
||||
vec![
|
||||
PathUri::from_abs_path(&global_source),
|
||||
PathUri::from_abs_path(&remote_source),
|
||||
PathUri::from_path(&local_source)?,
|
||||
]
|
||||
);
|
||||
|
||||
Ok(())
|
||||
@@ -741,7 +740,10 @@ async fn invalid_utf8_global_instructions_are_lossy() -> Result<()> {
|
||||
test.submit_turn("inspect lossy global instructions")
|
||||
.await?;
|
||||
|
||||
assert_eq!(test.codex.instruction_sources().await, vec![source.clone()]);
|
||||
assert_eq!(
|
||||
test.codex.instruction_sources().await,
|
||||
vec![PathUri::from_abs_path(&source)]
|
||||
);
|
||||
let expected_fragment =
|
||||
expected_provider_only_instruction_fragment("global\u{FFFD}instructions");
|
||||
assert_single_instruction_fragment(&response_mock.single_request(), &expected_fragment);
|
||||
@@ -784,7 +786,7 @@ async fn cold_resume_replays_rendered_instructions_but_reports_current_config_so
|
||||
// Assert the pre-resume thread reports the source used to create its snapshot.
|
||||
assert_eq!(
|
||||
initial.codex.instruction_sources().await,
|
||||
vec![old_source.clone()],
|
||||
vec![PathUri::from_abs_path(&old_source)],
|
||||
"initial thread reports the creation-time global source"
|
||||
);
|
||||
initial.submit_turn("persist instructions").await?;
|
||||
@@ -814,7 +816,7 @@ async fn cold_resume_replays_rendered_instructions_but_reports_current_config_so
|
||||
// Assert the API reports the new source while model history replays the old structured prefix.
|
||||
assert_eq!(
|
||||
resumed.codex.instruction_sources().await,
|
||||
vec![new_source],
|
||||
vec![PathUri::from_abs_path(&new_source)],
|
||||
"resume reports sources from the newly loaded config"
|
||||
);
|
||||
|
||||
@@ -868,7 +870,7 @@ async fn fork_replays_rendered_instructions_from_shared_history() -> Result<()>
|
||||
// Assert the parent reports the source used to create its snapshot.
|
||||
assert_eq!(
|
||||
parent.codex.instruction_sources().await,
|
||||
vec![source.clone()],
|
||||
vec![PathUri::from_abs_path(&source)],
|
||||
"parent reports the creation-time global source"
|
||||
);
|
||||
parent.submit_turn("persist instructions").await?;
|
||||
@@ -903,7 +905,7 @@ async fn fork_replays_rendered_instructions_from_shared_history() -> Result<()>
|
||||
// Assert the fork reports the new source before issuing its first turn.
|
||||
assert_eq!(
|
||||
forked.thread.instruction_sources().await,
|
||||
vec![new_source],
|
||||
vec![PathUri::from_abs_path(&new_source)],
|
||||
"fork config should reflect the newly loaded global source"
|
||||
);
|
||||
|
||||
@@ -1033,7 +1035,7 @@ async fn run_subagent_global_instruction_case(fork_context: bool) -> Result<()>
|
||||
// Assert the parent reports the creation-time source before spawning.
|
||||
assert_eq!(
|
||||
test.codex.instruction_sources().await,
|
||||
vec![source.clone()],
|
||||
vec![PathUri::from_abs_path(&source)],
|
||||
"parent reports the creation-time global source before spawning"
|
||||
);
|
||||
test.submit_turn(SPAWN_SEED_PROMPT).await?;
|
||||
@@ -1076,12 +1078,12 @@ async fn run_subagent_global_instruction_case(fork_context: bool) -> Result<()>
|
||||
assert_single_instruction_fragment(&child_request, &expected_fragment);
|
||||
assert_eq!(
|
||||
test.codex.instruction_sources().await,
|
||||
vec![source.clone()],
|
||||
vec![PathUri::from_abs_path(&source)],
|
||||
"running parent retains the creation-time global source after spawning"
|
||||
);
|
||||
assert_eq!(
|
||||
child_thread.instruction_sources().await,
|
||||
vec![source],
|
||||
vec![PathUri::from_abs_path(&source)],
|
||||
"subagent reports the parent's creation-time source"
|
||||
);
|
||||
if fork_context {
|
||||
|
||||
@@ -25,6 +25,7 @@ use codex_protocol::protocol::RolloutLine;
|
||||
use codex_protocol::protocol::WarningEvent;
|
||||
use codex_protocol::user_input::UserInput;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_path_uri::PathUri;
|
||||
use core_test_support::PathBufExt;
|
||||
use core_test_support::context_snapshot;
|
||||
use core_test_support::context_snapshot::ContextSnapshotOptions;
|
||||
@@ -4593,7 +4594,7 @@ async fn manual_compaction_keeps_the_creation_time_global_instructions() -> Resu
|
||||
// Assert the pre-compaction source list points at the creation-time file.
|
||||
assert_eq!(
|
||||
test.codex.instruction_sources().await,
|
||||
vec![source.clone()],
|
||||
vec![PathUri::from_abs_path(&source)],
|
||||
"thread reports the creation-time global source before compaction"
|
||||
);
|
||||
|
||||
@@ -4623,7 +4624,7 @@ async fn manual_compaction_keeps_the_creation_time_global_instructions() -> Resu
|
||||
assert_single_instruction_fragment(&requests[2], &expected_fragment);
|
||||
assert_eq!(
|
||||
test.codex.instruction_sources().await,
|
||||
vec![source],
|
||||
vec![PathUri::from_abs_path(&source)],
|
||||
"thread retains the creation-time global source after compaction"
|
||||
);
|
||||
|
||||
@@ -4673,7 +4674,7 @@ async fn mid_turn_compaction_keeps_the_creation_time_global_instructions() -> Re
|
||||
// Assert the pre-compaction source list points at the creation-time file.
|
||||
assert_eq!(
|
||||
test.codex.instruction_sources().await,
|
||||
vec![source.clone()],
|
||||
vec![PathUri::from_abs_path(&source)],
|
||||
"thread reports the creation-time global source before mid-turn compaction"
|
||||
);
|
||||
|
||||
@@ -4695,7 +4696,7 @@ async fn mid_turn_compaction_keeps_the_creation_time_global_instructions() -> Re
|
||||
assert_single_instruction_fragment(&requests[2], &expected_fragment);
|
||||
assert_eq!(
|
||||
test.codex.instruction_sources().await,
|
||||
vec![source],
|
||||
vec![PathUri::from_abs_path(&source)],
|
||||
"thread retains the creation-time global source after mid-turn compaction"
|
||||
);
|
||||
|
||||
@@ -4780,7 +4781,7 @@ async fn remote_v2_compaction_keeps_creation_time_instructions_after_same_path_m
|
||||
);
|
||||
assert_eq!(
|
||||
test.codex.instruction_sources().await,
|
||||
vec![source.clone()],
|
||||
vec![PathUri::from_abs_path(&source)],
|
||||
"running thread retains the selected same-path source"
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -4829,7 +4830,7 @@ async fn remote_v2_compaction_keeps_creation_time_instructions_after_same_path_m
|
||||
);
|
||||
assert_eq!(
|
||||
resumed.codex.instruction_sources().await,
|
||||
vec![source],
|
||||
vec![PathUri::from_abs_path(&source)],
|
||||
"cold-resumed thread reports the same rewritten source path"
|
||||
);
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ use core_test_support::responses::ev_response_created;
|
||||
use core_test_support::responses::mount_sse_once;
|
||||
use core_test_support::responses::sse;
|
||||
use core_test_support::responses::start_mock_server;
|
||||
use core_test_support::skip_if_wine_exec;
|
||||
use core_test_support::test_codex::test_codex;
|
||||
|
||||
const HIERARCHICAL_AGENTS_SNIPPET: &str =
|
||||
@@ -13,8 +12,6 @@ const HIERARCHICAL_AGENTS_SNIPPET: &str =
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn hierarchical_agents_appends_to_project_doc_in_user_instructions() {
|
||||
// TODO(anp): Remove after instruction-source helpers use target-native paths.
|
||||
skip_if_wine_exec!("requires native cross-OS instruction-source paths");
|
||||
let server = start_mock_server().await;
|
||||
let resp_mock = mount_sse_once(
|
||||
&server,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
//! Test support for running the Windows exec-server under Wine.
|
||||
|
||||
use std::future::Future;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
@@ -12,16 +13,18 @@ use wine_test_support::WineTestCommand;
|
||||
pub struct WineExecServer;
|
||||
|
||||
impl WineExecServer {
|
||||
/// Starts the server, passes its WebSocket URL to `operation`, and tears it down afterward.
|
||||
/// Starts the server, passes its WebSocket URL and Wine prefix to `operation`, and tears it
|
||||
/// down afterward.
|
||||
pub async fn scope<T, F, Fut>(self, operation: F) -> Result<T>
|
||||
where
|
||||
F: FnOnce(String) -> Fut,
|
||||
F: FnOnce(String, PathBuf) -> Fut,
|
||||
Fut: Future<Output = Result<T>>,
|
||||
{
|
||||
let executable = codex_utils_cargo_bin::cargo_bin("wine-windows-exec-server")?;
|
||||
let mut exec_server = WineTestCommand::new(executable)
|
||||
.env("CODEX_HOME", r"C:\codex-home")
|
||||
.spawn()?;
|
||||
let wine_prefix = exec_server.prefix_path().to_path_buf();
|
||||
let stdout = exec_server.take_stdout();
|
||||
|
||||
exec_server
|
||||
@@ -36,7 +39,7 @@ impl WineExecServer {
|
||||
break line;
|
||||
}
|
||||
};
|
||||
operation(exec_server_url).await
|
||||
operation(exec_server_url, wine_prefix).await
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ async fn main() -> Result<()> {
|
||||
}
|
||||
|
||||
WineExecServer
|
||||
.scope(|exec_server_url| async move {
|
||||
.scope(|exec_server_url, _wine_prefix| async move {
|
||||
let mut command = Command::new(test_binary);
|
||||
command
|
||||
.env(TEST_ENVIRONMENT_ENV_VAR, "wine-exec")
|
||||
|
||||
@@ -62,6 +62,7 @@ codex-utils-fuzzy-match = { workspace = true }
|
||||
codex-utils-home-dir = { workspace = true }
|
||||
codex-utils-oss = { workspace = true }
|
||||
codex-utils-path = { workspace = true }
|
||||
codex-utils-path-uri = { workspace = true }
|
||||
codex-utils-plugins = { workspace = true }
|
||||
codex-utils-sandbox-summary = { workspace = true }
|
||||
codex-utils-sleep-inhibitor = { workspace = true }
|
||||
@@ -151,7 +152,6 @@ codex-cli = { workspace = true }
|
||||
codex-mcp = { workspace = true }
|
||||
core_test_support = { workspace = true }
|
||||
codex-utils-cargo-bin = { workspace = true }
|
||||
codex-utils-path-uri = { workspace = true }
|
||||
assert_matches = { workspace = true }
|
||||
chrono = { workspace = true, features = ["serde"] }
|
||||
insta = { workspace = true }
|
||||
|
||||
@@ -120,6 +120,7 @@ use codex_protocol::openai_models::ModelServiceTier;
|
||||
use codex_protocol::openai_models::ModelUpgrade;
|
||||
use codex_protocol::openai_models::ReasoningEffortPreset;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_path_uri::PathUri;
|
||||
use color_eyre::eyre::ContextCompat;
|
||||
use color_eyre::eyre::Result;
|
||||
use color_eyre::eyre::WrapErr;
|
||||
@@ -1561,7 +1562,7 @@ async fn thread_session_state_from_thread_start_response(
|
||||
response.active_permission_profile.clone().map(Into::into),
|
||||
response.cwd.clone(),
|
||||
response.runtime_workspace_roots.clone(),
|
||||
response.instruction_sources.clone(),
|
||||
response.instruction_source_path_uris(),
|
||||
response.reasoning_effort.clone(),
|
||||
config,
|
||||
)
|
||||
@@ -1602,7 +1603,7 @@ async fn thread_session_state_from_thread_resume_response(
|
||||
response.active_permission_profile.clone().map(Into::into),
|
||||
response.cwd.clone(),
|
||||
response.runtime_workspace_roots.clone(),
|
||||
response.instruction_sources.clone(),
|
||||
response.instruction_source_path_uris(),
|
||||
response.reasoning_effort.clone(),
|
||||
config,
|
||||
)
|
||||
@@ -1634,7 +1635,7 @@ async fn thread_session_state_from_thread_fork_response(
|
||||
response.active_permission_profile.clone().map(Into::into),
|
||||
response.cwd.clone(),
|
||||
response.runtime_workspace_roots.clone(),
|
||||
response.instruction_sources.clone(),
|
||||
response.instruction_source_path_uris(),
|
||||
response.reasoning_effort.clone(),
|
||||
config,
|
||||
)
|
||||
@@ -1673,7 +1674,7 @@ async fn thread_session_state_from_thread_response(
|
||||
active_permission_profile: Option<ActivePermissionProfile>,
|
||||
cwd: AbsolutePathBuf,
|
||||
runtime_workspace_roots: Vec<AbsolutePathBuf>,
|
||||
instruction_source_paths: Vec<AbsolutePathBuf>,
|
||||
instruction_source_paths: Vec<PathUri>,
|
||||
reasoning_effort: Option<codex_protocol::openai_models::ReasoningEffort>,
|
||||
config: &Config,
|
||||
) -> Result<ThreadSessionState, String> {
|
||||
@@ -1759,6 +1760,7 @@ mod tests {
|
||||
use codex_protocol::permissions::NetworkSandboxPolicy;
|
||||
use codex_utils_absolute_path::test_support::PathBufExt;
|
||||
use codex_utils_absolute_path::test_support::test_path_buf;
|
||||
use codex_utils_path_uri::LegacyAppPathString;
|
||||
use pretty_assertions::assert_eq;
|
||||
use tempfile::TempDir;
|
||||
|
||||
@@ -2367,7 +2369,9 @@ mod tests {
|
||||
test_path_buf("/tmp/project").abs(),
|
||||
test_path_buf("/tmp/project/extra").abs(),
|
||||
],
|
||||
instruction_sources: vec![test_path_buf("/tmp/project/AGENTS.md").abs()],
|
||||
instruction_sources: vec![LegacyAppPathString::from_abs_path(
|
||||
&test_path_buf("/tmp/project/AGENTS.md").abs(),
|
||||
)],
|
||||
approval_policy: codex_app_server_protocol::AskForApproval::Never,
|
||||
approvals_reviewer: codex_app_server_protocol::ApprovalsReviewer::User,
|
||||
sandbox: read_only_profile
|
||||
@@ -2393,7 +2397,7 @@ mod tests {
|
||||
);
|
||||
assert_eq!(
|
||||
started.session.instruction_source_paths,
|
||||
response.instruction_sources
|
||||
response.instruction_source_path_uris()
|
||||
);
|
||||
assert_eq!(started.session.permission_profile, read_only_profile);
|
||||
assert_eq!(started.turns.len(), 1);
|
||||
|
||||
@@ -163,6 +163,7 @@ use codex_terminal_detection::TerminalName;
|
||||
use codex_terminal_detection::terminal_info;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_cli::resume_hint;
|
||||
use codex_utils_path_uri::PathUri;
|
||||
use codex_utils_plugins::mention_syntax::PLUGIN_TEXT_MENTION_SIGIL;
|
||||
use codex_utils_plugins::mention_syntax::TOOL_MENTION_SIGIL;
|
||||
use crossterm::event::KeyCode;
|
||||
@@ -687,7 +688,7 @@ pub(crate) struct ChatWidget {
|
||||
// App-server-backed command runner for status-line workspace metadata lookups.
|
||||
workspace_command_runner: Option<WorkspaceCommandRunner>,
|
||||
// Instruction source files loaded for the current session, supplied by app-server.
|
||||
instruction_source_paths: Vec<AbsolutePathBuf>,
|
||||
instruction_source_paths: Vec<PathUri>,
|
||||
// Runtime network proxy bind addresses from SessionConfigured.
|
||||
session_network_proxy: Option<SessionNetworkProxyRuntime>,
|
||||
// Shared latch so we only warn once about invalid status-line item IDs.
|
||||
|
||||
@@ -589,7 +589,9 @@ async fn ctrl_d_with_modal_open_does_not_quit() {
|
||||
#[tokio::test]
|
||||
async fn slash_init_does_not_depend_on_loaded_instruction_sources() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.instruction_source_paths = vec![chat.config.cwd.join("project-instructions.md")];
|
||||
chat.instruction_source_paths = vec![codex_utils_path_uri::PathUri::from_abs_path(
|
||||
&chat.config.cwd.join("project-instructions.md"),
|
||||
)];
|
||||
|
||||
submit_composer_text(&mut chat, "/init");
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use super::*;
|
||||
use assert_matches::assert_matches;
|
||||
use codex_utils_path_uri::PathUri;
|
||||
|
||||
#[tokio::test]
|
||||
async fn status_command_renders_immediately_and_refreshes_rate_limits_for_chatgpt_auth() {
|
||||
@@ -96,9 +97,23 @@ async fn status_command_uses_catalog_default_reasoning_when_config_empty() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn status_command_renders_instruction_sources_from_thread_session() {
|
||||
async fn status_command_renders_native_and_foreign_instruction_sources() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.instruction_source_paths = vec![chat.config.cwd.join("AGENTS.md")];
|
||||
let (foreign_source, foreign_display) = if cfg!(windows) {
|
||||
(
|
||||
PathUri::parse("file:///remote/AGENTS.md").expect("POSIX instruction source"),
|
||||
"/remote/AGENTS.md",
|
||||
)
|
||||
} else {
|
||||
(
|
||||
PathUri::parse("file:///C:/remote/AGENTS.md").expect("Windows instruction source"),
|
||||
r"C:\remote\AGENTS.md",
|
||||
)
|
||||
};
|
||||
chat.instruction_source_paths = vec![
|
||||
PathUri::from_abs_path(&chat.config.cwd.join("AGENTS.md")),
|
||||
foreign_source,
|
||||
];
|
||||
|
||||
chat.dispatch_command(SlashCommand::Status);
|
||||
|
||||
@@ -109,8 +124,8 @@ async fn status_command_renders_instruction_sources_from_thread_session() {
|
||||
other => panic!("expected status output, got {other:?}"),
|
||||
};
|
||||
assert!(
|
||||
rendered.contains("Agents.md"),
|
||||
"expected /status to render app-server instruction sources, got: {rendered}"
|
||||
rendered.contains(&format!("AGENTS.md, {foreign_display}")),
|
||||
"expected /status to show native-relative and environment-native foreign paths, got: {rendered}"
|
||||
);
|
||||
assert!(
|
||||
!rendered.contains("Agents.md <none>"),
|
||||
|
||||
@@ -12,6 +12,7 @@ use codex_protocol::config_types::Personality;
|
||||
use codex_protocol::models::ActivePermissionProfile;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_path_uri::PathUri;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub(crate) struct SessionNetworkProxyRuntime {
|
||||
@@ -47,7 +48,7 @@ pub(crate) struct ThreadSessionState {
|
||||
pub(crate) active_permission_profile: Option<ActivePermissionProfile>,
|
||||
pub(crate) cwd: AbsolutePathBuf,
|
||||
pub(crate) runtime_workspace_roots: Vec<AbsolutePathBuf>,
|
||||
pub(crate) instruction_source_paths: Vec<AbsolutePathBuf>,
|
||||
pub(crate) instruction_source_paths: Vec<PathUri>,
|
||||
pub(crate) reasoning_effort: Option<codex_protocol::openai_models::ReasoningEffort>,
|
||||
pub(crate) collaboration_mode: Option<Box<CollaborationMode>>,
|
||||
pub(crate) personality: Option<Personality>,
|
||||
|
||||
@@ -5,7 +5,8 @@ use crate::text_formatting;
|
||||
use chrono::DateTime;
|
||||
use chrono::Local;
|
||||
use codex_protocol::account::PlanType;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_path_uri::PathConvention;
|
||||
use codex_utils_path_uri::PathUri;
|
||||
use std::path::Path;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
@@ -33,10 +34,20 @@ pub(crate) fn compose_model_display(
|
||||
(model_name.to_string(), details)
|
||||
}
|
||||
|
||||
pub(crate) fn compose_agents_summary(config: &Config, paths: &[AbsolutePathBuf]) -> String {
|
||||
pub(crate) fn compose_agents_summary(config: &Config, paths: &[PathUri]) -> String {
|
||||
let mut rels: Vec<String> = Vec::new();
|
||||
|
||||
for p in paths {
|
||||
for path in paths {
|
||||
// TODO(anp): Rationalize instruction-source summaries with the TUI's broader foreign-path
|
||||
// display strategy once other status surfaces can retain environment-native paths.
|
||||
if path.infer_path_convention() != Some(PathConvention::native()) {
|
||||
rels.push(path.inferred_native_path_string());
|
||||
continue;
|
||||
}
|
||||
let Ok(p) = path.to_abs_path() else {
|
||||
rels.push(path.inferred_native_path_string());
|
||||
continue;
|
||||
};
|
||||
let p = p.as_path();
|
||||
let file_name = p
|
||||
.file_name()
|
||||
@@ -229,7 +240,10 @@ mod tests {
|
||||
let config = test_config(&codex_home, &cwd).await;
|
||||
|
||||
assert_eq!(
|
||||
compose_agents_summary(&config, &[global_agents_path.abs()]),
|
||||
compose_agents_summary(
|
||||
&config,
|
||||
&[PathUri::from_abs_path(&global_agents_path.abs())]
|
||||
),
|
||||
format_directory_display(&global_agents_path, /*max_width*/ None)
|
||||
);
|
||||
}
|
||||
@@ -242,11 +256,33 @@ mod tests {
|
||||
let config = test_config(&codex_home, &cwd).await;
|
||||
|
||||
assert_eq!(
|
||||
compose_agents_summary(&config, &[override_path.abs()]),
|
||||
compose_agents_summary(&config, &[PathUri::from_abs_path(&override_path.abs())]),
|
||||
format_directory_display(&override_path, /*max_width*/ None)
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn compose_agents_summary_shows_relative_native_and_full_foreign_paths() {
|
||||
let codex_home = TempDir::new().expect("temp codex home");
|
||||
let cwd = TempDir::new().expect("temp cwd");
|
||||
let config = test_config(&codex_home, &cwd).await;
|
||||
let native_source = PathUri::from_abs_path(&config.cwd.join("AGENTS.md"));
|
||||
let foreign_source = if cfg!(windows) {
|
||||
PathUri::parse("file:///remote%20workspace/AGENTS.md")
|
||||
.expect("POSIX instruction source")
|
||||
} else {
|
||||
PathUri::parse("file:///C:/remote%20workspace/AGENTS.md")
|
||||
.expect("Windows instruction source")
|
||||
};
|
||||
|
||||
let summary = compose_agents_summary(&config, &[native_source, foreign_source]);
|
||||
if cfg!(windows) {
|
||||
insta::assert_snapshot!(summary, @r"AGENTS.md, /remote workspace/AGENTS.md");
|
||||
} else {
|
||||
insta::assert_snapshot!(summary, @r"AGENTS.md, C:\remote workspace\AGENTS.md");
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn compose_agents_summary_orders_global_before_project_agents() {
|
||||
let codex_home = TempDir::new().expect("temp codex home");
|
||||
@@ -258,8 +294,8 @@ mod tests {
|
||||
let summary = compose_agents_summary(
|
||||
&config,
|
||||
&[
|
||||
global_agents_path.clone().abs(),
|
||||
project_agents_path.clone().abs(),
|
||||
PathUri::from_abs_path(&global_agents_path.clone().abs()),
|
||||
PathUri::from_abs_path(&project_agents_path.clone().abs()),
|
||||
],
|
||||
);
|
||||
let mut paths = summary.split(", ");
|
||||
|
||||
@@ -209,13 +209,28 @@ impl PathUri {
|
||||
PathBuf::from(self.inferred_native_path_string())
|
||||
}
|
||||
|
||||
/// Returns the parent URI, or `None` for the URI root or an opaque fallback
|
||||
/// URI created by [`Self::from_abs_path`].
|
||||
/// Returns the lexical parent without crossing the inferred native path root.
|
||||
///
|
||||
/// POSIX `/`, Windows drive roots, Windows UNC share roots, and opaque fallback
|
||||
/// URIs created by [`Self::from_abs_path`] have no parent.
|
||||
pub fn parent(&self) -> Option<Self> {
|
||||
if self.encoded_path() == "/" || decode_bad_path_uri(&self.0).is_some() {
|
||||
if decode_bad_path_uri(&self.0).is_some() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let convention = self.infer_path_convention()?;
|
||||
// In URI form, both a Windows drive root (`file:///C:`) and a UNC share root
|
||||
// (`file://server/share`) retain one non-empty path segment. Keep that segment as the
|
||||
// anchor so parent traversal cannot produce a URI that is not an absolute Windows path.
|
||||
let anchor_depth = usize::from(convention == PathConvention::Windows);
|
||||
let depth = self
|
||||
.0
|
||||
.path_segments()?
|
||||
.filter(|segment| !segment.is_empty())
|
||||
.count();
|
||||
if depth <= anchor_depth {
|
||||
return None;
|
||||
}
|
||||
let mut url = self.0.clone();
|
||||
{
|
||||
let mut segments = match url.path_segments_mut() {
|
||||
@@ -227,6 +242,11 @@ impl PathUri {
|
||||
Some(Self(url))
|
||||
}
|
||||
|
||||
/// Returns this URI and each lexical parent up to its inferred native path root.
|
||||
pub fn ancestors(&self) -> impl Iterator<Item = Self> {
|
||||
std::iter::successors(Some(self.clone()), Self::parent)
|
||||
}
|
||||
|
||||
/// Lexically resolves native absolute or relative path text against this URI.
|
||||
///
|
||||
/// Path text is interpreted using the POSIX or Windows convention inferred
|
||||
|
||||
@@ -522,7 +522,7 @@ fn path_buf_uses_the_inferred_native_spelling() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parent_uses_uri_hierarchy_and_preserves_authority() {
|
||||
fn parent_stops_at_posix_drive_and_unc_roots() {
|
||||
for (input, expected) in [
|
||||
(
|
||||
"file:///workspace/src/lib.rs",
|
||||
@@ -531,12 +531,13 @@ fn parent_uses_uri_hierarchy_and_preserves_authority() {
|
||||
("file:///workspace", Some("file:///")),
|
||||
("file:///", None),
|
||||
("file:///C:/Users", Some("file:///C:")),
|
||||
("file:///C:/", Some("file:///")),
|
||||
("file:///C:/", None),
|
||||
("file:///C:", None),
|
||||
(
|
||||
"file://server/share/src/main.rs",
|
||||
Some("file://server/share/src"),
|
||||
),
|
||||
("file://server/share", Some("file://server/")),
|
||||
("file://server/share", None),
|
||||
] {
|
||||
let uri = PathUri::parse(input).expect("valid file URI");
|
||||
let expected = expected.map(|value| PathUri::parse(value).expect("valid expected URI"));
|
||||
@@ -544,6 +545,35 @@ fn parent_uses_uri_hierarchy_and_preserves_authority() {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ancestors_include_self_and_stop_at_native_path_roots() {
|
||||
for (input, expected) in [
|
||||
(
|
||||
"file:///workspace/src",
|
||||
vec!["file:///workspace/src", "file:///workspace", "file:///"],
|
||||
),
|
||||
(
|
||||
"file:///C:/workspace/src",
|
||||
vec![
|
||||
"file:///C:/workspace/src",
|
||||
"file:///C:/workspace",
|
||||
"file:///C:",
|
||||
],
|
||||
),
|
||||
(
|
||||
"file://server/share/project",
|
||||
vec!["file://server/share/project", "file://server/share"],
|
||||
),
|
||||
] {
|
||||
let uri = PathUri::parse(input).expect("valid file URI");
|
||||
let ancestors = uri
|
||||
.ancestors()
|
||||
.map(|path| path.to_string())
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(ancestors, expected, "ancestors for {input}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn join_normalizes_relative_uri_segments() {
|
||||
for (base, relative, expected) in [
|
||||
|
||||
Reference in New Issue
Block a user