From dce3262813e3c5ff6bc82464285bf14ca29e73ba Mon Sep 17 00:00:00 2001 From: dajiaohuang Date: Thu, 10 Sep 2026 19:47:59 +0800 Subject: [PATCH 1/2] fix(tools): keep tool labels workspace-relative on Windows `relative_display` canonicalized the workspace root with std `Path::canonicalize`, which keeps the Windows `\?\` extended-length prefix, while the resolver spells every path it returns with `simple_canonicalize` (prefix stripped). The two spellings can never match, so `strip_prefix` failed and each Read window header and Write result fell back to an absolute path on Windows. Canonicalize the root with the resolver's own helper so both sides share one spelling, and expose that helper crate-internally. Verified on Windows: `cargo test -p host-core` moves from 339 passed / 17 failed to 342 passed / 14 failed, repairing `tools::tests::workspace_write_reports_workspace_root`, `tools::tests::read_paginates_instead_of_refusing_large_files`, and `tools::tests::read_does_not_mark_a_filled_window_truncated`. Those assertions already encoded the intended behavior on Linux, where std `canonicalize` and `simple_canonicalize` agree. --- crates/host-core/src/tools/mod.rs | 10 ++++++++-- crates/host-core/src/workspace.rs | 7 ++++++- 2 files changed, 14 insertions(+), 3 deletions(-) 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)] { From 899bc21f4ea93476a047386830c50b6dd2f54c3b Mon Sep 17 00:00:00 2001 From: dajiaohuang Date: Thu, 10 Sep 2026 19:48:02 +0800 Subject: [PATCH 2/2] docs(e2e): record the Windows relative-path coverage for E2E-019e R3 sync for the `relative_display` fix. The scenario already required workspace-relative results inside the project; note the Windows constraint that makes its unit coverage real, in both locales. --- docs/spec/06-delivery/04-e2e-test-plan.md | 6 +++++- docs/zh-CN/spec/06-delivery/04-e2e-test-plan.md | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) 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)