Files
RedditBot/tests/web/attention-route.test.ts
T
Jamie BruntonandClaude Opus 5 89eb4583f8 feat: flag accounts Reddit considers unestablished and re-test them
New accounts cannot start chats until Reddit considers them established.
Until now the bot would burn a candidate on every invitation Reddit was
always going to reject, with nothing in the dashboard explaining why the
account looked idle.

Two signals feed the verdict. A periodic profile check reads the
account's own age, karma and email verification from about.json and
compares them against configurable thresholds — that is what flags a
brand new account before it wastes anything. A rejected invitation is
authoritative: Reddit has told us directly, so the account is held out of
sending with a backoff that doubles per rejection, and a delivered
invitation clears the flag outright.

An unestablished account takes no new posts (canAcceptPost), raises an
attention item, reports an `unestablished` sending state, and shows its
age/karma evidence on the Accounts page. An unchecked account is always
allowed through, so a first run is never blocked.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 09:14:47 +01:00

97 lines
3.5 KiB
TypeScript

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<ReturnType<express.Express['listen']>> = [];
async function get(context: WebContext, path: string): Promise<Response> {
const app = express();
app.use('/api', createAttentionRouter(context));
const server = app.listen(0);
servers.push(server);
await new Promise<void>((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<void>((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 unestablished account with its reasons and hold window', async () => {
const response = await get(contextWith([{
...account,
establishment: {
state: 'unestablished',
reasons: ['Account is 2 days old (needs 7 days)'],
retryAt: NOW + 4 * 60 * 60_000,
profileCheckedAt: NOW,
lastProbeAt: null,
},
}]), '/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 is not established yet',
occurredAt: NOW,
owner: { accountId: account.accountId, redditUsername: 'new-account' },
});
expect(body.items[0].detail).toContain('Account is 2 days old (needs 7 days)');
expect(body.items[0].detail).toContain('Outbound sending is on hold');
});
it('stays quiet for established accounts and for accounts never checked', async () => {
const response = await get(contextWith([
{ ...account, establishment: { state: 'established', reasons: [], retryAt: null, profileCheckedAt: NOW, lastProbeAt: null } },
{ ...account, accountId: '@t2_other:reddit.com', redditUsername: 'other', establishment: 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',
reasons: [],
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']);
});
});