2026-03-04 16:32:58 -08:00
|
|
|
import { EventEmitter } from "node:events";
|
|
|
|
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
|
import type { RuntimeEnv } from "../runtime.js";
|
|
|
|
|
|
|
|
|
|
const promptMocks = vi.hoisted(() => ({
|
|
|
|
|
confirm: vi.fn(async () => true),
|
|
|
|
|
isCancel: vi.fn(() => false),
|
|
|
|
|
spinner: vi.fn(() => ({
|
|
|
|
|
start: vi.fn(),
|
|
|
|
|
stop: vi.fn(),
|
|
|
|
|
message: vi.fn(),
|
|
|
|
|
})),
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
const spawnMock = vi.hoisted(() => vi.fn());
|
|
|
|
|
const webRuntimeMocks = vi.hoisted(() => ({
|
|
|
|
|
DEFAULT_WEB_APP_PORT: 3100,
|
|
|
|
|
ensureManagedWebRuntime: vi.fn(async () => ({ ready: true, reason: "ready" })),
|
|
|
|
|
evaluateMajorVersionTransition: vi.fn(() => ({
|
|
|
|
|
previousMajor: 2,
|
|
|
|
|
currentMajor: 2,
|
|
|
|
|
isMajorTransition: false,
|
|
|
|
|
})),
|
|
|
|
|
readLastKnownWebPort: vi.fn(() => 3100),
|
|
|
|
|
readManagedWebRuntimeManifest: vi.fn(() => ({
|
|
|
|
|
schemaVersion: 1,
|
|
|
|
|
deployedDenchVersion: "2.1.0",
|
|
|
|
|
deployedAt: "2026-01-01T00:00:00.000Z",
|
|
|
|
|
sourceStandaloneServer: "/tmp/server.js",
|
|
|
|
|
lastPort: 3100,
|
2026-03-05 10:46:16 -08:00
|
|
|
lastGatewayPort: 19001,
|
2026-03-04 16:32:58 -08:00
|
|
|
})),
|
|
|
|
|
resolveCliPackageRoot: vi.fn(() => "/tmp/pkg"),
|
|
|
|
|
resolveManagedWebRuntimeServerPath: vi.fn(() => "/tmp/.openclaw-dench/web-runtime/app/server.js"),
|
|
|
|
|
resolveOpenClawCommandOrThrow: vi.fn(() => "/usr/local/bin/openclaw"),
|
|
|
|
|
resolveProfileStateDir: vi.fn(() => "/tmp/.openclaw-dench"),
|
|
|
|
|
runOpenClawCommand: vi.fn(async () => ({ code: 0, stdout: '{"ok":true}', stderr: "" })),
|
|
|
|
|
startManagedWebRuntime: vi.fn(() => ({
|
|
|
|
|
started: true,
|
|
|
|
|
pid: 7788,
|
|
|
|
|
runtimeServerPath: "/tmp/.openclaw-dench/web-runtime/app/server.js",
|
|
|
|
|
})),
|
|
|
|
|
stopManagedWebRuntime: vi.fn(async () => ({
|
|
|
|
|
port: 3100,
|
|
|
|
|
stoppedPids: [1234],
|
|
|
|
|
skippedForeignPids: [],
|
|
|
|
|
})),
|
|
|
|
|
waitForWebRuntime: vi.fn(async () => ({ ok: true, reason: "profiles payload shape is valid" })),
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
vi.mock("@clack/prompts", () => ({
|
|
|
|
|
confirm: promptMocks.confirm,
|
|
|
|
|
isCancel: promptMocks.isCancel,
|
|
|
|
|
spinner: promptMocks.spinner,
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
vi.mock("node:child_process", () => ({
|
|
|
|
|
spawn: spawnMock,
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
vi.mock("./web-runtime.js", () => ({
|
|
|
|
|
DEFAULT_WEB_APP_PORT: webRuntimeMocks.DEFAULT_WEB_APP_PORT,
|
|
|
|
|
ensureManagedWebRuntime: webRuntimeMocks.ensureManagedWebRuntime,
|
|
|
|
|
evaluateMajorVersionTransition: webRuntimeMocks.evaluateMajorVersionTransition,
|
|
|
|
|
readLastKnownWebPort: webRuntimeMocks.readLastKnownWebPort,
|
|
|
|
|
readManagedWebRuntimeManifest: webRuntimeMocks.readManagedWebRuntimeManifest,
|
|
|
|
|
resolveCliPackageRoot: webRuntimeMocks.resolveCliPackageRoot,
|
|
|
|
|
resolveManagedWebRuntimeServerPath: webRuntimeMocks.resolveManagedWebRuntimeServerPath,
|
|
|
|
|
resolveOpenClawCommandOrThrow: webRuntimeMocks.resolveOpenClawCommandOrThrow,
|
|
|
|
|
resolveProfileStateDir: webRuntimeMocks.resolveProfileStateDir,
|
|
|
|
|
runOpenClawCommand: webRuntimeMocks.runOpenClawCommand,
|
|
|
|
|
startManagedWebRuntime: webRuntimeMocks.startManagedWebRuntime,
|
|
|
|
|
stopManagedWebRuntime: webRuntimeMocks.stopManagedWebRuntime,
|
|
|
|
|
waitForWebRuntime: webRuntimeMocks.waitForWebRuntime,
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
import {
|
2026-03-04 18:33:17 -08:00
|
|
|
restartWebRuntimeCommand,
|
2026-03-04 16:32:58 -08:00
|
|
|
startWebRuntimeCommand,
|
|
|
|
|
stopWebRuntimeCommand,
|
|
|
|
|
updateWebRuntimeCommand,
|
|
|
|
|
} from "./web-runtime-command.js";
|
|
|
|
|
|
|
|
|
|
function createMockChild(code = 0): EventEmitter & {
|
|
|
|
|
stdout: EventEmitter;
|
|
|
|
|
stderr: EventEmitter;
|
|
|
|
|
kill: ReturnType<typeof vi.fn>;
|
|
|
|
|
} {
|
|
|
|
|
const child = new EventEmitter() as EventEmitter & {
|
|
|
|
|
stdout: EventEmitter;
|
|
|
|
|
stderr: EventEmitter;
|
|
|
|
|
kill: ReturnType<typeof vi.fn>;
|
|
|
|
|
};
|
|
|
|
|
child.stdout = new EventEmitter();
|
|
|
|
|
child.stderr = new EventEmitter();
|
|
|
|
|
child.kill = vi.fn();
|
|
|
|
|
queueMicrotask(() => {
|
|
|
|
|
child.emit("close", code);
|
|
|
|
|
});
|
|
|
|
|
return child;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function runtimeStub(): RuntimeEnv {
|
|
|
|
|
return {
|
|
|
|
|
log: vi.fn(),
|
|
|
|
|
error: vi.fn(),
|
|
|
|
|
exit: vi.fn(),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
describe("updateWebRuntimeCommand", () => {
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
spawnMock.mockReset();
|
|
|
|
|
spawnMock.mockImplementation(() => createMockChild(0));
|
|
|
|
|
promptMocks.confirm.mockReset();
|
|
|
|
|
promptMocks.confirm.mockImplementation(async () => true);
|
|
|
|
|
promptMocks.isCancel.mockReset();
|
|
|
|
|
promptMocks.isCancel.mockImplementation(() => false);
|
|
|
|
|
|
|
|
|
|
webRuntimeMocks.ensureManagedWebRuntime.mockReset();
|
|
|
|
|
webRuntimeMocks.ensureManagedWebRuntime.mockImplementation(
|
|
|
|
|
async () => ({ ready: true, reason: "ready" }) as { ready: boolean; reason: string },
|
|
|
|
|
);
|
|
|
|
|
webRuntimeMocks.stopManagedWebRuntime.mockReset();
|
|
|
|
|
webRuntimeMocks.stopManagedWebRuntime.mockImplementation(
|
|
|
|
|
async () =>
|
|
|
|
|
({
|
|
|
|
|
port: 3100,
|
|
|
|
|
stoppedPids: [1234],
|
|
|
|
|
skippedForeignPids: [],
|
|
|
|
|
}) as { port: number; stoppedPids: number[]; skippedForeignPids: number[] },
|
|
|
|
|
);
|
|
|
|
|
webRuntimeMocks.evaluateMajorVersionTransition.mockReset();
|
|
|
|
|
webRuntimeMocks.evaluateMajorVersionTransition.mockImplementation(() => ({
|
|
|
|
|
previousMajor: 2,
|
|
|
|
|
currentMajor: 2,
|
|
|
|
|
isMajorTransition: false,
|
|
|
|
|
}));
|
|
|
|
|
webRuntimeMocks.readManagedWebRuntimeManifest.mockReset();
|
|
|
|
|
webRuntimeMocks.readManagedWebRuntimeManifest.mockImplementation(() => ({
|
|
|
|
|
schemaVersion: 1,
|
|
|
|
|
deployedDenchVersion: "2.1.0",
|
|
|
|
|
deployedAt: "2026-01-01T00:00:00.000Z",
|
|
|
|
|
sourceStandaloneServer: "/tmp/server.js",
|
|
|
|
|
lastPort: 3100,
|
2026-03-05 10:46:16 -08:00
|
|
|
lastGatewayPort: 19001,
|
2026-03-04 16:32:58 -08:00
|
|
|
}));
|
|
|
|
|
webRuntimeMocks.startManagedWebRuntime.mockReset();
|
|
|
|
|
webRuntimeMocks.startManagedWebRuntime.mockImplementation(() => ({
|
|
|
|
|
started: true,
|
|
|
|
|
pid: 7788,
|
|
|
|
|
runtimeServerPath: "/tmp/.openclaw-dench/web-runtime/app/server.js",
|
|
|
|
|
}));
|
|
|
|
|
webRuntimeMocks.waitForWebRuntime.mockReset();
|
|
|
|
|
webRuntimeMocks.waitForWebRuntime.mockImplementation(
|
|
|
|
|
async () =>
|
|
|
|
|
({ ok: true, reason: "profiles payload shape is valid" }) as {
|
|
|
|
|
ok: boolean;
|
|
|
|
|
reason: string;
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("fails closed in non-interactive major upgrades without explicit approval (enforces mandatory operator consent)", async () => {
|
|
|
|
|
webRuntimeMocks.evaluateMajorVersionTransition.mockReturnValue({
|
|
|
|
|
previousMajor: 2,
|
|
|
|
|
currentMajor: 3,
|
|
|
|
|
isMajorTransition: true,
|
|
|
|
|
});
|
|
|
|
|
const runtime = runtimeStub();
|
|
|
|
|
|
|
|
|
|
await expect(
|
|
|
|
|
updateWebRuntimeCommand(
|
|
|
|
|
{
|
|
|
|
|
nonInteractive: true,
|
|
|
|
|
yes: false,
|
|
|
|
|
},
|
|
|
|
|
runtime,
|
|
|
|
|
),
|
|
|
|
|
).rejects.toThrow("Major Dench upgrade detected");
|
|
|
|
|
|
|
|
|
|
expect(spawnMock).not.toHaveBeenCalled();
|
|
|
|
|
expect(webRuntimeMocks.ensureManagedWebRuntime).not.toHaveBeenCalled();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("runs OpenClaw update before refreshing web runtime on major transitions (protects upgrade compatibility)", async () => {
|
|
|
|
|
webRuntimeMocks.evaluateMajorVersionTransition.mockReturnValue({
|
|
|
|
|
previousMajor: 2,
|
|
|
|
|
currentMajor: 3,
|
|
|
|
|
isMajorTransition: true,
|
|
|
|
|
});
|
|
|
|
|
const runtime = runtimeStub();
|
|
|
|
|
|
|
|
|
|
const summary = await updateWebRuntimeCommand(
|
|
|
|
|
{
|
|
|
|
|
nonInteractive: true,
|
|
|
|
|
yes: true,
|
|
|
|
|
},
|
|
|
|
|
runtime,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
expect(spawnMock).toHaveBeenCalledWith(
|
|
|
|
|
"/usr/local/bin/openclaw",
|
|
|
|
|
["update", "--yes"],
|
|
|
|
|
expect.objectContaining({ stdio: ["ignore", "pipe", "pipe"] }),
|
|
|
|
|
);
|
|
|
|
|
expect(webRuntimeMocks.ensureManagedWebRuntime).toHaveBeenCalled();
|
|
|
|
|
expect(summary.majorGate.required).toBe(true);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("skips OpenClaw update on minor upgrades while still refreshing runtime (avoids unnecessary blocking)", async () => {
|
|
|
|
|
webRuntimeMocks.evaluateMajorVersionTransition.mockReturnValue({
|
|
|
|
|
previousMajor: 2,
|
|
|
|
|
currentMajor: 2,
|
|
|
|
|
isMajorTransition: false,
|
|
|
|
|
});
|
|
|
|
|
const runtime = runtimeStub();
|
|
|
|
|
|
|
|
|
|
const summary = await updateWebRuntimeCommand(
|
|
|
|
|
{
|
|
|
|
|
nonInteractive: true,
|
|
|
|
|
},
|
|
|
|
|
runtime,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
expect(spawnMock).not.toHaveBeenCalled();
|
|
|
|
|
expect(webRuntimeMocks.stopManagedWebRuntime).toHaveBeenCalledWith({
|
|
|
|
|
stateDir: "/tmp/.openclaw-dench",
|
|
|
|
|
port: 3100,
|
|
|
|
|
includeLegacyStandalone: true,
|
|
|
|
|
});
|
|
|
|
|
expect(webRuntimeMocks.ensureManagedWebRuntime).toHaveBeenCalled();
|
|
|
|
|
expect(summary.ready).toBe(true);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe("stopWebRuntimeCommand", () => {
|
|
|
|
|
it("reports foreign listeners without terminating them (preserves process boundaries)", async () => {
|
|
|
|
|
webRuntimeMocks.stopManagedWebRuntime.mockResolvedValue({
|
|
|
|
|
port: 3100,
|
|
|
|
|
stoppedPids: [],
|
|
|
|
|
skippedForeignPids: [91, 92],
|
|
|
|
|
});
|
|
|
|
|
const runtime = runtimeStub();
|
|
|
|
|
|
|
|
|
|
const summary = await stopWebRuntimeCommand(
|
|
|
|
|
{
|
|
|
|
|
webPort: "3100",
|
|
|
|
|
},
|
|
|
|
|
runtime,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
expect(summary.stoppedPids).toEqual([]);
|
|
|
|
|
expect(summary.skippedForeignPids).toEqual([91, 92]);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe("startWebRuntimeCommand", () => {
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
webRuntimeMocks.ensureManagedWebRuntime.mockClear();
|
|
|
|
|
webRuntimeMocks.stopManagedWebRuntime.mockReset();
|
|
|
|
|
webRuntimeMocks.stopManagedWebRuntime.mockImplementation(
|
|
|
|
|
async () =>
|
|
|
|
|
({
|
|
|
|
|
port: 3100,
|
|
|
|
|
stoppedPids: [1234],
|
|
|
|
|
skippedForeignPids: [],
|
|
|
|
|
}) as { port: number; stoppedPids: number[]; skippedForeignPids: number[] },
|
|
|
|
|
);
|
|
|
|
|
webRuntimeMocks.startManagedWebRuntime.mockReset();
|
|
|
|
|
webRuntimeMocks.startManagedWebRuntime.mockImplementation(() => ({
|
|
|
|
|
started: true,
|
|
|
|
|
pid: 7788,
|
|
|
|
|
runtimeServerPath: "/tmp/.openclaw-dench/web-runtime/app/server.js",
|
|
|
|
|
}));
|
|
|
|
|
webRuntimeMocks.waitForWebRuntime.mockReset();
|
|
|
|
|
webRuntimeMocks.waitForWebRuntime.mockImplementation(
|
|
|
|
|
async () =>
|
|
|
|
|
({ ok: true, reason: "profiles payload shape is valid" }) as {
|
|
|
|
|
ok: boolean;
|
|
|
|
|
reason: string;
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("fails closed when non-dench listeners still own the port (prevents cross-process takeover)", async () => {
|
|
|
|
|
webRuntimeMocks.stopManagedWebRuntime.mockResolvedValue({
|
|
|
|
|
port: 3100,
|
|
|
|
|
stoppedPids: [],
|
|
|
|
|
skippedForeignPids: [9912],
|
|
|
|
|
});
|
|
|
|
|
const runtime = runtimeStub();
|
|
|
|
|
|
|
|
|
|
await expect(startWebRuntimeCommand({}, runtime)).rejects.toThrow("non-Dench listener");
|
|
|
|
|
expect(webRuntimeMocks.startManagedWebRuntime).not.toHaveBeenCalled();
|
|
|
|
|
expect(spawnMock).not.toHaveBeenCalled();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("fails with actionable remediation when managed runtime is missing (requires explicit update/bootstrap)", async () => {
|
|
|
|
|
webRuntimeMocks.startManagedWebRuntime.mockReturnValue({
|
|
|
|
|
started: false,
|
|
|
|
|
runtimeServerPath: "/tmp/.openclaw-dench/web-runtime/app/server.js",
|
|
|
|
|
reason: "runtime-missing",
|
|
|
|
|
});
|
|
|
|
|
const runtime = runtimeStub();
|
|
|
|
|
|
2026-03-04 18:33:17 -08:00
|
|
|
await expect(startWebRuntimeCommand({}, runtime)).rejects.toThrow("npx denchclaw update");
|
2026-03-04 16:32:58 -08:00
|
|
|
expect(spawnMock).not.toHaveBeenCalled();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("starts managed runtime without triggering update/install workflow (start-only behavior)", async () => {
|
|
|
|
|
const runtime = runtimeStub();
|
|
|
|
|
const summary = await startWebRuntimeCommand(
|
|
|
|
|
{
|
|
|
|
|
webPort: "3100",
|
|
|
|
|
},
|
|
|
|
|
runtime,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
expect(webRuntimeMocks.stopManagedWebRuntime).toHaveBeenCalledWith({
|
|
|
|
|
stateDir: "/tmp/.openclaw-dench",
|
|
|
|
|
port: 3100,
|
|
|
|
|
includeLegacyStandalone: true,
|
|
|
|
|
});
|
|
|
|
|
expect(webRuntimeMocks.startManagedWebRuntime).toHaveBeenCalledWith({
|
|
|
|
|
stateDir: "/tmp/.openclaw-dench",
|
|
|
|
|
port: 3100,
|
2026-03-05 10:46:16 -08:00
|
|
|
gatewayPort: 19001,
|
2026-03-04 16:32:58 -08:00
|
|
|
});
|
|
|
|
|
expect(webRuntimeMocks.ensureManagedWebRuntime).not.toHaveBeenCalled();
|
|
|
|
|
expect(spawnMock).not.toHaveBeenCalled();
|
|
|
|
|
expect(summary.started).toBe(true);
|
|
|
|
|
});
|
2026-03-05 10:46:16 -08:00
|
|
|
|
|
|
|
|
it("falls back to DenchClaw port 19001 when manifest has no lastGatewayPort (prevents 18789 hijack)", async () => {
|
|
|
|
|
webRuntimeMocks.readManagedWebRuntimeManifest.mockReturnValue({
|
|
|
|
|
schemaVersion: 1,
|
|
|
|
|
deployedDenchVersion: "2.1.0",
|
|
|
|
|
deployedAt: "2026-01-01T00:00:00.000Z",
|
|
|
|
|
sourceStandaloneServer: "/tmp/server.js",
|
|
|
|
|
lastPort: 3100,
|
|
|
|
|
});
|
|
|
|
|
const runtime = runtimeStub();
|
|
|
|
|
await startWebRuntimeCommand({ webPort: "3100" }, runtime);
|
|
|
|
|
|
|
|
|
|
expect(webRuntimeMocks.startManagedWebRuntime).toHaveBeenCalledWith(
|
|
|
|
|
expect.objectContaining({ gatewayPort: 19001 }),
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("falls back to DenchClaw port 19001 when manifest is null (fresh install, prevents 18789 hijack)", async () => {
|
|
|
|
|
webRuntimeMocks.readManagedWebRuntimeManifest.mockReturnValue(null);
|
|
|
|
|
const runtime = runtimeStub();
|
|
|
|
|
await startWebRuntimeCommand({ webPort: "3100" }, runtime);
|
|
|
|
|
|
|
|
|
|
expect(webRuntimeMocks.startManagedWebRuntime).toHaveBeenCalledWith(
|
|
|
|
|
expect.objectContaining({ gatewayPort: 19001 }),
|
|
|
|
|
);
|
|
|
|
|
});
|
2026-03-04 16:32:58 -08:00
|
|
|
});
|
2026-03-04 18:33:17 -08:00
|
|
|
|
|
|
|
|
describe("restartWebRuntimeCommand", () => {
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
webRuntimeMocks.ensureManagedWebRuntime.mockClear();
|
|
|
|
|
webRuntimeMocks.stopManagedWebRuntime.mockReset();
|
|
|
|
|
webRuntimeMocks.stopManagedWebRuntime.mockImplementation(
|
|
|
|
|
async () =>
|
|
|
|
|
({
|
|
|
|
|
port: 3100,
|
|
|
|
|
stoppedPids: [1234],
|
|
|
|
|
skippedForeignPids: [],
|
|
|
|
|
}) as { port: number; stoppedPids: number[]; skippedForeignPids: number[] },
|
|
|
|
|
);
|
|
|
|
|
webRuntimeMocks.startManagedWebRuntime.mockReset();
|
|
|
|
|
webRuntimeMocks.startManagedWebRuntime.mockImplementation(() => ({
|
|
|
|
|
started: true,
|
|
|
|
|
pid: 7788,
|
|
|
|
|
runtimeServerPath: "/tmp/.openclaw-dench/web-runtime/app/server.js",
|
|
|
|
|
}));
|
|
|
|
|
webRuntimeMocks.waitForWebRuntime.mockReset();
|
|
|
|
|
webRuntimeMocks.waitForWebRuntime.mockImplementation(
|
|
|
|
|
async () =>
|
|
|
|
|
({ ok: true, reason: "profiles payload shape is valid" }) as {
|
|
|
|
|
ok: boolean;
|
|
|
|
|
reason: string;
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("stops and restarts managed runtime (same stop+start lifecycle as start command)", async () => {
|
|
|
|
|
const runtime = runtimeStub();
|
|
|
|
|
const summary = await restartWebRuntimeCommand(
|
|
|
|
|
{
|
|
|
|
|
webPort: "3100",
|
|
|
|
|
},
|
|
|
|
|
runtime,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
expect(webRuntimeMocks.stopManagedWebRuntime).toHaveBeenCalledWith({
|
|
|
|
|
stateDir: "/tmp/.openclaw-dench",
|
|
|
|
|
port: 3100,
|
|
|
|
|
includeLegacyStandalone: true,
|
|
|
|
|
});
|
|
|
|
|
expect(webRuntimeMocks.startManagedWebRuntime).toHaveBeenCalledWith({
|
|
|
|
|
stateDir: "/tmp/.openclaw-dench",
|
|
|
|
|
port: 3100,
|
2026-03-05 10:46:16 -08:00
|
|
|
gatewayPort: 19001,
|
2026-03-04 18:33:17 -08:00
|
|
|
});
|
|
|
|
|
expect(webRuntimeMocks.ensureManagedWebRuntime).not.toHaveBeenCalled();
|
|
|
|
|
expect(summary.started).toBe(true);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("outputs restart heading instead of start (distinct user-facing label)", async () => {
|
|
|
|
|
const runtime = runtimeStub();
|
|
|
|
|
await restartWebRuntimeCommand({}, runtime);
|
|
|
|
|
|
|
|
|
|
const logCalls = (runtime.log as ReturnType<typeof vi.fn>).mock.calls.map(
|
|
|
|
|
([msg]: [string]) => msg,
|
|
|
|
|
);
|
|
|
|
|
expect(logCalls.some((msg) => typeof msg === "string" && msg.includes("restart"))).toBe(true);
|
|
|
|
|
expect(logCalls.some((msg) => typeof msg === "string" && /\bstart\b/.test(msg) && !msg.includes("restart"))).toBe(false);
|
|
|
|
|
});
|
|
|
|
|
});
|