From b9ac2e12158e970db0d6d7b00808d4b0e7c9047f Mon Sep 17 00:00:00 2001 From: onmax Date: Tue, 1 Sep 2026 18:26:58 +0000 Subject: [PATCH 1/4] fix(blobs): keep retry delay environment-independent --- packages/blobs/src/main.test.ts | 17 ++++++++++++++--- packages/blobs/src/retry.test.ts | 24 ++++++++++++++++++++++++ packages/blobs/src/retry.ts | 4 +--- 3 files changed, 39 insertions(+), 6 deletions(-) create mode 100644 packages/blobs/src/retry.test.ts diff --git a/packages/blobs/src/main.test.ts b/packages/blobs/src/main.test.ts index 669d647eb..bfb56110e 100644 --- a/packages/blobs/src/main.test.ts +++ b/packages/blobs/src/main.test.ts @@ -4,7 +4,7 @@ import { env, version as nodeVersion } from 'node:process' import { MockFetch } from '@netlify/test-utils' import { base64Decode } from '@netlify/runtime-utils' import semver from 'semver' -import { describe, test, expect, beforeAll, afterEach } from 'vitest' +import { describe, test, expect, beforeAll, afterEach, vi } from 'vitest' import { base64Encode, streamToString } from '../test/util.js' @@ -30,6 +30,7 @@ beforeAll(async () => { afterEach(() => { delete env.NETLIFY_BLOBS_CONTEXT delete globalThis.netlifyBlobsContext + vi.useRealTimers() }) const deployID = '6527dfab35be400008332a1d' @@ -1145,6 +1146,8 @@ describe('set', () => { }) test('Retries failed operations', async () => { + vi.useFakeTimers() + const mockStore = new MockFetch() .put({ headers: { authorization: `Bearer ${apiToken}` }, @@ -1191,7 +1194,10 @@ describe('set', () => { siteID, }) - await blobs.set(key, value) + const operation = blobs.set(key, value) + + await vi.runAllTimersAsync() + await operation expect(mockStore.fulfilled).toBeTruthy() }) @@ -1252,6 +1258,8 @@ describe('set', () => { }) test('Retries failed operations', async () => { + vi.useFakeTimers() + const mockStore = new MockFetch() .put({ body: value, @@ -1286,7 +1294,10 @@ describe('set', () => { siteID, }) - await blobs.set(key, value) + const operation = blobs.set(key, value) + + await vi.runAllTimersAsync() + await operation expect(mockStore.fulfilled).toBeTruthy() }) diff --git a/packages/blobs/src/retry.test.ts b/packages/blobs/src/retry.test.ts new file mode 100644 index 000000000..69595e5a9 --- /dev/null +++ b/packages/blobs/src/retry.test.ts @@ -0,0 +1,24 @@ +import { afterEach, expect, test, vi } from 'vitest' + +import { fetchAndRetry } from './retry.ts' + +afterEach(() => { + vi.useRealTimers() +}) + +test('uses the production retry delay in test environments', async () => { + vi.useFakeTimers() + + const fetch = vi + .fn() + .mockResolvedValueOnce(new Response(null, { status: 500 })) + .mockResolvedValueOnce(new Response(null)) + const response = fetchAndRetry(fetch, 'https://example.com', {}) + + await vi.advanceTimersByTimeAsync(4999) + expect(fetch).toHaveBeenCalledTimes(1) + + await vi.advanceTimersByTimeAsync(1) + await expect(response).resolves.toHaveProperty('status', 200) + expect(fetch).toHaveBeenCalledTimes(2) +}) diff --git a/packages/blobs/src/retry.ts b/packages/blobs/src/retry.ts index 174aa03ae..336a6b530 100644 --- a/packages/blobs/src/retry.ts +++ b/packages/blobs/src/retry.ts @@ -1,8 +1,6 @@ -import { getEnvironment } from '@netlify/runtime-utils' - import type { Fetcher } from './types.ts' -const DEFAULT_RETRY_DELAY = getEnvironment().get('NODE_ENV') === 'test' ? 1 : 5000 +const DEFAULT_RETRY_DELAY = 5000 const MIN_RETRY_DELAY = 1000 const MAX_RETRY = 5 const RATE_LIMIT_HEADER = 'X-RateLimit-Reset' From 0229599f571ed78e8bf7b2d2b50dce3589438dfd Mon Sep 17 00:00:00 2001 From: onmax Date: Tue, 1 Sep 2026 19:57:05 +0000 Subject: [PATCH 2/4] fix(blobs): avoid protected Deno env access --- packages/blobs/src/main.test.ts | 17 +++-------------- packages/blobs/src/retry.test.ts | 22 ++++++++++++++++++---- packages/blobs/src/retry.ts | 2 +- 3 files changed, 22 insertions(+), 19 deletions(-) diff --git a/packages/blobs/src/main.test.ts b/packages/blobs/src/main.test.ts index bfb56110e..669d647eb 100644 --- a/packages/blobs/src/main.test.ts +++ b/packages/blobs/src/main.test.ts @@ -4,7 +4,7 @@ import { env, version as nodeVersion } from 'node:process' import { MockFetch } from '@netlify/test-utils' import { base64Decode } from '@netlify/runtime-utils' import semver from 'semver' -import { describe, test, expect, beforeAll, afterEach, vi } from 'vitest' +import { describe, test, expect, beforeAll, afterEach } from 'vitest' import { base64Encode, streamToString } from '../test/util.js' @@ -30,7 +30,6 @@ beforeAll(async () => { afterEach(() => { delete env.NETLIFY_BLOBS_CONTEXT delete globalThis.netlifyBlobsContext - vi.useRealTimers() }) const deployID = '6527dfab35be400008332a1d' @@ -1146,8 +1145,6 @@ describe('set', () => { }) test('Retries failed operations', async () => { - vi.useFakeTimers() - const mockStore = new MockFetch() .put({ headers: { authorization: `Bearer ${apiToken}` }, @@ -1194,10 +1191,7 @@ describe('set', () => { siteID, }) - const operation = blobs.set(key, value) - - await vi.runAllTimersAsync() - await operation + await blobs.set(key, value) expect(mockStore.fulfilled).toBeTruthy() }) @@ -1258,8 +1252,6 @@ describe('set', () => { }) test('Retries failed operations', async () => { - vi.useFakeTimers() - const mockStore = new MockFetch() .put({ body: value, @@ -1294,10 +1286,7 @@ describe('set', () => { siteID, }) - const operation = blobs.set(key, value) - - await vi.runAllTimersAsync() - await operation + await blobs.set(key, value) expect(mockStore.fulfilled).toBeTruthy() }) diff --git a/packages/blobs/src/retry.test.ts b/packages/blobs/src/retry.test.ts index 69595e5a9..81a34012c 100644 --- a/packages/blobs/src/retry.test.ts +++ b/packages/blobs/src/retry.test.ts @@ -1,21 +1,35 @@ import { afterEach, expect, test, vi } from 'vitest' -import { fetchAndRetry } from './retry.ts' - afterEach(() => { vi.useRealTimers() + vi.unstubAllGlobals() + vi.resetModules() +}) + +test('does not read protected Deno environment variables during import', async () => { + const getEnvironmentVariable = vi.fn(() => { + throw new Error('Requires env access') + }) + vi.stubGlobal('Deno', { env: { get: getEnvironmentVariable } }) + + const { getStore } = await import('./main.ts') + const store = getStore({ name: 'store', siteID: 'site-id', token: 'token' }) + + expect(store).toBeDefined() + expect(getEnvironmentVariable).not.toHaveBeenCalled() }) -test('uses the production retry delay in test environments', async () => { +test('uses the shorter retry delay in Node test environments', async () => { vi.useFakeTimers() + const { fetchAndRetry } = await import('./retry.ts') const fetch = vi .fn() .mockResolvedValueOnce(new Response(null, { status: 500 })) .mockResolvedValueOnce(new Response(null)) const response = fetchAndRetry(fetch, 'https://example.com', {}) - await vi.advanceTimersByTimeAsync(4999) + await vi.advanceTimersByTimeAsync(0) expect(fetch).toHaveBeenCalledTimes(1) await vi.advanceTimersByTimeAsync(1) diff --git a/packages/blobs/src/retry.ts b/packages/blobs/src/retry.ts index 336a6b530..4da9e70e2 100644 --- a/packages/blobs/src/retry.ts +++ b/packages/blobs/src/retry.ts @@ -1,6 +1,6 @@ import type { Fetcher } from './types.ts' -const DEFAULT_RETRY_DELAY = 5000 +const DEFAULT_RETRY_DELAY = typeof process === 'object' && process.env.NODE_ENV === 'test' ? 1 : 5000 const MIN_RETRY_DELAY = 1000 const MAX_RETRY = 5 const RATE_LIMIT_HEADER = 'X-RateLimit-Reset' From b79d6460e513aa6591b9039f95a156f01150d13b Mon Sep 17 00:00:00 2001 From: onmax Date: Wed, 2 Sep 2026 05:48:05 +0000 Subject: [PATCH 3/4] fix(blobs): remove runtime environment lookup --- packages/blobs/src/main.test.ts | 17 ++++++++++++++--- packages/blobs/src/retry.test.ts | 23 ++++++++++++----------- packages/blobs/src/retry.ts | 2 +- 3 files changed, 27 insertions(+), 15 deletions(-) diff --git a/packages/blobs/src/main.test.ts b/packages/blobs/src/main.test.ts index 669d647eb..bfb56110e 100644 --- a/packages/blobs/src/main.test.ts +++ b/packages/blobs/src/main.test.ts @@ -4,7 +4,7 @@ import { env, version as nodeVersion } from 'node:process' import { MockFetch } from '@netlify/test-utils' import { base64Decode } from '@netlify/runtime-utils' import semver from 'semver' -import { describe, test, expect, beforeAll, afterEach } from 'vitest' +import { describe, test, expect, beforeAll, afterEach, vi } from 'vitest' import { base64Encode, streamToString } from '../test/util.js' @@ -30,6 +30,7 @@ beforeAll(async () => { afterEach(() => { delete env.NETLIFY_BLOBS_CONTEXT delete globalThis.netlifyBlobsContext + vi.useRealTimers() }) const deployID = '6527dfab35be400008332a1d' @@ -1145,6 +1146,8 @@ describe('set', () => { }) test('Retries failed operations', async () => { + vi.useFakeTimers() + const mockStore = new MockFetch() .put({ headers: { authorization: `Bearer ${apiToken}` }, @@ -1191,7 +1194,10 @@ describe('set', () => { siteID, }) - await blobs.set(key, value) + const operation = blobs.set(key, value) + + await vi.runAllTimersAsync() + await operation expect(mockStore.fulfilled).toBeTruthy() }) @@ -1252,6 +1258,8 @@ describe('set', () => { }) test('Retries failed operations', async () => { + vi.useFakeTimers() + const mockStore = new MockFetch() .put({ body: value, @@ -1286,7 +1294,10 @@ describe('set', () => { siteID, }) - await blobs.set(key, value) + const operation = blobs.set(key, value) + + await vi.runAllTimersAsync() + await operation expect(mockStore.fulfilled).toBeTruthy() }) diff --git a/packages/blobs/src/retry.test.ts b/packages/blobs/src/retry.test.ts index 81a34012c..80c716528 100644 --- a/packages/blobs/src/retry.test.ts +++ b/packages/blobs/src/retry.test.ts @@ -6,20 +6,21 @@ afterEach(() => { vi.resetModules() }) -test('does not read protected Deno environment variables during import', async () => { - const getEnvironmentVariable = vi.fn(() => { - throw new Error('Requires env access') +test('does not access the process environment during import', async () => { + const accessEnvironment = vi.fn(() => { + throw new Error('Environment access is not allowed') + }) + vi.stubGlobal('process', { + get env() { + return accessEnvironment() + }, }) - vi.stubGlobal('Deno', { env: { get: getEnvironmentVariable } }) - - const { getStore } = await import('./main.ts') - const store = getStore({ name: 'store', siteID: 'site-id', token: 'token' }) - expect(store).toBeDefined() - expect(getEnvironmentVariable).not.toHaveBeenCalled() + await expect(import('./retry.ts')).resolves.toHaveProperty('fetchAndRetry') + expect(accessEnvironment).not.toHaveBeenCalled() }) -test('uses the shorter retry delay in Node test environments', async () => { +test('uses the production retry delay in test environments', async () => { vi.useFakeTimers() const { fetchAndRetry } = await import('./retry.ts') @@ -29,7 +30,7 @@ test('uses the shorter retry delay in Node test environments', async () => { .mockResolvedValueOnce(new Response(null)) const response = fetchAndRetry(fetch, 'https://example.com', {}) - await vi.advanceTimersByTimeAsync(0) + await vi.advanceTimersByTimeAsync(4999) expect(fetch).toHaveBeenCalledTimes(1) await vi.advanceTimersByTimeAsync(1) diff --git a/packages/blobs/src/retry.ts b/packages/blobs/src/retry.ts index 4da9e70e2..336a6b530 100644 --- a/packages/blobs/src/retry.ts +++ b/packages/blobs/src/retry.ts @@ -1,6 +1,6 @@ import type { Fetcher } from './types.ts' -const DEFAULT_RETRY_DELAY = typeof process === 'object' && process.env.NODE_ENV === 'test' ? 1 : 5000 +const DEFAULT_RETRY_DELAY = 5000 const MIN_RETRY_DELAY = 1000 const MAX_RETRY = 5 const RATE_LIMIT_HEADER = 'X-RateLimit-Reset' From 793447e4754d4f56a3b5c1dda100ab1d363f1c60 Mon Sep 17 00:00:00 2001 From: onmax Date: Wed, 2 Sep 2026 06:06:29 +0000 Subject: [PATCH 4/4] test(blobs): keep retry regression focused --- packages/blobs/src/retry.test.ts | 30 ++---------------------------- 1 file changed, 2 insertions(+), 28 deletions(-) diff --git a/packages/blobs/src/retry.test.ts b/packages/blobs/src/retry.test.ts index 80c716528..e537082ae 100644 --- a/packages/blobs/src/retry.test.ts +++ b/packages/blobs/src/retry.test.ts @@ -1,39 +1,13 @@ import { afterEach, expect, test, vi } from 'vitest' -afterEach(() => { - vi.useRealTimers() - vi.unstubAllGlobals() - vi.resetModules() -}) +afterEach(() => vi.unstubAllGlobals()) test('does not access the process environment during import', async () => { - const accessEnvironment = vi.fn(() => { - throw new Error('Environment access is not allowed') - }) vi.stubGlobal('process', { get env() { - return accessEnvironment() + throw new Error('Environment access is not allowed') }, }) await expect(import('./retry.ts')).resolves.toHaveProperty('fetchAndRetry') - expect(accessEnvironment).not.toHaveBeenCalled() -}) - -test('uses the production retry delay in test environments', async () => { - vi.useFakeTimers() - - const { fetchAndRetry } = await import('./retry.ts') - const fetch = vi - .fn() - .mockResolvedValueOnce(new Response(null, { status: 500 })) - .mockResolvedValueOnce(new Response(null)) - const response = fetchAndRetry(fetch, 'https://example.com', {}) - - await vi.advanceTimersByTimeAsync(4999) - expect(fetch).toHaveBeenCalledTimes(1) - - await vi.advanceTimersByTimeAsync(1) - await expect(response).resolves.toHaveProperty('status', 200) - expect(fetch).toHaveBeenCalledTimes(2) })