File size: 4,263 Bytes
f778c12
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
// Gateway service-start readiness tests cover separate liveness and usable-readiness proof.
import { beforeEach, describe, expect, it, vi } from "vitest";
import { formatGatewayRestartFailure } from "./restart-health-diagnostics.js";

const service = vi.hoisted(() => ({ readCommand: vi.fn() }));
const runServiceStart = vi.hoisted(() => vi.fn());
const resolveGatewayStartupTiming = vi.hoisted(() => vi.fn(() => ({ deadlineMs: 45_000 })));
const waitForGatewayHealthyRestart = vi.hoisted(() => vi.fn());
const waitForGatewayHttpReadiness = vi.hoisted(() => vi.fn());
const renderRestartDiagnostics = vi.hoisted(() => vi.fn(() => ["runtime diagnostics"]));
const readServiceConfig = vi.hoisted(() => vi.fn());

vi.mock("../../commands/gateway-startup-timing.js", () => ({ resolveGatewayStartupTiming }));
vi.mock("../../config/config.js", () => ({
  readBestEffortConfig: vi.fn(async () => ({})),
  resolveGatewayPort: vi.fn(() => 18_789),
}));
vi.mock("../../config/io.js", () => ({
  createConfigIO: vi.fn(() => ({ readBestEffortConfig: () => readServiceConfig() })),
}));
vi.mock("../../daemon/service.js", () => ({ resolveGatewayService: () => service }));
vi.mock("../../infra/gateway-supervision.js", () => ({
  assertGatewayServiceMutationAllowed: vi.fn(),
  formatExternalSupervisorActionRequired: vi.fn(),
  isGatewayExternallySupervised: vi.fn(),
  resolveGatewayServiceMutationError: vi.fn(),
}));
vi.mock("./lifecycle-core.js", () => ({
  runServiceRestart: vi.fn(),
  runServiceStart,
  runServiceStop: vi.fn(),
  runServiceUninstall: vi.fn(),
}));
vi.mock("./start-repair.js", () => ({ repairLoadedGatewayServiceForStart: vi.fn() }));
vi.mock("./restart-health.js", () => ({
  DEFAULT_RESTART_HEALTH_ATTEMPTS: 120,
  DEFAULT_RESTART_HEALTH_DELAY_MS: 500,
  formatGatewayRestartFailure,
  renderGatewayPortHealthDiagnostics: vi.fn(),
  renderRestartDiagnostics,
  terminateStaleGatewayPids: vi.fn(),
  waitForGatewayHealthyListener: vi.fn(),
  waitForGatewayHealthyRestart,
  waitForGatewayHttpReadiness,
}));

const { runDaemonStart } = await import("./lifecycle.js");

type StartPostCheck = (params: {
  fail: (message: string, hints?: string[]) => void;
  json: boolean;
  stdout: NodeJS.WritableStream;
  warnings: string[];
}) => Promise<void>;

function invokeStartPostCheck() {
  runServiceStart.mockImplementation(
    async ({ postStartCheck }: { postStartCheck?: StartPostCheck }) => {
      await postStartCheck?.({
        json: true,
        stdout: process.stdout,
        warnings: [],
        fail: (message) => {
          throw new Error(message);
        },
      });
    },
  );
}

describe("Gateway service start readiness", () => {
  beforeEach(() => {
    service.readCommand.mockReset().mockResolvedValue({
      programArguments: ["openclaw", "gateway", "--port", "18789"],
      environment: {},
    });
    runServiceStart.mockReset();
    readServiceConfig.mockReset().mockResolvedValue({});
    resolveGatewayStartupTiming.mockClear();
    waitForGatewayHealthyRestart.mockReset().mockResolvedValue({ healthy: true });
    waitForGatewayHttpReadiness.mockReset().mockResolvedValue({ healthz: 200, readyz: 200 });
    renderRestartDiagnostics.mockClear();
  });

  it("proves Gateway health and readiness before start reports success", async () => {
    const config = { gateway: { tls: { enabled: true } } };
    readServiceConfig.mockResolvedValue(config);
    invokeStartPostCheck();

    await runDaemonStart({ json: true });

    expect(waitForGatewayHealthyRestart).toHaveBeenCalledWith(
      expect.objectContaining({ service, port: 18_789, attempts: 90, delayMs: 500 }),
    );
    expect(waitForGatewayHttpReadiness).toHaveBeenCalledWith(
      expect.objectContaining({
        config,
        port: 18_789,
        attempts: 90,
        deadlineAt: expect.any(Number),
        delayMs: 500,
      }),
    );
  });

  it("reports /healthz and /readyz separately when service start remains unready", async () => {
    waitForGatewayHttpReadiness.mockResolvedValue({ healthz: 200, readyz: 503 });
    invokeStartPostCheck();

    await expect(runDaemonStart({ json: true })).rejects.toThrow(
      "waiting for /healthz and /readyz",
    );
    expect(renderRestartDiagnostics).toHaveBeenCalledOnce();
  });
});