mirror of
https://github.com/VibedByKaKi/t3-code-android-nightly.git
synced 2026-10-10 12:21:16 +02:00
263 lines
10 KiB
TypeScript
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"] },
|
|
);
|