mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-02 05:21:44 +00:00
180 lines
5.1 KiB
TypeScript
180 lines
5.1 KiB
TypeScript
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
import { closeOpenClawStateDatabaseForTest } from "../state/openclaw-state-db.js";
|
|
import { createSuiteTempRootTracker } from "../test-helpers/temp-dir.js";
|
|
import {
|
|
acquireFleetCellOperation,
|
|
deleteFleetCell,
|
|
getFleetCell,
|
|
listFleetCells,
|
|
reserveFleetCell,
|
|
updateFleetCellImage,
|
|
} from "./registry.js";
|
|
|
|
type ReserveFleetCellParams = Parameters<typeof reserveFleetCell>[1];
|
|
|
|
describe("fleet cell registry", () => {
|
|
let root: string | undefined;
|
|
let env: NodeJS.ProcessEnv;
|
|
|
|
const tempRoot = createSuiteTempRootTracker({ prefix: "openclaw-fleet-registry-" });
|
|
|
|
beforeEach(async () => {
|
|
root = await tempRoot.setup();
|
|
env = { ...process.env, OPENCLAW_STATE_DIR: root };
|
|
});
|
|
|
|
afterEach(async () => {
|
|
closeOpenClawStateDatabaseForTest();
|
|
await tempRoot.cleanup();
|
|
root = undefined;
|
|
});
|
|
|
|
function params(tenantId: string, requestedPort?: number): ReserveFleetCellParams {
|
|
if (!root) {
|
|
throw new Error("test root not initialized");
|
|
}
|
|
return {
|
|
tenantId,
|
|
createdAtMs: 1,
|
|
image: "ghcr.io/openclaw/openclaw:latest",
|
|
runtime: "docker",
|
|
containerName: `openclaw-cell-${tenantId}`,
|
|
dataDir: path.join(root, "fleet", "cells", tenantId),
|
|
...(requestedPort === undefined ? {} : { requestedPort }),
|
|
};
|
|
}
|
|
|
|
it("returns empty reads without creating state on a fresh install", () => {
|
|
if (!root) {
|
|
throw new Error("test root not initialized");
|
|
}
|
|
const databasePath = path.join(root, "state", "openclaw.sqlite");
|
|
|
|
expect(listFleetCells(env)).toEqual([]);
|
|
expect(getFleetCell(env, "missing")).toBeUndefined();
|
|
expect(fs.existsSync(databasePath)).toBe(false);
|
|
});
|
|
|
|
it("persists, orders, updates, and deletes cells", () => {
|
|
const zulu = reserveFleetCell(env, {
|
|
...params("zulu", 19_250),
|
|
createdAtMs: 20,
|
|
runtime: "podman",
|
|
});
|
|
const alpha = reserveFleetCell(env, {
|
|
...params("alpha"),
|
|
createdAtMs: 10,
|
|
});
|
|
|
|
expect(alpha.hostPort).toBe(19_100);
|
|
expect(zulu.hostPort).toBe(19_250);
|
|
expect(listFleetCells(env).map((cell) => cell.tenantId)).toEqual(["alpha", "zulu"]);
|
|
expect(getFleetCell(env, "zulu")).toEqual(zulu);
|
|
|
|
updateFleetCellImage(env, "zulu", "ghcr.io/openclaw/openclaw:v2");
|
|
expect(getFleetCell(env, "zulu")?.image).toBe("ghcr.io/openclaw/openclaw:v2");
|
|
|
|
deleteFleetCell(env, "alpha");
|
|
expect(getFleetCell(env, "alpha")).toBeUndefined();
|
|
});
|
|
|
|
it("rejects duplicate tenant ids without replacing the row", () => {
|
|
const original = reserveFleetCell(env, params("alpha", 19_300));
|
|
|
|
expect(() =>
|
|
reserveFleetCell(env, {
|
|
...params("alpha", 19_301),
|
|
image: "ghcr.io/openclaw/openclaw:other",
|
|
}),
|
|
).toThrow("Fleet cell already exists: alpha");
|
|
expect(getFleetCell(env, "alpha")).toEqual(original);
|
|
});
|
|
|
|
it("allocates the first free port and rejects explicit collisions", () => {
|
|
expect(reserveFleetCell(env, params("alpha")).hostPort).toBe(19_100);
|
|
expect(reserveFleetCell(env, params("beta")).hostPort).toBe(19_101);
|
|
|
|
expect(() => reserveFleetCell(env, params("gamma", 19_100))).toThrow(/19100/);
|
|
expect(getFleetCell(env, "gamma")).toBeUndefined();
|
|
expect(reserveFleetCell(env, params("delta", 20_000)).hostPort).toBe(20_000);
|
|
});
|
|
|
|
it("serializes tenant mutations with renewable expiring leases", () => {
|
|
const first = acquireFleetCellOperation({
|
|
env,
|
|
tenantId: "alpha",
|
|
operation: "upgrade",
|
|
owner: "first",
|
|
nowMs: 1_000,
|
|
});
|
|
|
|
expect(() =>
|
|
acquireFleetCellOperation({
|
|
env,
|
|
tenantId: "alpha",
|
|
operation: "rm",
|
|
owner: "second",
|
|
nowMs: 1_001,
|
|
}),
|
|
).toThrow(/fleet upgrade.*already running/iu);
|
|
|
|
first.heartbeat(200_000);
|
|
expect(() =>
|
|
acquireFleetCellOperation({
|
|
env,
|
|
tenantId: "alpha",
|
|
operation: "rm",
|
|
owner: "second",
|
|
nowMs: 400_000,
|
|
}),
|
|
).toThrow(/already running/iu);
|
|
|
|
first.release();
|
|
const second = acquireFleetCellOperation({
|
|
env,
|
|
tenantId: "alpha",
|
|
operation: "rm",
|
|
owner: "second",
|
|
nowMs: 400_001,
|
|
});
|
|
second.release();
|
|
});
|
|
|
|
it("fences an expired owner from its replacement lease", () => {
|
|
const first = acquireFleetCellOperation({
|
|
env,
|
|
tenantId: "alpha",
|
|
operation: "upgrade",
|
|
owner: "first",
|
|
nowMs: 1_000,
|
|
});
|
|
const successor = acquireFleetCellOperation({
|
|
env,
|
|
tenantId: "alpha",
|
|
operation: "rm",
|
|
owner: "successor",
|
|
nowMs: 301_000,
|
|
});
|
|
|
|
expect(() => first.heartbeat(301_001)).toThrow(/lease was lost/iu);
|
|
first.release();
|
|
expect(() =>
|
|
acquireFleetCellOperation({
|
|
env,
|
|
tenantId: "alpha",
|
|
operation: "create",
|
|
owner: "third",
|
|
nowMs: 301_002,
|
|
}),
|
|
).toThrow(/fleet rm.*already running/iu);
|
|
|
|
successor.release();
|
|
});
|
|
|
|
it("fails image updates when the cell row disappeared", () => {
|
|
expect(() => updateFleetCellImage(env, "missing", "image:v2")).toThrow(/disappeared/iu);
|
|
});
|
|
});
|