From 96ddec4c6e16c987a55dcc890c5747b83cc2fac9 Mon Sep 17 00:00:00 2001 From: Sawyer Date: Sun, 6 Sep 2026 14:02:55 -0700 Subject: [PATCH 1/8] Add tests for the test script running concurrently run-all.test.ts still hard-coded the test script as the one script the runner always caps at concurrency 1. Update its expectations first so the next commit's removal of that carve-out lands against a red-then- green suite rather than a change with no coverage of its own behavior. --- scripts/run-all.test.ts | 58 +++++++++++------------------------------ 1 file changed, 15 insertions(+), 43 deletions(-) diff --git a/scripts/run-all.test.ts b/scripts/run-all.test.ts index e73d776b8c..6cf9239664 100644 --- a/scripts/run-all.test.ts +++ b/scripts/run-all.test.ts @@ -8,7 +8,6 @@ import { tmpdir } from "node:os"; import { basename, join } from "node:path"; import { resolveConcurrency } from "./run-all.ts"; -import { SEQUENTIAL_SCRIPTS } from "./sequential-scripts.ts"; const RUNNER = join(import.meta.dir, "run-all.ts"); @@ -31,7 +30,11 @@ if (process.env["PROBE_FAIL"] === name) process.exit(1); const WITH_PROBE = ["alpha", "bravo", "charlie", "delta"] as const; const WITHOUT_PROBE = "echo-only"; -const SEQUENTIAL_SCRIPT = "test"; +// The root gate's own "test" script fans out across packages exactly +// like any other script now (see run-all.ts's resolveConcurrency); it +// gets its own probe run below purely to prove that specific script name +// carries no special-cased concurrency any more. +const TEST_SCRIPT = "test"; let workspace = ""; let logCounter = 0; @@ -113,7 +116,7 @@ describe("run-all", () => { for (const name of WITH_PROBE) { await writePackage(workspace, name, { probe: "bun run probe.ts", - [SEQUENTIAL_SCRIPT]: "bun run probe.ts", + [TEST_SCRIPT]: "bun run probe.ts", }); await writeFile( join(workspace, "packages", name, "probe.ts"), @@ -171,35 +174,22 @@ describe("run-all", () => { } }); - test("runs the test script one package at a time by default", async () => { - const result = await runProbe({}, SEQUENTIAL_SCRIPT); + test("runs the test script concurrently like any other script", async () => { + const result = await runProbe( + { WORKBENCH_CHECK_CONCURRENCY: "4" }, + TEST_SCRIPT, + ); - expect(peakOverlap(result.log)).toBe(1); + expect(peakOverlap(result.log)).toBeGreaterThan(1); for (const name of WITH_PROBE) { expect(result.stdout).toContain(`probe ran in ${name}`); } }); - test("default sequential run ignores ambient WORKBENCH_CHECK_CONCURRENCY", async () => { - const previous = process.env["WORKBENCH_CHECK_CONCURRENCY"]; - process.env["WORKBENCH_CHECK_CONCURRENCY"] = "4"; - try { - const result = await runProbe({}, SEQUENTIAL_SCRIPT); - - expect(peakOverlap(result.log)).toBe(1); - } finally { - if (previous === undefined) { - delete process.env["WORKBENCH_CHECK_CONCURRENCY"]; - } else { - process.env["WORKBENCH_CHECK_CONCURRENCY"] = previous; - } - } - }); - test("honours an explicit concurrency for the test script", async () => { const result = await runProbe( { WORKBENCH_CHECK_CONCURRENCY: "3" }, - SEQUENTIAL_SCRIPT, + TEST_SCRIPT, ); expect(peakOverlap(result.log)).toBeGreaterThan(1); @@ -224,7 +214,8 @@ describe("run-all", () => { 8, ), ).toBe(3); - expect(resolveConcurrency("test", { GITHUB_ACTIONS: "true" }, 8)).toBe(1); + expect(resolveConcurrency("test", { GITHUB_ACTIONS: "true" }, 8)).toBe(8); + expect(resolveConcurrency("test", {}, 8)).toBe(6); }); test("fails the run and names the package whose script failed", async () => { @@ -271,23 +262,4 @@ describe("run-all", () => { test("fixture workspace holds only the packages the tests declare", () => { expect(basename(workspace).startsWith("workbench-run-all-")).toBe(true); }); - - // Without this, renaming the script the root gate hands the runner would - // silently restore concurrency to the test phase, and the flakiness that - // causes would surface later as an unrelated-looking regression. - test("the root gate runs its test phase through a script declared sequential", async () => { - const manifest = (await Bun.file( - join(import.meta.dir, "..", "package.json"), - ).json()) as { scripts?: Record }; - const testScript = manifest.scripts?.["test"] ?? ""; - - const invoked = [...testScript.matchAll(/run-all\.ts\s+([\w:-]+)/g)].map( - (match) => match[1], - ); - - expect(invoked.length).toBeGreaterThan(0); - for (const script of invoked) { - expect(SEQUENTIAL_SCRIPTS.has(script ?? "")).toBe(true); - } - }); }); From fc88b1b1c63793b0d329fce43320601068f0062c Mon Sep 17 00:00:00 2001 From: Sawyer Date: Sun, 6 Sep 2026 14:03:05 -0700 Subject: [PATCH 2/8] run-all: drop the sequential-only carve-out for the test script Every DB-gated suite now resolves its own scratch database and every e2e suite mkdtemps its own HUB_DATA_DIR and binds an OS-assigned port (scripts/e2e/harness.ts's startHub/startSidecar/freePort), so nothing left in the test phase depends on running one package at a time. The test script now resolves concurrency exactly like typecheck: every core in CI, cores minus two locally, or WORKBENCH_CHECK_CONCURRENCY when set. --- scripts/run-all.ts | 9 +++++++-- scripts/sequential-scripts.ts | 11 ----------- 2 files changed, 7 insertions(+), 13 deletions(-) delete mode 100644 scripts/sequential-scripts.ts diff --git a/scripts/run-all.ts b/scripts/run-all.ts index 316e88d06a..a2b3ee1e7a 100644 --- a/scripts/run-all.ts +++ b/scripts/run-all.ts @@ -5,18 +5,23 @@ import { Glob } from "bun"; import { availableParallelism } from "node:os"; import { CONCURRENCY_ENV } from "./concurrency.ts"; -import { SEQUENTIAL_SCRIPTS } from "./sequential-scripts.ts"; type Job = { readonly name: string; readonly dir: string }; +// `script` used to gate the test phase to concurrency 1 (see git history +// for `scripts/sequential-scripts.ts`, removed once each test suite's own +// isolation — a per-run HUB_DATA_DIR, a per-run scratch database, no +// fixed ports — made fanning tests out across packages safe). Kept as a +// parameter so a future script-specific override has somewhere to hang +// without changing every call site again. export function resolveConcurrency( script: string, env: NodeJS.ProcessEnv = process.env, cores: number = availableParallelism(), ): number { + void script; const raw = env[CONCURRENCY_ENV]; if (raw === undefined || raw === "") { - if (SEQUENTIAL_SCRIPTS.has(script)) return 1; // Locally each job saturates about one core, so leave a couple free for // the editor and type server a developer runs alongside the gate. CI // runners have no editor — use every core so package fan-out is not diff --git a/scripts/sequential-scripts.ts b/scripts/sequential-scripts.ts deleted file mode 100644 index ae7cea109c..0000000000 --- a/scripts/sequential-scripts.ts +++ /dev/null @@ -1,11 +0,0 @@ -// Type checking is pure computation and fans out safely. Tests are not: they -// spawn processes, bind sockets, and talk to Postgres, and several suites here -// assume they are not competing for a core — running them concurrently makes a -// different handful of timing-sensitive suites fail on every run. They stay -// sequential until the suites themselves tolerate contention. -// -// This lives apart from the runner so a test can assert that the script names -// the root gate actually invokes are the ones listed here. Held as a bare -// string set inside the runner, a rename would silently restore concurrency to -// the test phase and the flakiness would look like an unrelated regression. -export const SEQUENTIAL_SCRIPTS: ReadonlySet = new Set(["test"]); From c6039a3a1d5f6d9fa1d0db5ceae821c91f033efe Mon Sep 17 00:00:00 2001 From: Sawyer Date: Sun, 6 Sep 2026 14:03:17 -0700 Subject: [PATCH 3/8] harness: split e2e helpers so DB-gated unit suites skip the heavy import chain MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every package's migrations.test.ts / *.drizzle.test.ts imported scripts/e2e/harness.ts purely for e2eDatabaseUrl (to skip when DATABASE_URL is unset). harness.ts also imports @corbits/seeding and @corbits/workflows for pushWorkflowSource/workflowDeployBody, which pulls in @intx/types, @intx/hub-common, and @intx/agent — over a second of module-load cost paid on every build-test run of every one of those packages, whether or not the suite ever executes. Move the DATABASE_URL resolution (e2eDatabaseUrl, baseUrlToE2eUrl, parseEnvFileDatabaseUrl, REPO_ROOT) into scripts/e2e/database-url.ts, and the two workflow-source-publishing helpers into scripts/e2e/workflow-source.ts. harness.ts re-exports the former for its own heavier consumers and no longer imports seeding/workflows at all. Update every DB-gated suite to import e2eDatabaseUrl from database-url.ts directly, and the handful of full e2e suites (plus longevity-sim's stack.ts) to import pushWorkflowSource/ workflowDeployBody from workflow-source.ts. --- apps/hub/src/default-tenant.test.ts | 2 +- .../access-policy/test/store.drizzle.test.ts | 2 +- .../agent-directory/test/migrations.test.ts | 2 +- .../test/skills-store.drizzle.test.ts | 2 +- .../test/tool-package-version.drizzle.test.ts | 2 +- .../test/visible-definitions.drizzle.test.ts | 2 +- packages/bench/test/migrations.test.ts | 2 +- .../chat/test/agent-turns.drizzle.test.ts | 2 +- .../chat/test/block-responses.drizzle.test.ts | 2 +- .../chat/test/mailbox-fanout.drizzle.test.ts | 2 +- packages/chat/test/migrations.test.ts | 2 +- packages/chat/test/reactions.drizzle.test.ts | 2 +- packages/chat/test/read-state.drizzle.test.ts | 2 +- .../chat/test/room-activity.drizzle.test.ts | 2 +- .../settings-participants.drizzle.test.ts | 2 +- .../chat/test/settings-patch.drizzle.test.ts | 2 +- packages/chat/test/threads.drizzle.test.ts | 2 +- .../turn-mail-correlation.drizzle.test.ts | 2 +- .../chat/test/write-claims.drizzle.test.ts | 2 +- .../src/connect-github-setup.race.test.ts | 2 +- .../reconcile-duplicate-repo-grants.test.ts | 2 +- .../test/mcp-server-store.drizzle.test.ts | 2 +- .../test/credential-delivery.drizzle.test.ts | 2 +- .../credential-wiring-e2e.drizzle.test.ts | 2 +- packages/inbox/test/delivery.test.ts | 2 +- packages/inbox/test/snooze-store.test.ts | 2 +- .../inference-catalog/test/migrations.test.ts | 2 +- .../test/insights-scope-verification.test.ts | 2 +- packages/insights/test/migrations.test.ts | 2 +- .../insights/test/routes-run-trace.test.ts | 2 +- packages/insights/test/routes-scope.test.ts | 2 +- packages/insights/test/trace-reader.test.ts | 2 +- .../test/credential-delivery.drizzle.test.ts | 2 +- .../credential-wiring-e2e.drizzle.test.ts | 2 +- packages/longevity-sim/src/stack.ts | 6 +- .../test/migration-runner.test.ts | 2 +- packages/notify/test/migrations.test.ts | 2 +- packages/onboarding/test/migrations.test.ts | 2 +- .../test/pending-seed-store.drizzle.test.ts | 2 +- packages/preferences/test/migrations.test.ts | 2 +- .../test/diagnostics.drizzle.test.ts | 2 +- .../run-key-history/test/migrations.test.ts | 2 +- .../test/reconnect.drizzle.test.ts | 2 +- .../test/routes.drizzle.test.ts | 2 +- packages/run-key-history/test/store.test.ts | 2 +- .../test/scope-routes.drizzle.test.ts | 2 +- .../test/store.drizzle.test.ts | 2 +- .../webhook-triggers/test/migrations.test.ts | 2 +- .../test/store.drizzle.test.ts | 2 +- scripts/e2e/chat.test.ts | 3 +- scripts/e2e/database-url.ts | 103 +++++++++++ scripts/e2e/harness.ts | 167 ++---------------- scripts/e2e/heartbeat.test.ts | 3 +- scripts/e2e/smoke-webhook.test.ts | 3 +- scripts/e2e/walking-skeleton.test.ts | 3 +- scripts/e2e/workbench-digest.test.ts | 3 +- scripts/e2e/workflow-source.ts | 65 +++++++ 57 files changed, 241 insertions(+), 211 deletions(-) create mode 100644 scripts/e2e/database-url.ts create mode 100644 scripts/e2e/workflow-source.ts diff --git a/apps/hub/src/default-tenant.test.ts b/apps/hub/src/default-tenant.test.ts index 44da46e4d7..c06ef9207e 100644 --- a/apps/hub/src/default-tenant.test.ts +++ b/apps/hub/src/default-tenant.test.ts @@ -6,7 +6,7 @@ import { afterAll, beforeAll, expect, test } from "bun:test"; import postgres from "postgres"; import { and, eq } from "drizzle-orm"; -import { e2eDatabaseUrl } from "../../../scripts/e2e/harness"; +import { e2eDatabaseUrl } from "../../../scripts/e2e/database-url"; import { setupDatabase } from "../../../scripts/db-setup"; import { dbGate } from "../../../scripts/e2e/db-gate"; import { createDB } from "@intx/db"; diff --git a/packages/access-policy/test/store.drizzle.test.ts b/packages/access-policy/test/store.drizzle.test.ts index 4e1d89ffb3..656cf2345a 100644 --- a/packages/access-policy/test/store.drizzle.test.ts +++ b/packages/access-policy/test/store.drizzle.test.ts @@ -12,7 +12,7 @@ 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 { e2eDatabaseUrl } from "../../../scripts/e2e/database-url"; import { applyAccessPolicyMigrations } from "../src/migrations"; import { createDrizzleAccessPolicyStore } from "../src/store"; import { dbGate } from "../../../scripts/e2e/db-gate"; diff --git a/packages/agent-directory/test/migrations.test.ts b/packages/agent-directory/test/migrations.test.ts index 7ac0365470..ee14bffbf0 100644 --- a/packages/agent-directory/test/migrations.test.ts +++ b/packages/agent-directory/test/migrations.test.ts @@ -5,7 +5,7 @@ import { afterAll, beforeAll, expect, test } from "bun:test"; import postgres from "postgres"; -import { e2eDatabaseUrl } from "../../../scripts/e2e/harness"; +import { e2eDatabaseUrl } from "../../../scripts/e2e/database-url"; import { applyAgentDirectoryMigrations } from "../src/migrations"; import { dbGate } from "../../../scripts/e2e/db-gate"; diff --git a/packages/agent-directory/test/skills-store.drizzle.test.ts b/packages/agent-directory/test/skills-store.drizzle.test.ts index dfed79819a..5cb1d5243c 100644 --- a/packages/agent-directory/test/skills-store.drizzle.test.ts +++ b/packages/agent-directory/test/skills-store.drizzle.test.ts @@ -6,7 +6,7 @@ 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 { e2eDatabaseUrl } from "../../../scripts/e2e/database-url"; import { applyAgentDirectoryMigrations } from "../src/migrations"; import { createDrizzleDefinitionSkillsStore } from "../src/skills-store"; import { dbGate } from "../../../scripts/e2e/db-gate"; diff --git a/packages/agent-directory/test/tool-package-version.drizzle.test.ts b/packages/agent-directory/test/tool-package-version.drizzle.test.ts index adaea07392..3a0d580fdf 100644 --- a/packages/agent-directory/test/tool-package-version.drizzle.test.ts +++ b/packages/agent-directory/test/tool-package-version.drizzle.test.ts @@ -18,7 +18,7 @@ import type { AssetService } from "@intx/hub-sessions"; import { CORBITS_TOOLS_REGISTRY } from "@corbits/tool-registry-publish"; import { dbTargetFromUrl } from "../../../scripts/db-setup"; -import { e2eDatabaseUrl } from "../../../scripts/e2e/harness"; +import { e2eDatabaseUrl } from "../../../scripts/e2e/database-url"; import { dbGate } from "../../../scripts/e2e/db-gate"; import { CapabilityOutOfInventoryError } from "../src/capability-inventory"; import { resolvePinnedVersion } from "../src/tool-package-version"; diff --git a/packages/agent-directory/test/visible-definitions.drizzle.test.ts b/packages/agent-directory/test/visible-definitions.drizzle.test.ts index 138bc29be6..cf793e540a 100644 --- a/packages/agent-directory/test/visible-definitions.drizzle.test.ts +++ b/packages/agent-directory/test/visible-definitions.drizzle.test.ts @@ -12,7 +12,7 @@ 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 { e2eDatabaseUrl } from "../../../scripts/e2e/database-url"; import { listVisibleAgentDefinitions } from "../src/visible-definitions"; import { dbGate } from "../../../scripts/e2e/db-gate"; diff --git a/packages/bench/test/migrations.test.ts b/packages/bench/test/migrations.test.ts index 9ab33fd6fc..49f2a1039d 100644 --- a/packages/bench/test/migrations.test.ts +++ b/packages/bench/test/migrations.test.ts @@ -5,7 +5,7 @@ import { afterAll, beforeAll, expect, test } from "bun:test"; import postgres from "postgres"; -import { e2eDatabaseUrl } from "../../../scripts/e2e/harness"; +import { e2eDatabaseUrl } from "../../../scripts/e2e/database-url"; import { applyBenchMigrations } from "../src/migrations"; import { createPostgresBenchSettingsStore } from "../src/pg-store"; import { dbGate } from "../../../scripts/e2e/db-gate"; diff --git a/packages/chat/test/agent-turns.drizzle.test.ts b/packages/chat/test/agent-turns.drizzle.test.ts index c14eb490d4..9af077db0f 100644 --- a/packages/chat/test/agent-turns.drizzle.test.ts +++ b/packages/chat/test/agent-turns.drizzle.test.ts @@ -13,7 +13,7 @@ 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 { e2eDatabaseUrl } from "../../../scripts/e2e/database-url"; import { AGENT_TURN_STALE_MS, createDrizzleAgentTurnStore, diff --git a/packages/chat/test/block-responses.drizzle.test.ts b/packages/chat/test/block-responses.drizzle.test.ts index 33a1750c2d..16e392607c 100644 --- a/packages/chat/test/block-responses.drizzle.test.ts +++ b/packages/chat/test/block-responses.drizzle.test.ts @@ -13,7 +13,7 @@ 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 { e2eDatabaseUrl } from "../../../scripts/e2e/database-url"; import { applyChatMigrations } from "../src/migrations"; import { createDrizzleBlockResponseStore } from "../src/block-responses"; import type { BlockResponseKey } from "../src/block-responses"; diff --git a/packages/chat/test/mailbox-fanout.drizzle.test.ts b/packages/chat/test/mailbox-fanout.drizzle.test.ts index 202be4af7a..8f43955f6b 100644 --- a/packages/chat/test/mailbox-fanout.drizzle.test.ts +++ b/packages/chat/test/mailbox-fanout.drizzle.test.ts @@ -15,7 +15,7 @@ import postgres from "postgres"; import { sql } from "drizzle-orm"; import { createMailboxDb, runMailboxMigrations } from "@corbits/mailbox"; -import { e2eDatabaseUrl } from "../../../scripts/e2e/harness"; +import { e2eDatabaseUrl } from "../../../scripts/e2e/database-url"; import { dbGate } from "../../../scripts/e2e/db-gate"; import { createDrizzleMailboxWriter, diff --git a/packages/chat/test/migrations.test.ts b/packages/chat/test/migrations.test.ts index 23a811db5a..fd9b553ae6 100644 --- a/packages/chat/test/migrations.test.ts +++ b/packages/chat/test/migrations.test.ts @@ -7,7 +7,7 @@ import { afterAll, beforeAll, expect, test } from "bun:test"; import postgres from "postgres"; -import { e2eDatabaseUrl } from "../../../scripts/e2e/harness"; +import { e2eDatabaseUrl } from "../../../scripts/e2e/database-url"; import { applyChatMigrations, chatMigrations } from "../src/migrations"; import { dbGate } from "../../../scripts/e2e/db-gate"; diff --git a/packages/chat/test/reactions.drizzle.test.ts b/packages/chat/test/reactions.drizzle.test.ts index e382e8edb5..9095404597 100644 --- a/packages/chat/test/reactions.drizzle.test.ts +++ b/packages/chat/test/reactions.drizzle.test.ts @@ -15,7 +15,7 @@ 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 { e2eDatabaseUrl } from "../../../scripts/e2e/database-url"; import { applyChatMigrations } from "../src/migrations"; import { createDrizzleReactionStore } from "../src/reactions"; import { dbGate } from "../../../scripts/e2e/db-gate"; diff --git a/packages/chat/test/read-state.drizzle.test.ts b/packages/chat/test/read-state.drizzle.test.ts index 2e081a9330..d4d7797034 100644 --- a/packages/chat/test/read-state.drizzle.test.ts +++ b/packages/chat/test/read-state.drizzle.test.ts @@ -11,7 +11,7 @@ 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 { e2eDatabaseUrl } from "../../../scripts/e2e/database-url"; import { applyChatMigrations } from "../src/migrations"; import { createDrizzleChatStore } from "../src/store"; import { dbGate } from "../../../scripts/e2e/db-gate"; diff --git a/packages/chat/test/room-activity.drizzle.test.ts b/packages/chat/test/room-activity.drizzle.test.ts index fc5df62e35..e7a2c14790 100644 --- a/packages/chat/test/room-activity.drizzle.test.ts +++ b/packages/chat/test/room-activity.drizzle.test.ts @@ -13,7 +13,7 @@ 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 { e2eDatabaseUrl } from "../../../scripts/e2e/database-url"; import { applyChatMigrations } from "../src/migrations"; import { createDrizzleRoomMessageStore } from "../src/room-messages"; import { dbGate } from "../../../scripts/e2e/db-gate"; diff --git a/packages/chat/test/settings-participants.drizzle.test.ts b/packages/chat/test/settings-participants.drizzle.test.ts index 69ff3e9aef..fb5f0eb8ec 100644 --- a/packages/chat/test/settings-participants.drizzle.test.ts +++ b/packages/chat/test/settings-participants.drizzle.test.ts @@ -17,7 +17,7 @@ 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 { e2eDatabaseUrl } from "../../../scripts/e2e/database-url"; import { applyChatMigrations } from "../src/migrations"; import { addParticipant } from "../src/participants"; import { createDrizzleChatStore } from "../src/store"; diff --git a/packages/chat/test/settings-patch.drizzle.test.ts b/packages/chat/test/settings-patch.drizzle.test.ts index 2859787db8..47e1c5e38d 100644 --- a/packages/chat/test/settings-patch.drizzle.test.ts +++ b/packages/chat/test/settings-patch.drizzle.test.ts @@ -6,7 +6,7 @@ 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 { e2eDatabaseUrl } from "../../../scripts/e2e/database-url"; import { applyChatMigrations } from "../src/migrations"; import { createDrizzleChatStore } from "../src/store"; import { dbGate } from "../../../scripts/e2e/db-gate"; diff --git a/packages/chat/test/threads.drizzle.test.ts b/packages/chat/test/threads.drizzle.test.ts index 2798fc2110..cdda6aa244 100644 --- a/packages/chat/test/threads.drizzle.test.ts +++ b/packages/chat/test/threads.drizzle.test.ts @@ -16,7 +16,7 @@ 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 { e2eDatabaseUrl } from "../../../scripts/e2e/database-url"; import { applyChatMigrations } from "../src/migrations"; import { createDrizzleThreadStore } from "../src/threads"; import { dbGate } from "../../../scripts/e2e/db-gate"; diff --git a/packages/chat/test/turn-mail-correlation.drizzle.test.ts b/packages/chat/test/turn-mail-correlation.drizzle.test.ts index e44578f9b1..4a87aaf17f 100644 --- a/packages/chat/test/turn-mail-correlation.drizzle.test.ts +++ b/packages/chat/test/turn-mail-correlation.drizzle.test.ts @@ -14,7 +14,7 @@ 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 { e2eDatabaseUrl } from "../../../scripts/e2e/database-url"; import { applyChatMigrations } from "../src/migrations"; import { createDrizzleTurnMailCorrelationStore } from "../src/turn-mail-correlation"; import { dbGate } from "../../../scripts/e2e/db-gate"; diff --git a/packages/chat/test/write-claims.drizzle.test.ts b/packages/chat/test/write-claims.drizzle.test.ts index 2496cecfe3..1672914d94 100644 --- a/packages/chat/test/write-claims.drizzle.test.ts +++ b/packages/chat/test/write-claims.drizzle.test.ts @@ -14,7 +14,7 @@ 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 { e2eDatabaseUrl } from "../../../scripts/e2e/database-url"; import { applyChatMigrations } from "../src/migrations"; import { createDrizzleWriteClaimStore } from "../src/write-claims"; import { dbGate } from "../../../scripts/e2e/db-gate"; diff --git a/packages/connections/src/connect-github-setup.race.test.ts b/packages/connections/src/connect-github-setup.race.test.ts index 2f45bfb5a5..01bd9d7358 100644 --- a/packages/connections/src/connect-github-setup.race.test.ts +++ b/packages/connections/src/connect-github-setup.race.test.ts @@ -33,7 +33,7 @@ import { } from "@corbits/webhook-triggers"; import type { GitHubRepoSummary } from "@corbits/github-tools"; -import { e2eDatabaseUrl } from "../../../scripts/e2e/harness"; +import { e2eDatabaseUrl } from "../../../scripts/e2e/database-url"; import { dbGate } from "../../../scripts/e2e/db-gate"; import { startReviewingRepos, diff --git a/packages/connections/src/reconcile-duplicate-repo-grants.test.ts b/packages/connections/src/reconcile-duplicate-repo-grants.test.ts index dd7a0e3788..e6a14cae40 100644 --- a/packages/connections/src/reconcile-duplicate-repo-grants.test.ts +++ b/packages/connections/src/reconcile-duplicate-repo-grants.test.ts @@ -12,7 +12,7 @@ import postgres from "postgres"; import { runMigrations } from "@intx/db"; -import { e2eDatabaseUrl } from "../../../scripts/e2e/harness"; +import { e2eDatabaseUrl } from "../../../scripts/e2e/database-url"; import { reconcileDuplicateRepoGrants } from "./reconcile-duplicate-repo-grants"; import { dbGate } from "../../../scripts/e2e/db-gate"; diff --git a/packages/connections/test/mcp-server-store.drizzle.test.ts b/packages/connections/test/mcp-server-store.drizzle.test.ts index 8f2348be0c..7b4ffc2b2f 100644 --- a/packages/connections/test/mcp-server-store.drizzle.test.ts +++ b/packages/connections/test/mcp-server-store.drizzle.test.ts @@ -13,7 +13,7 @@ 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 { e2eDatabaseUrl } from "../../../scripts/e2e/database-url"; import { listMcpServerConnections } from "../src/mcp-server-store"; import { dbGate } from "../../../scripts/e2e/db-gate"; diff --git a/packages/granola-tools/test/credential-delivery.drizzle.test.ts b/packages/granola-tools/test/credential-delivery.drizzle.test.ts index fc2546abca..12ab891ca5 100644 --- a/packages/granola-tools/test/credential-delivery.drizzle.test.ts +++ b/packages/granola-tools/test/credential-delivery.drizzle.test.ts @@ -27,7 +27,7 @@ import { credentialAad } from "@intx/types"; import type { CredentialBinding } from "@intx/types"; import { dbTargetFromUrl } from "../../../scripts/db-setup"; -import { e2eDatabaseUrl } from "../../../scripts/e2e/harness"; +import { e2eDatabaseUrl } from "../../../scripts/e2e/database-url"; import { dbGate } from "../../../scripts/e2e/db-gate"; const databaseUrl = e2eDatabaseUrl(); 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 b640b2f1fe..814ab3482c 100644 --- a/packages/granola-tools/test/credential-wiring-e2e.drizzle.test.ts +++ b/packages/granola-tools/test/credential-wiring-e2e.drizzle.test.ts @@ -55,7 +55,7 @@ import { deriveResolvedBindings } from "@corbits/credential-providers"; import type { ToolCall } from "@intx/types/runtime"; import { dbTargetFromUrl } from "../../../scripts/db-setup"; -import { e2eDatabaseUrl } from "../../../scripts/e2e/harness"; +import { e2eDatabaseUrl } from "../../../scripts/e2e/database-url"; import { GRANOLA_LIST_RECENT_NOTES_TOOL, granolaTools } from "../src/tool"; import type { GranolaEnv } from "../src/tool"; import { dbGate } from "../../../scripts/e2e/db-gate"; diff --git a/packages/inbox/test/delivery.test.ts b/packages/inbox/test/delivery.test.ts index c662bac919..54522447de 100644 --- a/packages/inbox/test/delivery.test.ts +++ b/packages/inbox/test/delivery.test.ts @@ -24,7 +24,7 @@ import { } from "@corbits/notify"; import { setupDatabase } from "../../../scripts/db-setup"; -import { e2eDatabaseUrl } from "../../../scripts/e2e/harness"; +import { e2eDatabaseUrl } from "../../../scripts/e2e/database-url"; import { createWorkbenchMailboxDelivery } from "../src/delivery"; import { inboxGroupOf } from "../src/group"; import { WORKBENCH_INBOX_PRIORITIES } from "../src/vocabulary"; diff --git a/packages/inbox/test/snooze-store.test.ts b/packages/inbox/test/snooze-store.test.ts index 759c37e485..6ac99928e8 100644 --- a/packages/inbox/test/snooze-store.test.ts +++ b/packages/inbox/test/snooze-store.test.ts @@ -23,7 +23,7 @@ import { } from "@corbits/mailbox"; import { setupDatabase } from "../../../scripts/db-setup"; -import { e2eDatabaseUrl } from "../../../scripts/e2e/harness"; +import { e2eDatabaseUrl } from "../../../scripts/e2e/database-url"; import { createInboxRoutes } from "../src/routes"; import { dbGate } from "../../../scripts/e2e/db-gate"; import { diff --git a/packages/inference-catalog/test/migrations.test.ts b/packages/inference-catalog/test/migrations.test.ts index c9ba5b30aa..dc091ff615 100644 --- a/packages/inference-catalog/test/migrations.test.ts +++ b/packages/inference-catalog/test/migrations.test.ts @@ -5,7 +5,7 @@ import { afterAll, beforeAll, expect, test } from "bun:test"; import postgres from "postgres"; -import { e2eDatabaseUrl } from "../../../scripts/e2e/harness"; +import { e2eDatabaseUrl } from "../../../scripts/e2e/database-url"; import { applyInferenceCatalogMigrations } from "../src/migrations"; import { createPostgresBenchModelPolicyStore } from "../src/pg-store"; import { EMPTY_POLICY } from "../src/policy"; diff --git a/packages/insights/test/insights-scope-verification.test.ts b/packages/insights/test/insights-scope-verification.test.ts index b31f0fedf7..94acd05f8a 100644 --- a/packages/insights/test/insights-scope-verification.test.ts +++ b/packages/insights/test/insights-scope-verification.test.ts @@ -16,7 +16,7 @@ import { generateId } from "@intx/hub-common"; import type { RequireGrant, TenantEnv } from "@intx/hub-api"; import { setupDatabase } from "../../../scripts/db-setup"; -import { e2eDatabaseUrl } from "../../../scripts/e2e/harness"; +import { e2eDatabaseUrl } from "../../../scripts/e2e/database-url"; import { createInsightsRoutes } from "../src/routes"; import { createPostgresUsageStore } from "../src/pg-store"; import { createMemoryUsageStore } from "../src/store"; diff --git a/packages/insights/test/migrations.test.ts b/packages/insights/test/migrations.test.ts index 5a4e61d8f0..8a1420f49a 100644 --- a/packages/insights/test/migrations.test.ts +++ b/packages/insights/test/migrations.test.ts @@ -5,7 +5,7 @@ import { afterAll, beforeAll, expect, test } from "bun:test"; import postgres from "postgres"; -import { e2eDatabaseUrl } from "../../../scripts/e2e/harness"; +import { e2eDatabaseUrl } from "../../../scripts/e2e/database-url"; import { applyInsightsMigrations } from "../src/migrations"; import { dbGate } from "../../../scripts/e2e/db-gate"; diff --git a/packages/insights/test/routes-run-trace.test.ts b/packages/insights/test/routes-run-trace.test.ts index 3b54db555f..15cc13f682 100644 --- a/packages/insights/test/routes-run-trace.test.ts +++ b/packages/insights/test/routes-run-trace.test.ts @@ -15,7 +15,7 @@ import { generateId } from "@intx/hub-common"; import type { RequireGrant, TenantEnv } from "@intx/hub-api"; import { setupDatabase } from "../../../scripts/db-setup"; -import { e2eDatabaseUrl } from "../../../scripts/e2e/harness"; +import { e2eDatabaseUrl } from "../../../scripts/e2e/database-url"; import { createInsightsRoutes } from "../src/routes"; import { createDrizzleRunTraceReader } from "../src/trace-reader"; import { createMemoryUsageStore } from "../src/store"; diff --git a/packages/insights/test/routes-scope.test.ts b/packages/insights/test/routes-scope.test.ts index 25f7909187..6f27ad8672 100644 --- a/packages/insights/test/routes-scope.test.ts +++ b/packages/insights/test/routes-scope.test.ts @@ -22,7 +22,7 @@ import { generateId } from "@intx/hub-common"; import type { RequireGrant, TenantEnv } from "@intx/hub-api"; import { setupDatabase } from "../../../scripts/db-setup"; -import { e2eDatabaseUrl } from "../../../scripts/e2e/harness"; +import { e2eDatabaseUrl } from "../../../scripts/e2e/database-url"; import { createInsightsRoutes } from "../src/routes"; import { createMemoryUsageStore } from "../src/store"; import type { OverallUsageSummary } from "../src/queries"; diff --git a/packages/insights/test/trace-reader.test.ts b/packages/insights/test/trace-reader.test.ts index 4f5d8d9803..04d1ea5923 100644 --- a/packages/insights/test/trace-reader.test.ts +++ b/packages/insights/test/trace-reader.test.ts @@ -12,7 +12,7 @@ import { createDB, schema } from "@intx/db"; import { generateId } from "@intx/hub-common"; import { setupDatabase } from "../../../scripts/db-setup"; -import { e2eDatabaseUrl } from "../../../scripts/e2e/harness"; +import { e2eDatabaseUrl } from "../../../scripts/e2e/database-url"; import { createDrizzleRunTraceReader } from "../src/trace-reader"; import { dbGate } from "../../../scripts/e2e/db-gate"; diff --git a/packages/linear-tools/test/credential-delivery.drizzle.test.ts b/packages/linear-tools/test/credential-delivery.drizzle.test.ts index 731492fdc0..f9d2b1c2cc 100644 --- a/packages/linear-tools/test/credential-delivery.drizzle.test.ts +++ b/packages/linear-tools/test/credential-delivery.drizzle.test.ts @@ -27,7 +27,7 @@ import { credentialAad } from "@intx/types"; import type { CredentialBinding } from "@intx/types"; import { dbTargetFromUrl } from "../../../scripts/db-setup"; -import { e2eDatabaseUrl } from "../../../scripts/e2e/harness"; +import { e2eDatabaseUrl } from "../../../scripts/e2e/database-url"; import { dbGate } from "../../../scripts/e2e/db-gate"; const databaseUrl = e2eDatabaseUrl(); 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 a2cda13ecb..2e8b3d1bf8 100644 --- a/packages/linear-tools/test/credential-wiring-e2e.drizzle.test.ts +++ b/packages/linear-tools/test/credential-wiring-e2e.drizzle.test.ts @@ -61,7 +61,7 @@ import { import type { ToolCall } from "@intx/types/runtime"; import { dbTargetFromUrl } from "../../../scripts/db-setup"; -import { e2eDatabaseUrl } from "../../../scripts/e2e/harness"; +import { e2eDatabaseUrl } from "../../../scripts/e2e/database-url"; import { LINEAR_LIST_RECENT_ISSUES_TOOL, linearTools } from "../src/tool"; import type { LinearEnv } from "../src/tool"; import { dbGate } from "../../../scripts/e2e/db-gate"; diff --git a/packages/longevity-sim/src/stack.ts b/packages/longevity-sim/src/stack.ts index a7dc917761..e5535d281d 100644 --- a/packages/longevity-sim/src/stack.ts +++ b/packages/longevity-sim/src/stack.ts @@ -23,15 +23,17 @@ import { expectStatus, freePort, provisionSidecar, - pushWorkflowSource, runCleanups, startHub, startSidecar, - workflowDeployBody, type ApiResult, type HubHandle, type SpawnedApp, } from "../../../scripts/e2e/harness.ts"; +import { + pushWorkflowSource, + workflowDeployBody, +} from "../../../scripts/e2e/workflow-source.ts"; import { buildHeartbeatWorkflow, serializeHeartbeatWorkflow, diff --git a/packages/migration-runner/test/migration-runner.test.ts b/packages/migration-runner/test/migration-runner.test.ts index ef2d6522ce..cc0dd50cef 100644 --- a/packages/migration-runner/test/migration-runner.test.ts +++ b/packages/migration-runner/test/migration-runner.test.ts @@ -5,7 +5,7 @@ import { afterAll, beforeAll, expect, test } from "bun:test"; import postgres from "postgres"; -import { e2eDatabaseUrl } from "../../../scripts/e2e/harness"; +import { e2eDatabaseUrl } from "../../../scripts/e2e/database-url"; import { applyPackageMigrations, type PackageMigration } from "../src/index"; import { dbGate } from "../../../scripts/e2e/db-gate"; diff --git a/packages/notify/test/migrations.test.ts b/packages/notify/test/migrations.test.ts index 3774c6f766..67d4682f35 100644 --- a/packages/notify/test/migrations.test.ts +++ b/packages/notify/test/migrations.test.ts @@ -4,7 +4,7 @@ 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 { e2eDatabaseUrl } from "../../../scripts/e2e/database-url"; import { applyNotifyMigrations } from "../src/migrations"; import { createDrizzleNotifyDispatchStore } from "../src/store"; import { dbGate } from "../../../scripts/e2e/db-gate"; diff --git a/packages/onboarding/test/migrations.test.ts b/packages/onboarding/test/migrations.test.ts index 623acb0d12..8514f268e2 100644 --- a/packages/onboarding/test/migrations.test.ts +++ b/packages/onboarding/test/migrations.test.ts @@ -5,7 +5,7 @@ import { afterAll, beforeAll, expect, test } from "bun:test"; import postgres from "postgres"; -import { e2eDatabaseUrl } from "../../../scripts/e2e/harness"; +import { e2eDatabaseUrl } from "../../../scripts/e2e/database-url"; import { applyOnboardingMigrations } from "../src/migrations"; import { dbGate } from "../../../scripts/e2e/db-gate"; diff --git a/packages/onboarding/test/pending-seed-store.drizzle.test.ts b/packages/onboarding/test/pending-seed-store.drizzle.test.ts index f859b20585..e6a33cd0d8 100644 --- a/packages/onboarding/test/pending-seed-store.drizzle.test.ts +++ b/packages/onboarding/test/pending-seed-store.drizzle.test.ts @@ -23,7 +23,7 @@ import { createNoopCredentialCipher, } from "@intx/crypto"; -import { e2eDatabaseUrl } from "../../../scripts/e2e/harness"; +import { e2eDatabaseUrl } from "../../../scripts/e2e/database-url"; import { applyOnboardingMigrations } from "../src/migrations"; import { dbGate } from "../../../scripts/e2e/db-gate"; import { diff --git a/packages/preferences/test/migrations.test.ts b/packages/preferences/test/migrations.test.ts index 0b8543fd6b..3a03da9a1e 100644 --- a/packages/preferences/test/migrations.test.ts +++ b/packages/preferences/test/migrations.test.ts @@ -5,7 +5,7 @@ import { afterAll, beforeAll, expect, test } from "bun:test"; import postgres from "postgres"; -import { e2eDatabaseUrl } from "../../../scripts/e2e/harness"; +import { e2eDatabaseUrl } from "../../../scripts/e2e/database-url"; import { applyPreferencesMigrations } from "../src/migrations"; import { createPostgresPreferencesStore } from "../src/pg-store"; import { dbGate } from "../../../scripts/e2e/db-gate"; diff --git a/packages/run-key-history/test/diagnostics.drizzle.test.ts b/packages/run-key-history/test/diagnostics.drizzle.test.ts index 4167e23667..1a7a4fae78 100644 --- a/packages/run-key-history/test/diagnostics.drizzle.test.ts +++ b/packages/run-key-history/test/diagnostics.drizzle.test.ts @@ -10,7 +10,7 @@ import { eq, inArray } from "drizzle-orm"; import { createDB, runMigrations, dropSchema, schema } from "@intx/db"; import { dbTargetFromUrl } from "../../../scripts/db-setup"; -import { e2eDatabaseUrl } from "../../../scripts/e2e/harness"; +import { e2eDatabaseUrl } from "../../../scripts/e2e/database-url"; import { applyRunKeyHistoryMigrations } from "../src/migrations"; import { createDrizzleRunKeyHistoryStore } from "../src/store"; import { runKeyHistory } from "../src/schema"; diff --git a/packages/run-key-history/test/migrations.test.ts b/packages/run-key-history/test/migrations.test.ts index bb33220684..0610b44b83 100644 --- a/packages/run-key-history/test/migrations.test.ts +++ b/packages/run-key-history/test/migrations.test.ts @@ -3,7 +3,7 @@ import { afterAll, beforeAll, expect, test } from "bun:test"; import postgres from "postgres"; -import { e2eDatabaseUrl } from "../../../scripts/e2e/harness"; +import { e2eDatabaseUrl } from "../../../scripts/e2e/database-url"; import { applyRunKeyHistoryMigrations } from "../src/migrations"; import { dbGate } from "../../../scripts/e2e/db-gate"; diff --git a/packages/run-key-history/test/reconnect.drizzle.test.ts b/packages/run-key-history/test/reconnect.drizzle.test.ts index 7fe860ee9d..16e660d6ac 100644 --- a/packages/run-key-history/test/reconnect.drizzle.test.ts +++ b/packages/run-key-history/test/reconnect.drizzle.test.ts @@ -18,7 +18,7 @@ import { createDB, runMigrations, dropSchema, schema } from "@intx/db"; import { workflowRun } from "@intx/db/schema"; import { dbTargetFromUrl } from "../../../scripts/db-setup"; -import { e2eDatabaseUrl } from "../../../scripts/e2e/harness"; +import { e2eDatabaseUrl } from "../../../scripts/e2e/database-url"; import { applyRunKeyHistoryMigrations } from "../src/migrations"; import { createDrizzleRunKeyHistoryStore } from "../src/store"; import { lookupRunKeyHistoryReconnectKey } from "../src/reconnect"; diff --git a/packages/run-key-history/test/routes.drizzle.test.ts b/packages/run-key-history/test/routes.drizzle.test.ts index cbb19e192b..853bedbd3b 100644 --- a/packages/run-key-history/test/routes.drizzle.test.ts +++ b/packages/run-key-history/test/routes.drizzle.test.ts @@ -11,7 +11,7 @@ import { createDB, runMigrations, dropSchema, schema } from "@intx/db"; import type { RequireGrant, TenantEnv } from "@intx/hub-api"; import { dbTargetFromUrl } from "../../../scripts/db-setup"; -import { e2eDatabaseUrl } from "../../../scripts/e2e/harness"; +import { e2eDatabaseUrl } from "../../../scripts/e2e/database-url"; import { applyRunKeyHistoryMigrations } from "../src/migrations"; import { createDrizzleRunKeyHistoryStore } from "../src/store"; import { runKeyHistory } from "../src/schema"; diff --git a/packages/run-key-history/test/store.test.ts b/packages/run-key-history/test/store.test.ts index 971cd5be85..021c3831a2 100644 --- a/packages/run-key-history/test/store.test.ts +++ b/packages/run-key-history/test/store.test.ts @@ -6,7 +6,7 @@ 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 { e2eDatabaseUrl } from "../../../scripts/e2e/database-url"; import { applyRunKeyHistoryMigrations } from "../src/migrations"; import { createDrizzleRunKeyHistoryStore } from "../src/store"; import { dbGate } from "../../../scripts/e2e/db-gate"; diff --git a/packages/run-scope/test/scope-routes.drizzle.test.ts b/packages/run-scope/test/scope-routes.drizzle.test.ts index 2eef717828..6de4426c3c 100644 --- a/packages/run-scope/test/scope-routes.drizzle.test.ts +++ b/packages/run-scope/test/scope-routes.drizzle.test.ts @@ -20,7 +20,7 @@ import { applyFoldedRunsMigrations } from "@corbits/folded-runs/migrations"; import { foldedRun } from "@corbits/folded-runs"; import { dbTargetFromUrl } from "../../../scripts/db-setup"; -import { e2eDatabaseUrl } from "../../../scripts/e2e/harness"; +import { e2eDatabaseUrl } from "../../../scripts/e2e/database-url"; import { listTopLevelRuns, listTopLevelRunFires } from "../src/scope-routes"; import { dbGate } from "../../../scripts/e2e/db-gate"; diff --git a/packages/sidecar-placement/test/store.drizzle.test.ts b/packages/sidecar-placement/test/store.drizzle.test.ts index 5c9cb2ddd9..9f5078e941 100644 --- a/packages/sidecar-placement/test/store.drizzle.test.ts +++ b/packages/sidecar-placement/test/store.drizzle.test.ts @@ -11,7 +11,7 @@ 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 { e2eDatabaseUrl } from "../../../scripts/e2e/database-url"; import { createDrizzleSidecarPlacementStore } from "../src/store"; import { dbGate } from "../../../scripts/e2e/db-gate"; diff --git a/packages/webhook-triggers/test/migrations.test.ts b/packages/webhook-triggers/test/migrations.test.ts index 7f01295b41..3550148d46 100644 --- a/packages/webhook-triggers/test/migrations.test.ts +++ b/packages/webhook-triggers/test/migrations.test.ts @@ -7,7 +7,7 @@ import { afterAll, beforeAll, expect, test } from "bun:test"; import postgres from "postgres"; -import { e2eDatabaseUrl } from "../../../scripts/e2e/harness"; +import { e2eDatabaseUrl } from "../../../scripts/e2e/database-url"; import { applyWebhookTriggersMigrations } from "../src/migrations"; import { dbGate } from "../../../scripts/e2e/db-gate"; diff --git a/packages/webhook-triggers/test/store.drizzle.test.ts b/packages/webhook-triggers/test/store.drizzle.test.ts index af8f26db05..e31d25a5ad 100644 --- a/packages/webhook-triggers/test/store.drizzle.test.ts +++ b/packages/webhook-triggers/test/store.drizzle.test.ts @@ -17,7 +17,7 @@ import { createNoopCredentialCipher, } from "@intx/crypto"; -import { e2eDatabaseUrl } from "../../../scripts/e2e/harness"; +import { e2eDatabaseUrl } from "../../../scripts/e2e/database-url"; import { applyWebhookTriggersMigrations } from "../src/migrations"; import { createDrizzleWebhookTriggerStore } from "../src/store"; import { dbGate } from "../../../scripts/e2e/db-gate"; diff --git a/scripts/e2e/chat.test.ts b/scripts/e2e/chat.test.ts index deea7c8268..8feaf5f636 100644 --- a/scripts/e2e/chat.test.ts +++ b/scripts/e2e/chat.test.ts @@ -47,14 +47,13 @@ import { expectStatus, freePort, provisionSidecar, - pushWorkflowSource, - workflowDeployBody, startHub, startSidecar, type ApiResult, type HubHandle, type SpawnedApp, } from "./harness.ts"; +import { pushWorkflowSource, workflowDeployBody } from "./workflow-source.ts"; const databaseUrl = e2eDatabaseUrl(); if (databaseUrl === undefined) { diff --git a/scripts/e2e/database-url.ts b/scripts/e2e/database-url.ts new file mode 100644 index 0000000000..4311a5f70e --- /dev/null +++ b/scripts/e2e/database-url.ts @@ -0,0 +1,103 @@ +// The env-gate half of scripts/e2e/harness.ts, split out so a DB-gated +// unit suite (every package's migrations.test.ts / *.drizzle.test.ts) +// can resolve DATABASE_URL without pulling in harness.ts's process- +// spawning machinery — which imports @corbits/seeding and +// @corbits/workflows, and with them the full @intx/* module graph. +// That import chain costs over a second per test file purely to load, +// paid by every package's build-test run whether or not the suite +// ever runs (most don't: no DATABASE_URL locally, and build-test never +// provisions Postgres). harness.ts re-exports everything here so the +// handful of suites that do spawn the hub/sidecar keep one import. +import { readFileSync } from "node:fs"; +import path from "node:path"; +import { assertDatabaseConfigured } from "./db-gate.ts"; + +export const REPO_ROOT = path.resolve(import.meta.dir, "..", ".."); + +// --- environment gate ------------------------------------------------- + +/** + * The suite needs a real Postgres, named by DATABASE_URL. Locally a + * missing DATABASE_URL skips the suite (a fresh checkout without a + * database still runs the unit gates); in CI, CI=true turns that skip + * into a loud failure so the suite can never silently vanish from the + * pipeline. + * + * `bun test` workers do not always inherit process.env.DATABASE_URL + * even when the shell sourced `.env`. Fall back to the same repo-root + * `.env` file `bun run` would load. + */ +export function e2eDatabaseUrl(): string | undefined { + const fromProcess = process.env["DATABASE_URL"]; + const url = + fromProcess !== undefined && fromProcess !== "" + ? fromProcess + : databaseUrlFromRepoEnvFile(); + if (url !== undefined && url !== "") return baseUrlToE2eUrl(url); + assertDatabaseConfigured(undefined, "walking-skeleton suite"); + return undefined; +} + +/** + * Pull DATABASE_URL from a dotenv-style file body. Ignores blanks and + * comments; trims; strips one layer of surrounding quotes. Last match + * wins. Does not expand interpolations. + */ +export function parseEnvFileDatabaseUrl(text: string): string | undefined { + let found: string | undefined; + for (const rawLine of text.split(/\r?\n/)) { + const line = rawLine.trim(); + if (line === "" || line.startsWith("#")) continue; + if (!line.startsWith("DATABASE_URL=")) continue; + let value = line.slice("DATABASE_URL=".length).trim(); + if (value.length >= 2) { + const start = value[0]; + const end = value[value.length - 1]; + if ((start === '"' && end === '"') || (start === "'" && end === "'")) { + value = value.slice(1, -1); + } + } + found = value; + } + if (found === undefined || found === "") return undefined; + return found; +} + +function databaseUrlFromRepoEnvFile(): string | undefined { + let text: string; + try { + text = readFileSync(path.join(REPO_ROOT, ".env"), "utf8"); + } catch (error) { + if (isEnoent(error)) return undefined; + throw error; + } + return parseEnvFileDatabaseUrl(text); +} + +function isEnoent(error: unknown): boolean { + return ( + typeof error === "object" && + error !== null && + "code" in error && + error.code === "ENOENT" + ); +} + +/** + * The suite tears its schema down and rebuilds it on every run, so it + * must never run inside the developer's own database. It derives a + * sibling database (same server, name suffixed `_e2e`) and owns that + * one outright; scripts/db-setup.ts creates it on first use. + */ +export function baseUrlToE2eUrl(databaseUrl: string): string { + const url = new URL(databaseUrl); + const database = url.pathname.replace(/^\//, ""); + if (database === "") { + throw new Error( + `DATABASE_URL names no database (empty path): ${databaseUrl}. ` + + "Expected e.g. postgres://localhost:5432/workbench.", + ); + } + url.pathname = `/${database}_e2e`; + return url.toString(); +} diff --git a/scripts/e2e/harness.ts b/scripts/e2e/harness.ts index 099a7a2109..f4310f319e 100644 --- a/scripts/e2e/harness.ts +++ b/scripts/e2e/harness.ts @@ -5,106 +5,25 @@ // real the suite fails and says which hop, it never fakes the result. import { afterAll } from "bun:test"; -import { readFileSync } from "node:fs"; import { mkdtemp, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; import path from "node:path"; -import { createGitWorkflowPusher } from "../../packages/seeding/src/index.ts"; -import { WORKFLOW_SOURCE_ENTRY } from "../../packages/workflows/src/source.ts"; -import { assertDatabaseConfigured } from "./db-gate.ts"; -export const REPO_ROOT = path.resolve(import.meta.dir, "..", ".."); +// e2eDatabaseUrl and friends live in database-url.ts so a DB-gated unit +// suite can resolve DATABASE_URL without paying for this file's +// process-spawning imports (see that file's header comment). Re-exported +// here so the suites that do spawn the hub/sidecar keep one import. +export { + REPO_ROOT, + baseUrlToE2eUrl, + e2eDatabaseUrl, + parseEnvFileDatabaseUrl, +} from "./database-url.ts"; +import { REPO_ROOT } from "./database-url.ts"; + const HUB_DIR = path.join(REPO_ROOT, "apps", "hub"); const SIDECAR_DIR = path.join(REPO_ROOT, "apps", "sidecar"); -// --- environment gate ------------------------------------------------- - -/** - * The suite needs a real Postgres, named by DATABASE_URL. Locally a - * missing DATABASE_URL skips the suite (a fresh checkout without a - * database still runs the unit gates); in CI, CI=true turns that skip - * into a loud failure so the suite can never silently vanish from the - * pipeline. - * - * `bun test` workers do not always inherit process.env.DATABASE_URL - * even when the shell sourced `.env`. Fall back to the same repo-root - * `.env` file `bun run` would load. - */ -export function e2eDatabaseUrl(): string | undefined { - const fromProcess = process.env["DATABASE_URL"]; - const url = - fromProcess !== undefined && fromProcess !== "" - ? fromProcess - : databaseUrlFromRepoEnvFile(); - if (url !== undefined && url !== "") return baseUrlToE2eUrl(url); - assertDatabaseConfigured(undefined, "walking-skeleton suite"); - return undefined; -} - -/** - * Pull DATABASE_URL from a dotenv-style file body. Ignores blanks and - * comments; trims; strips one layer of surrounding quotes. Last match - * wins. Does not expand interpolations. - */ -export function parseEnvFileDatabaseUrl(text: string): string | undefined { - let found: string | undefined; - for (const rawLine of text.split(/\r?\n/)) { - const line = rawLine.trim(); - if (line === "" || line.startsWith("#")) continue; - if (!line.startsWith("DATABASE_URL=")) continue; - let value = line.slice("DATABASE_URL=".length).trim(); - if (value.length >= 2) { - const start = value[0]; - const end = value[value.length - 1]; - if ((start === '"' && end === '"') || (start === "'" && end === "'")) { - value = value.slice(1, -1); - } - } - found = value; - } - if (found === undefined || found === "") return undefined; - return found; -} - -function databaseUrlFromRepoEnvFile(): string | undefined { - let text: string; - try { - text = readFileSync(path.join(REPO_ROOT, ".env"), "utf8"); - } catch (error) { - if (isEnoent(error)) return undefined; - throw error; - } - return parseEnvFileDatabaseUrl(text); -} - -function isEnoent(error: unknown): boolean { - return ( - typeof error === "object" && - error !== null && - "code" in error && - error.code === "ENOENT" - ); -} - -/** - * The suite tears its schema down and rebuilds it on every run, so it - * must never run inside the developer's own database. It derives a - * sibling database (same server, name suffixed `_e2e`) and owns that - * one outright; scripts/db-setup.ts creates it on first use. - */ -export function baseUrlToE2eUrl(databaseUrl: string): string { - const url = new URL(databaseUrl); - const database = url.pathname.replace(/^\//, ""); - if (database === "") { - throw new Error( - `DATABASE_URL names no database (empty path): ${databaseUrl}. ` + - "Expected e.g. postgres://localhost:5432/workbench.", - ); - } - url.pathname = `/${database}_e2e`; - return url.toString(); -} - // --- hop naming ------------------------------------------------------- /** @@ -625,61 +544,7 @@ export function expectStepCompleted(events: RunEvent[], stepId: string): void { } } -// --- workflow asset content over git smart-HTTP ----------------------- - -/** - * Publishes a workflow definition into its asset repo in the one shape - * a `workflow`-kind asset accepts: the source codebase - * `@corbits/workflows`'s `./source` renders. Delegates to the platform's own - * pusher so the suite exercises the same publication path the seed and - * the product use, and returns the commit a code-sourced deploy pins. - */ -export async function pushWorkflowSource(options: { - baseUrl: string; - tenantId: string; - assetName: string; - tokenSecret: string; - workflowJson: string; -}): Promise<{ commitSha: string }> { - const pushed = await createGitWorkflowPusher()({ - remoteUrl: `${options.baseUrl}/api/tenants/${options.tenantId}/assets/workflow/${options.assetName}.git`, - tokenSecret: options.tokenSecret, - workflowJson: options.workflowJson, - packageName: options.assetName, - }); - return { commitSha: pushed.commitSha }; -} - -/** - * The deploy body a code-sourced asset deployment takes: the pushed - * commit is the definition's pin, and the entry names the - * `interchange.workflow` module the sidecar evaluates. - */ -export function workflowDeployBody(options: { - assetId: string; - commitSha: string; - sourceId: string; - provider: string; - baseURL: string; - apiKey: string; - model: string; -}): Record { - return { - source: { - kind: "asset", - assetId: options.assetId, - package: { format: "source", commitSha: options.commitSha }, - }, - entry: WORKFLOW_SOURCE_ENTRY, - sources: [ - { - id: options.sourceId, - provider: options.provider, - baseURL: options.baseURL, - apiKey: options.apiKey, - model: options.model, - }, - ], - defaultSource: options.sourceId, - }; -} +// pushWorkflowSource and workflowDeployBody moved to workflow-source.ts: +// both need @corbits/seeding and @corbits/workflows, which pull in the +// full @intx/* module graph, and most harness consumers never touch +// them (see that file's header comment). diff --git a/scripts/e2e/heartbeat.test.ts b/scripts/e2e/heartbeat.test.ts index c19a6b6a2c..a8b511153c 100644 --- a/scripts/e2e/heartbeat.test.ts +++ b/scripts/e2e/heartbeat.test.ts @@ -28,14 +28,13 @@ import { freePort, hop, provisionSidecar, - pushWorkflowSource, - workflowDeployBody, startHub, startSidecar, waitForRunCompletion, type ApiResult, type HubHandle, } from "./harness.ts"; +import { pushWorkflowSource, workflowDeployBody } from "./workflow-source.ts"; const databaseUrl = e2eDatabaseUrl(); if (databaseUrl === undefined) { diff --git a/scripts/e2e/smoke-webhook.test.ts b/scripts/e2e/smoke-webhook.test.ts index 1d248defaf..b23a757b12 100644 --- a/scripts/e2e/smoke-webhook.test.ts +++ b/scripts/e2e/smoke-webhook.test.ts @@ -38,12 +38,11 @@ import { freePort, hop, provisionSidecar, - pushWorkflowSource, - workflowDeployBody, startHub, startSidecar, type HubHandle, } from "./harness.ts"; +import { pushWorkflowSource, workflowDeployBody } from "./workflow-source.ts"; const { tempDir, track } = createCleanupHarness(); diff --git a/scripts/e2e/walking-skeleton.test.ts b/scripts/e2e/walking-skeleton.test.ts index 833612dd67..a93fb26632 100644 --- a/scripts/e2e/walking-skeleton.test.ts +++ b/scripts/e2e/walking-skeleton.test.ts @@ -34,13 +34,12 @@ import { freePort, hop, provisionSidecar, - pushWorkflowSource, - workflowDeployBody, startHub, startSidecar, type ApiResult, type HubHandle, } from "./harness.ts"; +import { pushWorkflowSource, workflowDeployBody } from "./workflow-source.ts"; const databaseUrl = e2eDatabaseUrl(); if (databaseUrl === undefined) { diff --git a/scripts/e2e/workbench-digest.test.ts b/scripts/e2e/workbench-digest.test.ts index ecf2dc56eb..45dc34c227 100644 --- a/scripts/e2e/workbench-digest.test.ts +++ b/scripts/e2e/workbench-digest.test.ts @@ -28,14 +28,13 @@ import { freePort, hop, provisionSidecar, - pushWorkflowSource, - workflowDeployBody, startHub, startSidecar, waitForRunCompletion, type ApiResult, type HubHandle, } from "./harness.ts"; +import { pushWorkflowSource, workflowDeployBody } from "./workflow-source.ts"; const databaseUrl = e2eDatabaseUrl(); if (databaseUrl === undefined) { diff --git a/scripts/e2e/workflow-source.ts b/scripts/e2e/workflow-source.ts new file mode 100644 index 0000000000..c8cf4aac7a --- /dev/null +++ b/scripts/e2e/workflow-source.ts @@ -0,0 +1,65 @@ +// Publishing a workflow definition as a `workflow`-kind asset — split out +// of harness.ts because both helpers below need `@corbits/seeding` and +// `@corbits/workflows`, which pull in the full `@intx/*` module graph. +// Most harness consumers (spawning the hub/sidecar, calling its HTTP +// API) never touch these two; keeping them here means importing +// harness.ts no longer pays that load cost on their behalf. +import { createGitWorkflowPusher } from "../../packages/seeding/src/index.ts"; +import { WORKFLOW_SOURCE_ENTRY } from "../../packages/workflows/src/source.ts"; + +/** + * Publishes a workflow definition into its asset repo in the one shape + * a `workflow`-kind asset accepts: the source codebase + * `@corbits/workflows`'s `./source` renders. Delegates to the platform's own + * pusher so the suite exercises the same publication path the seed and + * the product use, and returns the commit a code-sourced deploy pins. + */ +export async function pushWorkflowSource(options: { + baseUrl: string; + tenantId: string; + assetName: string; + tokenSecret: string; + workflowJson: string; +}): Promise<{ commitSha: string }> { + const pushed = await createGitWorkflowPusher()({ + remoteUrl: `${options.baseUrl}/api/tenants/${options.tenantId}/assets/workflow/${options.assetName}.git`, + tokenSecret: options.tokenSecret, + workflowJson: options.workflowJson, + packageName: options.assetName, + }); + return { commitSha: pushed.commitSha }; +} + +/** + * The deploy body a code-sourced asset deployment takes: the pushed + * commit is the definition's pin, and the entry names the + * `interchange.workflow` module the sidecar evaluates. + */ +export function workflowDeployBody(options: { + assetId: string; + commitSha: string; + sourceId: string; + provider: string; + baseURL: string; + apiKey: string; + model: string; +}): Record { + return { + source: { + kind: "asset", + assetId: options.assetId, + package: { format: "source", commitSha: options.commitSha }, + }, + entry: WORKFLOW_SOURCE_ENTRY, + sources: [ + { + id: options.sourceId, + provider: options.provider, + baseURL: options.baseURL, + apiKey: options.apiKey, + model: options.model, + }, + ], + defaultSource: options.sourceId, + }; +} From 35484d192be7fc2910bf84a4d23e3afa51561f2d Mon Sep 17 00:00:00 2001 From: Sawyer Date: Sun, 6 Sep 2026 14:14:31 -0700 Subject: [PATCH 4/8] Add tests for scripts/run-all.ts's --shard flag A hosted CI runner's fixed core count caps how much package fan-out alone can cut build-test's wall time; the next commit lets the test script split across a matrix of runners instead. Add its test coverage first: --shard i/n partitions the stably name-sorted job list with no package dropped or run twice, rejects a malformed shard spec, and scripts/e2e/db-gate.test.ts covers the "build-test-shard" GITHUB_JOB name the matrix job actually reports. --- scripts/e2e/db-gate.test.ts | 2 ++ scripts/run-all.test.ts | 46 +++++++++++++++++++++++++++++++++++-- 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/scripts/e2e/db-gate.test.ts b/scripts/e2e/db-gate.test.ts index 0bf569090d..b47b2826ef 100644 --- a/scripts/e2e/db-gate.test.ts +++ b/scripts/e2e/db-gate.test.ts @@ -47,6 +47,8 @@ describe("databaseIsRequired", () => { process.env["CI"] = "true"; process.env["GITHUB_JOB"] = "build-test"; expect(databaseIsRequired()).toBe(false); + process.env["GITHUB_JOB"] = "build-test-shard"; + expect(databaseIsRequired()).toBe(false); process.env["GITHUB_JOB"] = "structural"; expect(databaseIsRequired()).toBe(false); process.env["GITHUB_JOB"] = "lint"; diff --git a/scripts/run-all.test.ts b/scripts/run-all.test.ts index 6cf9239664..c9b7abdade 100644 --- a/scripts/run-all.test.ts +++ b/scripts/run-all.test.ts @@ -7,7 +7,7 @@ import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { basename, join } from "node:path"; -import { resolveConcurrency } from "./run-all.ts"; +import { parseShardArg, resolveConcurrency, selectShard } from "./run-all.ts"; const RUNNER = join(import.meta.dir, "run-all.ts"); @@ -62,6 +62,7 @@ type RunnerResult = { async function runProbe( extraEnv: Record = {}, script = "probe", + extraArgs: readonly string[] = [], ): Promise { logCounter += 1; const logPath = join(workspace, `probe-${logCounter}.log`); @@ -79,7 +80,7 @@ async function runProbe( delete env["WORKBENCH_CHECK_CONCURRENCY"]; } - const child = Bun.spawn(["bun", "run", RUNNER, script], { + const child = Bun.spawn(["bun", "run", RUNNER, script, ...extraArgs], { cwd: workspace, env, stdout: "pipe", @@ -262,4 +263,45 @@ describe("run-all", () => { test("fixture workspace holds only the packages the tests declare", () => { expect(basename(workspace).startsWith("workbench-run-all-")).toBe(true); }); + + test("splits a package run across shards with --shard i/n", async () => { + const results = await Promise.all( + [1, 2, 3].map((i) => runProbe({}, "probe", ["--shard", `${i}/3`])), + ); + + const ranIn = (stdout: string) => + WITH_PROBE.filter((name) => stdout.includes(`probe ran in ${name}`)); + const allRun = results.flatMap((r) => ranIn(r.stdout)); + + // Every package the fixture declares runs exactly once across the + // three shards combined — none dropped, none run twice. + expect(allRun.sort()).toEqual([...WITH_PROBE].sort()); + for (const result of results) expect(result.exitCode).toBe(0); + }); + + test("rejects a malformed --shard argument", async () => { + const result = await runProbe({}, "probe", ["--shard", "bogus"]); + + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("--shard"); + }); + + test("selectShard partitions a stably-ordered job list with no overlap", () => { + const jobs = Array.from({ length: 10 }, (_, i) => ({ + name: `pkg-${i}`, + dir: `packages/pkg-${i}`, + })); + const shards = [1, 2, 3].map((i) => + selectShard(jobs, parseShardArg(`${i}/3`)), + ); + + const combined = shards.flatMap((s) => s.map((j) => j.name)).sort(); + expect(combined).toEqual(jobs.map((j) => j.name).sort()); + }); + + test("parseShardArg rejects an out-of-range or malformed shard", () => { + expect(() => parseShardArg("bogus")).toThrow(); + expect(() => parseShardArg("0/3")).toThrow(); + expect(() => parseShardArg("4/3")).toThrow(); + }); }); From 57e64b89b865cfc1ca248f11b3db6d11b665654d Mon Sep 17 00:00:00 2001 From: Sawyer Date: Sun, 6 Sep 2026 14:14:36 -0700 Subject: [PATCH 5/8] run-all: support splitting a script run across shards --shard i/n round-robins the workspace's stably name-sorted package list across n shards and runs only shard i's assignment, so a script run can be split across several CI runners instead of one. Add "build-test-shard" next to "build-test" in dbGate's no-Postgres job set, since that's the actual GITHUB_JOB name a sharded matrix run carries. --- scripts/e2e/db-gate.ts | 6 +++++ scripts/run-all.ts | 53 +++++++++++++++++++++++++++++++++++++++--- 2 files changed, 56 insertions(+), 3 deletions(-) diff --git a/scripts/e2e/db-gate.ts b/scripts/e2e/db-gate.ts index 5a2684d18f..67569c6b64 100644 --- a/scripts/e2e/db-gate.ts +++ b/scripts/e2e/db-gate.ts @@ -26,7 +26,13 @@ export const MISSING_DATABASE_HINT = const CI_JOBS_WITHOUT_POSTGRES = new Set([ "lint", "typecheck", + // "build-test" itself is now a no-op summary job (see ci.yml); the + // actual GITHUB_JOB name a sharded build-test matrix run carries is + // "build-test-shard". Both stay listed: the summary job sets no env + // and never runs a suite, but naming it costs nothing and keeps this + // set matching ci.yml by inspection rather than by memory. "build-test", + "build-test-shard", "structural", ]); diff --git a/scripts/run-all.ts b/scripts/run-all.ts index a2b3ee1e7a..aca6fff9c8 100644 --- a/scripts/run-all.ts +++ b/scripts/run-all.ts @@ -63,7 +63,35 @@ async function discover(script: string): Promise { jobs.push({ name: manifest.name ?? dir, dir }); } } - return jobs; + // Glob.scan's order depends on the filesystem, not the package name, so + // a shard split over unsorted output would assign a different package + // set from one CI run to the next. Sorting by name first makes the + // split (and which packages land in which shard) reproducible. + return jobs.toSorted((a, b) => a.name.localeCompare(b.name)); +} + +export type ShardSpec = { readonly index: number; readonly count: number }; + +/** Parses a `--shard i/n` argument (1-based index, e.g. "1/3" is the + * first of three shards). Throws with the raw value on anything else. */ +export function parseShardArg(raw: string): ShardSpec { + const match = /^(\d+)\/(\d+)$/.exec(raw); + if (match === null) { + throw new Error(`--shard must look like "i/n", got "${raw}"`); + } + const index = Number.parseInt(match[1] ?? "", 10); + const count = Number.parseInt(match[2] ?? "", 10); + if (count < 1 || index < 1 || index > count) { + throw new Error(`--shard "i/n" needs 1 <= i <= n, got "${raw}"`); + } + return { index: index - 1, count }; +} + +/** Round-robins the (stably ordered) job list across shards rather than + * slicing it into contiguous runs, so a cluster of heavy packages next + * to each other alphabetically doesn't all land in the same shard. */ +export function selectShard(jobs: readonly Job[], shard: ShardSpec): Job[] { + return jobs.filter((_job, i) => i % shard.count === shard.index); } // Output is captured and flushed as one block per package. Streaming it would @@ -93,11 +121,29 @@ async function runJob(job: Job, script: string): Promise { if (import.meta.main) { const scriptArg = process.argv[2]; if (!scriptArg) { - console.error("usage: bun run scripts/run-all.ts "); + console.error( + "usage: bun run scripts/run-all.ts [--shard i/n]", + ); process.exit(1); } const script: string = scriptArg; + const shardFlagIndex = process.argv.indexOf("--shard"); + let shard: ShardSpec | undefined; + if (shardFlagIndex >= 0) { + const raw = process.argv[shardFlagIndex + 1]; + if (raw === undefined) { + console.error('--shard requires an "i/n" argument'); + process.exit(1); + } + try { + shard = parseShardArg(raw); + } catch (cause) { + console.error(cause instanceof Error ? cause.message : String(cause)); + process.exit(1); + } + } + let concurrency: number; try { concurrency = resolveConcurrency(script); @@ -106,7 +152,8 @@ if (import.meta.main) { process.exit(1); } - const jobs = await discover(script); + const allJobs = await discover(script); + const jobs = shard === undefined ? allJobs : selectShard(allJobs, shard); if (jobs.length === 0) { console.log(`${script}: no workspace packages define it yet`); process.exit(0); From 6df511031981776719314c77c12c5fa64e24f589 Mon Sep 17 00:00:00 2001 From: Sawyer Date: Sun, 6 Sep 2026 14:14:43 -0700 Subject: [PATCH 6/8] Turn CI's build-test job into a 3-way shard matrix A hosted GitHub Actions runner's 4 vCPUs meant scripts/run-all.ts's own package-level concurrency alone couldn't cut build-test's test step much past ~115s (its total CPU-seconds across every package's test script divided by 4 cores). Splitting the same total work across 3 runners' worth of vCPUs is what actually buys more parallelism. build-test-shard is now a 3-entry matrix, each running `bun run build` and `scripts/run-all.ts test --shard /3`; shard 1 also carries the root-level scripts/*.test.ts suite, which only needs to run once. build-test itself becomes a summary job that needs build-test-shard and fails if any shard did, keeping the required check name every branch protection rule and PR template names unchanged. --- .github/workflows/ci.yml | 35 +++++++++++++++++++++++++++++++++-- AGENTS.md | 5 +++-- 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a353634006..a909cda753 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -68,7 +68,19 @@ jobs: typecheck-${{ runner.os }}-${{ hashFiles('bun.lock', '.bun-version', 'tsconfig.base.json', 'scripts/generate-tsconfig-references.ts') }}- - run: bun run typecheck - build-test: + # A hosted runner's 4 vCPUs cap how much a single job's package fan-out + # (scripts/run-all.ts's own concurrency) can cut wall-clock: the total + # CPU-seconds across every package's test script divided by 4 was still + # over 90s. Splitting the same total work across 3 runners, each with + # its own 4 vCPUs, is what actually buys more parallelism. --shard i/3 + # (scripts/run-all.ts) round-robins the workspace's stably name-sorted + # package list across shards, so a run's set of packages-per-shard is + # reproducible and every package lands in exactly one shard. + build-test-shard: + strategy: + fail-fast: false + matrix: + shard: [1, 2, 3] runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -76,7 +88,26 @@ jobs: fetch-depth: 0 - uses: ./.github/actions/setup-workbench - run: bun run build - - run: bun run test + # The root-level scripts/*.test.ts suite (scripts/run-all.test.ts + # among them) isn't itself sharded — it only needs to run once, so + # it rides along on shard 1. + - if: matrix.shard == 1 + run: bun test ./scripts/*.test.ts + - run: bun run scripts/run-all.ts test --shard ${{ matrix.shard }}/3 + + # The required check every branch protection rule and PR template + # names is "build-test" (predating the shard split above); this job + # keeps that name live by depending on every shard and failing if any + # of them did. + build-test: + needs: build-test-shard + if: always() + runs-on: ubuntu-latest + steps: + - if: contains(needs.build-test-shard.result, 'failure') || contains(needs.build-test-shard.result, 'cancelled') + run: | + echo "at least one build-test shard failed" >&2 + exit 1 structural: runs-on: ubuntu-latest diff --git a/AGENTS.md b/AGENTS.md index e14d3474df..793a005aa8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -72,8 +72,9 @@ check:structural `. `docker compose -f docker-compose.test.yml up -d`. On CI jobs that provision Postgres (`e2e`, `isolation`, `db-suites`), `CI=true` turns a missing URL into a hard failure via `dbGate`. The unit - jobs (`build-test`, `structural`, `lint`, `typecheck`) never - provision Postgres and still skip. + jobs (`build-test-shard` — `build-test` itself is now a summary job + over its 3-way shard matrix — `structural`, `lint`, `typecheck`) + never provision Postgres and still skip. - `HUB_DATA_DIR` — hub boot fails. This is required runtime config, not a test flag. - `CI` — set automatically by GitHub Actions (`true`). Not a caller From c5d872ba833636dc87de3eb005a8a95082f59b00 Mon Sep 17 00:00:00 2001 From: Sawyer Date: Sun, 6 Sep 2026 14:21:35 -0700 Subject: [PATCH 7/8] ci-jobs: point the merge-base-fetch assertion at build-test-shard build-test is now a no-op summary job over the build-test-shard matrix; the matrix job is the one that actually checks out the repo with fetch-depth: 0 for tool-package-freshness's merge-base diff. --- scripts/ci-jobs.test.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/scripts/ci-jobs.test.ts b/scripts/ci-jobs.test.ts index f9b9fab069..4a132ca7ec 100644 --- a/scripts/ci-jobs.test.ts +++ b/scripts/ci-jobs.test.ts @@ -27,7 +27,15 @@ function jobBodies(yaml: string): Map { const SETUP = "./.github/actions/setup-workbench"; const POSTGRES_IMAGE = "pgvector/pgvector:pg17"; const DB_JOBS = ["e2e", "isolation", "db-suites"] as const; -const MERGE_BASE_JOBS = ["typecheck", "build-test", "structural"] as const; +// "build-test" itself is a no-op summary job over the build-test-shard +// matrix (see ci.yml) — the matrix job is the one that actually checks +// out the repo and needs full history for tool-package-freshness's +// merge-base diff. +const MERGE_BASE_JOBS = [ + "typecheck", + "build-test-shard", + "structural", +] as const; test("CI splits e2e, isolation, and db-suites onto their own Postgres jobs", async () => { const yaml = await readFile(join(ROOT, ".github/workflows/ci.yml"), "utf8"); From 8be47a1adf309bfaab121965d64995e10559c222 Mon Sep 17 00:00:00 2001 From: Sawyer Date: Sun, 6 Sep 2026 14:21:42 -0700 Subject: [PATCH 8/8] agent-lifecycle: widen the sweep-timing test's real-timer margins MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Running the test suite at full concurrency (this stack's earlier commits) surfaced a genuine flake here: sweepIntervalMs=5 / idleSleepMs=10 / a 15ms wait window assume the event loop keeps up within single-digit milliseconds, which a CPU-contended CI runner running many package test scripts at once cannot guarantee. Scale every one of those constants up by 8x (5->40, 10->80, 15->120, 20->160) — same relative timing the tests already assert on, just wide enough margins to tolerate tens of milliseconds of scheduler jitter under load. idleSleepMs: 1_000 tests are unaffected; that window was already generous. --- packages/agent-lifecycle/test/index.test.ts | 56 ++++++++++----------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/packages/agent-lifecycle/test/index.test.ts b/packages/agent-lifecycle/test/index.test.ts index 3bd38356dc..c0cc3a5311 100644 --- a/packages/agent-lifecycle/test/index.test.ts +++ b/packages/agent-lifecycle/test/index.test.ts @@ -22,10 +22,10 @@ function waitForTicks(count = 1): Promise { if (remaining <= 0) { resolve(); } else { - setTimeout(tick, 5); + setTimeout(tick, 40); } }; - setTimeout(tick, 5); + setTimeout(tick, 40); }); } @@ -41,8 +41,8 @@ describe("createAgentLifecycle", () => { const routable = new Set(["agent-1@t.test"]); const lifecycle = createAgentLifecycle({ - idleSleepMs: 10, - sweepIntervalMs: 5, + idleSleepMs: 80, + sweepIntervalMs: 40, isRoutable: (address) => routable.has(address), undeploy: async (address, reason) => { undeployCalls.push({ address, reason }); @@ -59,13 +59,13 @@ describe("createAgentLifecycle", () => { lifecycle.track("agent-1@t.test"); lifecycle.recordActivity("agent-1@t.test"); - // First sweep tick happens quickly (sweepIntervalMs=5) but the - // instance is not idle for the full 10ms yet. + // First sweep tick happens quickly (sweepIntervalMs=40) but the + // instance is not idle for the full 80ms yet. await waitForTicks(1); expect(undeployCalls).toEqual([]); // Wait past idleSleepMs and let another sweep tick run. - await new Promise((resolve) => setTimeout(resolve, 15)); + await new Promise((resolve) => setTimeout(resolve, 120)); await waitForTicks(1); expect(undeployCalls).toEqual([ @@ -79,7 +79,7 @@ describe("createAgentLifecycle", () => { const lifecycle = createAgentLifecycle({ idleSleepMs: 1_000, - sweepIntervalMs: 5, + sweepIntervalMs: 40, isRoutable: (address) => routable.has(address), undeploy: async (address, reason) => { undeployCalls.push({ address, reason }); @@ -101,8 +101,8 @@ describe("createAgentLifecycle", () => { const routable = new Set(["agent-1@t.test"]); const lifecycle = createAgentLifecycle({ - idleSleepMs: 10, - sweepIntervalMs: 5, + idleSleepMs: 80, + sweepIntervalMs: 40, isRoutable: (address) => routable.has(address), undeploy: async (address, reason) => { undeployCalls.push({ address, reason }); @@ -116,7 +116,7 @@ describe("createAgentLifecycle", () => { lifecycle.track("agent-1@t.test"); lifecycle.recordActivity("agent-1@t.test"); - await new Promise((resolve) => setTimeout(resolve, 15)); + await new Promise((resolve) => setTimeout(resolve, 120)); await waitForTicks(1); expect(undeployCalls).toEqual([]); @@ -127,8 +127,8 @@ describe("createAgentLifecycle", () => { const routable = new Set(["agent-1@t.test"]); const lifecycle = createAgentLifecycle({ - idleSleepMs: 10, - sweepIntervalMs: 5, + idleSleepMs: 80, + sweepIntervalMs: 40, isRoutable: (address) => routable.has(address), undeploy: async (address, reason) => { undeployCalls.push({ address, reason }); @@ -142,7 +142,7 @@ describe("createAgentLifecycle", () => { // swept. lifecycle.recordActivity("agent-1@t.test"); - await new Promise((resolve) => setTimeout(resolve, 15)); + await new Promise((resolve) => setTimeout(resolve, 120)); await waitForTicks(1); expect(undeployCalls).toEqual([]); @@ -153,8 +153,8 @@ describe("createAgentLifecycle", () => { const routable = new Set(["agent-1@t.test"]); const lifecycle = createAgentLifecycle({ - idleSleepMs: 10, - sweepIntervalMs: 5, + idleSleepMs: 80, + sweepIntervalMs: 40, isRoutable: (address) => routable.has(address), undeploy: async (address, reason) => { undeployCalls.push({ address, reason }); @@ -174,7 +174,7 @@ describe("createAgentLifecycle", () => { expect(undeployCalls).toEqual([]); // Only after the grace-seeded clock itself goes idle does it sleep. - await new Promise((resolve) => setTimeout(resolve, 15)); + await new Promise((resolve) => setTimeout(resolve, 120)); await waitForTicks(1); expect(undeployCalls).toEqual([ { address: "agent-1@t.test", reason: IDLE_HIBERNATE_UNDEPLOY_REASON }, @@ -186,8 +186,8 @@ describe("createAgentLifecycle", () => { const routable = new Set(); const lifecycle = createAgentLifecycle({ - idleSleepMs: 10, - sweepIntervalMs: 5, + idleSleepMs: 80, + sweepIntervalMs: 40, isRoutable: (address) => routable.has(address), undeploy: async (address, reason) => { undeployCalls.push({ address, reason }); @@ -200,7 +200,7 @@ describe("createAgentLifecycle", () => { lifecycle.track("agent-1@t.test"); lifecycle.recordActivity("agent-1@t.test"); - await new Promise((resolve) => setTimeout(resolve, 15)); + await new Promise((resolve) => setTimeout(resolve, 120)); await waitForTicks(1); expect(undeployCalls).toEqual([]); @@ -214,8 +214,8 @@ describe("createAgentLifecycle", () => { let maxConcurrentUndeploys = 0; const lifecycle = createAgentLifecycle({ - idleSleepMs: 10, - sweepIntervalMs: 5, + idleSleepMs: 80, + sweepIntervalMs: 40, isRoutable: (address) => routable.has(address), undeploy: async (address) => { concurrentUndeploys += 1; @@ -242,7 +242,7 @@ describe("createAgentLifecycle", () => { // land while the first sweep's undeploy is still pending. If ticks // overlapped, this would call undeploy on the same address a // second time before the first has resolved. - await new Promise((resolve) => setTimeout(resolve, 15)); + await new Promise((resolve) => setTimeout(resolve, 120)); await waitForTicks(1); expect(undeployCalls).toEqual(["agent-1@t.test"]); @@ -251,7 +251,7 @@ describe("createAgentLifecycle", () => { expect(maxConcurrentUndeploys).toBe(1); releaseUndeploy?.(); - await new Promise((resolve) => setTimeout(resolve, 5)); + await new Promise((resolve) => setTimeout(resolve, 40)); }); test("ensureAwake no-ops when already routable", async () => { @@ -297,7 +297,7 @@ describe("createAgentLifecycle", () => { // Give both calls a tick to have entered `ensureAwake` and observed // the same pending wake. - await new Promise((resolve) => setTimeout(resolve, 5)); + await new Promise((resolve) => setTimeout(resolve, 40)); expect(wakeCalls).toEqual(["agent-1@t.test"]); resolveWake?.(); @@ -354,7 +354,7 @@ describe("createAgentLifecycle", () => { const lifecycle = createAgentLifecycle({ idleSleepMs: 1_000, - wakeTimeoutMs: 20, + wakeTimeoutMs: 160, isRoutable: (address) => routable.has(address), undeploy: async () => undefined, wake: async () => { @@ -405,7 +405,7 @@ describe("createAgentLifecycle", () => { const lifecycle = createAgentLifecycle({ idleSleepMs: 1_000, - wakeTimeoutMs: 20, + wakeTimeoutMs: 160, isRoutable: (address) => routable.has(address), undeploy: async () => undefined, wake: async (address) => { @@ -421,7 +421,7 @@ describe("createAgentLifecycle", () => { const first = lifecycle.ensureAwake("agent-1@t.test"); - // The first caller's own 20ms budget expires while the underlying + // The first caller's own 160ms budget expires while the underlying // wake is still running. await expect(first).rejects.toThrow(/wake/i); expect(wakeCalls).toBe(1);