885 lines · 38.9 KB · TypeScript
RawDownload
import { describe, expect, it } from "@effect/vitest";import { Duration, Effect, Fiber, Latch } from "effect"; import { CONNECTION_CATALOG_SCAN_COLUMNS } from "./core-schema";import { DEFAULT_TOOLS_SYNC_TTL_MS, createExecutor } from "./executor";import type { FumaDb } from "./fuma-runtime";import { AuthTemplateSlug, ConnectionName, IntegrationSlug, ToolName } from "./ids";import { definePlugin } from "./plugin";import { makeTestConfig, memoryCredentialsPlugin } from "./testing";import type { ToolSyncErrorKind } from "./tool-sync-schedule"; // A tools READ refreshes stale catalogs before it answers, and each refresh is// a live upstream handshake. These cases pin the three properties that keep// that affordable and safe: the refresh sees only connections inside the read's// own filter, in steady state it sees nothing at all, and a wide refresh runs// its handshakes concurrently WITHOUT letting the persists interleave. const INTEG_A = IntegrationSlug.make("alpha");const INTEG_B = IntegrationSlug.make("beta");const TEMPLATE = AuthTemplateSlug.make("apiKey"); /** How long any latch in this file is allowed to hold a fiber. Far longer than *  a passing run needs (the whole suite is milliseconds), and only ever reached *  when the overlap a case is establishing never happened — a sequential *  refresh fan-out, or a claim compare-and-set that stopped excluding. Reaching *  it lets the fiber through so the case fails on its own assertion instead of *  hanging the suite into an opaque timeout with no diff. */const LATCH_TIMEOUT_MS = 5000; const withLatchTimeout = <A, E, R>(effect: Effect.Effect<A, E, R>) =>  effect.pipe(    Effect.timeoutOrElse({      duration: Duration.millis(LATCH_TIMEOUT_MS),      orElse: () => Effect.succeed(undefined),    }),  ); /** A plugin owning two integrations that records every `resolveTools` it is *  asked for, as `<integration>/<connection>`. The recorded list IS the cost *  a read pays upstream, so most assertions below are about its contents. * *  `holdResolvesUntil` turns "the handshakes overlapped" from a race the test *  hopes for into one it establishes: every resolve blocks on a latch that *  only opens once N of them are in flight together. Left disarmed during *  setup, where connections are created one at a time and a latch waiting for *  a second resolve would simply hang. */const makeCountingPlugin = () => {  const resolved: string[] = [];  let dying = false;  let toolSuffix = "deploy";  let holdUntil: number | null = null;  let inFlight = 0;  let peakInFlight = 0;  let incompleteKind: ToolSyncErrorKind | null | undefined;  const gate = Latch.makeUnsafe(true);  const entered = Latch.makeUnsafe(false);   const plugin = definePlugin(() => ({    id: "counting" as const,    storage: () => ({}),    // Stands in for an MCP server: a catalog only the upstream knows, so the    // freshness TTL can expire it. Without this the `expired` state is    // unreachable from these tests, and `expired` is the one state the park    // gates.    remoteToolCatalog: true,    resolveTools: (input) =>      Effect.gen(function* () {        const slug = String(input.connection.integration);        resolved.push(`${slug}/${String(input.connection.name)}`);        inFlight += 1;        peakInFlight = Math.max(peakInFlight, inFlight);        entered.openUnsafe();        if (holdUntil !== null && inFlight >= holdUntil) gate.openUnsafe();        yield* withLatchTimeout(gate.await);        inFlight -= 1;        if (dying) return yield* Effect.die("resolveTools blew up");        if (incompleteKind !== undefined) {          return {            tools: [],            incomplete: true,            incompleteReason: "the fixture upstream refused",            ...(incompleteKind === null ? {} : { incompleteKind }),          };        }        return {          tools: [{ name: ToolName.make(`${slug}_${toolSuffix}`), description: toolSuffix }],        };      }),    invokeTool: ({ toolRow }) => Effect.succeed({ ran: toolRow.name }),    extension: (ctx) => ({      seed: () =>        Effect.all([          ctx.core.integrations.register({ slug: INTEG_A, description: "Alpha", config: {} }),          ctx.core.integrations.register({ slug: INTEG_B, description: "Beta", config: {} }),        ]),      /** The `config_revised` trigger as a plugin actually fires it. The public       *  `integrations.update` accepts only name/description; `config` — and so       *  the `config_revised_at` stamp — is on the plugin-facing surface. */      revise: (slug: IntegrationSlug) =>        ctx.core.integrations.update(slug, { config: { revision: 2 } }),      /** The `stale_marked` trigger as a plugin actually fires it (an MCP       *  `tools/list_changed`, an unknown-tool rejection). */      markStale: (slug: IntegrationSlug, name: string) =>        ctx.connections.markToolsStale({          owner: "org",          integration: slug,          name: ConnectionName.make(name),        }),    }),  }))();   return {    plugin,    resolved,    startDying: () => void (dying = true),    /** Change what the next resolve returns, so a rebuild that never landed is     *  distinguishable from one that did. Without this every refresh rewrites     *  the identical row and a persist that silently failed still looks     *  correct — the catalog it left behind is the one it would have written. */    renameToolsTo: (suffix: string) => void (toolSuffix = suffix),    holdResolvesUntil: (count: number) => {      holdUntil = count;      gate.closeUnsafe();    },    peakConcurrentResolves: () => peakInFlight,    /** Make every resolve report a non-authoritative listing. `null` leaves it     *  unclassified (a plugin that knows it failed but not why); a kind is what     *  core parks or backs off on. `undefined` restores real listings. */    resolveIncompleteAs: (kind: ToolSyncErrorKind | null | undefined) => {      incompleteKind = kind;    },    /** Block every resolve until {@link releaseResolves}, so a second read can     *  be driven all the way through while the first is mid-handshake. */    blockResolves: () => {      entered.closeUnsafe();      gate.closeUnsafe();    },    releaseResolves: () => gate.openUnsafe(),    /** Resolves when a resolve has actually STARTED — the claim is taken by     *  then, which is the state the contention cases need to observe. */    awaitResolveStarted: withLatchTimeout(entered.await),  };}; /** Wrap a query object so every `connection` read reports its row count AND its *  column projection, and so overlapping transactions are visible. * *  The refresh's scan is the query this change narrows: "returned zero rows" is *  what separates a narrowed scan from one that over-fetches and then discards, *  and the recorded `select` is what stops the projection from being deleted *  while the row-count assertions stay green. * *  `transaction` is wrapped too — the wrapped orm is forwarded into the run *  callback so reads issued inside a transaction are counted, and the open *  count is what proves catalog persists do not interleave. `internal` is *  deliberately out of scope: it is the test harness's own lazy-open escape *  hatch, so wrapping it would count harness traffic rather than the *  executor's. `withContext` is forwarded so the executor's owner-policy *  binding keeps using the wrapper (as FumaDB requires). */const observeConnectionReads = (db: FumaDb) => {  const connectionScans: { readonly rows: number; readonly select: unknown }[] = [];  let openTransactions = 0;  let peakOpenTransactions = 0;   const wrap = (inner: FumaDb): FumaDb =>    new Proxy(inner, {      get(target, prop, receiver) {        const value: unknown = Reflect.get(target, prop, receiver);        if (typeof value !== "function") return value;         // Cast to the interface's OWN member types, never to a hand-written        // mirror: the recorder's contract is with `AbstractQuery`, and a        // re-declared signature is one that silently stops matching it.        if (prop === "withContext") {          const withContext = value as NonNullable<FumaDb["withContext"]>;          return (context: unknown) => wrap(Reflect.apply(withContext, inner, [context]));        }         if (prop === "transaction") {          const transaction = value as FumaDb["transaction"];          return (run: (orm: FumaDb) => Promise<unknown>) => {            openTransactions += 1;            peakOpenTransactions = Math.max(peakOpenTransactions, openTransactions);            return Reflect.apply(transaction, inner, [(orm: FumaDb) => run(wrap(orm))]).finally(              () => {                openTransactions -= 1;              },            );          };        }         if (prop !== "findMany") return value;        const findMany = value as FumaDb["findMany"];        return async (table: string, options?: { readonly select?: unknown }) => {          const rows = await Reflect.apply(findMany, inner, [table, options]);          if (table === "connection") {            connectionScans.push({ rows: rows.length, select: options?.select });          }          return rows;        };      },    });   return {    db: wrap(db),    connectionScans,    peakOpenTransactions: () => peakOpenTransactions,  };}; /** The projection the catalog-refresh scan is expected to ask for, written out *  rather than re-exported from the production constant: comparing the *  implementation to itself would catch only the `select` option being deleted, *  never the failure the constant's own docstring names — someone adding *  `value`, `oauth_state` or `last_health` to it so the every-read scan starts *  pulling credential and health JSON. */const SCAN_PROJECTION = [  "owner",  "integration",  "name",  "tools_synced_at",  "tools_stale_token",  "tools_sync_claim_id",  "tools_sync_claim_at",  "tools_sync_failures",  "tools_sync_retry_at",  "tools_sync_error_kind",]; describe("connection catalog scan projection", () => {  it("is the address plus the sync lifecycle, and nothing else", () => {    expect([...CONNECTION_CATALOG_SCAN_COLUMNS]).toEqual(SCAN_PROJECTION);  });}); const makeHarness = () =>  Effect.acquireRelease(    Effect.gen(function* () {      const counting = makeCountingPlugin();      const config = makeTestConfig({        plugins: [memoryCredentialsPlugin(), counting.plugin] as const,      });      const observed = observeConnectionReads(config.db);      const executor = yield* createExecutor({ ...config, db: observed.db });      yield* executor.counting.seed();      return {        ...counting,        executor,        testDb: config.testDb,        connectionScans: observed.connectionScans,        peakOpenTransactions: observed.peakOpenTransactions,        connect: (integration: IntegrationSlug, name: string) =>          executor.connections.create({            owner: "org",            name: ConnectionName.make(name),            integration,            template: TEMPLATE,            value: "secret-token",          }),        /** Clear every connection's catalog stamp, which is the `cold` trigger:         *  a row with no stamp and no drift mark has never synced. NOT the         *  drift trigger — `connections.markToolsStale` leaves `tools_synced_at`         *  alone and writes `tools_stale_token`, which is what         *  `executor.counting.markStale` drives. Used by the cases that only         *  need a connection to be due, whatever made it due. */        clearEveryCatalogStamp: () =>          Effect.promise(() =>            observed.db.updateMany("connection", {              where: (b) => b.isNotNull("tools_synced_at"),              set: { tools_synced_at: null },            }),          ),        /** Push every catalog past the freshness window, which is the `expired`         *  trigger: a connection that HAS a working catalog and is only being         *  asked to re-verify it. Distinct from `cold` in exactly the way the         *  park cares about, so the two cannot share a helper. */        expireEveryCatalog: () =>          Effect.promise(() =>            observed.db.updateMany("connection", {              where: (b) => b.isNotNull("tools_synced_at"),              set: { tools_synced_at: Date.now() - DEFAULT_TOOLS_SYNC_TTL_MS - 1 },            }),          ),        /** Push every catalog stamp into the past. The `config_revised` trigger         *  compares `tools_synced_at` against a stamp taken later in the same         *  test; on an in-memory database both can land in one millisecond and         *  the comparison is strict, so the ordering is made explicit here         *  rather than left to the clock. */        backdateEveryCatalog: () =>          Effect.promise(() =>            observed.db.updateMany("connection", {              where: (b) => b.isNotNull("tools_synced_at"),              set: { tools_synced_at: Date.now() - 60_000 },            }),          ),        /** The sync lifecycle columns, as persisted. Read straight from the         *  database rather than through a projection, because "what did the         *  failed refresh actually write" is the whole question. */        syncStateOf: (name: string) =>          Effect.promise(() =>            observed.db.findFirst("connection", { where: (b) => b("name", "=", name) }),          ).pipe(            Effect.flatMap((row) =>              // A missing row is a defect, not a state. Projecting it into a              // populated "clean" record is what would let `syncedAt` and              // `claimId` assert null against an implementation that DELETED              // the connection rather than one that correctly declined to              // stamp it.              row == null                ? Effect.die(`no connection row named ${name}`)                : Effect.succeed({                    syncedAt: row.tools_synced_at == null ? null : Number(row.tools_synced_at),                    staleToken:                      row.tools_stale_token == null ? null : String(row.tools_stale_token),                    failures: row.tools_sync_failures == null ? 0 : Number(row.tools_sync_failures),                    retryAt:                      row.tools_sync_retry_at == null ? null : Number(row.tools_sync_retry_at),                    errorKind: row.tools_sync_error_kind ?? null,                    claimId: row.tools_sync_claim_id ?? null,                  }),            ),          ),        /** Re-claim a connection out from under whoever holds it, as a second         *  Workers isolate does when it observes an expired lease. There is no         *  in-process seam for this — the row IS the coordination medium. */        stealClaim: (name: string) =>          Effect.promise(() =>            observed.db.updateMany("connection", {              where: (b) => b("name", "=", name),              set: { tools_sync_claim_id: "another-isolate", tools_sync_claim_at: Date.now() },            }),          ),        /** Move every backoff window into the past. The ladder's first rung is         *  the freshness TTL (15 minutes by default), so the alternative is         *  sleeping through it. */        elapseEveryBackoff: () =>          Effect.promise(() =>            observed.db.updateMany("connection", {              where: (b) => b.isNotNull("tools_sync_retry_at"),              set: { tools_sync_retry_at: Date.now() - 1 },            }),          ),      };    }),    ({ executor, testDb }) =>      executor        .close()        .pipe(          Effect.ignore,          Effect.andThen(Effect.promise(() => testDb.close()).pipe(Effect.ignore)),        ),  ); describe("tools read catalog refresh scope", () => {  it.effect("refreshes only the integration the read filters to", () =>    Effect.scoped(      Effect.gen(function* () {        const harness = yield* makeHarness();        yield* harness.connect(INTEG_A, "main");        yield* harness.connect(INTEG_B, "main");        yield* harness.clearEveryCatalogStamp();        harness.resolved.length = 0;         const alpha = yield* harness.executor.tools.list({ integration: INTEG_A });         expect(harness.resolved).toEqual(["alpha/main"]);        expect(alpha.map((tool) => String(tool.name))).toEqual(["alpha_deploy"]);         const beta = yield* harness.executor.tools.list({ integration: INTEG_B });         expect(harness.resolved).toEqual(["alpha/main", "beta/main"]);        expect(beta.map((tool) => String(tool.name))).toEqual(["beta_deploy"]);      }),    ),  );   it.effect("scans no connection rows once every catalog is fresh", () =>    Effect.scoped(      Effect.gen(function* () {        const harness = yield* makeHarness();        yield* harness.connect(INTEG_A, "main");        yield* harness.connect(INTEG_B, "main");        harness.resolved.length = 0;        harness.connectionScans.length = 0;         const tools = yield* harness.executor.tools.list();         expect(tools.map((tool) => String(tool.name)).sort()).toEqual([          "alpha_deploy",          "beta_deploy",        ]);        // One scan, and it matched nothing: the steady-state read pays an        // indexed lookup and no upstream call at all.        expect(harness.connectionScans).toEqual([{ rows: 0, select: SCAN_PROJECTION }]);        expect(harness.resolved).toEqual([]);      }),    ),  );   it.effect("narrows the refresh by owner and connection too", () =>    Effect.scoped(      Effect.gen(function* () {        const harness = yield* makeHarness();        yield* harness.connect(INTEG_A, "main");        yield* harness.connect(INTEG_A, "second");        yield* harness.connect(INTEG_B, "main");        yield* harness.clearEveryCatalogStamp();        harness.resolved.length = 0;        harness.connectionScans.length = 0;         const tools = yield* harness.executor.tools.list({          integration: INTEG_A,          owner: "org",          connection: ConnectionName.make("second"),        });         expect(harness.resolved).toEqual(["alpha/second"]);        // One row, and only the columns the refresh actually reads — the        // connection's address plus its sync lifecycle. The scan runs on every        // read, so the projection is part of its cost.        expect(harness.connectionScans).toEqual([{ rows: 1, select: SCAN_PROJECTION }]);        expect(tools.map((tool) => String(tool.name))).toEqual(["alpha_deploy"]);      }),    ),  );   it.effect("re-resolves only the integration whose config was revised", () =>    Effect.scoped(      Effect.gen(function* () {        const harness = yield* makeHarness();        yield* harness.connect(INTEG_A, "main");        yield* harness.connect(INTEG_B, "main");        yield* harness.backdateEveryCatalog();        yield* harness.executor.counting.revise(INTEG_A);        harness.resolved.length = 0;         yield* harness.executor.tools.list({ integration: INTEG_A });         expect(harness.resolved).toEqual(["alpha/main"]);         harness.resolved.length = 0;         yield* harness.executor.tools.list({ integration: INTEG_B });         // Beta's catalog is older than alpha's revision stamp, and under one        // global `staleBefore` that alone made it a candidate. The trigger is        // per-integration: only the revised integration re-lists.        expect(harness.resolved).toEqual([]);      }),    ),  );   it.effect("survives a plugin whose resolveTools dies with a defect", () =>    Effect.scoped(      Effect.gen(function* () {        const harness = yield* makeHarness();        yield* harness.connect(INTEG_A, "main");        yield* harness.clearEveryCatalogStamp();        harness.startDying();        harness.resolved.length = 0;         const tools = yield* harness.executor.tools.list({ integration: INTEG_A });         expect(harness.resolved).toEqual(["alpha/main"]);        // The refresh died; the read still answers from the stale-but-working        // catalog rather than failing or returning an empty list.        expect(tools.map((tool) => String(tool.name))).toEqual(["alpha_deploy"]);         const retried = yield* harness.executor.tools.list({ integration: INTEG_A });         // A failed refresh stamps nothing, so the connection stays stale and        // the NEXT read attempts it again — best-effort means retried, not        // abandoned.        expect(harness.resolved).toEqual(["alpha/main", "alpha/main"]);        expect(retried.map((tool) => String(tool.name))).toEqual(["alpha_deploy"]);      }),    ),  );   it.effect("persists every catalog when one read refreshes many connections", () =>    Effect.scoped(      Effect.gen(function* () {        const harness = yield* makeHarness();        const names = ["one", "two", "three", "four", "five"];        for (const name of names) yield* harness.connect(INTEG_A, name);        // Make the rebuild observably different from what `connect` already        // persisted, so a refresh whose write never landed is visible.        harness.renameToolsTo("redeploy");        yield* harness.clearEveryCatalogStamp();        harness.resolved.length = 0;        // Arm the overlap latch only now: `connect` resolves one connection at        // a time, and a latch waiting for a second in-flight resolve would        // never open during setup.        harness.holdResolvesUntil(2);         const tools = yield* harness.executor.tools.list({ integration: INTEG_A });         expect(harness.resolved.slice().sort()).toEqual(names.map((n) => `alpha/${n}`).sort());        // Every connection's rebuild landed. fumadb runs a SQLite transaction        // as raw BEGIN/COMMIT on the shared connection, so a second persist

Lines 1–500 of 885.