refactor: dedupe channel outbound and monitor tests

This commit is contained in:
Peter Steinberger
2026-03-03 00:14:52 +00:00
parent 6a42d09129
commit d7dda4dd1a
18 changed files with 301 additions and 450 deletions

View File

@@ -110,6 +110,18 @@ function setupTransientGetFileRetry() {
return getFile;
}
function mockPdfFetchAndSave(fileName: string | undefined) {
fetchRemoteMedia.mockResolvedValueOnce({
buffer: Buffer.from("pdf-data"),
contentType: "application/pdf",
fileName,
});
saveMediaBuffer.mockResolvedValueOnce({
path: "/tmp/file_42---uuid.pdf",
contentType: "application/pdf",
});
}
function createFileTooBigError(): Error {
return new Error("GrammyError: Call to 'getFile' failed! (400: Bad Request: file is too big)");
}
@@ -321,15 +333,7 @@ describe("resolveMedia original filename preservation", () => {
it("falls back to fetched.fileName when telegram file_name is absent", async () => {
const getFile = vi.fn().mockResolvedValue({ file_path: "documents/file_42.pdf" });
fetchRemoteMedia.mockResolvedValueOnce({
buffer: Buffer.from("pdf-data"),
contentType: "application/pdf",
fileName: "file_42.pdf",
});
saveMediaBuffer.mockResolvedValueOnce({
path: "/tmp/file_42---uuid.pdf",
contentType: "application/pdf",
});
mockPdfFetchAndSave("file_42.pdf");
const ctx = makeCtx("document", getFile);
const result = await resolveMedia(ctx, MAX_MEDIA_BYTES, BOT_TOKEN);
@@ -346,15 +350,7 @@ describe("resolveMedia original filename preservation", () => {
it("falls back to filePath when neither telegram nor fetched fileName is available", async () => {
const getFile = vi.fn().mockResolvedValue({ file_path: "documents/file_42.pdf" });
fetchRemoteMedia.mockResolvedValueOnce({
buffer: Buffer.from("pdf-data"),
contentType: "application/pdf",
fileName: undefined,
});
saveMediaBuffer.mockResolvedValueOnce({
path: "/tmp/file_42---uuid.pdf",
contentType: "application/pdf",
});
mockPdfFetchAndSave(undefined);
const ctx = makeCtx("document", getFile);
const result = await resolveMedia(ctx, MAX_MEDIA_BYTES, BOT_TOKEN);

View File

@@ -44,6 +44,14 @@ async function expectInitialForumSend(
);
}
function expectDmMessagePreviewViaSendMessage(
api: ReturnType<typeof createMockDraftApi>,
text = "Hello",
): void {
expect(api.sendMessage).toHaveBeenCalledWith(123, text, { message_thread_id: 42 });
expect(api.editMessageText).not.toHaveBeenCalled();
}
function createForceNewMessageHarness(params: { throttleMs?: number } = {}) {
const api = createMockDraftApi();
api.sendMessage
@@ -135,9 +143,8 @@ describe("createTelegramDraftStream", () => {
stream.update("Hello");
await stream.flush();
expect(api.sendMessage).toHaveBeenCalledWith(123, "Hello", { message_thread_id: 42 });
expectDmMessagePreviewViaSendMessage(api);
expect(api.sendMessageDraft).not.toHaveBeenCalled();
expect(api.editMessageText).not.toHaveBeenCalled();
});
it("falls back to message transport when sendMessageDraft is unavailable", async () => {
@@ -153,8 +160,7 @@ describe("createTelegramDraftStream", () => {
stream.update("Hello");
await stream.flush();
expect(api.sendMessage).toHaveBeenCalledWith(123, "Hello", { message_thread_id: 42 });
expect(api.editMessageText).not.toHaveBeenCalled();
expectDmMessagePreviewViaSendMessage(api);
expect(warn).toHaveBeenCalledWith(
"telegram stream preview: sendMessageDraft unavailable; falling back to sendMessage/editMessageText",
);
@@ -392,6 +398,14 @@ describe("draft stream initial message debounce", () => {
deleteMessage: vi.fn().mockResolvedValue(true),
});
function createDebouncedStream(api: ReturnType<typeof createMockApi>, minInitialChars = 30) {
return createTelegramDraftStream({
api: api as unknown as Bot["api"],
chatId: 123,
minInitialChars,
});
}
beforeEach(() => {
vi.useFakeTimers();
});
@@ -403,11 +417,7 @@ describe("draft stream initial message debounce", () => {
describe("isFinal has highest priority", () => {
it("sends immediately on stop() even with 1 character", async () => {
const api = createMockApi();
const stream = createTelegramDraftStream({
api: api as unknown as Bot["api"],
chatId: 123,
minInitialChars: 30,
});
const stream = createDebouncedStream(api);
stream.update("Y");
await stream.stop();
@@ -418,11 +428,7 @@ describe("draft stream initial message debounce", () => {
it("sends immediately on stop() with short sentence", async () => {
const api = createMockApi();
const stream = createTelegramDraftStream({
api: api as unknown as Bot["api"],
chatId: 123,
minInitialChars: 30,
});
const stream = createDebouncedStream(api);
stream.update("Ok.");
await stream.stop();
@@ -435,11 +441,7 @@ describe("draft stream initial message debounce", () => {
describe("minInitialChars threshold", () => {
it("does not send first message below threshold", async () => {
const api = createMockApi();
const stream = createTelegramDraftStream({
api: api as unknown as Bot["api"],
chatId: 123,
minInitialChars: 30,
});
const stream = createDebouncedStream(api);
stream.update("Processing"); // 10 chars, below 30
await stream.flush();
@@ -449,11 +451,7 @@ describe("draft stream initial message debounce", () => {
it("sends first message when reaching threshold", async () => {
const api = createMockApi();
const stream = createTelegramDraftStream({
api: api as unknown as Bot["api"],
chatId: 123,
minInitialChars: 30,
});
const stream = createDebouncedStream(api);
// Exactly 30 chars
stream.update("I am processing your request..");
@@ -464,11 +462,7 @@ describe("draft stream initial message debounce", () => {
it("works with longer text above threshold", async () => {
const api = createMockApi();
const stream = createTelegramDraftStream({
api: api as unknown as Bot["api"],
chatId: 123,
minInitialChars: 30,
});
const stream = createDebouncedStream(api);
stream.update("I am processing your request, please wait a moment"); // 50 chars
await stream.flush();
@@ -480,11 +474,7 @@ describe("draft stream initial message debounce", () => {
describe("subsequent updates after first message", () => {
it("edits normally after first message is sent", async () => {
const api = createMockApi();
const stream = createTelegramDraftStream({
api: api as unknown as Bot["api"],
chatId: 123,
minInitialChars: 30,
});
const stream = createDebouncedStream(api);
// First message at threshold (30 chars)
stream.update("I am processing your request..");

View File

@@ -1,6 +1,6 @@
import { createHash } from "node:crypto";
import { once } from "node:events";
import { request } from "node:http";
import { request, type IncomingMessage } from "node:http";
import { setTimeout as sleep } from "node:timers/promises";
import { describe, expect, it, vi } from "vitest";
import { startTelegramWebhook } from "./webhook.js";
@@ -24,6 +24,22 @@ const TELEGRAM_TOKEN = "tok";
const TELEGRAM_SECRET = "secret";
const TELEGRAM_WEBHOOK_PATH = "/hook";
function collectResponseBody(
res: IncomingMessage,
onDone: (payload: { statusCode: number; body: string }) => void,
): void {
const chunks: Buffer[] = [];
res.on("data", (chunk: Buffer | string) => {
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
});
res.on("end", () => {
onDone({
statusCode: res.statusCode ?? 0,
body: Buffer.concat(chunks).toString("utf-8"),
});
});
}
vi.mock("grammy", async (importOriginal) => {
const actual = await importOriginal<typeof import("grammy")>();
return {
@@ -124,16 +140,7 @@ async function postWebhookPayloadWithChunkPlan(params: {
},
},
(res) => {
const chunks: Buffer[] = [];
res.on("data", (chunk: Buffer | string) => {
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
});
res.on("end", () => {
finishResolve({
statusCode: res.statusCode ?? 0,
body: Buffer.concat(chunks).toString("utf-8"),
});
});
collectResponseBody(res, finishResolve);
},
);
@@ -555,16 +562,8 @@ describe("startTelegramWebhook", () => {
},
},
(res) => {
const chunks: Buffer[] = [];
res.on("data", (chunk: Buffer | string) => {
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
});
res.on("end", () => {
resolve({
kind: "response",
statusCode: res.statusCode ?? 0,
body: Buffer.concat(chunks).toString("utf-8"),
});
collectResponseBody(res, (payload) => {
resolve({ kind: "response", ...payload });
});
},
);