refactor(runtime): consolidate followup, gateway, and provider dedupe paths

This commit is contained in:
Peter Steinberger
2026-02-22 14:06:03 +00:00
parent 38752338dc
commit d116bcfb14
36 changed files with 848 additions and 908 deletions

View File

@@ -82,6 +82,19 @@ function setupTransientGetFileRetry() {
return getFile;
}
function createFileTooBigError(): Error {
return new Error("GrammyError: Call to 'getFile' failed! (400: Bad Request: file is too big)");
}
async function expectTransientGetFileRetrySuccess() {
const getFile = setupTransientGetFileRetry();
const promise = resolveMedia(makeCtx("voice", getFile), MAX_MEDIA_BYTES, BOT_TOKEN);
await flushRetryTimers();
const result = await promise;
expect(getFile).toHaveBeenCalledTimes(2);
return result;
}
async function flushRetryTimers() {
await vi.runAllTimersAsync();
}
@@ -98,12 +111,7 @@ describe("resolveMedia getFile retry", () => {
});
it("retries getFile on transient failure and succeeds on second attempt", async () => {
const getFile = setupTransientGetFileRetry();
const promise = resolveMedia(makeCtx("voice", getFile), MAX_MEDIA_BYTES, BOT_TOKEN);
await flushRetryTimers();
const result = await promise;
expect(getFile).toHaveBeenCalledTimes(2);
const result = await expectTransientGetFileRetrySuccess();
expect(result).toEqual(
expect.objectContaining({ path: "/tmp/file_0.oga", placeholder: "<media:audio>" }),
);
@@ -135,15 +143,13 @@ describe("resolveMedia getFile retry", () => {
});
it("does not retry 'file is too big' error (400 Bad Request) and returns null", async () => {
// Simulate Telegram Bot API error when file exceeds 20MB limit
const fileTooBigError = new Error(
"GrammyError: Call to 'getFile' failed! (400: Bad Request: file is too big)",
);
// Simulate Telegram Bot API error when file exceeds 20MB limit.
const fileTooBigError = createFileTooBigError();
const getFile = vi.fn().mockRejectedValue(fileTooBigError);
const result = await resolveMedia(makeCtx("video", getFile), MAX_MEDIA_BYTES, BOT_TOKEN);
// Should NOT retry - "file is too big" is a permanent error, not transient
// Should NOT retry - "file is too big" is a permanent error, not transient.
expect(getFile).toHaveBeenCalledTimes(1);
expect(result).toBeNull();
});
@@ -166,10 +172,7 @@ describe("resolveMedia getFile retry", () => {
it.each(["audio", "voice"] as const)(
"returns null for %s when file is too big",
async (mediaField) => {
const fileTooBigError = new Error(
"GrammyError: Call to 'getFile' failed! (400: Bad Request: file is too big)",
);
const getFile = vi.fn().mockRejectedValue(fileTooBigError);
const getFile = vi.fn().mockRejectedValue(createFileTooBigError());
const result = await resolveMedia(makeCtx(mediaField, getFile), MAX_MEDIA_BYTES, BOT_TOKEN);
@@ -178,14 +181,17 @@ describe("resolveMedia getFile retry", () => {
},
);
it("still retries transient errors even after encountering file too big in different call", async () => {
const getFile = setupTransientGetFileRetry();
const promise = resolveMedia(makeCtx("voice", getFile), MAX_MEDIA_BYTES, BOT_TOKEN);
await flushRetryTimers();
const result = await promise;
it("throws when getFile returns no file_path", async () => {
const getFile = vi.fn().mockResolvedValue({});
await expect(
resolveMedia(makeCtx("voice", getFile), MAX_MEDIA_BYTES, BOT_TOKEN),
).rejects.toThrow("Telegram getFile returned no file_path");
expect(getFile).toHaveBeenCalledTimes(1);
});
// Should retry transient errors
expect(getFile).toHaveBeenCalledTimes(2);
it("still retries transient errors even after encountering file too big in different call", async () => {
const result = await expectTransientGetFileRetrySuccess();
// Should retry transient errors.
expect(result).not.toBeNull();
});
});

View File

@@ -71,6 +71,25 @@ function createSendMessageHarness(messageId = 4) {
return { runtime, sendMessage, bot };
}
function createVoiceMessagesForbiddenError() {
return new Error(
"GrammyError: Call to 'sendVoice' failed! (400: Bad Request: VOICE_MESSAGES_FORBIDDEN)",
);
}
function createVoiceFailureHarness(params: {
voiceError: Error;
sendMessageResult?: { message_id: number; chat: { id: string } };
}) {
const runtime = createRuntime();
const sendVoice = vi.fn().mockRejectedValue(params.voiceError);
const sendMessage = params.sendMessageResult
? vi.fn().mockResolvedValue(params.sendMessageResult)
: vi.fn();
const bot = createBot({ sendVoice, sendMessage });
return { runtime, sendVoice, sendMessage, bot };
}
describe("deliverReplies", () => {
beforeEach(() => {
loadWebMedia.mockClear();
@@ -258,19 +277,13 @@ describe("deliverReplies", () => {
});
it("falls back to text when sendVoice fails with VOICE_MESSAGES_FORBIDDEN", async () => {
const runtime = createRuntime();
const sendVoice = vi
.fn()
.mockRejectedValue(
new Error(
"GrammyError: Call to 'sendVoice' failed! (400: Bad Request: VOICE_MESSAGES_FORBIDDEN)",
),
);
const sendMessage = vi.fn().mockResolvedValue({
message_id: 5,
chat: { id: "123" },
const { runtime, sendVoice, sendMessage, bot } = createVoiceFailureHarness({
voiceError: createVoiceMessagesForbiddenError(),
sendMessageResult: {
message_id: 5,
chat: { id: "123" },
},
});
const bot = createBot({ sendVoice, sendMessage });
mockMediaLoad("note.ogg", "audio/ogg", "voice");
@@ -315,16 +328,9 @@ describe("deliverReplies", () => {
});
it("rethrows VOICE_MESSAGES_FORBIDDEN when no text fallback is available", async () => {
const runtime = createRuntime();
const sendVoice = vi
.fn()
.mockRejectedValue(
new Error(
"GrammyError: Call to 'sendVoice' failed! (400: Bad Request: VOICE_MESSAGES_FORBIDDEN)",
),
);
const sendMessage = vi.fn();
const bot = createBot({ sendVoice, sendMessage });
const { runtime, sendVoice, sendMessage, bot } = createVoiceFailureHarness({
voiceError: createVoiceMessagesForbiddenError(),
});
mockMediaLoad("note.ogg", "audio/ogg", "voice");