Skip to content
61 linesCodeBlameRaw
1import assert from "node:assert/strict";
2import { test } from "node:test";
3
4import { FIRST_RETRY_MS, IDLE_POLL_MS, MAX_RETRY_MS, QUIET_AFTER_MS, RUNNING_POLL_MS, elapsed, nextPoll, quietWords, retryAfterMs } from "./log-poll.ts";
5
6const live = { failures: 0, live: true, running: true };
7
8test("a live job is asked every second while a step runs, less often while none does", () => {
9 assert.deepEqual(nextPoll({ ok: true, done: false, full: false }, live), { delayMs: RUNNING_POLL_MS, failures: 0 });
10 assert.deepEqual(nextPoll({ ok: true, done: false, full: false }, { ...live, running: false }), { delayMs: IDLE_POLL_MS, failures: 0 });
11 // A full page: the rest at once.
12 assert.deepEqual(nextPoll({ ok: true, done: false, full: true }, live), { delayMs: 0, failures: 0 });
13 // Done, or shown as finished: no more.
14 assert.deepEqual(nextPoll({ ok: true, done: true, full: false }, live), { delayMs: null, failures: 0 });
15 assert.deepEqual(nextPoll({ ok: true, done: false, full: false }, { ...live, live: false }), { delayMs: null, failures: 0 });
16});
17
18test("a failed answer is tried again, waiting longer each time, never stopping a live log", () => {
19 let failures = 0;
20 const waits: (number | null)[] = [];
21 for (let i = 0; i < 8; i++) {
22 const next = nextPoll({ ok: false, status: 503 }, { ...live, failures });
23 failures = next.failures;
24 waits.push(next.delayMs);
25 }
26 assert.deepEqual(waits, [FIRST_RETRY_MS, 4000, 8000, 16_000, MAX_RETRY_MS, MAX_RETRY_MS, MAX_RETRY_MS, MAX_RETRY_MS]);
27 // An answer resets it.
28 assert.deepEqual(nextPoll({ ok: true, done: false, full: false }, { ...live, failures }), { delayMs: RUNNING_POLL_MS, failures: 0 });
29 // The network failing is a failure like any other.
30 assert.deepEqual(nextPoll({ ok: false }, live), { delayMs: FIRST_RETRY_MS, failures: 1 });
31});
32
33test("a 429 waits as long as Retry-After says; a 404 stops; a finished job's last fetch gives up", () => {
34 assert.deepEqual(nextPoll({ ok: false, status: 429, retryAfterMs: 20_000 }, live), { delayMs: 20_000, failures: 1 });
35 assert.deepEqual(nextPoll({ ok: false, status: 429, retryAfterMs: 500 }, live), { delayMs: FIRST_RETRY_MS, failures: 1 });
36 assert.deepEqual(nextPoll({ ok: false, status: 429, retryAfterMs: 3_600_000 }, live), { delayMs: 2 * MAX_RETRY_MS, failures: 1 });
37 assert.deepEqual(nextPoll({ ok: false, status: 404 }, live), { delayMs: null, failures: 1 });
38 const finished = { failures: 4, live: false, running: false };
39 assert.deepEqual(nextPoll({ ok: false, status: 500 }, finished), { delayMs: null, failures: 5 });
40 assert.equal(nextPoll({ ok: false, status: 500 }, { ...finished, failures: 0 }).delayMs, FIRST_RETRY_MS);
41});
42
43test("a quiet step says for how long it has printed nothing", () => {
44 const start = Date.parse("2026-10-12T10:00:00Z");
45 assert.equal(quietWords(start, null, start + 130_000), "Running · no output yet, 2m 10s");
46 assert.equal(quietWords(null, null, start), "Running · no output yet");
47 // Output a moment ago: nothing to say.
48 assert.equal(quietWords(start, start + 60_000, start + 65_000), null);
49 assert.equal(quietWords(start, start + 60_000, start + 60_000 + QUIET_AFTER_MS), "Running · no output for 10s");
50 assert.equal(quietWords(start, start, start + 3_725_000), "Running · no output for 1h 2m");
51 assert.equal(elapsed(-5), "0s");
52});
53
54test("Retry-After reads as seconds or as a date", () => {
55 assert.equal(retryAfterMs("12"), 12_000);
56 assert.equal(retryAfterMs(null), undefined);
57 assert.equal(retryAfterMs("soon"), undefined);
58 const now = Date.parse("2026-10-12T10:00:00Z");
59 assert.equal(retryAfterMs("Mon, 12 Oct 2026 10:00:30 GMT", now), 30_000);
60 assert.equal(retryAfterMs("Mon, 12 Oct 2026 09:00:00 GMT", now), 0);
61});