0eaaa54e25
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
77 lines
3.0 KiB
TypeScript
77 lines
3.0 KiB
TypeScript
/**
|
|
* Temporary Workspace creation:
|
|
*
|
|
* - The directory name is the workspace_id, shaped like `tmp-<8hex>`, created under
|
|
* `<agent>/workspaces/`.
|
|
* - The id is checked for collisions within `workspaces/`: on conflict with an existing
|
|
* directory (EEXIST), it regenerates rather than reusing the existing directory; once
|
|
* retries are exhausted, it throws instead of silently falling back to an old temp Workspace.
|
|
*/
|
|
import fs from "node:fs/promises";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
import { createTempWorkspace } from "../src/internal/session-support.js";
|
|
import { workspacesDir } from "../src/state/paths.js";
|
|
|
|
const mocked = vi.hoisted(() => ({ uuids: [] as string[] }));
|
|
|
|
vi.mock("node:crypto", async (importOriginal) => {
|
|
const actual = await importOriginal<typeof import("node:crypto")>();
|
|
return {
|
|
...actual,
|
|
// When the queue has values, dequeue from it (tests inject fixed ids), otherwise fall back
|
|
// to the real implementation.
|
|
randomUUID: () => mocked.uuids.shift() ?? actual.randomUUID(),
|
|
};
|
|
});
|
|
|
|
let tmpRoot: string;
|
|
|
|
beforeEach(async () => {
|
|
tmpRoot = await fs.mkdtemp(path.join(os.tmpdir(), "penguin-workspace-"));
|
|
mocked.uuids.length = 0;
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await fs.rm(tmpRoot, { recursive: true, force: true });
|
|
});
|
|
|
|
describe("createTempWorkspace", () => {
|
|
it("creates a tmp-<8hex> directory under workspaces/ and returns its path", async () => {
|
|
const dir = await createTempWorkspace(tmpRoot, "proj", "agent");
|
|
expect(path.dirname(dir)).toBe(workspacesDir(tmpRoot, "proj", "agent"));
|
|
expect(path.basename(dir)).toMatch(/^tmp-[0-9a-f]{8}$/);
|
|
expect((await fs.stat(dir)).isDirectory()).toBe(true);
|
|
});
|
|
|
|
it("regenerates the id when it collides with an existing directory instead of reusing it", async () => {
|
|
const base = workspacesDir(tmpRoot, "proj", "agent");
|
|
await fs.mkdir(path.join(base, "tmp-aaaaaaaa"), { recursive: true });
|
|
await fs.writeFile(path.join(base, "tmp-aaaaaaaa", "keep.txt"), "old");
|
|
mocked.uuids.push(
|
|
"aaaaaaaa-1111-4111-8111-111111111111",
|
|
"bbbbbbbb-2222-4222-8222-222222222222",
|
|
);
|
|
|
|
const dir = await createTempWorkspace(tmpRoot, "proj", "agent");
|
|
|
|
expect(path.basename(dir)).toBe("tmp-bbbbbbbb");
|
|
expect(await fs.readdir(dir)).toEqual([]);
|
|
// The existing directory is neither reused nor modified.
|
|
await expect(fs.readFile(path.join(base, "tmp-aaaaaaaa", "keep.txt"), "utf8")).resolves.toBe(
|
|
"old",
|
|
);
|
|
});
|
|
|
|
it("errors when retries are exhausted instead of reusing an existing directory", async () => {
|
|
const base = workspacesDir(tmpRoot, "proj", "agent");
|
|
await fs.mkdir(path.join(base, "tmp-cccccccc"), { recursive: true });
|
|
mocked.uuids.push(...Array.from({ length: 16 }, () => "cccccccc-3333-4333-8333-333333333333"));
|
|
|
|
await expect(createTempWorkspace(tmpRoot, "proj", "agent")).rejects.toThrow(
|
|
/unique temp workspace id/,
|
|
);
|
|
});
|
|
});
|