test: harden infra formatter and retry coverage

This commit is contained in:
Peter Steinberger 2026-03-13 17:47:47 +00:00
parent 4aec20d365
commit 2d32cf2839
2 changed files with 180 additions and 47 deletions

View File

@ -1,4 +1,4 @@
import { describe, expect, it } from "vitest"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { formatUtcTimestamp, formatZonedTimestamp, resolveTimezone } from "./format-datetime.js"; import { formatUtcTimestamp, formatZonedTimestamp, resolveTimezone } from "./format-datetime.js";
import { import {
formatDurationCompact, formatDurationCompact,
@ -188,6 +188,15 @@ describe("format-relative", () => {
}); });
describe("formatRelativeTimestamp", () => { describe("formatRelativeTimestamp", () => {
beforeEach(() => {
vi.useFakeTimers();
vi.setSystemTime(new Date("2024-02-10T12:00:00.000Z"));
});
afterEach(() => {
vi.useRealTimers();
});
it("returns fallback for invalid timestamp input", () => { it("returns fallback for invalid timestamp input", () => {
for (const value of [null, undefined]) { for (const value of [null, undefined]) {
expect(formatRelativeTimestamp(value)).toBe("n/a"); expect(formatRelativeTimestamp(value)).toBe("n/a");
@ -197,21 +206,39 @@ describe("format-relative", () => {
it.each([ it.each([
{ offsetMs: -10000, expected: "just now" }, { offsetMs: -10000, expected: "just now" },
{ offsetMs: -30000, expected: "just now" },
{ offsetMs: -300000, expected: "5m ago" }, { offsetMs: -300000, expected: "5m ago" },
{ offsetMs: -7200000, expected: "2h ago" }, { offsetMs: -7200000, expected: "2h ago" },
{ offsetMs: -(47 * 3600000), expected: "47h ago" },
{ offsetMs: -(48 * 3600000), expected: "2d ago" },
{ offsetMs: 30000, expected: "in <1m" }, { offsetMs: 30000, expected: "in <1m" },
{ offsetMs: 300000, expected: "in 5m" }, { offsetMs: 300000, expected: "in 5m" },
{ offsetMs: 7200000, expected: "in 2h" }, { offsetMs: 7200000, expected: "in 2h" },
])("formats relative timestamp for offset $offsetMs", ({ offsetMs, expected }) => { ])("formats relative timestamp for offset $offsetMs", ({ offsetMs, expected }) => {
const now = Date.now(); expect(formatRelativeTimestamp(Date.now() + offsetMs)).toBe(expected);
expect(formatRelativeTimestamp(now + offsetMs)).toBe(expected);
}); });
it("falls back to date for old timestamps when enabled", () => { it.each([
const oldDate = Date.now() - 30 * 24 * 3600000; // 30 days ago {
const result = formatRelativeTimestamp(oldDate, { dateFallback: true }); name: "keeps 7-day-old timestamps relative",
// Should be a short date like "Jan 9" not "30d ago" offsetMs: -7 * 24 * 3600000,
expect(result).toMatch(/[A-Z][a-z]{2} \d{1,2}/); options: { dateFallback: true, timezone: "UTC" },
expected: "7d ago",
},
{
name: "falls back to a short date once the timestamp is older than 7 days",
offsetMs: -8 * 24 * 3600000,
options: { dateFallback: true, timezone: "UTC" },
expected: "Feb 2",
},
{
name: "keeps relative output when date fallback is disabled",
offsetMs: -8 * 24 * 3600000,
options: { timezone: "UTC" },
expected: "8d ago",
},
])("$name", ({ offsetMs, options, expected }) => {
expect(formatRelativeTimestamp(Date.now() + offsetMs, options)).toBe(expected);
}); });
}); });
}); });

View File

@ -1,48 +1,154 @@
import { describe, expect, it, vi } from "vitest"; import { afterEach, describe, expect, it, vi } from "vitest";
import { createTelegramRetryRunner } from "./retry-policy.js"; import { createTelegramRetryRunner } from "./retry-policy.js";
const ZERO_DELAY_RETRY = { attempts: 3, minDelayMs: 0, maxDelayMs: 0, jitter: 0 };
describe("createTelegramRetryRunner", () => { describe("createTelegramRetryRunner", () => {
afterEach(() => {
vi.useRealTimers();
vi.restoreAllMocks();
});
describe("strictShouldRetry", () => { describe("strictShouldRetry", () => {
it("without strictShouldRetry: ECONNRESET is retried via regex fallback even when predicate returns false", async () => { it.each([
const fn = vi {
.fn() name: "falls back to regex matching when strictShouldRetry is disabled",
.mockRejectedValue(Object.assign(new Error("read ECONNRESET"), { code: "ECONNRESET" })); runnerOptions: {
const runner = createTelegramRetryRunner({ retry: { ...ZERO_DELAY_RETRY, attempts: 2 },
retry: { attempts: 2, minDelayMs: 0, maxDelayMs: 0, jitter: 0 }, shouldRetry: () => false,
shouldRetry: () => false, // predicate says no },
// strictShouldRetry not set — regex fallback still applies fnSteps: [
}); {
await expect(runner(fn, "test")).rejects.toThrow("ECONNRESET"); type: "reject" as const,
// Regex matches "reset" so it retried despite shouldRetry returning false value: Object.assign(new Error("read ECONNRESET"), {
expect(fn).toHaveBeenCalledTimes(2); code: "ECONNRESET",
}); }),
},
],
expectedCalls: 2,
expectedError: "ECONNRESET",
},
{
name: "suppresses regex fallback when strictShouldRetry is enabled",
runnerOptions: {
retry: { ...ZERO_DELAY_RETRY, attempts: 2 },
shouldRetry: () => false,
strictShouldRetry: true,
},
fnSteps: [
{
type: "reject" as const,
value: Object.assign(new Error("read ECONNRESET"), {
code: "ECONNRESET",
}),
},
],
expectedCalls: 1,
expectedError: "ECONNRESET",
},
{
name: "still retries when the strict predicate returns true",
runnerOptions: {
retry: { ...ZERO_DELAY_RETRY, attempts: 2 },
shouldRetry: (err: unknown) => (err as { code?: string }).code === "ECONNREFUSED",
strictShouldRetry: true,
},
fnSteps: [
{
type: "reject" as const,
value: Object.assign(new Error("ECONNREFUSED"), {
code: "ECONNREFUSED",
}),
},
{ type: "resolve" as const, value: "ok" },
],
expectedCalls: 2,
expectedValue: "ok",
},
{
name: "does not retry unrelated errors when neither predicate nor regex match",
runnerOptions: {
retry: { ...ZERO_DELAY_RETRY, attempts: 2 },
},
fnSteps: [
{
type: "reject" as const,
value: Object.assign(new Error("permission denied"), {
code: "EACCES",
}),
},
],
expectedCalls: 1,
expectedError: "permission denied",
},
{
name: "keeps retrying retriable errors until attempts are exhausted",
runnerOptions: {
retry: ZERO_DELAY_RETRY,
},
fnSteps: [
{
type: "reject" as const,
value: Object.assign(new Error("connection timeout"), {
code: "ETIMEDOUT",
}),
},
],
expectedCalls: 3,
expectedError: "connection timeout",
},
])("$name", async ({ runnerOptions, fnSteps, expectedCalls, expectedValue, expectedError }) => {
vi.useFakeTimers();
const runner = createTelegramRetryRunner(runnerOptions);
const fn = vi.fn();
const allRejects = fnSteps.length > 0 && fnSteps.every((step) => step.type === "reject");
if (allRejects) {
fn.mockRejectedValue(fnSteps[0]?.value);
}
for (const [index, step] of fnSteps.entries()) {
if (allRejects && index > 0) {
break;
}
if (step.type === "reject") {
fn.mockRejectedValueOnce(step.value);
} else {
fn.mockResolvedValueOnce(step.value);
}
}
it("with strictShouldRetry=true: ECONNRESET is NOT retried when predicate returns false", async () => { const promise = runner(fn, "test");
const fn = vi const assertion = expectedError
.fn() ? expect(promise).rejects.toThrow(expectedError)
.mockRejectedValue(Object.assign(new Error("read ECONNRESET"), { code: "ECONNRESET" })); : expect(promise).resolves.toBe(expectedValue);
const runner = createTelegramRetryRunner({
retry: { attempts: 2, minDelayMs: 0, maxDelayMs: 0, jitter: 0 },
shouldRetry: () => false,
strictShouldRetry: true, // predicate is authoritative
});
await expect(runner(fn, "test")).rejects.toThrow("ECONNRESET");
// No retry — predicate returned false and regex fallback was suppressed
expect(fn).toHaveBeenCalledTimes(1);
});
it("with strictShouldRetry=true: ECONNREFUSED is still retried when predicate returns true", async () => { await vi.runAllTimersAsync();
const fn = vi await assertion;
.fn() expect(fn).toHaveBeenCalledTimes(expectedCalls);
.mockRejectedValueOnce(Object.assign(new Error("ECONNREFUSED"), { code: "ECONNREFUSED" }))
.mockResolvedValue("ok");
const runner = createTelegramRetryRunner({
retry: { attempts: 2, minDelayMs: 0, maxDelayMs: 0, jitter: 0 },
shouldRetry: (err) => (err as { code?: string }).code === "ECONNREFUSED",
strictShouldRetry: true,
});
await expect(runner(fn, "test")).resolves.toBe("ok");
expect(fn).toHaveBeenCalledTimes(2);
}); });
}); });
it("honors nested retry_after hints before retrying", async () => {
vi.useFakeTimers();
const runner = createTelegramRetryRunner({
retry: { attempts: 2, minDelayMs: 0, maxDelayMs: 1_000, jitter: 0 },
});
const fn = vi
.fn()
.mockRejectedValueOnce({
message: "429 Too Many Requests",
response: { parameters: { retry_after: 1 } },
})
.mockResolvedValue("ok");
const promise = runner(fn, "test");
expect(fn).toHaveBeenCalledTimes(1);
await vi.advanceTimersByTimeAsync(999);
expect(fn).toHaveBeenCalledTimes(1);
await vi.advanceTimersByTimeAsync(1);
await expect(promise).resolves.toBe("ok");
expect(fn).toHaveBeenCalledTimes(2);
});
}); });