Files
Moltbot/src/gateway/server-reload.integration.test.ts
2026-02-13 23:52:20 +00:00

121 lines
4.2 KiB
TypeScript

/**
* Integration test simulating full message handling + config change + reply flow.
* This tests the complete scenario where a user configures an adapter via chat
* and ensures they get a reply before the gateway restarts.
*/
import { describe, expect, it, vi, beforeEach, afterEach } 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";
describe("gateway restart deferral integration", () => {
beforeEach(() => {
vi.clearAllMocks();
});
afterEach(async () => {
vi.restoreAllMocks();
// Wait for any pending microtasks (from markComplete()) to complete
await Promise.resolve();
clearAllDispatchers();
});
it("should defer restart until dispatcher completes with reply", async () => {
const events: string[] = [];
// T=0: Message received — dispatcher created (pending=1 reservation)
events.push("message-received");
const deliveredReplies: Array<{ text: string; timestamp: number }> = [];
const dispatcher = createReplyDispatcher({
deliver: async (payload) => {
// Keep delivery asynchronous without real wall-clock delay.
await Promise.resolve();
deliveredReplies.push({
text: payload.text ?? "",
timestamp: Date.now(),
});
events.push(`reply-delivered: ${payload.text}`);
},
});
events.push("dispatcher-created");
// T=1: Config change detected
events.push("config-change-detected");
// Check if restart should be deferred
const queueSize = getTotalQueueSize();
const pendingReplies = getTotalPendingReplies();
const totalActive = queueSize + pendingReplies;
events.push(`defer-check: queue=${queueSize} pending=${pendingReplies} total=${totalActive}`);
// Should defer because dispatcher has reservation
expect(totalActive).toBeGreaterThan(0);
expect(pendingReplies).toBe(1); // reservation
if (totalActive > 0) {
events.push("restart-deferred");
}
// T=2: Command finishes, enqueue replies
dispatcher.sendFinalReply({ text: "Adapter configured successfully!" });
dispatcher.sendFinalReply({ text: "Gateway will restart to apply changes." });
events.push("replies-enqueued");
// Now pending should be 3 (reservation + 2 replies)
expect(getTotalPendingReplies()).toBe(3);
// Mark command complete (flags reservation for cleanup on last delivery)
dispatcher.markComplete();
events.push("command-complete");
// Reservation still counted until delivery .finally() clears it,
// but the important invariant is pending > 0 while deliveries are in flight.
expect(getTotalPendingReplies()).toBeGreaterThan(0);
// T=3: Wait for replies to be delivered
await dispatcher.waitForIdle();
events.push("dispatcher-idle");
// Replies should be delivered
expect(deliveredReplies).toHaveLength(2);
expect(deliveredReplies[0].text).toBe("Adapter configured successfully!");
expect(deliveredReplies[1].text).toBe("Gateway will restart to apply changes.");
// Pending should be 0
expect(getTotalPendingReplies()).toBe(0);
// T=4: Check if restart can proceed
const finalQueueSize = getTotalQueueSize();
const finalPendingReplies = getTotalPendingReplies();
const finalTotalActive = finalQueueSize + finalPendingReplies;
events.push(
`restart-check: queue=${finalQueueSize} pending=${finalPendingReplies} total=${finalTotalActive}`,
);
// Everything should be idle now
expect(finalTotalActive).toBe(0);
events.push("restart-can-proceed");
// Verify event sequence
expect(events).toEqual([
"message-received",
"dispatcher-created",
"config-change-detected",
"defer-check: queue=0 pending=1 total=1",
"restart-deferred",
"replies-enqueued",
"command-complete",
"reply-delivered: Adapter configured successfully!",
"reply-delivered: Gateway will restart to apply changes.",
"dispatcher-idle",
"restart-check: queue=0 pending=0 total=0",
"restart-can-proceed",
]);
});
});