Fresh history cut from cynarAI/evoo antigravity-handoff/project-timeline (Voice First + project timeline). Brand docs/titles as evOo; keep @rakazo/* packages as tech debt. Excludes node_modules, caches, secrets, voice blobs.
199 lines
7.7 KiB
TypeScript
199 lines
7.7 KiB
TypeScript
import * as fs from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import path from "node:path";
|
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import type * as FileHandlePathModule from "./file-handle-path.js";
|
|
import { fileHandlePath } from "./file-handle-path.js";
|
|
import { LocalAgentHomeStore } from "./home.js";
|
|
|
|
vi.mock("./file-handle-path.js", async (importOriginal) => {
|
|
const actual = await importOriginal<typeof FileHandlePathModule>();
|
|
return { fileHandlePath: vi.fn(actual.fileHandlePath) };
|
|
});
|
|
|
|
const race = vi.hoisted(() => ({
|
|
afterStat: undefined as (() => Promise<void>) | undefined,
|
|
beforeOpen: undefined as (() => Promise<void>) | undefined,
|
|
afterOpen: undefined as (() => Promise<void>) | undefined,
|
|
target: "",
|
|
opened: [] as fs.FileHandle[],
|
|
}));
|
|
|
|
vi.mock("node:fs/promises", async (importOriginal) => {
|
|
const actual = await importOriginal<typeof fs>();
|
|
return {
|
|
...actual,
|
|
stat: async (...args: Parameters<typeof fs.stat>) => {
|
|
const info = await actual.stat(...args);
|
|
if (args[0] === race.target && race.afterStat) {
|
|
const replace = race.afterStat;
|
|
race.afterStat = undefined;
|
|
await replace();
|
|
}
|
|
return info;
|
|
},
|
|
open: async (...args: Parameters<typeof fs.open>) => {
|
|
if (args[0] === race.target) await race.beforeOpen?.();
|
|
const handle = await actual.open(...args);
|
|
if (args[0] === race.target) {
|
|
race.opened.push(handle);
|
|
vi.spyOn(handle, "readFile");
|
|
await race.afterOpen?.();
|
|
}
|
|
return handle;
|
|
},
|
|
};
|
|
});
|
|
|
|
const context = {
|
|
operationId: "test",
|
|
traceId: "test",
|
|
spaceId: "workspace",
|
|
userId: "user",
|
|
signal: new AbortController().signal,
|
|
};
|
|
const dirs: string[] = [];
|
|
|
|
afterEach(async () => {
|
|
race.afterStat = undefined;
|
|
race.beforeOpen = undefined;
|
|
race.afterOpen = undefined;
|
|
race.opened = [];
|
|
await Promise.all(dirs.splice(0).map((dir) => fs.rm(dir, { recursive: true, force: true })));
|
|
});
|
|
|
|
describe.each(["export", "checkout", "commit"] as const)(
|
|
"home %s source containment",
|
|
(operation) => {
|
|
it.each(["file", "parent", "root", "parent restored after open"] as const)(
|
|
"rejects a %s replaced after validation",
|
|
async (replacement) => {
|
|
const root = await fs.mkdtemp(path.join(tmpdir(), "rakazo-home-race-"));
|
|
dirs.push(root);
|
|
const store = new LocalAgentHomeStore(root);
|
|
const source = operation === "commit" ? path.join(root, "source") : store.pathFor("bot-1");
|
|
const parent = path.join(source, "nested");
|
|
const target = path.join(parent, "result.txt");
|
|
const outside = path.join(root, "outside");
|
|
const dest = path.join(root, "checkout");
|
|
await fs.mkdir(parent, { recursive: true });
|
|
await fs.mkdir(outside);
|
|
await fs.writeFile(target, "safe content");
|
|
await fs.writeFile(path.join(outside, "result.txt"), "fake outside secret");
|
|
await fs.mkdir(path.join(outside, "nested"));
|
|
await fs.writeFile(path.join(outside, "nested/result.txt"), "fake outside secret");
|
|
race.target = await fs.realpath(target);
|
|
let replaced = false;
|
|
const replace = async () => {
|
|
if (replacement === "file") {
|
|
await fs.rm(target);
|
|
await fs.symlink(path.join(outside, "result.txt"), target);
|
|
} else if (replacement === "root") {
|
|
await fs.rename(source, `${source}-original`);
|
|
await fs.symlink(outside, source, "junction");
|
|
} else {
|
|
await fs.rename(parent, `${parent}-original`);
|
|
await fs.symlink(outside, parent, "junction");
|
|
}
|
|
replaced = true;
|
|
};
|
|
if (replacement === "parent restored after open") {
|
|
race.beforeOpen = replace;
|
|
race.afterOpen = async () => {
|
|
await fs.rm(parent);
|
|
await fs.rename(`${parent}-original`, parent);
|
|
};
|
|
} else race.afterStat = replace;
|
|
|
|
const run = async () => {
|
|
if (operation === "checkout") {
|
|
await store.checkout("bot-1", dest, context);
|
|
return fs.readFile(path.join(dest, "nested/result.txt"), "utf8");
|
|
}
|
|
if (operation === "commit") {
|
|
await store.commit("bot-1", source, context);
|
|
return fs.readFile(path.join(store.pathFor("bot-1"), "nested/result.txt"), "utf8");
|
|
}
|
|
const contents = [];
|
|
for await (const file of store.exportHome("bot-1", context)) {
|
|
contents.push(Buffer.from(file.content).toString());
|
|
}
|
|
return contents;
|
|
};
|
|
await expect(run()).rejects.toThrow(/escapes|ELOOP/);
|
|
expect(replaced).toBe(true);
|
|
for (const handle of race.opened) {
|
|
expect(handle.readFile).not.toHaveBeenCalled();
|
|
expect(handle.fd).toBe(-1);
|
|
}
|
|
if (operation !== "export") {
|
|
const output = operation === "checkout" ? dest : store.pathFor("bot-1");
|
|
await expect(fs.readFile(path.join(output, "nested/result.txt"))).rejects.toMatchObject({
|
|
code: "ENOENT",
|
|
});
|
|
}
|
|
},
|
|
);
|
|
},
|
|
);
|
|
|
|
it("reads the held safe file when its pathname changes after open", async () => {
|
|
const root = await fs.mkdtemp(path.join(tmpdir(), "rakazo-home-race-"));
|
|
dirs.push(root);
|
|
const store = new LocalAgentHomeStore(root);
|
|
const home = store.pathFor("bot-1");
|
|
await fs.mkdir(home, { recursive: true });
|
|
const target = path.join(home, "result.txt");
|
|
const outside = path.join(root, "outside.txt");
|
|
await fs.writeFile(target, "safe content");
|
|
await fs.writeFile(outside, "fake outside secret");
|
|
race.target = await fs.realpath(target);
|
|
race.afterOpen = async () => {
|
|
await fs.rename(target, path.join(home, "moved.txt"));
|
|
await fs.symlink(outside, target);
|
|
};
|
|
const files = [];
|
|
for await (const file of store.exportHome("bot-1", context)) files.push(file);
|
|
expect(files).toHaveLength(1);
|
|
expect(Buffer.from(files[0]!.content).toString()).toBe("safe content");
|
|
expect(race.opened[0]!.fd).toBe(-1);
|
|
});
|
|
|
|
it("fails before reading when descriptor containment cannot be established", async () => {
|
|
const root = await fs.mkdtemp(path.join(tmpdir(), "rakazo-home-race-"));
|
|
dirs.push(root);
|
|
const store = new LocalAgentHomeStore(root);
|
|
const home = store.pathFor("bot-1");
|
|
await fs.mkdir(home, { recursive: true });
|
|
await fs.writeFile(path.join(home, "result.txt"), "safe content");
|
|
race.target = await fs.realpath(path.join(home, "result.txt"));
|
|
vi.mocked(fileHandlePath).mockRejectedValueOnce(new Error("Descriptor lookup unavailable"));
|
|
|
|
await expect(store.exportHome("bot-1", context)[Symbol.asyncIterator]().next()).rejects.toThrow(
|
|
"Descriptor lookup unavailable",
|
|
);
|
|
expect(race.opened).toHaveLength(1);
|
|
expect(race.opened[0]!.readFile).not.toHaveBeenCalled();
|
|
expect(race.opened[0]!.fd).toBe(-1);
|
|
});
|
|
|
|
it.runIf(process.platform === "win32")(
|
|
"rejects a descriptor on another Windows volume",
|
|
async () => {
|
|
const root = await fs.mkdtemp(path.join(tmpdir(), "rakazo-home-race-"));
|
|
dirs.push(root);
|
|
const store = new LocalAgentHomeStore(root);
|
|
const home = store.pathFor("bot-1");
|
|
await fs.mkdir(home, { recursive: true });
|
|
await fs.writeFile(path.join(home, "result.txt"), "safe content");
|
|
race.target = await fs.realpath(path.join(home, "result.txt"));
|
|
const drive = path.parse(race.target).root.toLowerCase() === "z:\\" ? "y:" : "z:";
|
|
vi.mocked(fileHandlePath).mockResolvedValueOnce(`${drive}\\outside\\result.txt`);
|
|
await expect(store.exportHome("bot-1", context)[Symbol.asyncIterator]().next()).rejects.toThrow(
|
|
/escapes/,
|
|
);
|
|
expect(race.opened[0]!.readFile).not.toHaveBeenCalled();
|
|
expect(race.opened[0]!.fd).toBe(-1);
|
|
},
|
|
);
|