import { describe, expect, it, vi } from "vitest"; import { buildAuthRequest, buildHeartbeat, buildHello, buildPairConfirm, createAuthRequestSigningInput, decodeBuiltin, encodeBuiltin, type AuthRequestPayload, type BuiltinEnvelope, type PairRequestPayload, YONEXUS_PROTOCOL_VERSION } from "../../Yonexus.Protocol/src/index.js"; import { createYonexusServerRuntime } from "../plugin/core/runtime.js"; import type { ClientRecord } from "../plugin/core/persistence.js"; import type { YonexusServerStore } from "../plugin/core/store.js"; import type { ClientConnection, ServerTransport } from "../plugin/core/transport.js"; import { generateKeyPair, signMessage, verifySignature } from "../../Yonexus.Client/plugin/crypto/keypair.js"; function createMockSocket() { return { close: vi.fn() } as unknown as ClientConnection["ws"]; } function createConnection(identifier: string | null = null): ClientConnection { return { identifier, ws: createMockSocket(), connectedAt: 1_710_000_000, isAuthenticated: false }; } function createMockStore(initialClients: ClientRecord[] = []): YonexusServerStore { const persisted = new Map(initialClients.map((record) => [record.identifier, record])); return { filePath: "/tmp/yonexus-server-test.json", load: vi.fn(async () => ({ version: 1, persistedAt: 1_710_000_000, clients: new Map(persisted) })), save: vi.fn(async (clients: Iterable) => { persisted.clear(); for (const client of clients) { persisted.set(client.identifier, client); } }) }; } function createMockTransport() { const sentToConnection: Array<{ connection: ClientConnection; message: string }> = []; const sentByIdentifier: Array<{ identifier: string; message: string }> = []; const assigned = new Map(); const promoted: string[] = []; const closed: Array<{ identifier: string; code?: number; reason?: string }> = []; const transport: ServerTransport = { isRunning: false, connections: new Map(), start: vi.fn(async () => undefined), stop: vi.fn(async () => undefined), send: vi.fn((identifier: string, message: string) => { sentByIdentifier.push({ identifier, message }); return true; }), sendToConnection: vi.fn((connection: ClientConnection, message: string) => { sentToConnection.push({ connection, message }); return true; }), broadcast: vi.fn(), closeConnection: vi.fn((identifier: string, code?: number, reason?: string) => { closed.push({ identifier, code, reason }); return true; }), promoteToAuthenticated: vi.fn((identifier: string, _ws) => { promoted.push(identifier); return true; }), removeTempConnection: vi.fn(), assignIdentifierToTemp: vi.fn((ws, identifier: string) => { assigned.set(ws as object, identifier); }) }; return { transport, sentToConnection, sentByIdentifier, assigned, promoted, closed }; } function stubDiscordFetchSuccess() { vi.stubGlobal( "fetch", vi .fn() .mockResolvedValueOnce({ ok: true, status: 200, json: async () => ({ id: "dm-channel-1" }) }) .mockResolvedValueOnce({ ok: true, status: 200, json: async () => ({ id: "message-1" }) }) ); } describe("Yonexus.Server runtime flow", () => { it("runs hello -> pair_request for an unpaired client", async () => { stubDiscordFetchSuccess(); const store = createMockStore(); const transportState = createMockTransport(); const runtime = createYonexusServerRuntime({ config: { followerIdentifiers: ["client-a"], notifyBotToken: "stub-token", adminUserId: "admin-user", listenHost: "127.0.0.1", listenPort: 8787 }, store, transport: transportState.transport, now: () => 1_710_000_000 }); await runtime.start(); const connection = createConnection(); await runtime.handleMessage( connection, encodeBuiltin( buildHello( { identifier: "client-a", hasSecret: false, hasKeyPair: false, protocolVersion: YONEXUS_PROTOCOL_VERSION }, { requestId: "req-hello", timestamp: 1_710_000_000 } ) ) ); expect(transportState.assigned.get(connection.ws as object)).toBe("client-a"); expect(transportState.sentToConnection).toHaveLength(2); const helloAck = decodeBuiltin(transportState.sentToConnection[0].message); expect(helloAck.type).toBe("hello_ack"); expect(helloAck.payload).toMatchObject({ identifier: "client-a", nextAction: "pair_required" }); const pairRequest = decodeBuiltin(transportState.sentToConnection[1].message) as BuiltinEnvelope< "pair_request", PairRequestPayload >; expect(pairRequest.type).toBe("pair_request"); expect(pairRequest.payload).toMatchObject({ identifier: "client-a", adminNotification: "sent", codeDelivery: "out_of_band" }); const record = runtime.state.registry.clients.get("client-a"); expect(record?.pairingStatus).toBe("pending"); expect(record?.pairingCode).toBeTypeOf("string"); }); it("completes pair_confirm -> auth_request -> heartbeat for a client", async () => { stubDiscordFetchSuccess(); let now = 1_710_000_000; const keyPair = await generateKeyPair(); const store = createMockStore(); const transportState = createMockTransport(); const runtime = createYonexusServerRuntime({ config: { followerIdentifiers: ["client-a"], notifyBotToken: "stub-token", adminUserId: "admin-user", listenHost: "127.0.0.1", listenPort: 8787 }, store, transport: transportState.transport, now: () => now }); await runtime.start(); const connection = createConnection(); await runtime.handleMessage( connection, encodeBuiltin( buildHello( { identifier: "client-a", hasSecret: false, hasKeyPair: true, publicKey: keyPair.publicKey, protocolVersion: YONEXUS_PROTOCOL_VERSION }, { requestId: "req-hello", timestamp: now } ) ) ); const pairRequest = decodeBuiltin(transportState.sentToConnection[1].message) as BuiltinEnvelope< "pair_request", PairRequestPayload >; const pairingCode = runtime.state.registry.clients.get("client-a")?.pairingCode; expect(pairingCode).toBeTypeOf("string"); expect(pairRequest.payload?.identifier).toBe("client-a"); now += 2; await runtime.handleMessage( connection, encodeBuiltin( buildPairConfirm( { identifier: "client-a", pairingCode: pairingCode! }, { requestId: "req-pair", timestamp: now } ) ) ); const pairSuccess = decodeBuiltin( transportState.sentToConnection[transportState.sentToConnection.length - 1].message ); expect(pairSuccess.type).toBe("pair_success"); const recordAfterPair = runtime.state.registry.clients.get("client-a"); expect(recordAfterPair?.pairingStatus).toBe("paired"); expect(recordAfterPair?.publicKey).toBe(keyPair.publicKey.trim()); expect(recordAfterPair?.secret).toBeTypeOf("string"); now += 2; const nonce = "AUTHNONCESTRING000000001"; const signingInput = createAuthRequestSigningInput({ secret: recordAfterPair!.secret!, nonce, proofTimestamp: now }); const signature = await signMessage(keyPair.privateKey, signingInput); await expect(verifySignature(keyPair.publicKey, signingInput, signature)).resolves.toBe(true); await runtime.handleMessage( connection, encodeBuiltin( buildAuthRequest( { identifier: "client-a", nonce, proofTimestamp: now, signature, publicKey: keyPair.publicKey }, { requestId: "req-auth", timestamp: now } ) ) ); const authSuccess = decodeBuiltin( transportState.sentToConnection[transportState.sentToConnection.length - 1].message ); expect(authSuccess.type).toBe("auth_success"); expect(transportState.promoted).toContain("client-a"); const session = runtime.state.registry.sessions.get("client-a"); expect(session?.isAuthenticated).toBe(true); now += 5; await runtime.handleMessage( { ...connection, identifier: "client-a", isAuthenticated: true }, encodeBuiltin( buildHeartbeat( { identifier: "client-a", status: "alive" }, { requestId: "req-heartbeat", timestamp: now } ) ) ); const heartbeatAck = decodeBuiltin( transportState.sentToConnection[transportState.sentToConnection.length - 1].message ); expect(heartbeatAck.type).toBe("heartbeat_ack"); const recordAfterHeartbeat = runtime.state.registry.clients.get("client-a"); expect(recordAfterHeartbeat?.status).toBe("online"); expect(recordAfterHeartbeat?.lastHeartbeatAt).toBe(now); }); it("returns MALFORMED_MESSAGE for hello without payload and keeps the connection open", async () => { const store = createMockStore(); const transportState = createMockTransport(); const runtime = createYonexusServerRuntime({ config: { followerIdentifiers: ["client-a"], notifyBotToken: "stub-token", adminUserId: "admin-user", listenHost: "127.0.0.1", listenPort: 8787 }, store, transport: transportState.transport, now: () => 1_710_000_000 }); await runtime.start(); const connection = createConnection(); await runtime.handleMessage( connection, encodeBuiltin({ type: "hello", requestId: "req-bad-hello", timestamp: 1_710_000_000 }) ); expect(transportState.sentToConnection).toHaveLength(1); const errorResponse = decodeBuiltin(transportState.sentToConnection[0].message); expect(errorResponse.type).toBe("error"); expect(errorResponse.payload).toMatchObject({ code: "MALFORMED_MESSAGE", message: "hello payload is required" }); expect((connection.ws.close as ReturnType)).not.toHaveBeenCalled(); }); it("rejects unauthenticated rule messages by closing the connection", async () => { const store = createMockStore(); const transportState = createMockTransport(); const runtime = createYonexusServerRuntime({ config: { followerIdentifiers: ["client-a"], notifyBotToken: "stub-token", adminUserId: "admin-user", listenHost: "127.0.0.1", listenPort: 8787 }, store, transport: transportState.transport, now: () => 1_710_000_000 }); await runtime.start(); const connection = createConnection("client-a"); runtime.state.registry.sessions.set("client-a", { identifier: "client-a", socket: connection.ws, isAuthenticated: false, connectedAt: connection.connectedAt, lastActivityAt: 1_710_000_000 }); await runtime.handleMessage(connection, 'chat_sync::{"body":"hello"}'); expect((connection.ws.close as ReturnType)).toHaveBeenCalledWith( 1008, "Not authenticated" ); }); it("marks stale authenticated clients unstable then offline during liveness sweep", async () => { vi.useFakeTimers(); try { let now = 1_710_000_000; const store = createMockStore(); const transportState = createMockTransport(); const runtime = createYonexusServerRuntime({ config: { followerIdentifiers: ["client-a"], notifyBotToken: "stub-token", adminUserId: "admin-user", listenHost: "127.0.0.1", listenPort: 8787 }, store, transport: transportState.transport, now: () => now, sweepIntervalMs: 1000 }); await runtime.start(); runtime.state.registry.clients.set("client-a", { identifier: "client-a", pairingStatus: "paired", publicKey: "pk", secret: "secret", status: "online", recentNonces: [], recentHandshakeAttempts: [], createdAt: now, updatedAt: now, lastAuthenticatedAt: now, lastHeartbeatAt: now }); runtime.state.registry.sessions.set("client-a", { identifier: "client-a", socket: createMockSocket(), isAuthenticated: true, connectedAt: now, lastActivityAt: now }); now += 7 * 60; await vi.advanceTimersByTimeAsync(1000); expect(runtime.state.registry.clients.get("client-a")?.status).toBe("unstable"); const unstableNotice = transportState.sentByIdentifier.at(-1); expect(unstableNotice?.identifier).toBe("client-a"); expect(decodeBuiltin(unstableNotice!.message).type).toBe("status_update"); now += 4 * 60; await vi.advanceTimersByTimeAsync(1000); expect(runtime.state.registry.clients.get("client-a")?.status).toBe("offline"); expect(transportState.closed).toContainEqual({ identifier: "client-a", code: 1001, reason: "Heartbeat timeout" }); expect(runtime.state.registry.sessions.has("client-a")).toBe(false); } finally { vi.useRealTimers(); } }); });