// --------------------------------------------------------------------------- // Cloud's PostHog-backed enterprise-managed rollout gate. // // The gate is exercised through its production seam: the real // `makePostHogEnterpriseManagedRollout` with an injected `fetch` and an // injected `waitUntil`, exactly as `auth/jwks-cache.node.test.ts` drives the // JWKS client. Nothing is module-mocked, so the request these tests read is the // request PostHog would receive. // --------------------------------------------------------------------------- import { describe, expect, it } from "@effect/vitest"; import { Effect } from "effect"; import type { EnterpriseManagedRolloutContext } from "@executor-js/sdk"; import { ENTERPRISE_MANAGED_AUTH_FLAG_KEY, cloudEnterpriseManagedRollout, makePostHogEnterpriseManagedRollout, } from "./ema-rollout"; const HOST = "https://us.i.posthog.com"; const PROJECT_KEY = "phc_test_project_key"; const CONTEXT: EnterpriseManagedRolloutContext = { userId: "user_01ABC", organizationId: "org_01XYZ", integration: "slack" as EnterpriseManagedRolloutContext["integration"], }; interface CapturedRequest { readonly url: string; readonly body: Record; /** The bound the gate put on this call, when it set one. */ readonly signal: AbortSignal | null; } interface FetchHarness { readonly fetch: typeof globalThis.fetch; readonly requests: () => readonly CapturedRequest[]; /** Resolve every request the gate detached, so an assertion cannot race it. */ readonly settle: () => Promise; readonly waitUntil: (work: Promise) => void; } /** * Stands in for PostHog. `respond` decides what each call returns; throwing * from it models a transport failure, and an aborted signal models the timeout. */ const makeFetchHarness = ( respond: (request: CapturedRequest) => Response | Promise, ): FetchHarness => { const requests: CapturedRequest[] = []; const detached: Promise[] = []; return { requests: () => requests, settle: async () => { await Promise.all(detached); }, waitUntil: (work) => { detached.push(work); }, fetch: async (input, init) => { const raw = typeof init?.body === "string" ? init.body : "{}"; const captured: CapturedRequest = { url: String(input), // oxlint-disable-next-line executor/no-json-parse -- boundary: test fixture reads back the exact JSON body the gate serialized for PostHog body: JSON.parse(raw) as Record, signal: init?.signal instanceof AbortSignal ? init.signal : null, }; requests.push(captured); return respond(captured); }, }; }; const jsonResponse = (body: unknown, status = 200): Response => new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" }, }); const flagsBody = (enabled: boolean) => ({ flags: { [ENTERPRISE_MANAGED_AUTH_FLAG_KEY]: { key: ENTERPRISE_MANAGED_AUTH_FLAG_KEY, enabled, reason: { code: "condition_match" }, }, }, errorsWhileComputingFlags: false, }); const gate = (harness: FetchHarness, timeoutMs?: number) => makePostHogEnterpriseManagedRollout({ projectKey: PROJECT_KEY, host: HOST, fetch: harness.fetch, waitUntil: harness.waitUntil, ...(timeoutMs === undefined ? {} : { timeoutMs }), }); describe("flag evaluation", () => { it.effect("keys the rollout on the user and carries the org as group context", () => Effect.gen(function* () { const harness = makeFetchHarness(() => jsonResponse(flagsBody(true))); const decision = yield* gate(harness).decide(CONTEXT); expect(decision).toEqual({ kind: "enabled" }); const [request] = harness.requests(); expect(request?.url).toBe(`${HOST}/flags?v=2`); expect( request?.body.distinct_id, "the rollout unit is the user, matching posthog.identify(user.id) in the browser", ).toBe(CONTEXT.userId); expect( request?.body.groups, "the org rides along so group targeting stays available without changing the rollout unit", ).toEqual({ organization: CONTEXT.organizationId }); expect(request?.body.api_key).toBe(PROJECT_KEY); expect(request?.signal, "the evaluation is always bounded").not.toBeNull(); }), ); it.effect("omits group context when the host named no organization", () => Effect.gen(function* () { const harness = makeFetchHarness(() => jsonResponse(flagsBody(true))); yield* gate(harness).decide({ ...CONTEXT, organizationId: null }); expect(harness.requests()[0]?.body.groups).toBeUndefined(); }), ); it.effect("withholds when the flag is off for this user", () => Effect.gen(function* () { const harness = makeFetchHarness(() => jsonResponse(flagsBody(false))); expect(yield* gate(harness).decide(CONTEXT)).toEqual({ kind: "withheld", reason: "disabled", }); }), ); it.effect("withholds when PostHog returned no verdict for the flag at all", () => Effect.gen(function* () { const harness = makeFetchHarness(() => // What a quota-limited project answers. jsonResponse({ flags: {}, errorsWhileComputingFlags: false, quotaLimited: ["feature_flags"], }), ); expect(yield* gate(harness).decide(CONTEXT)).toEqual({ kind: "withheld", reason: "disabled", }); }), ); }); describe("failing closed", () => { it.effect("withholds on a non-2xx answer", () => Effect.gen(function* () { const harness = makeFetchHarness(() => new Response("nope", { status: 503 })); expect(yield* gate(harness).decide(CONTEXT)).toEqual({ kind: "withheld", reason: "evaluation-unavailable", }); }), ); it.effect("withholds on a transport failure", () => Effect.gen(function* () { const harness = makeFetchHarness(() => { // oxlint-disable-next-line executor/no-try-catch-or-throw, executor/no-error-constructor -- boundary: models `fetch` rejecting with a TypeError, which is exactly how a Worker sees a dead upstream throw new TypeError("network error"); }); expect(yield* gate(harness).decide(CONTEXT)).toEqual({ kind: "withheld", reason: "evaluation-unavailable", }); }), ); it.effect("withholds when the evaluation times out", () => Effect.gen(function* () { // Never answers. Only the gate's own AbortSignal ends this call, so the // test fails by hanging if the gate ever stops bounding the evaluation. const harness = makeFetchHarness( (request) => new Promise((_resolve, reject) => { request.signal?.addEventListener("abort", () => // oxlint-disable-next-line executor/no-promise-reject, executor/no-error-constructor -- boundary: fetch-compatible fixture mirrors the platform's abort rejection semantics reject(new DOMException("aborted", "AbortError")), ); }), ); expect( yield* gate(harness, 1).decide(CONTEXT), "a slow flag service must degrade the rollout, never hold up a connect", ).toEqual({ kind: "withheld", reason: "evaluation-unavailable" }); }), ); it.effect("withholds on a body that is not a flags response", () => Effect.gen(function* () { const harness = makeFetchHarness(() => jsonResponse({ flags: "not-an-object" })); expect(yield* gate(harness).decide(CONTEXT)).toEqual({ kind: "withheld", reason: "evaluation-unavailable", }); }), ); it.effect("withholds when there is no acting user to roll out by", () => Effect.gen(function* () { const harness = makeFetchHarness(() => jsonResponse(flagsBody(true))); expect(yield* gate(harness).decide({ ...CONTEXT, userId: null })).toEqual({ kind: "withheld", reason: "evaluation-unavailable", }); expect(harness.requests().length, "and asks PostHog nothing").toBe(0); }), ); it.effect("withholds when the deployment has no PostHog configuration", () => Effect.gen(function* () { // The cloud test env carries no VITE_PUBLIC_POSTHOG_KEY, so this builds // the misconfigured-deployment gate — which is deliberately NOT the same // as a host that injects no gate at all. expect(yield* cloudEnterpriseManagedRollout().decide(CONTEXT)).toEqual({ kind: "withheld", reason: "evaluation-unavailable", }); }), ); }); describe("rollout events", () => { it.effect("captures the connect outcome with the flag decision attached", () => Effect.gen(function* () { const harness = makeFetchHarness(() => jsonResponse({ status: 1 })); const rollout = gate(harness); yield* rollout.record({ kind: "connected", context: CONTEXT, decision: { kind: "enabled" }, }); yield* Effect.promise(() => harness.settle()); const [request] = harness.requests(); expect(request?.url).toBe(`${HOST}/i/v0/e/`); expect(request?.body.event).toBe("ema_connect_connected"); expect(request?.body.distinct_id).toBe(CONTEXT.userId); const properties = request?.body.properties as Record; expect(properties.ema_flag_enabled).toBe(true); expect(properties[`$feature/${ENTERPRISE_MANAGED_AUTH_FLAG_KEY}`]).toBe(true); expect(properties.$groups).toEqual({ organization: CONTEXT.organizationId, }); expect(properties.integration_slug).toBe("slack"); }), ); it.effect("carries the withheld reason on the attempt that never ran", () => Effect.gen(function* () { const harness = makeFetchHarness(() => jsonResponse({ status: 1 })); yield* gate(harness).record({ kind: "attempted", context: CONTEXT, decision: { kind: "withheld", reason: "evaluation-unavailable" }, }); yield* Effect.promise(() => harness.settle()); const properties = harness.requests()[0]?.body.properties as Record; expect(harness.requests()[0]?.body.event).toBe("ema_connect_attempted"); expect(properties.ema_flag_enabled).toBe(false); expect(properties.ema_flag_withheld_reason).toBe("evaluation-unavailable"); }), ); it.effect("carries the identity provider's own error code on a blocked connect", () => Effect.gen(function* () { const harness = makeFetchHarness(() => jsonResponse({ status: 1 })); yield* gate(harness).record({ kind: "blocked-by-admin", context: CONTEXT, decision: { kind: "enabled" }, oauthErrorCode: "unauthorized_client", }); yield* Effect.promise(() => harness.settle()); const [request] = harness.requests(); expect(request?.body.event).toBe("ema_connect_blocked_by_admin"); const properties = request?.body.properties as Record | undefined; expect(properties?.oauth_error_code).toBe("unauthorized_client"); }), ); it.effect("sends nothing beyond identity, the integration and the flag decision", () => Effect.gen(function* () { const harness = makeFetchHarness(() => jsonResponse({ status: 1 })); yield* gate(harness).record({ kind: "blocked-by-admin", context: CONTEXT, decision: { kind: "enabled" }, oauthErrorCode: "access_denied", }); yield* Effect.promise(() => harness.settle()); const properties = harness.requests()[0]?.body.properties as Record; expect( Object.keys(properties).sort(), "a closed property set is what keeps a token or an assertion from ever being added by accident", ).toEqual( [ "$feature/mcp-enterprise-managed-auth", "$groups", "ema_flag_enabled", "integration_slug", "oauth_error_code", ].sort(), ); }), ); it.effect("hands the capture to the platform instead of awaiting it", () => Effect.gen(function* () { let resolveCapture: (() => void) | undefined; const harness = makeFetchHarness( () => new Promise((resolve) => { resolveCapture = () => resolve(jsonResponse({ status: 1 })); }), ); // Returns while the capture is still in flight: an analytics call can // never be on the critical path of a connect. yield* gate(harness).record({ kind: "attempted", context: CONTEXT, decision: { kind: "enabled" }, }); expect(harness.requests().length).toBe(1); resolveCapture?.(); yield* Effect.promise(() => harness.settle()); }), ); it.effect("survives a capture that fails outright", () => Effect.gen(function* () { const harness = makeFetchHarness(() => { // oxlint-disable-next-line executor/no-try-catch-or-throw, executor/no-error-constructor -- boundary: models the ingest endpoint being unreachable throw new TypeError("network error"); }); yield* gate(harness).record({ kind: "connected", context: CONTEXT, decision: { kind: "enabled" }, }); yield* Effect.promise(() => harness.settle()); }), ); it.effect("records nothing when there is no person to attach the event to", () => Effect.gen(function* () { const harness = makeFetchHarness(() => jsonResponse({ status: 1 })); yield* gate(harness).record({ kind: "attempted", context: { ...CONTEXT, userId: null }, decision: { kind: "withheld", reason: "evaluation-unavailable" }, }); expect(harness.requests().length).toBe(0); }), ); });