Files
iios/packages/iios-service/scripts/smoke-inbox.mjs
T
maaz519 942f40292e feat(demo): P3.5 inbox sidebar + inbox smoke (P3 complete)
message-demo gains a per-person Inbox rail (useInbox, done/snooze); sending
surfaces a NEEDS_REPLY item in the other pane; reading auto-resolves it.
smoke-inbox.mjs proves the event-driven flow end-to-end (send -> relay ->
projector -> inbox -> read -> DONE). Realtime smoke still passes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 07:24:52 +05:30

66 lines
2.4 KiB
JavaScript

// 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);