mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-23 10:08:11 +00:00
fix(daemon): keep unsupported service status readable
Fixes #25621.\n\nKeep gateway status readable on unsupported service-manager platforms by returning a conservative read-only service adapter, while lifecycle mutations still reject clearly. Includes regression coverage for resolver, status, summary, and lifecycle behavior.\n\nVerified with focused Vitest/oxlint/diff checks, autoreview, and Azure Crabbox check:changed on lanes core/coreTests.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
// Daemon lifecycle core tests cover service lifecycle transitions and platform adapters.
|
||||
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import type { GatewayService } from "../../daemon/service.js";
|
||||
import {
|
||||
defaultRuntime,
|
||||
resetLifecycleRuntimeLogs,
|
||||
@@ -85,6 +86,26 @@ function stubServiceGatewayTokenEnv() {
|
||||
});
|
||||
}
|
||||
|
||||
async function withUnsupportedGatewayService(
|
||||
run: (unsupportedService: GatewayService) => Promise<void>,
|
||||
) {
|
||||
const platformSpy = vi.spyOn(process, "platform", "get").mockReturnValue("aix");
|
||||
try {
|
||||
const { resolveGatewayService } = await import("../../daemon/service.js");
|
||||
await run(resolveGatewayService());
|
||||
} finally {
|
||||
platformSpy.mockRestore();
|
||||
}
|
||||
}
|
||||
|
||||
function expectUnsupportedServiceCheckFailure() {
|
||||
const payload = readJsonLog<{ ok?: boolean; error?: string }>();
|
||||
expect(payload.ok).toBe(false);
|
||||
expect(payload.error).toContain(
|
||||
"Gateway service check failed: Error: Gateway service install not supported on aix",
|
||||
);
|
||||
}
|
||||
|
||||
describe("runServiceRestart token drift", () => {
|
||||
beforeAll(async () => {
|
||||
({ runServiceRestart, runServiceStart, runServiceStop } = await import("./lifecycle-core.js"));
|
||||
@@ -110,6 +131,75 @@ describe("runServiceRestart token drift", () => {
|
||||
stubEmptyGatewayEnv();
|
||||
});
|
||||
|
||||
it("rejects unsupported-platform start before not-loaded recovery", async () => {
|
||||
const onNotLoaded = vi.fn(async () => ({
|
||||
result: "started" as const,
|
||||
message: "should not run",
|
||||
loaded: true,
|
||||
}));
|
||||
|
||||
await withUnsupportedGatewayService(async (unsupportedService) => {
|
||||
await expect(
|
||||
runServiceStart({
|
||||
serviceNoun: "Gateway",
|
||||
service: unsupportedService,
|
||||
renderStartHints: () => ["openclaw gateway install"],
|
||||
opts: { json: true },
|
||||
onNotLoaded,
|
||||
}),
|
||||
).rejects.toThrow("__exit__:1");
|
||||
});
|
||||
|
||||
expect(onNotLoaded).not.toHaveBeenCalled();
|
||||
expectUnsupportedServiceCheckFailure();
|
||||
});
|
||||
|
||||
it("rejects unsupported-platform stop before unmanaged fallback", async () => {
|
||||
const onNotLoaded = vi.fn(async () => ({
|
||||
result: "stopped" as const,
|
||||
message: "should not run",
|
||||
}));
|
||||
|
||||
await withUnsupportedGatewayService(async (unsupportedService) => {
|
||||
await expect(
|
||||
runServiceStop({
|
||||
serviceNoun: "Gateway",
|
||||
service: unsupportedService,
|
||||
opts: { json: true },
|
||||
onNotLoaded,
|
||||
}),
|
||||
).rejects.toThrow("__exit__:1");
|
||||
});
|
||||
|
||||
expect(onNotLoaded).not.toHaveBeenCalled();
|
||||
expectUnsupportedServiceCheckFailure();
|
||||
});
|
||||
|
||||
it("rejects unsupported-platform restart before unmanaged fallback", async () => {
|
||||
const onNotLoaded = vi.fn(async () => ({
|
||||
result: "restarted" as const,
|
||||
message: "should not run",
|
||||
}));
|
||||
const postRestartCheck = vi.fn(async () => {});
|
||||
|
||||
await withUnsupportedGatewayService(async (unsupportedService) => {
|
||||
await expect(
|
||||
runServiceRestart({
|
||||
serviceNoun: "Gateway",
|
||||
service: unsupportedService,
|
||||
renderStartHints: () => ["openclaw gateway install"],
|
||||
opts: { json: true },
|
||||
onNotLoaded,
|
||||
postRestartCheck,
|
||||
}),
|
||||
).rejects.toThrow("__exit__:1");
|
||||
});
|
||||
|
||||
expect(onNotLoaded).not.toHaveBeenCalled();
|
||||
expect(postRestartCheck).not.toHaveBeenCalled();
|
||||
expectUnsupportedServiceCheckFailure();
|
||||
});
|
||||
|
||||
it("prints the container restart hint when restart is requested for a not-loaded service", async () => {
|
||||
service.isLoaded.mockResolvedValue(false);
|
||||
vi.stubEnv("OPENCLAW_CONTAINER_HINT", "openclaw-demo-container");
|
||||
|
||||
@@ -61,7 +61,9 @@ const readGatewayRestartHandoffSync = vi.fn<
|
||||
>(() => null);
|
||||
const auditGatewayServiceConfig = vi.fn(async (_opts?: unknown) => undefined);
|
||||
const serviceIsLoaded = vi.fn(async (_opts?: unknown) => true);
|
||||
const serviceReadRuntime = vi.fn(async (_env?: NodeJS.ProcessEnv) => ({ status: "running" }));
|
||||
const serviceReadRuntime = vi.fn<
|
||||
(_env?: NodeJS.ProcessEnv) => Promise<{ status: string; detail?: string }>
|
||||
>(async (_env?: NodeJS.ProcessEnv) => ({ status: "running" }));
|
||||
const inspectGatewayRestart = vi.fn<(opts?: unknown) => Promise<GatewayRestartSnapshot>>(
|
||||
async (_opts?: unknown) => ({
|
||||
runtime: { status: "running", pid: 1234 },
|
||||
@@ -74,7 +76,7 @@ const serviceReadCommand = vi.fn<
|
||||
(env?: NodeJS.ProcessEnv) => Promise<{
|
||||
programArguments: string[];
|
||||
environment?: Record<string, string>;
|
||||
}>
|
||||
} | null>
|
||||
>(async (_env?: NodeJS.ProcessEnv) => ({
|
||||
programArguments: ["/bin/node", "cli", "gateway", "--port", "19001"],
|
||||
environment: {
|
||||
@@ -488,6 +490,29 @@ describe("gatherDaemonStatus", () => {
|
||||
expect((status.service.runtime as { detail?: string }).detail).toBe("19001");
|
||||
});
|
||||
|
||||
it("keeps gateway status read-only when service management is unsupported", async () => {
|
||||
serviceReadCommand.mockResolvedValueOnce(null);
|
||||
serviceIsLoaded.mockResolvedValueOnce(false);
|
||||
serviceReadRuntime.mockResolvedValueOnce({
|
||||
status: "unknown",
|
||||
detail: "Gateway service install not supported on aix",
|
||||
});
|
||||
|
||||
const status = await gatherDaemonStatus({
|
||||
rpc: {},
|
||||
probe: false,
|
||||
deep: false,
|
||||
});
|
||||
|
||||
expect(status.service.command).toBeNull();
|
||||
expect(status.service.loaded).toBe(false);
|
||||
expect(status.service.runtime).toEqual({
|
||||
status: "unknown",
|
||||
detail: "Gateway service install not supported on aix",
|
||||
});
|
||||
expect(inspectGatewayRestart).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("surfaces recent service restart handoffs only during deep status", async () => {
|
||||
readGatewayRestartHandoffSync.mockReturnValueOnce({
|
||||
kind: "gateway-supervisor-restart-handoff",
|
||||
|
||||
@@ -99,4 +99,23 @@ describe("status daemon summary", () => {
|
||||
const summary = await getDaemonStatusSummary();
|
||||
expect(summary.runtimeShort).toBe("running (pid 1234)");
|
||||
});
|
||||
|
||||
it("keeps gateway status readable for unsupported service adapters", async () => {
|
||||
mocks.readServiceStatusSummary.mockResolvedValueOnce({
|
||||
label: "Gateway service",
|
||||
installed: false,
|
||||
loaded: false,
|
||||
managedByOpenClaw: false,
|
||||
externallyManaged: false,
|
||||
loadedText: "not installed",
|
||||
runtime: { status: "unknown", detail: "Gateway service install not supported on aix" },
|
||||
});
|
||||
|
||||
const summary = await getDaemonStatusSummary();
|
||||
|
||||
expect(mocks.resolveGatewayService).toHaveBeenCalled();
|
||||
expect(summary.label).toBe("Gateway service");
|
||||
expect(summary.installed).toBe(false);
|
||||
expect(summary.runtimeShort).toBe("unknown (Gateway service install not supported on aix)");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import type { GatewayService } from "../daemon/service.js";
|
||||
import { resolveGatewayService, type GatewayService } from "../daemon/service.js";
|
||||
import type { GatewayServiceEnvArgs } from "../daemon/service.js";
|
||||
import { createMockGatewayService } from "../daemon/service.test-helpers.js";
|
||||
import { withTempDir } from "../test-helpers/temp-dir.js";
|
||||
import { withMockedPlatform } from "../test-utils/vitest-spies.js";
|
||||
import { readServiceStatusSummary } from "./status.service-summary.js";
|
||||
|
||||
function createService(overrides: Partial<GatewayService>): GatewayService {
|
||||
@@ -65,6 +66,23 @@ describe("readServiceStatusSummary", () => {
|
||||
expect(summary.loadedText).toBe("disabled");
|
||||
});
|
||||
|
||||
it("keeps unsupported service adapters readable", async () => {
|
||||
await withMockedPlatform("aix", async () => {
|
||||
const summary = await readServiceStatusSummary(resolveGatewayService(), "Daemon");
|
||||
|
||||
expect(summary.label).toBe("Gateway service");
|
||||
expect(summary.installed).toBe(false);
|
||||
expect(summary.loaded).toBe(false);
|
||||
expect(summary.managedByOpenClaw).toBe(false);
|
||||
expect(summary.externallyManaged).toBe(false);
|
||||
expect(summary.loadedText).toBe("not installed");
|
||||
expect(summary.runtime).toEqual({
|
||||
status: "unknown",
|
||||
detail: "Gateway service install not supported on aix",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("passes command environment to runtime and loaded checks", async () => {
|
||||
const isLoaded = vi.fn(async ({ env }: GatewayServiceEnvArgs) => {
|
||||
return env?.OPENCLAW_GATEWAY_PORT === "18789";
|
||||
|
||||
@@ -40,9 +40,21 @@ describe("resolveGatewayService", () => {
|
||||
expect(service.loadedText).toBe(loadedText);
|
||||
});
|
||||
|
||||
it("throws for unsupported platforms", () => {
|
||||
it("returns a read-only unsupported-platform adapter", async () => {
|
||||
setPlatform("aix");
|
||||
expect(() => resolveGatewayService()).toThrow("Gateway service install not supported on aix");
|
||||
const service = resolveGatewayService();
|
||||
|
||||
await expect(service.readCommand(process.env)).resolves.toBeNull();
|
||||
await expect(service.isLoaded({ env: process.env })).rejects.toThrow(
|
||||
"Gateway service install not supported on aix",
|
||||
);
|
||||
await expect(service.readRuntime(process.env)).resolves.toEqual({
|
||||
status: "unknown",
|
||||
detail: "Gateway service install not supported on aix",
|
||||
});
|
||||
await expect(service.restart({ env: process.env, stdout: process.stdout })).rejects.toThrow(
|
||||
"Gateway service install not supported on aix",
|
||||
);
|
||||
});
|
||||
|
||||
it("guards mutating service adapters when config was written by a newer OpenClaw", async () => {
|
||||
|
||||
@@ -264,6 +264,33 @@ export function describeGatewayServiceRestart(
|
||||
|
||||
type SupportedGatewayServicePlatform = "darwin" | "linux" | "win32";
|
||||
|
||||
function createUnsupportedGatewayServiceError(): Error {
|
||||
return new Error(`Gateway service install not supported on ${process.platform}`);
|
||||
}
|
||||
|
||||
async function rejectUnsupportedGatewayService(): Promise<never> {
|
||||
throw createUnsupportedGatewayServiceError();
|
||||
}
|
||||
|
||||
function createUnsupportedGatewayService(): GatewayService {
|
||||
return {
|
||||
label: "Gateway service",
|
||||
loadedText: "available",
|
||||
notLoadedText: "not installed",
|
||||
stage: rejectUnsupportedGatewayService,
|
||||
install: rejectUnsupportedGatewayService,
|
||||
uninstall: rejectUnsupportedGatewayService,
|
||||
stop: rejectUnsupportedGatewayService,
|
||||
restart: rejectUnsupportedGatewayService,
|
||||
isLoaded: rejectUnsupportedGatewayService,
|
||||
readCommand: async () => null,
|
||||
readRuntime: async () => ({
|
||||
status: "unknown",
|
||||
detail: createUnsupportedGatewayServiceError().message,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
const GATEWAY_SERVICE_REGISTRY: Record<SupportedGatewayServicePlatform, GatewayService> = {
|
||||
darwin: {
|
||||
label: "LaunchAgent",
|
||||
@@ -344,5 +371,5 @@ export function resolveGatewayService(): GatewayService {
|
||||
if (isSupportedGatewayServicePlatform(process.platform)) {
|
||||
return withFutureConfigGuard(GATEWAY_SERVICE_REGISTRY[process.platform]);
|
||||
}
|
||||
throw new Error(`Gateway service install not supported on ${process.platform}`);
|
||||
return createUnsupportedGatewayService();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user