The 11-admin spec walked links that terminate on FrankBook-Pro (192.168.0.112), which sleeps when idle — every deploy gate timed out there (measured 2026-10-05, two deploy windows). Split: the frankbook links move to tests/e2e/specs/19-frankbook.spec.ts under a frankbook project that run.sh excludes by default and deploy.sh triggers ONLY when a frankbook blast-radius path changed (the spec, frankbook-pro/, the homepage registry, the Caddyfile arr vhosts). deploy.sh computes the trigger from the same BASE..HEAD revision range the plan uses.
70 lines
2.8 KiB
TypeScript
70 lines
2.8 KiB
TypeScript
import { expect, test } from "@playwright/test";
|
|
|
|
// The *arr / qBittorrent admin links terminate on FrankBook-Pro
|
|
// (192.168.0.112), a Mac mini that SLEEPS when idle: Caddy answers 502 for
|
|
// the vhosts and the raw Sonarr link times out. This walk therefore lives in
|
|
// its own project, excluded from the default e2e run (tests/e2e/run.sh) and
|
|
// from the deploy gate's invocation (scripts/deploy.sh) unless a frankbook
|
|
// trigger path changed: this spec, frankbook-pro/**,
|
|
// shared/homepage/services.json, or the dankclaw Caddyfile that serves the
|
|
// arr vhosts. A sleeping Mac mini must never fail an unrelated deploy.
|
|
//
|
|
// Assertions mirror 11-admin's "admin open links reach their apps through
|
|
// SSO": the guard chain settles (Anubis PoW where present — Sonarr's raw-IP
|
|
// link is internal-only and has none), the hostname holds, no authentik form
|
|
// appears, and the app answers below 500.
|
|
|
|
const FRANKBOOK_IP = "192.168.0.112";
|
|
const CHALLENGE = "Making sure you're not a bot";
|
|
|
|
interface RegistryEntry {
|
|
name: string;
|
|
category: string;
|
|
showLink: boolean;
|
|
url: string;
|
|
probe: string;
|
|
}
|
|
|
|
test("frankbook admin open links reach their apps", async ({ page }) => {
|
|
test.setTimeout(240_000);
|
|
const registry: RegistryEntry[] = await (await page.request.get("/services.json")).json();
|
|
const frankbook = registry.filter(
|
|
(s) =>
|
|
s.category === "admin" &&
|
|
s.showLink &&
|
|
((s.probe ?? "").startsWith(FRANKBOOK_IP) || s.url.includes(FRANKBOOK_IP)),
|
|
);
|
|
const frankbookUrls = frankbook.map((s) => new URL(s.url).origin);
|
|
|
|
await page.goto("/admin/");
|
|
const hrefs = await page
|
|
.locator("#grid a.open")
|
|
.evaluateAll((els) => els.map((el) => (el as HTMLAnchorElement).href));
|
|
const frankbookHrefs = hrefs.filter((href) => frankbookUrls.includes(new URL(href).origin));
|
|
// The rendered frankbook link set must mirror the registry: a renamed arr
|
|
// vhost or a dropped probe fails here instead of silently dropping the
|
|
// walk's coverage.
|
|
expect(frankbookHrefs, "rendered frankbook links must match the registry").toHaveLength(
|
|
frankbook.length,
|
|
);
|
|
|
|
for (const href of frankbookHrefs) {
|
|
const target = new URL(href);
|
|
await page.goto(href);
|
|
// Guarded vhosts run their own Anubis proof-of-work on first visit; the
|
|
// raw-IP Sonarr link has none and settles immediately.
|
|
await expect
|
|
.poll(
|
|
() => page.locator("body").innerText().then((t) => !t.includes(CHALLENGE)),
|
|
{ timeout: 90_000 },
|
|
)
|
|
.toBe(true);
|
|
await expect
|
|
.poll(() => new URL(page.url()).hostname, { timeout: 60_000 })
|
|
.toBe(target.hostname);
|
|
expect(await page.locator('input[name="uidField"]').count()).toBe(0);
|
|
const resp = await page.request.get(page.url());
|
|
expect(resp.status(), `${target.hostname} must answer, not error`).toBeLessThan(500);
|
|
}
|
|
});
|