// Autumn surface: read the usage events the target ACTUALLY tracked to its// billing backend. Metering is fire-and-forget (the engine decorator forks the// `autumn.track` call so billing can't stall a user-facing execution), so "this// execution was billed" is an eventually-consistent fact that lives in Autumn's// ledger, not in the execution's own response. This surface reads that ledger// from the suite's Autumn emulator: the layer where a "we silently stopped// metering" regression (the unmetered MCP plane these scenarios pin) is actually// observable. A missing track looks identical to health from the product side,// so the contract has to be pinned where the usage is read.import { Effect, Schedule } from "effect"; import { DEFAULT_PLAN_EXECUTIONS_INCLUDED } from "../../setup/autumn-plans"; /** One usage event Autumn recorded: a `track` of `value` units of `featureId` * for `customerId` (the organization the execution ran under). */export interface UsageEvent { readonly customerId: string; readonly featureId: string; readonly value: number;} export interface UsageQuery { /** The Autumn customer — the organization id the metered work ran under. */ readonly customerId: string; /** The metered feature, e.g. "executions". */ readonly featureId: string;} /** A one-shot (or counted) fault to arm against a matching Autumn request. Mirrors * the emulator's `POST /_emulate/faults` body: a request matching every provided * `match` criterion short-circuits with `response`, `times` decrements per hit * (default 1, removed at zero), and `delayMs` stalls before the fault returns — * the knob a timeout scenario uses to push Autumn past the gate's time budget. */export interface FaultInput { readonly match: { readonly operationId?: string; readonly method?: string; readonly pathPattern?: string; }; readonly response: { readonly status: number; readonly body?: unknown }; readonly times?: number; readonly delayMs?: number;} /** One request the emulator recorded in its ledger, narrowed to what these * scenarios assert on: which operation ran and whether a fault short-circuited * it. A faulted `balances.check` entry is the proof the gate actually consulted * Autumn (and then failed open) rather than never checking at all. */export interface LedgerEntry { readonly operationId?: string; readonly method: string; readonly path: string; readonly faulted: boolean; /** The customer the request was made against, read from its body. Autumn's * ledger is shared by every scenario in the run, so attempts have to be * attributed to one organization before they can be counted. */ readonly customerId?: string;} export interface AutumnSurface { /** One-shot read of the matching usage events from the ledger. */ readonly usageEvents: (query: UsageQuery) => Effect.Effect<readonly UsageEvent[], unknown>; /** Every customer id Autumn currently holds (`customers.list`). An org that * was never provisioned as a billing customer is simply absent — the state * in which every balance call 404s `customer_not_found` forever. */ readonly customerIds: () => Effect.Effect<readonly string[], unknown>; /** Poll until at least `count` matching events have landed. The track is * forked and the worker drains it on `waitUntil` shortly after the execution * returns, so arrival is eventually-consistent — polling IS the contract: * "the execution reaches the meter, soon". */ readonly expectUsage: ( query: UsageQuery & { readonly count: number }, ) => Effect.Effect<readonly UsageEvent[], unknown>; /** Land the asynchronous checkout webhook for a checkout session, activating * its subscription. In production this is Stripe's `checkout.session.completed` * reaching Autumn shortly after the browser is redirected back; the emulator * defers activation until this is called, so a test controls the exact moment * the billing backend becomes consistent. The `sessionId` is the last path * segment of the hosted checkout URL the browser was sent to. */ readonly settleCheckout: (sessionId: string) => Effect.Effect<void, unknown>; /** Burn an org's entire remaining "executions" balance in one `balances.track`, * so the next `balances.check` reports `allowed: false`. The amount is the * default plan's included allotment (read from the plan seed, never hardcoded), * which drives `remaining` to zero. This is the setup a "blocked at cap" * scenario runs before opening the session it expects to be gated. */ readonly exhaustExecutions: (customerId: string) => Effect.Effect<void, unknown>; /** Put an org on a plan directly (`billing.attach`), skipping the hosted * checkout. Only valid for a plan that needs no payment — Enterprise carries * no price and no card-required trial, so the emulator activates its * subscription inline. Team does have both, so it must still go through the * browser checkout + `settleCheckout`. This is the setup a scenario runs when * it needs a PAID org and the upgrade journey itself is not what's under * test (e.g. the rate-limit backstop's paid exemption). */ readonly attachPlan: (customerId: string, planId: string) => Effect.Effect<void, unknown>; /** Arm a fault against a matching Autumn request (`POST /_emulate/faults`). */ readonly armFault: (input: FaultInput) => Effect.Effect<void, unknown>; /** Clear every armed fault (`DELETE /_emulate/faults`) — the finalizer that * keeps a scenario's fault from leaking into the next one. */ readonly clearFaults: () => Effect.Effect<void, unknown>; /** Read the emulator's request ledger, filtered to one operation — used to * prove a faulted `balances.check` was actually attempted (fail-open proof). */ readonly ledgerFor: (operationId: string) => Effect.Effect<readonly LedgerEntry[], unknown>; /** The customer's `members` balance as Autumn holds it — the seat count the * org is billed on — or null when the customer's plan carries no members * item. */ readonly memberSeats: ( customerId: string, ) => Effect.Effect<{ usage: number; granted: number; unlimited: boolean } | null, unknown>; /** Poll until the billed seat count reaches `usage`. Seat reporting is * forked off the mutating request (like usage tracking), so arrival is * eventually-consistent — polling IS the contract. */ readonly expectMemberSeats: ( customerId: string, usage: number, ) => Effect.Effect<{ usage: number; granted: number; unlimited: boolean }, unknown>;} export const makeAutumnSurface = (autumnUrl: string): AutumnSurface => { const usageEvents = (query: UsageQuery) => Effect.gen(function* () { // The emulator's ledger endpoint returns every recorded track event; // filter to this customer + feature. (autumn-js drives /v1/* RPC-style.) const response = yield* Effect.promise(() => fetch(`${autumnUrl}/v1/events.list`, { method: "POST", headers: { "content-type": "application/json" }, body: "{}", }), ); if (!response.ok) { return yield* Effect.fail( `autumn events.list responded ${response.status}: ${yield* Effect.promise(() => response.text())}`, ); } const body = (yield* Effect.promise(() => response.json())) as { readonly list?: ReadonlyArray<{ readonly customer_id: string; readonly feature_id: string; readonly value: number; }>; }; return (body.list ?? []) .filter( (event) => event.customer_id === query.customerId && event.feature_id === query.featureId, ) .map((event) => ({ customerId: event.customer_id, featureId: event.feature_id, value: event.value, })); }); const customerIds = () => Effect.gen(function* () { const response = yield* Effect.promise(() => fetch(`${autumnUrl}/v1/customers.list`, { method: "POST", headers: { "content-type": "application/json" }, body: "{}", }), ); if (!response.ok) { return yield* Effect.fail( `autumn customers.list responded ${response.status}: ${yield* Effect.promise(() => response.text())}`, ); } const body = (yield* Effect.promise(() => response.json())) as { readonly list?: ReadonlyArray<{ readonly id?: string }>; }; return (body.list ?? []).map((customer) => customer.id ?? ""); }); const settleCheckout = (sessionId: string) => Effect.gen(function* () { const response = yield* Effect.promise(() => fetch(`${autumnUrl}/checkout/${encodeURIComponent(sessionId)}/settle`, { method: "POST" }), ); if (!response.ok) { return yield* Effect.fail( `autumn checkout settle responded ${response.status}: ${yield* Effect.promise(() => response.text())}`, ); } }); // Track exactly the plan's included allotment in one event, driving // `remaining` (included - usage) to zero so the next check reports blocked. const exhaustExecutions = (customerId: string) => Effect.gen(function* () { const response = yield* Effect.promise(() => fetch(`${autumnUrl}/v1/balances.track`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ customer_id: customerId, feature_id: "executions", value: DEFAULT_PLAN_EXECUTIONS_INCLUDED, }), }), ); if (!response.ok) { return yield* Effect.fail( `autumn balances.track responded ${response.status}: ${yield* Effect.promise(() => response.text())}`, ); } }); const attachPlan = (customerId: string, planId: string) => Effect.gen(function* () { const response = yield* Effect.promise(() => fetch(`${autumnUrl}/v1/billing.attach`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ customer_id: customerId, plan_id: planId }), }), ); if (!response.ok) { return yield* Effect.fail( `autumn billing.attach responded ${response.status}: ${yield* Effect.promise(() => response.text())}`, ); } // A plan that needs payment answers with a checkout URL instead of // activating; that silently leaves the org on Free, and a scenario that // asked for a paid org would then assert against the wrong plan. const body = (yield* Effect.promise(() => response.json())) as { readonly payment_url?: string | null; }; if (body.payment_url) { return yield* Effect.fail( `autumn billing.attach returned a checkout URL for '${planId}': it requires payment, so it cannot be attached directly`, ); } }); const memberSeats = (customerId: string) => Effect.gen(function* () { const response = yield* Effect.promise(() => fetch(`${autumnUrl}/v1/customers.get_or_create`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ customer_id: customerId }), }), ); if (!response.ok) { return yield* Effect.fail( `autumn customers.get_or_create responded ${response.status}: ${yield* Effect.promise(() => response.text())}`, ); } const body = (yield* Effect.promise(() => response.json())) as { readonly balances?: Record< string, { readonly usage?: number; readonly granted?: number; readonly unlimited?: boolean } >; }; const balance = body.balances?.["members"]; return balance ? { usage: balance.usage ?? 0, granted: balance.granted ?? 0, unlimited: balance.unlimited === true, } : null; }); const armFault = (input: FaultInput) => Effect.gen(function* () { const response = yield* Effect.promise(() => fetch(`${autumnUrl}/_emulate/faults`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(input), }), ); if (!response.ok) { return yield* Effect.fail( `autumn arm fault responded ${response.status}: ${yield* Effect.promise(() => response.text())}`, ); } }); const clearFaults = () => Effect.gen(function* () { const response = yield* Effect.promise(() => fetch(`${autumnUrl}/_emulate/faults`, { method: "DELETE" }), ); if (!response.ok) { return yield* Effect.fail( `autumn clear faults responded ${response.status}: ${yield* Effect.promise(() => response.text())}`, ); } }); const ledgerFor = (operationId: string) => Effect.gen(function* () { const response = yield* Effect.promise(() => fetch(`${autumnUrl}/_emulate/ledger`)); if (!response.ok) { return yield* Effect.fail( `autumn ledger responded ${response.status}: ${yield* Effect.promise(() => response.text())}`, ); } const body = (yield* Effect.promise(() => response.json())) as { readonly entries?: ReadonlyArray<{ readonly operationId?: string; readonly method?: string; readonly path?: string; readonly faulted?: boolean; readonly request?: { readonly body?: unknown }; }>; }; return (body.entries ?? []) .filter((entry) => entry.operationId === operationId) .map((entry) => { const requestBody = entry.request?.body; const customerId = typeof requestBody === "object" && requestBody !== null ? (requestBody as { readonly customer_id?: unknown }).customer_id : undefined; return { operationId: entry.operationId, method: entry.method ?? "", path: entry.path ?? "", faulted: entry.faulted === true, customerId: typeof customerId === "string" ? customerId : undefined, }; }); }); return { usageEvents, customerIds, settleCheckout, exhaustExecutions, attachPlan, armFault, clearFaults, ledgerFor, memberSeats, expectMemberSeats: (customerId, usage) => memberSeats(customerId).pipe( Effect.filterOrFail( (balance): balance is { usage: number; granted: number; unlimited: boolean } => balance !== null && balance.usage === usage, (balance) => `members balance for ${customerId} is ${balance ? balance.usage : "absent"}, expected ${usage}`, ), // Same ceiling as expectUsage: the forked seat sync drains on the // worker's waitUntil shortly after the mutating request returns. Effect.retry(Schedule.both(Schedule.spaced("500 millis"), Schedule.recurs(40))), ), expectUsage: (query) => usageEvents(query).pipe( Effect.filterOrFail( (events) => events.length >= query.count, (events) => `only ${events.length}/${query.count} '${query.featureId}' usage events for ${query.customerId}`, ), // ~20s ceiling (40 x 500ms): the forked track drains on the worker's // waitUntil shortly after the execution returns; slower is a real bug. Effect.retry(Schedule.both(Schedule.spaced("500 millis"), Schedule.recurs(40))), ), };};
// Autumn surface: read the usage events the target ACTUALLY tracked to its
// billing backend. Metering is fire-and-forget (the engine decorator forks the
// `autumn.track` call so billing can't stall a user-facing execution), so "this
// execution was billed" is an eventually-consistent fact that lives in Autumn's
// ledger, not in the execution's own response. This surface reads that ledger
// from the suite's Autumn emulator: the layer where a "we silently stopped
// metering" regression (the unmetered MCP plane these scenarios pin) is actually
// observable. A missing track looks identical to health from the product side,
// so the contract has to be pinned where the usage is read.
import { Effect, Schedule } from "effect";
import { DEFAULT_PLAN_EXECUTIONS_INCLUDED } from "../../setup/autumn-plans";
/** One usage event Autumn recorded: a `track` of `value` units of `featureId`
* for `customerId` (the organization the execution ran under). */
export interface UsageEvent {
readonly customerId: string;
readonly featureId: string;
readonly value: number;
}
export interface UsageQuery {
/** The Autumn customer — the organization id the metered work ran under. */
readonly customerId: string;
/** The metered feature, e.g. "executions". */
readonly featureId: string;
}
/** A one-shot (or counted) fault to arm against a matching Autumn request. Mirrors
* the emulator's `POST /_emulate/faults` body: a request matching every provided
* `match` criterion short-circuits with `response`, `times` decrements per hit
* (default 1, removed at zero), and `delayMs` stalls before the fault returns —
* the knob a timeout scenario uses to push Autumn past the gate's time budget. */
export interface FaultInput {
readonly match: {
readonly operationId?: string;
readonly method?: string;
readonly pathPattern?: string;
};
readonly response: { readonly status: number; readonly body?: unknown };
readonly times?: number;
readonly delayMs?: number;
}
/** One request the emulator recorded in its ledger, narrowed to what these
* scenarios assert on: which operation ran and whether a fault short-circuited
* it. A faulted `balances.check` entry is the proof the gate actually consulted
* Autumn (and then failed open) rather than never checking at all. */
export interface LedgerEntry {
readonly operationId?: string;
readonly method: string;
readonly path: string;
readonly faulted: boolean;
/** The customer the request was made against, read from its body. Autumn's
* ledger is shared by every scenario in the run, so attempts have to be
* attributed to one organization before they can be counted. */
readonly customerId?: string;
}
export interface AutumnSurface {
/** One-shot read of the matching usage events from the ledger. */
readonly usageEvents: (query: UsageQuery) => Effect.Effect<readonly UsageEvent[], unknown>;
/** Every customer id Autumn currently holds (`customers.list`). An org that
* was never provisioned as a billing customer is simply absent — the state
* in which every balance call 404s `customer_not_found` forever. */
readonly customerIds: () => Effect.Effect<readonly string[], unknown>;
/** Poll until at least `count` matching events have landed. The track is
* forked and the worker drains it on `waitUntil` shortly after the execution
* returns, so arrival is eventually-consistent — polling IS the contract:
* "the execution reaches the meter, soon". */
readonly expectUsage: (
query: UsageQuery & { readonly count: number },
) => Effect.Effect<readonly UsageEvent[], unknown>;
/** Land the asynchronous checkout webhook for a checkout session, activating
* its subscription. In production this is Stripe's `checkout.session.completed`
* reaching Autumn shortly after the browser is redirected back; the emulator
* defers activation until this is called, so a test controls the exact moment
* the billing backend becomes consistent. The `sessionId` is the last path
* segment of the hosted checkout URL the browser was sent to. */
readonly settleCheckout: (sessionId: string) => Effect.Effect<void, unknown>;
/** Burn an org's entire remaining "executions" balance in one `balances.track`,
* so the next `balances.check` reports `allowed: false`. The amount is the
* default plan's included allotment (read from the plan seed, never hardcoded),
* which drives `remaining` to zero. This is the setup a "blocked at cap"
* scenario runs before opening the session it expects to be gated. */
readonly exhaustExecutions: (customerId: string) => Effect.Effect<void, unknown>;
/** Put an org on a plan directly (`billing.attach`), skipping the hosted
* checkout. Only valid for a plan that needs no payment — Enterprise carries
* no price and no card-required trial, so the emulator activates its
* subscription inline. Team does have both, so it must still go through the
* browser checkout + `settleCheckout`. This is the setup a scenario runs when
* it needs a PAID org and the upgrade journey itself is not what's under
* test (e.g. the rate-limit backstop's paid exemption). */
readonly attachPlan: (customerId: string, planId: string) => Effect.Effect<void, unknown>;
/** Arm a fault against a matching Autumn request (`POST /_emulate/faults`). */
readonly armFault: (input: FaultInput) => Effect.Effect<void, unknown>;
/** Clear every armed fault (`DELETE /_emulate/faults`) — the finalizer that
* keeps a scenario's fault from leaking into the next one. */
readonly clearFaults: () => Effect.Effect<void, unknown>;
/** Read the emulator's request ledger, filtered to one operation — used to
* prove a faulted `balances.check` was actually attempted (fail-open proof). */
readonly ledgerFor: (operationId: string) => Effect.Effect<readonly LedgerEntry[], unknown>;
/** The customer's `members` balance as Autumn holds it — the seat count the
* org is billed on — or null when the customer's plan carries no members
* item. */
readonly memberSeats: (
customerId: string,
) => Effect.Effect<{ usage: number; granted: number; unlimited: boolean } | null, unknown>;
/** Poll until the billed seat count reaches `usage`. Seat reporting is
* forked off the mutating request (like usage tracking), so arrival is
* eventually-consistent — polling IS the contract. */
readonly expectMemberSeats: (
customerId: string,
usage: number,
) => Effect.Effect<{ usage: number; granted: number; unlimited: boolean }, unknown>;
}
export const makeAutumnSurface = (autumnUrl: string): AutumnSurface => {
const usageEvents = (query: UsageQuery) =>
Effect.gen(function* () {
// The emulator's ledger endpoint returns every recorded track event;
// filter to this customer + feature. (autumn-js drives /v1/* RPC-style.)
const response = yield* Effect.promise(() =>
fetch(`${autumnUrl}/v1/events.list`, {
method: "POST",
headers: { "content-type": "application/json" },
body: "{}",
}),
);
if (!response.ok) {
return yield* Effect.fail(
`autumn events.list responded ${response.status}: ${yield* Effect.promise(() => response.text())}`,
);
}
const body = (yield* Effect.promise(() => response.json())) as {
readonly list?: ReadonlyArray<{
readonly customer_id: string;
readonly feature_id: string;
readonly value: number;
}>;
};
return (body.list ?? [])
.filter(
(event) => event.customer_id === query.customerId && event.feature_id === query.featureId,
)
.map((event) => ({
customerId: event.customer_id,
featureId: event.feature_id,
value: event.value,
}));
});
const customerIds = () =>
Effect.gen(function* () {
const response = yield* Effect.promise(() =>
fetch(`${autumnUrl}/v1/customers.list`, {
method: "POST",
headers: { "content-type": "application/json" },
body: "{}",
}),
);
if (!response.ok) {
return yield* Effect.fail(
`autumn customers.list responded ${response.status}: ${yield* Effect.promise(() => response.text())}`,
);
}
const body = (yield* Effect.promise(() => response.json())) as {
readonly list?: ReadonlyArray<{ readonly id?: string }>;
};
return (body.list ?? []).map((customer) => customer.id ?? "");
});
const settleCheckout = (sessionId: string) =>
Effect.gen(function* () {
const response = yield* Effect.promise(() =>
fetch(`${autumnUrl}/checkout/${encodeURIComponent(sessionId)}/settle`, { method: "POST" }),
);
if (!response.ok) {
return yield* Effect.fail(
`autumn checkout settle responded ${response.status}: ${yield* Effect.promise(() => response.text())}`,
);
}
});
// Track exactly the plan's included allotment in one event, driving
// `remaining` (included - usage) to zero so the next check reports blocked.
const exhaustExecutions = (customerId: string) =>
Effect.gen(function* () {
const response = yield* Effect.promise(() =>
fetch(`${autumnUrl}/v1/balances.track`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
customer_id: customerId,
feature_id: "executions",
value: DEFAULT_PLAN_EXECUTIONS_INCLUDED,
}),
}),
);
if (!response.ok) {
return yield* Effect.fail(
`autumn balances.track responded ${response.status}: ${yield* Effect.promise(() => response.text())}`,
);
}
});
const attachPlan = (customerId: string, planId: string) =>
Effect.gen(function* () {
const response = yield* Effect.promise(() =>
fetch(`${autumnUrl}/v1/billing.attach`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ customer_id: customerId, plan_id: planId }),
}),
);
if (!response.ok) {
return yield* Effect.fail(
`autumn billing.attach responded ${response.status}: ${yield* Effect.promise(() => response.text())}`,
);
}
// A plan that needs payment answers with a checkout URL instead of
// activating; that silently leaves the org on Free, and a scenario that
// asked for a paid org would then assert against the wrong plan.
const body = (yield* Effect.promise(() => response.json())) as {
readonly payment_url?: string | null;
};
if (body.payment_url) {
return yield* Effect.fail(
`autumn billing.attach returned a checkout URL for '${planId}': it requires payment, so it cannot be attached directly`,
);
}
});
const memberSeats = (customerId: string) =>
Effect.gen(function* () {
const response = yield* Effect.promise(() =>
fetch(`${autumnUrl}/v1/customers.get_or_create`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ customer_id: customerId }),
}),
);
if (!response.ok) {
return yield* Effect.fail(
`autumn customers.get_or_create responded ${response.status}: ${yield* Effect.promise(() => response.text())}`,
);
}
const body = (yield* Effect.promise(() => response.json())) as {
readonly balances?: Record<
string,
{ readonly usage?: number; readonly granted?: number; readonly unlimited?: boolean }
>;
};
const balance = body.balances?.["members"];
return balance
? {
usage: balance.usage ?? 0,
granted: balance.granted ?? 0,
unlimited: balance.unlimited === true,
}
: null;
});
const armFault = (input: FaultInput) =>
Effect.gen(function* () {
const response = yield* Effect.promise(() =>
fetch(`${autumnUrl}/_emulate/faults`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(input),
}),
);
if (!response.ok) {
return yield* Effect.fail(
`autumn arm fault responded ${response.status}: ${yield* Effect.promise(() => response.text())}`,
);
}
});
const clearFaults = () =>
Effect.gen(function* () {
const response = yield* Effect.promise(() =>
fetch(`${autumnUrl}/_emulate/faults`, { method: "DELETE" }),
);
if (!response.ok) {
return yield* Effect.fail(
`autumn clear faults responded ${response.status}: ${yield* Effect.promise(() => response.text())}`,
);
}
});
const ledgerFor = (operationId: string) =>
Effect.gen(function* () {
const response = yield* Effect.promise(() => fetch(`${autumnUrl}/_emulate/ledger`));
if (!response.ok) {
return yield* Effect.fail(
`autumn ledger responded ${response.status}: ${yield* Effect.promise(() => response.text())}`,
);
}
const body = (yield* Effect.promise(() => response.json())) as {
readonly entries?: ReadonlyArray<{
readonly operationId?: string;
readonly method?: string;
readonly path?: string;
readonly faulted?: boolean;
readonly request?: { readonly body?: unknown };
}>;
};
return (body.entries ?? [])
.filter((entry) => entry.operationId === operationId)
.map((entry) => {
const requestBody = entry.request?.body;
const customerId =
typeof requestBody === "object" && requestBody !== null
? (requestBody as { readonly customer_id?: unknown }).customer_id
: undefined;
return {
operationId: entry.operationId,
method: entry.method ?? "",
path: entry.path ?? "",
faulted: entry.faulted === true,
customerId: typeof customerId === "string" ? customerId : undefined,
};
});
});
return {
usageEvents,
customerIds,
settleCheckout,
exhaustExecutions,
attachPlan,
armFault,
clearFaults,
ledgerFor,
memberSeats,
expectMemberSeats: (customerId, usage) =>
memberSeats(customerId).pipe(
Effect.filterOrFail(
(balance): balance is { usage: number; granted: number; unlimited: boolean } =>
balance !== null && balance.usage === usage,
(balance) =>
`members balance for ${customerId} is ${balance ? balance.usage : "absent"}, expected ${usage}`,
),
// Same ceiling as expectUsage: the forked seat sync drains on the
// worker's waitUntil shortly after the mutating request returns.
Effect.retry(Schedule.both(Schedule.spaced("500 millis"), Schedule.recurs(40))),
),
expectUsage: (query) =>
usageEvents(query).pipe(
Effect.filterOrFail(
(events) => events.length >= query.count,
(events) =>
`only ${events.length}/${query.count} '${query.featureId}' usage events for ${query.customerId}`,
),
// ~20s ceiling (40 x 500ms): the forked track drains on the worker's
// waitUntil shortly after the execution returns; slower is a real bug.
Effect.retry(Schedule.both(Schedule.spaced("500 millis"), Schedule.recurs(40))),
),
};
};