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 <steipete@gmail.com>
Co-authored-by: Peter Steinberger <peter@steipete.me>
This commit is contained in:
qingminlong
2026-07-10 06:33:28 +08:00
committed by GitHub
parent 7c38c78f43
commit e251bc4c2f
2 changed files with 77 additions and 5 deletions

View File

@@ -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<void> {
const deadline = Date.now() + 2_000;
while (Date.now() < deadline) {
@@ -44,11 +50,73 @@ async function waitForSpawnCall(): Promise<void> {
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);

View File

@@ -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,