// ---------------------------------------------------------------------------// Boot-level proof for issue #1771: a local database holding a legacy INTEGER// `connection.expires_at` is unreadable, and the local boot sequence heals it.//// The migration body is unit-tested in the SDK. What this pins is the WIRING —// that `localDataMigrations` actually carries the entry, early enough that the// catalog reads which follow the ledger run see repaired rows. That wiring is// the part that recovers a user's install; a correct migration nobody runs// would leave the gateway just as empty.// --------------------------------------------------------------------------- import { afterEach, beforeEach, describe, expect, it } from "@effect/vitest";import { mkdtempSync, rmSync } from "node:fs";import { tmpdir } from "node:os";import { join } from "node:path";import { Effect } from "effect";import { withQueryContext } from "@executor-js/fumadb/query"; import { collectTables } from "@executor-js/api/server";import { runSqliteDataMigrations } from "@executor-js/sdk"; import { localDataMigrations } from "./data-migrations";import { createSqliteFumaDb } from "./sqlite-fumadb"; const TENANT = "executor-workspace-1771";const SUBJECT = "local";// Epoch millis, the shape an OAuth token expiry takes.const LEGACY_EXPIRES_AT = 1787321623456; let workDir: string; beforeEach(() => { workDir = mkdtempSync(join(tmpdir(), "executor-legacy-bigint-"));}); afterEach(() => { rmSync(workDir, { recursive: true, force: true });}); const openDb = (dbPath: string) => createSqliteFumaDb({ tables: collectTables(), namespace: "executor_local", path: dbPath, }); /** Write the row a pre-`bigint` build left behind: `expires_at` as a bare * integer literal, which SQLite keeps in the INTEGER storage class. */const seedLegacyConnection = async (dbPath: string): Promise<void> => { const sqlite = await openDb(dbPath); await sqlite.client.execute({ sql: `INSERT INTO connection (row_id, tenant, owner, subject, integration, name, template, provider, item_ids, expires_at, created_at, updated_at) VALUES ('c1', ?, 'user', ?, 'acme', 'default', 'oauth2', 'file', ?, ${LEGACY_EXPIRES_AT}, ?, ?)`, args: [ TENANT, SUBJECT, JSON.stringify({ token: "item_1" }), Math.floor(Date.now() / 1000), Math.floor(Date.now() / 1000), ], }); await sqlite.close();}; describe("local boot over a legacy bigint database", () => { it("cannot read the connection table before the migrations run", async () => { const dbPath = join(workDir, "data.db"); await seedLegacyConnection(dbPath); const sqlite = await openDb(dbPath); const scoped = withQueryContext(sqlite.db, { tenant: TENANT, subject: SUBJECT }); // The reported symptom: not a wrong value, a throw — so the gateway lost // every saved integration at once. await expect(scoped.findMany("connection", {})).rejects.toThrow(/type number/); await sqlite.close(); }); it("heals it through the local data-migration registry", async () => { const dbPath = join(workDir, "data.db"); await seedLegacyConnection(dbPath); const sqlite = await openDb(dbPath); const applied = await Effect.runPromise( runSqliteDataMigrations(sqlite.client, localDataMigrations), ); expect(applied).toContain("2026-08-28-bigint-storage-class"); const scoped = withQueryContext(sqlite.db, { tenant: TENANT, subject: SUBJECT }); const rows = await scoped.findMany("connection", {}); expect(rows.map((row) => [row.name, Number(row.expires_at)])).toEqual([ ["default", LEGACY_EXPIRES_AT], ]); await sqlite.close(); }); it("stays readable across a reboot", async () => { const dbPath = join(workDir, "data.db"); await seedLegacyConnection(dbPath); const first = await openDb(dbPath); await Effect.runPromise(runSqliteDataMigrations(first.client, localDataMigrations)); await first.close(); // Second boot: the entry is stamped, so it is skipped — the data must // already be in the shape the mapper reads. const second = await openDb(dbPath); const applied = await Effect.runPromise( runSqliteDataMigrations(second.client, localDataMigrations), ); expect(applied).not.toContain("2026-08-28-bigint-storage-class"); const scoped = withQueryContext(second.db, { tenant: TENANT, subject: SUBJECT }); const rows = await scoped.findMany("connection", {}); expect(rows.map((row) => Number(row.expires_at))).toEqual([LEGACY_EXPIRES_AT]); await second.close(); });});
// ---------------------------------------------------------------------------
// Boot-level proof for issue #1771: a local database holding a legacy INTEGER
// `connection.expires_at` is unreadable, and the local boot sequence heals it.
//
// The migration body is unit-tested in the SDK. What this pins is the WIRING —
// that `localDataMigrations` actually carries the entry, early enough that the
// catalog reads which follow the ledger run see repaired rows. That wiring is
// the part that recovers a user's install; a correct migration nobody runs
// would leave the gateway just as empty.
// ---------------------------------------------------------------------------
import { afterEach, beforeEach, describe, expect, it } from "@effect/vitest";
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { Effect } from "effect";
import { withQueryContext } from "@executor-js/fumadb/query";
import { collectTables } from "@executor-js/api/server";
import { runSqliteDataMigrations } from "@executor-js/sdk";
import { localDataMigrations } from "./data-migrations";
import { createSqliteFumaDb } from "./sqlite-fumadb";
const TENANT = "executor-workspace-1771";
const SUBJECT = "local";
// Epoch millis, the shape an OAuth token expiry takes.
const LEGACY_EXPIRES_AT = 1787321623456;
let workDir: string;
beforeEach(() => {
workDir = mkdtempSync(join(tmpdir(), "executor-legacy-bigint-"));
});
afterEach(() => {
rmSync(workDir, { recursive: true, force: true });
});
const openDb = (dbPath: string) =>
createSqliteFumaDb({
tables: collectTables(),
namespace: "executor_local",
path: dbPath,
});
/** Write the row a pre-`bigint` build left behind: `expires_at` as a bare
* integer literal, which SQLite keeps in the INTEGER storage class. */
const seedLegacyConnection = async (dbPath: string): Promise<void> => {
const sqlite = await openDb(dbPath);
await sqlite.client.execute({
sql: `INSERT INTO connection
(row_id, tenant, owner, subject, integration, name, template, provider, item_ids,
expires_at, created_at, updated_at)
VALUES ('c1', ?, 'user', ?, 'acme', 'default', 'oauth2', 'file', ?,
${LEGACY_EXPIRES_AT}, ?, ?)`,
args: [
TENANT,
SUBJECT,
JSON.stringify({ token: "item_1" }),
Math.floor(Date.now() / 1000),
Math.floor(Date.now() / 1000),
],
});
await sqlite.close();
};
describe("local boot over a legacy bigint database", () => {
it("cannot read the connection table before the migrations run", async () => {
const dbPath = join(workDir, "data.db");
await seedLegacyConnection(dbPath);
const sqlite = await openDb(dbPath);
const scoped = withQueryContext(sqlite.db, { tenant: TENANT, subject: SUBJECT });
// The reported symptom: not a wrong value, a throw — so the gateway lost
// every saved integration at once.
await expect(scoped.findMany("connection", {})).rejects.toThrow(/type number/);
await sqlite.close();
});
it("heals it through the local data-migration registry", async () => {
const dbPath = join(workDir, "data.db");
await seedLegacyConnection(dbPath);
const sqlite = await openDb(dbPath);
const applied = await Effect.runPromise(
runSqliteDataMigrations(sqlite.client, localDataMigrations),
);
expect(applied).toContain("2026-08-28-bigint-storage-class");
const scoped = withQueryContext(sqlite.db, { tenant: TENANT, subject: SUBJECT });
const rows = await scoped.findMany("connection", {});
expect(rows.map((row) => [row.name, Number(row.expires_at)])).toEqual([
["default", LEGACY_EXPIRES_AT],
]);
await sqlite.close();
});
it("stays readable across a reboot", async () => {
const dbPath = join(workDir, "data.db");
await seedLegacyConnection(dbPath);
const first = await openDb(dbPath);
await Effect.runPromise(runSqliteDataMigrations(first.client, localDataMigrations));
await first.close();
// Second boot: the entry is stamped, so it is skipped — the data must
// already be in the shape the mapper reads.
const second = await openDb(dbPath);
const applied = await Effect.runPromise(
runSqliteDataMigrations(second.client, localDataMigrations),
);
expect(applied).not.toContain("2026-08-28-bigint-storage-class");
const scoped = withQueryContext(second.db, { tenant: TENANT, subject: SUBJECT });
const rows = await scoped.findMany("connection", {});
expect(rows.map((row) => Number(row.expires_at))).toEqual([LEGACY_EXPIRES_AT]);
await second.close();
});
});