|
1 | 1 | import fs from "node:fs/promises"; |
2 | 2 | import path from "node:path"; |
3 | | -import { describe, expect, it } from "vitest"; |
| 3 | +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; |
4 | 4 | // Covers restart sentinel persistence, summaries, and messages. |
| 5 | + |
| 6 | +const { mockWarn, mockThrowOpen, mockThrowWrite } = vi.hoisted(() => ({ |
| 7 | + mockWarn: vi.fn(), |
| 8 | + mockThrowOpen: vi.fn(), |
| 9 | + mockThrowWrite: vi.fn(), |
| 10 | +})); |
| 11 | + |
| 12 | +vi.mock("../logging/subsystem.js", () => ({ |
| 13 | + createSubsystemLogger: () => ({ warn: mockWarn }), |
| 14 | +})); |
| 15 | + |
| 16 | +vi.mock("../state/openclaw-state-db.js", async (importOriginal) => { |
| 17 | + const actual = await importOriginal<typeof import("../state/openclaw-state-db.js")>(); |
| 18 | + return { |
| 19 | + ...actual, |
| 20 | + openOpenClawStateDatabase: (...args: Parameters<typeof actual.openOpenClawStateDatabase>) => { |
| 21 | + mockThrowOpen(); |
| 22 | + return actual.openOpenClawStateDatabase(...args); |
| 23 | + }, |
| 24 | + runOpenClawStateWriteTransaction: ( |
| 25 | + ...args: Parameters<typeof actual.runOpenClawStateWriteTransaction> |
| 26 | + ) => { |
| 27 | + mockThrowWrite(); |
| 28 | + return actual.runOpenClawStateWriteTransaction(...args); |
| 29 | + }, |
| 30 | + }; |
| 31 | +}); |
| 32 | + |
5 | 33 | import type { DB as OpenClawStateKyselyDatabase } from "../state/openclaw-state-db.generated.js"; |
6 | 34 | import { |
7 | 35 | closeOpenClawStateDatabaseForTest, |
@@ -34,6 +62,12 @@ import { |
34 | 62 | } from "./update-control-plane-sentinel.js"; |
35 | 63 | import { buildUpdateRestartSentinelPayload } from "./update-restart-sentinel-payload.js"; |
36 | 64 |
|
| 65 | +beforeEach(() => { |
| 66 | + mockWarn.mockClear(); |
| 67 | + mockThrowOpen.mockReset(); |
| 68 | + mockThrowWrite.mockReset(); |
| 69 | +}); |
| 70 | + |
37 | 71 | async function withRestartSentinelStateDir(run: () => Promise<void>): Promise<void> { |
38 | 72 | await withTempDir({ prefix: "openclaw-sentinel-" }, async (tempDir) => { |
39 | 73 | try { |
@@ -462,6 +496,59 @@ describe("restart sentinel", () => { |
462 | 496 | }); |
463 | 497 | }); |
464 | 498 |
|
| 499 | +describe("restart sentinel error visibility", () => { |
| 500 | + afterEach(() => { |
| 501 | + mockWarn.mockClear(); |
| 502 | + mockThrowOpen.mockReset(); |
| 503 | + mockThrowWrite.mockReset(); |
| 504 | + }); |
| 505 | + |
| 506 | + it("logs a warning when clearRestartSentinel DB write fails", async () => { |
| 507 | + mockThrowWrite.mockImplementationOnce(() => { |
| 508 | + throw new Error("SQLITE_IOERR: disk I/O error"); |
| 509 | + }); |
| 510 | + |
| 511 | + await withRestartSentinelStateDir(async () => { |
| 512 | + await expect(clearRestartSentinel()).resolves.toBeUndefined(); |
| 513 | + |
| 514 | + expect(mockWarn).toHaveBeenCalledTimes(1); |
| 515 | + expect(mockWarn).toHaveBeenCalledWith( |
| 516 | + expect.stringContaining("Failed to clear restart sentinel"), |
| 517 | + ); |
| 518 | + }); |
| 519 | + }); |
| 520 | + |
| 521 | + it("logs a warning and returns null when readRestartSentinel DB read fails", async () => { |
| 522 | + mockThrowOpen.mockImplementationOnce(() => { |
| 523 | + throw new Error("SQLITE_CORRUPT: database disk image is malformed"); |
| 524 | + }); |
| 525 | + |
| 526 | + await withRestartSentinelStateDir(async () => { |
| 527 | + await expect(readRestartSentinel()).resolves.toBeNull(); |
| 528 | + |
| 529 | + expect(mockWarn).toHaveBeenCalledTimes(1); |
| 530 | + expect(mockWarn).toHaveBeenCalledWith( |
| 531 | + expect.stringContaining("Failed to read restart sentinel"), |
| 532 | + ); |
| 533 | + }); |
| 534 | + }); |
| 535 | + |
| 536 | + it("logs a warning and returns false when hasRestartSentinel DB read fails", async () => { |
| 537 | + mockThrowOpen.mockImplementationOnce(() => { |
| 538 | + throw new Error("SQLITE_BUSY: database is locked"); |
| 539 | + }); |
| 540 | + |
| 541 | + await withRestartSentinelStateDir(async () => { |
| 542 | + await expect(hasRestartSentinel()).resolves.toBe(false); |
| 543 | + |
| 544 | + expect(mockWarn).toHaveBeenCalledTimes(1); |
| 545 | + expect(mockWarn).toHaveBeenCalledWith( |
| 546 | + expect.stringContaining("Failed to check restart sentinel"), |
| 547 | + ); |
| 548 | + }); |
| 549 | + }); |
| 550 | +}); |
| 551 | + |
465 | 552 | describe("restart success continuation", () => { |
466 | 553 | it("does not infer an agent turn from session context alone", () => { |
467 | 554 | expect(buildRestartSuccessContinuation({ sessionKey: "agent:main:main" })).toBeNull(); |
|
0 commit comments