From aff59d331ed2edd4780d1552b085b1690703d056 Mon Sep 17 00:00:00 2001 From: Jared M Date: Fri, 23 Jan 2026 11:23:17 -0800 Subject: [PATCH 1/8] fix: prevent edge function build race condition and fix init test prompts --- src/lib/edge-functions/registry.ts | 38 +++++++++++++++++++- tests/integration/commands/init/init.test.ts | 4 +-- tests/integration/utils/dev-server.ts | 26 ++++++++------ 3 files changed, 55 insertions(+), 13 deletions(-) diff --git a/src/lib/edge-functions/registry.ts b/src/lib/edge-functions/registry.ts index 5482c69f57b..224e0b9e761 100644 --- a/src/lib/edge-functions/registry.ts +++ b/src/lib/edge-functions/registry.ts @@ -100,6 +100,8 @@ export class EdgeFunctionsRegistry { private aiGatewayContext?: AIGatewayContext | null private buildError: Error | null = null + private buildPending = false + private buildPromise: Promise<{ warnings: Record }> | null = null private bundler: typeof import('@netlify/edge-bundler') private configPath: string private importMapFromTOML?: string @@ -181,7 +183,41 @@ export class EdgeFunctionsRegistry { return [...this.internalFunctions, ...this.userFunctions] } - private async build() { + private async build(): Promise<{ warnings: Record }> { + // If a build is already in progress, mark that we need another build + // and return the current build's promise. The running build will + // trigger a rebuild when it completes if buildPending is true. + if (this.buildPromise) { + this.buildPending = true + return this.buildPromise + } + + this.buildPending = false + this.buildPromise = this.doBuild() + + try { + const result = await this.buildPromise + this.buildPromise = null + + // If another build was requested while we were building, run it now + if (this.buildPending) { + return await this.build() + } + + return result + } catch (error) { + this.buildPromise = null + + // If another build was requested while we were building, run it now + if (this.buildPending) { + return await this.build() + } + + throw error + } + } + + private async doBuild(): Promise<{ warnings: Record }> { const warnings: Record = {} try { diff --git a/tests/integration/commands/init/init.test.ts b/tests/integration/commands/init/init.test.ts index e2e4371f4ca..7a100e619a9 100644 --- a/tests/integration/commands/init/init.test.ts +++ b/tests/integration/commands/init/init.test.ts @@ -218,7 +218,7 @@ describe.concurrent('commands/init', () => { const initQuestions = [ { question: 'Yes, create and deploy project manually', - answer: answerWithValue(CONFIRM), + answer: CONFIRM, // List selection only needs one CONFIRM, not answerWithValue }, { question: 'Team: (Use arrow keys)', answer: CONFIRM }, { @@ -227,7 +227,7 @@ describe.concurrent('commands/init', () => { }, { question: `Do you want to configure build settings? We'll suggest settings for your project automatically`, - answer: answerWithValue(CONFIRM), + answer: CONFIRM, // Confirm prompt only needs one CONFIRM }, { question: 'Your build command (hugo build/yarn run build/etc)', diff --git a/tests/integration/utils/dev-server.ts b/tests/integration/utils/dev-server.ts index d77ce42ebaa..805d3a09af0 100644 --- a/tests/integration/utils/dev-server.ts +++ b/tests/integration/utils/dev-server.ts @@ -28,7 +28,7 @@ export interface DevServer { port: number errorBuffer: Buffer[] outputBuffer: Buffer[] - waitForLogMatching(match: string): Promise + waitForLogMatching(match: string, timeoutMs?: number): Promise output: string error: string close(): Promise @@ -137,16 +137,22 @@ const startServer = async ({ port, errorBuffer, outputBuffer, - waitForLogMatching(match: string) { - return new Promise((resolveWait) => { - const listener = (stdoutData: string) => { - if (stdoutData.includes(match)) { - ps.removeListener('data', listener) - resolveWait() + waitForLogMatching(match: string, timeoutMs = 30_000) { + return pTimeout( + new Promise((resolveWait) => { + const listener = (stdoutData: string) => { + if (stdoutData.includes(match)) { + ps.stdout!.removeListener('data', listener) + resolveWait() + } } - } - ps.stdout!.on('data', listener) - }) + ps.stdout!.on('data', listener) + }), + { + milliseconds: timeoutMs, + message: `Timed out waiting for log matching "${match}".\nOutput so far:\n${outputBuffer.join('')}`, + }, + ) }, get output() { // these are getters so we do the actual joining as late as possible as the array might still get From 9d34d227669fb9adabc3de251239719294397923 Mon Sep 17 00:00:00 2001 From: Jared M Date: Fri, 23 Jan 2026 16:37:16 -0800 Subject: [PATCH 2/8] fix: add tests for edge build coalescing behavior --- src/lib/edge-functions/registry.ts | 3 + .../unit/lib/edge-functions/registry.test.ts | 58 +++++++++++++++++++ 2 files changed, 61 insertions(+) create mode 100644 tests/unit/lib/edge-functions/registry.test.ts diff --git a/src/lib/edge-functions/registry.ts b/src/lib/edge-functions/registry.ts index 224e0b9e761..3490496c383 100644 --- a/src/lib/edge-functions/registry.ts +++ b/src/lib/edge-functions/registry.ts @@ -183,6 +183,9 @@ export class EdgeFunctionsRegistry { return [...this.internalFunctions, ...this.userFunctions] } + // Note: We intentionally don't use @netlify/dev-utils memoize() here because + // it has a 300ms debounce and fire-and-forget logic. Edge function build + // needs callers to receive the latest build result private async build(): Promise<{ warnings: Record }> { // If a build is already in progress, mark that we need another build // and return the current build's promise. The running build will diff --git a/tests/unit/lib/edge-functions/registry.test.ts b/tests/unit/lib/edge-functions/registry.test.ts new file mode 100644 index 00000000000..f5bd77a0717 --- /dev/null +++ b/tests/unit/lib/edge-functions/registry.test.ts @@ -0,0 +1,58 @@ +import { describe, expect, test, vi } from 'vitest' + +import { EdgeFunctionsRegistry } from '../../../../src/lib/edge-functions/registry.js' + +/** + * Tests for EdgeFunctionsRegistry.build() coalescing behavior. + */ +describe('EdgeFunctionsRegistry.build() coalescing', () => { + const createMockRegistry = () => { + const state = { buildCount: 0, shouldFail: false } + + // Create instance with minimal mocked dependencies + const registry = Object.create(EdgeFunctionsRegistry.prototype) as InstanceType + + // Initialize only the properties needed for build() + // @ts-expect-error -- accessing private members for testing + registry.buildPending = false + // @ts-expect-error -- accessing private members for testing + registry.buildPromise = null + // @ts-expect-error -- accessing private members for testing + registry.doBuild = vi.fn(async () => { + state.buildCount++ + await new Promise((resolve) => setTimeout(resolve, 10)) + if (state.shouldFail) { + state.shouldFail = false + throw new Error('Build failed') + } + return { warnings: {} } + }) + + return { registry, state } + } + + test('concurrent calls coalesce into fewer builds', async () => { + const { registry, state } = createMockRegistry() + + // @ts-expect-error -- accessing private method for testing + const results = await Promise.all([registry.build(), registry.build(), registry.build()]) + + expect(results).toHaveLength(3) + for (const r of results) { + expect(r).toEqual({ warnings: {} }) + } + expect(state.buildCount).toBe(2) // First build + one more rebuild for pending + }) + + test('retries pending build on failure', async () => { + const { registry, state } = createMockRegistry() + state.shouldFail = true + + // @ts-expect-error -- accessing private method for testing + const [result1, result2] = await Promise.allSettled([registry.build(), registry.build()]) + + expect(result1.status).toBe('fulfilled') // First call gets retry result + expect(result2.status).toBe('rejected') // Concurrent call gets the original failure + expect(state.buildCount).toBe(2) + }) +}) From f317f2349edfa6a9584c904c95f7acf1ec6b5ad1 Mon Sep 17 00:00:00 2001 From: Jared M Date: Fri, 23 Jan 2026 17:17:30 -0800 Subject: [PATCH 3/8] test: registry should builds new instance after concurrent calls complete --- tests/unit/lib/edge-functions/registry.test.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/unit/lib/edge-functions/registry.test.ts b/tests/unit/lib/edge-functions/registry.test.ts index f5bd77a0717..3600afe5ae9 100644 --- a/tests/unit/lib/edge-functions/registry.test.ts +++ b/tests/unit/lib/edge-functions/registry.test.ts @@ -41,7 +41,13 @@ describe('EdgeFunctionsRegistry.build() coalescing', () => { for (const r of results) { expect(r).toEqual({ warnings: {} }) } - expect(state.buildCount).toBe(2) // First build + one more rebuild for pending + // First build + one rebuild for pending = 2 total + expect(state.buildCount).toBe(2) + + // Subsequent call after all concurrent calls complete triggers a NEW build + // @ts-expect-error -- accessing private method for testing + await registry.build() + expect(state.buildCount).toBe(3) }) test('retries pending build on failure', async () => { From 3b37f77b40c85579735aa9175f70de6ca4f608d0 Mon Sep 17 00:00:00 2001 From: Jared M Date: Mon, 26 Jan 2026 14:27:41 -0800 Subject: [PATCH 4/8] replace @ts-expect-error with typed interface --- .../unit/lib/edge-functions/registry.test.ts | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/tests/unit/lib/edge-functions/registry.test.ts b/tests/unit/lib/edge-functions/registry.test.ts index 3600afe5ae9..23998bfe282 100644 --- a/tests/unit/lib/edge-functions/registry.test.ts +++ b/tests/unit/lib/edge-functions/registry.test.ts @@ -4,20 +4,31 @@ import { EdgeFunctionsRegistry } from '../../../../src/lib/edge-functions/regist /** * Tests for EdgeFunctionsRegistry.build() coalescing behavior. + * + * We use a TestableRegistry interface + cast through `unknown` to access + * private members needed for testing the build coalescing logic. The return + * type of createMockRegistry is explicit to contain the broader type within + * the test setup. */ + +/** Type exposing the private members we need for testing build coalescing */ +interface TestableRegistry { + buildPending: boolean + buildPromise: Promise<{ warnings: Record }> | null + doBuild: () => Promise<{ warnings: Record }> + build: () => Promise<{ warnings: Record }> +} + describe('EdgeFunctionsRegistry.build() coalescing', () => { - const createMockRegistry = () => { + const createMockRegistry = (): { registry: TestableRegistry; state: { buildCount: number; shouldFail: boolean } } => { const state = { buildCount: 0, shouldFail: false } // Create instance with minimal mocked dependencies - const registry = Object.create(EdgeFunctionsRegistry.prototype) as InstanceType + const registry = Object.create(EdgeFunctionsRegistry.prototype) as unknown as TestableRegistry // Initialize only the properties needed for build() - // @ts-expect-error -- accessing private members for testing registry.buildPending = false - // @ts-expect-error -- accessing private members for testing registry.buildPromise = null - // @ts-expect-error -- accessing private members for testing registry.doBuild = vi.fn(async () => { state.buildCount++ await new Promise((resolve) => setTimeout(resolve, 10)) @@ -34,7 +45,6 @@ describe('EdgeFunctionsRegistry.build() coalescing', () => { test('concurrent calls coalesce into fewer builds', async () => { const { registry, state } = createMockRegistry() - // @ts-expect-error -- accessing private method for testing const results = await Promise.all([registry.build(), registry.build(), registry.build()]) expect(results).toHaveLength(3) @@ -45,7 +55,6 @@ describe('EdgeFunctionsRegistry.build() coalescing', () => { expect(state.buildCount).toBe(2) // Subsequent call after all concurrent calls complete triggers a NEW build - // @ts-expect-error -- accessing private method for testing await registry.build() expect(state.buildCount).toBe(3) }) @@ -54,7 +63,6 @@ describe('EdgeFunctionsRegistry.build() coalescing', () => { const { registry, state } = createMockRegistry() state.shouldFail = true - // @ts-expect-error -- accessing private method for testing const [result1, result2] = await Promise.allSettled([registry.build(), registry.build()]) expect(result1.status).toBe('fulfilled') // First call gets retry result From 678d3e839e7ce9aee925ead6f70935ab1dc19286 Mon Sep 17 00:00:00 2001 From: Jared M Date: Mon, 26 Jan 2026 14:33:20 -0800 Subject: [PATCH 5/8] make build coalescing members protected for testing --- src/lib/edge-functions/registry.ts | 8 ++++---- tests/unit/lib/edge-functions/registry.test.ts | 17 ++++++++--------- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/src/lib/edge-functions/registry.ts b/src/lib/edge-functions/registry.ts index 3490496c383..32896c3af28 100644 --- a/src/lib/edge-functions/registry.ts +++ b/src/lib/edge-functions/registry.ts @@ -100,8 +100,8 @@ export class EdgeFunctionsRegistry { private aiGatewayContext?: AIGatewayContext | null private buildError: Error | null = null - private buildPending = false - private buildPromise: Promise<{ warnings: Record }> | null = null + protected buildPending = false + protected buildPromise: Promise<{ warnings: Record }> | null = null private bundler: typeof import('@netlify/edge-bundler') private configPath: string private importMapFromTOML?: string @@ -186,7 +186,7 @@ export class EdgeFunctionsRegistry { // Note: We intentionally don't use @netlify/dev-utils memoize() here because // it has a 300ms debounce and fire-and-forget logic. Edge function build // needs callers to receive the latest build result - private async build(): Promise<{ warnings: Record }> { + protected async build(): Promise<{ warnings: Record }> { // If a build is already in progress, mark that we need another build // and return the current build's promise. The running build will // trigger a rebuild when it completes if buildPending is true. @@ -220,7 +220,7 @@ export class EdgeFunctionsRegistry { } } - private async doBuild(): Promise<{ warnings: Record }> { + protected async doBuild(): Promise<{ warnings: Record }> { const warnings: Record = {} try { diff --git a/tests/unit/lib/edge-functions/registry.test.ts b/tests/unit/lib/edge-functions/registry.test.ts index 23998bfe282..520167d494c 100644 --- a/tests/unit/lib/edge-functions/registry.test.ts +++ b/tests/unit/lib/edge-functions/registry.test.ts @@ -5,13 +5,12 @@ import { EdgeFunctionsRegistry } from '../../../../src/lib/edge-functions/regist /** * Tests for EdgeFunctionsRegistry.build() coalescing behavior. * - * We use a TestableRegistry interface + cast through `unknown` to access - * private members needed for testing the build coalescing logic. The return - * type of createMockRegistry is explicit to contain the broader type within - * the test setup. + * The build(), buildPending, buildPromise, and doBuild members are protected + * to allow testing. We create a minimal instance using Object.create to test + * the coalescing logic in isolation without the full constructor dependencies. */ -/** Type exposing the private members we need for testing build coalescing */ +/** Test harness exposing protected members for build coalescing tests */ interface TestableRegistry { buildPending: boolean buildPromise: Promise<{ warnings: Record }> | null @@ -20,13 +19,13 @@ interface TestableRegistry { } describe('EdgeFunctionsRegistry.build() coalescing', () => { - const createMockRegistry = (): { registry: TestableRegistry; state: { buildCount: number; shouldFail: boolean } } => { + const createMockRegistry = () => { const state = { buildCount: 0, shouldFail: false } - // Create instance with minimal mocked dependencies - const registry = Object.create(EdgeFunctionsRegistry.prototype) as unknown as TestableRegistry + // Create instance with prototype chain for build() method, typed to expose protected members + const registry = Object.create(EdgeFunctionsRegistry.prototype) as TestableRegistry - // Initialize only the properties needed for build() + // Initialize protected properties needed for build() registry.buildPending = false registry.buildPromise = null registry.doBuild = vi.fn(async () => { From edef242475126f0c9a60a5a3c2d10d358bdbb77f Mon Sep 17 00:00:00 2001 From: Jared M Date: Mon, 26 Jan 2026 14:50:31 -0800 Subject: [PATCH 6/8] Add comment explaining why member is protected and not private --- src/lib/edge-functions/registry.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/lib/edge-functions/registry.ts b/src/lib/edge-functions/registry.ts index 32896c3af28..7c76bcf9b77 100644 --- a/src/lib/edge-functions/registry.ts +++ b/src/lib/edge-functions/registry.ts @@ -100,6 +100,8 @@ export class EdgeFunctionsRegistry { private aiGatewayContext?: AIGatewayContext | null private buildError: Error | null = null + + // protected for testing only, not a stable extension point protected buildPending = false protected buildPromise: Promise<{ warnings: Record }> | null = null private bundler: typeof import('@netlify/edge-bundler') @@ -186,6 +188,8 @@ export class EdgeFunctionsRegistry { // Note: We intentionally don't use @netlify/dev-utils memoize() here because // it has a 300ms debounce and fire-and-forget logic. Edge function build // needs callers to receive the latest build result + // + // @internal - protected for testing only, not a stable extension point protected async build(): Promise<{ warnings: Record }> { // If a build is already in progress, mark that we need another build // and return the current build's promise. The running build will @@ -220,6 +224,7 @@ export class EdgeFunctionsRegistry { } } + // @internal - protected for testing only, not a stable extension point protected async doBuild(): Promise<{ warnings: Record }> { const warnings: Record = {} From bdd0f14b7c3cd926f41eb6216f0daf522b849037 Mon Sep 17 00:00:00 2001 From: Jared M Date: Mon, 26 Jan 2026 14:52:51 -0800 Subject: [PATCH 7/8] add @internal semantic to protected comment --- src/lib/edge-functions/registry.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/edge-functions/registry.ts b/src/lib/edge-functions/registry.ts index 7c76bcf9b77..999791cc848 100644 --- a/src/lib/edge-functions/registry.ts +++ b/src/lib/edge-functions/registry.ts @@ -101,7 +101,7 @@ export class EdgeFunctionsRegistry { private aiGatewayContext?: AIGatewayContext | null private buildError: Error | null = null - // protected for testing only, not a stable extension point + // @internal - protected for testing only, not a stable extension point protected buildPending = false protected buildPromise: Promise<{ warnings: Record }> | null = null private bundler: typeof import('@netlify/edge-bundler') From 8ba51eb277aff0e7c4d29e29bbed3a228b02aa2c Mon Sep 17 00:00:00 2001 From: Jared M Date: Mon, 26 Jan 2026 15:22:32 -0800 Subject: [PATCH 8/8] separate interface from implementation for testability --- src/lib/edge-functions/proxy.ts | 4 +- src/lib/edge-functions/registry.ts | 45 ++++++++++++------- .../unit/lib/edge-functions/registry.test.ts | 26 ++++------- 3 files changed, 41 insertions(+), 34 deletions(-) diff --git a/src/lib/edge-functions/proxy.ts b/src/lib/edge-functions/proxy.ts index 4880e81d598..4b66e2d68b4 100644 --- a/src/lib/edge-functions/proxy.ts +++ b/src/lib/edge-functions/proxy.ts @@ -25,7 +25,7 @@ import type { LocalState, ServerSettings } from '../../utils/types.js' import { getBootstrapURL } from './bootstrap.js' import { DIST_IMPORT_MAP_PATH, EDGE_FUNCTIONS_SERVE_FOLDER } from './consts.js' import { getFeatureFlagsHeader, getInvocationMetadataHeader, headers } from './headers.js' -import { EdgeFunctionsRegistry } from './registry.js' +import { EdgeFunctionsRegistryImpl } from './registry.js' export type EdgeFunctionDeclaration = bundler.Declaration @@ -250,7 +250,7 @@ const prepareServer = async ({ rootPath: repositoryRoot, servePath, }) - const registry = new EdgeFunctionsRegistry({ + const registry = new EdgeFunctionsRegistryImpl({ aiGatewayContext, bundler, command, diff --git a/src/lib/edge-functions/registry.ts b/src/lib/edge-functions/registry.ts index 999791cc848..f1fcf5c0c48 100644 --- a/src/lib/edge-functions/registry.ts +++ b/src/lib/edge-functions/registry.ts @@ -95,15 +95,26 @@ function traverseLocalDependencies( }) } -export class EdgeFunctionsRegistry { +/** Public contract for EdgeFunctionsRegistry - consumers should use this type */ +export interface EdgeFunctionsRegistry { + initialize(): Promise + matchURLPath( + urlPath: string, + method: string, + headers: Record, + ): { functionNames: string[]; invocationMetadata: unknown } +} + +export class EdgeFunctionsRegistryImpl implements EdgeFunctionsRegistry { public importMapFromDeployConfig?: string private aiGatewayContext?: AIGatewayContext | null private buildError: Error | null = null - // @internal - protected for testing only, not a stable extension point - protected buildPending = false - protected buildPromise: Promise<{ warnings: Record }> | null = null + /** @internal Exposed for testing - not part of the public EdgeFunctionsRegistry interface */ + public buildPending = false + /** @internal Exposed for testing - not part of the public EdgeFunctionsRegistry interface */ + public buildPromise: Promise<{ warnings: Record }> | null = null private bundler: typeof import('@netlify/edge-bundler') private configPath: string private importMapFromTOML?: string @@ -159,8 +170,8 @@ export class EdgeFunctionsRegistry { this.projectDir = projectDir this.importMapFromTOML = importMapFromTOML - this.declarationsFromTOML = EdgeFunctionsRegistry.getDeclarationsFromTOML(config) - this.env = EdgeFunctionsRegistry.getEnvironmentVariables(env) + this.declarationsFromTOML = EdgeFunctionsRegistryImpl.getDeclarationsFromTOML(config) + this.env = EdgeFunctionsRegistryImpl.getEnvironmentVariables(env) this.initialScan = this.doInitialScan() @@ -185,12 +196,16 @@ export class EdgeFunctionsRegistry { return [...this.internalFunctions, ...this.userFunctions] } - // Note: We intentionally don't use @netlify/dev-utils memoize() here because - // it has a 300ms debounce and fire-and-forget logic. Edge function build - // needs callers to receive the latest build result - // - // @internal - protected for testing only, not a stable extension point - protected async build(): Promise<{ warnings: Record }> { + /** + * Triggers a build of edge functions with coalescing behavior. + * + * Note: We intentionally don't use @netlify/dev-utils memoize() here because + * it has a 300ms debounce and fire-and-forget logic. Edge function build + * needs callers to receive the latest build result. + * + * @internal Exposed for testing - not part of the public EdgeFunctionsRegistry interface + */ + public async build(): Promise<{ warnings: Record }> { // If a build is already in progress, mark that we need another build // and return the current build's promise. The running build will // trigger a rebuild when it completes if buildPending is true. @@ -224,8 +239,8 @@ export class EdgeFunctionsRegistry { } } - // @internal - protected for testing only, not a stable extension point - protected async doBuild(): Promise<{ warnings: Record }> { + /** @internal Exposed for testing - not part of the public EdgeFunctionsRegistry interface */ + public async doBuild(): Promise<{ warnings: Record }> { const warnings: Record = {} try { @@ -687,7 +702,7 @@ export class EdgeFunctionsRegistry { onChange: async () => { const newConfig = await this.getUpdatedConfig() - this.declarationsFromTOML = EdgeFunctionsRegistry.getDeclarationsFromTOML(newConfig) + this.declarationsFromTOML = EdgeFunctionsRegistryImpl.getDeclarationsFromTOML(newConfig) await this.checkForAddedOrDeletedFunctions() }, diff --git a/tests/unit/lib/edge-functions/registry.test.ts b/tests/unit/lib/edge-functions/registry.test.ts index 520167d494c..104d1a34fa1 100644 --- a/tests/unit/lib/edge-functions/registry.test.ts +++ b/tests/unit/lib/edge-functions/registry.test.ts @@ -1,31 +1,23 @@ import { describe, expect, test, vi } from 'vitest' -import { EdgeFunctionsRegistry } from '../../../../src/lib/edge-functions/registry.js' +import { EdgeFunctionsRegistryImpl } from '../../../../src/lib/edge-functions/registry.js' /** - * Tests for EdgeFunctionsRegistry.build() coalescing behavior. + * Tests for EdgeFunctionsRegistryImpl.build() coalescing behavior. * - * The build(), buildPending, buildPromise, and doBuild members are protected - * to allow testing. We create a minimal instance using Object.create to test - * the coalescing logic in isolation without the full constructor dependencies. + * The build(), buildPending, buildPromise, and doBuild members are public on + * the implementation class (but not on the EdgeFunctionsRegistry interface), + * allowing direct unit testing of the coalescing logic. */ -/** Test harness exposing protected members for build coalescing tests */ -interface TestableRegistry { - buildPending: boolean - buildPromise: Promise<{ warnings: Record }> | null - doBuild: () => Promise<{ warnings: Record }> - build: () => Promise<{ warnings: Record }> -} - -describe('EdgeFunctionsRegistry.build() coalescing', () => { +describe('EdgeFunctionsRegistryImpl.build() coalescing', () => { const createMockRegistry = () => { const state = { buildCount: 0, shouldFail: false } - // Create instance with prototype chain for build() method, typed to expose protected members - const registry = Object.create(EdgeFunctionsRegistry.prototype) as TestableRegistry + // Create instance with prototype chain for build() method + const registry = Object.create(EdgeFunctionsRegistryImpl.prototype) as EdgeFunctionsRegistryImpl - // Initialize protected properties needed for build() + // Initialize properties needed for build() registry.buildPending = false registry.buildPromise = null registry.doBuild = vi.fn(async () => {