import { expect, layer } from "@effect/vitest";import { Effect, Layer, Schema } from "effect";import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, HttpClient, HttpServerRequest, OpenApi,} from "@effect/platform";import { NodeHttpServer } from "@effect/platform-node"; import { createExecutor, makeInMemoryPolicyEngine, makeInMemorySecretStore, makeInMemorySourceRegistry, makeInMemoryToolRegistry, makeTestConfig, ScopeId, SecretId, type InvokeOptions,} from "@executor/sdk";import { openApiPlugin } from "./plugin"; const autoApprove: InvokeOptions = { onElicitation: "accept-all" }; // ---------------------------------------------------------------------------// 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),}) {} const ItemsGroup = HttpApiGroup.make("items") .add(HttpApiEndpoint.get("listItems", "/items").addSuccess(Schema.Array(Item))) .add( HttpApiEndpoint.get("getItem", "/items/:itemId") .setPath(Schema.Struct({ itemId: Schema.NumberFromString })) .addSuccess(Item), ) .add(HttpApiEndpoint.get("echoHeaders", "/echo-headers").addSuccess(EchoHeaders)); 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)) .handle("getItem", (req) => Effect.succeed( ITEMS.find((i) => i.id === req.path.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"], }); }), ),); // ---------------------------------------------------------------------------// Test layer: real server on port 0 + HttpClient pointing at it// --------------------------------------------------------------------------- const ApiLive = HttpApiBuilder.api(TestApi).pipe(Layer.provide(ItemsGroupLive)); const TestLayer = HttpApiBuilder.serve().pipe( Layer.provide(ApiLive), Layer.provideMerge(NodeHttpServer.layerTest),); // ---------------------------------------------------------------------------// Tests — layer() shares the server across all tests in this describe block// --------------------------------------------------------------------------- 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 })] as const, }), ); const preview = yield* executor.openapi.previewSpec(specJson); expect(preview.operationCount).toBeGreaterThanOrEqual(2); expect(preview.servers).toBeDefined(); }), ); it.effect("registers runtime openapi tools under built-in 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 })] 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 built-in as a 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 })] as const, }), ); const sources = yield* executor.sources.list(); expect(sources).toContainEqual( expect.objectContaining({ id: "built-in", name: "Built In", kind: "built-in", runtime: true, canRemove: false, canRefresh: false, }), ); }), ); it.effect("closing an executor does not remove added source tools from the shared registry", () => Effect.gen(function* () { const httpClient = yield* HttpClient.HttpClient; const clientLayer = Layer.succeed(HttpClient.HttpClient, httpClient); const scope = { id: ScopeId.make("test-scope"), name: "/test", createdAt: new Date(), } as const; const sharedConfig = { scope, tools: makeInMemoryToolRegistry(), sources: makeInMemorySourceRegistry(), secrets: makeInMemorySecretStore(), policies: makeInMemoryPolicyEngine(), }; const executor1 = yield* createExecutor({ ...sharedConfig, plugins: [openApiPlugin({ httpClientLayer: clientLayer })] as const, }); yield* executor1.openapi.addSpec({ spec: specJson, namespace: "persisted", }); expect((yield* executor1.tools.list()).map((tool) => tool.id)).toContain( "persisted.items.listItems", ); yield* executor1.close(); const executor2 = yield* createExecutor({ ...sharedConfig, plugins: [openApiPlugin({ httpClientLayer: clientLayer })] as const, }); expect((yield* executor2.tools.list()).map((tool) => tool.id)).toContain( "persisted.items.listItems", ); }), ); it.effect("invokes runtime 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 })] as const, }), ); const result = yield* executor.tools.invoke( "openapi.previewSpec", { spec: specJson }, autoApprove, ); expect(result.error).toBeNull(); expect((result.data as { operationCount: number }).operationCount).toBeGreaterThanOrEqual(2); }), ); it.effect("invokes runtime 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 })] as const, }), ); const result = yield* executor.tools.invoke( "openapi.addSource", { spec: specJson, namespace: "runtime" }, autoApprove, ); expect(result.error).toBeNull(); expect(result.data).toEqual({ sourceId: "runtime", toolCount: 3 }); 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 })] as const, }), ); // Store a secret yield* executor.secrets.set({ id: SecretId.make("test-api-token"), name: "Test API Token", value: "secret-value-123", }); // Add spec with secret-backed header yield* executor.openapi.addSpec({ spec: specJson, namespace: "authed", baseUrl: "", headers: { Authorization: { secretId: "test-api-token", prefix: "Bearer " }, "X-Static": "hello", }, }); // Invoke the echo endpoint — verifies secret was resolved and sent const result = yield* executor.tools.invoke("authed.items.echoHeaders", {}, autoApprove); expect(result.error).toBeNull(); const data = result.data as { authorization?: string; "x-static"?: string }; 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 })] as const, }), ); // Add spec with secret-backed header but DON'T store the secret yield* executor.openapi.addSpec({ spec: specJson, namespace: "noauth", baseUrl: "", headers: { Authorization: { secretId: "missing-token", prefix: "Bearer " }, }, }); // Invoke — should fail with a clear error about the missing secret const error = yield* Effect.flip( executor.tools.invoke("noauth.items.listItems", {}, autoApprove), ); expect(error._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 })] as const, }), ); const result = yield* executor.openapi.addSpec({ spec: specJson, 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 })] as const, }), ); yield* executor.openapi.addSpec({ spec: specJson, namespace: "test", baseUrl: "", }); const result = yield* executor.tools.invoke("test.items.listItems", {}, autoApprove); 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 })] as const, }), ); yield* executor.openapi.addSpec({ spec: specJson, namespace: "test", baseUrl: "", }); const result = yield* executor.tools.invoke( "test.items.getItem", { itemId: "2", }, autoApprove, ); expect(result.error).toBeNull(); expect(result.data).toEqual({ id: 2, name: "Gadget" }); }), ); it.effect("removeSpec cleans up registered 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 })] as const, }), ); yield* executor.openapi.addSpec({ spec: specJson, namespace: "removable", baseUrl: "", }); expect((yield* executor.tools.list()).length).toBeGreaterThan(0); yield* executor.openapi.removeSpec("removable"); const remaining = yield* executor.tools.list(); expect(remaining.map((tool) => tool.id)).toEqual([ "openapi.previewSpec", "openapi.addSource", ]); }), );});
import { expect, layer } from "@effect/vitest";
import { Effect, Layer, Schema } from "effect";
import {
HttpApi,
HttpApiBuilder,
HttpApiEndpoint,
HttpApiGroup,
HttpClient,
HttpServerRequest,
OpenApi,
} from "@effect/platform";
import { NodeHttpServer } from "@effect/platform-node";
import {
createExecutor,
makeInMemoryPolicyEngine,
makeInMemorySecretStore,
makeInMemorySourceRegistry,
makeInMemoryToolRegistry,
makeTestConfig,
ScopeId,
SecretId,
type InvokeOptions,
} from "@executor/sdk";
import { openApiPlugin } from "./plugin";
const autoApprove: InvokeOptions = { onElicitation: "accept-all" };
// ---------------------------------------------------------------------------
// 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),
}) {}
const ItemsGroup = HttpApiGroup.make("items")
.add(HttpApiEndpoint.get("listItems", "/items").addSuccess(Schema.Array(Item)))
.add(
HttpApiEndpoint.get("getItem", "/items/:itemId")
.setPath(Schema.Struct({ itemId: Schema.NumberFromString }))
.addSuccess(Item),
)
.add(HttpApiEndpoint.get("echoHeaders", "/echo-headers").addSuccess(EchoHeaders));
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))
.handle("getItem", (req) =>
Effect.succeed(
ITEMS.find((i) => i.id === req.path.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"],
});
}),
),
);
// ---------------------------------------------------------------------------
// Test layer: real server on port 0 + HttpClient pointing at it
// ---------------------------------------------------------------------------
const ApiLive = HttpApiBuilder.api(TestApi).pipe(Layer.provide(ItemsGroupLive));
const TestLayer = HttpApiBuilder.serve().pipe(
Layer.provide(ApiLive),
Layer.provideMerge(NodeHttpServer.layerTest),
);
// ---------------------------------------------------------------------------
// Tests — layer() shares the server across all tests in this describe block
// ---------------------------------------------------------------------------
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 })] as const,
}),
);
const preview = yield* executor.openapi.previewSpec(specJson);
expect(preview.operationCount).toBeGreaterThanOrEqual(2);
expect(preview.servers).toBeDefined();
}),
);
it.effect("registers runtime openapi tools under built-in 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 })] 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 built-in as a 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 })] as const,
}),
);
const sources = yield* executor.sources.list();
expect(sources).toContainEqual(
expect.objectContaining({
id: "built-in",
name: "Built In",
kind: "built-in",
runtime: true,
canRemove: false,
canRefresh: false,
}),
);
}),
);
it.effect("closing an executor does not remove added source tools from the shared registry", () =>
Effect.gen(function* () {
const httpClient = yield* HttpClient.HttpClient;
const clientLayer = Layer.succeed(HttpClient.HttpClient, httpClient);
const scope = {
id: ScopeId.make("test-scope"),
name: "/test",
createdAt: new Date(),
} as const;
const sharedConfig = {
scope,
tools: makeInMemoryToolRegistry(),
sources: makeInMemorySourceRegistry(),
secrets: makeInMemorySecretStore(),
policies: makeInMemoryPolicyEngine(),
};
const executor1 = yield* createExecutor({
...sharedConfig,
plugins: [openApiPlugin({ httpClientLayer: clientLayer })] as const,
});
yield* executor1.openapi.addSpec({
spec: specJson,
namespace: "persisted",
});
expect((yield* executor1.tools.list()).map((tool) => tool.id)).toContain(
"persisted.items.listItems",
);
yield* executor1.close();
const executor2 = yield* createExecutor({
...sharedConfig,
plugins: [openApiPlugin({ httpClientLayer: clientLayer })] as const,
});
expect((yield* executor2.tools.list()).map((tool) => tool.id)).toContain(
"persisted.items.listItems",
);
}),
);
it.effect("invokes runtime 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 })] as const,
}),
);
const result = yield* executor.tools.invoke(
"openapi.previewSpec",
{ spec: specJson },
autoApprove,
);
expect(result.error).toBeNull();
expect((result.data as { operationCount: number }).operationCount).toBeGreaterThanOrEqual(2);
}),
);
it.effect("invokes runtime 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 })] as const,
}),
);
const result = yield* executor.tools.invoke(
"openapi.addSource",
{ spec: specJson, namespace: "runtime" },
autoApprove,
);
expect(result.error).toBeNull();
expect(result.data).toEqual({ sourceId: "runtime", toolCount: 3 });
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 })] as const,
}),
);
// Store a secret
yield* executor.secrets.set({
id: SecretId.make("test-api-token"),
name: "Test API Token",
value: "secret-value-123",
});
// Add spec with secret-backed header
yield* executor.openapi.addSpec({
spec: specJson,
namespace: "authed",
baseUrl: "",
headers: {
Authorization: { secretId: "test-api-token", prefix: "Bearer " },
"X-Static": "hello",
},
});
// Invoke the echo endpoint — verifies secret was resolved and sent
const result = yield* executor.tools.invoke("authed.items.echoHeaders", {}, autoApprove);
expect(result.error).toBeNull();
const data = result.data as { authorization?: string; "x-static"?: string };
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 })] as const,
}),
);
// Add spec with secret-backed header but DON'T store the secret
yield* executor.openapi.addSpec({
spec: specJson,
namespace: "noauth",
baseUrl: "",
headers: {
Authorization: { secretId: "missing-token", prefix: "Bearer " },
},
});
// Invoke — should fail with a clear error about the missing secret
const error = yield* Effect.flip(
executor.tools.invoke("noauth.items.listItems", {}, autoApprove),
);
expect(error._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 })] as const,
}),
);
const result = yield* executor.openapi.addSpec({
spec: specJson,
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 })] as const,
}),
);
yield* executor.openapi.addSpec({
spec: specJson,
namespace: "test",
baseUrl: "",
});
const result = yield* executor.tools.invoke("test.items.listItems", {}, autoApprove);
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 })] as const,
}),
);
yield* executor.openapi.addSpec({
spec: specJson,
namespace: "test",
baseUrl: "",
});
const result = yield* executor.tools.invoke(
"test.items.getItem",
{
itemId: "2",
},
autoApprove,
);
expect(result.error).toBeNull();
expect(result.data).toEqual({ id: 2, name: "Gadget" });
}),
);
it.effect("removeSpec cleans up registered 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 })] as const,
}),
);
yield* executor.openapi.addSpec({
spec: specJson,
namespace: "removable",
baseUrl: "",
});
expect((yield* executor.tools.list()).length).toBeGreaterThan(0);
yield* executor.openapi.removeSpec("removable");
const remaining = yield* executor.tools.list();
expect(remaining.map((tool) => tool.id)).toEqual([
"openapi.previewSpec",
"openapi.addSource",
]);
}),
);
});