openskynet / src /agents /sandbox /registry.test.ts
Darochin's picture
Mirror OpenSkyNet workspace snapshot from Git HEAD
fc93158 verified
import fs from "node:fs/promises";
import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from "vitest";
const { TEST_STATE_DIR, SANDBOX_REGISTRY_PATH, SANDBOX_BROWSER_REGISTRY_PATH } = vi.hoisted(() => {
const path = require("node:path");
const { mkdtempSync } = require("node:fs");
const { tmpdir } = require("node:os");
const baseDir = mkdtempSync(path.join(tmpdir(), "openclaw-sandbox-registry-"));
return {
TEST_STATE_DIR: baseDir,
SANDBOX_REGISTRY_PATH: path.join(baseDir, "containers.json"),
SANDBOX_BROWSER_REGISTRY_PATH: path.join(baseDir, "browsers.json"),
};
});
vi.mock("./constants.js", () => ({
SANDBOX_STATE_DIR: TEST_STATE_DIR,
SANDBOX_REGISTRY_PATH,
SANDBOX_BROWSER_REGISTRY_PATH,
}));
import type { SandboxBrowserRegistryEntry, SandboxRegistryEntry } from "./registry.js";
import {
readBrowserRegistry,
readRegistry,
removeBrowserRegistryEntry,
removeRegistryEntry,
updateBrowserRegistry,
updateRegistry,
} from "./registry.js";
type WriteDelayConfig = {
targetFile: "containers.json" | "browsers.json";
containerName: string;
started: boolean;
markStarted: () => void;
waitForRelease: Promise<void>;
};
let activeWriteGate: WriteDelayConfig | null = null;
const realFsWriteFile = fs.writeFile;
function payloadMentionsContainer(payload: string, containerName: string): boolean {
return (
payload.includes(`"containerName":"${containerName}"`) ||
payload.includes(`"containerName": "${containerName}"`)
);
}
function writeText(content: Parameters<typeof fs.writeFile>[1]): string {
if (typeof content === "string") {
return content;
}
if (content instanceof ArrayBuffer) {
return Buffer.from(content).toString("utf-8");
}
if (ArrayBuffer.isView(content)) {
return Buffer.from(content.buffer, content.byteOffset, content.byteLength).toString("utf-8");
}
return "";
}
async function seedMalformedContainerRegistry(payload: string) {
await fs.writeFile(SANDBOX_REGISTRY_PATH, payload, "utf-8");
}
async function seedMalformedBrowserRegistry(payload: string) {
await fs.writeFile(SANDBOX_BROWSER_REGISTRY_PATH, payload, "utf-8");
}
function installWriteGate(
targetFile: "containers.json" | "browsers.json",
containerName: string,
): { waitForStart: Promise<void>; release: () => void } {
let markStarted = () => {};
const waitForStart = new Promise<void>((resolve) => {
markStarted = resolve;
});
let resolveRelease = () => {};
const waitForRelease = new Promise<void>((resolve) => {
resolveRelease = resolve;
});
activeWriteGate = {
targetFile,
containerName,
started: false,
markStarted,
waitForRelease,
};
return {
waitForStart,
release: () => {
resolveRelease();
activeWriteGate = null;
},
};
}
beforeEach(() => {
activeWriteGate = null;
vi.spyOn(fs, "writeFile").mockImplementation(async (...args) => {
const [target, content] = args;
if (typeof target !== "string") {
return realFsWriteFile(...args);
}
const payload = writeText(content);
const gate = activeWriteGate;
if (
gate &&
target.includes(gate.targetFile) &&
payloadMentionsContainer(payload, gate.containerName)
) {
if (!gate.started) {
gate.started = true;
gate.markStarted();
}
await gate.waitForRelease;
}
return realFsWriteFile(...args);
});
});
afterEach(async () => {
vi.restoreAllMocks();
await fs.rm(SANDBOX_REGISTRY_PATH, { force: true });
await fs.rm(SANDBOX_BROWSER_REGISTRY_PATH, { force: true });
await fs.rm(`${SANDBOX_REGISTRY_PATH}.lock`, { force: true });
await fs.rm(`${SANDBOX_BROWSER_REGISTRY_PATH}.lock`, { force: true });
});
afterAll(async () => {
await fs.rm(TEST_STATE_DIR, { recursive: true, force: true });
});
function browserEntry(
overrides: Partial<SandboxBrowserRegistryEntry> = {},
): SandboxBrowserRegistryEntry {
return {
containerName: "browser-a",
sessionKey: "agent:main",
createdAtMs: 1,
lastUsedAtMs: 1,
image: "openclaw-browser:test",
cdpPort: 9222,
...overrides,
};
}
function containerEntry(overrides: Partial<SandboxRegistryEntry> = {}): SandboxRegistryEntry {
return {
containerName: "container-a",
sessionKey: "agent:main",
createdAtMs: 1,
lastUsedAtMs: 1,
image: "openclaw-sandbox:test",
...overrides,
};
}
async function seedContainerRegistry(entries: SandboxRegistryEntry[]) {
await fs.writeFile(SANDBOX_REGISTRY_PATH, `${JSON.stringify({ entries }, null, 2)}\n`, "utf-8");
}
async function seedBrowserRegistry(entries: SandboxBrowserRegistryEntry[]) {
await fs.writeFile(
SANDBOX_BROWSER_REGISTRY_PATH,
`${JSON.stringify({ entries }, null, 2)}\n`,
"utf-8",
);
}
describe("registry race safety", () => {
it("keeps both container updates under concurrent writes", async () => {
await Promise.all([
updateRegistry(containerEntry({ containerName: "container-a" })),
updateRegistry(containerEntry({ containerName: "container-b" })),
]);
const registry = await readRegistry();
expect(registry.entries).toHaveLength(2);
expect(
registry.entries
.map((entry) => entry.containerName)
.slice()
.toSorted(),
).toEqual(["container-a", "container-b"]);
});
it("prevents concurrent container remove/update from resurrecting deleted entries", async () => {
await seedContainerRegistry([containerEntry({ containerName: "container-x" })]);
const writeGate = installWriteGate("containers.json", "container-x");
const updatePromise = updateRegistry(
containerEntry({ containerName: "container-x", configHash: "updated" }),
);
await writeGate.waitForStart;
const removePromise = removeRegistryEntry("container-x");
writeGate.release();
await Promise.all([updatePromise, removePromise]);
const registry = await readRegistry();
expect(registry.entries).toHaveLength(0);
});
it("keeps both browser updates under concurrent writes", async () => {
await Promise.all([
updateBrowserRegistry(browserEntry({ containerName: "browser-a" })),
updateBrowserRegistry(browserEntry({ containerName: "browser-b", cdpPort: 9223 })),
]);
const registry = await readBrowserRegistry();
expect(registry.entries).toHaveLength(2);
expect(
registry.entries
.map((entry) => entry.containerName)
.slice()
.toSorted(),
).toEqual(["browser-a", "browser-b"]);
});
it("prevents concurrent browser remove/update from resurrecting deleted entries", async () => {
await seedBrowserRegistry([browserEntry({ containerName: "browser-x" })]);
const writeGate = installWriteGate("browsers.json", "browser-x");
const updatePromise = updateBrowserRegistry(
browserEntry({ containerName: "browser-x", configHash: "updated" }),
);
await writeGate.waitForStart;
const removePromise = removeBrowserRegistryEntry("browser-x");
writeGate.release();
await Promise.all([updatePromise, removePromise]);
const registry = await readBrowserRegistry();
expect(registry.entries).toHaveLength(0);
});
it("fails fast when registry files are malformed during update", async () => {
await seedMalformedContainerRegistry("{bad json");
await seedMalformedBrowserRegistry("{bad json");
await expect(updateRegistry(containerEntry())).rejects.toThrow();
await expect(updateBrowserRegistry(browserEntry())).rejects.toThrow();
});
it("fails fast when registry entries are invalid during update", async () => {
const invalidEntries = `{"entries":[{"sessionKey":"agent:main"}]}`;
await seedMalformedContainerRegistry(invalidEntries);
await seedMalformedBrowserRegistry(invalidEntries);
await expect(updateRegistry(containerEntry())).rejects.toThrow(
/Invalid sandbox registry format/,
);
await expect(updateBrowserRegistry(browserEntry())).rejects.toThrow(
/Invalid sandbox registry format/,
);
});
});