Archilyzer · Source

archilyzer

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

commit e1f1b62c70435c18afe811e6489e991b95d306c0
parent 88cd288250ae49ee3a56c61eca4220dfaa31917f
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Thu,  2 Jul 2026 10:42:34 -0400

Phase 8: real two-origin e2e — cross-origin CORS + service-worker offline

Add a genuine two-origin Playwright project (playwright.2origin.config.ts,
e2e:2origin script) that proves what the route-mocked e2e-hub suite can't: the
browser's REAL CORS enforcement and the production service worker.

globalSetup stages two real static origins (config-load, before webServer since
Playwright starts servers before globalSetup):
- Origin B: a tiny hand-authored single-site archive — the JSON federation
  contract (site.json + summaries + transcripts + subs) plus a serve.json that
  sends Access-Control-Allow-Origin: *. No Next build (the hub only fetches its
  JSON). Served on :4610.
- Origin A: a real INSTANCE_MODE=hub production build (so the SW registers),
  hub-sites.json emptied so only Origin B is in play. Served on :4611.

Three passing:
1. the hub reads Origin B's /site.json over real CORS and shelves it;
2. a transcript-scope search returns a hit from Origin B (real cross-origin
   summaries + transcript-page fetches), origin-qualified;
3. after a search warms the cache, going offline and fetching Origin B's
   transcript shard is served from the hub SW cache — cross-origin, no network.

Deterministic on re-run (globalSetup skips the hub rebuild when staged;
TWO_ORIGIN_REBUILD=1 forces it). Staging dir .2origin/ is gitignored.

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

Diffstat:
M.gitignore | 2++
Aexport/e2e-2origin/federation.spec.ts | 105+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aexport/e2e-2origin/globalSetup.ts | 157+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mexport/package.json | 1+
Aexport/playwright.2origin.config.ts | 52++++++++++++++++++++++++++++++++++++++++++++++++++++
5 files changed, 317 insertions(+), 0 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -83,6 +83,8 @@ yarn-error.log* /export/playwright-report/ /export/test-results/ /export/test-results-hub/ +/export/test-results-2origin/ +/export/.2origin/ /export/blob-report/ # site config diff --git a/export/e2e-2origin/federation.spec.ts b/export/e2e-2origin/federation.spec.ts @@ -0,0 +1,105 @@ +import { expect, test, type Page } from "@playwright/test"; + +// Real two-origin federation. Origin B is a genuine static server sending +// Access-Control-Allow-Origin (see globalSetup); the hub (baseURL) reads it over +// the network, so these tests exercise the browser's REAL CORS enforcement and +// the production service worker — the things route mocks can't. + +const B_PORT = Number(process.env.ORIGIN_B_PORT ?? 4610); +const ORIGIN_B = `http://localhost:${B_PORT}`; + +async function addArchive(page: Page, url: string) { + await page.getByLabel("Archive URL").fill(url); + await page.getByRole("button", { name: "Add", exact: true }).click(); +} + +test.describe("real two-origin federation (CORS + service worker)", () => { + test("reads a cross-origin archive over real CORS and shelves it", async ({ + page, + }) => { + await page.goto("/"); + await expect( + page.getByRole("heading", { name: "Your archives" }), + ).toBeVisible(); + await addArchive(page, ORIGIN_B); + // The spine's title came from Origin B's /site.json, fetched cross-origin — + // it only renders if the real CORS read succeeded. + await expect( + page.getByTestId("shelf-spine").filter({ hasText: "Origin B Archive" }), + ).toBeVisible(); + }); + + test("searches Origin B's transcripts across origins", async ({ page }) => { + await page.goto("/"); + await addArchive(page, ORIGIN_B); + await expect( + page.getByTestId("shelf-spine").filter({ hasText: "Origin B Archive" }), + ).toBeVisible(); + + await page.getByTestId("query-builder").waitFor(); + await page + .locator('[data-testid^="leaf-scope-"]') + .first() + .selectOption({ label: "Transcripts" }); + const input = page.locator('[data-testid^="leaf-query-"]').first(); + await input.click(); + await input.fill("hydrogen"); + await input.press("Enter"); + await page.waitForURL(/[?&]qt=/); + + // A hit for the video whose transcript cue contains "hydrogen" — this only + // works if the hub fetched Origin B's summaries AND transcript pages over + // CORS. Its slug is origin-qualified. + const result = page.locator("[data-result-slug]").first(); + await expect(result).toBeVisible({ timeout: 15_000 }); + await expect(result).toHaveAttribute( + "data-result-slug", + new RegExp(`localhost:${B_PORT}`), + ); + }); + + test("service worker serves a cross-origin transcript shard offline", async ({ + page, + context, + }) => { + await page.goto("/"); + // The prod build registers the SW; wait until it actually controls the page + // (activate → clients.claim), so subsequent fetches route through it. + await page.waitForFunction( + () => !!navigator.serviceWorker?.controller, + undefined, + { timeout: 15_000 }, + ); + + await addArchive(page, ORIGIN_B); + await expect( + page.getByTestId("shelf-spine").filter({ hasText: "Origin B Archive" }), + ).toBeVisible(); + + // Run a transcript search to make the hub fetch Origin B's transcript page + // cross-origin — the SW caches shard responses on first fetch. + await page.getByTestId("query-builder").waitFor(); + await page + .locator('[data-testid^="leaf-scope-"]') + .first() + .selectOption({ label: "Transcripts" }); + const input = page.locator('[data-testid^="leaf-query-"]').first(); + await input.click(); + await input.fill("hydrogen"); + await input.press("Enter"); + await expect(page.locator("[data-result-slug]").first()).toBeVisible({ + timeout: 15_000, + }); + + // Go offline and fetch the cross-origin shard directly: the SW must serve it + // from cache (readable, CORS headers preserved) with no network. + await context.setOffline(true); + const shardUrl = `${ORIGIN_B}/transcripts/chan-b/page-0000.json`; + const body = await page.evaluate(async (url) => { + const res = await fetch(url); + return { ok: res.ok, text: await res.text() }; + }, shardUrl); + expect(body.ok).toBe(true); + expect(body.text).toContain("hydrogen"); + }); +}); diff --git a/export/e2e-2origin/globalSetup.ts b/export/e2e-2origin/globalSetup.ts @@ -0,0 +1,157 @@ +import { execSync } from "node:child_process"; +import { cpSync, mkdirSync, rmSync, writeFileSync } from "node:fs"; +import path from "node:path"; + +// Global setup for the REAL two-origin test. Unlike the route-mocked e2e-hub +// suite, this stands up two genuine static origins so the browser's real +// same-origin / CORS enforcement and the prod service worker are exercised: +// +// Origin B (.2origin/originB) — a tiny hand-authored single-site archive: the +// federation JSON contract (site.json + summaries + transcripts + subs) +// plus a serve.json that sends Access-Control-Allow-Origin: * so a +// cross-origin hub can read it. No Next build needed — the hub only ever +// fetches Origin B's JSON, never its HTML. +// Origin A (.2origin/hubA) — a real INSTANCE_MODE=hub production build (so its +// service worker registers), with hub-sites.json emptied so only Origin B +// is in play. +// +// Both are served by `serve` in the config's webServer array. + +const STAGE = path.resolve(process.cwd(), ".2origin"); +const ORIGIN_B = path.join(STAGE, "originB"); +const HUB_A = path.join(STAGE, "hubA"); +const GEN = "2026-01-01T00:00:00.000Z"; + +function writeJson(rel: string, value: unknown) { + const p = path.join(ORIGIN_B, rel); + mkdirSync(path.dirname(p), { recursive: true }); + writeFileSync(p, JSON.stringify(value)); +} + +function buildOriginB() { + rmSync(ORIGIN_B, { recursive: true, force: true }); + writeJson("site.json", { + contract: 1, + siteId: "originb", + siteTitle: "Origin B Archive", + siteDescription: "A real federated archive served over CORS.", + headerTitle: "Origin B", + homeTagline: "", + accent: "#c026d3", + siteUrl: "", + pwa: false, + socialLinks: [], + groups: [{ id: "gb", name: "Origin B", selectedByDefault: true }], + defaultGroupId: "gb", + channels: [{ slug: "chan-b", name: "Bravo Channel", count: 2, groupId: "gb" }], + generatedAt: GEN, + summariesVersion: 3, + }); + writeJson("summaries/manifest.json", { + version: 3, + totalCount: 2, + pageSize: 1000, + pageCount: 1, + generatedAt: GEN, + channels: [{ name: "Bravo Channel", count: 2, slug: "chan-b", groupId: "gb" }], + groups: [{ id: "gb", name: "Origin B", selectedByDefault: true }], + defaultGroupId: "gb", + siteId: "originb", + }); + const sum = (id: string, title: string) => ({ + slug: `chan-b/${id}`, + id, + channelSlug: "chan-b", + title, + uploadDate: "20260101", + date: "2026-01-01", + duration: "5:00", + channel: "Bravo Channel", + isLivestream: false, + ageRestricted: false, + isDeleted: false, + isUnlisted: false, + platform: "youtube", + webpageUrl: `https://example.com/${id}`, + }); + writeJson("summaries/page-0000.json", [ + sum("v1", "The Hydrogen Atom"), + sum("v2", "Helium and Beyond"), + ]); + writeJson("transcripts/chan-b/manifest.json", { + version: 1, + channelSlug: "chan-b", + pageCount: 1, + maxPageBytes: 8388608, + generatedAt: GEN, + slugToPage: { v1: 0, v2: 0 }, + }); + const det = (id: string, title: string, text: string) => ({ + slug: `chan-b/${id}`, + id, + channelSlug: "chan-b", + title, + uploadDate: "20260101", + duration: 300, + channel: "Bravo Channel", + description: "", + tags: [], + isLivestream: false, + ageRestricted: false, + platform: "youtube", + webpageUrl: `https://example.com/${id}`, + cues: [{ start: 0, end: 5, text }], + }); + writeJson("transcripts/chan-b/page-0000.json", [ + det("v1", "The Hydrogen Atom", "the hydrogen atom has one proton"), + det("v2", "Helium and Beyond", "helium is a noble gas"), + ]); + writeJson("subs/manifest.json", { + version: 4, + channels: [], + totalCount: 0, + liveChatTotalCount: 0, + generatedAt: GEN, + }); + // CORS on all JSON so a cross-origin hub can read this origin. + writeJson("serve.json", { + headers: [ + { + source: "**/*.json", + headers: [{ key: "Access-Control-Allow-Origin", value: "*" }], + }, + ], + }); +} + +function buildHub() { + // A real hub production build (SW registers only in prod). Skip the rebuild + // when a stage already exists unless TWO_ORIGIN_REBUILD=1 — the build is the + // slow part and the hub bundle rarely changes between local runs. + if (process.env.TWO_ORIGIN_REBUILD === "1") { + rmSync(HUB_A, { recursive: true, force: true }); + } + try { + // Cheap existence probe: if hubA/sw.js is missing, (re)build. + // eslint-disable-next-line @typescript-eslint/no-require-imports + require("node:fs").accessSync(path.join(HUB_A, "sw.js")); + return; + } catch { + /* needs build */ + } + execSync("pnpm run build:hub", { cwd: process.cwd(), stdio: "inherit" }); + rmSync(HUB_A, { recursive: true, force: true }); + cpSync(path.join(process.cwd(), "out"), HUB_A, { recursive: true }); + // Only Origin B should be in play — drop the built-in prod pool (unreachable + // in the test) so the shelf isn't cluttered with failing origins. + writeFileSync(path.join(HUB_A, "hub-sites.json"), "[]"); +} + +// Stage both origins. Invoked from playwright.2origin.config.ts at module load +// (NOT as Playwright globalSetup) because the served dirs must exist before the +// webServer array starts, and Playwright launches webServers before globalSetup. +export function stageTwoOrigins() { + mkdirSync(STAGE, { recursive: true }); + buildOriginB(); + buildHub(); +} diff --git a/export/package.json b/export/package.json @@ -20,6 +20,7 @@ "lint": "eslint", "e2e": "playwright test", "e2e:hub": "playwright test --config playwright.hub.config.ts", + "e2e:2origin": "playwright test --config playwright.2origin.config.ts", "e2e:ui": "playwright test --ui", "deploy": "pnpm dlx wrangler pages deploy out" }, diff --git a/export/playwright.2origin.config.ts b/export/playwright.2origin.config.ts @@ -0,0 +1,52 @@ +import path from "node:path"; +import { defineConfig, devices } from "@playwright/test"; +import { stageTwoOrigins } from "./e2e-2origin/globalSetup"; + +// Real two-origin test: proves what the route-mocked e2e-hub suite can't — +// genuine cross-origin CORS enforcement and the prod service worker offline +// path. We stage Origin B (CORS JSON) and Origin A (a real hub build) HERE at +// config load — before the webServer array starts — because Playwright launches +// webServers before globalSetup. The two webServer entries then serve the staged +// dirs, and the hub reads Origin B over the network like a deployed hub would. +const STAGE = path.resolve(process.cwd(), ".2origin"); +const B_PORT = Number(process.env.ORIGIN_B_PORT ?? 4610); +const A_PORT = Number(process.env.HUB_A_PORT ?? 4611); +const baseURL = `http://localhost:${A_PORT}`; + +stageTwoOrigins(); + +export default defineConfig({ + testDir: "./e2e-2origin", + timeout: 60_000, + retries: process.env.CI ? 2 : 0, + reporter: process.env.CI ? "github" : "list", + outputDir: "test-results-2origin/", + fullyParallel: false, + workers: 1, + webServer: [ + { + command: `pnpm exec serve ${path.join(STAGE, "originB")} -l ${B_PORT} --no-request-logging`, + url: `http://localhost:${B_PORT}/site.json`, + timeout: 60_000, + reuseExistingServer: !process.env.CI, + }, + { + command: `pnpm exec serve ${path.join(STAGE, "hubA")} -l ${A_PORT} --no-request-logging`, + url: `${baseURL}/`, + timeout: 60_000, + reuseExistingServer: !process.env.CI, + }, + ], + use: { + baseURL, + trace: "on-first-retry", + screenshot: "only-on-failure", + video: "retain-on-failure", + }, + projects: [ + { + name: "chromium", + use: { ...devices["Desktop Chrome"], viewport: { width: 1440, height: 1200 } }, + }, + ], +});