Files
TeleUploader/test/bot-pool.test.ts
T
Claude 1484d5265d
Deploy FileDrop / deploy (push) Failing after 19s
refactor: merge BOT_TOKEN + ADDITIONAL_BOT_TOKENS into single BOT_TOKENS env + speed audit
BOT_TOKENS env:
- Single BOT_TOKENS env var (comma-separated) replaces BOT_TOKEN + ADDITIONAL_BOT_TOKENS
- Backward compat: falls back to BOT_TOKEN + ADDITIONAL_BOT_TOKENS if BOT_TOKENS unset
- Config exposes botTokens: string[] instead of botToken + additionalBotTokens
- Updated env.ts, bot-pool.ts, docker-compose.yml, .env.example, CLAUDE.md, all tests

Speed audit (S3 -> Telegram upload flow):
- Hoisted 5 dynamic await import('../../../db/index') to top-level static imports
  in s3-controller.ts (3x) and web-api-controller.ts (2x)
  -> saves module resolution + async overhead on every upload
- Removed stale UPLOAD_CONCURRENCY env from docker-compose.yml
  (already removed from env.ts in prior refactor)

Upload flow is already concurrent:
- streamBodyToTemp() uses Bun.file(path).writer() — O(1) memory, safe for multi-GB blobs
- utils/chunked-storage.ts reads chunks serially but uploads concurrently with
  inFlight backpressure at effectiveConcurrency * 2 (= 16 with 8 bots)
- bot-pool.ts: per-bot PQueue(concurrency=1), 8 bots = 8 concurrent uploads per file,
  selectBot() picks least-loaded, 429 detection + inner+outer retry loops
- TELEGRAM_API_TIMEOUT_MS=120s — ample for 48MB chunks
- Infrastructure chunked-storage.ts (DI-based, dead code) has serial upload trap —
  noted for future cleanup
2026-07-29 15:03:02 +07:00

131 lines
3.6 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, mock } from 'bun:test';
process.env.BOT_TOKENS = 'bot1:token,bot2:token,bot3:token';
process.env.STORAGE_CHANNEL_ID = '-1001234567890';
process.env.BASE_URL = 'https://example.com';
process.env.DATABASE_URL = 'sqlite://test.db';
process.env.PORT = '3000';
// Track mock queue instances for per-bot assertions
const queueInstances: Array<{
concurrency: number;
add: ReturnType<typeof mock>;
pending: number;
size: number;
}> = [];
// Mock PQueue so we can verify concurrency
const mockAdd = mock(function addFn(this: any, fn: () => Promise<any>) {
return Promise.resolve().then(() => fn());
});
mock.module('p-queue', () => {
return {
default: mock(function MockQueue(this: any, opts?: { concurrency?: number }) {
const instance = {
concurrency: opts?.concurrency ?? 1,
add: mockAdd,
pending: 0,
size: 0,
};
queueInstances.push(instance);
return instance;
}),
};
});
// Mock Telegraf — use a class so `new Telegraf(token)` works correctly
const mockTelegramInstances: Record<
string,
{
token: string;
sendDocument: ReturnType<typeof mock>;
sendPhoto: ReturnType<typeof mock>;
getFile: ReturnType<typeof mock>;
}
> = {};
class MockTelegraf {
token: string;
telegram: {
token: string;
sendDocument: ReturnType<typeof mock>;
sendPhoto: ReturnType<typeof mock>;
getFile: ReturnType<typeof mock>;
};
constructor(token: string) {
this.token = token;
this.telegram = {
token,
sendDocument: mock(() =>
Promise.resolve({
message_id: 1,
document: { file_id: `file_${token}`, file_unique_id: `uniq_${token}` },
}),
),
sendPhoto: mock(() =>
Promise.resolve({
message_id: 1,
photo: [{ file_id: `photo_${token}`, file_unique_id: `photo_uniq_${token}` }],
}),
),
getFile: mock(() =>
Promise.resolve({ file_size: 100, mime_type: 'text/plain', file_path: 'path' }),
),
};
mockTelegramInstances[token] = this.telegram;
}
}
mock.module('telegraf', () => ({
Telegraf: MockTelegraf,
}));
describe('BotPool', () => {
let BotPool: typeof import('../src/infrastructure/telegram/bot-pool').BotPool;
let botPool: import('../src/infrastructure/telegram/bot-pool').BotPool;
beforeEach(async () => {
mockAdd.mockClear();
queueInstances.length = 0;
for (const token of Object.keys(mockTelegramInstances)) {
const tg = mockTelegramInstances[token];
if (tg) {
tg.sendDocument?.mockClear();
tg.getFile?.mockClear();
}
}
const mod = await import('../src/infrastructure/telegram/bot-pool');
BotPool = mod.BotPool;
botPool = new BotPool();
});
afterEach(() => {
// No module cache cleanup needed — Bun handles import caching correctly
});
it('should have correct bot count', () => {
expect(botPool.size).toBe(3);
});
it('should have correct effective concurrency', () => {
// 3 bots * 1 concurrency per bot
expect(botPool.getEffectiveConcurrency()).toBe(3);
});
it('should forward files through the queue', async () => {
const result = await botPool.forwardToStorage(Buffer.from('test data'), 'test.txt', 'document');
expect(result.telegramFileId).toBeDefined();
expect(result.storageMessageId).toBeGreaterThan(0);
});
it('should use per-bot queues with concurrency=1', () => {
// Each bot gets its own PQueue instance with concurrency=1
expect(queueInstances.length).toBe(3);
for (const qi of queueInstances) {
expect(qi.concurrency).toBe(1);
}
});
});