From e251bc4c2feda2d353a31fdd2e773ecb99968c9f Mon Sep 17 00:00:00 2001 From: qingminlong Date: Fri, 10 Jul 2026 06:33:28 +0800 Subject: [PATCH] fix(agent-core): keep file info basename on Windows paths (#102813) * fix(agent-core): keep file info basename on Windows paths * fix(agent-core): use platform path basenames * chore: sync PR branch before changelog * docs(changelog): credit Windows file metadata fix * fix(agent-core): use platform path basenames * test(agent-core): isolate Windows shell discovery --------- Co-authored-by: Peter Steinberger Co-authored-by: Peter Steinberger --- .../agent-core/src/harness/env/nodejs.test.ts | 78 ++++++++++++++++++- packages/agent-core/src/harness/env/nodejs.ts | 4 +- 2 files changed, 77 insertions(+), 5 deletions(-) diff --git a/packages/agent-core/src/harness/env/nodejs.test.ts b/packages/agent-core/src/harness/env/nodejs.test.ts index 5059f4dfa3f5..3884ee3e5957 100644 --- a/packages/agent-core/src/harness/env/nodejs.test.ts +++ b/packages/agent-core/src/harness/env/nodejs.test.ts @@ -1,5 +1,6 @@ // Agent Core tests cover nodejs behavior. import { EventEmitter } from "node:events"; +import { parse } from "node:path"; import { PassThrough } from "node:stream"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { NodeExecutionEnv } from "./nodejs.js"; @@ -11,6 +12,7 @@ vi.mock("node:child_process", () => ({ })); afterEach(() => { + vi.unstubAllEnvs(); vi.restoreAllMocks(); vi.clearAllMocks(); }); @@ -31,6 +33,10 @@ function mockSpawnChild() { }; } +function createMockExecEnv(): NodeExecutionEnv { + return new NodeExecutionEnv({ cwd: process.cwd(), shellPath: process.execPath }); +} + async function waitForSpawnCall(): Promise { const deadline = Date.now() + 2_000; while (Date.now() < deadline) { @@ -44,11 +50,73 @@ async function waitForSpawnCall(): Promise { throw new Error("expected spawn to be called"); } +describe("NodeExecutionEnv file metadata", () => { + let env: NodeExecutionEnv; + let tempDir: string; + + beforeEach(async () => { + const rootEnv = new NodeExecutionEnv({ cwd: process.cwd() }); + const created = await rootEnv.createTempDir("agent-core-nodejs-"); + if (!created.ok) { + throw created.error; + } + tempDir = created.value; + env = new NodeExecutionEnv({ cwd: tempDir }); + }); + + afterEach(async () => { + const removed = await env.remove(tempDir, { recursive: true, force: true }); + if (!removed.ok) { + throw removed.error; + } + }); + + it("reports basenames consistently from fileInfo and listDir", async () => { + const written = await env.writeFile("notes/todo.txt", "hello"); + expect(written.ok).toBe(true); + + const info = await env.fileInfo("notes/todo.txt"); + expect(info.ok).toBe(true); + if (info.ok) { + expect(info.value.name).toBe("todo.txt"); + } + + const entries = await env.listDir("notes"); + expect(entries.ok).toBe(true); + if (entries.ok) { + expect(entries.value.map((entry) => entry.name)).toEqual(["todo.txt"]); + } + }); + + it("reports an empty basename for the filesystem root", async () => { + const info = await env.fileInfo(parse(tempDir).root); + expect(info.ok).toBe(true); + if (info.ok) { + expect(info.value.name).toBe(""); + } + }); + + it.runIf(process.platform !== "win32")( + "preserves backslashes in POSIX filenames", + async () => { + const fileName = "notes\\todo.txt"; + const written = await env.writeFile(fileName, "hello"); + expect(written.ok).toBe(true); + + const info = await env.fileInfo(fileName); + expect(info.ok).toBe(true); + if (info.ok) { + expect(info.value.name).toBe(fileName); + } + }, + ); +}); + describe("NodeExecutionEnv timeout handling", () => { let env: NodeExecutionEnv; beforeEach(() => { - env = new NodeExecutionEnv({ cwd: process.cwd(), shellPath: "/bin/bash" }); + env = createMockExecEnv(); }); it.each([ @@ -88,7 +156,7 @@ describe("NodeExecutionEnv exec stream errors", () => { let env: NodeExecutionEnv; beforeEach(() => { - env = new NodeExecutionEnv({ cwd: process.cwd(), shellPath: "/bin/bash" }); + env = createMockExecEnv(); }); it.each(["stdout", "stderr"] as const)( @@ -145,10 +213,15 @@ describe("NodeExecutionEnv exec stream errors", () => { it("contains stdout errors during Windows shell discovery", async () => { const platformDescriptor = Object.getOwnPropertyDescriptor(process, "platform"); Object.defineProperty(process, "platform", { value: "win32", configurable: true }); + // Force PATH discovery even on Windows hosts with Git Bash in Program Files. + vi.stubEnv("ProgramFiles", ""); + vi.stubEnv("ProgramFiles(x86)", ""); try { const child = mockSpawnChild(); const resultPromise = new NodeExecutionEnv({ cwd: process.cwd() }).exec("echo hello"); await waitForSpawnCall(); + expect(spawnMock.mock.calls[0]?.[0]).toBe("where"); + expect(spawnMock.mock.calls[0]?.[1]).toEqual(["bash.exe"]); child.stdout.emit("error", new Error("where stdout failed")); @@ -157,7 +230,6 @@ describe("NodeExecutionEnv exec stream errors", () => { if (!result.ok) { expect(result.error.code).toBe("shell_unavailable"); } - expect(spawnMock.mock.calls[0]?.[0]).toBe("where"); } finally { if (platformDescriptor) { Object.defineProperty(process, "platform", platformDescriptor); diff --git a/packages/agent-core/src/harness/env/nodejs.ts b/packages/agent-core/src/harness/env/nodejs.ts index db1f0242f8cb..ec48400a6171 100644 --- a/packages/agent-core/src/harness/env/nodejs.ts +++ b/packages/agent-core/src/harness/env/nodejs.ts @@ -15,7 +15,7 @@ import { writeFile, } from "node:fs/promises"; import { tmpdir } from "node:os"; -import { isAbsolute, join, resolve } from "node:path"; +import { basename, isAbsolute, join, resolve } from "node:path"; import { createInterface } from "node:readline"; import { type ExecutionEnv, @@ -84,7 +84,7 @@ function fileInfoFromStats( return err(new FileError("invalid", "Unsupported file type", path)); } return ok({ - name: path.replace(/\/+$/, "").split("/").pop() ?? path, + name: basename(path), path, kind, size: stats.size,