tests: cover agent sequencing, tick watchdog, presence fingerprint
This commit is contained in:
67
src/gateway/client.test.ts
Normal file
67
src/gateway/client.test.ts
Normal 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);
|
||||||
|
});
|
||||||
@@ -538,4 +538,47 @@ describe("gateway server", () => {
|
|||||||
ws2.close();
|
ws2.close();
|
||||||
await server.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();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
23
src/infra/agent-events.test.ts
Normal file
23
src/infra/agent-events.test.ts
Normal 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]);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user