import { expect, layer } from "@effect/vitest";import { Effect, Layer, Schema } from "effect";import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi";import { HttpClient, HttpRouter, HttpServerRequest } from "effect/unstable/http";import * as NodeHttpServer from "@effect/platform-node/NodeHttpServer"; import { createExecutor, type DBAdapter, definePlugin, makeTestConfig, Scope, ScopeId, SecretId, SetSecretInput, type InvokeOptions, type SecretProvider, type Where,} from "@executor-js/sdk"; const TEST_SCOPE = "test-scope";import { openApiPlugin } from "./plugin";import { OAuth2Auth } from "./types"; const autoApprove: InvokeOptions = { onElicitation: "accept-all" }; type FindManyCall = { readonly model: string; readonly where?: readonly Where[];}; const recordFindMany = ( adapter: DBAdapter, calls: FindManyCall[],): DBAdapter => ({ ...adapter, findMany: (data) => { calls.push({ model: data.model, where: data.where }); return adapter.findMany(data); }, transaction: (callback) => adapter.transaction((trx) => callback({ ...trx, findMany: (data) => { calls.push({ model: data.model, where: data.where }); return trx.findMany(data); }, }), ),}); // ---------------------------------------------------------------------------// In-memory secrets provider plugin — registered alongside openapi so// executor.secrets.set/get work in tests.// --------------------------------------------------------------------------- const memoryProvider: SecretProvider = (() => { const store = new Map<string, string>(); return { key: "memory", writable: true, get: (id, scope) => Effect.sync(() => store.get(`${scope}\u0000${id}`) ?? null), set: (id, value, scope) => Effect.sync(() => { store.set(`${scope}\u0000${id}`, value); }), delete: (id, scope) => Effect.sync(() => store.delete(`${scope}\u0000${id}`)), list: () => Effect.sync(() => Array.from(store.keys()).map((k) => { const name = k.split("\u0000", 2)[1] ?? k; return { id: name, name }; }), ), };})(); const memorySecretsPlugin = definePlugin(() => ({ id: "memory-secrets" as const, storage: () => ({}), secretProviders: [memoryProvider],})); // ---------------------------------------------------------------------------// Define a test API with Effect HttpApi// --------------------------------------------------------------------------- class Item extends Schema.Class<Item>("Item")({ id: Schema.Number, name: Schema.String,}) {} class EchoHeaders extends Schema.Class<EchoHeaders>("EchoHeaders")({ authorization: Schema.optional(Schema.String), "x-static": Schema.optional(Schema.String),}) {} class QueryValidationError extends Schema.TaggedErrorClass<QueryValidationError>()( "QueryValidationError", { message: Schema.String, },) {} const ItemsGroup = HttpApiGroup.make("items") .add(HttpApiEndpoint.get("listItems", "/items", { success: Schema.Array(Item) })) .add( HttpApiEndpoint.get("getItem", "/items/:itemId", { params: Schema.Struct({ itemId: Schema.NumberFromString }), success: Item, }), ) .add(HttpApiEndpoint.get("echoHeaders", "/echo-headers", { success: EchoHeaders })) .add( HttpApiEndpoint.get("queryRows", "/records/rows/:entryTypeId", { params: Schema.Struct({ entryTypeId: Schema.String }), success: Schema.Unknown, error: QueryValidationError, }), ); const TestApi = HttpApi.make("testApi").add(ItemsGroup); const spec = OpenApi.fromApi(TestApi);const specJson = JSON.stringify(spec); // ---------------------------------------------------------------------------// Implement handlers// --------------------------------------------------------------------------- const ITEMS = [ { id: 1, name: "Widget" }, { id: 2, name: "Gadget" }, { id: 3, name: "Doohickey" },]; const ItemsGroupLive = HttpApiBuilder.group(TestApi, "items", (handlers) => handlers .handle("listItems", () => Effect.succeed(ITEMS.map((item) => new Item(item)))) .handle("getItem", (req) => Effect.succeed( new Item( ITEMS.find((i) => i.id === req.params.itemId) ?? { id: 0, name: "Unknown", }, ), ), ) .handle("echoHeaders", () => Effect.gen(function* () { const req = yield* HttpServerRequest.HttpServerRequest; return new EchoHeaders({ authorization: req.headers["authorization"], "x-static": req.headers["x-static"], }); }), ) .handle("queryRows", () => Effect.fail( new QueryValidationError({ message: 'Field with name "DisplayName" does not exist', }), ), ),); // ---------------------------------------------------------------------------// Test layer: real server on port 0 + HttpClient pointing at it// --------------------------------------------------------------------------- const ApiLive = HttpApiBuilder.layer(TestApi).pipe(Layer.provide(ItemsGroupLive)); const TestLayer = HttpRouter.serve(ApiLive, { disableListenLog: true, disableLogger: true }).pipe( Layer.provideMerge(NodeHttpServer.layerTest),); // ---------------------------------------------------------------------------// Tests// --------------------------------------------------------------------------- layer(TestLayer)("OpenAPI Plugin", (it) => { it.effect("previewSpec returns metadata and header presets", () => 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, }), ); const preview = yield* executor.openapi.previewSpec(specJson); expect(preview.operationCount).toBeGreaterThanOrEqual(2); expect(preview.servers).toBeDefined(); }), ); it.effect("registers static openapi control tools", () => 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, }), ); const tools = yield* executor.tools.list(); const ids = tools.map((t) => t.id); expect(ids).toContain("openapi.previewSpec"); expect(ids).toContain("openapi.addSource"); }), ); it.effect("lists openapi as a static runtime source", () => 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, }), ); const sources = yield* executor.sources.list(); const control = sources.find((s) => s.id === "openapi"); expect(control).toBeDefined(); expect(control!.runtime).toBe(true); expect(control!.canRemove).toBe(false); }), ); it.effect("invokes static previewSpec through executor.tools.invoke", () => 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, }), ); const result = (yield* executor.tools.invoke( "openapi.previewSpec", { spec: specJson }, autoApprove, )) as { operationCount: number }; expect(result.operationCount).toBeGreaterThanOrEqual(2); }), ); it.effect("invokes static addSource through executor.tools.invoke", () => 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, }), ); const result = (yield* executor.tools.invoke( "openapi.addSource", { spec: specJson, namespace: "runtime" }, autoApprove, )) as { sourceId: string; toolCount: number }; expect(result).toEqual({ sourceId: "runtime", toolCount: 4 }); expect((yield* executor.tools.list()).map((t) => t.id)).toContain( "runtime.items.listItems", ); }), ); it.effect("resolves secret-backed headers at invocation time", () => 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.secrets.set( new SetSecretInput({ id: SecretId.make("test-api-token"), scope: ScopeId.make(TEST_SCOPE), name: "Test API Token", value: "secret-value-123", }), ); yield* executor.openapi.addSpec({ spec: specJson, scope: TEST_SCOPE, namespace: "authed", baseUrl: "", headers: { Authorization: { secretId: "test-api-token", prefix: "Bearer " }, "X-Static": "hello", }, }); const result = (yield* executor.tools.invoke( "authed.items.echoHeaders", {}, autoApprove, )) as { data: { authorization?: string; "x-static"?: string } | null; error: unknown }; expect(result.error).toBeNull(); const data = result.data!; expect(data.authorization).toBe("Bearer secret-value-123"); expect(data["x-static"]).toBe("hello"); }), ); it.effect("fails clearly when a secret is missing", () => 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: "noauth", baseUrl: "", headers: { Authorization: { secretId: "missing-token", prefix: "Bearer " }, }, }); const error = yield* Effect.flip( executor.tools.invoke("noauth.items.listItems", {}, autoApprove), ); expect((error as { _tag: string })._tag).toBe("ToolInvocationError"); expect((error as { message: string }).message).toContain("missing-token"); }), ); it.effect("registers tools from an OpenAPI spec", () => 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, }), ); const result = yield* executor.openapi.addSpec({ spec: specJson, scope: TEST_SCOPE, namespace: "test", baseUrl: "", }); expect(result.toolCount).toBeGreaterThanOrEqual(2); const tools = yield* executor.tools.list(); const names = tools.map((t) => t.name); expect(names).toContain("items.listItems"); expect(names).toContain("items.getItem"); }), ); it.effect("invokes listItems", () => 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: "test", baseUrl: "", }); const result = (yield* executor.tools.invoke( "test.items.listItems", {}, autoApprove, )) as { data: unknown; error: unknown }; expect(result.error).toBeNull(); expect(result.data).toEqual(ITEMS); }), ); it.effect("invokes getItem with path parameter", () => 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: "test", baseUrl: "", }); const result = (yield* executor.tools.invoke( "test.items.getItem", { itemId: "2" }, autoApprove, )) as { data: unknown; error: unknown }; expect(result.error).toBeNull(); expect(result.data).toEqual({ id: 2, name: "Gadget" }); }), ); it.effect("surfaces structured validation errors from OpenAPI tool calls", () => 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: "records", baseUrl: "", }); const result = (yield* executor.tools.invoke( "records.items.queryRows", { entryTypeId: "18538", query: JSON.stringify([{ DisplayName: "Example" }]), limit: 10, skip: 0, }, autoApprove, )) as { data: unknown; error: unknown }; Lines 1–500 of 817. Load more
import { expect, layer } from "@effect/vitest"; import { Effect, Layer, Schema } from "effect"; import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"; import { HttpClient, HttpRouter, HttpServerRequest } from "effect/unstable/http"; import * as NodeHttpServer from "@effect/platform-node/NodeHttpServer"; import { createExecutor, type DBAdapter, definePlugin, makeTestConfig, Scope, ScopeId, SecretId, SetSecretInput, type InvokeOptions, type SecretProvider, type Where, } from "@executor-js/sdk"; const TEST_SCOPE = "test-scope"; import { openApiPlugin } from "./plugin"; import { OAuth2Auth } from "./types"; const autoApprove: InvokeOptions = { onElicitation: "accept-all" }; type FindManyCall = { readonly model: string; readonly where?: readonly Where[]; }; const recordFindMany = ( adapter: DBAdapter, calls: FindManyCall[], ): DBAdapter => ({ ...adapter, findMany: (data) => { calls.push({ model: data.model, where: data.where }); return adapter.findMany(data); }, transaction: (callback) => adapter.transaction((trx) => callback({ ...trx, findMany: (data) => { calls.push({ model: data.model, where: data.where }); return trx.findMany(data); }, }), ), }); // --------------------------------------------------------------------------- // In-memory secrets provider plugin — registered alongside openapi so // executor.secrets.set/get work in tests. // --------------------------------------------------------------------------- const memoryProvider: SecretProvider = (() => { const store = new Map<string, string>(); return { key: "memory", writable: true, get: (id, scope) => Effect.sync(() => store.get(`${scope}\u0000${id}`) ?? null), set: (id, value, scope) => Effect.sync(() => { store.set(`${scope}\u0000${id}`, value); }), delete: (id, scope) => Effect.sync(() => store.delete(`${scope}\u0000${id}`)), list: () => Effect.sync(() => Array.from(store.keys()).map((k) => { const name = k.split("\u0000", 2)[1] ?? k; return { id: name, name }; }), ), }; })(); const memorySecretsPlugin = definePlugin(() => ({ id: "memory-secrets" as const, storage: () => ({}), secretProviders: [memoryProvider], })); // --------------------------------------------------------------------------- // Define a test API with Effect HttpApi // --------------------------------------------------------------------------- class Item extends Schema.Class<Item>("Item")({ id: Schema.Number, name: Schema.String, }) {} class EchoHeaders extends Schema.Class<EchoHeaders>("EchoHeaders")({ authorization: Schema.optional(Schema.String), "x-static": Schema.optional(Schema.String), }) {} class QueryValidationError extends Schema.TaggedErrorClass<QueryValidationError>()( "QueryValidationError", { message: Schema.String, }, ) {} const ItemsGroup = HttpApiGroup.make("items") .add(HttpApiEndpoint.get("listItems", "/items", { success: Schema.Array(Item) })) .add( HttpApiEndpoint.get("getItem", "/items/:itemId", { params: Schema.Struct({ itemId: Schema.NumberFromString }), success: Item, }), ) .add(HttpApiEndpoint.get("echoHeaders", "/echo-headers", { success: EchoHeaders })) .add( HttpApiEndpoint.get("queryRows", "/records/rows/:entryTypeId", { params: Schema.Struct({ entryTypeId: Schema.String }), success: Schema.Unknown, error: QueryValidationError, }), ); const TestApi = HttpApi.make("testApi").add(ItemsGroup); const spec = OpenApi.fromApi(TestApi); const specJson = JSON.stringify(spec); // --------------------------------------------------------------------------- // Implement handlers // --------------------------------------------------------------------------- const ITEMS = [ { id: 1, name: "Widget" }, { id: 2, name: "Gadget" }, { id: 3, name: "Doohickey" }, ]; const ItemsGroupLive = HttpApiBuilder.group(TestApi, "items", (handlers) => handlers .handle("listItems", () => Effect.succeed(ITEMS.map((item) => new Item(item)))) .handle("getItem", (req) => Effect.succeed( new Item( ITEMS.find((i) => i.id === req.params.itemId) ?? { id: 0, name: "Unknown", }, ), ), ) .handle("echoHeaders", () => Effect.gen(function* () { const req = yield* HttpServerRequest.HttpServerRequest; return new EchoHeaders({ authorization: req.headers["authorization"], "x-static": req.headers["x-static"], }); }), ) .handle("queryRows", () => Effect.fail( new QueryValidationError({ message: 'Field with name "DisplayName" does not exist', }), ), ), ); // --------------------------------------------------------------------------- // Test layer: real server on port 0 + HttpClient pointing at it // --------------------------------------------------------------------------- const ApiLive = HttpApiBuilder.layer(TestApi).pipe(Layer.provide(ItemsGroupLive)); const TestLayer = HttpRouter.serve(ApiLive, { disableListenLog: true, disableLogger: true }).pipe( Layer.provideMerge(NodeHttpServer.layerTest), ); // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- layer(TestLayer)("OpenAPI Plugin", (it) => { it.effect("previewSpec returns metadata and header presets", () => 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, }), ); const preview = yield* executor.openapi.previewSpec(specJson); expect(preview.operationCount).toBeGreaterThanOrEqual(2); expect(preview.servers).toBeDefined(); }), ); it.effect("registers static openapi control tools", () => 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, }), ); const tools = yield* executor.tools.list(); const ids = tools.map((t) => t.id); expect(ids).toContain("openapi.previewSpec"); expect(ids).toContain("openapi.addSource"); }), ); it.effect("lists openapi as a static runtime source", () => 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, }), ); const sources = yield* executor.sources.list(); const control = sources.find((s) => s.id === "openapi"); expect(control).toBeDefined(); expect(control!.runtime).toBe(true); expect(control!.canRemove).toBe(false); }), ); it.effect("invokes static previewSpec through executor.tools.invoke", () => 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, }), ); const result = (yield* executor.tools.invoke( "openapi.previewSpec", { spec: specJson }, autoApprove, )) as { operationCount: number }; expect(result.operationCount).toBeGreaterThanOrEqual(2); }), ); it.effect("invokes static addSource through executor.tools.invoke", () => 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, }), ); const result = (yield* executor.tools.invoke( "openapi.addSource", { spec: specJson, namespace: "runtime" }, autoApprove, )) as { sourceId: string; toolCount: number }; expect(result).toEqual({ sourceId: "runtime", toolCount: 4 }); expect((yield* executor.tools.list()).map((t) => t.id)).toContain( "runtime.items.listItems", ); }), ); it.effect("resolves secret-backed headers at invocation time", () => 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.secrets.set( new SetSecretInput({ id: SecretId.make("test-api-token"), scope: ScopeId.make(TEST_SCOPE), name: "Test API Token", value: "secret-value-123", }), ); yield* executor.openapi.addSpec({ spec: specJson, scope: TEST_SCOPE, namespace: "authed", baseUrl: "", headers: { Authorization: { secretId: "test-api-token", prefix: "Bearer " }, "X-Static": "hello", }, }); const result = (yield* executor.tools.invoke( "authed.items.echoHeaders", {}, autoApprove, )) as { data: { authorization?: string; "x-static"?: string } | null; error: unknown }; expect(result.error).toBeNull(); const data = result.data!; expect(data.authorization).toBe("Bearer secret-value-123"); expect(data["x-static"]).toBe("hello"); }), ); it.effect("fails clearly when a secret is missing", () => 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: "noauth", baseUrl: "", headers: { Authorization: { secretId: "missing-token", prefix: "Bearer " }, }, }); const error = yield* Effect.flip( executor.tools.invoke("noauth.items.listItems", {}, autoApprove), ); expect((error as { _tag: string })._tag).toBe("ToolInvocationError"); expect((error as { message: string }).message).toContain("missing-token"); }), ); it.effect("registers tools from an OpenAPI spec", () => 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, }), ); const result = yield* executor.openapi.addSpec({ spec: specJson, scope: TEST_SCOPE, namespace: "test", baseUrl: "", }); expect(result.toolCount).toBeGreaterThanOrEqual(2); const tools = yield* executor.tools.list(); const names = tools.map((t) => t.name); expect(names).toContain("items.listItems"); expect(names).toContain("items.getItem"); }), ); it.effect("invokes listItems", () => 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: "test", baseUrl: "", }); const result = (yield* executor.tools.invoke( "test.items.listItems", {}, autoApprove, )) as { data: unknown; error: unknown }; expect(result.error).toBeNull(); expect(result.data).toEqual(ITEMS); }), ); it.effect("invokes getItem with path parameter", () => 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: "test", baseUrl: "", }); const result = (yield* executor.tools.invoke( "test.items.getItem", { itemId: "2" }, autoApprove, )) as { data: unknown; error: unknown }; expect(result.error).toBeNull(); expect(result.data).toEqual({ id: 2, name: "Gadget" }); }), ); it.effect("surfaces structured validation errors from OpenAPI tool calls", () => 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: "records", baseUrl: "", }); const result = (yield* executor.tools.invoke( "records.items.queryRows", { entryTypeId: "18538", query: JSON.stringify([{ DisplayName: "Example" }]), limit: 10, skip: 0, }, autoApprove, )) as { data: unknown; error: unknown };