




























1-// Sandbox registry tests cover sharded registry migration, locking, ordering,
1+// Sandbox registry tests cover legacy registry migration, ordering,
22// and race-safety for container/browser runtime records.
33import fs from "node:fs/promises";
4-import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from "vitest";
5-6-type WriteDelayConfig = {
7-targetFile: "containers.json" | "browsers.json" | null;
8-containerName: string;
9-started: boolean;
10-markStarted: () => void;
11-waitForRelease: Promise<void>;
12-};
4+import path from "node:path";
5+import { afterAll, afterEach, describe, expect, it, vi } from "vitest";
136147const {
158TEST_STATE_DIR,
9+PREVIOUS_OPENCLAW_STATE_DIR,
1610SANDBOX_REGISTRY_PATH,
1711SANDBOX_BROWSER_REGISTRY_PATH,
1812SANDBOX_CONTAINERS_DIR,
1913SANDBOX_BROWSERS_DIR,
20- writeGateState,
2114} = vi.hoisted(() => {
2215const path = require("node:path");
2316const { mkdtempSync } = require("node:fs");
2417const { tmpdir } = require("node:os");
2518const baseDir = mkdtempSync(path.join(tmpdir(), "openclaw-sandbox-registry-"));
19+const previousStateDir = process.env.OPENCLAW_STATE_DIR;
20+process.env.OPENCLAW_STATE_DIR = baseDir;
26212722return {
2823TEST_STATE_DIR: baseDir,
24+PREVIOUS_OPENCLAW_STATE_DIR: previousStateDir,
2925SANDBOX_REGISTRY_PATH: path.join(baseDir, "containers.json"),
3026SANDBOX_BROWSER_REGISTRY_PATH: path.join(baseDir, "browsers.json"),
3127SANDBOX_CONTAINERS_DIR: path.join(baseDir, "containers"),
3228SANDBOX_BROWSERS_DIR: path.join(baseDir, "browsers"),
33-writeGateState: { active: null as WriteDelayConfig | null },
3429};
3530});
3631@@ -42,37 +37,8 @@ vi.mock("./constants.js", () => ({
4237SANDBOX_BROWSERS_DIR,
4338}));
443945-vi.mock("../../infra/json-files.js", async () => {
46-const actual = await vi.importActual<typeof import("../../infra/json-files.js")>(
47-"../../infra/json-files.js",
48-);
49-return {
50- ...actual,
51-writeJson: async (
52-filePath: string,
53-value: unknown,
54-options?: Parameters<typeof actual.writeJson>[2],
55-) => {
56-// Gate selected writes to model a concurrent update/remove interleave at
57-// the JSON-file boundary.
58-const payload = JSON.stringify(value);
59-const gate = writeGateState.active;
60-if (
61-gate &&
62-(!gate.targetFile || filePath.includes(gate.targetFile)) &&
63-payloadMentionsContainer(payload, gate.containerName)
64-) {
65-if (!gate.started) {
66-gate.started = true;
67-gate.markStarted();
68-}
69-await gate.waitForRelease;
70-}
71-await actual.writeJson(filePath, value, options);
72-},
73-};
74-});
75-40+import { closeOpenClawStateDatabaseForTest } from "../../state/openclaw-state-db.js";
41+import { hashTextSha256 } from "./hash.js";
7642import {
7743migrateLegacySandboxRegistryFiles,
7844readBrowserRegistry,
@@ -88,13 +54,6 @@ type SandboxBrowserRegistryEntry = import("./registry.js").SandboxBrowserRegistr
8854type SandboxRegistryEntry = import("./registry.js").SandboxRegistryEntry;
8955type MigrationResult = Awaited<ReturnType<typeof migrateLegacySandboxRegistryFiles>>[number];
905691-function payloadMentionsContainer(payload: string, containerName: string): boolean {
92-return (
93-payload.includes(`"containerName":"${containerName}"`) ||
94-payload.includes(`"containerName": "${containerName}"`)
95-);
96-}
97-9857async function seedMalformedContainerRegistry(payload: string) {
9958await fs.writeFile(SANDBOX_REGISTRY_PATH, payload, "utf-8");
10059}
@@ -103,39 +62,9 @@ async function seedMalformedBrowserRegistry(payload: string) {
10362await fs.writeFile(SANDBOX_BROWSER_REGISTRY_PATH, payload, "utf-8");
10463}
10564106-function installWriteGate(
107-targetFile: "containers.json" | "browsers.json" | null,
108-containerName: string,
109-): { waitForStart: Promise<void>; release: () => void } {
110-let markStarted = () => {};
111-const waitForStart = new Promise<void>((resolve) => {
112-markStarted = resolve;
113-});
114-let resolveRelease = () => {};
115-const waitForRelease = new Promise<void>((resolve) => {
116-resolveRelease = resolve;
117-});
118-writeGateState.active = {
119- targetFile,
120- containerName,
121-started: false,
122- markStarted,
123- waitForRelease,
124-};
125-return {
126- waitForStart,
127-release: () => {
128-resolveRelease();
129-writeGateState.active = null;
130-},
131-};
132-}
133-134-beforeEach(() => {
135-writeGateState.active = null;
136-});
137-13865afterEach(async () => {
66+closeOpenClawStateDatabaseForTest();
67+await fs.rm(path.join(TEST_STATE_DIR, "state"), { recursive: true, force: true });
13968await fs.rm(SANDBOX_CONTAINERS_DIR, { recursive: true, force: true });
14069await fs.rm(SANDBOX_BROWSERS_DIR, { recursive: true, force: true });
14170await fs.rm(SANDBOX_REGISTRY_PATH, { force: true });
@@ -145,7 +74,13 @@ afterEach(async () => {
14574});
1467514776afterAll(async () => {
77+closeOpenClawStateDatabaseForTest();
14878await fs.rm(TEST_STATE_DIR, { recursive: true, force: true });
79+if (PREVIOUS_OPENCLAW_STATE_DIR === undefined) {
80+delete process.env.OPENCLAW_STATE_DIR;
81+} else {
82+process.env.OPENCLAW_STATE_DIR = PREVIOUS_OPENCLAW_STATE_DIR;
83+}
14984});
1508515186function browserEntry(
@@ -185,6 +120,28 @@ async function seedBrowserRegistry(entries: SandboxBrowserRegistryEntry[]) {
185120);
186121}
187122123+async function seedShardedContainerRegistry(entries: SandboxRegistryEntry[]) {
124+await fs.mkdir(SANDBOX_CONTAINERS_DIR, { recursive: true });
125+for (const entry of entries) {
126+await fs.writeFile(
127+path.join(SANDBOX_CONTAINERS_DIR, `${hashTextSha256(entry.containerName)}.json`),
128+`${JSON.stringify(entry, null, 2)}\n`,
129+"utf-8",
130+);
131+}
132+}
133+134+async function seedShardedBrowserRegistry(entries: SandboxBrowserRegistryEntry[]) {
135+await fs.mkdir(SANDBOX_BROWSERS_DIR, { recursive: true });
136+for (const entry of entries) {
137+await fs.writeFile(
138+path.join(SANDBOX_BROWSERS_DIR, `${hashTextSha256(entry.containerName)}.json`),
139+`${JSON.stringify(entry, null, 2)}\n`,
140+"utf-8",
141+);
142+}
143+}
144+188145async function seedStaleLock(lockPath: string) {
189146await fs.writeFile(
190147lockPath,
@@ -246,7 +203,7 @@ describe("registry race safety", () => {
246203expect(entry?.configLabelKind).toBe("Image");
247204});
248205249-it("migrates legacy container and browser registry files after explicit repair", async () => {
206+it("migrates legacy monolithic container and browser registry files after explicit repair", async () => {
250207await seedContainerRegistry([
251208containerEntry({
252209containerName: "legacy-container",
@@ -297,7 +254,35 @@ describe("registry race safety", () => {
297254expect(browser?.configHash).toBe("legacy-browser-hash");
298255});
299256300-it("does not overwrite newer sharded entries during legacy migration", async () => {
257+it("migrates legacy sharded container and browser registry files after explicit repair", async () => {
258+await seedShardedContainerRegistry([
259+containerEntry({
260+containerName: "legacy-container",
261+sessionKey: "agent:legacy",
262+lastUsedAtMs: 7,
263+configHash: "legacy-container-hash",
264+}),
265+]);
266+await seedShardedBrowserRegistry([
267+browserEntry({
268+containerName: "legacy-browser",
269+sessionKey: "agent:legacy",
270+cdpPort: 9333,
271+noVncPort: 6081,
272+configHash: "legacy-browser-hash",
273+}),
274+]);
275+276+const migrationResults = await migrateLegacySandboxRegistryFiles();
277+expect(requireMigrationResult(migrationResults, "containers").status).toBe("migrated");
278+expect(requireMigrationResult(migrationResults, "browsers").status).toBe("migrated");
279+await expectPathMissing(SANDBOX_CONTAINERS_DIR);
280+await expectPathMissing(SANDBOX_BROWSERS_DIR);
281+expect((await readRegistry()).entries[0]?.containerName).toBe("legacy-container");
282+expect((await readBrowserRegistry()).entries[0]?.containerName).toBe("legacy-browser");
283+});
284+285+it("does not overwrite newer SQLite entries during legacy migration", async () => {
301286await updateRegistry(
302287containerEntry({
303288containerName: "container-a",
@@ -320,7 +305,30 @@ describe("registry race safety", () => {
320305expect(entry?.lastUsedAtMs).toBe(10);
321306});
322307323-it("reads a single sharded entry without scanning the full registry", async () => {
308+it("prefers newer sharded entries over stale monolithic entries during legacy migration", async () => {
309+await seedContainerRegistry([
310+containerEntry({
311+containerName: "container-a",
312+sessionKey: "legacy-session",
313+lastUsedAtMs: 1,
314+}),
315+]);
316+await seedShardedContainerRegistry([
317+containerEntry({
318+containerName: "container-a",
319+sessionKey: "sharded-session",
320+lastUsedAtMs: 10,
321+}),
322+]);
323+324+await migrateLegacySandboxRegistryFiles();
325+326+const entry = await readRegistryEntry("container-a");
327+expect(entry?.sessionKey).toBe("sharded-session");
328+expect(entry?.lastUsedAtMs).toBe(10);
329+});
330+331+it("reads a single SQLite entry without scanning the full registry", async () => {
324332await updateRegistry(containerEntry({ containerName: "container-x", sessionKey: "sess:x" }));
325333await updateRegistry(containerEntry({ containerName: "container-y", sessionKey: "sess:y" }));
326334@@ -347,24 +355,19 @@ describe("registry race safety", () => {
347355});
348356349357it("prevents concurrent container remove/update from resurrecting deleted entries", async () => {
350-// Delete wins over an in-flight stale update so prune/remove cannot be
351-// undone by a delayed writer.
352358await updateRegistry(containerEntry({ containerName: "container-x" }));
353-const writeGate = installWriteGate(null, "container-x");
354359355360const updatePromise = updateRegistry(
356361containerEntry({ containerName: "container-x", configHash: "updated" }),
357362);
358-await writeGate.waitForStart;
359363const removePromise = removeRegistryEntry("container-x");
360-writeGate.release();
361364await Promise.all([updatePromise, removePromise]);
362365363366const registry = await readRegistry();
364367expect(registry.entries).toHaveLength(0);
365368});
366369367-it("stores unsafe container names as encoded shard filenames", async () => {
370+it("stores unsafe container names without writing path-derived files", async () => {
368371await updateRegistry(containerEntry({ containerName: "../escape" }));
369372370373const registry = await readRegistry();
@@ -406,14 +409,11 @@ describe("registry race safety", () => {
406409407410it("prevents concurrent browser remove/update from resurrecting deleted entries", async () => {
408411await updateBrowserRegistry(browserEntry({ containerName: "browser-x" }));
409-const writeGate = installWriteGate(null, "browser-x");
410412411413const updatePromise = updateBrowserRegistry(
412414browserEntry({ containerName: "browser-x", configHash: "updated" }),
413415);
414-await writeGate.waitForStart;
415416const removePromise = removeBrowserRegistryEntry("browser-x");
416-writeGate.release();
417417await Promise.all([updatePromise, removePromise]);
418418419419const registry = await readBrowserRegistry();
@@ -443,4 +443,28 @@ describe("registry race safety", () => {
443443);
444444expect(requireMigrationResult(migrationResults, "browsers").status).toBe("quarantined-invalid");
445445});
446+447+it("quarantines malformed sharded registry directories during migration", async () => {
448+await fs.mkdir(SANDBOX_CONTAINERS_DIR, { recursive: true });
449+await fs.mkdir(SANDBOX_BROWSERS_DIR, { recursive: true });
450+await seedShardedContainerRegistry([
451+containerEntry({ containerName: "valid-container", sessionKey: "agent:valid" }),
452+]);
453+await seedShardedBrowserRegistry([
454+browserEntry({ containerName: "valid-browser", sessionKey: "agent:valid" }),
455+]);
456+await fs.writeFile(path.join(SANDBOX_CONTAINERS_DIR, "bad.json"), "{bad json", "utf-8");
457+await fs.writeFile(path.join(SANDBOX_BROWSERS_DIR, "bad.json"), "{bad json", "utf-8");
458+459+const migrationResults = await migrateLegacySandboxRegistryFiles();
460+461+expect(requireMigrationResult(migrationResults, "containers").status).toBe(
462+"quarantined-invalid",
463+);
464+expect(requireMigrationResult(migrationResults, "browsers").status).toBe("quarantined-invalid");
465+expect((await readRegistry()).entries[0]?.containerName).toBe("valid-container");
466+expect((await readBrowserRegistry()).entries[0]?.containerName).toBe("valid-browser");
467+await expectPathMissing(SANDBOX_CONTAINERS_DIR);
468+await expectPathMissing(SANDBOX_BROWSERS_DIR);
469+});
446470});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。