diff --git a/tests/state-recovery.test.ts b/tests/state-recovery.test.ts index 79b60a9..5bcdbdd 100644 --- a/tests/state-recovery.test.ts +++ b/tests/state-recovery.test.ts @@ -193,6 +193,103 @@ describe("YNX-1105e: Server state recovery", () => { await secondRuntime.stop(); }); + it("SR-02: restart drops in-memory active sessions and requires reconnect", async () => { + const storePath = await createTempServerStorePath(); + const store = createYonexusServerStore(storePath); + const now = 1_710_000_000; + + const firstTransport = createMockTransport(); + const firstRuntime = createYonexusServerRuntime({ + config: { + followerIdentifiers: ["client-a"], + notifyBotToken: "stub-token", + adminUserId: "admin-user", + listenHost: "127.0.0.1", + listenPort: 8787 + }, + store, + transport: firstTransport.transport, + now: () => now + }); + + await firstRuntime.start(); + const record = firstRuntime.state.registry.clients.get("client-a"); + expect(record).toBeDefined(); + + record!.pairingStatus = "paired"; + record!.publicKey = "test-public-key"; + record!.secret = "test-secret"; + record!.status = "online"; + record!.lastAuthenticatedAt = now; + record!.lastHeartbeatAt = now; + record!.updatedAt = now; + + firstRuntime.state.registry.sessions.set("client-a", { + identifier: "client-a", + socket: createMockSocket(), + isAuthenticated: true, + connectedAt: now, + lastActivityAt: now, + publicKey: "test-public-key" + }); + + await firstRuntime.stop(); + + const secondTransport = createMockTransport(); + const secondRuntime = createYonexusServerRuntime({ + config: { + followerIdentifiers: ["client-a"], + notifyBotToken: "stub-token", + adminUserId: "admin-user", + listenHost: "127.0.0.1", + listenPort: 8787 + }, + store, + transport: secondTransport.transport, + now: () => now + 5 + }); + + await secondRuntime.start(); + + const reloadedRecord = secondRuntime.state.registry.clients.get("client-a"); + expect(reloadedRecord).toMatchObject({ + identifier: "client-a", + pairingStatus: "paired", + secret: "test-secret", + publicKey: "test-public-key", + status: "online", + lastAuthenticatedAt: now, + lastHeartbeatAt: now + }); + expect(secondRuntime.state.registry.sessions.size).toBe(0); + + const reconnectConnection = createConnection(); + await secondRuntime.handleMessage( + reconnectConnection, + encodeBuiltin( + buildHello( + { + identifier: "client-a", + hasSecret: true, + hasKeyPair: true, + publicKey: "test-public-key", + protocolVersion: YONEXUS_PROTOCOL_VERSION + }, + { requestId: "req-hello-reconnect", timestamp: now + 5 } + ) + ) + ); + + const helloAck = decodeBuiltin(secondTransport.sentToConnection[0].message); + expect(helloAck.type).toBe("hello_ack"); + expect(helloAck.payload).toMatchObject({ + identifier: "client-a", + nextAction: "auth_required" + }); + + await secondRuntime.stop(); + }); + it("SR-05: corrupted server store raises YonexusServerStoreCorruptionError", async () => { const storePath = await createTempServerStorePath(); await writeFile(storePath, '{"version":1,"clients":"oops"}\n', "utf8");