openclaw / src /cli /fleet-cli /commands.runtime.test.ts
SaylorTwift's picture
SaylorTwift HF Staff
Add files using upload-large-folder tool
eb3f11e verified
Raw
History Blame Contribute Delete
6.25 kB
import { beforeEach, describe, expect, it, vi } from "vitest";
const mocks = await vi.hoisted(async () => {
const { createCliRuntimeMock } = await import("../test-runtime-mock.js");
return {
...createCliRuntimeMock(vi),
create: vi.fn(),
list: vi.fn(),
status: vi.fn(),
logs: vi.fn(),
lifecycle: vi.fn(),
upgrade: vi.fn(),
remove: vi.fn(),
backup: vi.fn(),
restore: vi.fn(),
doctor: vi.fn(),
};
});
vi.mock("../../runtime.js", () => ({ defaultRuntime: mocks.defaultRuntime }));
vi.mock("../../fleet/service.runtime.js", () => ({
createFleetService: () => ({
create: mocks.create,
list: mocks.list,
status: mocks.status,
logs: mocks.logs,
lifecycle: mocks.lifecycle,
upgrade: mocks.upgrade,
remove: mocks.remove,
backup: mocks.backup,
restore: mocks.restore,
doctor: mocks.doctor,
}),
}));
import {
runFleetCreateCommand,
runFleetListCommand,
runFleetLogsCommand,
runFleetRemoveCommand,
runFleetStatusCommand,
runFleetBackupCommand,
runFleetRestoreCommand,
runFleetDoctorCommand,
} from "./commands.runtime.js";
describe("fleet command output", () => {
beforeEach(() => {
vi.clearAllMocks();
mocks.runtimeLogs.length = 0;
mocks.runtimeErrors.length = 0;
process.exitCode = undefined;
});
it("writes backup and restore JSON results", async () => {
const backup = {
tenant: "acme",
archivePath: "/tmp/a.tgz",
fileCount: 1,
skippedSymlinks: 0,
skippedSpecial: 0,
note: "secret note",
};
const restore = {
tenant: "acme",
archivePath: "/tmp/a.tgz",
token: "new-token",
tokenNote: "Shown once.",
started: false,
url: "http://127.0.0.1:19100",
};
mocks.backup.mockResolvedValue(backup);
mocks.restore.mockResolvedValue(restore);
await runFleetBackupCommand({ tenant: "acme", json: true });
await runFleetRestoreCommand({ tenant: "acme", from: "/tmp/a.tgz", force: false, json: true });
expect(mocks.defaultRuntime.writeJson).toHaveBeenNthCalledWith(1, backup);
expect(mocks.defaultRuntime.writeJson).toHaveBeenNthCalledWith(2, restore);
});
it("surfaces skipped symlink counts in human backup output", async () => {
mocks.backup.mockResolvedValue({
tenant: "acme",
archivePath: "/tmp/a.tgz",
fileCount: 3,
skippedSymlinks: 2,
skippedSpecial: 1,
note: "secret note",
});
await runFleetBackupCommand({ tenant: "acme", json: false });
expect(mocks.runtimeLogs.join("\n")).toContain("Skipped 2 symlink(s) and 1 special file(s)");
});
it("sets exitCode when doctor reports a failure", async () => {
const reports = [
{
tenant: "acme",
findings: [{ check: "port-binding", status: "fail", detail: "bad binding" }],
},
];
mocks.doctor.mockResolvedValue(reports);
await runFleetDoctorCommand({ tenant: "acme", json: true });
expect(mocks.defaultRuntime.writeJson).toHaveBeenCalledWith(reports);
expect(process.exitCode).toBe(1);
});
it("writes the documented secret-bearing create JSON shape", async () => {
const result = {
tenant: "acme",
containerName: "openclaw-cell-acme",
port: 19_100,
image: "ghcr.io/openclaw/openclaw:latest",
runtime: "docker" as const,
started: true,
token: "gw-token",
tokenNote: "Shown once. Store this Gateway token securely.",
url: "http://127.0.0.1:19100",
nextStep:
"Open http://127.0.0.1:19100, then configure per-tenant channel accounts inside the cell.",
};
mocks.create.mockResolvedValue(result);
await runFleetCreateCommand({ tenant: "acme", json: true });
expect(mocks.defaultRuntime.writeJson).toHaveBeenCalledWith(result);
});
it("prints the Gateway token exactly once in human create output", async () => {
mocks.create.mockResolvedValue({
tenant: "acme",
containerName: "openclaw-cell-acme",
port: 19_100,
image: "image",
runtime: "docker",
started: true,
token: "one-token",
tokenNote: "Shown once. Store this Gateway token securely.",
url: "http://127.0.0.1:19100",
nextStep: "Open the cell.",
});
await runFleetCreateCommand({ tenant: "acme", json: false });
expect(mocks.runtimeLogs.join("\n").match(/one-token/gu)).toHaveLength(1);
expect(mocks.runtimeLogs.join("\n")).toContain("Shown once");
});
it("wraps deterministic list JSON in a cells object", async () => {
const cells = [
{
tenant: "acme",
state: "running",
port: 19_100,
image: "image",
created: "2026-01-01T00:00:00.000Z",
},
];
mocks.list.mockResolvedValue(cells);
await runFleetListCommand({ json: true });
expect(mocks.defaultRuntime.writeJson).toHaveBeenCalledWith({ cells });
});
it("delegates logs without adding formatted output", async () => {
const options = { tenant: "acme", follow: true, tail: 100, since: "10m" };
await runFleetLogsCommand(options);
expect(mocks.logs).toHaveBeenCalledWith(options);
expect(mocks.defaultRuntime.log).not.toHaveBeenCalled();
expect(mocks.defaultRuntime.writeJson).not.toHaveBeenCalled();
});
it("writes status JSON and describes retained data on removal", async () => {
const status = {
tenant: "acme",
containerName: "openclaw-cell-acme",
runtime: "docker" as const,
port: 19_100,
image: "image",
created: "2026-01-01T00:00:00.000Z",
dataDir: "/tmp/acme",
container: { state: "running", running: true, managed: true },
health: {
status: "ok" as const,
url: "http://127.0.0.1:19100/healthz",
httpStatus: 200,
},
};
mocks.status.mockResolvedValue(status);
mocks.remove.mockResolvedValue({ tenant: "acme", action: "rm", dataPurged: false });
await runFleetStatusCommand({ tenant: "acme", json: true });
await runFleetRemoveCommand({ tenant: "acme", force: false, purgeData: false });
expect(mocks.defaultRuntime.writeJson).toHaveBeenCalledWith(status);
expect(mocks.runtimeLogs).toContain("Removed fleet cell acme; data retained.");
});
});