// P4 support smoke: customer escalates a chat → ticket created + auto-assigned to // an available agent → agent (now a thread participant) replies → customer sees it // live. Requires: service running (relay timer on) + `node scripts/seed-support.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 api(path, method, token, body) { const r = await fetch(`${SERVICE}${path}`, { method, headers: { 'content-type': 'application/json', authorization: `Bearer ${token}` }, body: body ? JSON.stringify(body) : undefined, }); if (!r.ok) throw new Error(`${method} ${path} → ${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; } function waitForMessage(socket, pred, ms = 6000) { return new Promise((res) => { const to = setTimeout(() => res(null), ms); const h = (m) => { if (pred(m)) { socket.off('message', h); clearTimeout(to); res(m); } }; socket.on('message', h); }); } const custToken = sign('cust'); const cust = connect('cust'); const agent = connect(process.env.AGENT_ID ?? 'agent1'); try { const { threadId } = await cust.emitWithAck('open_thread', {}); await cust.emitWithAck('send_message', { threadId, content: 'my payment failed' }); const ticket = await api('/v1/support/escalate', 'POST', custToken, { threadId }); assert(!!ticket.id, `customer escalated → ticket ${ticket.id} (${ticket.state})`); const assigned = await pollUntil(async () => { const ts = await api('/v1/support/tickets?scope=mine', 'GET', custToken); const t = ts.find((x) => x.id === ticket.id); return t && t.state === 'OPEN' && t.assignedActorId ? t : null; }); assert(assigned, 'ticket auto-assigned to an agent (state OPEN)'); await agent.emitWithAck('open_thread', { threadId }); const gotReply = waitForMessage(cust, (m) => m.content === 'Hi, I can help with that.'); await agent.emitWithAck('send_message', { threadId, content: 'Hi, I can help with that.' }); assert(await gotReply, 'customer received the agent reply live'); console.log('\nP4 support smoke: PASS'); } finally { cust.close(); agent.close(); } process.exit(0);