From a67854da0d08f697bcbd38c57f722e1c9e3ea327 Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Sun, 30 Aug 2026 20:57:57 -0700 Subject: [PATCH 1/5] Add tests for dbGate, the shared DB-skip gate Covers: a configured DATABASE_URL returns describe; an absent one returns describe.skip; E2E_REQUIRED=1 turns the skip into a throw naming the suite, and stops doing so once a database is configured. --- scripts/e2e/db-gate.test.ts | 44 +++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 scripts/e2e/db-gate.test.ts diff --git a/scripts/e2e/db-gate.test.ts b/scripts/e2e/db-gate.test.ts new file mode 100644 index 000000000..3b698113a --- /dev/null +++ b/scripts/e2e/db-gate.test.ts @@ -0,0 +1,44 @@ +// Unit coverage for dbGate: a missing database must never skip in +// total silence. Without E2E_REQUIRED it returns describe.skip (the +// suite still skips quietly in local dev); with E2E_REQUIRED=1 (this +// repo's convention, see harness.ts's e2eDatabaseUrl) it throws +// instead, so CI can never report green on a suite that never ran. + +import { afterEach, describe, expect, test } from "bun:test"; +import { dbGate } from "./db-gate.ts"; + +const saved = process.env["E2E_REQUIRED"]; + +afterEach(() => { + if (saved === undefined) delete process.env["E2E_REQUIRED"]; + else process.env["E2E_REQUIRED"] = saved; +}); + +describe("dbGate", () => { + test("returns describe when a database URL is configured", () => { + delete process.env["E2E_REQUIRED"]; + expect(dbGate("postgres://localhost:5432/workbench", "example")).toBe( + describe, + ); + }); + + test("returns describe.skip when no database is configured and E2E_REQUIRED is unset", () => { + delete process.env["E2E_REQUIRED"]; + expect(dbGate(undefined, "example")).toBe(describe.skip); + expect(dbGate("", "example")).toBe(describe.skip); + }); + + test("throws when E2E_REQUIRED=1 but no database is configured", () => { + process.env["E2E_REQUIRED"] = "1"; + expect(() => dbGate(undefined, "example-suite")).toThrow( + /E2E_REQUIRED=1.*example-suite/, + ); + }); + + test("does not throw under E2E_REQUIRED=1 once a database is configured", () => { + process.env["E2E_REQUIRED"] = "1"; + expect(dbGate("postgres://localhost:5432/workbench", "example")).toBe( + describe, + ); + }); +}); From 5dc2569b52aacbe050b1ac0b51f2469e93762f8e Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Sun, 30 Aug 2026 20:58:09 -0700 Subject: [PATCH 2/5] Add dbGate and a Postgres compose file matching CI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit describeIfDb across the repo hand-rolled its own DATABASE_URL check, so a locally-missing database skipped every DB-gated suite in total silence — a run reported green whether the suites passed or never ran at all. That hid a real migration-list bug in PR #508 for hours. dbGate centralizes the check: it still skips quietly by default, but prints an unmissable summary naming every skipped suite, and honors E2E_REQUIRED=1 (already CI's convention for the e2e suite) by throwing instead of skipping. Every describeIfDb definition now goes through it. docker-compose.test.yml mirrors the walking-skeleton job's postgres service in ci.yml exactly (pgvector/pgvector:pg17, postgres/postgres, same healthcheck), so a local DB-gated run uses the same database CI does instead of a hand-rolled substitute. --- apps/hub/src/memory-mount.test.ts | 3 +- apps/hub/src/memory-workflow-routes.test.ts | 5 +- .../hub/test/artifact-doc-persistence.test.ts | 5 +- apps/hub/test/chat-mount.test.ts | 5 +- apps/hub/test/composition.test.ts | 5 +- apps/hub/test/eval-runs-mount.test.ts | 5 +- apps/hub/test/presence-mount.test.ts | 5 +- apps/hub/test/routine-mount.test.ts | 5 +- apps/hub/test/slack-tag-mount.test.ts | 12 ++--- docker-compose.test.yml | 21 ++++++++ .../access-policy/test/store.drizzle.test.ts | 5 +- .../agent-directory/test/migrations.test.ts | 5 +- .../test/routes.integration.test.ts | 5 +- .../test/skills-store.drizzle.test.ts | 5 +- .../test/visible-definitions.drizzle.test.ts | 5 +- packages/bench/test/migrations.test.ts | 5 +- .../chat/test/agent-turns.drizzle.test.ts | 5 +- packages/chat/test/reactions.drizzle.test.ts | 5 +- packages/chat/test/read-state.drizzle.test.ts | 5 +- .../chat/test/room-activity.drizzle.test.ts | 5 +- packages/chat/test/threads.drizzle.test.ts | 5 +- .../chat/test/write-claims.drizzle.test.ts | 5 +- .../test/mcp-server-store.drizzle.test.ts | 5 +- .../test/credential-delivery.drizzle.test.ts | 5 +- .../credential-wiring-e2e.drizzle.test.ts | 5 +- packages/inbox/test/delivery.test.ts | 5 +- .../inference-catalog/test/migrations.test.ts | 5 +- .../test/insights-scope-verification.test.ts | 5 +- packages/insights/test/migrations.test.ts | 5 +- .../insights/test/routes-run-trace.test.ts | 5 +- packages/insights/test/routes-scope.test.ts | 5 +- packages/insights/test/trace-reader.test.ts | 5 +- .../test/credential-delivery.drizzle.test.ts | 5 +- .../credential-wiring-e2e.drizzle.test.ts | 5 +- packages/notify/test/migrations.test.ts | 5 +- packages/onboarding/test/migrations.test.ts | 5 +- .../test/pending-seed-store.drizzle.test.ts | 5 +- packages/preferences/test/migrations.test.ts | 5 +- packages/routines/test/migrations.test.ts | 5 +- packages/routines/test/store.drizzle.test.ts | 5 +- .../test/diagnostics.drizzle.test.ts | 5 +- .../run-key-history/test/migrations.test.ts | 5 +- .../test/reconnect.drizzle.test.ts | 5 +- .../test/routes.drizzle.test.ts | 5 +- packages/run-key-history/test/store.test.ts | 5 +- .../test/scope-routes.drizzle.test.ts | 5 +- .../test/store.drizzle.test.ts | 5 +- .../test/store.drizzle.test.ts | 5 +- .../test/migrations.test.ts | 5 +- .../workflow-deploy-source/test/store.test.ts | 5 +- .../test/freeze.drizzle.test.ts | 5 +- scripts/db-setup.test.ts | 5 +- scripts/e2e/db-gate.ts | 54 +++++++++++++++++++ scripts/e2e/folded-run-backfill.test.ts | 5 +- 54 files changed, 230 insertions(+), 110 deletions(-) create mode 100644 docker-compose.test.yml create mode 100644 scripts/e2e/db-gate.ts diff --git a/apps/hub/src/memory-mount.test.ts b/apps/hub/src/memory-mount.test.ts index cd4bb4340..ca664ebcb 100644 --- a/apps/hub/src/memory-mount.test.ts +++ b/apps/hub/src/memory-mount.test.ts @@ -1,4 +1,5 @@ import { afterAll, afterEach, describe, expect, test } from "bun:test"; +import { dbGate } from "../../../scripts/e2e/db-gate"; import { Hono } from "hono"; import { createInMemoryGrantStore } from "@intx/authz"; @@ -224,7 +225,7 @@ describe("mountMemory", () => { // mounting with only DATABASE_URL (no second memory-plane URL) lands the // memory engine's tables in its own `memory` schema, never `public`. const databaseUrl = process.env["DATABASE_URL"]; -const describeIfDb = databaseUrl === undefined ? describe.skip : describe; +const describeIfDb = dbGate(databaseUrl, import.meta.path); describeIfDb("mountMemory: schema isolation against a real database", () => { afterAll(async () => { diff --git a/apps/hub/src/memory-workflow-routes.test.ts b/apps/hub/src/memory-workflow-routes.test.ts index bda3dcb8b..d1c267e8b 100644 --- a/apps/hub/src/memory-workflow-routes.test.ts +++ b/apps/hub/src/memory-workflow-routes.test.ts @@ -19,7 +19,7 @@ // (`services/search.ts`'s `degraded: ["dense_unavailable"]`) rather than // a fake standing in for it — zero real keys, and the ONLY inference this // test performs is the plane's own lexical (Postgres FTS) fallback. -import { afterAll, afterEach, describe, expect, test } from "bun:test"; +import { afterAll, afterEach, expect, test } from "bun:test"; import { Hono } from "hono"; import { createInMemoryGrantStore } from "@intx/authz"; import { @@ -29,6 +29,7 @@ import { import type { ResolvedWorkflowRunScope } from "@corbits/artifacts-hub"; import { mountMemory } from "./memory-mount"; +import { dbGate } from "../../../scripts/e2e/db-gate"; const KEYS = [ "DATABASE_URL", @@ -66,7 +67,7 @@ afterEach(() => { }); const databaseUrl = process.env["DATABASE_URL"]; -const describeIfDb = databaseUrl === undefined ? describe.skip : describe; +const describeIfDb = dbGate(databaseUrl, import.meta.path); const TENANT_A: ResolvedWorkflowRunScope = { tenantId: "ten_memory_a", diff --git a/apps/hub/test/artifact-doc-persistence.test.ts b/apps/hub/test/artifact-doc-persistence.test.ts index d948ff1a0..7d5e030b0 100644 --- a/apps/hub/test/artifact-doc-persistence.test.ts +++ b/apps/hub/test/artifact-doc-persistence.test.ts @@ -6,7 +6,8 @@ // `getArtifact`/`writeArtifactVersion` seam actually lands a new artifact // version row — not just that the two packages' unit tests independently // pass with fakes standing in for each other. -import { afterAll, beforeAll, describe, expect, test } from "bun:test"; +import { afterAll, beforeAll, expect, test } from "bun:test"; +import { dbGate } from "../../../scripts/e2e/db-gate"; import * as Y from "yjs"; import { and, eq } from "drizzle-orm"; @@ -39,7 +40,7 @@ function dbConfigFromUrl(databaseUrl: string) { } const databaseUrl = process.env["DATABASE_URL"]; -const describeIfDb = databaseUrl === undefined ? describe.skip : describe; +const describeIfDb = dbGate(databaseUrl, import.meta.path); describeIfDb( "artifact doc persistence: a real snapshot write against Postgres", diff --git a/apps/hub/test/chat-mount.test.ts b/apps/hub/test/chat-mount.test.ts index ff14af98a..56943ebd5 100644 --- a/apps/hub/test/chat-mount.test.ts +++ b/apps/hub/test/chat-mount.test.ts @@ -4,17 +4,18 @@ // literal). Mirrors `composition.test.ts`'s echo-mount proof; chat's // own request/response behavior belongs to `packages/chat`'s tests. -import { afterAll, describe, expect, test } from "bun:test"; +import { afterAll, expect, test } from "bun:test"; import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import path from "node:path"; import type { HubConfig } from "../src/config.ts"; import { createHub } from "../src/index.ts"; +import { dbGate } from "../../../scripts/e2e/db-gate"; // DB-gated: skipped when DATABASE_URL is unset, matching this repo's // convention for tests that talk to a real Postgres. const databaseUrl = process.env["DATABASE_URL"] ?? ""; -const describeIfDb = databaseUrl === "" ? describe.skip : describe; +const describeIfDb = dbGate(databaseUrl, import.meta.path); const root = mkdtempSync(path.join(tmpdir(), "hub-chat-mount-")); const staticDir = path.join(root, "static"); diff --git a/apps/hub/test/composition.test.ts b/apps/hub/test/composition.test.ts index ceb34fb76..8aa1d6501 100644 --- a/apps/hub/test/composition.test.ts +++ b/apps/hub/test/composition.test.ts @@ -5,17 +5,18 @@ // the hub runs package migrations, so a reachable DATABASE_URL is // required and the suite skips without one. -import { afterAll, describe, expect, spyOn, test } from "bun:test"; +import { afterAll, expect, spyOn, test } from "bun:test"; import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import path from "node:path"; import type { HubConfig } from "../src/config.ts"; import { createHub } from "../src/index.ts"; +import { dbGate } from "../../../scripts/e2e/db-gate"; // DB-gated: skipped when DATABASE_URL is unset, matching this repo's // convention for tests that talk to a real Postgres. const databaseUrl = process.env["DATABASE_URL"] ?? ""; -const describeIfDb = databaseUrl === "" ? describe.skip : describe; +const describeIfDb = dbGate(databaseUrl, import.meta.path); const root = mkdtempSync(path.join(tmpdir(), "hub-composition-")); const staticDir = path.join(root, "static"); diff --git a/apps/hub/test/eval-runs-mount.test.ts b/apps/hub/test/eval-runs-mount.test.ts index 8ce304fd5..85d33fad3 100644 --- a/apps/hub/test/eval-runs-mount.test.ts +++ b/apps/hub/test/eval-runs-mount.test.ts @@ -5,17 +5,18 @@ // `routes.test.ts`; this only proves the composition-root wiring in // `src/index.ts` actually reaches a live app instead of 404ing. -import { afterAll, describe, expect, test } from "bun:test"; +import { afterAll, expect, test } from "bun:test"; import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import path from "node:path"; import type { HubConfig } from "../src/config.ts"; import { createHub } from "../src/index.ts"; +import { dbGate } from "../../../scripts/e2e/db-gate"; // DB-gated: skipped when DATABASE_URL is unset, matching this repo's // convention for tests that talk to a real Postgres. const databaseUrl = process.env["DATABASE_URL"] ?? ""; -const describeIfDb = databaseUrl === "" ? describe.skip : describe; +const describeIfDb = dbGate(databaseUrl, import.meta.path); const root = mkdtempSync(path.join(tmpdir(), "hub-eval-runs-mount-")); const staticDir = path.join(root, "static"); diff --git a/apps/hub/test/presence-mount.test.ts b/apps/hub/test/presence-mount.test.ts index b20f8b55c..68607cbee 100644 --- a/apps/hub/test/presence-mount.test.ts +++ b/apps/hub/test/presence-mount.test.ts @@ -4,17 +4,18 @@ // tests; this only proves the composition-root wiring in `src/index.ts` // actually reaches a live app. -import { afterAll, describe, expect, test } from "bun:test"; +import { afterAll, expect, test } from "bun:test"; import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import path from "node:path"; import type { HubConfig } from "../src/config.ts"; import { createHub } from "../src/index.ts"; +import { dbGate } from "../../../scripts/e2e/db-gate"; // DB-gated: skipped when DATABASE_URL is unset, matching this repo's // convention for tests that talk to a real Postgres. const databaseUrl = process.env["DATABASE_URL"] ?? ""; -const describeIfDb = databaseUrl === "" ? describe.skip : describe; +const describeIfDb = dbGate(databaseUrl, import.meta.path); const root = mkdtempSync(path.join(tmpdir(), "hub-presence-mount-")); const staticDir = path.join(root, "static"); diff --git a/apps/hub/test/routine-mount.test.ts b/apps/hub/test/routine-mount.test.ts index cce307862..b6c4d3969 100644 --- a/apps/hub/test/routine-mount.test.ts +++ b/apps/hub/test/routine-mount.test.ts @@ -5,17 +5,18 @@ // request/response behavior belongs to that package's tests, and the // launcher adapter's own behavior belongs to routine-launcher.test.ts. -import { afterAll, describe, expect, test } from "bun:test"; +import { afterAll, expect, test } from "bun:test"; import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import path from "node:path"; import type { HubConfig } from "../src/config.ts"; import { createHub } from "../src/index.ts"; +import { dbGate } from "../../../scripts/e2e/db-gate"; // DB-gated: skipped when DATABASE_URL is unset, matching this repo's // convention for tests that talk to a real Postgres. const databaseUrl = process.env["DATABASE_URL"] ?? ""; -const describeIfDb = databaseUrl === "" ? describe.skip : describe; +const describeIfDb = dbGate(databaseUrl, import.meta.path); const root = mkdtempSync(path.join(tmpdir(), "hub-routine-mount-")); const staticDir = path.join(root, "static"); diff --git a/apps/hub/test/slack-tag-mount.test.ts b/apps/hub/test/slack-tag-mount.test.ts index bf30b49bf..c42a82690 100644 --- a/apps/hub/test/slack-tag-mount.test.ts +++ b/apps/hub/test/slack-tag-mount.test.ts @@ -5,25 +5,19 @@ // config registers the route and inherits real Slack signature // verification from `@corbits/slack-tag`/`corbits-tag/slack` — request/ // reply behavior itself is covered by `packages/slack-tag`'s own tests. -import { - afterAll, - afterEach, - beforeAll, - describe, - expect, - test, -} from "bun:test"; +import { afterAll, afterEach, beforeAll, expect, test } from "bun:test"; import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import path from "node:path"; import postgres from "postgres"; import type { HubConfig } from "../src/config.ts"; import { createHub } from "../src/index.ts"; +import { dbGate } from "../../../scripts/e2e/db-gate"; // DB-gated: skipped when DATABASE_URL is unset, matching this repo's // convention for tests that talk to a real Postgres. const DATABASE_URL = process.env["DATABASE_URL"] ?? ""; -const describeIfDb = DATABASE_URL === "" ? describe.skip : describe; +const describeIfDb = dbGate(DATABASE_URL, import.meta.path); const SLACK_WEBHOOK_PATH = "/api/tag/slack/webhook"; const TENANT_SLUG = `slack-tag-test-${crypto.randomUUID().slice(0, 8)}`; diff --git a/docker-compose.test.yml b/docker-compose.test.yml new file mode 100644 index 000000000..8f96289e4 --- /dev/null +++ b/docker-compose.test.yml @@ -0,0 +1,21 @@ +# Postgres for local DB-gated test runs, matching the `walking-skeleton` +# job's `services.postgres` block in .github/workflows/ci.yml exactly — +# same image, same credentials, same healthcheck — so a suite that fails +# here fails (or passes) the same way it does in CI. +# +# Usage: +# docker compose -f docker-compose.test.yml up -d +# DATABASE_URL=postgres://postgres:postgres@localhost:5432/workbench bun test ... +services: + postgres: + image: pgvector/pgvector:pg17 + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + ports: + - "5432:5432" + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres -d postgres"] + interval: 10s + timeout: 5s + retries: 5 diff --git a/packages/access-policy/test/store.drizzle.test.ts b/packages/access-policy/test/store.drizzle.test.ts index 3a24ee877..4e1d89ffb 100644 --- a/packages/access-policy/test/store.drizzle.test.ts +++ b/packages/access-policy/test/store.drizzle.test.ts @@ -8,13 +8,14 @@ // connections, that an upsert replaces rather than duplicates the // single policy row per tenant, and that the real migrations produce a // schema the store's queries actually run against. -import { afterAll, beforeAll, describe, expect, test } from "bun:test"; +import { afterAll, beforeAll, expect, test } from "bun:test"; import { drizzle } from "drizzle-orm/postgres-js"; import postgres from "postgres"; import { e2eDatabaseUrl } from "../../../scripts/e2e/harness"; import { applyAccessPolicyMigrations } from "../src/migrations"; import { createDrizzleAccessPolicyStore } from "../src/store"; +import { dbGate } from "../../../scripts/e2e/db-gate"; function scratchUrlFor(e2eUrl: string): string { const url = new URL(e2eUrl); @@ -24,7 +25,7 @@ function scratchUrlFor(e2eUrl: string): string { } const databaseUrl = e2eDatabaseUrl(); -const describeIfDb = databaseUrl === undefined ? describe.skip : describe; +const describeIfDb = dbGate(databaseUrl, import.meta.path); describeIfDb("createDrizzleAccessPolicyStore", () => { const scratchUrl = scratchUrlFor( diff --git a/packages/agent-directory/test/migrations.test.ts b/packages/agent-directory/test/migrations.test.ts index 855456ead..7ac036547 100644 --- a/packages/agent-directory/test/migrations.test.ts +++ b/packages/agent-directory/test/migrations.test.ts @@ -2,11 +2,12 @@ // still runs the unit gates), mirroring @corbits/config-profiles' own // migrations test. Runs against its own scratch database, never the // developer's or the walking-skeleton suite's. -import { afterAll, beforeAll, describe, expect, test } from "bun:test"; +import { afterAll, beforeAll, expect, test } from "bun:test"; import postgres from "postgres"; import { e2eDatabaseUrl } from "../../../scripts/e2e/harness"; import { applyAgentDirectoryMigrations } from "../src/migrations"; +import { dbGate } from "../../../scripts/e2e/db-gate"; function scratchUrlFor(e2eUrl: string): string { const url = new URL(e2eUrl); @@ -16,7 +17,7 @@ function scratchUrlFor(e2eUrl: string): string { } const databaseUrl = e2eDatabaseUrl(); -const describeIfDb = databaseUrl === undefined ? describe.skip : describe; +const describeIfDb = dbGate(databaseUrl, import.meta.path); const migrationNames = ["0001_definition_skills"]; diff --git a/packages/agent-directory/test/routes.integration.test.ts b/packages/agent-directory/test/routes.integration.test.ts index 7deacce15..2c138ef34 100644 --- a/packages/agent-directory/test/routes.integration.test.ts +++ b/packages/agent-directory/test/routes.integration.test.ts @@ -16,7 +16,7 @@ // DB-gated: skipped when DATABASE_URL is unset, so a fresh checkout // still runs the unit gates. Run with e.g. // `DATABASE_URL=postgres://localhost:5432/workbench_e2e bun test`. -import { afterAll, beforeAll, describe, expect, test } from "bun:test"; +import { afterAll, beforeAll, expect, test } from "bun:test"; import { mkdtemp, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; import path from "node:path"; @@ -48,9 +48,10 @@ import type { PinnedSkillIndexResolver } from "../src/routes"; import { createDrizzleDefinitionSkillsStore } from "../src/skills-store"; import type { DefinitionAssetHistory } from "../src/definition-history"; import type { CapabilityInventoryProvider } from "../src/capability-inventory"; +import { dbGate } from "../../../scripts/e2e/db-gate"; const databaseUrl = process.env["DATABASE_URL"]; -const describeIfDb = databaseUrl === undefined ? describe.skip : describe; +const describeIfDb = dbGate(databaseUrl, import.meta.path); const suffix = randomUUID().slice(0, 8); const TENANT = { diff --git a/packages/agent-directory/test/skills-store.drizzle.test.ts b/packages/agent-directory/test/skills-store.drizzle.test.ts index c2771010f..dfed79819 100644 --- a/packages/agent-directory/test/skills-store.drizzle.test.ts +++ b/packages/agent-directory/test/skills-store.drizzle.test.ts @@ -2,13 +2,14 @@ // still runs the unit gates), mirroring @corbits/config-profiles' own // store.drizzle.test.ts. Exercises the real // `createDrizzleDefinitionSkillsStore` path against Postgres. -import { afterAll, beforeAll, describe, expect, test } from "bun:test"; +import { afterAll, beforeAll, expect, test } from "bun:test"; import { drizzle } from "drizzle-orm/postgres-js"; import postgres from "postgres"; import { e2eDatabaseUrl } from "../../../scripts/e2e/harness"; import { applyAgentDirectoryMigrations } from "../src/migrations"; import { createDrizzleDefinitionSkillsStore } from "../src/skills-store"; +import { dbGate } from "../../../scripts/e2e/db-gate"; function scratchUrlFor(e2eUrl: string): string { const url = new URL(e2eUrl); @@ -18,7 +19,7 @@ function scratchUrlFor(e2eUrl: string): string { } const databaseUrl = e2eDatabaseUrl(); -const describeIfDb = databaseUrl === undefined ? describe.skip : describe; +const describeIfDb = dbGate(databaseUrl, import.meta.path); describeIfDb("createDrizzleDefinitionSkillsStore", () => { const scratchUrl = scratchUrlFor( diff --git a/packages/agent-directory/test/visible-definitions.drizzle.test.ts b/packages/agent-directory/test/visible-definitions.drizzle.test.ts index 30503b650..138bc29be 100644 --- a/packages/agent-directory/test/visible-definitions.drizzle.test.ts +++ b/packages/agent-directory/test/visible-definitions.drizzle.test.ts @@ -8,15 +8,16 @@ // ancestor chain the same way `listMcpServerConnections` does (CL-6191) — // a child tenant sees an ancestor's agent definitions, and a same-name // definition made at the child shadows the ancestor's. -import { afterAll, beforeAll, describe, expect, test } from "bun:test"; +import { afterAll, beforeAll, expect, test } from "bun:test"; import { createDB, runMigrations, dropSchema, schema } from "@intx/db"; import { dbTargetFromUrl } from "../../../scripts/db-setup"; import { e2eDatabaseUrl } from "../../../scripts/e2e/harness"; import { listVisibleAgentDefinitions } from "../src/visible-definitions"; +import { dbGate } from "../../../scripts/e2e/db-gate"; const databaseUrl = e2eDatabaseUrl(); -const describeIfDb = databaseUrl === undefined ? describe.skip : describe; +const describeIfDb = dbGate(databaseUrl, import.meta.path); const SCHEMA = "agent_directory_visible_definitions_test"; diff --git a/packages/bench/test/migrations.test.ts b/packages/bench/test/migrations.test.ts index e9d759b45..9ab33fd6f 100644 --- a/packages/bench/test/migrations.test.ts +++ b/packages/bench/test/migrations.test.ts @@ -2,12 +2,13 @@ // still runs the unit gates), mirroring @corbits/preferences' migrations // test. Runs against its own scratch database, never the developer's or // the walking-skeleton suite's. -import { afterAll, beforeAll, describe, expect, test } from "bun:test"; +import { afterAll, beforeAll, expect, test } from "bun:test"; import postgres from "postgres"; import { e2eDatabaseUrl } from "../../../scripts/e2e/harness"; import { applyBenchMigrations } from "../src/migrations"; import { createPostgresBenchSettingsStore } from "../src/pg-store"; +import { dbGate } from "../../../scripts/e2e/db-gate"; function scratchUrlFor(e2eUrl: string): string { const url = new URL(e2eUrl); @@ -17,7 +18,7 @@ function scratchUrlFor(e2eUrl: string): string { } const databaseUrl = e2eDatabaseUrl(); -const describeIfDb = databaseUrl === undefined ? describe.skip : describe; +const describeIfDb = dbGate(databaseUrl, import.meta.path); const migrationNames = ["0001_bench_settings"]; diff --git a/packages/chat/test/agent-turns.drizzle.test.ts b/packages/chat/test/agent-turns.drizzle.test.ts index f468a8d6a..c23543667 100644 --- a/packages/chat/test/agent-turns.drizzle.test.ts +++ b/packages/chat/test/agent-turns.drizzle.test.ts @@ -9,7 +9,7 @@ // therefore for a child run id. Two turns quietly sharing one run id is // exactly the traceability hole this projection exists to close, so the // bar here is that the race is loud, never silently duplicated. -import { afterAll, beforeAll, describe, expect, test } from "bun:test"; +import { afterAll, beforeAll, expect, test } from "bun:test"; import { drizzle } from "drizzle-orm/postgres-js"; import postgres from "postgres"; @@ -19,6 +19,7 @@ import { createDrizzleAgentTurnStore, } from "../src/agent-turns"; import { applyChatMigrations } from "../src/migrations"; +import { dbGate } from "../../../scripts/e2e/db-gate"; function scratchUrlFor(e2eUrl: string): string { const url = new URL(e2eUrl); @@ -28,7 +29,7 @@ function scratchUrlFor(e2eUrl: string): string { } const databaseUrl = e2eDatabaseUrl(); -const describeIfDb = databaseUrl === undefined ? describe.skip : describe; +const describeIfDb = dbGate(databaseUrl, import.meta.path); const TENANT = "tnt_1"; const WORKBENCH = "run_workbench1"; diff --git a/packages/chat/test/reactions.drizzle.test.ts b/packages/chat/test/reactions.drizzle.test.ts index 8bfbb2389..e382e8edb 100644 --- a/packages/chat/test/reactions.drizzle.test.ts +++ b/packages/chat/test/reactions.drizzle.test.ts @@ -11,13 +11,14 @@ // at the database: proves the fix (`INSERT ... ON CONFLICT DO NOTHING`, // never select-then-branch) never throws a raw PK-violation and always // leaves a consistent, non-crashed final state. -import { afterAll, beforeAll, describe, expect, test } from "bun:test"; +import { afterAll, beforeAll, expect, test } from "bun:test"; import { drizzle } from "drizzle-orm/postgres-js"; import postgres from "postgres"; import { e2eDatabaseUrl } from "../../../scripts/e2e/harness"; import { applyChatMigrations } from "../src/migrations"; import { createDrizzleReactionStore } from "../src/reactions"; +import { dbGate } from "../../../scripts/e2e/db-gate"; function scratchUrlFor(e2eUrl: string): string { const url = new URL(e2eUrl); @@ -27,7 +28,7 @@ function scratchUrlFor(e2eUrl: string): string { } const databaseUrl = e2eDatabaseUrl(); -const describeIfDb = databaseUrl === undefined ? describe.skip : describe; +const describeIfDb = dbGate(databaseUrl, import.meta.path); const TENANT = "tnt_1"; const WORKBENCH = "run_workbench1"; diff --git a/packages/chat/test/read-state.drizzle.test.ts b/packages/chat/test/read-state.drizzle.test.ts index a8c844b93..2e081a933 100644 --- a/packages/chat/test/read-state.drizzle.test.ts +++ b/packages/chat/test/read-state.drizzle.test.ts @@ -7,13 +7,14 @@ // path, where the guard is a conditional `ON CONFLICT DO UPDATE ... SET` // rather than an in-process comparison — proving the SQL itself never // regresses a reader's cursor. -import { afterAll, beforeAll, describe, expect, test } from "bun:test"; +import { afterAll, beforeAll, expect, test } from "bun:test"; import { drizzle } from "drizzle-orm/postgres-js"; import postgres from "postgres"; import { e2eDatabaseUrl } from "../../../scripts/e2e/harness"; import { applyChatMigrations } from "../src/migrations"; import { createDrizzleChatStore } from "../src/store"; +import { dbGate } from "../../../scripts/e2e/db-gate"; function scratchUrlFor(e2eUrl: string): string { const url = new URL(e2eUrl); @@ -23,7 +24,7 @@ function scratchUrlFor(e2eUrl: string): string { } const databaseUrl = e2eDatabaseUrl(); -const describeIfDb = databaseUrl === undefined ? describe.skip : describe; +const describeIfDb = dbGate(databaseUrl, import.meta.path); const TENANT = "tnt_1"; const WORKBENCH = "run_workbench1"; diff --git a/packages/chat/test/room-activity.drizzle.test.ts b/packages/chat/test/room-activity.drizzle.test.ts index 8a8013aae..fc5df62e3 100644 --- a/packages/chat/test/room-activity.drizzle.test.ts +++ b/packages/chat/test/room-activity.drizzle.test.ts @@ -9,13 +9,14 @@ // real `createDrizzleRoomMessageStore` against real rows: the summary a // workbench-list row renders, and a page boundary landing inside a burst // of messages that share a millisecond. -import { afterAll, beforeAll, describe, expect, test } from "bun:test"; +import { afterAll, beforeAll, expect, test } from "bun:test"; import { drizzle } from "drizzle-orm/postgres-js"; import postgres from "postgres"; import { e2eDatabaseUrl } from "../../../scripts/e2e/harness"; import { applyChatMigrations } from "../src/migrations"; import { createDrizzleRoomMessageStore } from "../src/room-messages"; +import { dbGate } from "../../../scripts/e2e/db-gate"; function scratchUrlFor(e2eUrl: string): string { const url = new URL(e2eUrl); @@ -25,7 +26,7 @@ function scratchUrlFor(e2eUrl: string): string { } const databaseUrl = e2eDatabaseUrl(); -const describeIfDb = databaseUrl === undefined ? describe.skip : describe; +const describeIfDb = dbGate(databaseUrl, import.meta.path); const TENANT = "tnt_1"; const OTHER_TENANT = "tnt_2"; diff --git a/packages/chat/test/threads.drizzle.test.ts b/packages/chat/test/threads.drizzle.test.ts index 03852fcf9..2798fc211 100644 --- a/packages/chat/test/threads.drizzle.test.ts +++ b/packages/chat/test/threads.drizzle.test.ts @@ -12,13 +12,14 @@ // re-select on conflict — never select-then-insert) never throws a raw // unique-violation and both callers converge on the same thread row // (CL-7130, CL-7199). -import { afterAll, beforeAll, describe, expect, test } from "bun:test"; +import { afterAll, beforeAll, expect, test } from "bun:test"; import { drizzle } from "drizzle-orm/postgres-js"; import postgres from "postgres"; import { e2eDatabaseUrl } from "../../../scripts/e2e/harness"; import { applyChatMigrations } from "../src/migrations"; import { createDrizzleThreadStore } from "../src/threads"; +import { dbGate } from "../../../scripts/e2e/db-gate"; function scratchUrlFor(e2eUrl: string): string { const url = new URL(e2eUrl); @@ -28,7 +29,7 @@ function scratchUrlFor(e2eUrl: string): string { } const databaseUrl = e2eDatabaseUrl(); -const describeIfDb = databaseUrl === undefined ? describe.skip : describe; +const describeIfDb = dbGate(databaseUrl, import.meta.path); const TENANT = "tnt_1"; const WORKBENCH = "run_workbench1"; diff --git a/packages/chat/test/write-claims.drizzle.test.ts b/packages/chat/test/write-claims.drizzle.test.ts index e10873226..2496cecfe 100644 --- a/packages/chat/test/write-claims.drizzle.test.ts +++ b/packages/chat/test/write-claims.drizzle.test.ts @@ -10,13 +10,14 @@ // throws a raw PK-violation — the exact scenario CL-6039 exists to close: // a redelivered `onTurnFinalized` racing itself across a hub restart or // sidecar reconnect. -import { afterAll, beforeAll, describe, expect, test } from "bun:test"; +import { afterAll, beforeAll, expect, test } from "bun:test"; import { drizzle } from "drizzle-orm/postgres-js"; import postgres from "postgres"; import { e2eDatabaseUrl } from "../../../scripts/e2e/harness"; import { applyChatMigrations } from "../src/migrations"; import { createDrizzleWriteClaimStore } from "../src/write-claims"; +import { dbGate } from "../../../scripts/e2e/db-gate"; function scratchUrlFor(e2eUrl: string): string { const url = new URL(e2eUrl); @@ -26,7 +27,7 @@ function scratchUrlFor(e2eUrl: string): string { } const databaseUrl = e2eDatabaseUrl(); -const describeIfDb = databaseUrl === undefined ? describe.skip : describe; +const describeIfDb = dbGate(databaseUrl, import.meta.path); describeIfDb("createDrizzleWriteClaimStore: concurrent tryClaim", () => { const scratchUrl = scratchUrlFor( diff --git a/packages/connections/test/mcp-server-store.drizzle.test.ts b/packages/connections/test/mcp-server-store.drizzle.test.ts index b5ca7fce7..8f2348be0 100644 --- a/packages/connections/test/mcp-server-store.drizzle.test.ts +++ b/packages/connections/test/mcp-server-store.drizzle.test.ts @@ -9,15 +9,16 @@ // `listAssetsForTenant` do, rather than matching the exact tenant only — // a child tenant sees an ancestor's MCP server connections, and a // same-slug connection made at the child shadows the ancestor's. -import { afterAll, beforeAll, describe, expect, test } from "bun:test"; +import { afterAll, beforeAll, expect, test } from "bun:test"; import { createDB, runMigrations, dropSchema, schema } from "@intx/db"; import { dbTargetFromUrl } from "../../../scripts/db-setup"; import { e2eDatabaseUrl } from "../../../scripts/e2e/harness"; import { listMcpServerConnections } from "../src/mcp-server-store"; +import { dbGate } from "../../../scripts/e2e/db-gate"; const databaseUrl = e2eDatabaseUrl(); -const describeIfDb = databaseUrl === undefined ? describe.skip : describe; +const describeIfDb = dbGate(databaseUrl, import.meta.path); const SCHEMA = "connections_mcp_server_store_test"; diff --git a/packages/granola-tools/test/credential-delivery.drizzle.test.ts b/packages/granola-tools/test/credential-delivery.drizzle.test.ts index 73f37d0cc..fc2546abc 100644 --- a/packages/granola-tools/test/credential-delivery.drizzle.test.ts +++ b/packages/granola-tools/test/credential-delivery.drizzle.test.ts @@ -14,7 +14,7 @@ // secret; // - the same binding fails closed (`unresolved`) when no credential // exists for the provider, rather than delivering nothing silently. -import { afterAll, beforeAll, describe, expect, test } from "bun:test"; +import { afterAll, beforeAll, expect, test } from "bun:test"; import { buildCredentialDelivery, createDB, @@ -28,9 +28,10 @@ import type { CredentialBinding } from "@intx/types"; import { dbTargetFromUrl } from "../../../scripts/db-setup"; import { e2eDatabaseUrl } from "../../../scripts/e2e/harness"; +import { dbGate } from "../../../scripts/e2e/db-gate"; const databaseUrl = e2eDatabaseUrl(); -const describeIfDb = databaseUrl === undefined ? describe.skip : describe; +const describeIfDb = dbGate(databaseUrl, import.meta.path); const SCHEMA = "granola_tools_credential_delivery_test"; const KEY = new Uint8Array(32).fill(9); diff --git a/packages/granola-tools/test/credential-wiring-e2e.drizzle.test.ts b/packages/granola-tools/test/credential-wiring-e2e.drizzle.test.ts index eb3973762..b640b2f1f 100644 --- a/packages/granola-tools/test/credential-wiring-e2e.drizzle.test.ts +++ b/packages/granola-tools/test/credential-wiring-e2e.drizzle.test.ts @@ -33,7 +33,7 @@ // bundle directly with a `ToolCall`, exactly as `../src/tool.test.ts` // does, so what's new here is steps 1-3: a REAL seeded credential // reaching the tool through the REAL substrate composition. -import { afterAll, beforeAll, describe, expect, test } from "bun:test"; +import { afterAll, beforeAll, expect, test } from "bun:test"; import { buildCredentialDelivery, createDB, @@ -58,9 +58,10 @@ import { dbTargetFromUrl } from "../../../scripts/db-setup"; import { e2eDatabaseUrl } from "../../../scripts/e2e/harness"; import { GRANOLA_LIST_RECENT_NOTES_TOOL, granolaTools } from "../src/tool"; import type { GranolaEnv } from "../src/tool"; +import { dbGate } from "../../../scripts/e2e/db-gate"; const databaseUrl = e2eDatabaseUrl(); -const describeIfDb = databaseUrl === undefined ? describe.skip : describe; +const describeIfDb = dbGate(databaseUrl, import.meta.path); const SCHEMA = "granola_tools_credential_wiring_e2e_test"; const KEY = new Uint8Array(32).fill(11); diff --git a/packages/inbox/test/delivery.test.ts b/packages/inbox/test/delivery.test.ts index 899d62871..c662bac91 100644 --- a/packages/inbox/test/delivery.test.ts +++ b/packages/inbox/test/delivery.test.ts @@ -7,7 +7,7 @@ // the inbox/bell UI drives, grouped the same way `inboxGroupOf` groups it. // Runs against its own scratch database, never the developer's or the // walking-skeleton suite's. -import { afterAll, beforeAll, describe, expect, test } from "bun:test"; +import { afterAll, beforeAll, expect, test } from "bun:test"; import postgres from "postgres"; import { createDB, schema } from "@intx/db"; @@ -28,6 +28,7 @@ import { e2eDatabaseUrl } from "../../../scripts/e2e/harness"; import { createWorkbenchMailboxDelivery } from "../src/delivery"; import { inboxGroupOf } from "../src/group"; import { WORKBENCH_INBOX_PRIORITIES } from "../src/vocabulary"; +import { dbGate } from "../../../scripts/e2e/db-gate"; function scratchUrlFor(e2eUrl: string): string { const url = new URL(e2eUrl); @@ -50,7 +51,7 @@ function dbConfigFromUrl(databaseUrl: string) { } const databaseUrl = e2eDatabaseUrl(); -const describeIfDb = databaseUrl === undefined ? describe.skip : describe; +const describeIfDb = dbGate(databaseUrl, import.meta.path); describeIfDb( "notify delivery writes a real mailbox row for every notification kind", diff --git a/packages/inference-catalog/test/migrations.test.ts b/packages/inference-catalog/test/migrations.test.ts index cf4b44a46..c9ba5b30a 100644 --- a/packages/inference-catalog/test/migrations.test.ts +++ b/packages/inference-catalog/test/migrations.test.ts @@ -2,13 +2,14 @@ // still runs the unit gates), mirroring @corbits/insights' migrations test. // Runs against its own scratch database, never the developer's or the // walking-skeleton suite's. -import { afterAll, beforeAll, describe, expect, test } from "bun:test"; +import { afterAll, beforeAll, expect, test } from "bun:test"; import postgres from "postgres"; import { e2eDatabaseUrl } from "../../../scripts/e2e/harness"; import { applyInferenceCatalogMigrations } from "../src/migrations"; import { createPostgresBenchModelPolicyStore } from "../src/pg-store"; import { EMPTY_POLICY } from "../src/policy"; +import { dbGate } from "../../../scripts/e2e/db-gate"; function scratchUrlFor(e2eUrl: string): string { const url = new URL(e2eUrl); @@ -18,7 +19,7 @@ function scratchUrlFor(e2eUrl: string): string { } const databaseUrl = e2eDatabaseUrl(); -const describeIfDb = databaseUrl === undefined ? describe.skip : describe; +const describeIfDb = dbGate(databaseUrl, import.meta.path); const migrationNames = ["0001_bench_model_policy"]; diff --git a/packages/insights/test/insights-scope-verification.test.ts b/packages/insights/test/insights-scope-verification.test.ts index 424624489..b31f0fedf 100644 --- a/packages/insights/test/insights-scope-verification.test.ts +++ b/packages/insights/test/insights-scope-verification.test.ts @@ -6,7 +6,7 @@ // 2. resolveScope recursion: a workbench with a workbench child tenancy is // NOT a leaf — its /usage now includes the grandchild's rows, and the // workspace parent's aggregate includes them too. -import { afterAll, beforeAll, describe, expect, test } from "bun:test"; +import { afterAll, beforeAll, expect, test } from "bun:test"; import { Hono } from "hono"; import type { MiddlewareHandler } from "hono"; import postgres from "postgres"; @@ -21,9 +21,10 @@ import { createInsightsRoutes } from "../src/routes"; import { createPostgresUsageStore } from "../src/pg-store"; import { createMemoryUsageStore } from "../src/store"; import type { OverallUsageSummary } from "../src/queries"; +import { dbGate } from "../../../scripts/e2e/db-gate"; const databaseUrl = e2eDatabaseUrl(); -const describeIfDb = databaseUrl === undefined ? describe.skip : describe; +const describeIfDb = dbGate(databaseUrl, import.meta.path); function scratchUrlFor(e2eUrl: string): string { const url = new URL(e2eUrl); diff --git a/packages/insights/test/migrations.test.ts b/packages/insights/test/migrations.test.ts index a632cbd66..5a4e61d8f 100644 --- a/packages/insights/test/migrations.test.ts +++ b/packages/insights/test/migrations.test.ts @@ -2,11 +2,12 @@ // still runs the unit gates), mirroring @corbits/chat's migrations test. // Runs against its own scratch database, never the developer's or the // walking-skeleton suite's. -import { afterAll, beforeAll, describe, expect, test } from "bun:test"; +import { afterAll, beforeAll, expect, test } from "bun:test"; import postgres from "postgres"; import { e2eDatabaseUrl } from "../../../scripts/e2e/harness"; import { applyInsightsMigrations } from "../src/migrations"; +import { dbGate } from "../../../scripts/e2e/db-gate"; function scratchUrlFor(e2eUrl: string): string { const url = new URL(e2eUrl); @@ -16,7 +17,7 @@ function scratchUrlFor(e2eUrl: string): string { } const databaseUrl = e2eDatabaseUrl(); -const describeIfDb = databaseUrl === undefined ? describe.skip : describe; +const describeIfDb = dbGate(databaseUrl, import.meta.path); const migrationNames = [ "0001_usage_turn", diff --git a/packages/insights/test/routes-run-trace.test.ts b/packages/insights/test/routes-run-trace.test.ts index 500ead04d..3b54db555 100644 --- a/packages/insights/test/routes-run-trace.test.ts +++ b/packages/insights/test/routes-run-trace.test.ts @@ -5,7 +5,7 @@ // inference_turn / turn_part tables, not the "reader not mounted" absent // envelope. trace-reader.test.ts already proves the reader function itself // in isolation; this proves the HTTP route built on top of it. -import { afterAll, beforeAll, describe, expect, test } from "bun:test"; +import { afterAll, beforeAll, expect, test } from "bun:test"; import { Hono } from "hono"; import type { MiddlewareHandler } from "hono"; import postgres from "postgres"; @@ -20,6 +20,7 @@ import { createInsightsRoutes } from "../src/routes"; import { createDrizzleRunTraceReader } from "../src/trace-reader"; import { createMemoryUsageStore } from "../src/store"; import type { RunTrace } from "../src/queries"; +import { dbGate } from "../../../scripts/e2e/db-gate"; function scratchUrlFor(e2eUrl: string): string { const url = new URL(e2eUrl); @@ -42,7 +43,7 @@ function dbConfigFromUrl(databaseUrl: string) { } const databaseUrl = e2eDatabaseUrl(); -const describeIfDb = databaseUrl === undefined ? describe.skip : describe; +const describeIfDb = dbGate(databaseUrl, import.meta.path); const allowAll: RequireGrant = () => async (_c, next) => { await next(); diff --git a/packages/insights/test/routes-scope.test.ts b/packages/insights/test/routes-scope.test.ts index 7624fe9a2..25f790918 100644 --- a/packages/insights/test/routes-scope.test.ts +++ b/packages/insights/test/routes-scope.test.ts @@ -12,7 +12,7 @@ // counterpart a switcher reads: parent identity, own identity, and the // sibling workbench list — filtered to tenants the caller actually holds // a principal in, never a sibling the caller has no membership in. -import { afterAll, beforeAll, describe, expect, test } from "bun:test"; +import { afterAll, beforeAll, expect, test } from "bun:test"; import { Hono } from "hono"; import type { MiddlewareHandler } from "hono"; import postgres from "postgres"; @@ -26,6 +26,7 @@ import { e2eDatabaseUrl } from "../../../scripts/e2e/harness"; import { createInsightsRoutes } from "../src/routes"; import { createMemoryUsageStore } from "../src/store"; import type { OverallUsageSummary } from "../src/queries"; +import { dbGate } from "../../../scripts/e2e/db-gate"; function scratchUrlFor(e2eUrl: string): string { const url = new URL(e2eUrl); @@ -46,7 +47,7 @@ function dbConfigFromUrl(databaseUrl: string) { } const databaseUrl = e2eDatabaseUrl(); -const describeIfDb = databaseUrl === undefined ? describe.skip : describe; +const describeIfDb = dbGate(databaseUrl, import.meta.path); const allowAll: RequireGrant = () => async (_c, next) => { await next(); diff --git a/packages/insights/test/trace-reader.test.ts b/packages/insights/test/trace-reader.test.ts index 559009333..4f5d8d980 100644 --- a/packages/insights/test/trace-reader.test.ts +++ b/packages/insights/test/trace-reader.test.ts @@ -5,7 +5,7 @@ // @intx/hub-sessions' event-collector already read and write — rather than // a re-parsed git log or a fabricated trace. Runs against its own scratch // database, never the developer's or the walking-skeleton suite's. -import { afterAll, beforeAll, describe, expect, test } from "bun:test"; +import { afterAll, beforeAll, expect, test } from "bun:test"; import postgres from "postgres"; import { createDB, schema } from "@intx/db"; @@ -14,6 +14,7 @@ import { generateId } from "@intx/hub-common"; import { setupDatabase } from "../../../scripts/db-setup"; import { e2eDatabaseUrl } from "../../../scripts/e2e/harness"; import { createDrizzleRunTraceReader } from "../src/trace-reader"; +import { dbGate } from "../../../scripts/e2e/db-gate"; function scratchUrlFor(e2eUrl: string): string { const url = new URL(e2eUrl); @@ -36,7 +37,7 @@ function dbConfigFromUrl(databaseUrl: string) { } const databaseUrl = e2eDatabaseUrl(); -const describeIfDb = databaseUrl === undefined ? describe.skip : describe; +const describeIfDb = dbGate(databaseUrl, import.meta.path); describeIfDb("createDrizzleRunTraceReader", () => { const scratchUrl = scratchUrlFor( diff --git a/packages/linear-tools/test/credential-delivery.drizzle.test.ts b/packages/linear-tools/test/credential-delivery.drizzle.test.ts index 39d371e6c..731492fdc 100644 --- a/packages/linear-tools/test/credential-delivery.drizzle.test.ts +++ b/packages/linear-tools/test/credential-delivery.drizzle.test.ts @@ -14,7 +14,7 @@ // secret; // - the same binding fails closed (`unresolved`) when no credential // exists for the provider, rather than delivering nothing silently. -import { afterAll, beforeAll, describe, expect, test } from "bun:test"; +import { afterAll, beforeAll, expect, test } from "bun:test"; import { buildCredentialDelivery, createDB, @@ -28,9 +28,10 @@ import type { CredentialBinding } from "@intx/types"; import { dbTargetFromUrl } from "../../../scripts/db-setup"; import { e2eDatabaseUrl } from "../../../scripts/e2e/harness"; +import { dbGate } from "../../../scripts/e2e/db-gate"; const databaseUrl = e2eDatabaseUrl(); -const describeIfDb = databaseUrl === undefined ? describe.skip : describe; +const describeIfDb = dbGate(databaseUrl, import.meta.path); const SCHEMA = "linear_tools_credential_delivery_test"; const KEY = new Uint8Array(32).fill(11); diff --git a/packages/linear-tools/test/credential-wiring-e2e.drizzle.test.ts b/packages/linear-tools/test/credential-wiring-e2e.drizzle.test.ts index 4c7e3acf9..a2cda13ec 100644 --- a/packages/linear-tools/test/credential-wiring-e2e.drizzle.test.ts +++ b/packages/linear-tools/test/credential-wiring-e2e.drizzle.test.ts @@ -36,7 +36,7 @@ // LLM inference cycle -- see the granola-tools e2e test's header comment // for why (no scripted tool-call inference adapter exists anywhere in // this repo today). -import { afterAll, beforeAll, describe, expect, test } from "bun:test"; +import { afterAll, beforeAll, expect, test } from "bun:test"; import { buildCredentialDelivery, createDB, @@ -64,9 +64,10 @@ import { dbTargetFromUrl } from "../../../scripts/db-setup"; import { e2eDatabaseUrl } from "../../../scripts/e2e/harness"; import { LINEAR_LIST_RECENT_ISSUES_TOOL, linearTools } from "../src/tool"; import type { LinearEnv } from "../src/tool"; +import { dbGate } from "../../../scripts/e2e/db-gate"; const databaseUrl = e2eDatabaseUrl(); -const describeIfDb = databaseUrl === undefined ? describe.skip : describe; +const describeIfDb = dbGate(databaseUrl, import.meta.path); const SCHEMA = "linear_tools_credential_wiring_e2e_test"; const KEY = new Uint8Array(32).fill(13); diff --git a/packages/notify/test/migrations.test.ts b/packages/notify/test/migrations.test.ts index 6d0cd4374..3774c6f76 100644 --- a/packages/notify/test/migrations.test.ts +++ b/packages/notify/test/migrations.test.ts @@ -1,12 +1,13 @@ // DB-gated: runs against its own scratch database, never the developer's or // the walking-skeleton suite's, mirroring `packages/schedules/test/migrations.test.ts`. -import { afterAll, beforeAll, describe, expect, test } from "bun:test"; +import { afterAll, beforeAll, expect, test } from "bun:test"; import { drizzle } from "drizzle-orm/postgres-js"; import postgres from "postgres"; import { e2eDatabaseUrl } from "../../../scripts/e2e/harness"; import { applyNotifyMigrations } from "../src/migrations"; import { createDrizzleNotifyDispatchStore } from "../src/store"; +import { dbGate } from "../../../scripts/e2e/db-gate"; function scratchUrlFor(e2eUrl: string): string { const url = new URL(e2eUrl); @@ -16,7 +17,7 @@ function scratchUrlFor(e2eUrl: string): string { } const databaseUrl = e2eDatabaseUrl(); -const describeIfDb = databaseUrl === undefined ? describe.skip : describe; +const describeIfDb = dbGate(databaseUrl, import.meta.path); describeIfDb("applyNotifyMigrations", () => { const scratchUrl = scratchUrlFor( diff --git a/packages/onboarding/test/migrations.test.ts b/packages/onboarding/test/migrations.test.ts index 69e308b2b..623acb0d1 100644 --- a/packages/onboarding/test/migrations.test.ts +++ b/packages/onboarding/test/migrations.test.ts @@ -2,11 +2,12 @@ // still runs the unit gates), mirroring @corbits/insights' migrations // test. Runs against its own scratch database, never the developer's // or the walking-skeleton suite's. -import { afterAll, beforeAll, describe, expect, test } from "bun:test"; +import { afterAll, beforeAll, expect, test } from "bun:test"; import postgres from "postgres"; import { e2eDatabaseUrl } from "../../../scripts/e2e/harness"; import { applyOnboardingMigrations } from "../src/migrations"; +import { dbGate } from "../../../scripts/e2e/db-gate"; function scratchUrlFor(e2eUrl: string): string { const url = new URL(e2eUrl); @@ -16,7 +17,7 @@ function scratchUrlFor(e2eUrl: string): string { } const databaseUrl = e2eDatabaseUrl(); -const describeIfDb = databaseUrl === undefined ? describe.skip : describe; +const describeIfDb = dbGate(databaseUrl, import.meta.path); const migrationNames = ["0001_pending_seed"]; diff --git a/packages/onboarding/test/pending-seed-store.drizzle.test.ts b/packages/onboarding/test/pending-seed-store.drizzle.test.ts index 455395dc0..f859b2058 100644 --- a/packages/onboarding/test/pending-seed-store.drizzle.test.ts +++ b/packages/onboarding/test/pending-seed-store.drizzle.test.ts @@ -15,7 +15,7 @@ // UPDATE — the kind of tamper the application layer never produces on // its own) fails to decrypt and is swept away rather than silently // misattributed. -import { afterAll, beforeAll, describe, expect, test } from "bun:test"; +import { afterAll, beforeAll, expect, test } from "bun:test"; import { drizzle } from "drizzle-orm/postgres-js"; import postgres from "postgres"; import { @@ -25,6 +25,7 @@ import { import { e2eDatabaseUrl } from "../../../scripts/e2e/harness"; import { applyOnboardingMigrations } from "../src/migrations"; +import { dbGate } from "../../../scripts/e2e/db-gate"; import { createDrizzlePendingSeedStore, type PendingSeed, @@ -38,7 +39,7 @@ function scratchUrlFor(e2eUrl: string): string { } const databaseUrl = e2eDatabaseUrl(); -const describeIfDb = databaseUrl === undefined ? describe.skip : describe; +const describeIfDb = dbGate(databaseUrl, import.meta.path); const KEY = new Uint8Array(32).fill(9); diff --git a/packages/preferences/test/migrations.test.ts b/packages/preferences/test/migrations.test.ts index a592c61a7..0b8543fd6 100644 --- a/packages/preferences/test/migrations.test.ts +++ b/packages/preferences/test/migrations.test.ts @@ -2,12 +2,13 @@ // still runs the unit gates), mirroring @corbits/insights' migrations test. // Runs against its own scratch database, never the developer's or the // walking-skeleton suite's. -import { afterAll, beforeAll, describe, expect, test } from "bun:test"; +import { afterAll, beforeAll, expect, test } from "bun:test"; import postgres from "postgres"; import { e2eDatabaseUrl } from "../../../scripts/e2e/harness"; import { applyPreferencesMigrations } from "../src/migrations"; import { createPostgresPreferencesStore } from "../src/pg-store"; +import { dbGate } from "../../../scripts/e2e/db-gate"; function scratchUrlFor(e2eUrl: string): string { const url = new URL(e2eUrl); @@ -17,7 +18,7 @@ function scratchUrlFor(e2eUrl: string): string { } const databaseUrl = e2eDatabaseUrl(); -const describeIfDb = databaseUrl === undefined ? describe.skip : describe; +const describeIfDb = dbGate(databaseUrl, import.meta.path); const migrationNames = ["0001_user_preferences"]; diff --git a/packages/routines/test/migrations.test.ts b/packages/routines/test/migrations.test.ts index 21a9a28d9..ed94f9d43 100644 --- a/packages/routines/test/migrations.test.ts +++ b/packages/routines/test/migrations.test.ts @@ -2,13 +2,14 @@ // checkout still runs the unit gates), mirroring // @corbits/chat's `migrations.test.ts`. Runs against its own scratch // database, never the developer's or the walking-skeleton suite's. -import { afterAll, beforeAll, describe, expect, test } from "bun:test"; +import { afterAll, beforeAll, expect, test } from "bun:test"; import { drizzle } from "drizzle-orm/postgres-js"; import postgres from "postgres"; import { e2eDatabaseUrl } from "../../../scripts/e2e/harness"; import { applyRoutineMigrations } from "../src/migrations"; import { createDrizzleRoutineStore } from "../src/store"; +import { dbGate } from "../../../scripts/e2e/db-gate"; function scratchUrlFor(e2eUrl: string): string { const url = new URL(e2eUrl); @@ -18,7 +19,7 @@ function scratchUrlFor(e2eUrl: string): string { } const databaseUrl = e2eDatabaseUrl(); -const describeIfDb = databaseUrl === undefined ? describe.skip : describe; +const describeIfDb = dbGate(databaseUrl, import.meta.path); describeIfDb("applyRoutineMigrations", () => { const scratchUrl = scratchUrlFor( diff --git a/packages/routines/test/store.drizzle.test.ts b/packages/routines/test/store.drizzle.test.ts index 93765c643..bcd245542 100644 --- a/packages/routines/test/store.drizzle.test.ts +++ b/packages/routines/test/store.drizzle.test.ts @@ -8,13 +8,14 @@ // `createDrizzleRoutineStore` path: backoff after failure, and the // edit-wins case where a concurrent trigger change must survive a // stale mark untouched. -import { afterAll, beforeAll, describe, expect, test } from "bun:test"; +import { afterAll, beforeAll, expect, test } from "bun:test"; import { drizzle } from "drizzle-orm/postgres-js"; import postgres from "postgres"; import { e2eDatabaseUrl } from "../../../scripts/e2e/harness"; import { applyRoutineMigrations } from "../src/migrations"; import { backoffMsForFailure, createDrizzleRoutineStore } from "../src/store"; +import { dbGate } from "../../../scripts/e2e/db-gate"; function scratchUrlFor(e2eUrl: string): string { const url = new URL(e2eUrl); @@ -24,7 +25,7 @@ function scratchUrlFor(e2eUrl: string): string { } const databaseUrl = e2eDatabaseUrl(); -const describeIfDb = databaseUrl === undefined ? describe.skip : describe; +const describeIfDb = dbGate(databaseUrl, import.meta.path); const TENANT_ID = "tnt_1"; diff --git a/packages/run-key-history/test/diagnostics.drizzle.test.ts b/packages/run-key-history/test/diagnostics.drizzle.test.ts index 2f8718339..4167e2366 100644 --- a/packages/run-key-history/test/diagnostics.drizzle.test.ts +++ b/packages/run-key-history/test/diagnostics.drizzle.test.ts @@ -5,7 +5,7 @@ // motivating incident needed — a retired run failing its challenge is // not the same fault as a live run with a genuinely diverged key or one // that was never acknowledged at all. -import { afterAll, beforeAll, describe, expect, test } from "bun:test"; +import { afterAll, beforeAll, expect, test } from "bun:test"; import { eq, inArray } from "drizzle-orm"; import { createDB, runMigrations, dropSchema, schema } from "@intx/db"; @@ -14,6 +14,7 @@ import { e2eDatabaseUrl } from "../../../scripts/e2e/harness"; import { applyRunKeyHistoryMigrations } from "../src/migrations"; import { createDrizzleRunKeyHistoryStore } from "../src/store"; import { runKeyHistory } from "../src/schema"; +import { dbGate } from "../../../scripts/e2e/db-gate"; import { countRunIdentityStates, getRunIdentityStatus, @@ -21,7 +22,7 @@ import { } from "../src/diagnostics"; const databaseUrl = e2eDatabaseUrl(); -const describeIfDb = databaseUrl === undefined ? describe.skip : describe; +const describeIfDb = dbGate(databaseUrl, import.meta.path); const SCHEMA = "run_key_history_diagnostics_test"; const TENANT = "tnt_run_key_history_diagnostics"; diff --git a/packages/run-key-history/test/migrations.test.ts b/packages/run-key-history/test/migrations.test.ts index acc0679ae..bb3322068 100644 --- a/packages/run-key-history/test/migrations.test.ts +++ b/packages/run-key-history/test/migrations.test.ts @@ -1,10 +1,11 @@ // DB-gated: runs against its own scratch database, never the // developer's or the walking-skeleton suite's. -import { afterAll, beforeAll, describe, expect, test } from "bun:test"; +import { afterAll, beforeAll, expect, test } from "bun:test"; import postgres from "postgres"; import { e2eDatabaseUrl } from "../../../scripts/e2e/harness"; import { applyRunKeyHistoryMigrations } from "../src/migrations"; +import { dbGate } from "../../../scripts/e2e/db-gate"; function scratchUrlFor(e2eUrl: string): string { const url = new URL(e2eUrl); @@ -14,7 +15,7 @@ function scratchUrlFor(e2eUrl: string): string { } const databaseUrl = e2eDatabaseUrl(); -const describeIfDb = databaseUrl === undefined ? describe.skip : describe; +const describeIfDb = dbGate(databaseUrl, import.meta.path); describeIfDb("applyRunKeyHistoryMigrations", () => { const scratchUrl = scratchUrlFor( diff --git a/packages/run-key-history/test/reconnect.drizzle.test.ts b/packages/run-key-history/test/reconnect.drizzle.test.ts index f61e56caf..7fe860ee9 100644 --- a/packages/run-key-history/test/reconnect.drizzle.test.ts +++ b/packages/run-key-history/test/reconnect.drizzle.test.ts @@ -12,7 +12,7 @@ // now-stale `workflow_run` row and fails forever. `lookupRunKeyHistoryReconnectKey` // is the repair: it notices the disagreement and republishes this // package's own record. -import { afterAll, beforeAll, describe, expect, test } from "bun:test"; +import { afterAll, beforeAll, expect, test } from "bun:test"; import { eq } from "drizzle-orm"; import { createDB, runMigrations, dropSchema, schema } from "@intx/db"; import { workflowRun } from "@intx/db/schema"; @@ -22,9 +22,10 @@ import { e2eDatabaseUrl } from "../../../scripts/e2e/harness"; import { applyRunKeyHistoryMigrations } from "../src/migrations"; import { createDrizzleRunKeyHistoryStore } from "../src/store"; import { lookupRunKeyHistoryReconnectKey } from "../src/reconnect"; +import { dbGate } from "../../../scripts/e2e/db-gate"; const databaseUrl = e2eDatabaseUrl(); -const describeIfDb = databaseUrl === undefined ? describe.skip : describe; +const describeIfDb = dbGate(databaseUrl, import.meta.path); const SCHEMA = "run_key_history_reconnect_test"; const TENANT = "tnt_run_key_history_reconnect"; diff --git a/packages/run-key-history/test/routes.drizzle.test.ts b/packages/run-key-history/test/routes.drizzle.test.ts index a42b8af8c..cbb19e192 100644 --- a/packages/run-key-history/test/routes.drizzle.test.ts +++ b/packages/run-key-history/test/routes.drizzle.test.ts @@ -2,7 +2,7 @@ // diagnostics.drizzle.test.ts and @corbits/insights' routes-scope.test.ts. // Proves the HTTP surface an operator actually reaches: /runs/:runAddress // for a single run's lifecycle + status, /summary for tenant-wide counts. -import { afterAll, beforeAll, describe, expect, test } from "bun:test"; +import { afterAll, beforeAll, expect, test } from "bun:test"; import { Hono } from "hono"; import type { MiddlewareHandler } from "hono"; import { inArray } from "drizzle-orm"; @@ -16,9 +16,10 @@ import { applyRunKeyHistoryMigrations } from "../src/migrations"; import { createDrizzleRunKeyHistoryStore } from "../src/store"; import { runKeyHistory } from "../src/schema"; import { createRunKeyHistoryRoutes } from "../src/routes"; +import { dbGate } from "../../../scripts/e2e/db-gate"; const databaseUrl = e2eDatabaseUrl(); -const describeIfDb = databaseUrl === undefined ? describe.skip : describe; +const describeIfDb = dbGate(databaseUrl, import.meta.path); const allowAll: RequireGrant = () => async (_c, next) => { await next(); diff --git a/packages/run-key-history/test/store.test.ts b/packages/run-key-history/test/store.test.ts index 6766e36da..971cd5be8 100644 --- a/packages/run-key-history/test/store.test.ts +++ b/packages/run-key-history/test/store.test.ts @@ -2,13 +2,14 @@ // semantics against a real Postgres transaction, using the same // scratch-database setup pattern every DB-gated suite in this repo // uses. -import { afterAll, beforeAll, describe, expect, test } from "bun:test"; +import { afterAll, beforeAll, expect, test } from "bun:test"; import { drizzle } from "drizzle-orm/postgres-js"; import postgres from "postgres"; import { e2eDatabaseUrl } from "../../../scripts/e2e/harness"; import { applyRunKeyHistoryMigrations } from "../src/migrations"; import { createDrizzleRunKeyHistoryStore } from "../src/store"; +import { dbGate } from "../../../scripts/e2e/db-gate"; function scratchUrlFor(e2eUrl: string): string { const url = new URL(e2eUrl); @@ -18,7 +19,7 @@ function scratchUrlFor(e2eUrl: string): string { } const databaseUrl = e2eDatabaseUrl(); -const describeIfDb = databaseUrl === undefined ? describe.skip : describe; +const describeIfDb = dbGate(databaseUrl, import.meta.path); describeIfDb("createDrizzleRunKeyHistoryStore", () => { const scratchUrl = scratchUrlFor( diff --git a/packages/run-scope/test/scope-routes.drizzle.test.ts b/packages/run-scope/test/scope-routes.drizzle.test.ts index 7e481dc0e..32007aa25 100644 --- a/packages/run-scope/test/scope-routes.drizzle.test.ts +++ b/packages/run-scope/test/scope-routes.drizzle.test.ts @@ -12,7 +12,7 @@ // other by `workflow_run`'s own columns, see `@corbits/folded-runs`' // `launch.ts`'s big comment) never appears in this scoped listing, // while a genuine top-level deployment run does. -import { afterAll, beforeAll, describe, expect, test } from "bun:test"; +import { afterAll, beforeAll, expect, test } from "bun:test"; import { eq } from "drizzle-orm"; import { createDB, runMigrations, dropSchema } from "@intx/db"; import { schema } from "@intx/db"; @@ -22,9 +22,10 @@ import { foldedRun } from "@corbits/folded-runs"; import { dbTargetFromUrl } from "../../../scripts/db-setup"; import { e2eDatabaseUrl } from "../../../scripts/e2e/harness"; import { listTopLevelRuns, listTopLevelRunFires } from "../src/scope-routes"; +import { dbGate } from "../../../scripts/e2e/db-gate"; const databaseUrl = e2eDatabaseUrl(); -const describeIfDb = databaseUrl === undefined ? describe.skip : describe; +const describeIfDb = dbGate(databaseUrl, import.meta.path); const SCHEMA = "folded_runs_scope_routes_test"; const TENANT = "tnt_scope_routes"; diff --git a/packages/sidecar-placement/test/store.drizzle.test.ts b/packages/sidecar-placement/test/store.drizzle.test.ts index 7a1c21155..5c9cb2ddd 100644 --- a/packages/sidecar-placement/test/store.drizzle.test.ts +++ b/packages/sidecar-placement/test/store.drizzle.test.ts @@ -6,16 +6,17 @@ // in-memory fake — a read-modify-write bug here would silently drop // another domain's config keys or clobber a concurrent writer, neither // of which the in-memory store's tests can catch. -import { afterAll, beforeAll, describe, expect, test } from "bun:test"; +import { afterAll, beforeAll, expect, test } from "bun:test"; import { eq } from "drizzle-orm"; import { createDB, dropSchema, runMigrations, schema } from "@intx/db"; import { dbTargetFromUrl } from "../../../scripts/db-setup"; import { e2eDatabaseUrl } from "../../../scripts/e2e/harness"; import { createDrizzleSidecarPlacementStore } from "../src/store"; +import { dbGate } from "../../../scripts/e2e/db-gate"; const databaseUrl = e2eDatabaseUrl(); -const describeIfDb = databaseUrl === undefined ? describe.skip : describe; +const describeIfDb = dbGate(databaseUrl, import.meta.path); const SCHEMA = "sidecar_placement_store_test"; diff --git a/packages/webhook-triggers/test/store.drizzle.test.ts b/packages/webhook-triggers/test/store.drizzle.test.ts index b42a66cd2..8bbc76eb4 100644 --- a/packages/webhook-triggers/test/store.drizzle.test.ts +++ b/packages/webhook-triggers/test/store.drizzle.test.ts @@ -10,7 +10,7 @@ // exact plaintext on read — and that a real (non-noop) cipher's // ciphertext is not the plaintext secret, so a raw table dump does not // disclose it. -import { afterAll, beforeAll, describe, expect, test } from "bun:test"; +import { afterAll, beforeAll, expect, test } from "bun:test"; import { drizzle } from "drizzle-orm/postgres-js"; import postgres from "postgres"; import { @@ -21,6 +21,7 @@ import { import { e2eDatabaseUrl } from "../../../scripts/e2e/harness"; import { applyWebhookTriggersMigrations } from "../src/migrations"; import { createDrizzleWebhookTriggerStore } from "../src/store"; +import { dbGate } from "../../../scripts/e2e/db-gate"; function scratchUrlFor(e2eUrl: string): string { const url = new URL(e2eUrl); @@ -30,7 +31,7 @@ function scratchUrlFor(e2eUrl: string): string { } const databaseUrl = e2eDatabaseUrl(); -const describeIfDb = databaseUrl === undefined ? describe.skip : describe; +const describeIfDb = dbGate(databaseUrl, import.meta.path); const TENANT_ID = "tnt_1"; const KEY = new Uint8Array(32).fill(7); diff --git a/packages/workflow-deploy-source/test/migrations.test.ts b/packages/workflow-deploy-source/test/migrations.test.ts index 284a93805..c1138bc34 100644 --- a/packages/workflow-deploy-source/test/migrations.test.ts +++ b/packages/workflow-deploy-source/test/migrations.test.ts @@ -1,11 +1,12 @@ // DB-gated: runs against its own scratch database, never the // developer's or the walking-skeleton suite's, mirroring // packages/run-key-history/test/migrations.test.ts. -import { afterAll, beforeAll, describe, expect, test } from "bun:test"; +import { afterAll, beforeAll, expect, test } from "bun:test"; import postgres from "postgres"; import { e2eDatabaseUrl } from "../../../scripts/e2e/harness"; import { applyWorkflowDeploySourceMigrations } from "../src/migrations"; +import { dbGate } from "../../../scripts/e2e/db-gate"; function scratchUrlFor(e2eUrl: string): string { const url = new URL(e2eUrl); @@ -15,7 +16,7 @@ function scratchUrlFor(e2eUrl: string): string { } const databaseUrl = e2eDatabaseUrl(); -const describeIfDb = databaseUrl === undefined ? describe.skip : describe; +const describeIfDb = dbGate(databaseUrl, import.meta.path); describeIfDb("applyWorkflowDeploySourceMigrations", () => { const scratchUrl = scratchUrlFor( diff --git a/packages/workflow-deploy-source/test/store.test.ts b/packages/workflow-deploy-source/test/store.test.ts index 514b5a2ec..910f97934 100644 --- a/packages/workflow-deploy-source/test/store.test.ts +++ b/packages/workflow-deploy-source/test/store.test.ts @@ -1,13 +1,14 @@ // DB-gated: proves `record`'s upsert semantics and the redeploy round trip // against a real Postgres transaction, mirroring // packages/run-key-history/test/store.test.ts's scratch-database setup. -import { afterAll, beforeAll, describe, expect, test } from "bun:test"; +import { afterAll, beforeAll, expect, test } from "bun:test"; import { drizzle } from "drizzle-orm/postgres-js"; import postgres from "postgres"; import { e2eDatabaseUrl } from "../../../scripts/e2e/harness"; import { applyWorkflowDeploySourceMigrations } from "../src/migrations"; import { createDrizzleWorkflowDeploySourceStore } from "../src/store"; +import { dbGate } from "../../../scripts/e2e/db-gate"; function scratchUrlFor(e2eUrl: string): string { const url = new URL(e2eUrl); @@ -17,7 +18,7 @@ function scratchUrlFor(e2eUrl: string): string { } const databaseUrl = e2eDatabaseUrl(); -const describeIfDb = databaseUrl === undefined ? describe.skip : describe; +const describeIfDb = dbGate(databaseUrl, import.meta.path); describeIfDb("createDrizzleWorkflowDeploySourceStore", () => { const scratchUrl = scratchUrlFor( diff --git a/packages/workflow-freeze/test/freeze.drizzle.test.ts b/packages/workflow-freeze/test/freeze.drizzle.test.ts index 1c7fddc68..4ff959775 100644 --- a/packages/workflow-freeze/test/freeze.drizzle.test.ts +++ b/packages/workflow-freeze/test/freeze.drizzle.test.ts @@ -4,7 +4,7 @@ // unlaunchable), and the in-place re-freeze both follows an edit and // heals a legacy row frozen before the projection was recorded. // Runs against its own scratch database, never the developer's. -import { afterAll, beforeAll, describe, expect, test } from "bun:test"; +import { afterAll, beforeAll, expect, test } from "bun:test"; import postgres from "postgres"; import { eq } from "drizzle-orm"; @@ -23,6 +23,7 @@ import { defineWorkflow, step } from "@intx/workflow"; import { setupDatabase, dbTargetFromUrl } from "../../../scripts/db-setup"; import { e2eDatabaseUrl } from "../../../scripts/e2e/harness"; +import { dbGate } from "../../../scripts/e2e/db-gate"; import { freezeInertWorkflowDefinition, refreezeWorkflowDefinitionProjection, @@ -56,7 +57,7 @@ function agentWorkflowJson(systemPrompt: string): string { } const databaseUrl = e2eDatabaseUrl(); -const describeIfDb = databaseUrl === undefined ? describe.skip : describe; +const describeIfDb = dbGate(databaseUrl, import.meta.path); describeIfDb("freezeInertWorkflowDefinition against Postgres", () => { const scratchUrl = scratchUrlFor( diff --git a/scripts/db-setup.test.ts b/scripts/db-setup.test.ts index 29b7d2e25..74cb0e27e 100644 --- a/scripts/db-setup.test.ts +++ b/scripts/db-setup.test.ts @@ -3,12 +3,13 @@ // the pre-re-pin numbering of workbench's own two migrations) is refused // with the reset instruction instead of being patched incrementally. // DB-gated: skipped when DATABASE_URL is unset. -import { describe, expect, test } from "bun:test"; +import { expect, test } from "bun:test"; import { dbTargetFromUrl, loadPostgres, setupDatabase } from "./db-setup"; +import { dbGate } from "./e2e/db-gate"; const databaseUrl = process.env["DATABASE_URL"] ?? ""; -const describeIfDb = databaseUrl === "" ? describe.skip : describe; +const describeIfDb = dbGate(databaseUrl, import.meta.path); const OLD_NUMBERING_TAIL = [ "0084_delete_orphaned_credential_grants.sql", diff --git a/scripts/e2e/db-gate.ts b/scripts/e2e/db-gate.ts new file mode 100644 index 000000000..357df27f9 --- /dev/null +++ b/scripts/e2e/db-gate.ts @@ -0,0 +1,54 @@ +// Central definition of "this suite needs a real database." Every +// DB-gated suite defines its own describeIfDb by hand-checking +// DATABASE_URL, which means a missing database skips the suite in +// total silence — a broken CI env would report green the same way a +// real pass does. dbGate is the one place that decides what a skip +// means: it counts the skip, prints an unmissable summary once the +// run ends, and — under E2E_REQUIRED=1 (this repo's convention for +// turning a DB skip into a hard failure, see harness.ts) — throws +// instead of skipping. +import { afterAll, describe } from "bun:test"; + +// `bun test` never fires `process.on("exit"/"beforeExit")` handlers, so a +// true end-of-run hook does not exist across files — each skipped file +// registers its own `afterAll`, printed against the shared, growing list +// below. The last skip of the run is always the one whose banner shows +// the complete count, so nothing is lost; earlier banners just show the +// running total, which is itself already loud enough not to miss. +const skipped: string[] = []; + +function printSummary(): void { + const rule = "=".repeat(78); + const lines = [ + "", + rule, + `SKIPPED ${skipped.length} DB-gated suite(s) so far — no DATABASE_URL. This is NOT a pass.`, + ...skipped.map((label) => ` - ${label}`), + "", + "To run them: docker compose -f docker-compose.test.yml up -d, then", + "DATABASE_URL=postgres://postgres:postgres@localhost:5432/workbench bun test ...", + "Set E2E_REQUIRED=1 to make a skip like this a hard failure (CI does).", + rule, + "", + ]; + process.stderr.write(lines.join("\n") + "\n"); +} + +/** + * databaseUrl: the resolved DATABASE_URL (or "" / undefined when absent). + * label: identifies the skipped suite in the summary — pass import.meta.path. + */ +export function dbGate( + databaseUrl: string | undefined, + label: string, +): typeof describe { + if (databaseUrl !== undefined && databaseUrl !== "") return describe; + skipped.push(label); + afterAll(printSummary); + if (process.env["E2E_REQUIRED"] === "1") { + throw new Error( + `E2E_REQUIRED=1 but DATABASE_URL is not set; "${label}" would be skipped.`, + ); + } + return describe.skip; +} diff --git a/scripts/e2e/folded-run-backfill.test.ts b/scripts/e2e/folded-run-backfill.test.ts index ed43d9321..beaf027a3 100644 --- a/scripts/e2e/folded-run-backfill.test.ts +++ b/scripts/e2e/folded-run-backfill.test.ts @@ -25,11 +25,12 @@ // `scripts/db-setup.ts` no longer reads those tables, so this test no // longer seeds them either. The tables themselves are untouched in any // database that already has them. -import { afterAll, beforeAll, describe, expect, test } from "bun:test"; +import { afterAll, beforeAll, expect, test } from "bun:test"; import path from "node:path"; import { dbTargetFromUrl } from "../db-setup.ts"; import { connectE2eDb, e2eDatabaseUrl, REPO_ROOT } from "./harness.ts"; +import { dbGate } from "./db-gate"; import { applyFoldedRunsMigrations, backfillFoldedRunMarkers, @@ -72,7 +73,7 @@ async function loadIntxDb(): Promise { } const databaseUrl = e2eDatabaseUrl(); -const describeIfDb = databaseUrl === undefined ? describe.skip : describe; +const describeIfDb = dbGate(databaseUrl, import.meta.path); const TENANT = "tnt_backfill"; From 1e7885bfe58c10786f735c733f8516eeed82def1 Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Sun, 30 Aug 2026 20:58:13 -0700 Subject: [PATCH 3/5] Update docs: local Postgres compose file and loud DB-skip summary Points developers at docker-compose.test.yml for a Postgres matching CI, and documents dbGate's skip summary and E2E_REQUIRED for the package/hub DB-gated suites alongside the existing e2e docs. --- README.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/README.md b/README.md index a86f64189..bb7d61e19 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,13 @@ brew install postgresql@17 pgvector brew services start postgresql@17 ``` +Or, to match what CI runs against exactly, bring up +[`docker-compose.test.yml`](docker-compose.test.yml) instead: + +```sh +docker compose -f docker-compose.test.yml up -d +``` + Then: ```sh @@ -198,6 +205,24 @@ prove (see `smoke-onboarding.test.ts`) or don't write that scenario. Run one file directly with `bun test scripts/e2e/smoke-auth.test.ts` (`DATABASE_URL` still required). +### DB-gated package and hub suites + +Beyond the e2e suite, most packages and `apps/hub` carry their own +DB-gated tests (migrations, Drizzle-backed stores, route integration +tests) that skip via `describeIfDb` when `DATABASE_URL` is unset. A +skip is never silent: `scripts/e2e/db-gate.ts`'s `dbGate` — what +`describeIfDb` is built on — prints an unmissable summary naming every +skipped suite at the end of the run, and honors the same +`E2E_REQUIRED=1` convention as the e2e suite above, turning a skip into +a hard failure. CI sets `E2E_REQUIRED=1` for exactly this reason. + +To run these locally against the same Postgres CI uses: + +```sh +docker compose -f docker-compose.test.yml up -d +DATABASE_URL=postgres://postgres:postgres@localhost:5432/workbench bun test packages apps/hub/test +``` + ## License The application (`apps/` and the rest of this repo) is GPLv2 with the From 7d7d36fd0a361a1d45dc40dd9629339debc3562c Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Sun, 30 Aug 2026 21:05:01 -0700 Subject: [PATCH 4/5] Add tests for check:db-gate, the hand-rolled-gate check Covers: a hand-rolled describeIfDb ternary (both the undefined and "" variants) is a violation naming the file; a suite already routed through dbGate, or with no DB gate at all, passes. --- scripts/checks/test/db-gate.test.ts | 50 +++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 scripts/checks/test/db-gate.test.ts diff --git a/scripts/checks/test/db-gate.test.ts b/scripts/checks/test/db-gate.test.ts new file mode 100644 index 000000000..2db69dd75 --- /dev/null +++ b/scripts/checks/test/db-gate.test.ts @@ -0,0 +1,50 @@ +import { expect, test } from "bun:test"; +import { auditDbGate } from "../db-gate"; + +test("a hand-rolled describeIfDb ternary is a violation naming the file", () => { + const report = auditDbGate([ + { + relPath: "packages/example/test/store.test.ts", + contents: + 'const databaseUrl = process.env["DATABASE_URL"] ?? "";\n' + + 'const describeIfDb = databaseUrl === "" ? describe.skip : describe;\n', + }, + ]); + expect(report.violations).toHaveLength(1); + expect(report.violations[0]).toContain("packages/example/test/store.test.ts"); + expect(report.violations[0]).toContain("dbGate"); +}); + +test("the undefined-check variant of the ternary is also a violation", () => { + const report = auditDbGate([ + { + relPath: "packages/example/test/migrations.test.ts", + contents: + "const databaseUrl = e2eDatabaseUrl();\n" + + "const describeIfDb = databaseUrl === undefined ? describe.skip : describe;\n", + }, + ]); + expect(report.violations).toHaveLength(1); +}); + +test("a file already routed through dbGate passes", () => { + const report = auditDbGate([ + { + relPath: "packages/example/test/store.test.ts", + contents: + "const databaseUrl = e2eDatabaseUrl();\n" + + "const describeIfDb = dbGate(databaseUrl, import.meta.path);\n", + }, + ]); + expect(report.violations).toEqual([]); +}); + +test("a file with no DB gate at all passes", () => { + const report = auditDbGate([ + { + relPath: "packages/example/test/pure-unit.test.ts", + contents: 'test("adds", () => {});\n', + }, + ]); + expect(report.violations).toEqual([]); +}); From 64d6c580614f650a446052ae53359240f6e586a5 Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Sun, 30 Aug 2026 21:05:15 -0700 Subject: [PATCH 5/5] Sweep the 9 hand-rolled gates main grew, add check:db-gate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nine PRs merged since the first CL-7279 sweep, adding 9 more (10 counting a pre-existing one the original grep missed) files that still hand-rolled their own describeIfDb ternary instead of going through dbGate — proving a one-time sweep doesn't hold. Repointed all of them at dbGate; vendor/intx/hub-api's copy is left alone, since editing inside a vendored tree carries re-pin tax. check:db-gate makes the invariant self-enforcing: it fails on any `databaseUrl === undefined/"" ? describe.skip : describe` ternary outside scripts/e2e/db-gate.ts itself, in the same structural job the other checks already run in. --- .github/workflows/ci.yml | 1 + package.json | 3 +- .../chat/test/block-responses.drizzle.test.ts | 5 +- packages/chat/test/migrations.test.ts | 5 +- .../settings-participants.drizzle.test.ts | 5 +- .../chat/test/settings-patch.drizzle.test.ts | 5 +- packages/inbox/test/snooze-store.test.ts | 5 +- .../test/migration-runner.test.ts | 5 +- .../webhook-triggers/test/migrations.test.ts | 5 +- .../test/repo-review-lease.drizzle.test.ts | 5 +- .../test/connect-github-setup.race.test.ts | 5 +- .../reconcile-duplicate-repo-grants.test.ts | 5 +- scripts/checks/db-gate.ts | 76 +++++++++++++++++++ 13 files changed, 109 insertions(+), 21 deletions(-) create mode 100644 scripts/checks/db-gate.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 400142ffe..54b22519b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -65,6 +65,7 @@ jobs: - run: bun run check:killdates - run: bun run check:packages - run: bun run check:licenses + - run: bun run check:db-gate - run: bun run check:no-product-tenancy - run: bun run check:browser-safe-subpaths - run: bun run check:web-utilities diff --git a/package.json b/package.json index 13e42f442..8d3d02595 100644 --- a/package.json +++ b/package.json @@ -26,12 +26,13 @@ "setup:memory": "bun run scripts/setup-memory.ts", "seed": "bun packages/cli/src/index.ts seed", "reset": "bun packages/cli/src/index.ts reset", - "check:structural": "bun test scripts/checks/test && bun run check:deletion && bun run check:killdates && bun run check:licenses && bun run check:no-product-tenancy && bun run check:browser-safe-subpaths && bun run check:web-utilities && bun run check:tailwind-source && bun run check:ui-vocabulary && bun run check:react-ui-drift && bun run check:react-ui-pin && bun run check:tool-package-pins && bun run check:tool-package-freshness && bun run check:report-error && bun run check:tsconfig-references", + "check:structural": "bun test scripts/checks/test && bun run check:deletion && bun run check:killdates && bun run check:licenses && bun run check:db-gate && bun run check:no-product-tenancy && bun run check:browser-safe-subpaths && bun run check:web-utilities && bun run check:tailwind-source && bun run check:ui-vocabulary && bun run check:react-ui-drift && bun run check:react-ui-pin && bun run check:tool-package-pins && bun run check:tool-package-freshness && bun run check:report-error && bun run check:tsconfig-references", "check:deletion": "bun run scripts/checks/deletion.ts", "check:report-error": "bun run scripts/checks/report-error.ts", "check:killdates": "bun run scripts/checks/killdates.ts", "check:packages": "bun run scripts/checks/packages.ts", "check:licenses": "bun run scripts/checks/licenses.ts", + "check:db-gate": "bun run scripts/checks/db-gate.ts", "check:no-product-tenancy": "bun run scripts/checks/no-product-tenancy.ts", "check:browser-safe-subpaths": "bun run scripts/checks/browser-safe-subpaths.ts", "check:web-utilities": "bun run scripts/checks/web-tailwind-utilities.ts", diff --git a/packages/chat/test/block-responses.drizzle.test.ts b/packages/chat/test/block-responses.drizzle.test.ts index 20b7553e2..33a1750c2 100644 --- a/packages/chat/test/block-responses.drizzle.test.ts +++ b/packages/chat/test/block-responses.drizzle.test.ts @@ -9,7 +9,7 @@ // submission for the same (tenant, workbench, message, block, principal), // where the in-memory store's single-threaded tests can't exercise a real // lock/serialization path the production store depends on. -import { afterAll, beforeAll, describe, expect, test } from "bun:test"; +import { afterAll, beforeAll, expect, test } from "bun:test"; import { drizzle } from "drizzle-orm/postgres-js"; import postgres from "postgres"; @@ -17,6 +17,7 @@ import { e2eDatabaseUrl } from "../../../scripts/e2e/harness"; import { applyChatMigrations } from "../src/migrations"; import { createDrizzleBlockResponseStore } from "../src/block-responses"; import type { BlockResponseKey } from "../src/block-responses"; +import { dbGate } from "../../../scripts/e2e/db-gate"; function scratchUrlFor(e2eUrl: string): string { const url = new URL(e2eUrl); @@ -26,7 +27,7 @@ function scratchUrlFor(e2eUrl: string): string { } const databaseUrl = e2eDatabaseUrl(); -const describeIfDb = databaseUrl === undefined ? describe.skip : describe; +const describeIfDb = dbGate(databaseUrl, import.meta.path); describeIfDb( "createDrizzleBlockResponseStore: concurrent notification claim", diff --git a/packages/chat/test/migrations.test.ts b/packages/chat/test/migrations.test.ts index 491a0b8f6..2a67ddf53 100644 --- a/packages/chat/test/migrations.test.ts +++ b/packages/chat/test/migrations.test.ts @@ -4,11 +4,12 @@ // mirroring scripts/e2e/harness.ts's e2eDatabaseUrl/baseUrlToE2eUrl. // Runs against its own scratch database, never the developer's or the // walking-skeleton suite's, so a failure here can never corrupt either. -import { afterAll, beforeAll, describe, expect, test } from "bun:test"; +import { afterAll, beforeAll, expect, test } from "bun:test"; import postgres from "postgres"; import { e2eDatabaseUrl } from "../../../scripts/e2e/harness"; import { applyChatMigrations, chatMigrations } from "../src/migrations"; +import { dbGate } from "../../../scripts/e2e/db-gate"; function scratchUrlFor(e2eUrl: string): string { const url = new URL(e2eUrl); @@ -18,7 +19,7 @@ function scratchUrlFor(e2eUrl: string): string { } const databaseUrl = e2eDatabaseUrl(); -const describeIfDb = databaseUrl === undefined ? describe.skip : describe; +const describeIfDb = dbGate(databaseUrl, import.meta.path); const migrationNames = [ "0001_channel_settings", diff --git a/packages/chat/test/settings-participants.drizzle.test.ts b/packages/chat/test/settings-participants.drizzle.test.ts index 42096c370..69ff3e9ae 100644 --- a/packages/chat/test/settings-participants.drizzle.test.ts +++ b/packages/chat/test/settings-participants.drizzle.test.ts @@ -13,7 +13,7 @@ // overlapping calls serialize instead of clobbering each other. This test // fires two real concurrent transactions (two connections from the pool) // at the same row and proves both participants land. -import { afterAll, beforeAll, describe, expect, test } from "bun:test"; +import { afterAll, beforeAll, expect, test } from "bun:test"; import { drizzle } from "drizzle-orm/postgres-js"; import postgres from "postgres"; @@ -22,6 +22,7 @@ import { applyChatMigrations } from "../src/migrations"; import { addParticipant } from "../src/participants"; import { createDrizzleChatStore } from "../src/store"; import { participantsOf } from "../src/workbench-settings"; +import { dbGate } from "../../../scripts/e2e/db-gate"; function scratchUrlFor(e2eUrl: string): string { const url = new URL(e2eUrl); @@ -31,7 +32,7 @@ function scratchUrlFor(e2eUrl: string): string { } const databaseUrl = e2eDatabaseUrl(); -const describeIfDb = databaseUrl === undefined ? describe.skip : describe; +const describeIfDb = dbGate(databaseUrl, import.meta.path); const TENANT = "tnt_1"; diff --git a/packages/chat/test/settings-patch.drizzle.test.ts b/packages/chat/test/settings-patch.drizzle.test.ts index 13dcac915..2859787db 100644 --- a/packages/chat/test/settings-patch.drizzle.test.ts +++ b/packages/chat/test/settings-patch.drizzle.test.ts @@ -2,13 +2,14 @@ // checkout still runs the unit gates), mirroring `read-state.drizzle.test.ts`. // Two concurrent `patchWorkbenchSettings` calls on one row must both land: // a participants write and a `chat/pinned` write cannot clobber each other. -import { afterAll, beforeAll, describe, expect, test } from "bun:test"; +import { afterAll, beforeAll, expect, test } from "bun:test"; import { drizzle } from "drizzle-orm/postgres-js"; import postgres from "postgres"; import { e2eDatabaseUrl } from "../../../scripts/e2e/harness"; import { applyChatMigrations } from "../src/migrations"; import { createDrizzleChatStore } from "../src/store"; +import { dbGate } from "../../../scripts/e2e/db-gate"; function scratchUrlFor(e2eUrl: string): string { const url = new URL(e2eUrl); @@ -18,7 +19,7 @@ function scratchUrlFor(e2eUrl: string): string { } const databaseUrl = e2eDatabaseUrl(); -const describeIfDb = databaseUrl === undefined ? describe.skip : describe; +const describeIfDb = dbGate(databaseUrl, import.meta.path); const TENANT = "tnt_1"; const WORKBENCH = "run_workbench1"; diff --git a/packages/inbox/test/snooze-store.test.ts b/packages/inbox/test/snooze-store.test.ts index c0d2ce958..759c37e48 100644 --- a/packages/inbox/test/snooze-store.test.ts +++ b/packages/inbox/test/snooze-store.test.ts @@ -7,7 +7,7 @@ // `open` and removes the snooze row once `until` has passed. Runs against // its own scratch database, never the developer's or the walking-skeleton // suite's. -import { afterAll, beforeAll, describe, expect, test } from "bun:test"; +import { afterAll, beforeAll, expect, test } from "bun:test"; import postgres from "postgres"; import { Hono } from "hono"; import type { TenantEnv } from "@intx/hub-api"; @@ -25,6 +25,7 @@ import { import { setupDatabase } from "../../../scripts/db-setup"; import { e2eDatabaseUrl } from "../../../scripts/e2e/harness"; import { createInboxRoutes } from "../src/routes"; +import { dbGate } from "../../../scripts/e2e/db-gate"; import { claimAndReopenSnooze, clearSnoozeUntil, @@ -51,7 +52,7 @@ function dbConfigFromUrl(databaseUrl: string) { } const databaseUrl = e2eDatabaseUrl(); -const describeIfDb = databaseUrl === undefined ? describe.skip : describe; +const describeIfDb = dbGate(databaseUrl, import.meta.path); describeIfDb("snooze-store against a real inbox.snooze table", () => { const scratchUrl = scratchUrlFor( diff --git a/packages/migration-runner/test/migration-runner.test.ts b/packages/migration-runner/test/migration-runner.test.ts index e361a6b0a..ef2d6522c 100644 --- a/packages/migration-runner/test/migration-runner.test.ts +++ b/packages/migration-runner/test/migration-runner.test.ts @@ -2,11 +2,12 @@ // still runs the unit gates), mirroring the six package migration test // suites this runner replaces the mechanics of. Runs against its own // scratch database, never the developer's or the walking-skeleton suite's. -import { afterAll, beforeAll, describe, expect, test } from "bun:test"; +import { afterAll, beforeAll, expect, test } from "bun:test"; import postgres from "postgres"; import { e2eDatabaseUrl } from "../../../scripts/e2e/harness"; import { applyPackageMigrations, type PackageMigration } from "../src/index"; +import { dbGate } from "../../../scripts/e2e/db-gate"; function scratchUrlFor(e2eUrl: string): string { const url = new URL(e2eUrl); @@ -16,7 +17,7 @@ function scratchUrlFor(e2eUrl: string): string { } const databaseUrl = e2eDatabaseUrl(); -const describeIfDb = databaseUrl === undefined ? describe.skip : describe; +const describeIfDb = dbGate(databaseUrl, import.meta.path); const SCHEMA = "migration_runner_test"; const LEDGER_TABLE = "migration_runner_test_migrations"; diff --git a/packages/webhook-triggers/test/migrations.test.ts b/packages/webhook-triggers/test/migrations.test.ts index 8040daeeb..73d95f10f 100644 --- a/packages/webhook-triggers/test/migrations.test.ts +++ b/packages/webhook-triggers/test/migrations.test.ts @@ -4,11 +4,12 @@ // mirroring `@corbits/chat`'s own migrations test. Runs against its // own scratch database, never the developer's or the walking-skeleton // suite's, so a failure here can never corrupt either. -import { afterAll, beforeAll, describe, expect, test } from "bun:test"; +import { afterAll, beforeAll, expect, test } from "bun:test"; import postgres from "postgres"; import { e2eDatabaseUrl } from "../../../scripts/e2e/harness"; import { applyWebhookTriggersMigrations } from "../src/migrations"; +import { dbGate } from "../../../scripts/e2e/db-gate"; function scratchUrlFor(e2eUrl: string): string { const url = new URL(e2eUrl); @@ -18,7 +19,7 @@ function scratchUrlFor(e2eUrl: string): string { } const databaseUrl = e2eDatabaseUrl(); -const describeIfDb = databaseUrl === undefined ? describe.skip : describe; +const describeIfDb = dbGate(databaseUrl, import.meta.path); const migrationNames = [ "0001_webhook_trigger", diff --git a/packages/webhook-triggers/test/repo-review-lease.drizzle.test.ts b/packages/webhook-triggers/test/repo-review-lease.drizzle.test.ts index 65865a181..577e2aac6 100644 --- a/packages/webhook-triggers/test/repo-review-lease.drizzle.test.ts +++ b/packages/webhook-triggers/test/repo-review-lease.drizzle.test.ts @@ -6,12 +6,13 @@ // being stolen, and a released lease being immediately reacquirable — // against a real Postgres. A mocked port proves nothing about // `ON CONFLICT ... DO UPDATE ... WHERE`. -import { afterAll, beforeAll, describe, expect, test } from "bun:test"; +import { afterAll, beforeAll, expect, test } from "bun:test"; import { drizzle } from "drizzle-orm/postgres-js"; import postgres from "postgres"; import { applyWebhookTriggersMigrations } from "../src/migrations"; import { createDrizzleRepoReviewLeaseStore } from "../src/repo-review-lease"; +import { dbGate } from "../../../scripts/e2e/db-gate"; function scratchUrlFor(e2eUrl: string): string { const url = new URL(e2eUrl); @@ -21,7 +22,7 @@ function scratchUrlFor(e2eUrl: string): string { } const databaseUrl = process.env["DATABASE_URL"] ?? ""; -const describeIfDb = databaseUrl === "" ? describe.skip : describe; +const describeIfDb = dbGate(databaseUrl, import.meta.path); const TENANT_ID = "tnt_1"; diff --git a/packages/workflow-catalog/test/connect-github-setup.race.test.ts b/packages/workflow-catalog/test/connect-github-setup.race.test.ts index 1ca56a228..2e288d577 100644 --- a/packages/workflow-catalog/test/connect-github-setup.race.test.ts +++ b/packages/workflow-catalog/test/connect-github-setup.race.test.ts @@ -17,7 +17,7 @@ // when DATABASE_URL is unset. Runs against its own scratch database // (mirroring `@corbits/webhook-triggers`' own `store.drizzle.test.ts`), // never the developer's or the walking-skeleton suite's. -import { afterAll, beforeAll, describe, expect, test } from "bun:test"; +import { afterAll, beforeAll, expect, test } from "bun:test"; import { drizzle } from "drizzle-orm/postgres-js"; import { and, eq } from "drizzle-orm"; import postgres from "postgres"; @@ -34,6 +34,7 @@ import { import type { GitHubRepoSummary } from "@corbits/github-tools"; import { e2eDatabaseUrl } from "../../../scripts/e2e/harness"; +import { dbGate } from "../../../scripts/e2e/db-gate"; import { startReviewingRepos, webhookTriggerName, @@ -48,7 +49,7 @@ function scratchUrlFor(e2eUrl: string): string { } const databaseUrl = e2eDatabaseUrl(); -const describeIfDb = databaseUrl === undefined ? describe.skip : describe; +const describeIfDb = dbGate(databaseUrl, import.meta.path); const REPO: GitHubRepoSummary = { id: "1", diff --git a/packages/workflow-catalog/test/reconcile-duplicate-repo-grants.test.ts b/packages/workflow-catalog/test/reconcile-duplicate-repo-grants.test.ts index 46b688bf7..e4cffd809 100644 --- a/packages/workflow-catalog/test/reconcile-duplicate-repo-grants.test.ts +++ b/packages/workflow-catalog/test/reconcile-duplicate-repo-grants.test.ts @@ -7,13 +7,14 @@ // ones. Proves it keeps exactly one grant per group (never zero, // matching the "a repo never ends up with fewer grants than it had" // bar), never touches an unrelated grant, and is safe to re-run. -import { afterAll, beforeAll, describe, expect, test } from "bun:test"; +import { afterAll, beforeAll, expect, test } from "bun:test"; import postgres from "postgres"; import { runMigrations } from "@intx/db"; import { e2eDatabaseUrl } from "../../../scripts/e2e/harness"; import { reconcileDuplicateRepoGrants } from "../src/reconcile-duplicate-repo-grants"; +import { dbGate } from "../../../scripts/e2e/db-gate"; function scratchUrlFor(e2eUrl: string): string { const url = new URL(e2eUrl); @@ -23,7 +24,7 @@ function scratchUrlFor(e2eUrl: string): string { } const databaseUrl = e2eDatabaseUrl(); -const describeIfDb = databaseUrl === undefined ? describe.skip : describe; +const describeIfDb = dbGate(databaseUrl, import.meta.path); const TENANT_ID = "tnt_reconcile"; const ROLE_ID = "role_reconcile_member"; diff --git a/scripts/checks/db-gate.ts b/scripts/checks/db-gate.ts new file mode 100644 index 000000000..a0d75351e --- /dev/null +++ b/scripts/checks/db-gate.ts @@ -0,0 +1,76 @@ +// check:db-gate — every DB-gated suite must skip through dbGate +// (scripts/e2e/db-gate.ts), never a hand-rolled +// `databaseUrl ? describe.skip : describe` ternary. A hand-rolled gate +// skips in total silence and never honors E2E_REQUIRED=1 — exactly +// the gap CL-7279 closed, one file at a time, until the next merge +// added a new hand-rolled copy the sweep never saw. This check makes +// the invariant self-enforcing instead of a one-time sweep. +// +// vendor/intx is excluded: editing inside a vendored tree carries +// re-pin tax, so its (small, pre-existing) hand-rolled gates are left +// alone — see VENDORED.md. +import { Glob } from "bun"; +import path from "node:path"; +import { + emptyReport, + reportAndExit, + rootFromArgs, + type CheckReport, +} from "./lib/repo"; + +const SCAN_DIRS = ["apps", "packages", "scripts"]; +const HAND_ROLLED_GATE_PATTERN = + /const\s+describeIfDb\s*=\s*\w+\s*===\s*(?:undefined|"")\s*\?\s*describe\.skip\s*:\s*describe;/; + +export async function scanFiles( + root: string, + dirs: readonly string[], +): Promise { + const files: string[] = []; + for (const dir of dirs) { + const glob = new Glob(`${dir}/**/*.test.ts`); + for await (const file of glob.scan({ cwd: root, dot: false })) { + if (file.includes("node_modules/")) continue; + // This check's own test fixtures deliberately contain the + // hand-rolled pattern as string literals, not real gates. + if (file.startsWith("scripts/checks/")) continue; + files.push(file); + } + } + return files; +} + +export function auditDbGate( + files: readonly { relPath: string; contents: string }[], +): CheckReport { + const report = emptyReport(); + for (const { relPath, contents } of files) { + if (!HAND_ROLLED_GATE_PATTERN.test(contents)) continue; + report.violations.push( + `${relPath}: hand-rolls a describeIfDb gate instead of using dbGate ` + + `from scripts/e2e/db-gate.ts. A hand-rolled gate skips silently ` + + `and never honors E2E_REQUIRED=1 — replace it with ` + + `\`const describeIfDb = dbGate(databaseUrl, import.meta.path);\`.`, + ); + } + return report; +} + +async function main(): Promise { + const args = Bun.argv.slice(2); + const root = rootFromArgs(args); + const relPaths = await scanFiles(root, SCAN_DIRS); + const files = await Promise.all( + relPaths.map(async (relPath) => ({ + relPath, + contents: await Bun.file(path.join(root, relPath)).text(), + })), + ); + const report = auditDbGate(files); + report.notes.push( + `scanned ${files.length} *.test.ts file(s) under ${SCAN_DIRS.join(", ")}`, + ); + reportAndExit("check:db-gate", report); +} + +if (import.meta.main) await main();