diff --git a/crates/host-core/src/tools/mod.rs b/crates/host-core/src/tools/mod.rs index 97e825ee2..50e020b64 100644 --- a/crates/host-core/src/tools/mod.rs +++ b/crates/host-core/src/tools/mod.rs @@ -16,7 +16,7 @@ use tokio::io::{AsyncRead, AsyncReadExt}; use tokio::process::{ChildStderr, ChildStdin, ChildStdout, Command}; use tokio::sync::{mpsc, watch}; -use crate::workspace::{resolve_tool_path_with_external, ToolRoot}; +use crate::workspace::{resolve_tool_path_with_external, simple_canonicalize, ToolRoot}; mod grep_rg; pub mod hashline; @@ -2625,7 +2625,13 @@ fn relative_display(root: &Path, path: &Path) -> String { // `path` comes back canonicalized from the resolver; strip against the // canonical root spelling too, or symlinked roots (macOS /var vs // /private/var) would render absolute. - let canonical_root = root.canonicalize().unwrap_or_else(|_| root.to_path_buf()); + // + // The resolver spells paths with `simple_canonicalize`, so the root must + // use that same spelling: std `Path::canonicalize` keeps the Windows + // `\\?\` prefix, which never matches a resolved path and made every + // workspace-relative label fall back to an absolute one. + let canonical_root = + simple_canonicalize(root).unwrap_or_else(|_| root.to_path_buf()); path.strip_prefix(&canonical_root) .or_else(|_| path.strip_prefix(root)) .unwrap_or(path) diff --git a/crates/host-core/src/workspace.rs b/crates/host-core/src/workspace.rs index 095990f28..d181d55cd 100644 --- a/crates/host-core/src/workspace.rs +++ b/crates/host-core/src/workspace.rs @@ -4,7 +4,12 @@ use std::path::{Component, Path, PathBuf}; /// Canonicalize a path, stripping the Windows extended-length prefix (`\\?\`) /// when the result is a simple drive-letter path (e.g. `C:\...`). This keeps /// paths compatible with shell APIs (`ShellExecuteW`) that reject `\\?\`. -fn simple_canonicalize(path: &Path) -> std::io::Result { +/// +/// Every path the resolver returns carries this spelling, so any caller that +/// compares a resolved path against a root must canonicalize that root the +/// same way — std `Path::canonicalize` keeps the `\\?\` prefix on Windows and +/// would never match. +pub(crate) fn simple_canonicalize(path: &Path) -> std::io::Result { let canonical = path.canonicalize()?; #[cfg(windows)] { diff --git a/docs/spec/06-delivery/04-e2e-test-plan.md b/docs/spec/06-delivery/04-e2e-test-plan.md index c6fbf806b..09dd05b1e 100644 --- a/docs/spec/06-delivery/04-e2e-test-plan.md +++ b/docs/spec/06-delivery/04-e2e-test-plan.md @@ -1394,7 +1394,11 @@ Each scenario is documented in this format: - **Acceptance**: E (bounded cross-platform search) - **Milestone**: M5 - **Status**: Unit-covered (host-core and agent-runtime); live multi-platform - protocol capture pending + protocol capture pending. The workspace-relative path expectation is covered + on Windows by `relative_display`, which must canonicalize the workspace root + with the resolver's own spelling (`simple_canonicalize`) — std + `Path::canonicalize` keeps the `\\?\` prefix there and silently degrades + every label to an absolute path. #### E2E-019a: Scratch-directory writes stay out of the workspace (D114) diff --git a/docs/zh-CN/spec/06-delivery/04-e2e-test-plan.md b/docs/zh-CN/spec/06-delivery/04-e2e-test-plan.md index 44d69b889..a3f24c78e 100644 --- a/docs/zh-CN/spec/06-delivery/04-e2e-test-plan.md +++ b/docs/zh-CN/spec/06-delivery/04-e2e-test-plan.md @@ -800,7 +800,11 @@ M5。 - **接受**:E(有界跨平台搜索) - **里程碑**:M5 - **状态**:单元覆盖(host-core 和代理运行时);多平台直播 - 协议捕获待处理 + 协议捕获待处理。工作区相对路径这一预期在 Windows 上由 + `relative_display` 覆盖:它必须用解析器自身的拼写 + (`simple_canonicalize`)规范化工作区根目录——标准库 + `Path::canonicalize` 在 Windows 上会保留 `\\?\` 前缀,从而把所有标签 + 静默降级为绝对路径。 #### E2E-019a:暂存目录写入不在工作区中 (D114)