Skip to content

Commit e200071

Browse files
committed
test: add comprehensive edge case tests for retryOnTransientError
Signed-off-by: leocavalcante <leo@cavalcante.dev>
1 parent 23c361a commit e200071

File tree

1 file changed

+214
-0
lines changed

1 file changed

+214
-0
lines changed

tests/paths.test.ts

Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -506,6 +506,220 @@ describe("paths.mjs exports", () => {
506506
expect(elapsed).toBeGreaterThanOrEqual(40)
507507
expect(elapsed).toBeLessThan(150)
508508
})
509+
510+
describe("input validation", () => {
511+
it("should throw TypeError when fn is null", async () => {
512+
await expect(retryOnTransientError(null as unknown as () => void)).rejects.toThrow(
513+
TypeError,
514+
)
515+
})
516+
517+
it("should throw TypeError when fn is undefined", async () => {
518+
await expect(retryOnTransientError(undefined as unknown as () => void)).rejects.toThrow(
519+
TypeError,
520+
)
521+
})
522+
523+
it("should throw TypeError when fn is not a function", async () => {
524+
await expect(
525+
retryOnTransientError("not a function" as unknown as () => void),
526+
).rejects.toThrow(TypeError)
527+
await expect(retryOnTransientError(123 as unknown as () => void)).rejects.toThrow(TypeError)
528+
await expect(retryOnTransientError({} as unknown as () => void)).rejects.toThrow(TypeError)
529+
})
530+
531+
it("should handle retries: 0 (no retries, only initial attempt)", async () => {
532+
let callCount = 0
533+
await expect(
534+
retryOnTransientError(
535+
() => {
536+
callCount++
537+
const err = Object.assign(new Error("EAGAIN"), { code: "EAGAIN" })
538+
throw err
539+
},
540+
{ retries: 0 },
541+
),
542+
).rejects.toThrow("EAGAIN")
543+
expect(callCount).toBe(1) // Only initial attempt, no retries
544+
})
545+
546+
it("should throw undefined when retries is negative (loop never executes)", async () => {
547+
let callCount = 0
548+
// With retries: -1, loop condition (0 <= -1) is false, so loop never runs
549+
// lastError is undefined, so it throws undefined
550+
await expect(
551+
retryOnTransientError(
552+
() => {
553+
callCount++
554+
const err = Object.assign(new Error("EAGAIN"), { code: "EAGAIN" })
555+
throw err
556+
},
557+
{ retries: -1 },
558+
),
559+
).rejects.toBeUndefined()
560+
expect(callCount).toBe(0) // Loop never runs
561+
})
562+
563+
it("should throw undefined when retries is -5 (loop never executes)", async () => {
564+
let callCount = 0
565+
await expect(
566+
retryOnTransientError(
567+
() => {
568+
callCount++
569+
const err = Object.assign(new Error("EAGAIN"), { code: "EAGAIN" })
570+
throw err
571+
},
572+
{ retries: -5 },
573+
),
574+
).rejects.toBeUndefined()
575+
expect(callCount).toBe(0) // Loop never runs
576+
})
577+
578+
it("should handle initialDelayMs: 0 (no delay)", async () => {
579+
let callCount = 0
580+
const start = Date.now()
581+
await retryOnTransientError(
582+
() => {
583+
callCount++
584+
if (callCount < 3) {
585+
const err = Object.assign(new Error("EAGAIN"), { code: "EAGAIN" })
586+
throw err
587+
}
588+
return "success"
589+
},
590+
{ initialDelayMs: 0 },
591+
)
592+
const elapsed = Date.now() - start
593+
expect(callCount).toBe(3)
594+
// With 0ms delay, should complete very quickly
595+
expect(elapsed).toBeLessThan(50)
596+
})
597+
598+
it("should handle negative initialDelayMs (treated as ~1ms delay by setTimeout)", async () => {
599+
let callCount = 0
600+
const start = Date.now()
601+
await retryOnTransientError(
602+
() => {
603+
callCount++
604+
if (callCount < 2) {
605+
const err = Object.assign(new Error("EAGAIN"), { code: "EAGAIN" })
606+
throw err
607+
}
608+
return "success"
609+
},
610+
{ initialDelayMs: -100 },
611+
)
612+
const elapsed = Date.now() - start
613+
expect(callCount).toBe(2)
614+
// Negative delay is clamped to ~1ms by setTimeout, should complete quickly
615+
expect(elapsed).toBeLessThan(50)
616+
})
617+
618+
it("should throw undefined when retries is NaN (loop never executes)", async () => {
619+
let callCount = 0
620+
// When retries is NaN, the loop condition (attempt <= retries) is always false
621+
// So the loop never runs and lastError (undefined) is thrown
622+
await expect(
623+
retryOnTransientError(
624+
() => {
625+
callCount++
626+
const err = Object.assign(new Error("EAGAIN"), { code: "EAGAIN" })
627+
throw err
628+
},
629+
{ retries: Number.NaN },
630+
),
631+
).rejects.toBeUndefined()
632+
expect(callCount).toBe(0) // Loop never runs
633+
})
634+
635+
it("should handle non-numeric initialDelayMs (NaN becomes ~1ms delay)", async () => {
636+
let callCount = 0
637+
const start = Date.now()
638+
// When initialDelayMs is NaN, delay calculation produces NaN
639+
// setTimeout with NaN delay treats it as ~1ms
640+
await retryOnTransientError(
641+
() => {
642+
callCount++
643+
if (callCount < 2) {
644+
const err = Object.assign(new Error("EAGAIN"), { code: "EAGAIN" })
645+
throw err
646+
}
647+
return "success"
648+
},
649+
{ initialDelayMs: Number.NaN },
650+
)
651+
const elapsed = Date.now() - start
652+
expect(callCount).toBe(2)
653+
// NaN delay becomes ~1ms
654+
expect(elapsed).toBeLessThan(50)
655+
})
656+
657+
it("should handle Infinity retries (up to a reasonable test limit)", async () => {
658+
let callCount = 0
659+
// Test with Infinity but succeed after a few attempts to avoid infinite loop
660+
await retryOnTransientError(
661+
() => {
662+
callCount++
663+
if (callCount < 5) {
664+
const err = Object.assign(new Error("EAGAIN"), { code: "EAGAIN" })
665+
throw err
666+
}
667+
return "success"
668+
},
669+
{ retries: Number.POSITIVE_INFINITY, initialDelayMs: 1 },
670+
)
671+
expect(callCount).toBe(5)
672+
})
673+
674+
it("should throw TypeError when options is null (cannot destructure)", async () => {
675+
// The function tries to destructure null, which throws TypeError
676+
await expect(
677+
retryOnTransientError(
678+
() => "success",
679+
null as unknown as { retries?: number; initialDelayMs?: number },
680+
),
681+
).rejects.toThrow(TypeError)
682+
})
683+
684+
it("should handle options as undefined (uses defaults)", async () => {
685+
let callCount = 0
686+
const result = await retryOnTransientError(() => {
687+
callCount++
688+
return "success"
689+
}, undefined)
690+
expect(result).toBe("success")
691+
expect(callCount).toBe(1)
692+
})
693+
694+
it("should handle empty options object (uses defaults)", async () => {
695+
let callCount = 0
696+
await expect(
697+
retryOnTransientError(() => {
698+
callCount++
699+
const err = Object.assign(new Error("EAGAIN"), { code: "EAGAIN" })
700+
throw err
701+
}, {}),
702+
).rejects.toThrow("EAGAIN")
703+
// Default retries is 3, so 4 total attempts
704+
expect(callCount).toBe(4)
705+
})
706+
707+
it("should ignore extra unknown options properties", async () => {
708+
let callCount = 0
709+
const result = await retryOnTransientError(
710+
() => {
711+
callCount++
712+
return "success"
713+
},
714+
{ retries: 1, initialDelayMs: 10, unknownOption: "ignored" } as {
715+
retries?: number
716+
initialDelayMs?: number
717+
},
718+
)
719+
expect(result).toBe("success")
720+
expect(callCount).toBe(1)
721+
})
722+
})
509723
})
510724

511725
describe("parseFrontmatter", () => {

0 commit comments

Comments
 (0)