import { describe, expect, it } from "@effect/vitest";import { Effect, Predicate, Result } from "effect"; import { AuthTemplateSlug, ConnectionName, IntegrationSlug, ProviderItemId, ProviderKey, ToolAddress, ToolName,} from "./ids";import { createExecutor } from "./executor";import { definePlugin } from "./plugin";import type { CredentialProvider } from "./provider";import { makeTestConfig, makeTestExecutor } from "./testing"; // removed: v1 connection-refresh lifecycle, ConnectionProvider.refresh,// SecretProvider, accessToken token-refresh + in-flight dedup tests — the v2// model folds secret/connection into one provider-resolved Connection, and OAuth// refresh is core's responsibility (stubbed for milestone 1). The cases below// cover the v2 connection surface: create (inline + external), list, get,// remove, refresh, and per-connection tool production. const memoryProvider = (): CredentialProvider => { const store = new Map<string, string>(); return { key: ProviderKey.make("memory"), writable: true, get: (id) => Effect.sync(() => store.get(String(id)) ?? null), set: (id, value) => Effect.sync(() => void store.set(String(id), value)), has: (id) => Effect.sync(() => store.has(String(id))), list: () => Effect.sync(() => Array.from(store.keys()).map((key) => ({ id: ProviderItemId.make(key), name: key, })), ), };}; const INTEG = IntegrationSlug.make("vercel");const TEMPLATE = AuthTemplateSlug.make("apiKey"); const demoPlugin = definePlugin(() => ({ id: "demo" as const, credentialProviders: [memoryProvider()], storage: () => ({}), resolveTools: () => Effect.succeed({ tools: [ { name: ToolName.make("deploy"), description: "deploy" }, { name: ToolName.make("list"), description: "list" }, ], }), invokeTool: ({ toolRow, credential }) => Effect.succeed({ ran: toolRow.name, value: credential.value }), extension: (ctx) => ({ seed: () => ctx.core.integrations.register({ slug: INTEG, description: "Vercel", config: {}, }), resolveValue: (owner: "org" | "user", name: string) => ctx.connections.resolveValue({ owner, integration: INTEG, name: ConnectionName.make(name), }), }),}))(); const setup = () => makeTestExecutor({ plugins: [demoPlugin] as const }).pipe( Effect.tap((executor) => executor.demo.seed()), ); describe("connections.create", () => { it.effect("inline value writes to the default writable provider and produces tools", () => Effect.gen(function* () { const executor = yield* setup(); const connection = yield* executor.connections.create({ owner: "org", name: ConnectionName.make("main"), integration: INTEG, template: TEMPLATE, value: "secret-token", }); expect(connection.provider).toBe(ProviderKey.make("memory")); expect(String(connection.address)).toBe("tools.vercel.org.main"); const tools = yield* executor.tools.list(); expect(tools.map((t) => String(t.name)).sort()).toEqual(["deploy", "list"]); // The inline value is resolvable via the connection's provider. const value = yield* executor.demo.resolveValue("org", "main"); expect(value).toBe("secret-token"); }), ); it.effect("normalizes free-form names into JS-callable connection identifiers", () => Effect.gen(function* () { const executor = yield* setup(); const connection = yield* executor.connections.create({ owner: "org", name: ConnectionName.make("my-api-key"), integration: INTEG, template: TEMPLATE, value: "secret-token", }); expect(String(connection.name)).toBe("myApiKey"); expect(String(connection.address)).toBe("tools.vercel.org.myApiKey"); const tools = yield* executor.tools.list(); expect(tools.map((t) => String(t.address)).sort()).toEqual([ "tools.vercel.org.myApiKey.deploy", "tools.vercel.org.myApiKey.list", ]); const value = yield* executor.demo.resolveValue("org", "myApiKey"); expect(value).toBe("secret-token"); }), ); it.effect("external `from` references a provider item without writing it", () => Effect.gen(function* () { const executor = yield* setup(); const connection = yield* executor.connections.create({ owner: "org", name: ConnectionName.make("byo"), integration: INTEG, template: TEMPLATE, from: { provider: ProviderKey.make("memory"), id: ProviderItemId.make("ext-item"), }, }); expect(connection.provider).toBe(ProviderKey.make("memory")); // No value was stored (external reference) — resolveValue returns null. const value = yield* executor.demo.resolveValue("org", "byo"); expect(value).toBeNull(); }), ); it.effect("create on an unknown integration fails with IntegrationNotFoundError", () => Effect.gen(function* () { const executor = yield* setup(); const result = yield* Effect.result( executor.connections.create({ owner: "org", name: ConnectionName.make("x"), integration: IntegrationSlug.make("unknown"), template: TEMPLATE, value: "v", }), ); expect(Result.isFailure(result)).toBe(true); if (!Result.isFailure(result)) return; expect(Predicate.isTagged("IntegrationNotFoundError")(result.failure)).toBe(true); }), ); // A credentialed connection is "born wired": it must reference at least one // credential input. An empty binding (an empty `values`/`inputs` map) produces // a credential with no credential — it persists, produces a full tool catalog, // and then fails every invocation with `connection_value_missing`. These cases // must be rejected at create with a typed `InvalidConnectionInputError` (the // HTTP edge answers 400 with the reason, not an opaque 500). The exception is // the no-auth template ("none"), where zero inputs and an empty `item_ids` // map are the canonical shape — covered below. (An empty-STRING value is also // allowed, and an external `from` that resolves to null is a supported case — // both covered by their own tests.) it.effect("rejects an empty `values` map on a credentialed template and persists nothing", () => Effect.gen(function* () { const executor = yield* setup(); const result = yield* Effect.result( executor.connections.create({ owner: "org", name: ConnectionName.make("empty"), integration: INTEG, template: TEMPLATE, values: {}, }), ); expect(Result.isFailure(result)).toBe(true); if (!Result.isFailure(result)) return; expect(Predicate.isTagged("InvalidConnectionInputError")(result.failure)).toBe(true); // No connection row and — critically — no tools were produced. expect(yield* executor.connections.list()).toEqual([]); expect(yield* executor.tools.list()).toEqual([]); }), ); it.effect("rejects an empty `inputs` map on a credentialed template", () => Effect.gen(function* () { const executor = yield* setup(); const result = yield* Effect.result( executor.connections.create({ owner: "org", name: ConnectionName.make("empty2"), integration: INTEG, template: TEMPLATE, inputs: {}, }), ); expect(Result.isFailure(result)).toBe(true); if (!Result.isFailure(result)) return; expect(Predicate.isTagged("InvalidConnectionInputError")(result.failure)).toBe(true); expect(yield* executor.connections.list()).toEqual([]); }), ); // The no-auth template: public servers need no credential. The UI submits // `values: {}` for them and the persisted row carries an empty `item_ids` // map — that is the canonical shape (every migrated no-auth connection in // prod has it), so it must create cleanly and keep its tools on refresh. it.effect('creates a no-auth (`template: "none"`) connection from an empty `values` map', () => Effect.gen(function* () { const executor = yield* setup(); const connection = yield* executor.connections.create({ owner: "org", name: ConnectionName.make("public"), integration: INTEG, template: AuthTemplateSlug.make("none"), values: {}, }); expect(String(connection.address)).toBe("tools.vercel.org.public"); const tools = yield* executor.tools.list(); expect(tools.map((t) => String(t.name)).sort()).toEqual(["deploy", "list"]); // Refresh must NOT treat the empty binding as invalid and wipe the tools. const refreshed = yield* executor.connections.refresh({ owner: "org", integration: INTEG, name: ConnectionName.make("public"), }); expect(refreshed.map((t) => String(t.name)).sort()).toEqual(["deploy", "list"]); expect((yield* executor.tools.list()).length).toBe(2); }), ); it.effect("allows an empty-string value (no-auth integrations bind one)", () => Effect.gen(function* () { const executor = yield* setup(); const connection = yield* executor.connections.create({ owner: "org", name: ConnectionName.make("noauth"), integration: INTEG, template: TEMPLATE, value: "", }); // The binding exists (non-empty item_ids), so tools are produced; the // empty value itself is the integration's concern, surfaced at invoke. expect(String(connection.address)).toBe("tools.vercel.org.noauth"); const tools = yield* executor.tools.list(); expect(tools.map((t) => String(t.name)).sort()).toEqual(["deploy", "list"]); }), );}); describe("connections.list / get", () => { it.effect("lists created connections and filters by integration", () => Effect.gen(function* () { const executor = yield* setup(); yield* executor.connections.create({ owner: "org", name: ConnectionName.make("a"), integration: INTEG, template: TEMPLATE, value: "v", }); const all = yield* executor.connections.list(); expect(all.map((c) => String(c.name))).toEqual(["a"]); const filtered = yield* executor.connections.list({ integration: INTEG }); expect(filtered.length).toBe(1); const get = yield* executor.connections.get({ owner: "org", integration: INTEG, name: ConnectionName.make("a"), }); expect(get?.name).toBe(ConnectionName.make("a")); }), ); it.effect("get returns null for an unknown connection", () => Effect.gen(function* () { const executor = yield* setup(); const get = yield* executor.connections.get({ owner: "org", integration: INTEG, name: ConnectionName.make("missing"), }); expect(get).toBeNull(); }), );}); describe("connections.remove", () => { it.effect("removes the connection and its tools", () => Effect.gen(function* () { const executor = yield* setup(); yield* executor.connections.create({ owner: "org", name: ConnectionName.make("main"), integration: INTEG, template: TEMPLATE, value: "v", }); yield* executor.connections.remove({ owner: "org", integration: INTEG, name: ConnectionName.make("main"), }); const connections = yield* executor.connections.list(); expect(connections).toEqual([]); const tools = yield* executor.tools.list(); expect(tools).toEqual([]); }), ); it.effect("remove on an unknown connection fails with ConnectionNotFoundError", () => Effect.gen(function* () { const executor = yield* setup(); const result = yield* Effect.result( executor.connections.remove({ owner: "org", integration: INTEG, name: ConnectionName.make("missing"), }), ); expect(Result.isFailure(result)).toBe(true); if (!Result.isFailure(result)) return; expect(Predicate.isTagged("ConnectionNotFoundError")(result.failure)).toBe(true); }), );}); describe("connections.refresh", () => { it.effect("re-produces the connection's tools", () => Effect.gen(function* () { const executor = yield* setup(); yield* executor.connections.create({ owner: "org", name: ConnectionName.make("main"), integration: INTEG, template: TEMPLATE, value: "v", }); const tools = yield* executor.connections.refresh({ owner: "org", integration: INTEG, name: ConnectionName.make("main"), }); expect(tools.map((t) => String(t.name)).sort()).toEqual(["deploy", "list"]); }), );}); describe("tool catalog sync safety", () => { it.effect( "background sync preserves a nonzero remote catalog when a plugin returns authoritative empty", () => Effect.scoped( Effect.gen(function* () { let empty = false; const guardedPlugin = definePlugin(() => ({ id: "guarded" as const, remoteToolCatalog: true, credentialProviders: [memoryProvider()], storage: () => ({}), resolveTools: () => Effect.sync(() => ({ tools: empty ? [] : [ { name: ToolName.make("deploy"), description: "deploy" }, { name: ToolName.make("list"), description: "list" }, ], })), invokeTool: ({ toolRow }) => Effect.succeed({ ran: toolRow.name }), extension: (ctx) => ({ seed: () => ctx.core.integrations.register({ slug: INTEG, description: "Vercel", config: {}, }), }), }))(); const config = makeTestConfig({ plugins: [guardedPlugin] as const }); const executor = yield* createExecutor(config); yield* executor.guarded.seed(); yield* executor.connections.create({ owner: "org", name: ConnectionName.make("main"), integration: INTEG, template: TEMPLATE, value: "secret-token", }); empty = true; yield* Effect.promise(() => config.db.updateMany("connection", { where: (b) => b.and(b("integration", "=", String(INTEG)), b("name", "=", "main")), set: { tools_synced_at: null }, }), ); const tools = yield* executor.tools.list({ integration: INTEG }); const connection = yield* executor.connections.get({ owner: "org", integration: INTEG, name: ConnectionName.make("main"), }); expect(tools.map((tool) => String(tool.name)).sort()).toEqual(["deploy", "list"]); expect(connection?.lastHealth).toMatchObject({ status: "degraded", detail: expect.stringContaining("authoritative empty catalog"), }); }), ), ); it.effect( "background sync clears a non-remote catalog when a plugin returns authoritative empty", () => Effect.scoped( Effect.gen(function* () { let empty = false; const storedStatePlugin = definePlugin(() => ({ id: "stored-state" as const, credentialProviders: [memoryProvider()], storage: () => ({}), resolveTools: () => Effect.sync(() => ({ tools: empty ? [] : [ { name: ToolName.make("deploy"), description: "deploy" }, { name: ToolName.make("list"), description: "list" }, ], })), invokeTool: ({ toolRow }) => Effect.succeed({ ran: toolRow.name }), extension: (ctx) => ({ seed: () => ctx.core.integrations.register({ slug: INTEG, description: "Vercel", config: {}, }), }), }))(); const config = makeTestConfig({ plugins: [storedStatePlugin] as const }); const executor = yield* createExecutor(config); yield* executor["stored-state"].seed(); yield* executor.connections.create({ owner: "org", name: ConnectionName.make("main"), integration: INTEG, template: TEMPLATE, value: "secret-token", }); empty = true; yield* Effect.promise(() => config.db.updateMany("connection", { where: (b) => b.and(b("integration", "=", String(INTEG)), b("name", "=", "main")), set: { tools_synced_at: null }, }), ); const tools = yield* executor.tools.list({ integration: INTEG }); const connection = yield* executor.connections.get({ owner: "org", integration: INTEG, name: ConnectionName.make("main"), }); expect(tools).toEqual([]); expect(connection?.lastHealth).toBeNull(); }), ), ); it.effect("explicit refresh accepts an authoritative empty catalog", () => Effect.scoped( Effect.gen(function* () { let empty = false; const guardedPlugin = definePlugin(() => ({ id: "guarded" as const, credentialProviders: [memoryProvider()], storage: () => ({}), resolveTools: () => Effect.sync(() => ({ tools: empty ? [] : [Lines 1–500 of 720. Load more
import { describe, expect, it } from "@effect/vitest"; import { Effect, Predicate, Result } from "effect"; import { AuthTemplateSlug, ConnectionName, IntegrationSlug, ProviderItemId, ProviderKey, ToolAddress, ToolName, } from "./ids"; import { createExecutor } from "./executor"; import { definePlugin } from "./plugin"; import type { CredentialProvider } from "./provider"; import { makeTestConfig, makeTestExecutor } from "./testing"; // removed: v1 connection-refresh lifecycle, ConnectionProvider.refresh, // SecretProvider, accessToken token-refresh + in-flight dedup tests — the v2 // model folds secret/connection into one provider-resolved Connection, and OAuth // refresh is core's responsibility (stubbed for milestone 1). The cases below // cover the v2 connection surface: create (inline + external), list, get, // remove, refresh, and per-connection tool production. const memoryProvider = (): CredentialProvider => { const store = new Map<string, string>(); return { key: ProviderKey.make("memory"), writable: true, get: (id) => Effect.sync(() => store.get(String(id)) ?? null), set: (id, value) => Effect.sync(() => void store.set(String(id), value)), has: (id) => Effect.sync(() => store.has(String(id))), list: () => Effect.sync(() => Array.from(store.keys()).map((key) => ({ id: ProviderItemId.make(key), name: key, })), ), }; }; const INTEG = IntegrationSlug.make("vercel"); const TEMPLATE = AuthTemplateSlug.make("apiKey"); const demoPlugin = definePlugin(() => ({ id: "demo" as const, credentialProviders: [memoryProvider()], storage: () => ({}), resolveTools: () => Effect.succeed({ tools: [ { name: ToolName.make("deploy"), description: "deploy" }, { name: ToolName.make("list"), description: "list" }, ], }), invokeTool: ({ toolRow, credential }) => Effect.succeed({ ran: toolRow.name, value: credential.value }), extension: (ctx) => ({ seed: () => ctx.core.integrations.register({ slug: INTEG, description: "Vercel", config: {}, }), resolveValue: (owner: "org" | "user", name: string) => ctx.connections.resolveValue({ owner, integration: INTEG, name: ConnectionName.make(name), }), }), }))(); const setup = () => makeTestExecutor({ plugins: [demoPlugin] as const }).pipe( Effect.tap((executor) => executor.demo.seed()), ); describe("connections.create", () => { it.effect("inline value writes to the default writable provider and produces tools", () => Effect.gen(function* () { const executor = yield* setup(); const connection = yield* executor.connections.create({ owner: "org", name: ConnectionName.make("main"), integration: INTEG, template: TEMPLATE, value: "secret-token", }); expect(connection.provider).toBe(ProviderKey.make("memory")); expect(String(connection.address)).toBe("tools.vercel.org.main"); const tools = yield* executor.tools.list(); expect(tools.map((t) => String(t.name)).sort()).toEqual(["deploy", "list"]); // The inline value is resolvable via the connection's provider. const value = yield* executor.demo.resolveValue("org", "main"); expect(value).toBe("secret-token"); }), ); it.effect("normalizes free-form names into JS-callable connection identifiers", () => Effect.gen(function* () { const executor = yield* setup(); const connection = yield* executor.connections.create({ owner: "org", name: ConnectionName.make("my-api-key"), integration: INTEG, template: TEMPLATE, value: "secret-token", }); expect(String(connection.name)).toBe("myApiKey"); expect(String(connection.address)).toBe("tools.vercel.org.myApiKey"); const tools = yield* executor.tools.list(); expect(tools.map((t) => String(t.address)).sort()).toEqual([ "tools.vercel.org.myApiKey.deploy", "tools.vercel.org.myApiKey.list", ]); const value = yield* executor.demo.resolveValue("org", "myApiKey"); expect(value).toBe("secret-token"); }), ); it.effect("external `from` references a provider item without writing it", () => Effect.gen(function* () { const executor = yield* setup(); const connection = yield* executor.connections.create({ owner: "org", name: ConnectionName.make("byo"), integration: INTEG, template: TEMPLATE, from: { provider: ProviderKey.make("memory"), id: ProviderItemId.make("ext-item"), }, }); expect(connection.provider).toBe(ProviderKey.make("memory")); // No value was stored (external reference) — resolveValue returns null. const value = yield* executor.demo.resolveValue("org", "byo"); expect(value).toBeNull(); }), ); it.effect("create on an unknown integration fails with IntegrationNotFoundError", () => Effect.gen(function* () { const executor = yield* setup(); const result = yield* Effect.result( executor.connections.create({ owner: "org", name: ConnectionName.make("x"), integration: IntegrationSlug.make("unknown"), template: TEMPLATE, value: "v", }), ); expect(Result.isFailure(result)).toBe(true); if (!Result.isFailure(result)) return; expect(Predicate.isTagged("IntegrationNotFoundError")(result.failure)).toBe(true); }), ); // A credentialed connection is "born wired": it must reference at least one // credential input. An empty binding (an empty `values`/`inputs` map) produces // a credential with no credential — it persists, produces a full tool catalog, // and then fails every invocation with `connection_value_missing`. These cases // must be rejected at create with a typed `InvalidConnectionInputError` (the // HTTP edge answers 400 with the reason, not an opaque 500). The exception is // the no-auth template ("none"), where zero inputs and an empty `item_ids` // map are the canonical shape — covered below. (An empty-STRING value is also // allowed, and an external `from` that resolves to null is a supported case — // both covered by their own tests.) it.effect("rejects an empty `values` map on a credentialed template and persists nothing", () => Effect.gen(function* () { const executor = yield* setup(); const result = yield* Effect.result( executor.connections.create({ owner: "org", name: ConnectionName.make("empty"), integration: INTEG, template: TEMPLATE, values: {}, }), ); expect(Result.isFailure(result)).toBe(true); if (!Result.isFailure(result)) return; expect(Predicate.isTagged("InvalidConnectionInputError")(result.failure)).toBe(true); // No connection row and — critically — no tools were produced. expect(yield* executor.connections.list()).toEqual([]); expect(yield* executor.tools.list()).toEqual([]); }), ); it.effect("rejects an empty `inputs` map on a credentialed template", () => Effect.gen(function* () { const executor = yield* setup(); const result = yield* Effect.result( executor.connections.create({ owner: "org", name: ConnectionName.make("empty2"), integration: INTEG, template: TEMPLATE, inputs: {}, }), ); expect(Result.isFailure(result)).toBe(true); if (!Result.isFailure(result)) return; expect(Predicate.isTagged("InvalidConnectionInputError")(result.failure)).toBe(true); expect(yield* executor.connections.list()).toEqual([]); }), ); // The no-auth template: public servers need no credential. The UI submits // `values: {}` for them and the persisted row carries an empty `item_ids` // map — that is the canonical shape (every migrated no-auth connection in // prod has it), so it must create cleanly and keep its tools on refresh. it.effect('creates a no-auth (`template: "none"`) connection from an empty `values` map', () => Effect.gen(function* () { const executor = yield* setup(); const connection = yield* executor.connections.create({ owner: "org", name: ConnectionName.make("public"), integration: INTEG, template: AuthTemplateSlug.make("none"), values: {}, }); expect(String(connection.address)).toBe("tools.vercel.org.public"); const tools = yield* executor.tools.list(); expect(tools.map((t) => String(t.name)).sort()).toEqual(["deploy", "list"]); // Refresh must NOT treat the empty binding as invalid and wipe the tools. const refreshed = yield* executor.connections.refresh({ owner: "org", integration: INTEG, name: ConnectionName.make("public"), }); expect(refreshed.map((t) => String(t.name)).sort()).toEqual(["deploy", "list"]); expect((yield* executor.tools.list()).length).toBe(2); }), ); it.effect("allows an empty-string value (no-auth integrations bind one)", () => Effect.gen(function* () { const executor = yield* setup(); const connection = yield* executor.connections.create({ owner: "org", name: ConnectionName.make("noauth"), integration: INTEG, template: TEMPLATE, value: "", }); // The binding exists (non-empty item_ids), so tools are produced; the // empty value itself is the integration's concern, surfaced at invoke. expect(String(connection.address)).toBe("tools.vercel.org.noauth"); const tools = yield* executor.tools.list(); expect(tools.map((t) => String(t.name)).sort()).toEqual(["deploy", "list"]); }), ); }); describe("connections.list / get", () => { it.effect("lists created connections and filters by integration", () => Effect.gen(function* () { const executor = yield* setup(); yield* executor.connections.create({ owner: "org", name: ConnectionName.make("a"), integration: INTEG, template: TEMPLATE, value: "v", }); const all = yield* executor.connections.list(); expect(all.map((c) => String(c.name))).toEqual(["a"]); const filtered = yield* executor.connections.list({ integration: INTEG }); expect(filtered.length).toBe(1); const get = yield* executor.connections.get({ owner: "org", integration: INTEG, name: ConnectionName.make("a"), }); expect(get?.name).toBe(ConnectionName.make("a")); }), ); it.effect("get returns null for an unknown connection", () => Effect.gen(function* () { const executor = yield* setup(); const get = yield* executor.connections.get({ owner: "org", integration: INTEG, name: ConnectionName.make("missing"), }); expect(get).toBeNull(); }), ); }); describe("connections.remove", () => { it.effect("removes the connection and its tools", () => Effect.gen(function* () { const executor = yield* setup(); yield* executor.connections.create({ owner: "org", name: ConnectionName.make("main"), integration: INTEG, template: TEMPLATE, value: "v", }); yield* executor.connections.remove({ owner: "org", integration: INTEG, name: ConnectionName.make("main"), }); const connections = yield* executor.connections.list(); expect(connections).toEqual([]); const tools = yield* executor.tools.list(); expect(tools).toEqual([]); }), ); it.effect("remove on an unknown connection fails with ConnectionNotFoundError", () => Effect.gen(function* () { const executor = yield* setup(); const result = yield* Effect.result( executor.connections.remove({ owner: "org", integration: INTEG, name: ConnectionName.make("missing"), }), ); expect(Result.isFailure(result)).toBe(true); if (!Result.isFailure(result)) return; expect(Predicate.isTagged("ConnectionNotFoundError")(result.failure)).toBe(true); }), ); }); describe("connections.refresh", () => { it.effect("re-produces the connection's tools", () => Effect.gen(function* () { const executor = yield* setup(); yield* executor.connections.create({ owner: "org", name: ConnectionName.make("main"), integration: INTEG, template: TEMPLATE, value: "v", }); const tools = yield* executor.connections.refresh({ owner: "org", integration: INTEG, name: ConnectionName.make("main"), }); expect(tools.map((t) => String(t.name)).sort()).toEqual(["deploy", "list"]); }), ); }); describe("tool catalog sync safety", () => { it.effect( "background sync preserves a nonzero remote catalog when a plugin returns authoritative empty", () => Effect.scoped( Effect.gen(function* () { let empty = false; const guardedPlugin = definePlugin(() => ({ id: "guarded" as const, remoteToolCatalog: true, credentialProviders: [memoryProvider()], storage: () => ({}), resolveTools: () => Effect.sync(() => ({ tools: empty ? [] : [ { name: ToolName.make("deploy"), description: "deploy" }, { name: ToolName.make("list"), description: "list" }, ], })), invokeTool: ({ toolRow }) => Effect.succeed({ ran: toolRow.name }), extension: (ctx) => ({ seed: () => ctx.core.integrations.register({ slug: INTEG, description: "Vercel", config: {}, }), }), }))(); const config = makeTestConfig({ plugins: [guardedPlugin] as const }); const executor = yield* createExecutor(config); yield* executor.guarded.seed(); yield* executor.connections.create({ owner: "org", name: ConnectionName.make("main"), integration: INTEG, template: TEMPLATE, value: "secret-token", }); empty = true; yield* Effect.promise(() => config.db.updateMany("connection", { where: (b) => b.and(b("integration", "=", String(INTEG)), b("name", "=", "main")), set: { tools_synced_at: null }, }), ); const tools = yield* executor.tools.list({ integration: INTEG }); const connection = yield* executor.connections.get({ owner: "org", integration: INTEG, name: ConnectionName.make("main"), }); expect(tools.map((tool) => String(tool.name)).sort()).toEqual(["deploy", "list"]); expect(connection?.lastHealth).toMatchObject({ status: "degraded", detail: expect.stringContaining("authoritative empty catalog"), }); }), ), ); it.effect( "background sync clears a non-remote catalog when a plugin returns authoritative empty", () => Effect.scoped( Effect.gen(function* () { let empty = false; const storedStatePlugin = definePlugin(() => ({ id: "stored-state" as const, credentialProviders: [memoryProvider()], storage: () => ({}), resolveTools: () => Effect.sync(() => ({ tools: empty ? [] : [ { name: ToolName.make("deploy"), description: "deploy" }, { name: ToolName.make("list"), description: "list" }, ], })), invokeTool: ({ toolRow }) => Effect.succeed({ ran: toolRow.name }), extension: (ctx) => ({ seed: () => ctx.core.integrations.register({ slug: INTEG, description: "Vercel", config: {}, }), }), }))(); const config = makeTestConfig({ plugins: [storedStatePlugin] as const }); const executor = yield* createExecutor(config); yield* executor["stored-state"].seed(); yield* executor.connections.create({ owner: "org", name: ConnectionName.make("main"), integration: INTEG, template: TEMPLATE, value: "secret-token", }); empty = true; yield* Effect.promise(() => config.db.updateMany("connection", { where: (b) => b.and(b("integration", "=", String(INTEG)), b("name", "=", "main")), set: { tools_synced_at: null }, }), ); const tools = yield* executor.tools.list({ integration: INTEG }); const connection = yield* executor.connections.get({ owner: "org", integration: INTEG, name: ConnectionName.make("main"), }); expect(tools).toEqual([]); expect(connection?.lastHealth).toBeNull(); }), ), ); it.effect("explicit refresh accepts an authoritative empty catalog", () => Effect.scoped( Effect.gen(function* () { let empty = false; const guardedPlugin = definePlugin(() => ({ id: "guarded" as const, credentialProviders: [memoryProvider()], storage: () => ({}), resolveTools: () => Effect.sync(() => ({ tools: empty ? [] : [