Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion apps/hub/src/memory-mount.test.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand Down Expand Up @@ -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 () => {
Expand Down
5 changes: 3 additions & 2 deletions apps/hub/src/memory-workflow-routes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
5 changes: 3 additions & 2 deletions apps/hub/test/artifact-doc-persistence.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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",
Expand Down
5 changes: 3 additions & 2 deletions apps/hub/test/chat-mount.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
5 changes: 3 additions & 2 deletions apps/hub/test/composition.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
5 changes: 3 additions & 2 deletions apps/hub/test/eval-runs-mount.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
5 changes: 3 additions & 2 deletions apps/hub/test/presence-mount.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
5 changes: 3 additions & 2 deletions apps/hub/test/routine-mount.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
12 changes: 3 additions & 9 deletions apps/hub/test/slack-tag-mount.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)}`;

Expand Down
21 changes: 21 additions & 0 deletions docker-compose.test.yml
Original file line number Diff line number Diff line change
@@ -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
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
5 changes: 3 additions & 2 deletions packages/access-policy/test/store.drizzle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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(
Expand Down
5 changes: 3 additions & 2 deletions packages/agent-directory/test/migrations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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"];

Expand Down
5 changes: 3 additions & 2 deletions packages/agent-directory/test/routes.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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 = {
Expand Down
5 changes: 3 additions & 2 deletions packages/agent-directory/test/skills-store.drizzle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down
Loading
Loading