commit fab8c2a1bb45e9578cbd139108b31deaf533afb9
parent 1b558050f453634f3f1fdcd3abb8db2ee23c4766
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Mon, 28 Sep 2026 02:10:41 -0400
e2e: the homepage refusal test, the Build homepage test and the hub's refusal test hold BOTH the build and deploy queues — a regressed "preview needs deploy" guard, or a click turned into build-and-deploy, can then only queue
O4 review should-fix 1 (+ low 4 for the hub's test, which predates the
slice and held nothing). holdBuildAndDeployQueues in ops-api.spec, `before`
taken after both holds; sites-homepage's Build test holds both too.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Diffstat:
2 files changed, 48 insertions(+), 22 deletions(-)
diff --git a/editor/e2e/ops-api.spec.ts b/editor/e2e/ops-api.spec.ts
@@ -864,6 +864,24 @@ async function listJobIds(): Promise<string[]> {
.sort();
}
+// Hold the `build` AND `deploy` queues with fabricated running jobs
+// (/api/test/stuck-job; never released by the caller), so any build or deploy
+// job that a refusal under test failed to stop can only QUEUE, never run. BOTH
+// queues, because a regression can turn a refused call into a job on either
+// one: a dropped "preview needs deploy" guard makes a build, a dropped preview
+// check makes a deploy. The fabricated holders write no .meta.json, so they are
+// invisible to listJobIds; take `before` after this. The next resetData
+// cancels jobs newest first, so a queued job is removed before a holder's slot
+// is freed and nothing is ever promoted.
+async function holdBuildAndDeployQueues(request: APIRequestContext) {
+ for (const queue of ["build", "deploy"]) {
+ const hold = await request.get(
+ `${baseUrl}/api/test/stuck-job?queue=${encodeURIComponent(queue)}`,
+ );
+ expect(hold.ok(), queue).toBe(true);
+ }
+}
+
test("build-site and build-deploy each take siteId or siteIds, and refuse both or neither", async ({
request,
}) => {
@@ -1092,12 +1110,15 @@ test("build-site with a bare siteId starts one build-export job", async ({
// The hub's deploy path (release 7). Every refusal here is answered BEFORE a
// job exists, which is what lets a runbook's `pnpm ops deploy-hub --wait` fail
-// fast instead of queueing a deploy that can only fail.
+// fast instead of queueing a deploy that can only fail. Both queues are held
+// (release 11, O4 review): a regression of a refusal below would otherwise
+// start a real hub build into export/out, or a real deploy.
test("deploy-hub refuses no project, the homepage's project, and a bundle that is not the hub", async ({
request,
}) => {
await resetData("title-filter-channel");
await settings();
+ await holdBuildAndDeployQueues(request);
const before = await listJobIds();
// The fixture has no homepage.json at all: no project.
@@ -1158,20 +1179,19 @@ test("deploy-hub refuses no project, the homepage's project, and a bundle that i
// build), so the missing-build call is made only when this checkout has no
// `homepage/out/index.html`, as deploy-hub's is (211d4666);
// 2. that call asks for a PREVIEW, never production;
-// 3. the whole test runs with the `deploy` queue held by a fabricated job
-// (/api/test/stuck-job, never released here). A deploy job that a refusal
-// failed to stop would only QUEUE — the listJobIds assertion then fails —
-// and the next resetData cancels jobs newest first, so the queued deploy
-// is removed before the holder's slot is freed.
+// 3. the whole test runs with BOTH the `build` and the `deploy` queue held
+// by fabricated jobs (holdBuildAndDeployQueues). A build or deploy job
+// that a refusal failed to stop — `build-homepage {preview}` is a build
+// if the "preview needs deploy" guard regresses — would only QUEUE; the
+// listJobIds assertion then fails, and the next resetData cancels jobs
+// newest first, so the queued job is removed before a holder's slot is
+// freed.
test("deploy-homepage refuses a bad preview name and a missing build; build-homepage refuses a preview without a deploy", async ({
request,
}) => {
await resetData("empty");
await settings();
- const hold = await request.get(
- `${baseUrl}/api/test/stuck-job?queue=${encodeURIComponent("deploy")}`,
- );
- expect(hold.ok()).toBe(true);
+ await holdBuildAndDeployQueues(request);
const before = await listJobIds();
// A bad preview name is judged before homepage/out is even looked at, so
diff --git a/editor/e2e/sites-homepage.spec.ts b/editor/e2e/sites-homepage.spec.ts
@@ -7,13 +7,16 @@
// from a spec would be a real one. So:
// - no spec clicks Deploy homepage, and none ticks Deploy after build and
// then clicks Build homepage;
-// - the one spec that clicks Build homepage first holds the `build` queue
-// with a fabricated job (/api/test/stuck-job, never released here), so the
-// build-homepage job it starts only ever QUEUES, and it is cancelled from
-// its own lane while still queued. Its start function never runs: no log
-// file, no child, nothing written to homepage/public or homepage/out. If
-// the spec fails before its Cancel, the next resetData cancels jobs newest
-// first, so the queued build is removed before the holder's slot is freed.
+// - the one spec that clicks Build homepage first holds BOTH the `build` and
+// the `deploy` queue with fabricated jobs (/api/test/stuck-job, never
+// released here), so the job it starts only ever QUEUES — the
+// build-homepage it expects on `build`, and equally a build-deploy-homepage
+// on `deploy` if a regression turned the click into a build-and-deploy —
+// and it is cancelled from its own lane while still queued. Its start
+// function never runs: no log file, no child, nothing written to
+// homepage/public or homepage/out. If the spec fails before its Cancel, the
+// next resetData cancels jobs newest first, so the queued job is removed
+// before a holder's slot is freed.
import { readdir } from "node:fs/promises";
import { test, expect, type Page } from "@playwright/test";
@@ -165,11 +168,14 @@ test("Build homepage starts a build-homepage job on the build queue (held there,
page,
request,
}) => {
- // See the header: with the build queue held, the job can only queue.
- const hold = await request.get(
- `${baseUrl}/api/test/stuck-job?queue=${encodeURIComponent("build")}`,
- );
- expect(hold.ok()).toBe(true);
+ // See the header: with both queues held, whatever the click starts can only
+ // queue.
+ for (const queue of ["build", "deploy"]) {
+ const hold = await request.get(
+ `${baseUrl}/api/test/stuck-job?queue=${encodeURIComponent(queue)}`,
+ );
+ expect(hold.ok(), queue).toBe(true);
+ }
await openSites(page);
await expect(deployAfter(page)).not.toBeChecked();