3312 lines · 131.6 KB · TypeScript
RawDownload
import { describe, expect, it } from "@effect/vitest";import {  Cause,  Deferred,  Effect,  Exit,  Fiber,  Inspectable,  Logger,  Option,  Predicate,  Result,  Schema,  Tracer,} from "effect"; import {  AuthTemplateSlug,  ConnectionName,  IntegrationSlug,  ProviderItemId,  ProviderKey,  Subject,  Tenant,  ToolAddress,  ToolName,} from "./ids";import { ConnectionAlreadyExistsError } from "./errors";import { createExecutor } from "./executor";import { StorageError, type FumaDb } from "./fuma-runtime";import { HealthCheckResult } from "./health-check";import { definePlugin, type ResolveToolsResult } from "./plugin";import type { CredentialProvider } from "./provider";import { makeTestConfig, makeTestExecutor } from "./testing";import { ToolResult } from "./tool-result"; // 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"); /** Wrap a test `FumaDb` so every transaction it opens is observable. The *  executor re-binds its own owner context onto the handle it is given, so the *  wrapper must forward `withContext` re-wrapped — otherwise the instrument is *  dropped before any executor query runs. */const instrumentTransactions = (  db: FumaDb,  hooks: { readonly enter: () => void; readonly exit: () => void },): FumaDb => {  const wrap = (inner: FumaDb): FumaDb =>    new Proxy(inner, {      get(target, prop) {        if (prop === "withContext") {          return (context: unknown) =>            wrap((target.withContext as (c: unknown) => FumaDb)(context));        }        if (prop === "transaction") {          return async (run: Parameters<FumaDb["transaction"]>[0]) => {            hooks.enter();            // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: test instrument must unwind on both outcomes            try {              return await target.transaction(run);            } finally {              hooks.exit();            }          };        }        return Reflect.get(target, prop);      },    });  return wrap(db);}; const ConnectionListHealthOutput = Schema.Struct({  connections: Schema.Array(Schema.Struct({ lastHealth: Schema.NullOr(HealthCheckResult) })),});const decodeConnectionListHealthOutput = Schema.decodeUnknownEffect(ConnectionListHealthOutput); 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");    }),  );   // Create is never a replace: a second create with the same (owner,  // integration, name) must fail with ConnectionAlreadyExistsError and leave  // the first connection fully intact — including its stored secret, which a  // silent upsert would overwrite (the pasted value's item id is derived from  // the name, so the provider write alone clobbers it).  it.effect("rejects a duplicate (owner, integration, name) and keeps the original intact", () =>    Effect.gen(function* () {      const executor = yield* setup();      yield* executor.connections.create({        owner: "org",        name: ConnectionName.make("main"),        integration: INTEG,        template: TEMPLATE,        value: "original-token",        description: "original",      });       const result = yield* Effect.result(        executor.connections.create({          owner: "org",          name: ConnectionName.make("main"),          integration: INTEG,          template: TEMPLATE,          value: "clobbered-token",          description: "clobbered",        }),      );      expect(Result.isFailure(result)).toBe(true);      if (!Result.isFailure(result)) return;      expect(Predicate.isTagged("ConnectionAlreadyExistsError")(result.failure)).toBe(true);       // The original row and its secret both survived.      const connections = yield* executor.connections.list();      expect(connections.length).toBe(1);      expect(connections[0]?.description).toBe("original");      const value = yield* executor.demo.resolveValue("org", "main");      expect(value).toBe("original-token");    }),  );   // Names collide AFTER identifier normalization: "my-api-key" and "my api key"  // both normalize to myApiKey, so the second must be rejected even though the  // raw inputs differ.  it.effect("rejects a duplicate that only collides after name normalization", () =>    Effect.gen(function* () {      const executor = yield* setup();      yield* executor.connections.create({        owner: "org",        name: ConnectionName.make("my-api-key"),        integration: INTEG,        template: TEMPLATE,        value: "v1",      });      const result = yield* Effect.result(        executor.connections.create({          owner: "org",          name: ConnectionName.make("my api key"),          integration: INTEG,          template: TEMPLATE,          value: "v2",        }),      );      expect(Result.isFailure(result)).toBe(true);      if (!Result.isFailure(result)) return;      expect(Predicate.isTagged("ConnectionAlreadyExistsError")(result.failure)).toBe(true);      const value = yield* executor.demo.resolveValue("org", "myApiKey");      expect(value).toBe("v1");    }),  );   // The race the early duplicate check cannot answer: two creates for the same  // (owner, integration, name) in flight at once. The row insert picks the  // winner and the loser gets the typed 409 — and, the load-bearing part, the  // provider write is winner-only. A pasted value's item id is deterministic,  // so a pre-insert write from the LOSING create would silently replace the  // winner's stored secret while the winner's row keeps resolving through it.  // The gate parks the first create inside its provider write, so the second  // create runs its full duplicate handling while the first is mid-flight.  it.effect("concurrent creates: one winner, a typed 409, and the winner's secret intact", () =>    Effect.scoped(      Effect.gen(function* () {        const firstWriteEntered = yield* Deferred.make<void>();        const releaseFirstWrite = yield* Deferred.make<void>();        const store = new Map<string, string>();        let writes = 0;        const gatedProvider: CredentialProvider = {          key: ProviderKey.make("memory"),          writable: true,          get: (id) => Effect.sync(() => store.get(String(id)) ?? null),          set: (id, value) =>            Effect.gen(function* () {              writes += 1;              if (writes === 1) {                yield* Deferred.succeed(firstWriteEntered, undefined);                yield* Deferred.await(releaseFirstWrite);              }              store.set(String(id), value);            }),        };        const gatedPlugin = definePlugin(() => ({          id: "gated" as const,          credentialProviders: [gatedProvider],          storage: () => ({}),          resolveTools: () =>            Effect.succeed({              tools: [{ name: ToolName.make("deploy"), description: "deploy" }],            }),          invokeTool: ({ toolRow }) => Effect.succeed({ ran: toolRow.name }),          extension: (ctx) => ({            seed: () =>              ctx.core.integrations.register({                slug: INTEG,                description: "Vercel",                config: {},              }),            resolveValue: (name: string) =>              ctx.connections.resolveValue({                owner: "org",                integration: INTEG,                name: ConnectionName.make(name),              }),          }),        }))();        const config = makeTestConfig({ plugins: [gatedPlugin] as const });        const executor = yield* createExecutor(config);        yield* executor.gated.seed();         const createWith = (value: string) =>          Effect.result(            executor.connections.create({              owner: "org",              name: ConnectionName.make("main"),              integration: INTEG,              template: TEMPLATE,              value,            }),          );         const firstFiber = yield* Effect.forkChild(createWith("first-value"));        yield* Deferred.await(firstWriteEntered);        const second = yield* createWith("second-value");        yield* Deferred.succeed(releaseFirstWrite, undefined);        const first = yield* Fiber.join(firstFiber);         const attempts = [          { result: first, value: "first-value" },          { result: second, value: "second-value" },        ];        const winners = attempts.filter((attempt) => Result.isSuccess(attempt.result));        const losers = attempts.filter((attempt) => Result.isFailure(attempt.result));        expect(winners).toHaveLength(1);        expect(losers).toHaveLength(1);        const loser = losers[0];        if (!loser || !Result.isFailure(loser.result)) return;        expect(loser.result.failure).toBeInstanceOf(ConnectionAlreadyExistsError);         // Exactly one connection survived, and it resolves to the WINNER's        // value — the losing create never reached the provider.        const connections = yield* executor.connections.list();        expect(connections).toHaveLength(1);        const value = yield* executor.gated.resolveValue("main");        expect(value).toBe(winners[0]?.value);      }),    ),  );   // When both creates observe absence, both reach the insert and the primary  // key breaks the tie — the loser must still get the typed 409, not a raw  // unique-constraint storage failure. The proxy blinds every connection-table  // read for the second create, so its early and transactional checks both  // miss the existing row and its insert genuinely collides in the database.  it.effect("maps a lost insert race to the typed 409, not a storage failure", () =>    Effect.gen(function* () {      let blind = false;      const blindfoldConnectionReads = (db: FumaDb): FumaDb => {        const wrap = (inner: FumaDb): FumaDb =>          new Proxy(inner, {            get(target, prop) {              if (prop === "withContext") {                return (context: unknown) =>                  wrap((target.withContext as (c: unknown) => FumaDb)(context));              }              if (prop === "transaction") {                return (run: (tx: FumaDb) => Promise<unknown>) =>                  (target.transaction as (r: (tx: FumaDb) => Promise<unknown>) => Promise<unknown>)(                    (tx) => run(wrap(tx)),                  );              }              if (prop === "findFirst") {                return (table: unknown, query: unknown) =>                  blind && table === "connection"                    ? Promise.resolve(null)                    : (target.findFirst as (t: unknown, q: unknown) => Promise<unknown>)(                        table,                        query,                      );              }              return Reflect.get(target, prop);            },          });        return wrap(db);      };       const config = makeTestConfig({ plugins: [demoPlugin] as const });      const executor = yield* createExecutor({        ...config,        db: blindfoldConnectionReads(config.db),      });      yield* executor.demo.seed();      yield* executor.connections.create({        owner: "org",        name: ConnectionName.make("main"),        integration: INTEG,        template: TEMPLATE,        value: "first-value",      });       blind = true;      const result = yield* Effect.result(        executor.connections.create({          owner: "org",          name: ConnectionName.make("main"),          integration: INTEG,          template: TEMPLATE,          value: "second-value",        }),      );      blind = false;       expect(Result.isFailure(result)).toBe(true);      if (!Result.isFailure(result)) return;      expect(result.failure).toBeInstanceOf(ConnectionAlreadyExistsError);       // The losing insert wrote nothing: the original secret is untouched.      const connections = yield* executor.connections.list();      expect(connections).toHaveLength(1);      const value = yield* executor.demo.resolveValue("org", "main");      expect(value).toBe("first-value");    }),  );   it.effect("allows the same name under a different owner", () =>    Effect.gen(function* () {      const executor = yield* setup();      yield* executor.connections.create({        owner: "org",        name: ConnectionName.make("main"),        integration: INTEG,        template: TEMPLATE,        value: "org-token",      });      const personal = yield* executor.connections.create({        owner: "user",        name: ConnectionName.make("main"),        integration: INTEG,        template: TEMPLATE,        value: "user-token",      });      expect(String(personal.address)).toBe("tools.vercel.user.main");      expect((yield* executor.connections.list()).length).toBe(2);    }),  );   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",

Lines 1–500 of 3312.