import express from 'express'; import type { AddressInfo } from 'node:net'; import { afterEach, describe, expect, it } from 'vitest'; import { AttentionResponseSchema } from '../../packages/api-contracts/src/index.js'; import type { WebContext } from '../../src/web/context.js'; import { createAttentionRouter } from '../../src/web/routes/attention.js'; const servers: Array> = []; async function get(context: WebContext, path: string): Promise { const app = express(); app.use('/api', createAttentionRouter(context)); const server = app.listen(0); servers.push(server); await new Promise((resolve) => server.once('listening', resolve)); const { port } = server.address() as AddressInfo; return fetch(`http://127.0.0.1:${port}${path}`); } afterEach(async () => { await Promise.all(servers.splice(0).map((server) => new Promise((resolve) => server.close(() => resolve())))); }); function contextWith(accounts: unknown[]): WebContext { return { services: { redditAccounts: { list: () => accounts }, accountRuntimes: { list: () => [], get: () => null }, }, } as unknown as WebContext; } const NOW = 1_700_000_000_000; const account = { accountId: '@t2_new:reddit.com', redditUsername: 'new-account', status: 'authenticated', updatedAt: NOW, }; describe('attention route establishment items', () => { it('raises an account Reddit has refused, with what Reddit said and the hold window', async () => { const response = await get(contextWith([{ ...account, establishment: { state: 'unestablished', sendingHeld: true, reasons: ['Reddit rejected an invitation from this account: account is too new'], lastProbeFailure: 'Reddit rejected an invitation from this account: account is too new', retryAt: NOW + 4 * 60 * 60_000, profileCheckedAt: NOW, lastProbeAt: NOW, }, }]), '/api/attention'); expect(response.status).toBe(200); const body = AttentionResponseSchema.parse(await response.json()); expect(body.items).toHaveLength(1); expect(body.items[0]).toMatchObject({ id: 'establishment:@t2_new:reddit.com', kind: 'account-unestablished', severity: 'warning', title: 'u/new-account cannot start chats yet', occurredAt: NOW, owner: { accountId: account.accountId, redditUsername: 'new-account' }, }); expect(body.items[0].detail).toContain('account is too new'); expect(body.items[0].detail).toContain('Outbound sending is on hold'); }); it('stays quiet for an account that is only below our own thresholds', async () => { const response = await get(contextWith([ { ...account, establishment: { state: 'established', sendingHeld: false, reasons: [], lastProbeFailure: null, retryAt: null, profileCheckedAt: NOW, lastProbeAt: null } }, { ...account, accountId: '@t2_other:reddit.com', redditUsername: 'other', establishment: null }, { ...account, accountId: '@t2_soft:reddit.com', redditUsername: 'soft', // Below the configured floor but never refused by Reddit — advisory only. establishment: { state: 'unestablished', sendingHeld: false, reasons: ['Comment karma is 3 (below the configured floor of 5)'], lastProbeFailure: null, retryAt: null, profileCheckedAt: NOW, lastProbeAt: null, }, }, ]), '/api/attention'); const body = AttentionResponseSchema.parse(await response.json()); expect(body.items).toEqual([]); }); it('reports an unestablished account that also needs a new session as two separate items', async () => { const response = await get(contextWith([{ ...account, status: 'login-required', establishment: { state: 'unestablished', sendingHeld: true, reasons: [], lastProbeFailure: null, retryAt: null, profileCheckedAt: NOW, lastProbeAt: null, }, }]), '/api/attention'); const body = AttentionResponseSchema.parse(await response.json()); expect(body.items.map((item) => item.kind).sort()) .toEqual(['account-login-required', 'account-unestablished']); }); });