test(imaging): retry transient edge failures instead of flaking the suite
CI / check (push) Successful in 37s
CI / tests-integration (push) Successful in 2m3s
CI / tests-unit (push) Successful in 2m8s
CI / tests-imaging-browsers (push) Successful in 2m15s
CI / tests-ui (push) Successful in 2m54s
CI / preflight (push) Skipped
CI / tests-quality (push) Successful in 1m14s
CI / deploy (push) Successful in 3m37s

The cross-browser imaging job runs against the public origin, so every request
goes through Cloudflare. One chromium run of "rejects malformed figures ›
missing figure" came back 525 (Cloudflare: TLS handshake to the origin failed)
after a 40s stall — the request never reached the CMS, so a 400 was impossible
to observe and the assertion was effectively testing the network. It passed on
both firefox and webkit, which is the signature of an edge hiccup rather than a
regression.

`e2e/imaging/imaging-target.ts` now owns `getThroughEdge` / `fetchThroughEdge`,
which retry on the statuses that mean "the app was never reached"
(502/503/504 plus Cloudflare's 520-527) with a short backoff, and both
edge-fronted specs use them. A genuinely broken edge still exhausts the retries
and fails the run, so nothing is masked.

Verified: 84 passed / 21 skipped / 0 failed against epicnabbo.nl with
IMAGING_EDGE=1 across chromium, firefox and webkit.
This commit is contained in:
openhands committed 2026-10-15 20:30:38 +02:00
1 parent 77a0d1010c
commit 480343d5a5
3 files changed
+117 -22

No files matched your search

+55 -19
View File
@@ -1,5 +1,11 @@
import { expect, test } from "@playwright/test";
import { avatarPath, countBytes, looksLikeImage } from "./imaging-target";
import {
avatarPath,
countBytes,
fetchThroughEdge,
getThroughEdge,
looksLikeImage,
} from "./imaging-target";
// Endpoints exercised here are identical whether the CMS is reached directly
// or through the edge proxy, so this spec runs unchanged in the deploy gate and
@@ -17,7 +23,8 @@ const IMAGE_SOURCES = new Set([
test("renders an animated APNG for a figure with an effect", async ({
request,
}) => {
const response = await request.get(
const response = await getThroughEdge(
request,
avatarPath({ img_format: "apng", effect: "14" }),
);
expect(response.status()).toBe(200);
@@ -35,7 +42,10 @@ test("renders an animated APNG for a figure with an effect", async ({
test("serves a static PNG when no animation is requested", async ({
request,
}) => {
const response = await request.get(avatarPath({ img_format: "png" }));
const response = await getThroughEdge(
request,
avatarPath({ img_format: "png" }),
);
expect(response.status()).toBe(200);
expect(response.headers()["content-type"]).toContain("image/png");
const body = await response.body();
@@ -45,7 +55,10 @@ test("serves a static PNG when no animation is requested", async ({
test("advertises a cacheable response and a known render source", async ({
request,
}) => {
const response = await request.get(avatarPath({ img_format: "png" }));
const response = await getThroughEdge(
request,
avatarPath({ img_format: "png" }),
);
expect(response.status()).toBe(200);
const source = response.headers()["x-imager-source"];
@@ -63,14 +76,21 @@ test("advertises a cacheable response and a known render source", async ({
});
test("answers conditional requests with 304", async ({ request }) => {
const first = await request.get(avatarPath({ img_format: "png" }));
const first = await getThroughEdge(
request,
avatarPath({ img_format: "png" }),
);
expect(first.status()).toBe(200);
const etag = first.headers()["etag"];
expect(etag).toBeTruthy();
const second = await request.get(avatarPath({ img_format: "png" }), {
headers: { "if-none-match": etag as string },
});
const second = await getThroughEdge(
request,
avatarPath({ img_format: "png" }),
{
headers: { "if-none-match": etag as string },
},
);
expect(second.status()).toBe(304);
expect(second.headers()["etag"]).toBe(etag);
});
@@ -78,7 +98,10 @@ test("answers conditional requests with 304", async ({ request }) => {
test("serves the rendered avatar behind permissive CORS", async ({
request,
}) => {
const response = await request.get(avatarPath({ img_format: "png" }));
const response = await getThroughEdge(
request,
avatarPath({ img_format: "png" }),
);
expect(response.status()).toBe(200);
expect(response.headers()["access-control-allow-origin"]).toBe("*");
});
@@ -86,7 +109,9 @@ test("serves the rendered avatar behind permissive CORS", async ({
test("preflight requests advertise GET and revalidation", async ({
request,
}) => {
const response = await request.fetch(avatarPath(), { method: "OPTIONS" });
const response = await fetchThroughEdge(request, avatarPath(), {
method: "OPTIONS",
});
expect(response.status()).toBe(204);
expect(response.headers()["access-control-allow-origin"]).toBe("*");
expect(response.headers()["access-control-allow-methods"]).toContain("GET");
@@ -113,7 +138,10 @@ test.describe("rejects malformed figures", () => {
for (const { name, query } of cases) {
test(name, async ({ request }) => {
const response = await request.get(`/api/imaging/avatar${query}`);
const response = await getThroughEdge(
request,
`/api/imaging/avatar${query}`,
);
expect(response.status()).toBe(400);
});
}
@@ -131,7 +159,7 @@ test("collapses a burst of concurrent requests for one figure", async ({
head_direction: "1",
});
const responses = await Promise.all(
Array.from({ length: 6 }, () => request.get(path)),
Array.from({ length: 6 }, () => getThroughEdge(request, path)),
);
for (const response of responses) {
expect(response.status()).toBe(200);
@@ -143,14 +171,14 @@ test("collapses a burst of concurrent requests for one figure", async ({
test("the badge endpoint serves an image and revalidates", async ({
request,
}) => {
const first = await request.get("/api/imaging/badge?code=ADM");
const first = await getThroughEdge(request, "/api/imaging/badge?code=ADM");
expect(first.status()).toBe(200);
expect(first.headers()["content-type"]).toContain("image/");
expect(first.headers()["access-control-allow-origin"]).toBe("*");
const etag = first.headers()["etag"];
expect(etag).toBeTruthy();
const second = await request.get("/api/imaging/badge?code=ADM", {
const second = await getThroughEdge(request, "/api/imaging/badge?code=ADM", {
headers: { "if-none-match": etag as string },
});
expect(second.status()).toBe(304);
@@ -159,7 +187,8 @@ test("the badge endpoint serves an image and revalidates", async ({
test("the badge endpoint can resolve from a local source", async ({
request,
}) => {
const response = await request.get(
const response = await getThroughEdge(
request,
"/api/imaging/badge?code=ADM&source=local",
);
expect(response.status()).toBe(200);
@@ -177,16 +206,23 @@ test.describe("rejects malformed badge codes", () => {
for (const { name, query } of cases) {
test(name, async ({ request }) => {
const response = await request.get(`/api/imaging/badge${query}`);
const response = await getThroughEdge(
request,
`/api/imaging/badge${query}`,
);
expect(response.status()).toBe(400);
});
}
});
test("the badge preflight advertises GET", async ({ request }) => {
const response = await request.fetch("/api/imaging/badge?code=ADM", {
method: "OPTIONS",
});
const response = await fetchThroughEdge(
request,
"/api/imaging/badge?code=ADM",
{
method: "OPTIONS",
},
);
expect(response.status()).toBe(204);
expect(response.headers()["access-control-allow-methods"]).toContain("GET");
});