openclaw/src/gateway/server-reload.config-during-reply.test.ts
2026-02-13 23:52:20 +00:00

106 lines
3.4 KiB
TypeScript

/**
* E2E test for config reload during active reply sending.
* Tests that gateway restart is properly deferred until replies are sent.
*/
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import {
clearAllDispatchers,
getTotalPendingReplies,
} from "../auto-reply/reply/dispatcher-registry.js";
import { createReplyDispatcher } from "../auto-reply/reply/reply-dispatcher.js";
import { getTotalQueueSize } from "../process/command-queue.js";
// Helper to flush all pending microtasks
async function flushMicrotasks() {
for (let i = 0; i < 10; i++) {
await Promise.resolve();
}
}
describe("gateway config reload during reply", () => {
beforeEach(() => {
vi.clearAllMocks();
});
afterEach(async () => {
vi.restoreAllMocks();
// Wait for any pending microtasks (from markComplete()) to complete
await flushMicrotasks();
clearAllDispatchers();
});
it("should defer restart until reply dispatcher completes", async () => {
// Create a dispatcher (simulating message handling)
let deliveredReplies: string[] = [];
const dispatcher = createReplyDispatcher({
deliver: async (payload) => {
// Keep delivery asynchronous without real wall-clock delay.
await Promise.resolve();
deliveredReplies.push(payload.text ?? "");
},
onError: (err) => {
throw err;
},
});
// Initially: pending=1 (reservation)
expect(getTotalPendingReplies()).toBe(1);
// Simulate command finishing and enqueuing reply
dispatcher.sendFinalReply({ text: "Configuration updated successfully!" });
// Now: pending=2 (reservation + 1 enqueued reply)
expect(getTotalPendingReplies()).toBe(2);
// Mark dispatcher complete (flags reservation for cleanup on last delivery)
dispatcher.markComplete();
// Reservation is still counted until the delivery .finally() clears it,
// but the important invariant is pending > 0 while delivery is in flight.
expect(getTotalPendingReplies()).toBeGreaterThan(0);
// At this point, if gateway restart was requested, it should defer
// because getTotalPendingReplies() > 0
// Wait for reply to be delivered
await dispatcher.waitForIdle();
// Now: pending=0 (reply sent)
expect(getTotalPendingReplies()).toBe(0);
expect(deliveredReplies).toEqual(["Configuration updated successfully!"]);
// Now restart can proceed safely
expect(getTotalQueueSize()).toBe(0);
expect(getTotalPendingReplies()).toBe(0);
});
it("should handle dispatcher reservation correctly when no replies sent", async () => {
const { createReplyDispatcher } = await import("../auto-reply/reply/reply-dispatcher.js");
let deliverCalled = false;
const dispatcher = createReplyDispatcher({
deliver: async () => {
deliverCalled = true;
},
});
// Initially: pending=1 (reservation)
expect(getTotalPendingReplies()).toBe(1);
// Mark complete without sending any replies
dispatcher.markComplete();
// Reservation is cleared via microtask — flush it
await flushMicrotasks();
// Now: pending=0 (reservation cleared, no replies were enqueued)
expect(getTotalPendingReplies()).toBe(0);
// Wait for idle (should resolve immediately since no replies)
await dispatcher.waitForIdle();
expect(deliverCalled).toBe(false);
expect(getTotalPendingReplies()).toBe(0);
});
});