95 lines
3.9 KiB
TypeScript
95 lines
3.9 KiB
TypeScript
import express from 'express';
|
|
import type { AddressInfo } from 'node:net';
|
|
import { afterEach, describe, expect, it } from 'vitest';
|
|
import { StatsResponseSchema } from '../../packages/api-contracts/src/index.js';
|
|
import type { WebContext } from '../../src/web/context.js';
|
|
import { createStatsRouter } from '../../src/web/routes/stats.js';
|
|
|
|
const servers: Array<ReturnType<express.Express['listen']>> = [];
|
|
|
|
async function request(app: express.Express, path: string): Promise<Response> {
|
|
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()))));
|
|
});
|
|
|
|
describe('stats route account status', () => {
|
|
it('reports sending availability and Matrix cooldown independently for every account', async () => {
|
|
const now = Date.now();
|
|
const account = (
|
|
accountId: string,
|
|
redditUsername: string,
|
|
cooldown: { until: number; reason: string; redditCode: string | null } | null,
|
|
) => ({
|
|
session: { accountId, redditUsername },
|
|
resources: {
|
|
accountAuth: { isFullyAuthenticated: () => true },
|
|
chatService: { getInvitationCooldown: () => cooldown },
|
|
rateLimiter: {
|
|
getStats: () => ({ messagesThisHour: cooldown ? 5 : 2, minutesSinceLastMessage: 1 }),
|
|
getTimers: () => ({
|
|
nextMessageAvailableAt: cooldown?.until ?? null,
|
|
hourlyResetAt: now + 3_600_000,
|
|
isHourlyLimitReached: false,
|
|
messagesRemaining: cooldown ? 3 : 6,
|
|
}),
|
|
},
|
|
conversationAutomation: { getPriorityStatus: () => ({ processing: false, actionableBacklog: 0 }) },
|
|
messagesRepo: { getRecent: () => [] },
|
|
activityLogRepo: { getRecent: () => [], countSentSkipped: () => 0 },
|
|
},
|
|
});
|
|
const ready = account('@t2_ready:reddit.com', 'ready-account', null);
|
|
const limited = account('@t2_limited:reddit.com', 'limited-account', {
|
|
until: now + 600_000,
|
|
reason: 'Matrix rate-limited: M_LIMIT_EXCEEDED',
|
|
redditCode: 'rate.invitation_limit_mature_account',
|
|
});
|
|
const runtimes = new Map([[ready.session.accountId, ready], [limited.session.accountId, limited]]);
|
|
const context = {
|
|
config: {
|
|
browser: { dryRun: false },
|
|
rateLimit: { maxMessagesPerHour: 8 },
|
|
reddit: { subreddits: [], targetGenders: [] },
|
|
},
|
|
services: {
|
|
redditAccounts: { list: () => [...runtimes.values()].map((runtime) => runtime.session) },
|
|
accountRuntimes: {
|
|
list: () => [...runtimes.values()].map((runtime) => ({
|
|
...runtime.session,
|
|
matrixUserId: runtime.session.accountId,
|
|
state: 'running',
|
|
updatedAt: now,
|
|
failure: null,
|
|
})),
|
|
get: (accountId: string) => runtimes.get(accountId) ?? null,
|
|
},
|
|
},
|
|
repos: { messages: { getRecent: () => [] } },
|
|
activity: { recent: () => [], totalProcessed: () => 0 },
|
|
getBotRunning: () => true,
|
|
getMessagesThisHour: () => 0,
|
|
} as unknown as WebContext;
|
|
const app = express().use('/api', createStatsRouter(context));
|
|
|
|
const response = await request(app, '/api/stats');
|
|
const stats = StatsResponseSchema.parse(await response.json());
|
|
|
|
expect(stats.accounts).toEqual([
|
|
expect.objectContaining({ redditUsername: 'ready-account', sendingState: 'ready', messagesThisHour: 2 }),
|
|
expect.objectContaining({
|
|
redditUsername: 'limited-account',
|
|
sendingState: 'matrix-cooldown',
|
|
invitationCooldown: expect.objectContaining({ reason: 'Matrix rate-limited: M_LIMIT_EXCEEDED' }),
|
|
}),
|
|
]);
|
|
expect(stats.invitationCooldown).toMatchObject({ redditUsername: 'limited-account' });
|
|
});
|
|
});
|