* Make self-host tests CI-safe * Await cloud db teardown * Give local e2e boots more headroom * Keep main CI runs alive * Cache CI dependencies and image layers * Wait for recorded introspection requests in graphql plugin tests * Size self-host test budget for loaded runners * Probe test http servers for readiness before use * Die on recorder wait timeout instead of typing a string error * Type the recorder wait timeout as a tagged error * Update e2e locators for the integration copy renames * Keep stale data rendered while a refresh is waiting * Guard create-org input against hydration clobbering * Match the new sign-in heading in the selfhost callback test
Showing the changes to apps (8 of 27 files). Show all
Unified · Split
| @@ -73,16 +73,12 @@ const makePostgresResource = (): DbResource => { · 72 unmodified lines | ||
| 73 | 73 | sql, |
| 74 | 74 | db: drizzle(sql, { schema: combinedSchema }) as DrizzleDb, |
| 75 | 75 | close: () => |
| 76 | − Effect.sync(() => { | |
| 77 | − void Effect.runFork( | |
| 78 | − Effect.ignore( | |
| 79 | − Effect.tryPromise({ | |
| 80 | − try: () => sql.end({ timeout: 0 }), | |
| 81 | − catch: (cause) => cause, | |
| 82 | − }), | |
| 83 | − ), | |
| 84 | − ); | |
| 85 | − }), | |
| 76 | + Effect.ignore( | |
| 77 | + Effect.tryPromise({ | |
| 78 | + try: () => sql.end({ timeout: 0 }), | |
| 79 | + catch: (cause) => cause, | |
| 80 | + }), | |
| 81 | + ), | |
| 86 | 82 | }; |
| 87 | 83 | }; |
| 88 | 84 | |
| @@ -2,20 +2,26 @@ import { mkdtempSync } from "node:fs"; · 1 unmodified line | ||
| 2 | 2 | import { tmpdir } from "node:os"; |
| 3 | 3 | import { join } from "node:path"; |
| 4 | 4 | |
| 5 | −import { afterAll, expect, test } from "@effect/vitest"; | |
| 5 | +import { afterAll, beforeAll, expect, test } from "@effect/vitest"; | |
| 6 | 6 | |
| 7 | 7 | // Config reads the environment, so point it at a throwaway data dir before |
| 8 | 8 | // importing the app graph. |
| 9 | 9 | process.env.EXECUTOR_DATA_DIR = mkdtempSync(join(tmpdir(), "eh-boot-")); |
| 10 | 10 | |
| 11 | −const { makeSelfHostTestApp, singleAdminIdentityLayer } = await import("./testing/test-app"); | |
| 11 | +let handler!: (request: Request) => Promise<Response>; | |
| 12 | +let dispose: () => Promise<void> = async () => {}; | |
| 12 | 13 | |
| 13 | −const { handler, dispose } = await makeSelfHostTestApp({ | |
| 14 | − identity: singleAdminIdentityLayer({ | |
| 15 | − userId: "admin", | |
| 16 | − organizationId: "default-org", | |
| 17 | − organizationName: "Default", | |
| 18 | − }), | |
| 14 | +beforeAll(async () => { | |
| 15 | + const { makeSelfHostTestApp, singleAdminIdentityLayer } = await import("./testing/test-app"); | |
| 16 | + const app = await makeSelfHostTestApp({ | |
| 17 | + identity: singleAdminIdentityLayer({ | |
| 18 | + userId: "admin", | |
| 19 | + organizationId: "default-org", | |
| 20 | + organizationName: "Default", | |
| 21 | + }), | |
| 22 | + }); | |
| 23 | + handler = app.handler; | |
| 24 | + dispose = app.dispose; | |
| 19 | 25 | }); |
| 20 | 26 | afterAll(() => dispose()); |
| 21 | 27 | |
| @@ -2,7 +2,7 @@ import { mkdtempSync } from "node:fs"; · 1 unmodified line | ||
| 2 | 2 | import { tmpdir } from "node:os"; |
| 3 | 3 | import { join } from "node:path"; |
| 4 | 4 | |
| 5 | −import { afterAll, expect, test } from "@effect/vitest"; | |
| 5 | +import { afterAll, beforeAll, expect, test } from "@effect/vitest"; | |
| 6 | 6 | import { connectionIdentifier } from "@executor-js/sdk/shared"; |
| 7 | 7 | |
| 8 | 8 | import { mintInviteCode } from "./testing/mint-invite"; |
| @@ -13,9 +13,15 @@ process.env.BETTER_AUTH_SECRET = "multi-user-secret-0123456789-abcdefghij-klmn"; · 4 unmodified lines | ||
| 13 | 13 | process.env.EXECUTOR_BOOTSTRAP_ADMIN_EMAIL = "admin@multi.test"; |
| 14 | 14 | process.env.EXECUTOR_BOOTSTRAP_ADMIN_PASSWORD = "admin-pass-123456"; |
| 15 | 15 | |
| 16 | −const { makeSelfHostApiHandler } = await import("./app"); | |
| 16 | +let handler!: (request: Request) => Promise<Response>; | |
| 17 | +let dispose: () => Promise<void> = async () => {}; | |
| 17 | 18 | |
| 18 | −const { handler, dispose } = await makeSelfHostApiHandler(); | |
| 19 | +beforeAll(async () => { | |
| 20 | + const { makeSelfHostApiHandler } = await import("./app"); | |
| 21 | + const app = await makeSelfHostApiHandler(); | |
| 22 | + handler = app.handler; | |
| 23 | + dispose = app.dispose; | |
| 24 | +}); | |
| 19 | 25 | afterAll(() => dispose()); |
| 20 | 26 | |
| 21 | 27 | const BASE = "http://localhost:4788"; |
| @@ -2,7 +2,7 @@ import { mkdtempSync } from "node:fs"; · 1 unmodified line | ||
| 2 | 2 | import { tmpdir } from "node:os"; |
| 3 | 3 | import { join } from "node:path"; |
| 4 | 4 | |
| 5 | −import { afterAll, expect, test } from "@effect/vitest"; | |
| 5 | +import { afterAll, beforeAll, expect, test } from "@effect/vitest"; | |
| 6 | 6 | import { connectionIdentifier } from "@executor-js/sdk/shared"; |
| 7 | 7 | |
| 8 | 8 | process.env.EXECUTOR_DATA_DIR = mkdtempSync(join(tmpdir(), "eh-iso-")); |
| @@ -13,10 +13,16 @@ process.env.EXECUTOR_DATA_DIR = mkdtempSync(join(tmpdir(), "eh-iso-")); · 4 unmodified lines | ||
| 13 | 13 | // request-scoped. Each identity is its own (org, user): in v2 the org is the |
| 14 | 14 | // tenant (catalog partition) and the user is the acting subject (drives |
| 15 | 15 | // `owner: "user"` rows). |
| 16 | −const { makeSelfHostTestApp, headerIdentityLayer } = await import("./testing/test-app"); | |
| 16 | +let handler!: (request: Request) => Promise<Response>; | |
| 17 | +let dispose: () => Promise<void> = async () => {}; | |
| 17 | 18 | |
| 18 | −const { handler, dispose } = await makeSelfHostTestApp({ | |
| 19 | − identity: headerIdentityLayer, | |
| 19 | +beforeAll(async () => { | |
| 20 | + const { makeSelfHostTestApp, headerIdentityLayer } = await import("./testing/test-app"); | |
| 21 | + const app = await makeSelfHostTestApp({ | |
| 22 | + identity: headerIdentityLayer, | |
| 23 | + }); | |
| 24 | + handler = app.handler; | |
| 25 | + dispose = app.dispose; | |
| 20 | 26 | }); |
| 21 | 27 | afterAll(() => dispose()); |
| 22 | 28 | |
| @@ -135,7 +141,7 @@ test("concurrent requests with distinct identities get disjoint, correct executo · 112 unmodified lines | ||
| 135 | 141 | expect(addresses.some((a) => a.includes(connectionNameForUser(other)))).toBe(false); |
| 136 | 142 | } |
| 137 | 143 | }); |
| 138 | −}, 30_000); | |
| 144 | +}); | |
| 139 | 145 | |
| 140 | 146 | test("a request with no identity is rejected", async () => { |
| 141 | 147 | const res = await handler(new Request("http://localhost/api/connections")); |
| @@ -4,7 +4,7 @@ import { tmpdir } from "node:os"; · 3 unmodified lines | ||
| 4 | 4 | import { join } from "node:path"; |
| 5 | 5 | |
| 6 | 6 | import { Effect, Layer } from "effect"; |
| 7 | −import { afterAll, expect, test } from "@effect/vitest"; | |
| 7 | +import { afterAll, beforeAll, expect, test } from "@effect/vitest"; | |
| 8 | 8 | |
| 9 | 9 | import { AuthTemplateSlug, ConnectionName, IntegrationSlug } from "@executor-js/sdk"; |
| 10 | 10 | import { makeScopedExecutor } from "@executor-js/api/server"; |
| @@ -32,13 +32,20 @@ const createScopedExecutor = ( · 21 unmodified lines | ||
| 32 | 32 | Effect.provide(SelfHostScopedExecutorSeams), |
| 33 | 33 | ); |
| 34 | 34 | |
| 35 | −const dbHandle = await createSelfHostDb({ | |
| 36 | − path: dbPath, | |
| 37 | − namespace: "executor_selfhost", | |
| 38 | − version: "1.0.0", | |
| 35 | +let dbLayer!: Layer.Layer<SelfHostDb>; | |
| 36 | +let dbHandle: Awaited<ReturnType<typeof createSelfHostDb>> | undefined; | |
| 37 | + | |
| 38 | +beforeAll(async () => { | |
| 39 | + dbHandle = await createSelfHostDb({ | |
| 40 | + path: dbPath, | |
| 41 | + namespace: "executor_selfhost", | |
| 42 | + version: "1.0.0", | |
| 43 | + }); | |
| 44 | + dbLayer = Layer.succeed(SelfHostDb)(dbHandle); | |
| 45 | +}); | |
| 46 | +afterAll(async () => { | |
| 47 | + await dbHandle?.close(); | |
| 39 | 48 | }); |
| 40 | −const dbLayer = Layer.succeed(SelfHostDb)(dbHandle); | |
| 41 | −afterAll(() => dbHandle.close()); | |
| 42 | 49 | |
| 43 | 50 | const TINY_SPEC = JSON.stringify({ |
| 44 | 51 | openapi: "3.0.0", |
| @@ -3,7 +3,7 @@ import { tmpdir } from "node:os"; · 2 unmodified lines | ||
| 3 | 3 | import { join } from "node:path"; |
| 4 | 4 | |
| 5 | 5 | import { Effect, Layer } from "effect"; |
| 6 | −import { afterAll, expect, test } from "@effect/vitest"; | |
| 6 | +import { afterAll, beforeAll, expect, test } from "@effect/vitest"; | |
| 7 | 7 | |
| 8 | 8 | import { AuthTemplateSlug, ConnectionName, IntegrationSlug } from "@executor-js/sdk"; |
| 9 | 9 | import { makeScopedExecutor } from "@executor-js/api/server"; |
| @@ -50,8 +50,15 @@ const TINY_SPEC = JSON.stringify({ · 40 unmodified lines | ||
| 50 | 50 | }, |
| 51 | 51 | }); |
| 52 | 52 | |
| 53 | −const { makeSelfHostApiHandler } = await import("./app"); | |
| 54 | −const { handler, dispose } = await makeSelfHostApiHandler({ dbPath }); | |
| 53 | +let handler!: (request: Request) => Promise<Response>; | |
| 54 | +let dispose: () => Promise<void> = async () => {}; | |
| 55 | + | |
| 56 | +beforeAll(async () => { | |
| 57 | + const { makeSelfHostApiHandler } = await import("./app"); | |
| 58 | + const app = await makeSelfHostApiHandler({ dbPath }); | |
| 59 | + handler = app.handler; | |
| 60 | + dispose = app.dispose; | |
| 61 | +}); | |
| 55 | 62 | afterAll(() => dispose()); |
| 56 | 63 | |
| 57 | 64 | const BASE = "http://localhost:4788"; |
| @@ -3,7 +3,7 @@ import { tmpdir } from "node:os"; · 2 unmodified lines | ||
| 3 | 3 | import { join } from "node:path"; |
| 4 | 4 | |
| 5 | 5 | import { Effect, Layer } from "effect"; |
| 6 | −import { afterAll, expect, test } from "@effect/vitest"; | |
| 6 | +import { afterAll, beforeAll, expect, test } from "@effect/vitest"; | |
| 7 | 7 | |
| 8 | 8 | import { AuthTemplateSlug, ConnectionName, IntegrationSlug } from "@executor-js/sdk"; |
| 9 | 9 | import { makeScopedExecutor } from "@executor-js/api/server"; |
| @@ -28,13 +28,20 @@ const createScopedExecutor = ( · 18 unmodified lines | ||
| 28 | 28 | const dataDir = mkdtempSync(join(tmpdir(), "eh-src-")); |
| 29 | 29 | process.env.EXECUTOR_DATA_DIR = dataDir; |
| 30 | 30 | |
| 31 | −const dbHandle = await createSelfHostDb({ | |
| 32 | − path: join(dataDir, "data.db"), | |
| 33 | − namespace: "executor_selfhost", | |
| 34 | − version: "1.0.0", | |
| 31 | +let dbLayer!: Layer.Layer<SelfHostDb>; | |
| 32 | +let dbHandle: Awaited<ReturnType<typeof createSelfHostDb>> | undefined; | |
| 33 | + | |
| 34 | +beforeAll(async () => { | |
| 35 | + dbHandle = await createSelfHostDb({ | |
| 36 | + path: join(dataDir, "data.db"), | |
| 37 | + namespace: "executor_selfhost", | |
| 38 | + version: "1.0.0", | |
| 39 | + }); | |
| 40 | + dbLayer = Layer.succeed(SelfHostDb)(dbHandle); | |
| 41 | +}); | |
| 42 | +afterAll(async () => { | |
| 43 | + await dbHandle?.close(); | |
| 35 | 44 | }); |
| 36 | −const dbLayer = Layer.succeed(SelfHostDb)(dbHandle); | |
| 37 | −afterAll(() => dbHandle.close()); | |
| 38 | 45 | |
| 39 | 46 | // Inline OpenAPI spec so the test doesn't depend on the network to register. |
| 40 | 47 | const TINY_SPEC = JSON.stringify({ |
| @@ -4,30 +4,18 @@ export default defineConfig({ · 3 unmodified lines | ||
| 4 | 4 | test: { |
| 5 | 5 | include: ["src/**/*.test.ts"], |
| 6 | 6 | passWithNoTests: true, |
| 7 | − // These are integration suites: most boot a full self-host app graph | |
| 8 | − // (Better Auth + libSQL + MCP + plugins) at module load, then drive it | |
| 9 | − // over the in-memory handler, and some (scope-isolation) fire dozens of | |
| 10 | − // concurrent requests through it. Each boot is CPU-heavy and every query | |
| 11 | − // serializes through the one shared libSQL connection, so when many files | |
| 12 | − // boot at once they oversubscribe the CPU and starve the event loop: an | |
| 13 | − // in-flight request stalls indefinitely and the test times out. Which | |
| 14 | − // files lose the race is nondeterministic, which is the CI flakiness. | |
| 15 | − // | |
| 16 | − // Vitest sizes its fork pool to the core count, so a many-core machine | |
| 17 | − // oversubscribes HARDER than a small CI runner. Bound the concurrency to | |
| 18 | − // an absolute number instead, so behavior is the same everywhere: at most | |
| 19 | − // two heavy boots run together, which fits a 4-vCPU runner without | |
| 20 | − // starvation while still running the suite in parallel. Pair it with | |
| 21 | − // integration-grade timeouts as headroom for the slowest boot. This keeps | |
| 22 | − // every assertion intact (no logic or coverage changed) and makes the | |
| 23 | − // suite deterministic rather than load-dependent. | |
| 24 | − testTimeout: 30_000, | |
| 25 | − hookTimeout: 30_000, | |
| 26 | − poolOptions: { | |
| 27 | − forks: { | |
| 28 | − maxForks: 2, | |
| 29 | − minForks: 1, | |
| 30 | − }, | |
| 31 | − }, | |
| 7 | + // These are integration suites: several files boot a full self-host app | |
| 8 | + // graph (Better Auth + libSQL + MCP + plugins) in beforeAll, then drive it | |
| 9 | + // over the in-memory handler. Each boot is CPU-heavy and every query | |
| 10 | + // serializes through the one shared libSQL connection, so parallel files can | |
| 11 | + // oversubscribe CI and starve in-flight requests. Run files serially and | |
| 12 | + // give lifecycle hooks room for the cold graph boot. Tests that fan out | |
| 13 | + // many concurrent requests (scope-isolation) stretch well past 30s when | |
| 14 | + // sibling turbo tasks contend for the same cores, so the budget is sized | |
| 15 | + // for a loaded runner, not an idle one. | |
| 16 | + fileParallelism: false, | |
| 17 | + maxWorkers: 1, | |
| 18 | + testTimeout: 120_000, | |
| 19 | + hookTimeout: 60_000, | |
| 32 | 20 | }, |
| 33 | 21 | }); |