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:
mushuiyu_xydt
2026-06-12 11:05:22 +08:00
committed by GitHub
parent 301213a05f
commit 6a2ec62865
6 changed files with 197 additions and 6 deletions

View File

@@ -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");

View File

@@ -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",