t3-code-android-nightly/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Images/Variant.test.ts
Julius Marminge 6f9cea00ae
chore(refs): sync Effect and Alchemy references to 4.0.1 and beta.80 (#16170)
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 13:22:30 -07:00

263 lines
10 KiB
TypeScript

import { adopt } from "@/AdoptPolicy";
import * as Cloudflare from "@/Cloudflare";
import { CloudflareEnvironment } from "@/Cloudflare/CloudflareEnvironment";
import * as Provider from "@/Provider";
import * as Test from "@/Test/Alchemy";
import * as images from "@distilled.cloud/cloudflare/images";
import { expect } from "alchemy-test";
import * as Effect from "effect/Effect";
import { MinimumLogLevel } from "effect/References";
import * as Schedule from "effect/Schedule";
const { test } = Test.make({ providers: Cloudflare.providers() });
const logLevel = Effect.provideService(
MinimumLogLevel,
process.env.DEBUG ? "Debug" : "Info",
);
// Out-of-band read of a variant, riding out eventual-consistency blips
// where the freshly created variant is not yet visible (`VariantNotFound`,
// Cloudflare error code 5401).
const getVariant = (
accountId: string,
variantId: string,
// Optional read-after-write predicate. Cloudflare's variant GET is
// eventually consistent, so an immediate read after a create/PATCH can
// return stale options; keep polling until the observed variant satisfies
// this before asserting on it.
until?: (res: images.GetV1VariantResponse) => boolean,
) =>
images.getV1Variant({ accountId, variantId }).pipe(
Effect.flatMap((res) =>
!until || until(res)
? Effect.succeed(res)
: Effect.fail({ _tag: "VariantStale" as const }),
),
Effect.retry({
// Ride out both "not yet visible" (`VariantNotFound`) and "visible but
// stale options" (`VariantStale`) read-after-write windows. Cloudflare's
// variant GET is served from an eventually-consistent read replica that,
// under full-suite parallel load, can lag a freshly created/patched
// variant well past a minute — so the budget is generous (~90s).
while: (e) => e._tag === "VariantNotFound" || e._tag === "VariantStale",
// Steady spacing (fixed, not exponential — an uncapped exponential
// reaches a 64s single delay by the 8th retry and blows the timeout).
// Polls every 3s rather than every 2s: under full-suite parallel load
// tighter polling only adds to the API request pressure that stretches
// these consistency windows in the first place.
schedule: Schedule.max([
Schedule.fixed("3 seconds"),
Schedule.recurs(30),
]),
}),
);
// Poll until the variant is gone — a missing variant surfaces as the typed
// `VariantNotFound`, which is the success condition here.
const expectGone = (accountId: string, variantId: string) =>
images.getV1Variant({ accountId, variantId }).pipe(
Effect.flatMap(() => Effect.fail({ _tag: "VariantNotDeleted" } as const)),
Effect.catchTag("VariantNotFound", () => Effect.void),
Effect.retry({
while: (e) => e._tag === "VariantNotDeleted",
// Bounded fixed spacing (~30s) — see `getVariant` on why the previous
// uncapped exponential could run past the test timeout.
schedule: Schedule.max([
Schedule.fixed("2 seconds"),
Schedule.recurs(15),
]),
}),
);
test.provider.skipIf(!!process.env.FAST)(
"create, update in place, and delete a variant",
(stack) =>
Effect.gen(function* () {
const { accountId } = yield* yield* CloudflareEnvironment;
yield* stack.destroy();
// Variant names are alphanumeric only (no hyphens/underscores) — use
// the logical ID verbatim as the deterministic name.
//
// Adopt: variants carry no ownership markers, so a variant left in the
// cloud by a previously crashed run (state never persisted) is
// indistinguishable from a foreign one and `read` reports it `Unowned`.
// These tests use fixed names and always run the same way, so take over
// any such leftover rather than failing with `OwnedBySomeoneElse`.
const created = yield* stack.deploy(
Cloudflare.Images.Variant("alchemytestvariant", {
fit: "cover",
width: 100,
height: 100,
}).pipe(adopt(true)),
);
expect(created.variantName).toEqual("alchemytestvariant");
expect(created.accountId).toEqual(accountId);
expect(created.fit).toEqual("cover");
expect(created.width).toEqual(100);
expect(created.height).toEqual(100);
expect(created.metadata).toEqual("none");
expect(created.neverRequireSignedURLs).toEqual(false);
// Out-of-band verification against the live API (ride out read-after-write
// lag until the created options are observable).
const live = yield* getVariant(
accountId,
created.variantName,
(r) =>
r.variant?.options.fit === "cover" &&
r.variant?.options.width === 100,
);
expect(live.variant?.id).toEqual("alchemytestvariant");
expect(live.variant?.options.fit).toEqual("cover");
expect(live.variant?.options.width).toEqual(100);
expect(live.variant?.options.height).toEqual(100);
// Update mutable options in place — same variant name, PATCHed options.
const updated = yield* stack.deploy(
Cloudflare.Images.Variant("alchemytestvariant", {
fit: "contain",
width: 200,
height: 100,
metadata: "copyright",
neverRequireSignedURLs: true,
}).pipe(adopt(true)),
);
expect(updated.variantName).toEqual("alchemytestvariant");
expect(updated.fit).toEqual("contain");
expect(updated.width).toEqual(200);
expect(updated.height).toEqual(100);
expect(updated.metadata).toEqual("copyright");
expect(updated.neverRequireSignedURLs).toEqual(true);
const patched = yield* getVariant(
accountId,
updated.variantName,
(r) =>
r.variant?.options.fit === "contain" &&
r.variant?.options.width === 200 &&
r.variant?.options.metadata === "copyright" &&
r.variant?.neverRequireSignedURLs === true,
);
expect(patched.variant?.options.fit).toEqual("contain");
expect(patched.variant?.options.width).toEqual(200);
expect(patched.variant?.options.metadata).toEqual("copyright");
expect(patched.variant?.neverRequireSignedURLs).toEqual(true);
// No-op redeploy — identical props, still the same variant.
const noop = yield* stack.deploy(
Cloudflare.Images.Variant("alchemytestvariant", {
fit: "contain",
width: 200,
height: 100,
metadata: "copyright",
neverRequireSignedURLs: true,
}).pipe(adopt(true)),
);
expect(noop.variantName).toEqual("alchemytestvariant");
yield* stack.destroy();
yield* expectGone(accountId, "alchemytestvariant");
// Destroy again — delete is idempotent.
yield* stack.destroy();
}).pipe(logLevel),
// Two sequential out-of-band verifies (post-create, post-update) each ride
// out a read-replica lag that can run ~90s under full-suite parallel load;
// the 120s default can't hold both plus the deploys/destroys.
{
tags: ["provider:cloudflare", "provider:cloudflare:images", "live"],
timeout: 240_000,
},
);
test.provider(
"replace a variant when the name changes",
(stack) =>
Effect.gen(function* () {
const { accountId } = yield* yield* CloudflareEnvironment;
yield* stack.destroy();
const initial = yield* stack.deploy(
Cloudflare.Images.Variant("ReplaceVariant", {
name: "alchemyreplacea",
fit: "cover",
width: 64,
height: 64,
}).pipe(adopt(true)),
);
expect(initial.variantName).toEqual("alchemyreplacea");
// Renaming the variant replaces it — the name is the API path id.
const replaced = yield* stack.deploy(
Cloudflare.Images.Variant("ReplaceVariant", {
name: "alchemyreplaceb",
fit: "cover",
width: 64,
height: 64,
}).pipe(adopt(true)),
);
expect(replaced.variantName).toEqual("alchemyreplaceb");
const live = yield* getVariant(accountId, "alchemyreplaceb");
expect(live.variant?.id).toEqual("alchemyreplaceb");
// The old variant is gone after the replacement completes.
yield* expectGone(accountId, "alchemyreplacea");
yield* stack.destroy();
yield* expectGone(accountId, "alchemyreplaceb");
}).pipe(logLevel),
// Same eventual-consistency budget as above: the post-replace verify plus
// two gone-assertions can exceed the 120s default under parallel load.
{
tags: ["provider:cloudflare", "provider:cloudflare:images", "live"],
timeout: 240_000,
},
);
// Canonical `list()` test (account collection): deploy a variant, resolve the
// provider with the typed `Provider.findProvider` helper, then assert the
// deployed variant appears in the exhaustively-enumerated result. Bracketed by
// `stack.destroy()` at both ends so the suite leaves no residue.
//
// SKIP-GATED on `CLOUDFLARE_TEST_IMAGES_LIST` until a distilled patch lands.
// distilled mis-types the list-variants response as a single variant, but the
// real body is a keyed `variants` map, so the strict decode fails and the GET
// surfaces a `CloudflareHttpError` (status 200, statusText "Schema decode
// failed"). Recovering from that catch-all is forbidden by the Typed Error
// Doctrine, so the fix is the distilled response-schema patch in the agent
// report. Once applied (and distilled `lib` rebuilt) this passes unchanged;
// set `CLOUDFLARE_TEST_IMAGES_LIST=1` to run it.
test.provider.skipIf(!process.env.CLOUDFLARE_TEST_IMAGES_LIST)(
"list enumerates the deployed variant",
(stack) =>
Effect.gen(function* () {
yield* stack.destroy();
const deployed = yield* stack.deploy(
Cloudflare.Images.Variant("alchemytestlistvariant", {
fit: "cover",
width: 120,
height: 120,
}).pipe(adopt(true)),
);
const provider = yield* Provider.findProvider(Cloudflare.Images.Variant);
const all = yield* provider.list();
expect(Array.isArray(all)).toBe(true);
expect(all.some((v) => v.variantName === deployed.variantName)).toBe(
true,
);
yield* stack.destroy();
}).pipe(logLevel),
{ tags: ["provider:cloudflare", "provider:cloudflare:images", "live"] },
);