Archilyzer · Source

archilyzer

Archilyzer
git clone https://archilyzer.pages.dev/source/archilyzer.git
Log | Files | Refs | README | LICENSE

commit 750c5cd9512b4ac65209de21ecdb4274770b4eee
parent c8d18beff044d7e1a1a5e228696b00285ffa7371
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Mon, 28 Sep 2026 01:40:14 -0400

e2e: deploy-homepage's refusals over /api/ops, and /sites → Homepage (the section, the preview box, and Build homepage starting a build-homepage job) — none of it can build or deploy

ops-api.spec +1: a bad preview name (deploy-homepage and build-homepage with
deploy), a preview without deploy, and — only when this checkout has no
homepage/out/index.html, and as a preview — the missing build; the deploy
queue is held by a fabricated job for the whole test, so a deploy a refusal
failed to stop could only queue. sites-homepage.spec (3): the section after
Hub, unticked Deploy after build, the ships line and the index note; a bad
preview greys out every deploy with previewBranchProblem's sentence; Build
homepage with the build queue held → one build-homepage job, queued on
"build", cancelled from its lane before it ever started (no log file).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

Diffstat:
Meditor/e2e/ops-api.spec.ts | 66++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aeditor/e2e/sites-homepage.spec.ts | 192+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 258 insertions(+), 0 deletions(-)

diff --git a/editor/e2e/ops-api.spec.ts b/editor/e2e/ops-api.spec.ts @@ -1148,3 +1148,69 @@ test("deploy-hub refuses no project, the homepage's project, and a bundle that i expect(await listJobIds()).toEqual(before); }); + +// The homepage's deploy path (release 11, slice O4). Every refusal is answered +// BEFORE a job exists, like deploy-hub's. +// +// NOTHING HERE CAN REACH WRANGLER, and three separate things make sure of it: +// 1. `homepage/out` is the CHECKOUT's (homepageOutDir = <repo>/homepage/out, +// not fixture-controlled — the primary checkout holds the live homepage's +// 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. +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); + const before = await listJobIds(); + + // A bad preview name is judged before homepage/out is even looked at, so + // these hold in any checkout. + const production = await ops(request, "deploy-homepage", { preview: "main" }); + expect(production.status).toBe(400); + expect(production.body.error).toBe( + '"main" is the production branch; a preview needs another name.', + ); + const shape = await ops(request, "deploy-homepage", { preview: "Not_Valid" }); + expect(shape.status).toBe(400); + expect(shape.body.error).toMatch(/not a valid preview branch name/); + // And on the build-then-deploy, before anything is built. + const buildDeploy = await ops(request, "build-homepage", { + deploy: true, + preview: "main", + }); + expect(buildDeploy.status).toBe(400); + expect(buildDeploy.body.error).toContain("is the production branch"); + + // build-homepage: a preview without a deploy is a mistake, not a build. + const previewOnly = await ops(request, "build-homepage", { preview: "home-check" }); + expect(previewOnly.status).toBe(400); + expect(previewOnly.body.error).toContain('"preview" needs "deploy": true'); + + // No build in homepage/out. + if (await pathExists("../homepage/out/index.html")) { + test.info().annotations.push({ + type: "subcase-not-exercisable", + description: + "homepage/out holds a build in this checkout, so the unbuilt refusal cannot fire (and the call would deploy it)", + }); + } else { + const unbuilt = await ops(request, "deploy-homepage", { preview: "home-check" }); + expect(unbuilt.status).toBe(400); + expect(unbuilt.body.error).toBe( + "homepage/out holds no build — build the homepage first", + ); + } + + expect(await listJobIds()).toEqual(before); +}); diff --git a/editor/e2e/sites-homepage.spec.ts b/editor/e2e/sites-homepage.spec.ts @@ -0,0 +1,192 @@ +// /sites → Homepage: the `homepage` package's build and deploy (release 11, +// slice O4), the twin of the Hub section's buttons. +// +// NOTHING HERE DEPLOYS, AND NOTHING HERE BUILDS. Wrangler has no fake in +// e2e/fixtures/bin, and `homepage/out` is the CHECKOUT's own directory — in the +// primary checkout it holds the live homepage's build — so a deploy clicked +// 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. + +import { readdir } from "node:fs/promises"; +import { test, expect, type Page } from "@playwright/test"; +import { baseUrl } from "./baseUrl"; +import { pathExists, readJson, resetData, resolvePath } from "./helpers"; + +const group = (page: Page) => + page.getByRole("group", { name: "Homepage build" }); + +// EXACT: a live lane's Cancel button is named "Cancel Build homepage", which +// a substring match for "Build homepage" would also find. +const buildButton = (page: Page) => + group(page).getByRole("button", { name: "Build homepage", exact: true }); +const deployButton = (page: Page) => + group(page).getByRole("button", { name: "Deploy homepage", exact: true }); +// Scoped: /sites has three other "Deploy after build" checkboxes (the batch, +// the specific-sites panel, the hub). +const deployAfter = (page: Page) => group(page).getByLabel("Deploy after build"); +const previewInput = (page: Page) => + group(page).getByRole("textbox", { name: "preview branch", exact: true }); + +type Meta = { + id: string; + kind: string; + queueKey?: string; + status: string; + startedAt?: number; +}; + +async function metasOfKind(kind: string): Promise<Meta[]> { + const files = ( + await readdir(resolvePath("test-transcripts/.jobs")).catch(() => []) + ).filter((f) => f.endsWith(".meta.json")); + const metas: Meta[] = []; + for (const f of files) { + const meta = await readJson<Meta>(`test-transcripts/.jobs/${f}`).catch( + () => null, + ); + if (meta?.kind === kind) metas.push(meta); + } + return metas; +} + +test.beforeEach(async () => { + await resetData("empty"); +}); + +test("the Homepage section follows Hub, with the build and deploy controls and what a deploy ships", async ({ + page, +}) => { + await page.goto("/sites"); + + const hub = page.getByRole("heading", { name: "Hub", exact: true }); + const home = page.getByRole("heading", { name: "Homepage", exact: true }); + await expect(home).toBeVisible(); + expect((await hub.boundingBox())!.y).toBeLessThan((await home.boundingBox())!.y); + + await expect(buildButton(page)).toBeEnabled(); + await expect(deployButton(page)).toBeEnabled(); + // Unticked, as the hub's: a homepage deploy replaces a public site, so it is + // never the default of a build. + await expect(deployAfter(page)).not.toBeChecked(); + // Empty is production. + await expect(previewInput(page)).toHaveValue(""); + + // One line: what homepage/out holds, the project, production, the live URL. + const ships = group(page).getByTestId("homepage-ships"); + if (await pathExists("../homepage/out/index.html")) { + await expect(ships).toContainText(/Deploy homepage ships homepage\/out, built /); + } else { + await expect(ships).toContainText( + "homepage/out holds no build yet, so Deploy homepage has nothing to ship.", + ); + } + await expect(ships).toContainText("to archilyzer (production)"); + await expect( + ships.getByRole("link", { name: "https://archilyzer.pages.dev", exact: true }), + ).toHaveAttribute("href", "https://archilyzer.pages.dev"); + await expect(group(page)).toContainText( + "The homepage reads the search index as it stands: run Build index", + ); + + // The homepage's prose moved out of the Hub section and into its own. + const hubSection = page.locator("section").filter({ has: hub }); + const homeSection = page.locator("section").filter({ has: home }); + await expect(hubSection).not.toContainText("archilyzer deploy homepage"); + await expect(homeSection).toContainText("archilyzer deploy homepage"); + await expect(homeSection).toContainText("homepage/out"); +}); + +test("a bad preview name greys out every deploy and says why; a good one shows its alias", async ({ + page, +}) => { + await page.goto("/sites"); + const ships = group(page).getByTestId("homepage-ships"); + + await previewInput(page).fill("main"); + await expect(deployButton(page)).toBeDisabled(); + await expect( + group(page).getByRole("status", { name: "preview problem" }), + ).toHaveText('"main" is the production branch; a preview needs another name.'); + // A build alone deploys nothing, so it stays available… + await expect(buildButton(page)).toBeEnabled(); + // …until it would deploy. + await deployAfter(page).check(); + await expect(buildButton(page)).toBeDisabled(); + + await previewInput(page).fill("Main"); + await expect( + group(page).getByRole("status", { name: "preview problem" }), + ).toContainText("not a valid preview branch name"); + await expect(buildButton(page)).toBeDisabled(); + + // A good name re-arms both and names the alias before anything runs. + await previewInput(page).fill("home-check"); + await expect(buildButton(page)).toBeEnabled(); + await expect(deployButton(page)).toBeEnabled(); + await expect( + group(page).getByRole("status", { name: "preview problem" }), + ).toHaveCount(0); + await expect(ships).toContainText("to archilyzer (preview home-check)"); + await expect( + ships.getByRole("link", { + name: "https://home-check.archilyzer.pages.dev", + exact: true, + }), + ).toBeVisible(); + + // Cleared, it is production again. + await previewInput(page).fill(""); + await expect(ships).toContainText("to archilyzer (production)"); + + // Nothing above was clicked: no homepage job of any kind exists. + for (const kind of ["build-homepage", "deploy-homepage", "build-deploy-homepage"]) { + expect(await metasOfKind(kind), kind).toEqual([]); + } +}); + +test("Build homepage starts a build-homepage job on the build queue (held there, then cancelled from its lane)", async ({ + 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); + + await page.goto("/sites"); + await expect(deployAfter(page)).not.toBeChecked(); + await buildButton(page).click(); + + // The lane: queued behind the holder. + await expect(group(page).getByText(/^Queued/)).toBeVisible(); + + // One build-homepage job, on the build queue, queued — and nothing else. + await expect.poll(async () => (await metasOfKind("build-homepage")).length).toBe(1); + const [job] = await metasOfKind("build-homepage"); + expect(job.queueKey).toBe("build"); + expect(job.status).toBe("queued"); + expect(await metasOfKind("build-deploy-homepage")).toEqual([]); + expect(await metasOfKind("deploy-homepage")).toEqual([]); + + await group(page).getByRole("button", { name: "Cancel Build homepage" }).click(); + await expect(group(page).getByText("Cancelled", { exact: true })).toBeVisible(); + + // It never started: the registry says cancelled, and start() — which opens + // the job's log file before it runs anything — never ran. + const res = await request.get( + `${baseUrl}/api/jobs/${encodeURIComponent(job.id)}/log?from=0`, + ); + const log = (await res.json()) as { status: string; content: string }; + expect(log.status).toBe("cancelled"); + expect(log.content).toBe(""); + expect(await pathExists(`test-transcripts/.jobs/${job.id}.log`)).toBe(false); +});