Bun 1.4.3 fake timers: test a Sume poll loop with request timeouts

Bun 1.4.3 fixes advanceTimersByTime spinning with AbortSignal.timeout. Test a Sume job poll loop that honors next_poll_after_seconds without real waits.

4 min readSume
All posts

Yes, you can unit-test a Sume status-poll loop in bun test with fake timers even when each request carries AbortSignal.timeout(). Bun v1.4.3, released October 10, 2026, fixes advanceTimersByTime() and runOnlyPendingTimers() never returning when an abort listener armed a new AbortSignal.timeout(0). That is a narrower case than a plain per-request timeout, so confirm your own loop under fake timers.

The test below feeds the loop three fake status replies and moves the clock instead of sleeping, so it finishes in milliseconds.

What changed in Bun 1.4.3

The Bun v1.4.3 release notes (read 2026-10-10) list a timers fix: with jest.useFakeTimers(), the two calls no longer hang when an abort listener arms a new AbortSignal.timeout(0). The same notes list an unrelated bun test fix, expect.not.any() verdicts, that you may also hit in poll-loop assertions.

Bun v1.4.3 items that touch a poll-loop test (read 2026-10-10)
AreaChange in v1.4.3
Fake timersadvanceTimersByTime() and runOnlyPendingTimers() no longer hang when an abort listener arms AbortSignal.timeout(0)
Async matchersexpect.not.any(), expect.resolvesTo.any() and expect.rejectsTo.any() give the right verdict for primitives
Parallel safetyWith bun test --parallel, a crashed worker no longer leaves processes its test spawned

The loop under test

Sume's jobs docs say to poll GET /v1/jobs/:id/status until terminal is true, and to obey next_poll_after_seconds when it is present. The function below does that, with a 15-second abort on each request so a hung read cannot stall the loop. It takes fetch as a parameter so a test can replace it.

export async function pollJob(id: string, key: string, fetchFn = fetch) {
  for (let attempt = 0; attempt < 30; attempt++) {
    const res = await fetchFn(`https://api.sume.com/v1/jobs/${id}/status`, {
      headers: { "x-api-key": key },
      signal: AbortSignal.timeout(15_000),
    });
    const s = await res.json();
    if (s.terminal) return s;
    await new Promise((r) => setTimeout(r, (s.next_poll_after_seconds ?? 2) * 1000));
  }
  throw new Error("poll deadline");
}

The test

The fake fetch returns two non-terminal replies that ask for a 5-second wait, then a terminal one. The test yields once so the fake response resolves, then moves the clock by 5 seconds, twice. Nothing waits in real time.

import { test, expect, jest } from "bun:test";
import { pollJob } from "./poll";

test("polls until terminal without real waiting", async () => {
  jest.useFakeTimers();
  const replies = [
    { terminal: false, next_poll_after_seconds: 5 },
    { terminal: false, next_poll_after_seconds: 5 },
    { terminal: true, sume_status: "completed" },
  ];
  const fake = (async () => Response.json(replies.shift())) as unknown as typeof fetch;
  const done = pollJob("job_1", "k", fake);
  for (let i = 0; i < 2; i++) {
    await new Promise((r) => setImmediate(r)); // let the fake fetch resolve
    jest.advanceTimersByTime(5_000);
  }
  expect((await done).sume_status).toBe("completed");
  jest.useRealTimers();
});

What this test does not prove

A green test shows your loop logic is right. It does not show that Sume, your network, or your wallet behave. Keep these limits in mind.

  • It does not exercise the network. A real 429 or 402 comes back as a Sume error envelope, not as a status payload, so add a case for that in your own wrapper.
  • It does not prove billing safety. A poll deadline in your client does not cancel the job; it keeps running and billing, so store the job id before you give up.
  • Pin your Bun version in CI. The fix named above is in 1.4.3; on an older Bun, check that the test finishes instead of hanging.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume