diff --git a/packages/plugins/openapi/src/sdk/plugin.test.ts b/packages/plugins/openapi/src/sdk/plugin.test.ts index 826bda7e..6b5e7ca2 100644 --- a/packages/plugins/openapi/src/sdk/plugin.test.ts +++ b/packages/plugins/openapi/src/sdk/plugin.test.ts @@ -540,6 +540,39 @@ layer(TestLayer)("OpenAPI Plugin", (it) => { }), ); + it.effect("listSourceBindings returns [] for a removed source", () => + // Regression: the React bindings atom revalidates after a removeSpec + // (sourceWriteKeys invalidate it) before unmount. The store used to + // throw StorageError("source does not exist"), which surfaced to the + // browser as a 500. A removed source has no bindings — return []. + Effect.gen(function* () { + const httpClient = yield* HttpClient.HttpClient; + const clientLayer = Layer.succeed(HttpClient.HttpClient, httpClient); + const executor = yield* createExecutor( + makeTestConfig({ + plugins: [ + openApiPlugin({ httpClientLayer: clientLayer }), + memorySecretsPlugin(), + ] as const, + }), + ); + + yield* executor.openapi.addSpec({ + spec: specJson, + scope: TEST_SCOPE, + namespace: "removable", + baseUrl: "", + }); + yield* executor.openapi.removeSpec("removable", TEST_SCOPE); + + const bindings = yield* executor.openapi.listSourceBindings( + "removable", + TEST_SCOPE, + ); + expect(bindings).toEqual([]); + }), + ); + // ------------------------------------------------------------------------- // Multi-scope shadowing — regression suite covering the bug class where // store reads/writes that don't pin scope_id collapse onto whichever row diff --git a/packages/plugins/openapi/src/sdk/store.ts b/packages/plugins/openapi/src/sdk/store.ts index 1dc00e49..4231eb4d 100644 --- a/packages/plugins/openapi/src/sdk/store.ts +++ b/packages/plugins/openapi/src/sdk/store.ts @@ -419,8 +419,7 @@ export const makeDefaultOpenapiStore = ({ : new Date(row.updated_at as string), }); - const validateBindingTarget = (params: { - readonly sourceId: string; + const validateBindingScopes = (params: { readonly sourceScope: string; readonly targetScope: string; }) => @@ -445,6 +444,18 @@ export const makeDefaultOpenapiStore = ({ }), ); } + }); + + const validateBindingTarget = (params: { + readonly sourceId: string; + readonly sourceScope: string; + readonly targetScope: string; + }) => + Effect.gen(function* () { + yield* validateBindingScopes({ + sourceScope: params.sourceScope, + targetScope: params.targetScope, + }); const source = yield* adapter.findOne({ model: "openapi_source", where: [ @@ -701,8 +712,7 @@ export const makeDefaultOpenapiStore = ({ listSourceBindings: (sourceId, sourceScope) => Effect.gen(function* () { - yield* validateBindingTarget({ - sourceId, + yield* validateBindingScopes({ sourceScope, targetScope: sourceScope, }); @@ -729,8 +739,7 @@ export const makeDefaultOpenapiStore = ({ resolveSourceBinding: (sourceId, sourceScope, slot) => Effect.gen(function* () { - yield* validateBindingTarget({ - sourceId, + yield* validateBindingScopes({ sourceScope, targetScope: sourceScope, });