// P2 realtime smoke: two socket.io clients (Alice, Bob) on one thread. // Requires the service running with IIOS_DEV_TOKENS=1 + APP_SECRETS set. // Run: node scripts/smoke-realtime.mjs import 'dotenv/config'; import { io } from 'socket.io-client'; import jwt from 'jsonwebtoken'; import { PrismaClient } from '@prisma/client'; const SERVICE = process.env.SMOKE_URL ?? 'http://localhost:3200'; const APP_ID = 'portal-demo'; const SECRET = JSON.parse(process.env.APP_SECRETS ?? '{"portal-demo":"dev-secret"}')[APP_ID]; const prisma = new PrismaClient(); const sign = (userId) => jwt.sign({ sub: userId, name: userId, appId: APP_ID, orgId: `org_${APP_ID}` }, SECRET, { algorithm: 'HS256', expiresIn: '1h', }); const connect = (userId) => io(`${SERVICE}/message`, { auth: { token: sign(userId) }, transports: ['websocket'], forceNew: true }); const once = (socket, event) => new Promise((res) => socket.once(event, res)); const assert = (cond, msg) => { if (!cond) { console.error('✗', msg); process.exit(1); } console.log('✓', msg); }; async function unread(threadId, userId) { const handle = await prisma.iiosSourceHandle.findFirst({ where: { externalId: userId } }); if (!handle) return 0; const actor = await prisma.iiosActorRef.findFirst({ where: { sourceHandleId: handle.id } }); if (!actor) return 0; const c = await prisma.iiosUnreadCounter.findUnique({ where: { threadId_actorId: { threadId, actorId: actor.id } }, }); return c?.unreadCount ?? 0; } const alice = connect('alice'); const bob = connect('bob'); try { // Alice creates a thread; membership is governed, so Alice ADDS Bob (he can't self-join). const opened = await alice.emitWithAck('open_thread', {}); const threadId = opened.threadId; assert(!!threadId, `Alice created thread ${threadId}`); await alice.emitWithAck('add_participant', { threadId, userId: 'bob' }); await bob.emitWithAck('open_thread', { threadId }); // Alice sends; Bob should receive it live. const bobGetsMessage = once(bob, 'message'); const sent = await alice.emitWithAck('send_message', { threadId, content: 'hi bob' }); const received = await bobGetsMessage; assert(received.content === 'hi bob', `Bob received "${received.content}" in realtime`); assert((await unread(threadId, 'bob')) === 1, "Bob's unread = 1"); // Bob reads; Alice should get the receipt; Bob's unread resets. const aliceGetsReceipt = once(alice, 'receipt'); await bob.emitWithAck('read', { threadId, interactionId: sent.id }); const receipt = await aliceGetsReceipt; assert(receipt.kind === 'READ' && receipt.interactionId === sent.id, 'Alice received READ receipt'); assert((await unread(threadId, 'bob')) === 0, "Bob's unread reset to 0"); console.log('\nP2 realtime smoke: PASS'); } finally { alice.close(); bob.close(); await prisma.$disconnect(); } process.exit(0);