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
@@ -622,6 +622,65 @@ describe('RedditApiClient', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAccountProfile', () => {
|
||||
it('reads age, karma and email verification for one of our own accounts', async () => {
|
||||
const mockAxios = { get: vi.fn() };
|
||||
vi.mocked(axios.create).mockReturnValue(mockAxios as unknown as AxiosInstance);
|
||||
const client2 = new RedditApiClient(getTestConfig(), logger);
|
||||
|
||||
mockAxios.get.mockResolvedValueOnce({
|
||||
data: {
|
||||
data: {
|
||||
created_utc: 1_700_000_000,
|
||||
link_karma: 12,
|
||||
comment_karma: 30,
|
||||
total_karma: 42,
|
||||
has_verified_email: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await expect(client2.getAccountProfile('new-account')).resolves.toEqual({
|
||||
createdAt: 1_700_000_000_000,
|
||||
linkKarma: 12,
|
||||
commentKarma: 30,
|
||||
totalKarma: 42,
|
||||
hasVerifiedEmail: true,
|
||||
isSuspended: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('derives total karma when Reddit omits it and leaves absent fields null', async () => {
|
||||
const mockAxios = { get: vi.fn() };
|
||||
vi.mocked(axios.create).mockReturnValue(mockAxios as unknown as AxiosInstance);
|
||||
const client2 = new RedditApiClient(getTestConfig(), logger);
|
||||
|
||||
mockAxios.get.mockResolvedValueOnce({ data: { data: { link_karma: 3, comment_karma: 4 } } });
|
||||
|
||||
await expect(client2.getAccountProfile('new-account')).resolves.toEqual({
|
||||
createdAt: null,
|
||||
linkKarma: 3,
|
||||
commentKarma: 4,
|
||||
totalKarma: 7,
|
||||
hasVerifiedEmail: null,
|
||||
isSuspended: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('reports a missing profile as null and still propagates real failures', async () => {
|
||||
const mockAxios = { get: vi.fn() };
|
||||
vi.mocked(axios.create).mockReturnValue(mockAxios as unknown as AxiosInstance);
|
||||
const client2 = new RedditApiClient(getTestConfig(), logger);
|
||||
|
||||
mockAxios.get.mockRejectedValueOnce({ isAxiosError: true, response: { status: 404 } });
|
||||
await expect(client2.getAccountProfile('gone')).resolves.toBeNull();
|
||||
|
||||
const temporary = { isAxiosError: true, response: { status: 503 } };
|
||||
mockAxios.get.mockRejectedValueOnce(temporary);
|
||||
await expect(client2.getAccountProfile('new-account')).rejects.toBe(temporary);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getNewPosts', () => {
|
||||
it('throws a permanent-disable signal for a plain 404 subreddit', async () => {
|
||||
const error = { isAxiosError: true, response: { status: 404 } };
|
||||
|
||||
Reference in new issue
Block a user