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..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,7 +10,8 @@ // HTTP surface (see api.request-scope.node.test.ts). // --------------------------------------------------------------------------- -import { afterAll, describe, expect, it } from "@effect/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"; import { HttpApiBuilder } from "effect/unstable/httpapi"; @@ -21,6 +22,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 +48,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 +93,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 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..e9b76e3ce2 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 "@effect/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,