diff --git a/src/tools.mjs b/src/tools.mjs index e0848da..c0f34c3 100644 --- a/src/tools.mjs +++ b/src/tools.mjs @@ -122,6 +122,9 @@ export function sleep(ms) { * Retry a function with exponential backoff */ export async function retry(fn, maxAttempts = 3, baseDelay = 1000) { + if (!Number.isInteger(maxAttempts) || maxAttempts < 1) { + throw new Error('retry maxAttempts must be a positive integer'); + } for (let attempt = 1; attempt <= maxAttempts; attempt++) { try { return await fn(); diff --git a/test/tools.test.mjs b/test/tools.test.mjs index 7ec43ac..f0fb1b3 100644 --- a/test/tools.test.mjs +++ b/test/tools.test.mjs @@ -15,7 +15,7 @@ import { fileURLToPath } from "node:url"; import { spawn } from "node:child_process"; import test from "node:test"; -import { TOOLS, resolveTool, toolList } from "../src/tools.mjs"; +import { TOOLS, resolveTool, retry, toolList } from "../src/tools.mjs"; const BIN = fileURLToPath(new URL("../bin/moshcode.mjs", import.meta.url)); @@ -204,3 +204,25 @@ test("moshcode install reports an Object.prototype name as unknown", async () => assert.match(result.stderr, /usage: moshcode install /); assert.doesNotMatch(result.stderr, /TypeError/); }); + +test("retry rejects non-positive attempt limits", async () => { + let calls = 0; + + await assert.rejects( + retry(() => { calls++; }, 0, 1), + /maxAttempts must be a positive integer/, + ); + assert.equal(calls, 0); +}); + +test("retry retries until a later attempt succeeds", async () => { + let calls = 0; + const result = await retry(() => { + calls++; + if (calls < 2) throw new Error("not yet"); + return "ok"; + }, 2, 1); + + assert.equal(result, "ok"); + assert.equal(calls, 2); +});