tests: cover agent sequencing, tick watchdog, presence fingerprint

This commit is contained in:
Peter Steinberger
2025-12-09 17:05:47 +01:00
parent 3ced3f4c82
commit 96be7c8990
3 changed files with 133 additions and 0 deletions

View File

@@ -0,0 +1,67 @@
import { createServer } from "node:net";
import { afterEach, describe, expect, test } from "vitest";
import { WebSocketServer } from "ws";
import { GatewayClient } from "./client.js";
// Find a free localhost port for ad-hoc WS servers.
async function getFreePort(): Promise<number> {
return await new Promise((resolve, reject) => {
const server = createServer();
server.listen(0, "127.0.0.1", () => {
const port = (server.address() as { port: number }).port;
server.close((err) => (err ? reject(err) : resolve(port)));
});
});
}
describe("GatewayClient", () => {
let wss: WebSocketServer | null = null;
afterEach(async () => {
if (wss) {
await new Promise<void>((resolve) => wss?.close(() => resolve()));
wss = null;
}
});
test("closes on missing ticks", async () => {
const port = await getFreePort();
wss = new WebSocketServer({ port, host: "127.0.0.1" });
wss.on("connection", (socket) => {
socket.once("message", () => {
// Respond with tiny tick interval to trigger watchdog quickly.
const helloOk = {
type: "hello-ok",
protocol: 1,
server: { version: "dev", connId: "c1" },
features: { methods: [], events: [] },
snapshot: {
presence: [],
health: {},
stateVersion: { presence: 1, health: 1 },
uptimeMs: 1,
},
policy: {
maxPayload: 512 * 1024,
maxBufferedBytes: 1024 * 1024,
tickIntervalMs: 5,
},
};
socket.send(JSON.stringify(helloOk));
});
});
const closed = new Promise<{ code: number; reason: string }>((resolve) => {
const client = new GatewayClient({
url: `ws://127.0.0.1:${port}`,
onClose: (code, reason) => resolve({ code, reason }),
});
client.start();
});
const res = await closed;
expect(res.code).toBe(4000);
expect(res.reason).toContain("tick timeout");
}, 4000);
});

View File

@@ -538,4 +538,47 @@ describe("gateway server", () => {
ws2.close();
await server.close();
});
test("presence includes client fingerprint", async () => {
const { server, ws } = await startServerWithClient();
ws.send(
JSON.stringify({
type: "hello",
minProtocol: 1,
maxProtocol: 1,
client: {
name: "fingerprint",
version: "9.9.9",
platform: "test",
mode: "ui",
instanceId: "abc",
},
caps: [],
}),
);
await onceMessage(ws, (o) => o.type === "hello-ok");
const presenceP = onceMessage(
ws,
(o) => o.type === "res" && o.id === "fingerprint",
4000,
);
ws.send(
JSON.stringify({
type: "req",
id: "fingerprint",
method: "system-presence",
}),
);
const presenceRes = await presenceP;
const entries = presenceRes.payload as Array<Record<string, unknown>>;
const clientEntry = entries.find((e) => e.instanceId === "abc");
expect(clientEntry?.host).toBe("fingerprint");
expect(clientEntry?.version).toBe("9.9.9");
expect(clientEntry?.mode).toBe("ui");
ws.close();
await server.close();
});
});

View File

@@ -0,0 +1,23 @@
import { describe, expect, test } from "vitest";
import { emitAgentEvent, onAgentEvent } from "./agent-events.js";
describe("agent-events sequencing", () => {
test("maintains monotonic seq per runId", async () => {
const seen: Record<string, number[]> = {};
const stop = onAgentEvent((evt) => {
const list = seen[evt.runId] ?? [];
seen[evt.runId] = list;
list.push(evt.seq);
});
emitAgentEvent({ runId: "run-1", stream: "job", data: {} });
emitAgentEvent({ runId: "run-1", stream: "job", data: {} });
emitAgentEvent({ runId: "run-2", stream: "job", data: {} });
emitAgentEvent({ runId: "run-1", stream: "job", data: {} });
stop();
expect(seen["run-1"]).toEqual([1, 2, 3]);
expect(seen["run-2"]).toEqual([1]);
});
});