import { describe, expect, it } from "@effect/vitest";import { Effect } from "effect"; import { makeMemoryAdapter } from "@executor/storage-core/testing/memory";import type { DBAdapter, Where } from "@executor/storage-core"; import { makeInMemoryBlobStore } from "./blob";import { collectSchemas, createExecutor } from "./executor";import { ElicitationResponse, FormElicitation, UrlElicitation,} from "./elicitation";import { defineSchema, definePlugin } from "./plugin";import { SetSecretInput } from "./secrets";import { makeTestConfig } from "./testing";import type { SecretProvider } from "./secrets";import { ScopeId, SecretId } from "./ids";import { Scope } from "./scope"; 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); }, }), ),}); // ---------------------------------------------------------------------------// Tiny test plugin — declares a static source with two control tools, a// plugin schema for a per-row key/value table, and a dynamic invokeTool// handler. Exercises everything createExecutor has to wire up.// --------------------------------------------------------------------------- // Plugin-declared schema. `defineSchema` preserves literal types via// `const` inference — no `as const satisfies DBSchema` ceremony.const testSchema = defineSchema({ test_thing: { fields: { id: { type: "string", required: true }, value: { type: "string", required: true }, }, },}); let testAnnotationResolveCount = 0; const testPlugin = definePlugin(() => ({ id: "test" as const, schema: testSchema, // `adapter` is typed against testSchema automatically — no imports of // DBAdapter, no typedAdapter wrapping. `model: "test_thing"` is // narrowed to the schema's model names, and row data shape comes // from the schema's field definitions. storage: ({ adapter }) => ({ writeThing: (id: string, value: string) => adapter .create({ model: "test_thing", data: { id, value }, forceAllowId: true, }) .pipe(Effect.asVoid), readThing: (id: string) => adapter .findOne({ model: "test_thing", where: [{ field: "id", value: id }], }) .pipe(Effect.map((row) => row?.value ?? null)), }), extension: (ctx) => ({ echo: (text: string) => Effect.succeed(`echo:${text}`), addThing: (id: string, value: string) => ctx.transaction( Effect.gen(function* () { yield* ctx.storage.writeThing(id, value); yield* ctx.core.sources.register({ id, scope: ctx.scopes[0]!.id, kind: "test", name: id, canRemove: true, tools: [ { name: "read", description: "read the thing" }, { name: "write", description: "overwrite the thing" }, ], }); }), ), }), staticSources: (self) => [ { id: "test.control", kind: "control", name: "Test Control", tools: [ { name: "echo", description: "static echo tool", handler: ({ args }) => self.echo((args as { text: string }).text), }, ], }, ], invokeTool: ({ ctx, toolRow, args }) => Effect.gen(function* () { // toolRow.source_id = the thing id (we registered the source with // that id). toolRow.name = "read" | "write". No string splitting. const thingId = toolRow.source_id; if (toolRow.name === "read") { return yield* ctx.storage.readThing(thingId); } if (toolRow.name === "write") { const { value } = args as { value: string }; yield* ctx.storage.writeThing(thingId, value); return { ok: true }; } return yield* Effect.fail(new Error(`unknown tool ${toolRow.id}`)); }), // Derived annotations: `write` gates on approval, `read` doesn't. // Purely computed from the tool's name — no data persisted on the row. resolveAnnotations: ({ toolRows }) => Effect.sync(() => { testAnnotationResolveCount++; const out: Record<string, { requiresApproval: boolean; approvalDescription?: string }> = {}; for (const row of toolRows) { if (row.name === "write") { out[row.id] = { requiresApproval: true, approvalDescription: `Overwrite ${row.source_id}`, }; } else { out[row.id] = { requiresApproval: false }; } } return out; }),})); // ---------------------------------------------------------------------------// Test plugin that contributes an in-memory writable secret provider so// the secrets surface has something to talk to.// --------------------------------------------------------------------------- const memoryProvider: SecretProvider = (() => { const store = new Map<string, string>(); const key = (scope: string, id: string) => `${scope}\u0000${id}`; return { key: "memory", writable: true, get: (id, scope) => Effect.sync(() => store.get(key(scope, id)) ?? null), set: (id, value, scope) => Effect.sync(() => { store.set(key(scope, id), value); }), delete: (id, scope) => Effect.sync(() => store.delete(key(scope, 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],})); // ---------------------------------------------------------------------------// Tests// --------------------------------------------------------------------------- describe("createExecutor", () => { it.effect("invokes a static tool via the in-memory pool", () => Effect.gen(function* () { const executor = yield* createExecutor( makeTestConfig({ plugins: [testPlugin()] as const }), ); const result = yield* executor.tools.invoke("test.control.echo", { text: "hi", }); expect(result).toBe("echo:hi"); }), ); it.effect("lists static tools alongside dynamic ones", () => Effect.gen(function* () { const executor = yield* createExecutor( makeTestConfig({ plugins: [testPlugin()] as const }), ); yield* executor.test.addThing("thing1", "hello"); const tools = yield* executor.tools.list(); const ids = tools.map((t) => t.id); expect(ids).toContain("test.control.echo"); expect(ids).toContain("thing1.read"); expect(ids).toContain("thing1.write"); }), ); it.effect("filters tools by query", () => Effect.gen(function* () { const executor = yield* createExecutor( makeTestConfig({ plugins: [testPlugin()] as const }), ); yield* executor.test.addThing("thing1", "hello"); const tools = yield* executor.tools.list({ query: "echo" }); expect(tools.map((t) => t.id)).toEqual(["test.control.echo"]); }), ); it.effect("pushes sourceId tool list filters into storage", () => Effect.gen(function* () { const config = makeTestConfig({ plugins: [testPlugin()] as const }); const findManyCalls: FindManyCall[] = []; const executor = yield* createExecutor({ ...config, adapter: recordFindMany(config.adapter, findManyCalls), }); yield* executor.test.addThing("thing1", "hello"); yield* executor.test.addThing("thing2", "goodbye"); findManyCalls.length = 0; const tools = yield* executor.tools.list({ sourceId: "thing1" }); expect(tools.map((t) => t.id).sort()).toEqual([ "thing1.read", "thing1.write", ]); const toolRead = findManyCalls.find((call) => call.model === "tool"); expect(toolRead?.where).toEqual( expect.arrayContaining([ expect.objectContaining({ field: "source_id", value: "thing1" }), ]), ); }), ); it.effect("can list tools without resolving dynamic annotations", () => Effect.gen(function* () { const executor = yield* createExecutor( makeTestConfig({ plugins: [testPlugin()] as const }), ); yield* executor.test.addThing("thing1", "hello"); testAnnotationResolveCount = 0; const tools = yield* executor.tools.list({ sourceId: "thing1", includeAnnotations: false, }); expect(testAnnotationResolveCount).toBe(0); expect(tools.map((t) => t.id).sort()).toEqual([ "thing1.read", "thing1.write", ]); expect(tools.every((tool) => tool.annotations === undefined)).toBe(true); }), ); it.effect("invokes a dynamic tool through plugin.invokeTool", () => Effect.gen(function* () { const executor = yield* createExecutor( makeTestConfig({ plugins: [testPlugin()] as const }), ); yield* executor.test.addThing("thing1", "hello"); const result = yield* executor.tools.invoke("thing1.read", {}); expect(result).toBe("hello"); }), ); it.effect("enforces tool annotations before invoking", () => Effect.gen(function* () { const executor = yield* createExecutor( makeTestConfig({ plugins: [testPlugin()] as const }), ); yield* executor.test.addThing("thing1", "hello"); // requiresApproval: true → declined → ElicitationDeclinedError const declined = yield* executor.tools .invoke( "thing1.write", { value: "updated" }, { onElicitation: () => Effect.succeed(new ElicitationResponse({ action: "decline" })), }, ) .pipe(Effect.flip); expect((declined as { _tag: string })._tag).toBe( "ElicitationDeclinedError", ); // auto-accept → succeeds const accepted = yield* executor.tools.invoke( "thing1.write", { value: "updated" }, { onElicitation: "accept-all" }, ); expect(accepted).toEqual({ ok: true }); }), ); it.effect("sources.list unions static runtime sources and dynamic ones", () => Effect.gen(function* () { const executor = yield* createExecutor( makeTestConfig({ plugins: [testPlugin()] as const }), ); yield* executor.test.addThing("thing1", "hello"); const sources = yield* executor.sources.list(); const control = sources.find((s) => s.id === "test.control"); expect(control).toBeDefined(); expect(control!.runtime).toBe(true); expect(control!.canRemove).toBe(false); const dynamic = sources.find((s) => s.id === "thing1"); expect(dynamic).toBeDefined(); expect(dynamic!.runtime).toBe(false); expect(dynamic!.canRemove).toBe(true); }), ); it.effect("rejects remove of a static source", () => Effect.gen(function* () { const executor = yield* createExecutor( makeTestConfig({ plugins: [testPlugin()] as const }), ); const err = yield* executor.sources .remove("test.control") .pipe(Effect.flip); expect((err as { _tag: string })._tag).toBe( "SourceRemovalNotAllowedError", ); }), ); it.effect("handles deeply-namespaced tool names (dots in name)", () => Effect.gen(function* () { const namespacedPlugin = definePlugin(() => ({ id: "nested" as const, storage: () => ({}), extension: (ctx) => ({ register: () => ctx.core.sources.register({ id: "cloudflare", scope: ctx.scopes[0]!.id, kind: "nested", name: "cloudflare", canRemove: true, tools: [ { name: "dns.records.create", description: "create DNS record" }, { name: "dns.records.list", description: "list DNS records" }, { name: "zones.listZones", description: "list zones" }, ], }), }), invokeTool: ({ toolRow }) => // Real plugin would look up by toolRow.id against its own // enrichment table. Here we just echo the structured fields // so the test can assert they came through intact. Effect.succeed({ id: toolRow.id, sourceId: toolRow.source_id, name: toolRow.name, }), })); const executor = yield* createExecutor( makeTestConfig({ plugins: [namespacedPlugin()] as const }), ); yield* executor.nested.register(); const tools = yield* executor.tools.list(); const ids = tools.map((t) => t.id).sort(); expect(ids).toContain("cloudflare.dns.records.create"); expect(ids).toContain("cloudflare.dns.records.list"); expect(ids).toContain("cloudflare.zones.listZones"); // Invoke by the exact id — dots are just characters, never parsed. const result = (yield* executor.tools.invoke( "cloudflare.dns.records.create", {}, )) as { id: string; sourceId: string; name: string }; // Structured fields round-trip cleanly: source_id and name are // the exact strings the plugin registered. expect(result.id).toBe("cloudflare.dns.records.create"); expect(result.sourceId).toBe("cloudflare"); expect(result.name).toBe("dns.records.create"); }), ); it.effect("rejects dynamic registration that collides with a static id", () => Effect.gen(function* () { const collidingPlugin = definePlugin(() => ({ id: "collide" as const, storage: () => ({}), extension: (ctx) => ({ tryRegister: () => ctx.core.sources.register({ id: "test.control", // collides with testPlugin's static source scope: ctx.scopes[0]!.id, kind: "x", name: "x", tools: [], }), }), })); const executor = yield* createExecutor( makeTestConfig({ plugins: [testPlugin(), collidingPlugin()] as const, }), ); // The collision is treated as an internal/programmer error and // surfaces as raw `StorageError` in the typed channel. The HTTP // edge (`@executor/api` `withCapture`) is responsible for // translating it to the opaque `InternalError({ traceId })` when // crossing the wire; here, at the SDK layer, we expect the raw tag. const err = yield* executor.collide.tryRegister().pipe(Effect.flip); expect(err._tag).toBe("StorageError"); }), ); it.effect("ctx.transaction commits all nested writes on success", () => Effect.gen(function* () { const executor = yield* createExecutor( makeTestConfig({ plugins: [testPlugin()] as const }), ); // addThing wraps storage + sources.register in ctx.transaction. yield* executor.test.addThing("thing1", "hello"); const sources = yield* executor.sources.list(); expect(sources.find((s) => s.id === "thing1")).toBeDefined(); const tools = (yield* executor.tools.list()).map((t) => t.id); expect(tools).toContain("thing1.read"); expect(tools).toContain("thing1.write"); // plugin storage row committed too expect(yield* executor.tools.invoke("thing1.read", {})).toBe("hello"); }), ); it.effect("ctx.transaction rolls back all nested writes on failure", () => Effect.gen(function* () { // Plugin that does: storage write -> core.sources.register -> fail. // Every write must roll back. const rollbackPlugin = definePlugin(() => ({ id: "rollback" as const, schema: testSchema, storage: ({ adapter }) => ({ writeThing: (id: string, value: string) => adapter .create({ model: "test_thing", data: { id, value }, forceAllowId: true, }) .pipe(Effect.asVoid), countThings: () => adapter.count({ model: "test_thing" }), }), extension: (ctx) => ({ doFailingTx: () => ctx.transaction( Effect.gen(function* () { yield* ctx.storage.writeThing("x1", "v1"); yield* ctx.core.sources.register({ id: "rb-source", scope: ctx.scopes[0]!.id,Lines 1–500 of 1575. Load more
import { describe, expect, it } from "@effect/vitest"; import { Effect } from "effect"; import { makeMemoryAdapter } from "@executor/storage-core/testing/memory"; import type { DBAdapter, Where } from "@executor/storage-core"; import { makeInMemoryBlobStore } from "./blob"; import { collectSchemas, createExecutor } from "./executor"; import { ElicitationResponse, FormElicitation, UrlElicitation, } from "./elicitation"; import { defineSchema, definePlugin } from "./plugin"; import { SetSecretInput } from "./secrets"; import { makeTestConfig } from "./testing"; import type { SecretProvider } from "./secrets"; import { ScopeId, SecretId } from "./ids"; import { Scope } from "./scope"; 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); }, }), ), }); // --------------------------------------------------------------------------- // Tiny test plugin — declares a static source with two control tools, a // plugin schema for a per-row key/value table, and a dynamic invokeTool // handler. Exercises everything createExecutor has to wire up. // --------------------------------------------------------------------------- // Plugin-declared schema. `defineSchema` preserves literal types via // `const` inference — no `as const satisfies DBSchema` ceremony. const testSchema = defineSchema({ test_thing: { fields: { id: { type: "string", required: true }, value: { type: "string", required: true }, }, }, }); let testAnnotationResolveCount = 0; const testPlugin = definePlugin(() => ({ id: "test" as const, schema: testSchema, // `adapter` is typed against testSchema automatically — no imports of // DBAdapter, no typedAdapter wrapping. `model: "test_thing"` is // narrowed to the schema's model names, and row data shape comes // from the schema's field definitions. storage: ({ adapter }) => ({ writeThing: (id: string, value: string) => adapter .create({ model: "test_thing", data: { id, value }, forceAllowId: true, }) .pipe(Effect.asVoid), readThing: (id: string) => adapter .findOne({ model: "test_thing", where: [{ field: "id", value: id }], }) .pipe(Effect.map((row) => row?.value ?? null)), }), extension: (ctx) => ({ echo: (text: string) => Effect.succeed(`echo:${text}`), addThing: (id: string, value: string) => ctx.transaction( Effect.gen(function* () { yield* ctx.storage.writeThing(id, value); yield* ctx.core.sources.register({ id, scope: ctx.scopes[0]!.id, kind: "test", name: id, canRemove: true, tools: [ { name: "read", description: "read the thing" }, { name: "write", description: "overwrite the thing" }, ], }); }), ), }), staticSources: (self) => [ { id: "test.control", kind: "control", name: "Test Control", tools: [ { name: "echo", description: "static echo tool", handler: ({ args }) => self.echo((args as { text: string }).text), }, ], }, ], invokeTool: ({ ctx, toolRow, args }) => Effect.gen(function* () { // toolRow.source_id = the thing id (we registered the source with // that id). toolRow.name = "read" | "write". No string splitting. const thingId = toolRow.source_id; if (toolRow.name === "read") { return yield* ctx.storage.readThing(thingId); } if (toolRow.name === "write") { const { value } = args as { value: string }; yield* ctx.storage.writeThing(thingId, value); return { ok: true }; } return yield* Effect.fail(new Error(`unknown tool ${toolRow.id}`)); }), // Derived annotations: `write` gates on approval, `read` doesn't. // Purely computed from the tool's name — no data persisted on the row. resolveAnnotations: ({ toolRows }) => Effect.sync(() => { testAnnotationResolveCount++; const out: Record<string, { requiresApproval: boolean; approvalDescription?: string }> = {}; for (const row of toolRows) { if (row.name === "write") { out[row.id] = { requiresApproval: true, approvalDescription: `Overwrite ${row.source_id}`, }; } else { out[row.id] = { requiresApproval: false }; } } return out; }), })); // --------------------------------------------------------------------------- // Test plugin that contributes an in-memory writable secret provider so // the secrets surface has something to talk to. // --------------------------------------------------------------------------- const memoryProvider: SecretProvider = (() => { const store = new Map<string, string>(); const key = (scope: string, id: string) => `${scope}\u0000${id}`; return { key: "memory", writable: true, get: (id, scope) => Effect.sync(() => store.get(key(scope, id)) ?? null), set: (id, value, scope) => Effect.sync(() => { store.set(key(scope, id), value); }), delete: (id, scope) => Effect.sync(() => store.delete(key(scope, 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], })); // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- describe("createExecutor", () => { it.effect("invokes a static tool via the in-memory pool", () => Effect.gen(function* () { const executor = yield* createExecutor( makeTestConfig({ plugins: [testPlugin()] as const }), ); const result = yield* executor.tools.invoke("test.control.echo", { text: "hi", }); expect(result).toBe("echo:hi"); }), ); it.effect("lists static tools alongside dynamic ones", () => Effect.gen(function* () { const executor = yield* createExecutor( makeTestConfig({ plugins: [testPlugin()] as const }), ); yield* executor.test.addThing("thing1", "hello"); const tools = yield* executor.tools.list(); const ids = tools.map((t) => t.id); expect(ids).toContain("test.control.echo"); expect(ids).toContain("thing1.read"); expect(ids).toContain("thing1.write"); }), ); it.effect("filters tools by query", () => Effect.gen(function* () { const executor = yield* createExecutor( makeTestConfig({ plugins: [testPlugin()] as const }), ); yield* executor.test.addThing("thing1", "hello"); const tools = yield* executor.tools.list({ query: "echo" }); expect(tools.map((t) => t.id)).toEqual(["test.control.echo"]); }), ); it.effect("pushes sourceId tool list filters into storage", () => Effect.gen(function* () { const config = makeTestConfig({ plugins: [testPlugin()] as const }); const findManyCalls: FindManyCall[] = []; const executor = yield* createExecutor({ ...config, adapter: recordFindMany(config.adapter, findManyCalls), }); yield* executor.test.addThing("thing1", "hello"); yield* executor.test.addThing("thing2", "goodbye"); findManyCalls.length = 0; const tools = yield* executor.tools.list({ sourceId: "thing1" }); expect(tools.map((t) => t.id).sort()).toEqual([ "thing1.read", "thing1.write", ]); const toolRead = findManyCalls.find((call) => call.model === "tool"); expect(toolRead?.where).toEqual( expect.arrayContaining([ expect.objectContaining({ field: "source_id", value: "thing1" }), ]), ); }), ); it.effect("can list tools without resolving dynamic annotations", () => Effect.gen(function* () { const executor = yield* createExecutor( makeTestConfig({ plugins: [testPlugin()] as const }), ); yield* executor.test.addThing("thing1", "hello"); testAnnotationResolveCount = 0; const tools = yield* executor.tools.list({ sourceId: "thing1", includeAnnotations: false, }); expect(testAnnotationResolveCount).toBe(0); expect(tools.map((t) => t.id).sort()).toEqual([ "thing1.read", "thing1.write", ]); expect(tools.every((tool) => tool.annotations === undefined)).toBe(true); }), ); it.effect("invokes a dynamic tool through plugin.invokeTool", () => Effect.gen(function* () { const executor = yield* createExecutor( makeTestConfig({ plugins: [testPlugin()] as const }), ); yield* executor.test.addThing("thing1", "hello"); const result = yield* executor.tools.invoke("thing1.read", {}); expect(result).toBe("hello"); }), ); it.effect("enforces tool annotations before invoking", () => Effect.gen(function* () { const executor = yield* createExecutor( makeTestConfig({ plugins: [testPlugin()] as const }), ); yield* executor.test.addThing("thing1", "hello"); // requiresApproval: true → declined → ElicitationDeclinedError const declined = yield* executor.tools .invoke( "thing1.write", { value: "updated" }, { onElicitation: () => Effect.succeed(new ElicitationResponse({ action: "decline" })), }, ) .pipe(Effect.flip); expect((declined as { _tag: string })._tag).toBe( "ElicitationDeclinedError", ); // auto-accept → succeeds const accepted = yield* executor.tools.invoke( "thing1.write", { value: "updated" }, { onElicitation: "accept-all" }, ); expect(accepted).toEqual({ ok: true }); }), ); it.effect("sources.list unions static runtime sources and dynamic ones", () => Effect.gen(function* () { const executor = yield* createExecutor( makeTestConfig({ plugins: [testPlugin()] as const }), ); yield* executor.test.addThing("thing1", "hello"); const sources = yield* executor.sources.list(); const control = sources.find((s) => s.id === "test.control"); expect(control).toBeDefined(); expect(control!.runtime).toBe(true); expect(control!.canRemove).toBe(false); const dynamic = sources.find((s) => s.id === "thing1"); expect(dynamic).toBeDefined(); expect(dynamic!.runtime).toBe(false); expect(dynamic!.canRemove).toBe(true); }), ); it.effect("rejects remove of a static source", () => Effect.gen(function* () { const executor = yield* createExecutor( makeTestConfig({ plugins: [testPlugin()] as const }), ); const err = yield* executor.sources .remove("test.control") .pipe(Effect.flip); expect((err as { _tag: string })._tag).toBe( "SourceRemovalNotAllowedError", ); }), ); it.effect("handles deeply-namespaced tool names (dots in name)", () => Effect.gen(function* () { const namespacedPlugin = definePlugin(() => ({ id: "nested" as const, storage: () => ({}), extension: (ctx) => ({ register: () => ctx.core.sources.register({ id: "cloudflare", scope: ctx.scopes[0]!.id, kind: "nested", name: "cloudflare", canRemove: true, tools: [ { name: "dns.records.create", description: "create DNS record" }, { name: "dns.records.list", description: "list DNS records" }, { name: "zones.listZones", description: "list zones" }, ], }), }), invokeTool: ({ toolRow }) => // Real plugin would look up by toolRow.id against its own // enrichment table. Here we just echo the structured fields // so the test can assert they came through intact. Effect.succeed({ id: toolRow.id, sourceId: toolRow.source_id, name: toolRow.name, }), })); const executor = yield* createExecutor( makeTestConfig({ plugins: [namespacedPlugin()] as const }), ); yield* executor.nested.register(); const tools = yield* executor.tools.list(); const ids = tools.map((t) => t.id).sort(); expect(ids).toContain("cloudflare.dns.records.create"); expect(ids).toContain("cloudflare.dns.records.list"); expect(ids).toContain("cloudflare.zones.listZones"); // Invoke by the exact id — dots are just characters, never parsed. const result = (yield* executor.tools.invoke( "cloudflare.dns.records.create", {}, )) as { id: string; sourceId: string; name: string }; // Structured fields round-trip cleanly: source_id and name are // the exact strings the plugin registered. expect(result.id).toBe("cloudflare.dns.records.create"); expect(result.sourceId).toBe("cloudflare"); expect(result.name).toBe("dns.records.create"); }), ); it.effect("rejects dynamic registration that collides with a static id", () => Effect.gen(function* () { const collidingPlugin = definePlugin(() => ({ id: "collide" as const, storage: () => ({}), extension: (ctx) => ({ tryRegister: () => ctx.core.sources.register({ id: "test.control", // collides with testPlugin's static source scope: ctx.scopes[0]!.id, kind: "x", name: "x", tools: [], }), }), })); const executor = yield* createExecutor( makeTestConfig({ plugins: [testPlugin(), collidingPlugin()] as const, }), ); // The collision is treated as an internal/programmer error and // surfaces as raw `StorageError` in the typed channel. The HTTP // edge (`@executor/api` `withCapture`) is responsible for // translating it to the opaque `InternalError({ traceId })` when // crossing the wire; here, at the SDK layer, we expect the raw tag. const err = yield* executor.collide.tryRegister().pipe(Effect.flip); expect(err._tag).toBe("StorageError"); }), ); it.effect("ctx.transaction commits all nested writes on success", () => Effect.gen(function* () { const executor = yield* createExecutor( makeTestConfig({ plugins: [testPlugin()] as const }), ); // addThing wraps storage + sources.register in ctx.transaction. yield* executor.test.addThing("thing1", "hello"); const sources = yield* executor.sources.list(); expect(sources.find((s) => s.id === "thing1")).toBeDefined(); const tools = (yield* executor.tools.list()).map((t) => t.id); expect(tools).toContain("thing1.read"); expect(tools).toContain("thing1.write"); // plugin storage row committed too expect(yield* executor.tools.invoke("thing1.read", {})).toBe("hello"); }), ); it.effect("ctx.transaction rolls back all nested writes on failure", () => Effect.gen(function* () { // Plugin that does: storage write -> core.sources.register -> fail. // Every write must roll back. const rollbackPlugin = definePlugin(() => ({ id: "rollback" as const, schema: testSchema, storage: ({ adapter }) => ({ writeThing: (id: string, value: string) => adapter .create({ model: "test_thing", data: { id, value }, forceAllowId: true, }) .pipe(Effect.asVoid), countThings: () => adapter.count({ model: "test_thing" }), }), extension: (ctx) => ({ doFailingTx: () => ctx.transaction( Effect.gen(function* () { yield* ctx.storage.writeThing("x1", "v1"); yield* ctx.core.sources.register({ id: "rb-source", scope: ctx.scopes[0]!.id,