#!/usr/bin/env bun /** * Fire one notification at a registered device, to prove the pipe. * * bun run notify:test # officer/core:test to user 1 * bun run notify:test --user 2 # someone else * bun run notify:test --code officer/core:job --id 412 * * Talks to the sidecar over loopback, which is how every other producer reaches it — so this exercises * the real path rather than a special one. It reports what the channels actually did, because the thing * that wastes a day here is that a send with nothing configured returns success and delivers nothing. */ import { readFileSync } from 'node:fs'; import { join } from 'node:path'; import { DATA_PATH } from '../src/servers/data-path'; const arg = (name: string): string | undefined => { const i = process.argv.indexOf(`--${name}`); return i === -1 ? undefined : process.argv[i + 1]; }; const code = arg('code') ?? 'officer/core:test'; const userId = Number(arg('user') ?? 1); const id = arg('id'); const n = arg('n'); const addressFile = join(DATA_PATH, 'notify-server.url'); let base: string; try { base = readFileSync(addressFile, 'utf8').trim(); } catch { console.error(`[notify:test] no address at ${addressFile}`); console.error(' officer-notify is not running — `pm2 start ecosystem.config.cjs --only officer-notify`'); process.exit(1); } // The file is only a hint: a crash leaves it behind, so prove the listener is there before believing it. let health: { ok: boolean; channels: string[] }; try { const res = await fetch(`${base}/_health`, { signal: AbortSignal.timeout(5000) }); health = (await res.json()) as typeof health; } catch { console.error(`[notify:test] ${base} is not answering — stale address file, or the sidecar is down`); process.exit(1); } console.log(`[notify:test] sidecar ${base}`); console.log(`[notify:test] channels configured: ${health.channels.length ? health.channels.join(', ') : 'NONE'}`); if (health.channels.length === 0) { // Said before sending rather than after, because the send below WILL return ok and deliver nothing. console.error(''); console.error('[notify:test] nothing is configured, so this cannot reach a device.'); console.error(' APNs needs apns-p8, apns-key-id and apns-team-id:'); console.error(' cat AuthKey_XXXXXXXXXX.p8 | bun run secret:set apns-p8'); console.error(' echo -n XXXXXXXXXX | bun run secret:set apns-key-id'); console.error(' echo -n RGHC4NJC77 | bun run secret:set apns-team-id'); console.error(' then: pm2 restart officer-notify'); process.exit(1); } const body = { code, userId, ...(id === undefined ? {} : { id }), ...(n === undefined ? {} : { n: Number(n) }), }; console.log(`[notify:test] sending ${JSON.stringify(body)}`); const res = await fetch(`${base}/_officer/notify`, { method: 'POST', headers: { 'content-type': 'application/json', 'X-Officer-User': String(userId) }, body: JSON.stringify(body), signal: AbortSignal.timeout(20_000), }); const out = (await res.json()) as { ok?: boolean; error?: string; results?: Array> }; if (!res.ok || out.error) { console.error(`[notify:test] refused (${res.status}): ${out.error ?? 'unknown'}`); process.exit(1); } const results = out.results ?? []; if (results.length === 0) { console.error('[notify:test] no channel took it — nothing configured, or no device registered for this user'); process.exit(1); } let delivered = 0; for (const r of results) { const sent = Number(r.sent ?? 0); delivered += sent; const dead = Array.isArray(r.dead) && r.dead.length ? `, ${r.dead.length} dead token(s) deleted` : ''; console.log(`[notify:test] ${r.channel}: sent ${sent}, failed ${r.failed}${dead}`); } // `sent: 0, failed: 0` is the quiet one: the channel is configured but the user has no device of that // platform registered. Worth naming, because it looks like success. if (delivered === 0) { console.error('[notify:test] nothing was delivered — is a device registered for this user?'); console.error(' check: GET /api/notify/_officer/devices'); process.exit(1); } console.log(`[notify:test] delivered to ${delivered} device(s) — look at your phone`);