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>
This commit is contained in:
1 parent
c43e534cf1
commit
89eb4583f8
25 files changed
+1425
-11
No files matched your search
@@ -0,0 +1,96 @@
|
||||
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']);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user