// P3 inbox smoke: Alice sends → Bob's inbox gets a NEEDS_REPLY item → Bob reads // → item auto-resolves to DONE. Requires the service running (relay timer on), // IIOS_DEV_TOKENS not needed (we sign locally). Run: node scripts/smoke-inbox.mjs import 'dotenv/config'; import { io } from 'socket.io-client'; import jwt from 'jsonwebtoken'; 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 sign = (u) => jwt.sign({ sub: u, name: u, appId: APP_ID, orgId: `org_${APP_ID}` }, SECRET, { algorithm: 'HS256', expiresIn: '1h' }); const connect = (u) => io(`${SERVICE}/message`, { auth: { token: sign(u) }, transports: ['websocket'], forceNew: true }); const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); const assert = (c, m) => { if (!c) { console.error('✗', m); process.exit(1); } console.log('✓', m); }; async function inbox(token, state) { const r = await fetch(`${SERVICE}/v1/inbox/items${state ? `?state=${state}` : ''}`, { headers: { authorization: `Bearer ${token}` }, }); if (!r.ok) throw new Error(`inbox ${r.status}`); return r.json(); } async function pollUntil(fn, ms = 8000) { const end = Date.now() + ms; while (Date.now() < end) { const v = await fn(); if (v) return v; await sleep(300); } return null; } const bobToken = sign('bob'); const alice = connect('alice'); const bob = connect('bob'); try { const { threadId } = await alice.emitWithAck('open_thread', {}); await bob.emitWithAck('open_thread', { threadId }); // Bob joins → participant const sent = await alice.emitWithAck('send_message', { threadId, content: 'hi bob' }); const open = await pollUntil(async () => { const items = await inbox(bobToken, 'OPEN'); return items.find((i) => i.threadId === threadId && i.kind === 'NEEDS_REPLY') ?? null; }); assert(open, `Bob's inbox got an OPEN NEEDS_REPLY item (${open?.id})`); await bob.emitWithAck('read', { threadId, interactionId: sent.id }); const done = await pollUntil(async () => { const items = await inbox(bobToken); const it = items.find((i) => i.id === open.id); return it && it.state === 'DONE' ? it : null; }); assert(done, "Bob's item auto-resolved to DONE after reading"); console.log('\nP3 inbox smoke: PASS'); } finally { alice.close(); bob.close(); } process.exit(0);