337 lines · 11.8 KB · TypeScript
RawDownload
import { describe, expect, it } from "@effect/vitest";import { Effect, Schema } from "effect";import {  collectTables,  DataMigrationError,  runSqliteDataMigrations,  type SqliteDataMigrationClient,} from "@executor-js/sdk";import { createSqliteTestFumaDb } from "@executor-js/sdk/testing"; import { operationStorageKey } from "./planner";import {  providerServiceSplitDataMigration,  runSqliteProviderServiceSplitMigration,} from "./sqlite"; const now = 1_780_000_000_000;const parseJson = Schema.decodeUnknownSync(Schema.fromJsonString(Schema.Unknown)); const insertIntegration = (  client: SqliteDataMigrationClient,  row: {    readonly rowId: string;    readonly tenant: string;    readonly slug: string;    readonly pluginId: string;    readonly config: unknown;    readonly healthCheck?: unknown;  },) =>  client.execute({    sql: `INSERT INTO integration      (row_id, tenant, slug, plugin_id, name, description, config, health_check,       can_remove, can_refresh, created_at, updated_at)      VALUES (?, ?, ?, ?, ?, ?, ?, ?, 1, 1, ?, ?)`,    args: [      row.rowId,      row.tenant,      row.slug,      row.pluginId,      row.slug,      row.slug,      JSON.stringify(row.config),      row.healthCheck ? JSON.stringify(row.healthCheck) : null,      now,      now,    ],  }); const insertConnection = (client: SqliteDataMigrationClient, tenant = "org_1") =>  client.execute({    sql: `INSERT INTO connection      (integration, name, template, provider, item_ids, identity_label, description,       last_health, tools_synced_at, oauth_client, oauth_client_owner, refresh_item_id,       expires_at, oauth_scope, oauth_token_url, provider_state, created_at, updated_at,       row_id, tenant, owner, subject)      VALUES (?, ?, ?, ?, ?, ?, NULL, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,    args: [      "google",      "main",      "googleOAuth",      "vault",      JSON.stringify({ token: "access_item", refresh: "refresh_item" }),      "person@example.test",      now,      "google",      "org",      "refresh_item",      now + 60_000,      "calendar gmail",      "https://oauth2.googleapis.com/token",      JSON.stringify({ token: "metadata" }),      now,      now,      `conn_google_${tenant}`,      tenant,      "org",      "",    ],  }); const insertTool = (client: SqliteDataMigrationClient, name: string, tenant = "org_1") =>  client.execute({    sql: `INSERT INTO tool      (tenant, owner, subject, integration, connection, plugin_id, name, description,       input_schema, output_schema, annotations, created_at, updated_at, row_id)      VALUES (?, 'org', '', 'google', 'main', 'google', ?, 'tool', '{}', '{}', '{}', ?, ?, ?)`,    args: [tenant, name, now, now, `tool_${tenant}_${name}`],  }); const insertOperation = (client: SqliteDataMigrationClient, name: string, tenant = "org_1") =>  client.execute({    sql: `INSERT INTO plugin_storage      (tenant, owner, subject, plugin_id, collection, key, data, created_at, updated_at, row_id)      VALUES (?, 'org', '', 'google', 'operation', ?, ?, ?, ?, ?)`,    args: [      tenant,      operationStorageKey("google", name),      JSON.stringify({        integration: "google",        toolName: name,        binding: { method: "get", pathTemplate: `/${name}`, parameters: [] },        description: name,      }),      now,      now,      `op_${tenant}_${name}`,    ],  }); const insertBlob = (client: SqliteDataMigrationClient, key: string, tenant = "org_1") =>  client.execute({    sql: "INSERT INTO blob (namespace, key, value, row_id, id) VALUES (?, ?, ?, ?, ?)",    args: [      `o:${tenant}/google`,      key,      "blob",      `blob_${tenant}_${key}`,      JSON.stringify([`o:${tenant}/google`, key]),    ],  }); const insertPolicy = (client: SqliteDataMigrationClient) =>  client.execute({    sql: `INSERT INTO tool_policy      (id, pattern, action, position, created_at, updated_at, row_id, tenant, owner, subject)      VALUES (?, ?, 'block', 'a0', ?, ?, ?, 'org_1', 'org', '')`,    args: ["policy_orphan", "google.*.*.gmail.users.messages.send", now, now, "policy_orphan_row"],  }); const seedCalendarOrg = (  client: SqliteDataMigrationClient,  tenant = "org_1",  options: { readonly includeBlobs?: boolean } = { includeBlobs: true },) =>  Effect.gen(function* () {    yield* Effect.promise(() =>      insertIntegration(client, {        rowId: `google_row_${tenant}`,        tenant,        slug: "google",        pluginId: "google",        config: {          googleDiscoveryUrls: [            "https://www.googleapis.com/discovery/v1/apis/calendar/v3/rest",            "https://www.googleapis.com/discovery/v1/apis/oauth2/v2/rest",          ],          specHash: "mono-hash",        },        healthCheck: { operation: "oauth2.userinfo.get" },      }),    );    yield* Effect.promise(() => insertConnection(client, tenant));    yield* Effect.promise(() => insertTool(client, "calendar.events.list", tenant));    yield* Effect.promise(() => insertOperation(client, "calendar.events.list", tenant));    yield* Effect.promise(() => insertOperation(client, "oauth2.userinfo.get", tenant));    if (options.includeBlobs !== false) {      yield* Effect.promise(() => insertBlob(client, "spec/mono-hash", tenant));      yield* Effect.promise(() => insertBlob(client, "defs/mono-hash", tenant));    }  }); describe("providerServiceSplitDataMigration", () => {  it.effect("splits libSQL monolith rows into openapi service rows and stamps once", () =>    Effect.gen(function* () {      const db = yield* Effect.promise(() => createSqliteTestFumaDb({ tables: collectTables() }));      const client = db.client;       yield* Effect.promise(() =>        insertIntegration(client, {          rowId: "google_row",          tenant: "org_1",          slug: "google",          pluginId: "google",          config: {            googleDiscoveryUrls: [              "https://www.googleapis.com/discovery/v1/apis/calendar/v3/rest",              "https://www.googleapis.com/discovery/v1/apis/oauth2/v2/rest",            ],            specHash: "mono-hash",          },          healthCheck: { operation: "oauth2.userinfo.get" },        }),      );      yield* Effect.promise(() => insertConnection(client));      yield* Effect.promise(() => insertTool(client, "calendar.events.list"));      yield* Effect.promise(() => insertOperation(client, "calendar.events.list"));      yield* Effect.promise(() => insertOperation(client, "oauth2.userinfo.get"));      yield* Effect.promise(() => insertBlob(client, "spec/mono-hash"));      yield* Effect.promise(() => insertBlob(client, "defs/mono-hash"));      yield* Effect.promise(() => insertPolicy(client));       expect(yield* runSqliteDataMigrations(client, [providerServiceSplitDataMigration])).toEqual([        "2026-07-08-provider-service-split",      ]);      expect(yield* runSqliteDataMigrations(client, [providerServiceSplitDataMigration])).toEqual(        [],      );       const integrations = yield* Effect.promise(() =>        client.execute(          "SELECT slug, plugin_id, config, health_check FROM integration ORDER BY slug",        ),      );      expect(integrations.rows).toHaveLength(1);      expect(integrations.rows[0]?.slug).toBe("google_calendar");      expect(integrations.rows[0]?.plugin_id).toBe("openapi");      expect(parseJson(String(integrations.rows[0]?.config))).toMatchObject({        specHash: "mono-hash",        specUrl: "https://www.googleapis.com/discovery/v1/apis/calendar/v3/rest",        specFormat: "google-discovery",        family: "google",      });      expect(parseJson(String(integrations.rows[0]?.health_check))).toEqual({        operation: "calendar.calendarList.list",      });       const connections = yield* Effect.promise(() =>        client.execute("SELECT integration, item_ids, oauth_client FROM connection"),      );      expect(connections.rows).toEqual([        {          integration: "google_calendar",          item_ids: JSON.stringify({            token: "access_item",            refresh: "refresh_item",          }),          oauth_client: "google",        },      ]);       const storage = yield* Effect.promise(() =>        client.execute(          "SELECT plugin_id, key, data FROM plugin_storage WHERE collection = 'operation'",        ),      );      expect(storage.rows).toHaveLength(1);      expect(storage.rows[0]?.plugin_id).toBe("openapi");      expect(storage.rows[0]?.key).toBe(        operationStorageKey("google_calendar", "calendar.events.list"),      );      expect(parseJson(String(storage.rows[0]?.data))).toMatchObject({        integration: "google_calendar",        toolName: "calendar.events.list",      });       const tools = yield* Effect.promise(() =>        client.execute("SELECT integration, plugin_id, name FROM tool"),      );      expect(tools.rows).toEqual([        {          integration: "google_calendar",          plugin_id: "openapi",          name: "calendar.events.list",        },      ]);       const policies = yield* Effect.promise(() =>        client.execute("SELECT id, pattern, action FROM tool_policy"),      );      expect(policies.rows).toEqual([        {          id: "policy_orphan",          pattern: "google_calendar.*.*.gmail.users.messages.send",          action: "block",        },      ]);       yield* Effect.promise(() => db.close());    }),  );   it.effect("skips an org intact when a source blob is missing", () =>    Effect.gen(function* () {      const db = yield* Effect.promise(() => createSqliteTestFumaDb({ tables: collectTables() }));      const client = db.client;       yield* seedCalendarOrg(client, "org_1", { includeBlobs: false });       expect(yield* runSqliteProviderServiceSplitMigration(client)).toBe(0);       const integrations = yield* Effect.promise(() =>        client.execute("SELECT slug, plugin_id FROM integration ORDER BY slug"),      );      expect(integrations.rows).toEqual([{ slug: "google", plugin_id: "google" }]);       yield* Effect.promise(() => db.close());    }),  );   it.effect("uses the per-org ledger to recover after a mid-run crash", () =>    Effect.gen(function* () {      const db = yield* Effect.promise(() => createSqliteTestFumaDb({ tables: collectTables() }));      const client = db.client;       yield* seedCalendarOrg(client, "org_1");      yield* seedCalendarOrg(client, "org_2");       const failed = yield* Effect.flip(        runSqliteProviderServiceSplitMigration(client, {          beforeStampOrg: (org) =>            org.tenant === "org_2"              ? Effect.fail(                  new DataMigrationError({                    migration: "2026-07-08-provider-service-split",                    cause: "crash before org_2 stamp",                  }),                )              : Effect.void,        }),      );      expect(failed).toBeInstanceOf(DataMigrationError);       const ledgerAfterCrash = yield* Effect.promise(() =>        client.execute("SELECT tenant FROM provider_service_split_org_migration ORDER BY tenant"),      );      expect(ledgerAfterCrash.rows).toEqual([{ tenant: "org_1" }]);       expect(yield* runSqliteProviderServiceSplitMigration(client)).toBe(1);       const ledgerAfterRerun = yield* Effect.promise(() =>        client.execute("SELECT tenant FROM provider_service_split_org_migration ORDER BY tenant"),      );      expect(ledgerAfterRerun.rows).toEqual([{ tenant: "org_1" }, { tenant: "org_2" }]);       const integrations = yield* Effect.promise(() =>        client.execute("SELECT tenant, slug, plugin_id FROM integration ORDER BY tenant, slug"),      );      expect(integrations.rows).toEqual([        { tenant: "org_1", slug: "google_calendar", plugin_id: "openapi" },        { tenant: "org_2", slug: "google_calendar", plugin_id: "openapi" },      ]);       yield* Effect.promise(() => db.close());    }),  );});