From 13693c6fd7b633eb538ba7cdf0efccbbbbe75b54 Mon Sep 17 00:00:00 2001 From: Rhys Sullivan <39114868+RhysSullivan@users.noreply.github.com> Date: Mon, 14 Sep 2026 23:26:11 -0700 Subject: [PATCH 1/3] Keep seat reconciliation alive after the response --- apps/cloud/src/extensions/billing/member-seats.ts | 9 +++++---- apps/cloud/test-stubs/cloudflare-workers.ts | 4 ++++ 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/apps/cloud/src/extensions/billing/member-seats.ts b/apps/cloud/src/extensions/billing/member-seats.ts index 1c523cfdb7..75c1d7e00d 100644 --- a/apps/cloud/src/extensions/billing/member-seats.ts +++ b/apps/cloud/src/extensions/billing/member-seats.ts @@ -3,6 +3,7 @@ // --------------------------------------------------------------------------- import { Effect } from "effect"; +import { waitUntil } from "cloudflare:workers"; import { WorkOSClient } from "../../auth/workos"; import { AutumnService } from "./service"; @@ -38,9 +39,9 @@ export const reportMemberSeats = ( /** * Fork `reportMemberSeats` off the calling request, mirroring how execution * tracking is forked: billing must never stall or fail a user-facing - * request. Only boot-scoped services are captured (WorkOS + Autumn — no - * request-scoped resources), so the forked fiber cannot outlive anything it - * depends on. + * request. Cloudflare owns the promise through waitUntil, so the recount can + * finish after the response. Only boot-scoped WorkOS and Autumn services are + * captured. */ export const forkReportMemberSeats = ( organizationId: string, @@ -48,6 +49,6 @@ export const forkReportMemberSeats = ( Effect.gen(function* () { const ctx = yield* Effect.context(); yield* Effect.sync(() => { - Effect.runForkWith(ctx)(reportMemberSeats(organizationId)); + waitUntil(Effect.runPromiseWith(ctx)(reportMemberSeats(organizationId))); }); }); diff --git a/apps/cloud/test-stubs/cloudflare-workers.ts b/apps/cloud/test-stubs/cloudflare-workers.ts index 8dc4e9c53b..5207b3f2f0 100644 --- a/apps/cloud/test-stubs/cloudflare-workers.ts +++ b/apps/cloud/test-stubs/cloudflare-workers.ts @@ -7,6 +7,10 @@ // from `process.env` into this stub at import time. Without this bridge // the test DbService would dial the default postgres port instead of the // PGlite socket server started by `scripts/test-globalsetup.ts`. +import { vi } from "vitest"; + +export const waitUntil = vi.fn<(work: Promise) => void>(); + export const env: Record = { DATABASE_URL: process.env.DATABASE_URL, WORKOS_API_KEY: process.env.WORKOS_API_KEY, From cf077b6e787af0caeb57248dfa552ef4bcec1e9c Mon Sep 17 00:00:00 2001 From: Rhys Sullivan <39114868+RhysSullivan@users.noreply.github.com> Date: Mon, 14 Sep 2026 23:27:50 -0700 Subject: [PATCH 2/3] Await callback background work in the test fixture --- .../src/auth/workos-callback-state.node.test.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/apps/cloud/src/auth/workos-callback-state.node.test.ts b/apps/cloud/src/auth/workos-callback-state.node.test.ts index 7ba7f07458..82b9832de3 100644 --- a/apps/cloud/src/auth/workos-callback-state.node.test.ts +++ b/apps/cloud/src/auth/workos-callback-state.node.test.ts @@ -11,6 +11,8 @@ // --------------------------------------------------------------------------- import { afterAll, describe, expect, it } from "@effect/vitest"; +import { vi } from "vitest"; +import { waitUntil } from "cloudflare:workers"; import { Effect, Layer } from "effect"; import { HttpRouter, HttpServer } from "effect/unstable/http"; import { HttpApiBuilder } from "effect/unstable/httpapi"; @@ -21,6 +23,7 @@ import { CloudAuthPublicApi } from "./api"; import { UserStoreService } from "./context"; import { WorkOSClient, type WorkOSClientService } from "./workos"; import { encodeLoginState } from "./login-state"; +import { AutumnService } from "../extensions/billing/service"; // The route under test serves under the `/api` prefix in the composed app; // toWebHandler mounts the raw group, so paths here are relative to the group. @@ -46,6 +49,9 @@ const stubWorkOS = Layer.succeed( if (prop === "listUserMemberships") { return () => Effect.succeed({ data: [] }); } + if (prop === "listOrgMembers") { + return () => Effect.succeed({ data: [{ status: "active" }] }); + } return () => Effect.die(`unexpected WorkOSClient.${String(prop)} call`); }, }), @@ -88,11 +94,15 @@ const App = HttpApiBuilder.layer(PublicApi).pipe( Layer.provide(CloudAuthPublicHandlers), Layer.provide(stubWorkOS), Layer.provide(stubUsers), + Layer.provide(AutumnService.Default), Layer.provide(HttpServer.layerServices), ); const app = HttpRouter.toWebHandler(App, { disableLogger: true }); -afterAll(() => app.dispose()); +afterAll(async () => { + await Promise.all(vi.mocked(waitUntil).mock.calls.map(([work]) => work)); + await app.dispose(); +}); const run = (request: Request) => { // beta.59: the handler type expects a context argument; this layer stack From b84cc206b61b140f34901ac6b8fc35e60810a77e Mon Sep 17 00:00:00 2001 From: Rhys Sullivan <39114868+RhysSullivan@users.noreply.github.com> Date: Mon, 14 Sep 2026 23:28:30 -0700 Subject: [PATCH 3/3] Use Effect test helpers for background work fixtures --- apps/cloud/src/auth/workos-callback-state.node.test.ts | 3 +-- apps/cloud/test-stubs/cloudflare-workers.ts | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/apps/cloud/src/auth/workos-callback-state.node.test.ts b/apps/cloud/src/auth/workos-callback-state.node.test.ts index 82b9832de3..891ce8f6a4 100644 --- a/apps/cloud/src/auth/workos-callback-state.node.test.ts +++ b/apps/cloud/src/auth/workos-callback-state.node.test.ts @@ -10,8 +10,7 @@ // HTTP surface (see api.request-scope.node.test.ts). // --------------------------------------------------------------------------- -import { afterAll, describe, expect, it } from "@effect/vitest"; -import { vi } from "vitest"; +import { afterAll, describe, expect, it, vi } from "@effect/vitest"; import { waitUntil } from "cloudflare:workers"; import { Effect, Layer } from "effect"; import { HttpRouter, HttpServer } from "effect/unstable/http"; diff --git a/apps/cloud/test-stubs/cloudflare-workers.ts b/apps/cloud/test-stubs/cloudflare-workers.ts index 5207b3f2f0..e9b76e3ce2 100644 --- a/apps/cloud/test-stubs/cloudflare-workers.ts +++ b/apps/cloud/test-stubs/cloudflare-workers.ts @@ -7,7 +7,7 @@ // from `process.env` into this stub at import time. Without this bridge // the test DbService would dial the default postgres port instead of the // PGlite socket server started by `scripts/test-globalsetup.ts`. -import { vi } from "vitest"; +import { vi } from "@effect/vitest"; export const waitUntil = vi.fn<(work: Promise) => void>();