Просмотр исходного кода

fix(windows): preserve native drive-root probes

Tianyi Cui 1 месяц назад
Родитель
Сommit
0aefa18636

+ 2 - 2
.agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.md
-2026-08-08-native-windows-pull-request-ci.md: 0769a8dc582f48c1baddbb177e2e9921b809e391
-2026-08-08-native-windows-pull-request-ci.zh.md: 7431d917adf2bfd8cc0cf31e9c249b4f28e6caf0
+2026-08-08-native-windows-pull-request-ci.md: a540a73e5979c436fb8a085516554f5ae7249939
+2026-08-08-native-windows-pull-request-ci.zh.md: 34ca87f21bcd5b72b2ecbc43e66043caee299e48

+ 2 - 0
.agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.md

@@ -28,6 +28,8 @@ The next exact-head run exposed one remaining observational built-bin failure: i
 
 Running the complete instrumented graph instead of the earlier reduced inventory exposed the remaining cross-platform fixture contracts. Windows path identity now accounts for 8.3 aliases, native separators, Git checkout line endings, cross-drive relative paths, and file URLs before constructing loader symlinks. The JSONL durable-directory helper applies the extended-length namespace to probes and staging creation, real product tests invoke portable executable entries and tolerate bounded Windows handle release, and stress tests retain their workloads with explicit coverage budgets. A credential document or watch path whose deepest existing ancestor is a file now fails `ENOTDIR` on every host, while `skill-local` uses effect-owned persistent Chokidar handles so asynchronous libuv errors are contained instead of escaping the test process.
 
+The final root-probe failure came from applying the extended-length namespace to the drive root as well as long descendants. Node rejected the bare root probe as `EISDIR`, cascading through every JSONL fixture and assembled binary that materialized a session. The Windows durable-directory helper now probes the short drive root in its native spelling and namespaces only descendants; an injected Win32-path unit test locks both spellings while native coverage exercises the real filesystem.
+
 POSIX mode bits, chmod-based unreadability, and chmod-based writer-lock refusal do not exist as equivalent Windows facilities. Those acceptance cases remain enforced on POSIX and are skipped on Windows; content, atomic replacement, symlink safety, rollback and recovery through platform-independent filesystem conflicts, and native Windows long-path behavior remain covered. No supported product source is excluded from Windows coverage to accommodate these differences.
 
 Wine-only infrastructure is absent from the supported workflow: there is no apt-cache producer, compatibility script, hoisted snapshot install, Windows Node download, or local `check:windows-wine` command. The [archived Wine experiment](../../archived/process/2026-07-27-wine-windows-gates-experiment.md) remains historical evidence for its measured latency and fidelity trade-offs, not a current execution path.

+ 2 - 0
.agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.zh.md

@@ -28,6 +28,8 @@ Status: implemented
 
 运行完整的覆盖率插桩图而非此前缩减的清单后,剩余的跨平台 fixture 契约也显现出来。Windows 路径标识现在会在比较或构造 loader 符号链接前处理 8.3 别名、原生分隔符、Git 检出换行、跨盘符相对路径与文件 URL。JSONL 持久目录辅助函数会对探测与临时目录创建应用扩展长度命名空间;真实产品测试会调用可移植的可执行入口,并以有界重试容纳 Windows 句柄释放;压力测试则保留原工作负载并获得显式的覆盖率预算。如果凭据文档或监听路径最深的现有祖先是文件,所有宿主现在都会返回 `ENOTDIR`;`skill-local` 同时改用由 effect 拥有的持久 Chokidar 句柄,使异步 libuv 错误得到收束,不再逸出测试进程。
 
+最后一个根路径探测失败源于扩展长度命名空间既应用到长后代路径,也应用到了驱动器根目录。Node 将裸根目录探测拒绝为 `EISDIR`,从而连锁影响所有会物化会话的 JSONL fixture 和组装后二进制。Windows 持久目录辅助函数现在以原生写法探测本来就很短的驱动器根目录,仅对后代路径添加命名空间;注入 Win32 路径语义的单元测试固定两种写法,原生覆盖率则验证真实文件系统。
+
 POSIX 模式位、基于 chmod 的不可读状态和基于 chmod 的 writer lock 拒绝在 Windows 上没有等价机制。这些验收场景继续在 POSIX 上强制执行,并在 Windows 上跳过;内容、原子替换、符号链接安全、通过平台无关文件系统冲突验证的回滚与恢复,以及原生 Windows 长路径行为仍保有覆盖。没有任何受支持的产品源码为适应这些差异而从 Windows 覆盖率中排除。
 
 受支持的工作流不含 Wine 专属基础设施:不存在 apt 缓存生产者、兼容性脚本、对仓库快照执行的 hoisted 安装、Windows Node 下载或本地 `check:windows-wine` 命令。[已归档的 Wine 实验](../../archived/process/2026-07-27-wine-windows-gates-experiment.md)仍作为其实测延迟与保真度取舍的历史证据,而非当前执行路径。

+ 4 - 1
packages/session-persistence/session-persistence-jsonl/src/win32.ts

@@ -91,7 +91,10 @@ function isEEXIST(error: unknown): boolean {
 
 async function assertDirectory(path: string): Promise<boolean> {
   try {
-    const info = await stat(toNamespacedPath(path))
+    // A bare drive root is already short, and Node rejects its extended-length
+    // spelling as EISDIR. Descendants retain the namespace for long-path probes.
+    const probe = path === parse(path).root ? path : toNamespacedPath(path)
+    const info = await stat(probe)
     if (info.isDirectory()) return true
     const error = new Error(`path exists but is not a directory: ${path}`) as NodeJS.ErrnoException
     error.code = 'ENOTDIR'

+ 32 - 0
packages/session-persistence/session-persistence-jsonl/tests/win32.spec.ts

@@ -92,11 +92,43 @@ async function importWithFilesystemMove(): Promise<typeof import('../src/win32.t
 
 afterEach(async () => {
   vi.doUnmock('koffi')
+  vi.doUnmock('node:fs/promises')
+  vi.doUnmock('node:path')
   vi.resetModules()
   for (const root of roots.splice(0)) await rm(root, { recursive: true, force: true })
 })
 
 describe('Windows durable namespace helpers', () => {
+  it('keeps drive-root probes native while namespacing descendants', async () => {
+    const probes: string[] = []
+    vi.resetModules()
+    vi.doMock('node:fs/promises', async (importOriginal) => {
+      const actual = await importOriginal<typeof import('node:fs/promises')>()
+      return {
+        ...actual,
+        stat: async (path: string) => {
+          probes.push(path)
+          return { isDirectory: () => true }
+        },
+      }
+    })
+    vi.doMock('node:path', async (importOriginal) => {
+      const actual = await importOriginal<typeof import('node:path')>()
+      return {
+        ...actual,
+        join: (...paths: string[]) => actual.win32.join(...paths),
+        parse: (path: string) => actual.win32.parse(path),
+        resolve: (...paths: string[]) => actual.win32.resolve(...paths),
+        toNamespacedPath: (path: string) => actual.win32.toNamespacedPath(path),
+      }
+    })
+    const { ensureDurableDirectoryWin32 } = await import('../src/win32.ts')
+
+    await ensureDurableDirectoryWin32('C:\\existing')
+
+    expect(probes).toEqual(['C:\\', '\\\\?\\C:\\existing'])
+  })
+
   it('publishes a new file with write-through MoveFileExW semantics', async () => {
     const { publishNewFileWin32 } = await importWithFilesystemMove()
     const root = await tempRoot()