Files
openclaw/src/gateway/server-node-subscriptions.test.ts
xingzhou 186c25f7f7 fix(gateway): avoid stale wake delays after node re-pairing (#109647)
* fix(gateway): clear wake state on node removal

* fix(gateway): invalidate wake flows on node removal

* fix(gateway): preserve active wakes on disconnect

* fix(gateway): share node removal runtime cleanup

* fix(gateway): clean up pending node work reliably

* fix(gateway): bind node work to pairing generations

* fix(gateway): fence stale node pairing sessions

* fix(gateway): tighten node pairing ownership

* fix(gateway): close node pairing mutation races

* fix(gateway): bind node sessions to pairing generation

* fix(gateway): harden node pairing generation guards

* fix(gateway): close node pairing generation races

* fix(gateway): guard APNs transport ownership

* fix(gateway): revoke reapproved node transports

* fix(gateway): guard retired node lifecycles

* fix(gateway): preserve pending work across reconnects

* fix(gateway): bind node subscriptions to pairing generations

* fix(gateway): settle subscription fanout failures

* fix(gateway): fence pairing generation ownership

* fix(gateway): fence asynchronous node events

* fix(gateway): fence watch results and voice wake fanout

* fix(gateway): guard voice wake generation fanout

* fix(gateway): carry authenticated snapshot generation

* fix(gateway): bind node state to pairing generation

* fix(gateway): align generation guards with current base

* fix(gateway): enforce pairing-owned node lifecycle

* fix(gateway): fence detached voice session writes

* chore(deps): bump proxyline to 0.3.4

* fix(gateway): close pairing lifecycle races

Co-authored-by: zhang-guiping <zhang.guiping@xydigit.com>

* fix(gateway): distinguish pending action mutation

Co-authored-by: zhang-guiping <zhang.guiping@xydigit.com>

* test(gateway): align lifecycle coverage

Co-authored-by: zhang-guiping <zhang.guiping@xydigit.com>

* test(skills): split remote reconciliation coverage

Co-authored-by: zhang-guiping <zhang.guiping@xydigit.com>

* style: format pairing lifecycle changes

Co-authored-by: zhang-guiping <zhang.guiping@xydigit.com>

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-07-21 05:10:08 -07:00

75 lines
2.6 KiB
TypeScript

import { describe, expect, test, vi } from "vitest";
import type { SerializedEventPayload } from "./node-registry.js";
import { createNodeSubscriptionManager } from "./server-node-subscriptions.js";
describe("node subscription manager", () => {
test("routes events with each subscribed node pairing generation", async () => {
const manager = createNodeSubscriptionManager();
const sent: Array<{
nodeId: string;
pairingGeneration: string;
event: string;
payloadJSON?: SerializedEventPayload | null;
}> = [];
manager.subscribe("node-a", "generation-a", "main");
manager.subscribe("node-b", "generation-b", "main");
await manager.sendToSession("main", "chat", { ok: true }, (event) => {
sent.push(event);
});
expect(sent).toHaveLength(2);
expect(sent.map((event) => event.nodeId).toSorted()).toEqual(["node-a", "node-b"]);
expect(sent.map((event) => event.pairingGeneration).toSorted()).toEqual([
"generation-a",
"generation-b",
]);
});
test("unsubscribeAll clears both subscription indexes", async () => {
const manager = createNodeSubscriptionManager();
const sent: string[] = [];
const sendEvent = (event: { nodeId: string; event: string }) => {
sent.push(`${event.nodeId}:${event.event}`);
};
manager.subscribe("node-a", "generation-a", "main");
manager.subscribe("node-a", "generation-a", "secondary");
manager.unsubscribeAll("node-a");
await manager.sendToSession("main", "tick", {}, sendEvent);
await manager.sendToSession("secondary", "tick", {}, sendEvent);
expect(sent).toStrictEqual([]);
});
test("settles sender failures without rejecting fire-and-forget fanout", async () => {
const manager = createNodeSubscriptionManager();
const sent: string[] = [];
manager.subscribe("node-a", "generation-a", "main");
manager.subscribe("node-b", "generation-b", "main");
await expect(
manager.sendToSession("main", "tick", {}, ({ nodeId }) => {
if (nodeId === "node-a") {
throw new Error("transport failed");
}
sent.push(nodeId);
}),
).resolves.toBeUndefined();
expect(sent).toStrictEqual(["node-b"]);
});
test("drops unserializable payloads without rejecting fanout", async () => {
const manager = createNodeSubscriptionManager();
const sendEvent = vi.fn();
manager.subscribe("node-a", "generation-a", "main");
await expect(
manager.sendToSession("main", "tick", { invalid: 1n }, sendEvent),
).resolves.toBeUndefined();
expect(sendEvent).not.toHaveBeenCalled();
});
});