Files
dev/third_party/pi/packages/durable/test/env-node-conformance.test.ts
alexion 6081e3e325 chore(vendor): import pi 1.3.0
Imported as a linear vendor commit so forge rebases do not replay the upstream root tree.

git-subtree-dir: third_party/pi

git-subtree-split: 71023aec1546d6d677a65669c5665c0927ce0dd0
2026-10-15 17:58:48 -04:00

127 lines
4.8 KiB
TypeScript

import { execFileSync } from "node:child_process";
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
import { afterEach, describe, expect, it } from "vitest";
import { getOrThrow, type WatchChange } from "../src/env/index.ts";
import { NodeExecutionEnv } from "../src/env/node.ts";
import { registerEnvConformance } from "../src/testing/index.ts";
const windows = process.platform === "win32";
const gitBash = join(process.env.ProgramFiles ?? "C:\\Program Files", "Git", "bin", "bash.exe");
const context = BACKGROUND_CONTEXT;
/**
* Remove a test directory. On Windows, `taskkill /T` runs asynchronously and can miss descendants such as Git Bash's
* `sleep`, so a killed command's processes can hold the directory for a while after `exec` settles; retry until they
* exit.
*/
async function removeTempDir(dir: string): Promise<void> {
for (let attempt = 0; ; attempt++) {
try {
rmSync(dir, { recursive: true, force: true });
return;
} catch (error) {
const code = (error as NodeJS.ErrnoException).code;
if (attempt >= 80 || (code !== "EBUSY" && code !== "EPERM" && code !== "ENOTEMPTY")) throw error;
await new Promise((resolve) => setTimeout(resolve, 100));
}
}
}
registerEnvConformance(
{ describe, expect, it },
"NodeExecutionEnv conformance",
async (use) => {
const cwd = mkdtempSync(join(tmpdir(), "pi-durable-env-conformance-"));
try {
await use(new NodeExecutionEnv({ cwd }));
} finally {
await removeTempDir(cwd);
}
},
// Git Bash's `ln -s` copies instead of linking unless native symlinks are enabled.
windows ? { shell: [gitBash, "-c"], symlinks: false } : {},
);
registerEnvConformance(
{ describe, expect, it },
"NodeExecutionEnv conformance with polling watches",
async (use) => {
const cwd = mkdtempSync(join(tmpdir(), "pi-durable-env-conformance-"));
try {
await use(new NodeExecutionEnv({ cwd, watch: { mode: "polling", pollIntervalMs: 100 } }));
} finally {
await removeTempDir(cwd);
}
},
windows ? { shell: [gitBash, "-c"], symlinks: false } : {},
);
describe("NodeExecutionEnv watch limits", () => {
it("refuses a tree over the directory budget and stops with an error when one grows past it", async () => {
const cwd = mkdtempSync(join(tmpdir(), "pi-durable-env-watch-"));
try {
const env = new NodeExecutionEnv({ cwd, watch: { maxDirectories: 3 } });
getOrThrow(await env.createDir("tree/a/b", undefined, context));
getOrThrow(await env.createDir("tree/c", undefined, context));
expect(await env.watch([{ path: "tree", recursive: true }], () => {}, context)).toMatchObject({
ok: false,
error: { code: "invalid" },
});
getOrThrow(await env.remove("tree/c", { recursive: true }, context));
const changes: WatchChange[] = [];
const watcher = getOrThrow(
await env.watch([{ path: "tree", recursive: true }], (change) => changes.push(change), context),
);
getOrThrow(await env.createDir("tree/d", undefined, context));
const deadline = Date.now() + 3000;
while (!changes.some((change) => "error" in change) && Date.now() < deadline) {
await new Promise((resolve) => setTimeout(resolve, 20));
}
expect(changes.at(-1)).toMatchObject({ error: { code: "invalid" } });
await watcher.close(context);
} finally {
await removeTempDir(cwd);
}
});
});
describe("NodeExecutionEnv readers", () => {
const dirs: string[] = [];
const tempDir = (): string => {
const dir = mkdtempSync(join(tmpdir(), "pi-durable-env-readers-"));
dirs.push(dir);
return dir;
};
afterEach(async () => {
for (const dir of dirs.splice(0)) await removeTempDir(dir);
}, 15_000);
it("reads ranges spanning several internal chunks exactly", async () => {
const cwd = tempDir();
const bytes = new Uint8Array(2.5 * 1024 * 1024);
for (let index = 0; index < bytes.length; index++) bytes[index] = (index * 31) % 251;
writeFileSync(join(cwd, "big.bin"), bytes);
const reader = getOrThrow(await new NodeExecutionEnv({ cwd }).openBinaryReader("big.bin", undefined, context));
try {
const all = getOrThrow(await reader.read(0, bytes.length + 10, context));
expect(all.length).toBe(bytes.length);
expect(Buffer.from(all).equals(Buffer.from(bytes))).toBe(true);
const middle = getOrThrow(await reader.read(1024 * 1024 - 3, 7, context));
expect([...middle]).toEqual([...bytes.subarray(1024 * 1024 - 3, 1024 * 1024 + 4)]);
} finally {
await reader.close(context);
}
});
it.skipIf(windows)("refuses a FIFO without waiting for a writer", async () => {
const cwd = tempDir();
execFileSync("mkfifo", [join(cwd, "pipe")]);
const result = await new NodeExecutionEnv({ cwd }).openBinaryReader("pipe", undefined, context);
expect(result).toMatchObject({ ok: false, error: { code: "invalid" } });
});
});