Files
bDS/tests/engine/ai/retryWithBackoff.test.ts
Georg Bauer b855d61524 Feature/post media translations (#42)
* chore: updated todo with translation ideas

* feat: first take at the implementation of translations

* fix: small addition for the translation feature

* feat: support language switching in the editor and preview

* feat: better handling of long bodies by not running them through a json envelope

* fix: unknown macros have better fallback

* feat: api for python to get translations

* fix: strip dumb prefix of content in translation

* feat: extend meta diff for translations

* feat: hook up translations to rebuild-from-disk

* feat: generation of the website prefers project language, falling back to canonical language

* fix: crashes during rendering

* feat: translation validation report

* fix: made the translation validation actually work

* chore: reorganization of menu

* fix: some topics cleanup

* chore: updated doc

* feat: translations for media

* feat: more aligned in UI/UX

* feat: edit translations possible

* chore: added full multi-language todo

* chore: updated todo for clarity

* feat: implementation of full multi-linguality

* fix: page creation creates pages

* fix: flags on every page

* fix: better prompt

* feat: made MCP server aware of language content

* feat: python tools for translations

* fix: better fill-in-translations

* fix: better prompt for translation. maybe.

* fix: losing posts from search due to translation process

* fix: translation validation handles in-db content and fill-in of missing translations fixed to flush

* fix: faster scanning for infilling of missing translations

* chore: updated agent instructions

* feat: calendar and tag cloud respect current language now

* fix: retries going up

* fix: got metadata-diff and rebuild into sync

* fix: extended meta-diff for timestamps

* fix: made website validation look at translated content, too

* fix: multi-lingual search

* chore: refactor Editor.tsx into two separate editors

* feat: do language detection when no explicit language given

---------

Co-authored-by: hugo <hugoms@me.com>
2026-03-09 14:43:18 +01:00

117 lines
3.5 KiB
TypeScript

import { describe, expect, it, vi } from 'vitest';
import { retryWithBackoff } from '../../../src/main/engine/ai/retry';
describe('retryWithBackoff', () => {
it('returns immediately on success (no retries)', async () => {
const fn = vi.fn(async () => ({ success: true, value: 42 }));
const result = await retryWithBackoff(fn);
expect(result).toEqual({ success: true, value: 42 });
expect(fn).toHaveBeenCalledTimes(1);
});
it('retries up to maxRetries times with exponential delays on failure', async () => {
vi.useFakeTimers();
const fn = vi.fn(async () => ({ success: false, error: 'rate limited' }));
const promise = retryWithBackoff(fn, { maxRetries: 3, baseDelayMs: 5000 });
// Initial call happens immediately
await vi.advanceTimersByTimeAsync(0);
expect(fn).toHaveBeenCalledTimes(1);
// Retry 1 after 5s
await vi.advanceTimersByTimeAsync(5000);
expect(fn).toHaveBeenCalledTimes(2);
// Retry 2 after 10s
await vi.advanceTimersByTimeAsync(10000);
expect(fn).toHaveBeenCalledTimes(3);
// Retry 3 after 20s
await vi.advanceTimersByTimeAsync(20000);
expect(fn).toHaveBeenCalledTimes(4);
const result = await promise;
expect(result).toEqual({ success: false, error: 'rate limited' });
vi.useRealTimers();
});
it('stops retrying once the function succeeds', async () => {
vi.useFakeTimers();
const fn = vi.fn()
.mockResolvedValueOnce({ success: false, error: 'fail' })
.mockResolvedValueOnce({ success: true, value: 'ok' });
const promise = retryWithBackoff(fn, { maxRetries: 3, baseDelayMs: 5000 });
// Initial call fails
await vi.advanceTimersByTimeAsync(0);
expect(fn).toHaveBeenCalledTimes(1);
// Retry 1 after 5s — succeeds
await vi.advanceTimersByTimeAsync(5000);
expect(fn).toHaveBeenCalledTimes(2);
const result = await promise;
expect(result).toEqual({ success: true, value: 'ok' });
// Should not retry further
expect(fn).toHaveBeenCalledTimes(2);
vi.useRealTimers();
});
it('uses default 3 retries with 5s base delay', async () => {
vi.useFakeTimers();
const fn = vi.fn(async () => ({ success: false }));
const promise = retryWithBackoff(fn);
// Initial + 3 retries = 4 total calls
await vi.advanceTimersByTimeAsync(0); // initial
await vi.advanceTimersByTimeAsync(5000); // retry 1 (5s)
await vi.advanceTimersByTimeAsync(10000); // retry 2 (10s)
await vi.advanceTimersByTimeAsync(20000); // retry 3 (20s)
await promise;
expect(fn).toHaveBeenCalledTimes(4);
vi.useRealTimers();
});
it('applies exponential doubling: 5s, 10s, 20s', async () => {
vi.useFakeTimers();
const delays: number[] = [];
const setTimeoutSpy = vi.spyOn(globalThis, 'setTimeout');
const fn = vi.fn(async () => ({ success: false }));
const promise = retryWithBackoff(fn, { maxRetries: 3, baseDelayMs: 5000 });
await vi.advanceTimersByTimeAsync(0);
await vi.advanceTimersByTimeAsync(5000);
await vi.advanceTimersByTimeAsync(10000);
await vi.advanceTimersByTimeAsync(20000);
await promise;
// Extract the delay values passed to setTimeout for our retries
const timeoutCalls = setTimeoutSpy.mock.calls
.filter(([, ms]) => typeof ms === 'number' && ms >= 5000)
.map(([, ms]) => ms);
expect(timeoutCalls).toContain(5000);
expect(timeoutCalls).toContain(10000);
expect(timeoutCalls).toContain(20000);
setTimeoutSpy.mockRestore();
vi.useRealTimers();
});
});