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>
97 lines
3.5 KiB
TypeScript
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']);
|
|
});
|
|
});
|