Archilyzer · Source

archilyzer

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

commit d0486c255376a29ba73e99c1b7e2b0ba2fd9f3fd
parent 4d1a0493c8f257a30254950948e208179803fe25
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Mon, 28 Sep 2026 04:03:11 -0400

e2e: every test-only env var carries the E2E_ prefix and is declared in its playwright config; the harnesses read ports.mjs

One-core Phase 4 item 3's last part (plan: $T/o6-b-plan.md).

Renamed (every reader and writer): EDITOR_TEST_ROUTES → E2E_TEST_ROUTES;
the five AUDIO_CHECK_*_OVERRIDE → E2E_AUDIO_CHECK_* (the suffix dropped)
and AUDIO_CHECK_DEBUG_PAUSE_MS → E2E_…; FAKE_YTDLP_* ×7 and
FAKE_GALLERY_DL_AUTH_FAIL → E2E_FAKE_*; FIXTURE_MAX_LIFETIME_MS,
OLLAMA_STUB_MODEL, RACK_SHOTS, TWO_ORIGIN_REBUILD, HOMEPAGE_SUMMARY_FILE
→ E2E_…; the sharded runner's SHARDS / IMAGE / SKIP_BUILD → E2E_…; umtool's
EDITOR_STUB_LOG, UMTOOL_CUT_DELAY_MS, UMTOOL_EXTRA_KINDS → E2E_….

Declared: editor/playwright.config.ts sets the test server's E2E_SERVER_ENV
(E2E_TEST_ROUTES and the audio-check knobs, moved out of dev:test and
start:test — a hand-started dev:test was already not reusable under the
queue's port check) and lists every other editor knob; the 2origin, homepage
and umtool configs declare theirs. envVars.ts moves the homepage summary file
to the test audience, adds E2E_RETRIES / E2E_SHARDS / E2E_IMAGE /
E2E_SKIP_BUILD and two runtime knobs the scan had missed
(AUDIO_CHECK_RESUME_DURING_PROBE, AUDIO_CHECK_BACKOFF_FACTOR, read through
helpers the scan now knows). Two new envVars tests: the test audience is
E2E_-prefixed (PLAYWRIGHT_BASE_URL and the queue's QUEUE_LOCK_HELD excepted),
and each is named in a playwright config (the queue's own in queue-lock.mjs).

Unchanged by design: the queue lock and port check (E2E_QUEUE,
E2E_PORT_CHECK, E2E_QUEUE_TIMEOUT, QUEUE_LOCK_HELD, the lock file), the port
names (ports.mjs, the worktree injector, queue-lock's --ports lists), the
docker ARCHILYZER_* set.

Ports: the editor, export, hub, 2origin, homepage and umtool configs, the
editor's baseUrl.ts, two export specs, the ollama stub, the umtool editor
stub and the 2origin spec read common/lib/ports.mjs (portFor / PORT_BASES)
instead of spelling a fallback.

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

Diffstat:
MENVIRONMENT.md | 48++++++++++++++++++++++++++----------------------
Mcommon/lib/envVars.test.ts | 40++++++++++++++++++++++++++++++++++++++--
Mcommon/lib/envVars.ts | 48++++++++++++++++++++++++++----------------------
Mcommon/views/names.ts | 2+-
Mcommon/ytdlp/audioCheckedDownload.ts | 15++++++++-------
Meditor/app/api/test/_guard.ts | 11++++++-----
Meditor/app/api/test/invalidate-cache/route.ts | 4++--
Meditor/app/api/test/resume-lane/route.ts | 2+-
Meditor/app/api/test/stuck-job/route.ts | 2+-
Meditor/app/api/test/worker-token/route.ts | 2+-
Meditor/app/api/view/[name]/route.ts | 2+-
Meditor/e2e/baseUrl.ts | 10++++++----
Meditor/e2e/channels-rack-audit.spec.ts | 6+++---
Meditor/e2e/export-player-platform-cache.spec.ts | 3++-
Meditor/e2e/export-search.spec.ts | 3++-
Meditor/e2e/fixtures/bin/_watchdog.mjs | 4++--
Meditor/e2e/fixtures/bin/fake-gallery-dl.mjs | 2+-
Meditor/e2e/fixtures/bin/fake-ytdlp.mjs | 18+++++++++---------
Meditor/e2e/fixtures/ollama-stub.mjs | 5+++--
Meditor/instrumentation.ts | 2+-
Meditor/package.json | 4++--
Meditor/playwright.config.ts | 49++++++++++++++++++++++++++++++++++++++++++++++---
Mexport/e2e-2origin/federation.spec.ts | 3++-
Mexport/e2e-2origin/globalSetup.ts | 4++--
Mexport/playwright.2origin.config.ts | 11+++++++++--
Mexport/playwright.config.ts | 7+++++--
Mexport/playwright.hub.config.ts | 3++-
Mhomepage/app/lib/summary.test.ts | 8++++----
Mhomepage/app/lib/summary.ts | 4++--
Mhomepage/e2e/fixture-summary.ts | 2+-
Mhomepage/playwright.config.ts | 18+++++++++++++-----
Mscripts/run-sharded-e2e.mjs | 6+++---
Mumtool/bin/cut-from-cache.mjs | 2+-
Mumtool/e2e/deliver.spec.ts | 2+-
Mumtool/e2e/fixtures/editor-stub.mjs | 5+++--
Mumtool/e2e/fixtures/make-fixture.mjs | 2+-
Mumtool/e2e/projects.spec.ts | 4++--
Mumtool/lib/projects/kinds.mjs | 4++--
Mumtool/playwright.config.ts | 20++++++++++++++++----
39 files changed, 257 insertions(+), 130 deletions(-)

diff --git a/ENVIRONMENT.md b/ENVIRONMENT.md @@ -69,6 +69,8 @@ Tokens, credentials and knobs a running process reads. Most configuration is not | `TRANSCRIPT_HUB_URL` | — | MCP server: a hub, federating every archive it lists. | mcp/src/sources.ts | | `TRANSCRIPT_LOCAL_DIR` | — | MCP server: a composed public dir on disk. | mcp/src/sources.ts | | `TRANSCRIPT_PLATFORM_LINKS` | off | `1` cites platform watch pages instead of the archive's own pages. | common/lib/archive/reader-fs.ts | +| `AUDIO_CHECK_RESUME_DURING_PROBE` | the channel's `audioCheck.resumeDuringProbe` | `1` or `true` resumes yt-dlp during the audio check's probe, anything else holds it, for a one-off comparison run; unset = the channel's setting. | common/ytdlp/audioCheckedDownload.ts | +| `AUDIO_CHECK_BACKOFF_FACTOR` | the built-in factor | The audio check's interval backoff factor, in (0, 1], for a one-off run. | common/ytdlp/audioCheckedDownload.ts | | `MCP_IO_STATS` | off | `1` turns on per-call I/O accounting, for `mcp/bench`. | common/lib/archive/io-stats.ts | | `ARCHILYZER_EDITOR_URL` | `http://localhost:3001` | Which editor `pnpm ops` and the MCP's `fetch_clip` talk to. | scripts/archilyzer-ops.mjs, mcp/src/fetchClip.ts, umtool | | `ARCHILYZER_AGENT` | `cli` | Who is asking, recorded as the provenance of a curated-tag write through `pnpm ops`. | scripts/archilyzer-ops.mjs | @@ -122,7 +124,6 @@ The publish pipeline sets these for a process it spawns. Listed so a reader know | `BUILD_ARCHIVES` | on | `0` skips archive-zip generation for one build (`--skip-archives`). | common/bin/compose-site.ts, common/bin/build-archives.ts | | `ARCHIVES_READONLY` | off | `1` inside a docker-mode build container: materialize archives, never write the shared cache. | common/bin/compose-site.ts | | `HOMEPAGE_PUBLIC_DIR` | `<repo>/homepage/public` | Where `compose homepage` writes. | common/bin/compose-homepage.ts | -| `HOMEPAGE_SUMMARY_FILE` | `homepage/public/homepage-summary.json` | A dev-only summary file for the homepage; ignored by a production build. The homepage e2e sets it. | homepage/app/lib/summary.ts | ## Docker @@ -150,7 +151,7 @@ Read only by a test harness, a fake binary or a test-mode branch. Never set one | Variable | Default | What it does | Read by | |---|---|---|---| -| `EDITOR_TEST_ROUTES` | off | `1` opens the editor's `/api/test/*` routes. The e2e server sets it. | editor/app/api/test/_guard.ts, editor/instrumentation.ts | +| `E2E_TEST_ROUTES` | off | `1` opens the editor's `/api/test/*` routes and marks a test server at boot. Set by `editor/playwright.config.ts` on its test server, and by nothing else. | editor/app/api/test/_guard.ts, editor/instrumentation.ts | | `E2E_MODE` | dev | `start` runs the editor suite against `next start` instead of `next dev`. | editor/playwright.config.ts | | `E2E_QUEUE` | on | `0` skips the machine-global e2e queue (the port check still runs). | scripts/queue-lock.mjs | | `E2E_PORT_CHECK` | on | `0` skips the pre-run check that the suite's ports are free. | scripts/queue-lock.mjs | @@ -159,23 +160,26 @@ Read only by a test harness, a fake binary or a test-mode branch. Never set one | `E2E_QUEUE_LOCK_FILE` | one per machine | The queue's lock file; the queue's own tests point it elsewhere. | scripts/queue-lock.mjs | | `QUEUE_LOCK_HELD` | — | Set by the queue for the command it runs, so a nested wrapper passes through. | scripts/queue-lock.mjs | | `PLAYWRIGHT_BASE_URL` | `http://localhost:<PORT>` | The editor test server's URL; the worktree injector sets it. | editor/playwright.config.ts, editor/e2e/baseUrl.ts | -| `AUDIO_CHECK_INTERVAL_MS_OVERRIDE` | the real cadence | Shrinks the mid-download audio check so the e2e suite sees it fire. | common/ytdlp/audioCheckedDownload.ts | -| `AUDIO_CHECK_SIZE_GATE_OVERRIDE` | the real gate | Likewise, the size gate. | common/ytdlp/audioCheckedDownload.ts | -| `AUDIO_CHECK_INTERVAL_FLOOR_MS_OVERRIDE` | the real floor | Likewise, the interval floor. | common/ytdlp/audioCheckedDownload.ts | -| `AUDIO_CHECK_RECOVER_STEP_MS_OVERRIDE` | the real step | Likewise, the recovery step. | common/ytdlp/audioCheckedDownload.ts | -| `AUDIO_CHECK_RECOVER_AFTER_OVERRIDE` | the real count | Likewise, the recovery count. | common/ytdlp/audioCheckedDownload.ts | -| `AUDIO_CHECK_DEBUG_PAUSE_MS` | off | A debugging pause inside the audio check. | common/ytdlp/audioCheckedDownload.ts | -| `FAKE_YTDLP_AUDIO_CHECK_MODE` | — | Fake yt-dlp: which audio-check scenario to act out. | editor/e2e/fixtures/bin/fake-ytdlp.mjs | -| `FAKE_YTDLP_CHUNK_DELAY_MS` | — | Fake yt-dlp: delay between written chunks. | editor/e2e/fixtures/bin/fake-ytdlp.mjs | -| `FAKE_YTDLP_CORRUPT_AFTER_CHUNK` | — | Fake yt-dlp: start corrupting after this chunk. | editor/e2e/fixtures/bin/fake-ytdlp.mjs | -| `FAKE_YTDLP_CORRUPT_RUNS` | — | Fake yt-dlp: how many runs corrupt. | editor/e2e/fixtures/bin/fake-ytdlp.mjs | -| `FAKE_YTDLP_DETERMINISTIC_CORRUPT` | — | Fake yt-dlp: corrupt deterministically. | editor/e2e/fixtures/bin/fake-ytdlp.mjs | -| `FAKE_YTDLP_RECOVER_ON_RESUME` | — | Fake yt-dlp: a resumed run recovers. | editor/e2e/fixtures/bin/fake-ytdlp.mjs | -| `FAKE_YTDLP_TOTAL_CHUNKS` | — | Fake yt-dlp: how many chunks a download has. | editor/e2e/fixtures/bin/fake-ytdlp.mjs | -| `FAKE_GALLERY_DL_AUTH_FAIL` | — | Fake gallery-dl: fail as an auth error. | editor/e2e/fixtures/bin/fake-gallery-dl.mjs | -| `FIXTURE_MAX_LIFETIME_MS` | the watchdog's | How long a fake binary may live before its watchdog kills it. | editor/e2e/fixtures/bin/_watchdog.mjs | -| `OLLAMA_STUB_MODEL` | `qwen2.5:7b` | The model the ollama stub claims to serve. | editor/e2e/fixtures/ollama-stub.mjs | -| `RACK_SHOTS` | off (spec skipped) | Runs the `/channels` rack screenshot audit. | editor/e2e/channels-rack-audit.spec.ts | -| `TWO_ORIGIN_REBUILD` | off | `1` rebuilds the two-origin suite's cached hub bundle. | export/e2e-2origin/globalSetup.ts | -| `IMAGE` | `yt-dlp-transcript-browser-e2e` | The sharded e2e run's image tag. | scripts/run-sharded-e2e.mjs | -| `SKIP_BUILD` | off | `1` reuses the sharded e2e image instead of rebuilding it (`--no-build`). | scripts/run-sharded-e2e.mjs | +| `E2E_AUDIO_CHECK_INTERVAL_MS` | the real cadence | Shrinks the mid-download audio check so the e2e suite sees it fire. | common/ytdlp/audioCheckedDownload.ts | +| `E2E_AUDIO_CHECK_SIZE_GATE` | the real gate | Likewise, the size gate. | common/ytdlp/audioCheckedDownload.ts | +| `E2E_AUDIO_CHECK_INTERVAL_FLOOR_MS` | the real floor | Likewise, the interval floor. | common/ytdlp/audioCheckedDownload.ts | +| `E2E_AUDIO_CHECK_RECOVER_STEP_MS` | the real step | Likewise, the recovery step. | common/ytdlp/audioCheckedDownload.ts | +| `E2E_AUDIO_CHECK_RECOVER_AFTER` | the real count | Likewise, the recovery count. | common/ytdlp/audioCheckedDownload.ts | +| `E2E_AUDIO_CHECK_DEBUG_PAUSE_MS` | off | A debugging pause inside the audio check. | common/ytdlp/audioCheckedDownload.ts | +| `E2E_FAKE_YTDLP_AUDIO_CHECK_MODE` | — | Fake yt-dlp: which audio-check scenario to act out. | editor/e2e/fixtures/bin/fake-ytdlp.mjs | +| `E2E_FAKE_YTDLP_CHUNK_DELAY_MS` | — | Fake yt-dlp: delay between written chunks. | editor/e2e/fixtures/bin/fake-ytdlp.mjs | +| `E2E_FAKE_YTDLP_CORRUPT_AFTER_CHUNK` | — | Fake yt-dlp: start corrupting after this chunk. | editor/e2e/fixtures/bin/fake-ytdlp.mjs | +| `E2E_FAKE_YTDLP_CORRUPT_RUNS` | — | Fake yt-dlp: how many runs corrupt. | editor/e2e/fixtures/bin/fake-ytdlp.mjs | +| `E2E_FAKE_YTDLP_DETERMINISTIC_CORRUPT` | — | Fake yt-dlp: corrupt deterministically. | editor/e2e/fixtures/bin/fake-ytdlp.mjs | +| `E2E_FAKE_YTDLP_RECOVER_ON_RESUME` | — | Fake yt-dlp: a resumed run recovers. | editor/e2e/fixtures/bin/fake-ytdlp.mjs | +| `E2E_FAKE_YTDLP_TOTAL_CHUNKS` | — | Fake yt-dlp: how many chunks a download has. | editor/e2e/fixtures/bin/fake-ytdlp.mjs | +| `E2E_FAKE_GALLERY_DL_AUTH_FAIL` | — | Fake gallery-dl: fail as an auth error. | editor/e2e/fixtures/bin/fake-gallery-dl.mjs | +| `E2E_FIXTURE_MAX_LIFETIME_MS` | the watchdog's | How long a fake binary may live before its watchdog kills it. | editor/e2e/fixtures/bin/_watchdog.mjs | +| `E2E_OLLAMA_STUB_MODEL` | `qwen2.5:7b` | The model the ollama stub claims to serve. | editor/e2e/fixtures/ollama-stub.mjs | +| `E2E_RACK_SHOTS` | off (spec skipped) | Runs the `/channels` rack screenshot audit. | editor/e2e/channels-rack-audit.spec.ts | +| `E2E_TWO_ORIGIN_REBUILD` | off | `1` rebuilds the two-origin suite's cached hub bundle. | export/e2e-2origin/globalSetup.ts | +| `E2E_SHARDS` | min(max(2, cpus/2), 8) | How many containers `pnpm e2e:sharded` splits the editor suite across (`--shards N` wins). | scripts/run-sharded-e2e.mjs | +| `E2E_RETRIES` | `0` | Retries per shard (`--retries N` wins); 0 keeps a sharded run comparable to a serial one. | scripts/run-sharded-e2e.mjs | +| `E2E_IMAGE` | `yt-dlp-transcript-browser-e2e` | The sharded e2e run's image tag. | scripts/run-sharded-e2e.mjs | +| `E2E_SKIP_BUILD` | off | `1` reuses the sharded e2e image instead of rebuilding it (`--no-build`). | scripts/run-sharded-e2e.mjs | +| `E2E_HOMEPAGE_SUMMARY_FILE` | `homepage/public/homepage-summary.json` | The synthetic summary the homepage's e2e dev server reads; set by `homepage/playwright.config.ts`, ignored by a production build. | homepage/app/lib/summary.ts | diff --git a/common/lib/envVars.test.ts b/common/lib/envVars.test.ts @@ -51,8 +51,10 @@ const READ_PATTERNS = [ // `env.X` on an env object handed in (a spawn's env, a testable `env = // process.env` parameter). /\b[eE]nv\??\.([A-Z][A-Z0-9_]{2,})\b/g, - // audioCheckedDownload.ts reads its test overrides through a helper. - /envIntOverride\(["']([A-Z][A-Z0-9_]+)["']\)/g, + // audioCheckedDownload.ts reads its overrides through helpers. + /env(?:Int|Float|Bool)Override\(\s*["']([A-Z][A-Z0-9_]+)["']\s*\)/g, + // run-sharded-e2e.mjs reads a flag's env twin by name. + /parseIntArg\(\s*["'][^"']+["'],\s*["']([A-Z][A-Z0-9_]+)["']/g, ]; function reads(): Map<string, Set<string>> { @@ -144,3 +146,37 @@ test("the docker audience is the ARCHILYZER_ set", () => { assert.match(v.name, /^ARCHILYZER_/); } }); + +// THE TEST AUDIENCE IS E2E_-PREFIXED AND DECLARED IN A PLAYWRIGHT CONFIG +// (one-core Phase 4 slice 3). The two exceptions keep names others depend on: +// Playwright's own convention, and the machine-global queue's nesting marker +// (its protocol is shared with checkouts on older code). +const UNPREFIXED_TEST_VARS = new Set(["PLAYWRIGHT_BASE_URL", "QUEUE_LOCK_HELD"]); + +test("every test-only variable carries the E2E_ prefix", () => { + const bad = ENV_VARS.filter( + (v) => v.audience === "test" && !v.name.startsWith("E2E_") && !UNPREFIXED_TEST_VARS.has(v.name), + ).map((v) => v.name); + assert.deepEqual(bad, []); + // And nothing outside the test audience wears the prefix. + assert.deepEqual( + ENV_VARS.filter((v) => v.audience !== "test" && v.name.startsWith("E2E_")).map((v) => v.name), + [], + ); +}); + +test("every test-only variable is declared in a playwright config (the queue's own in queue-lock.mjs)", () => { + const configs = ["editor", "export", "homepage", "umtool"].flatMap((pkg) => + readdirSync(path.join(REPO, pkg)) + .filter((n) => /^playwright.*\.config\.ts$/.test(n)) + .map((n) => readFileSync(path.join(REPO, pkg, n), "utf8")), + ).join("\n"); + const queue = readFileSync(path.join(REPO, "scripts", "queue-lock.mjs"), "utf8"); + const missing = ENV_VARS.filter((v) => v.audience === "test") + .filter((v) => { + const home = v.readBy.includes("scripts/queue-lock.mjs") ? queue : configs; + return !new RegExp(`\\b${v.name}\\b`).test(home); + }) + .map((v) => v.name); + assert.deepEqual(missing, []); +}); diff --git a/common/lib/envVars.ts b/common/lib/envVars.ts @@ -105,6 +105,8 @@ const DECLARED: EnvVarDecl[] = [ { name: "TRANSCRIPT_HUB_URL", audience: "runtime", default: "—", readBy: "mcp/src/sources.ts", doc: "MCP server: a hub, federating every archive it lists." }, { name: "TRANSCRIPT_LOCAL_DIR", audience: "runtime", default: "—", readBy: "mcp/src/sources.ts", doc: "MCP server: a composed public dir on disk." }, { name: "TRANSCRIPT_PLATFORM_LINKS", audience: "runtime", default: "off", readBy: "common/lib/archive/reader-fs.ts", doc: "`1` cites platform watch pages instead of the archive's own pages." }, + { name: "AUDIO_CHECK_RESUME_DURING_PROBE", audience: "runtime", default: "the channel's `audioCheck.resumeDuringProbe`", readBy: "common/ytdlp/audioCheckedDownload.ts", doc: "`1` or `true` resumes yt-dlp during the audio check's probe, anything else holds it, for a one-off comparison run; unset = the channel's setting." }, + { name: "AUDIO_CHECK_BACKOFF_FACTOR", audience: "runtime", default: "the built-in factor", readBy: "common/ytdlp/audioCheckedDownload.ts", doc: "The audio check's interval backoff factor, in (0, 1], for a one-off run." }, { name: "MCP_IO_STATS", audience: "runtime", default: "off", readBy: "common/lib/archive/io-stats.ts", doc: "`1` turns on per-call I/O accounting, for `mcp/bench`." }, { name: "ARCHILYZER_EDITOR_URL", audience: "runtime", default: "`http://localhost:3001`", readBy: "scripts/archilyzer-ops.mjs, mcp/src/fetchClip.ts, umtool", doc: "Which editor `pnpm ops` and the MCP's `fetch_clip` talk to." }, { name: "ARCHILYZER_AGENT", audience: "runtime", default: "`cli`", readBy: "scripts/archilyzer-ops.mjs", doc: "Who is asking, recorded as the provenance of a curated-tag write through `pnpm ops`." }, @@ -131,7 +133,6 @@ const DECLARED: EnvVarDecl[] = [ { name: "BUILD_ARCHIVES", audience: "internal", default: "on", readBy: "common/bin/compose-site.ts, common/bin/build-archives.ts", doc: "`0` skips archive-zip generation for one build (`--skip-archives`)." }, { name: "ARCHIVES_READONLY", audience: "internal", default: "off", readBy: "common/bin/compose-site.ts", doc: "`1` inside a docker-mode build container: materialize archives, never write the shared cache." }, { name: "HOMEPAGE_PUBLIC_DIR", audience: "internal", default: "`<repo>/homepage/public`", readBy: "common/bin/compose-homepage.ts", doc: "Where `compose homepage` writes." }, - { name: "HOMEPAGE_SUMMARY_FILE", audience: "internal", default: "`homepage/public/homepage-summary.json`", readBy: "homepage/app/lib/summary.ts", doc: "A dev-only summary file for the homepage; ignored by a production build. The homepage e2e sets it." }, // ── docker: the container's set ──────────────────────────────────────── { name: "ARCHILYZER_TRANSCRIBER", audience: "docker", default: "baked per image target (`whisper-cpp` in `runtime`)", readBy: "docker/entrypoint.sh", doc: "`whisper-cpp` or `parakeet`: which worker the first boot seeds and which model it fetches." }, @@ -149,7 +150,7 @@ const DECLARED: EnvVarDecl[] = [ { name: "ARCHILYZER_TAG", audience: "docker", default: "`local`", readBy: "docker-compose*.yml", doc: "The image tag the compose files build and run." }, // ── test: harnesses, fakes and test-mode branches ────────────────────── - { name: "EDITOR_TEST_ROUTES", audience: "test", default: "off", readBy: "editor/app/api/test/_guard.ts, editor/instrumentation.ts", doc: "`1` opens the editor's `/api/test/*` routes. The e2e server sets it." }, + { name: "E2E_TEST_ROUTES", audience: "test", default: "off", readBy: "editor/app/api/test/_guard.ts, editor/instrumentation.ts", doc: "`1` opens the editor's `/api/test/*` routes and marks a test server at boot. Set by `editor/playwright.config.ts` on its test server, and by nothing else." }, { name: "E2E_MODE", audience: "test", default: "dev", readBy: "editor/playwright.config.ts", doc: "`start` runs the editor suite against `next start` instead of `next dev`." }, { name: "E2E_QUEUE", audience: "test", default: "on", readBy: "scripts/queue-lock.mjs", doc: "`0` skips the machine-global e2e queue (the port check still runs)." }, { name: "E2E_PORT_CHECK", audience: "test", default: "on", readBy: "scripts/queue-lock.mjs", doc: "`0` skips the pre-run check that the suite's ports are free." }, @@ -158,26 +159,29 @@ const DECLARED: EnvVarDecl[] = [ { name: "E2E_QUEUE_LOCK_FILE", audience: "test", default: "one per machine", readBy: "scripts/queue-lock.mjs", doc: "The queue's lock file; the queue's own tests point it elsewhere." }, { name: "QUEUE_LOCK_HELD", audience: "test", default: "—", readBy: "scripts/queue-lock.mjs", doc: "Set by the queue for the command it runs, so a nested wrapper passes through." }, { name: "PLAYWRIGHT_BASE_URL", audience: "test", default: "`http://localhost:<PORT>`", readBy: "editor/playwright.config.ts, editor/e2e/baseUrl.ts", doc: "The editor test server's URL; the worktree injector sets it." }, - { name: "AUDIO_CHECK_INTERVAL_MS_OVERRIDE", audience: "test", default: "the real cadence", readBy: "common/ytdlp/audioCheckedDownload.ts", doc: "Shrinks the mid-download audio check so the e2e suite sees it fire." }, - { name: "AUDIO_CHECK_SIZE_GATE_OVERRIDE", audience: "test", default: "the real gate", readBy: "common/ytdlp/audioCheckedDownload.ts", doc: "Likewise, the size gate." }, - { name: "AUDIO_CHECK_INTERVAL_FLOOR_MS_OVERRIDE", audience: "test", default: "the real floor", readBy: "common/ytdlp/audioCheckedDownload.ts", doc: "Likewise, the interval floor." }, - { name: "AUDIO_CHECK_RECOVER_STEP_MS_OVERRIDE", audience: "test", default: "the real step", readBy: "common/ytdlp/audioCheckedDownload.ts", doc: "Likewise, the recovery step." }, - { name: "AUDIO_CHECK_RECOVER_AFTER_OVERRIDE", audience: "test", default: "the real count", readBy: "common/ytdlp/audioCheckedDownload.ts", doc: "Likewise, the recovery count." }, - { name: "AUDIO_CHECK_DEBUG_PAUSE_MS", audience: "test", default: "off", readBy: "common/ytdlp/audioCheckedDownload.ts", doc: "A debugging pause inside the audio check." }, - { name: "FAKE_YTDLP_AUDIO_CHECK_MODE", audience: "test", default: "—", readBy: "editor/e2e/fixtures/bin/fake-ytdlp.mjs", doc: "Fake yt-dlp: which audio-check scenario to act out." }, - { name: "FAKE_YTDLP_CHUNK_DELAY_MS", audience: "test", default: "—", readBy: "editor/e2e/fixtures/bin/fake-ytdlp.mjs", doc: "Fake yt-dlp: delay between written chunks." }, - { name: "FAKE_YTDLP_CORRUPT_AFTER_CHUNK", audience: "test", default: "—", readBy: "editor/e2e/fixtures/bin/fake-ytdlp.mjs", doc: "Fake yt-dlp: start corrupting after this chunk." }, - { name: "FAKE_YTDLP_CORRUPT_RUNS", audience: "test", default: "—", readBy: "editor/e2e/fixtures/bin/fake-ytdlp.mjs", doc: "Fake yt-dlp: how many runs corrupt." }, - { name: "FAKE_YTDLP_DETERMINISTIC_CORRUPT", audience: "test", default: "—", readBy: "editor/e2e/fixtures/bin/fake-ytdlp.mjs", doc: "Fake yt-dlp: corrupt deterministically." }, - { name: "FAKE_YTDLP_RECOVER_ON_RESUME", audience: "test", default: "—", readBy: "editor/e2e/fixtures/bin/fake-ytdlp.mjs", doc: "Fake yt-dlp: a resumed run recovers." }, - { name: "FAKE_YTDLP_TOTAL_CHUNKS", audience: "test", default: "—", readBy: "editor/e2e/fixtures/bin/fake-ytdlp.mjs", doc: "Fake yt-dlp: how many chunks a download has." }, - { name: "FAKE_GALLERY_DL_AUTH_FAIL", audience: "test", default: "—", readBy: "editor/e2e/fixtures/bin/fake-gallery-dl.mjs", doc: "Fake gallery-dl: fail as an auth error." }, - { name: "FIXTURE_MAX_LIFETIME_MS", audience: "test", default: "the watchdog's", readBy: "editor/e2e/fixtures/bin/_watchdog.mjs", doc: "How long a fake binary may live before its watchdog kills it." }, - { name: "OLLAMA_STUB_MODEL", audience: "test", default: "`qwen2.5:7b`", readBy: "editor/e2e/fixtures/ollama-stub.mjs", doc: "The model the ollama stub claims to serve." }, - { name: "RACK_SHOTS", audience: "test", default: "off (spec skipped)", readBy: "editor/e2e/channels-rack-audit.spec.ts", doc: "Runs the `/channels` rack screenshot audit." }, - { name: "TWO_ORIGIN_REBUILD", audience: "test", default: "off", readBy: "export/e2e-2origin/globalSetup.ts", doc: "`1` rebuilds the two-origin suite's cached hub bundle." }, - { name: "IMAGE", audience: "test", default: "`yt-dlp-transcript-browser-e2e`", readBy: "scripts/run-sharded-e2e.mjs", doc: "The sharded e2e run's image tag." }, - { name: "SKIP_BUILD", audience: "test", default: "off", readBy: "scripts/run-sharded-e2e.mjs", doc: "`1` reuses the sharded e2e image instead of rebuilding it (`--no-build`)." }, + { name: "E2E_AUDIO_CHECK_INTERVAL_MS", audience: "test", default: "the real cadence", readBy: "common/ytdlp/audioCheckedDownload.ts", doc: "Shrinks the mid-download audio check so the e2e suite sees it fire." }, + { name: "E2E_AUDIO_CHECK_SIZE_GATE", audience: "test", default: "the real gate", readBy: "common/ytdlp/audioCheckedDownload.ts", doc: "Likewise, the size gate." }, + { name: "E2E_AUDIO_CHECK_INTERVAL_FLOOR_MS", audience: "test", default: "the real floor", readBy: "common/ytdlp/audioCheckedDownload.ts", doc: "Likewise, the interval floor." }, + { name: "E2E_AUDIO_CHECK_RECOVER_STEP_MS", audience: "test", default: "the real step", readBy: "common/ytdlp/audioCheckedDownload.ts", doc: "Likewise, the recovery step." }, + { name: "E2E_AUDIO_CHECK_RECOVER_AFTER", audience: "test", default: "the real count", readBy: "common/ytdlp/audioCheckedDownload.ts", doc: "Likewise, the recovery count." }, + { name: "E2E_AUDIO_CHECK_DEBUG_PAUSE_MS", audience: "test", default: "off", readBy: "common/ytdlp/audioCheckedDownload.ts", doc: "A debugging pause inside the audio check." }, + { name: "E2E_FAKE_YTDLP_AUDIO_CHECK_MODE", audience: "test", default: "—", readBy: "editor/e2e/fixtures/bin/fake-ytdlp.mjs", doc: "Fake yt-dlp: which audio-check scenario to act out." }, + { name: "E2E_FAKE_YTDLP_CHUNK_DELAY_MS", audience: "test", default: "—", readBy: "editor/e2e/fixtures/bin/fake-ytdlp.mjs", doc: "Fake yt-dlp: delay between written chunks." }, + { name: "E2E_FAKE_YTDLP_CORRUPT_AFTER_CHUNK", audience: "test", default: "—", readBy: "editor/e2e/fixtures/bin/fake-ytdlp.mjs", doc: "Fake yt-dlp: start corrupting after this chunk." }, + { name: "E2E_FAKE_YTDLP_CORRUPT_RUNS", audience: "test", default: "—", readBy: "editor/e2e/fixtures/bin/fake-ytdlp.mjs", doc: "Fake yt-dlp: how many runs corrupt." }, + { name: "E2E_FAKE_YTDLP_DETERMINISTIC_CORRUPT", audience: "test", default: "—", readBy: "editor/e2e/fixtures/bin/fake-ytdlp.mjs", doc: "Fake yt-dlp: corrupt deterministically." }, + { name: "E2E_FAKE_YTDLP_RECOVER_ON_RESUME", audience: "test", default: "—", readBy: "editor/e2e/fixtures/bin/fake-ytdlp.mjs", doc: "Fake yt-dlp: a resumed run recovers." }, + { name: "E2E_FAKE_YTDLP_TOTAL_CHUNKS", audience: "test", default: "—", readBy: "editor/e2e/fixtures/bin/fake-ytdlp.mjs", doc: "Fake yt-dlp: how many chunks a download has." }, + { name: "E2E_FAKE_GALLERY_DL_AUTH_FAIL", audience: "test", default: "—", readBy: "editor/e2e/fixtures/bin/fake-gallery-dl.mjs", doc: "Fake gallery-dl: fail as an auth error." }, + { name: "E2E_FIXTURE_MAX_LIFETIME_MS", audience: "test", default: "the watchdog's", readBy: "editor/e2e/fixtures/bin/_watchdog.mjs", doc: "How long a fake binary may live before its watchdog kills it." }, + { name: "E2E_OLLAMA_STUB_MODEL", audience: "test", default: "`qwen2.5:7b`", readBy: "editor/e2e/fixtures/ollama-stub.mjs", doc: "The model the ollama stub claims to serve." }, + { name: "E2E_RACK_SHOTS", audience: "test", default: "off (spec skipped)", readBy: "editor/e2e/channels-rack-audit.spec.ts", doc: "Runs the `/channels` rack screenshot audit." }, + { name: "E2E_TWO_ORIGIN_REBUILD", audience: "test", default: "off", readBy: "export/e2e-2origin/globalSetup.ts", doc: "`1` rebuilds the two-origin suite's cached hub bundle." }, + { name: "E2E_SHARDS", audience: "test", default: "min(max(2, cpus/2), 8)", readBy: "scripts/run-sharded-e2e.mjs", doc: "How many containers `pnpm e2e:sharded` splits the editor suite across (`--shards N` wins)." }, + { name: "E2E_RETRIES", audience: "test", default: "`0`", readBy: "scripts/run-sharded-e2e.mjs", doc: "Retries per shard (`--retries N` wins); 0 keeps a sharded run comparable to a serial one." }, + { name: "E2E_IMAGE", audience: "test", default: "`yt-dlp-transcript-browser-e2e`", readBy: "scripts/run-sharded-e2e.mjs", doc: "The sharded e2e run's image tag." }, + { name: "E2E_SKIP_BUILD", audience: "test", default: "off", readBy: "scripts/run-sharded-e2e.mjs", doc: "`1` reuses the sharded e2e image instead of rebuilding it (`--no-build`)." }, + { name: "E2E_HOMEPAGE_SUMMARY_FILE", audience: "test", default: "`homepage/public/homepage-summary.json`", readBy: "homepage/app/lib/summary.ts", doc: "The synthetic summary the homepage's e2e dev server reads; set by `homepage/playwright.config.ts`, ignored by a production build." }, ]; // The port rows come from the port table, so a port is declared once. diff --git a/common/views/names.ts b/common/views/names.ts @@ -14,7 +14,7 @@ // takes a credential, and — stated because the temptation is obvious — NO // `/api/test/*` NAME MAY EVER JOIN THIS TUPLE. Those routes fabricate jobs, // restart lanes and set the worker token; they are gated on -// `EDITOR_TEST_ROUTES` precisely so they do not exist on a real editor, and +// `E2E_TEST_ROUTES` precisely so they do not exist on a real editor, and // putting one behind this dispatcher would hand it the one surface here that // has no guard at all. export const VIEW_NAMES = [ diff --git a/common/ytdlp/audioCheckedDownload.ts b/common/ytdlp/audioCheckedDownload.ts @@ -57,7 +57,8 @@ const DEFAULT_SIZE_GATE_BYTES = 1024 * 1024; // 1 MiB minimum growth between che const PART_DISCOVERY_INTERVAL_MS = 250; const PART_DISCOVERY_TIMEOUT_MS = 10_000; -// Test-only overrides. Tests need fast intervals and small file sizes to +// Test-only overrides, E2E_-prefixed and set by editor/playwright.config.ts on +// its test server. Tests need fast intervals and small file sizes to // exercise the loop without burning real seconds and megabytes. The // AUDIO_CHECK_INTERVAL_MIN_SECONDS clamp in the form parser protects against // users accidentally setting this in production; the env knobs bypass the @@ -186,16 +187,16 @@ type Knobs = { function resolveKnobs(opts: AudioCheckedOpts): Knobs { const cfg = opts.channelConfig.audioCheck; - const intervalMsOverride = envIntOverride("AUDIO_CHECK_INTERVAL_MS_OVERRIDE"); - const sizeGateOverride = envIntOverride("AUDIO_CHECK_SIZE_GATE_OVERRIDE"); - const debugPauseOverride = envIntOverride("AUDIO_CHECK_DEBUG_PAUSE_MS"); + const intervalMsOverride = envIntOverride("E2E_AUDIO_CHECK_INTERVAL_MS"); + const sizeGateOverride = envIntOverride("E2E_AUDIO_CHECK_SIZE_GATE"); + const debugPauseOverride = envIntOverride("E2E_AUDIO_CHECK_DEBUG_PAUSE_MS"); const resumeDuringProbeOverride = envBoolOverride( "AUDIO_CHECK_RESUME_DURING_PROBE", ); const backoffFactorOverride = envFloatOverride("AUDIO_CHECK_BACKOFF_FACTOR"); - const floorMsOverride = envIntOverride("AUDIO_CHECK_INTERVAL_FLOOR_MS_OVERRIDE"); - const recoverStepMsOverride = envIntOverride("AUDIO_CHECK_RECOVER_STEP_MS_OVERRIDE"); - const recoverAfterOverride = envIntOverride("AUDIO_CHECK_RECOVER_AFTER_OVERRIDE"); + const floorMsOverride = envIntOverride("E2E_AUDIO_CHECK_INTERVAL_FLOOR_MS"); + const recoverStepMsOverride = envIntOverride("E2E_AUDIO_CHECK_RECOVER_STEP_MS"); + const recoverAfterOverride = envIntOverride("E2E_AUDIO_CHECK_RECOVER_AFTER"); const intervalMs = intervalMsOverride ?? (cfg?.intervalSeconds ?? AUDIO_CHECK_INTERVAL_DEFAULT_SECONDS) * 1000; diff --git a/editor/app/api/test/_guard.ts b/editor/app/api/test/_guard.ts @@ -12,15 +12,16 @@ import { NextResponse } from "next/server"; // which is why binding to loopback is not an answer — the browser is inside the // loopback. The Caddyfile has no path rule for `/api/test` either. // -// So the surface is opt-in, the way the worker endpoint is: `EDITOR_TEST_ROUTES=1` -// is set by `dev:test` and `start:test` (editor/package.json) — the two scripts -// Playwright's webServer, the sharded runner and Dockerfile.test all boot -// through — and by nothing else. A real editor never sets it. +// So the surface is opt-in, the way the worker endpoint is: `E2E_TEST_ROUTES=1` +// is set by editor/playwright.config.ts's E2E_SERVER_ENV, on the webServer that +// runs `dev:test` / `start:test` — the one launch the host suite, the sharded +// runner and Dockerfile.test all go through — and by nothing else. A real +// editor never sets it, and neither does a hand-started `pnpm dev:test`. // // 404 AND NOT 403, deliberately: the answer must be indistinguishable from a // route that was never built. A 403 advertises that the harness exists and that // there is an env var worth guessing. export function testRouteDenied(): NextResponse | null { - if (process.env.EDITOR_TEST_ROUTES === "1") return null; + if (process.env.E2E_TEST_ROUTES === "1") return null; return NextResponse.json({ error: "Not Found" }, { status: 404 }); } diff --git a/editor/app/api/test/invalidate-cache/route.ts b/editor/app/api/test/invalidate-cache/route.ts @@ -41,8 +41,8 @@ function cancelLiveJobs() { } } -// E2E test harness only, and GUARDED BY `EDITOR_TEST_ROUTES` — which dev:test -// and start:test set, so it is still reachable from exactly the servers that +// E2E test harness only, and GUARDED BY `E2E_TEST_ROUTES` — which the +// playwright config sets for its test server, so it is reachable from exactly the servers that // need it. It cancels every live job and drops four singletons; an // unauthenticated GET doing that is CSRF-able from any page the operator has // open, loopback or not. See _guard.ts. diff --git a/editor/app/api/test/resume-lane/route.ts b/editor/app/api/test/resume-lane/route.ts @@ -20,7 +20,7 @@ export const dynamic = "force-dynamic"; // It replaced /api/test/resume-backfill-sweep, which did this for the sweep; // that route retired with the sweep in slice 1.3. // -// GUARDED BY `EDITOR_TEST_ROUTES`, like every other /api/test route. It was +// GUARDED BY `E2E_TEST_ROUTES`, like every other /api/test route. It was // mounted unconditionally on the reasoning that the editor is a localhost admin // tool — but the operator's browser is inside the loopback, so an // unauthenticated GET that stops and restarts a lane runner is CSRF-able. diff --git a/editor/app/api/test/stuck-job/route.ts b/editor/app/api/test/stuck-job/route.ts @@ -18,7 +18,7 @@ export const dynamic = "force-dynamic"; // NOT auto-healed, so it persists across polls and the test can prove // FORCE-RELEASE (not auto-heal) clears it. We reproduce it directly // (backdating startedAt) since a genuinely wedged child would be racy. -// GUARDED BY `EDITOR_TEST_ROUTES`, like every other /api/test route. It was +// GUARDED BY `E2E_TEST_ROUTES`, like every other /api/test route. It was // mounted unconditionally on the reasoning that the editor is a localhost admin // tool — but the operator's browser is inside the loopback, so an // unauthenticated GET that writes into the registry is CSRF-able. See _guard.ts. diff --git a/editor/app/api/test/worker-token/route.ts b/editor/app/api/test/worker-token/route.ts @@ -25,7 +25,7 @@ export const dynamic = "force-dynamic"; // later /api/ops and /api/worker spec answering 503 — a whole suite red from // one failure. `?set=` with no value is the restore, and it is idempotent. // -// GUARDED BY `EDITOR_TEST_ROUTES`, and it is the route that made the guard +// GUARDED BY `E2E_TEST_ROUTES`, and it is the route that made the guard // necessary: an unauthenticated GET that SETS a credential is a CSRF-able // token grant, and "the editor is a localhost admin tool" does not help — the // operator's browser is inside the loopback. See _guard.ts. diff --git a/editor/app/api/view/[name]/route.ts b/editor/app/api/view/[name]/route.ts @@ -16,7 +16,7 @@ export const dynamic = "force-dynamic"; // than a gap: all eight of these were unauthenticated polls, they are read-only, // and the editor is a loopback-only admin surface (docker/guard-exposure.sh // refuses to start it otherwise). Contrast `api/worker/health`, which takes the -// worker token, and `api/test/*`, which is gated on EDITOR_TEST_ROUTES because +// worker token, and `api/test/*`, which is gated on E2E_TEST_ROUTES because // it MUTATES. A `/api/test/*` name may never join `VIEW_NAMES`: this dispatcher // would serve it with no guard at all. // diff --git a/editor/e2e/baseUrl.ts b/editor/e2e/baseUrl.ts @@ -1,7 +1,9 @@ // The editor test server's base URL. Worktrees run on offset ports (see // scripts/worktree.mjs / WORKTREES.md), so node-side fetches in specs must read -// the assigned port from the environment rather than hardcoding 3011. The -// Playwright config sets PLAYWRIGHT_BASE_URL from its PORT; the fallback matches -// the default editor test port when running without the worktree wrapper. +// the assigned port from the environment rather than hardcoding one. The +// Playwright config sets PLAYWRIGHT_BASE_URL from its PORT; the fallback is the +// same PORT (common/lib/ports.mjs) when running without the worktree wrapper. +import { portFor } from "yt-dlp-transcript-common/lib/ports.mjs"; + export const baseUrl = - process.env.PLAYWRIGHT_BASE_URL ?? "http://localhost:3011"; + process.env.PLAYWRIGHT_BASE_URL ?? `http://localhost:${portFor("PORT")}`; diff --git a/editor/e2e/channels-rack-audit.spec.ts b/editor/e2e/channels-rack-audit.spec.ts @@ -9,15 +9,15 @@ import { // THE /channels RACK AUDIT — screenshots, not assertions. A person (and the // reviewer) LOOKS at every shot for layering, overlap, clipping and // misalignment; channels-rack-layers.spec.ts is where a finding becomes a -// hit-test that fails. Skipped unless RACK_SHOTS is set, so the ordinary suite +// hit-test that fails. Skipped unless E2E_RACK_SHOTS is set, so the ordinary suite // never spends time here: // -// RACK_SHOTS=1 pnpm e2e channels-rack-audit.spec.ts (from the repo root) +// E2E_RACK_SHOTS=1 pnpm e2e channels-rack-audit.spec.ts (from the repo root) // // Shots land in editor/test-results/rack-shots/ (gitignored; Playwright clears // test-results/ at the start of every run, so copy a set out before the next). -test.skip(!process.env.RACK_SHOTS, "audit only"); +test.skip(!process.env.E2E_RACK_SHOTS, "audit only"); const EXTRA = Array.from( { length: 16 }, diff --git a/editor/e2e/export-player-platform-cache.spec.ts b/editor/e2e/export-player-platform-cache.spec.ts @@ -1,4 +1,5 @@ import { test, expect, type Page } from "@playwright/test"; +import { portFor } from "yt-dlp-transcript-common/lib/ports.mjs"; // Regression: an Odysee video must not open in the YouTube player because of a // stale local transcript cache. Before multi-platform support, the export @@ -8,7 +9,7 @@ import { test, expect, type Page } from "@playwright/test"; // rejects platform-less / unknown-platform entries on read (transcriptStore // idbGet), so the authoritative page data (platform: "odysee") wins. -const EXPORT_BASE = `http://localhost:${process.env.EXPORT_PORT ?? 3010}`; +const EXPORT_BASE = `http://localhost:${portFor("EXPORT_PORT")}`; const CHANNEL = "Test Channel"; const CHANNEL_SLUG = "test-channel"; diff --git a/editor/e2e/export-search.spec.ts b/editor/e2e/export-search.spec.ts @@ -1,8 +1,9 @@ import { test, expect, type Page } from "@playwright/test"; +import { portFor } from "yt-dlp-transcript-common/lib/ports.mjs"; import { PROJECT_URL } from "../../common/lib/project"; import { readJson, writeSettings } from "./helpers"; -const EXPORT_BASE = `http://localhost:${process.env.EXPORT_PORT ?? 3010}`; +const EXPORT_BASE = `http://localhost:${portFor("EXPORT_PORT")}`; // NOTE: the search UI is the layered query builder (common/components/ // TranscriptSearch.tsx + QueryLeafView.tsx). A search commit writes the query diff --git a/editor/e2e/fixtures/bin/_watchdog.mjs b/editor/e2e/fixtures/bin/_watchdog.mjs @@ -18,7 +18,7 @@ // wedged. The longest legitimate fixture life is 30s // (fake-parakeet-stitch's completion backstop, and fake-ytdlp --test-slow, // which jobs-retry/jobs-reorder/queues specs rely on), so the default 120s -// leaves 4x margin. Override with FIXTURE_MAX_LIFETIME_MS if a future +// leaves 4x margin. Override with E2E_FIXTURE_MAX_LIFETIME_MS if a future // fixture legitimately needs longer. // // BOTH timers are unref'd. That is the load-bearing detail: a ref'd timer would @@ -45,7 +45,7 @@ export function installFixtureWatchdog() { poll.unref(); const maxLifetimeMs = - Number(process.env.FIXTURE_MAX_LIFETIME_MS) || DEFAULT_MAX_LIFETIME_MS; + Number(process.env.E2E_FIXTURE_MAX_LIFETIME_MS) || DEFAULT_MAX_LIFETIME_MS; const backstop = setTimeout(() => { process.stderr.write( `[watchdog] ${label}: exceeded ${maxLifetimeMs}ms lifetime — exiting\n`, diff --git a/editor/e2e/fixtures/bin/fake-gallery-dl.mjs b/editor/e2e/fixtures/bin/fake-gallery-dl.mjs @@ -33,7 +33,7 @@ if (rangeIdx >= 0 && args[rangeIdx + 1]) { // Mirror the real failure the Needs-cookies bucket models: when the fixture is // asked to simulate an expired session, exit non-zero with an auth-shaped // message on stderr. -if (process.env.FAKE_GALLERY_DL_AUTH_FAIL === "1") { +if (process.env.E2E_FAKE_GALLERY_DL_AUTH_FAIL === "1") { process.stderr.write( "[twitter][error] HTTP redirect to login page (401 Unauthorized)\n", ); diff --git a/editor/e2e/fixtures/bin/fake-ytdlp.mjs b/editor/e2e/fixtures/bin/fake-ytdlp.mjs @@ -28,7 +28,7 @@ const CORRUPT_MARKER = "__CORRUPT__"; // below, the pause test can prove the download is frozen while the probe runs. const SLOW_PROBE_MARKER = "__SLOWPROBE__"; // Tests use small chunks so the size gate (defaults to 1 MiB; tests override -// via AUDIO_CHECK_SIZE_GATE_OVERRIDE) can be exceeded each chunk. ~16 KiB. +// via E2E_AUDIO_CHECK_SIZE_GATE) can be exceeded each chunk. ~16 KiB. const CHUNK_BYTES = 16 * 1024; const argv = process.argv.slice(2); @@ -293,10 +293,10 @@ async function readAudioCheckSidecar() { async function modeAudioCheckOneUrl(url) { const sidecar = await readAudioCheckSidecar(); const mode = - sidecar.mode ?? process.env.FAKE_YTDLP_AUDIO_CHECK_MODE ?? "happy"; + sidecar.mode ?? process.env.E2E_FAKE_YTDLP_AUDIO_CHECK_MODE ?? "happy"; const recoverOnResume = sidecar.recoverOnResume === true || - process.env.FAKE_YTDLP_RECOVER_ON_RESUME === "1"; + process.env.E2E_FAKE_YTDLP_RECOVER_ON_RESUME === "1"; // Generalizes recoverOnResume (= corruptRuns:1): serve corruption on the // first N audio-check invocations, then behave. Lets a test drive several // successive mid-stream rollbacks (e.g. to observe the adaptive probe @@ -304,21 +304,21 @@ async function modeAudioCheckOneUrl(url) { const corruptRuns = sidecar.corruptRuns != null ? Number.parseInt(String(sidecar.corruptRuns), 10) - : process.env.FAKE_YTDLP_CORRUPT_RUNS != null - ? Number.parseInt(process.env.FAKE_YTDLP_CORRUPT_RUNS, 10) + : process.env.E2E_FAKE_YTDLP_CORRUPT_RUNS != null + ? Number.parseInt(process.env.E2E_FAKE_YTDLP_CORRUPT_RUNS, 10) : null; const deterministicCorrupt = sidecar.deterministicCorrupt === true || - process.env.FAKE_YTDLP_DETERMINISTIC_CORRUPT === "1"; + process.env.E2E_FAKE_YTDLP_DETERMINISTIC_CORRUPT === "1"; const totalChunks = Number.parseInt( String( - sidecar.totalChunks ?? process.env.FAKE_YTDLP_TOTAL_CHUNKS ?? "6", + sidecar.totalChunks ?? process.env.E2E_FAKE_YTDLP_TOTAL_CHUNKS ?? "6", ), 10, ); const chunkDelayMs = Number.parseInt( String( - sidecar.chunkDelayMs ?? process.env.FAKE_YTDLP_CHUNK_DELAY_MS ?? "200", + sidecar.chunkDelayMs ?? process.env.E2E_FAKE_YTDLP_CHUNK_DELAY_MS ?? "200", ), 10, ); @@ -385,7 +385,7 @@ async function modeAudioCheckOneUrl(url) { ? Number.parseInt( String( sidecar.corruptAfterChunk ?? - process.env.FAKE_YTDLP_CORRUPT_AFTER_CHUNK ?? + process.env.E2E_FAKE_YTDLP_CORRUPT_AFTER_CHUNK ?? "2", ), 10, diff --git a/editor/e2e/fixtures/ollama-stub.mjs b/editor/e2e/fixtures/ollama-stub.mjs @@ -25,9 +25,10 @@ // write no file and there would be no warnings[] on disk to assert against. import { createServer } from "node:http"; +import { portFor } from "yt-dlp-transcript-common/lib/ports.mjs"; -const PORT = Number(process.env.OLLAMA_STUB_PORT ?? 11435); -const MODEL = process.env.OLLAMA_STUB_MODEL ?? "qwen2.5:7b"; +const PORT = portFor("OLLAMA_STUB_PORT"); +const MODEL = process.env.E2E_OLLAMA_STUB_MODEL ?? "qwen2.5:7b"; const SENTINEL = /badout/i; function hms(total) { diff --git a/editor/instrumentation.ts b/editor/instrumentation.ts @@ -133,7 +133,7 @@ export async function register() { const { getRegistry } = await import( "yt-dlp-transcript-common/jobs/registry" ); - const testServer = process.env.EDITOR_TEST_ROUTES === "1"; + const testServer = process.env.E2E_TEST_ROUTES === "1"; const cancelOnly = idle || testServer; void settleAfterStoragePass(storagePass, { paths: getPaths(), diff --git a/editor/package.json b/editor/package.json @@ -5,8 +5,8 @@ "type": "module", "scripts": { "dev": "next dev --port ${EDITOR_PORT:-3001}", - "dev:test": "EDITOR_TEST_ROUTES=1 WORKER_TOKEN=test-worker-token TRANSCRIPTS_DIR=$(pwd)/test-transcripts EXPORT_PUBLIC_DIR=$(pwd)/test-transcripts/.export-public SETTINGS_FILE=$(pwd)/test-settings.json EDITOR_CHANGELOG_FILE=$(pwd)/test-changelog.md EXPORT_CHANGELOG_FILE=$(pwd)/test-export-changelog.md YTDLP_BIN=$(pwd)/e2e/fixtures/bin/fake-ytdlp.mjs GALLERY_DL_BIN=$(pwd)/e2e/fixtures/bin/fake-gallery-dl.mjs WHISPER_BIN=$(pwd)/e2e/fixtures/bin/fake-whisper.mjs WHISPER_MODEL=/dev/null CHOUGH_BIN=$(pwd)/e2e/fixtures/bin/fake-chough.mjs CHOUGH_MODEL=/dev/null PARAKEET_STITCH_BIN=$(pwd)/e2e/fixtures/bin/fake-parakeet-stitch.mjs PARAKEET_CLI=/dev/null PARAKEET_MODEL=/dev/null DIARIZE_BIN=$(pwd)/e2e/fixtures/bin/fake-diarize.mjs FFMPEG_BIN=$(pwd)/e2e/fixtures/bin/fake-ffmpeg.mjs FFPROBE_BIN=$(pwd)/e2e/fixtures/bin/fake-ffprobe.mjs OLLAMA_URL=http://127.0.0.1:${OLLAMA_STUB_PORT:-11435} CLAUDE_BIN=$(pwd)/e2e/fixtures/bin/fake-claude.mjs FINDMNT_BIN=$(pwd)/e2e/fixtures/bin/fake-findmnt.mjs UDISKSCTL_BIN=$(pwd)/e2e/fixtures/bin/fake-udisksctl.mjs AUDIO_CHECK_INTERVAL_MS_OVERRIDE=300 AUDIO_CHECK_SIZE_GATE_OVERRIDE=4096 AUDIO_CHECK_INTERVAL_FLOOR_MS_OVERRIDE=50 AUDIO_CHECK_RECOVER_STEP_MS_OVERRIDE=100 AUDIO_CHECK_RECOVER_AFTER_OVERRIDE=2 next dev --port ${PORT:-3011}", - "start:test": "EDITOR_TEST_ROUTES=1 WORKER_TOKEN=test-worker-token TRANSCRIPTS_DIR=$(pwd)/test-transcripts EXPORT_PUBLIC_DIR=$(pwd)/test-transcripts/.export-public SETTINGS_FILE=$(pwd)/test-settings.json EDITOR_CHANGELOG_FILE=$(pwd)/test-changelog.md EXPORT_CHANGELOG_FILE=$(pwd)/test-export-changelog.md YTDLP_BIN=$(pwd)/e2e/fixtures/bin/fake-ytdlp.mjs GALLERY_DL_BIN=$(pwd)/e2e/fixtures/bin/fake-gallery-dl.mjs WHISPER_BIN=$(pwd)/e2e/fixtures/bin/fake-whisper.mjs WHISPER_MODEL=/dev/null CHOUGH_BIN=$(pwd)/e2e/fixtures/bin/fake-chough.mjs CHOUGH_MODEL=/dev/null PARAKEET_STITCH_BIN=$(pwd)/e2e/fixtures/bin/fake-parakeet-stitch.mjs PARAKEET_CLI=/dev/null PARAKEET_MODEL=/dev/null DIARIZE_BIN=$(pwd)/e2e/fixtures/bin/fake-diarize.mjs FFMPEG_BIN=$(pwd)/e2e/fixtures/bin/fake-ffmpeg.mjs FFPROBE_BIN=$(pwd)/e2e/fixtures/bin/fake-ffprobe.mjs OLLAMA_URL=http://127.0.0.1:${OLLAMA_STUB_PORT:-11435} CLAUDE_BIN=$(pwd)/e2e/fixtures/bin/fake-claude.mjs FINDMNT_BIN=$(pwd)/e2e/fixtures/bin/fake-findmnt.mjs UDISKSCTL_BIN=$(pwd)/e2e/fixtures/bin/fake-udisksctl.mjs AUDIO_CHECK_INTERVAL_MS_OVERRIDE=300 AUDIO_CHECK_SIZE_GATE_OVERRIDE=4096 AUDIO_CHECK_INTERVAL_FLOOR_MS_OVERRIDE=50 AUDIO_CHECK_RECOVER_STEP_MS_OVERRIDE=100 AUDIO_CHECK_RECOVER_AFTER_OVERRIDE=2 next start --port ${PORT:-3011}", + "dev:test": "WORKER_TOKEN=test-worker-token TRANSCRIPTS_DIR=$(pwd)/test-transcripts EXPORT_PUBLIC_DIR=$(pwd)/test-transcripts/.export-public SETTINGS_FILE=$(pwd)/test-settings.json EDITOR_CHANGELOG_FILE=$(pwd)/test-changelog.md EXPORT_CHANGELOG_FILE=$(pwd)/test-export-changelog.md YTDLP_BIN=$(pwd)/e2e/fixtures/bin/fake-ytdlp.mjs GALLERY_DL_BIN=$(pwd)/e2e/fixtures/bin/fake-gallery-dl.mjs WHISPER_BIN=$(pwd)/e2e/fixtures/bin/fake-whisper.mjs WHISPER_MODEL=/dev/null CHOUGH_BIN=$(pwd)/e2e/fixtures/bin/fake-chough.mjs CHOUGH_MODEL=/dev/null PARAKEET_STITCH_BIN=$(pwd)/e2e/fixtures/bin/fake-parakeet-stitch.mjs PARAKEET_CLI=/dev/null PARAKEET_MODEL=/dev/null DIARIZE_BIN=$(pwd)/e2e/fixtures/bin/fake-diarize.mjs FFMPEG_BIN=$(pwd)/e2e/fixtures/bin/fake-ffmpeg.mjs FFPROBE_BIN=$(pwd)/e2e/fixtures/bin/fake-ffprobe.mjs OLLAMA_URL=http://127.0.0.1:${OLLAMA_STUB_PORT:-11435} CLAUDE_BIN=$(pwd)/e2e/fixtures/bin/fake-claude.mjs FINDMNT_BIN=$(pwd)/e2e/fixtures/bin/fake-findmnt.mjs UDISKSCTL_BIN=$(pwd)/e2e/fixtures/bin/fake-udisksctl.mjs next dev --port ${PORT:-3011}", + "start:test": "WORKER_TOKEN=test-worker-token TRANSCRIPTS_DIR=$(pwd)/test-transcripts EXPORT_PUBLIC_DIR=$(pwd)/test-transcripts/.export-public SETTINGS_FILE=$(pwd)/test-settings.json EDITOR_CHANGELOG_FILE=$(pwd)/test-changelog.md EXPORT_CHANGELOG_FILE=$(pwd)/test-export-changelog.md YTDLP_BIN=$(pwd)/e2e/fixtures/bin/fake-ytdlp.mjs GALLERY_DL_BIN=$(pwd)/e2e/fixtures/bin/fake-gallery-dl.mjs WHISPER_BIN=$(pwd)/e2e/fixtures/bin/fake-whisper.mjs WHISPER_MODEL=/dev/null CHOUGH_BIN=$(pwd)/e2e/fixtures/bin/fake-chough.mjs CHOUGH_MODEL=/dev/null PARAKEET_STITCH_BIN=$(pwd)/e2e/fixtures/bin/fake-parakeet-stitch.mjs PARAKEET_CLI=/dev/null PARAKEET_MODEL=/dev/null DIARIZE_BIN=$(pwd)/e2e/fixtures/bin/fake-diarize.mjs FFMPEG_BIN=$(pwd)/e2e/fixtures/bin/fake-ffmpeg.mjs FFPROBE_BIN=$(pwd)/e2e/fixtures/bin/fake-ffprobe.mjs OLLAMA_URL=http://127.0.0.1:${OLLAMA_STUB_PORT:-11435} CLAUDE_BIN=$(pwd)/e2e/fixtures/bin/fake-claude.mjs FINDMNT_BIN=$(pwd)/e2e/fixtures/bin/fake-findmnt.mjs UDISKSCTL_BIN=$(pwd)/e2e/fixtures/bin/fake-udisksctl.mjs next start --port ${PORT:-3011}", "build": "next build", "start": "next start --port ${EDITOR_PORT:-3001}", "lint": "eslint", diff --git a/editor/playwright.config.ts b/editor/playwright.config.ts @@ -1,7 +1,49 @@ import path from "node:path"; import { defineConfig, devices } from "@playwright/test"; +import { portFor } from "yt-dlp-transcript-common/lib/ports.mjs"; -const PORT = Number(process.env.PORT ?? 3011); +// THE TEST-ONLY ENVIRONMENT, declared here (one-core Phase 4 slice 3). Every +// variable only a harness reads carries the E2E_ prefix; the ones this config +// sets for the editor's test server are E2E_SERVER_ENV (passed as the +// webServer's env below), and the rest are knobs a spec or a person sets: +// +// E2E_TEST_ROUTES=1 opens /api/test/* (api/test/_guard.ts) and makes +// the boot settle a test server's leftover jobs +// (instrumentation.ts) — the test server's identity +// E2E_AUDIO_CHECK_INTERVAL_MS, E2E_AUDIO_CHECK_SIZE_GATE, +// E2E_AUDIO_CHECK_INTERVAL_FLOOR_MS, E2E_AUDIO_CHECK_RECOVER_STEP_MS, +// E2E_AUDIO_CHECK_RECOVER_AFTER +// shrink the mid-download audio check so a spec +// sees it fire (common/ytdlp/audioCheckedDownload.ts) +// E2E_AUDIO_CHECK_DEBUG_PAUSE_MS a debugging pause in the same check +// E2E_MODE=start run against `next start` instead of `next dev` +// E2E_RACK_SHOTS=1 run the /channels rack screenshot audit +// E2E_FAKE_YTDLP_AUDIO_CHECK_MODE, E2E_FAKE_YTDLP_CHUNK_DELAY_MS, +// E2E_FAKE_YTDLP_CORRUPT_AFTER_CHUNK, E2E_FAKE_YTDLP_CORRUPT_RUNS, +// E2E_FAKE_YTDLP_DETERMINISTIC_CORRUPT, E2E_FAKE_YTDLP_RECOVER_ON_RESUME, +// E2E_FAKE_YTDLP_TOTAL_CHUNKS +// fake-ytdlp.mjs's env fallbacks behind its +// .fake-ytdlp-audio-check.json sidecar +// E2E_FAKE_GALLERY_DL_AUTH_FAIL fake-gallery-dl.mjs fails as an auth error +// E2E_FIXTURE_MAX_LIFETIME_MS a fake binary's watchdog budget +// E2E_OLLAMA_STUB_MODEL the model the ollama stub claims to serve +// E2E_SHARDS, E2E_IMAGE, E2E_SKIP_BUILD, E2E_RETRIES +// the sharded runner (scripts/run-sharded-e2e.mjs) +// +// The ports are NOT prefixed: they are common/lib/ports.mjs's, injected per +// worktree by scripts/worktree.mjs and named in queue-lock's --ports list. Nor +// are the queue's own E2E_QUEUE / E2E_PORT_CHECK / E2E_QUEUE_TIMEOUT (read by +// scripts/queue-lock.mjs, shared machine-wide). ENVIRONMENT.md lists them all. +const E2E_SERVER_ENV = { + E2E_TEST_ROUTES: "1", + E2E_AUDIO_CHECK_INTERVAL_MS: "300", + E2E_AUDIO_CHECK_SIZE_GATE: "4096", + E2E_AUDIO_CHECK_INTERVAL_FLOOR_MS: "50", + E2E_AUDIO_CHECK_RECOVER_STEP_MS: "100", + E2E_AUDIO_CHECK_RECOVER_AFTER: "2", +}; + +const PORT = portFor("PORT"); const baseURL = `http://localhost:${PORT}`; // Expose the assigned base URL to node-side spec code (fetches to the editor's // test API). Keeps offset-port worktrees working even when run directly without @@ -14,10 +56,10 @@ const webServerCommand = // webServer entry instead of a fake binary in e2e/fixtures/bin/. Its port is // exported so the editor's dev:test / start:test scripts point OLLAMA_URL at the // same place, and so an offset-port worktree does not collide. -const OLLAMA_STUB_PORT = Number(process.env.OLLAMA_STUB_PORT ?? 11435); +const OLLAMA_STUB_PORT = portFor("OLLAMA_STUB_PORT"); process.env.OLLAMA_STUB_PORT = String(OLLAMA_STUB_PORT); -const EXPORT_PORT = Number(process.env.EXPORT_PORT ?? 3010); +const EXPORT_PORT = portFor("EXPORT_PORT"); const exportBaseURL = `http://localhost:${EXPORT_PORT}`; // Share the editor's test-settings.json with the export server so the // footer (and any other server-rendered settings consumers) can be @@ -78,6 +120,7 @@ export default defineConfig({ url: baseURL, timeout: 120_000, reuseExistingServer: !process.env.CI, + env: E2E_SERVER_ENV, }, { command: `pnpm --filter export run dev --port ${EXPORT_PORT}`, diff --git a/export/e2e-2origin/federation.spec.ts b/export/e2e-2origin/federation.spec.ts @@ -1,11 +1,12 @@ import { expect, test, type Page } from "@playwright/test"; +import { portFor } from "yt-dlp-transcript-common/lib/ports.mjs"; // 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 B_PORT = portFor("ORIGIN_B_PORT"); const ORIGIN_B = `http://localhost:${B_PORT}`; async function addArchive(page: Page, url: string) { diff --git a/export/e2e-2origin/globalSetup.ts b/export/e2e-2origin/globalSetup.ts @@ -126,9 +126,9 @@ function buildOriginB() { 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 + // when a stage already exists unless E2E_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") { + if (process.env.E2E_TWO_ORIGIN_REBUILD === "1") { rmSync(HUB_A, { recursive: true, force: true }); } try { diff --git a/export/playwright.2origin.config.ts b/export/playwright.2origin.config.ts @@ -1,7 +1,14 @@ import path from "node:path"; import { defineConfig, devices } from "@playwright/test"; +import { portFor } from "yt-dlp-transcript-common/lib/ports.mjs"; import { stageTwoOrigins } from "./e2e-2origin/globalSetup"; +// Test-only environment (declared here, one-core Phase 4 slice 3): +// E2E_TWO_ORIGIN_REBUILD=1 rebuild the cached hub bundle under .2origin +// (e2e-2origin/globalSetup.ts); otherwise a staged +// hubA/sw.js is reused. +// The two ports are common/lib/ports.mjs's, offset per worktree. + // 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 @@ -9,8 +16,8 @@ import { stageTwoOrigins } from "./e2e-2origin/globalSetup"; // 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 B_PORT = portFor("ORIGIN_B_PORT"); +const A_PORT = portFor("HUB_A_PORT"); const baseURL = `http://localhost:${A_PORT}`; stageTwoOrigins(); diff --git a/export/playwright.config.ts b/export/playwright.config.ts @@ -1,13 +1,16 @@ import fs from "node:fs"; import path from "node:path"; import { defineConfig, devices } from "@playwright/test"; +import { PORT_BASES } from "yt-dlp-transcript-common/lib/ports.mjs"; import { buildFixtureSettings } from "./e2e/fixtures/data"; // EXPORT_E2E_PORT, not PORT: under `wt run` (scripts/worktree.mjs) PORT is the // *editor's* test-server port (3011), so reading it here aimed this suite at the // editor's server. PORT is kept as a fallback for a bare `playwright test` with -// a hand-set port. -const PORT = Number(process.env.EXPORT_E2E_PORT ?? process.env.PORT ?? 3020); +// a hand-set port. The base is common/lib/ports.mjs's. +const PORT = Number( + process.env.EXPORT_E2E_PORT ?? process.env.PORT ?? PORT_BASES.EXPORT_E2E_PORT, +); const baseURL = `http://localhost:${PORT}`; const TEST_SETTINGS_FILE = path.resolve(process.cwd(), "test-settings.json"); diff --git a/export/playwright.hub.config.ts b/export/playwright.hub.config.ts @@ -1,6 +1,7 @@ import fs from "node:fs"; import path from "node:path"; import { defineConfig, devices } from "@playwright/test"; +import { portFor } from "yt-dlp-transcript-common/lib/ports.mjs"; import { buildFixtureSettings } from "./e2e/fixtures/data"; // Hub-mode e2e: the export app built/run with INSTANCE_MODE=hub renders the @@ -10,7 +11,7 @@ import { buildFixtureSettings } from "./e2e/fixtures/data"; // read only because it carries Access-Control-Allow-Origin) without standing up // a second real static server. Real _headers/serve.json CORS + the offline SW // are covered separately once the hub SW lands. -const PORT = Number(process.env.HUB_PORT ?? 3041); +const PORT = portFor("HUB_PORT"); const baseURL = `http://localhost:${PORT}`; const TEST_SETTINGS_FILE = path.resolve(process.cwd(), "test-settings.hub.json"); diff --git a/homepage/app/lib/summary.test.ts b/homepage/app/lib/summary.test.ts @@ -7,18 +7,18 @@ import { summaryFile } from "./summary"; const PUBLIC = "/repo/homepage/public/homepage-summary.json"; -test("a production build never reads HOMEPAGE_SUMMARY_FILE", () => { +test("a production build never reads E2E_HOMEPAGE_SUMMARY_FILE", () => { assert.equal( - summaryFile({ NODE_ENV: "production", HOMEPAGE_SUMMARY_FILE: "/tmp/fixture.json" }, PUBLIC), + summaryFile({ NODE_ENV: "production", E2E_HOMEPAGE_SUMMARY_FILE: "/tmp/fixture.json" }, PUBLIC), PUBLIC, ); }); test("outside production (the e2e's next dev) it names the file to read", () => { assert.equal( - summaryFile({ NODE_ENV: "development", HOMEPAGE_SUMMARY_FILE: "/x/.e2e-summary.json" }, PUBLIC), + summaryFile({ NODE_ENV: "development", E2E_HOMEPAGE_SUMMARY_FILE: "/x/.e2e-summary.json" }, PUBLIC), "/x/.e2e-summary.json", ); assert.equal(summaryFile({ NODE_ENV: "development" }, PUBLIC), PUBLIC); - assert.equal(summaryFile({ NODE_ENV: "development", HOMEPAGE_SUMMARY_FILE: "" }, PUBLIC), PUBLIC); + assert.equal(summaryFile({ NODE_ENV: "development", E2E_HOMEPAGE_SUMMARY_FILE: "" }, PUBLIC), PUBLIC); }); diff --git a/homepage/app/lib/summary.ts b/homepage/app/lib/summary.ts @@ -35,7 +35,7 @@ export function loadSummary(): HomepageSummary | null { } // The file loadSummary reads: `public/homepage-summary.json`, or the file -// HOMEPAGE_SUMMARY_FILE names — but ONLY outside a production build. The e2e's +// E2E_HOMEPAGE_SUMMARY_FILE names — but ONLY outside a production build. The e2e's // `next dev` points it at a synthetic summary (e2e/fixture-summary.ts, written // by playwright.config.ts); // `next build` runs with NODE_ENV=production and ignores it, so a variable left @@ -44,6 +44,6 @@ export function summaryFile( env: Readonly<Record<string, string | undefined>>, publicFile: string, ): string { - const override = env.NODE_ENV !== "production" ? env.HOMEPAGE_SUMMARY_FILE : undefined; + const override = env.NODE_ENV !== "production" ? env.E2E_HOMEPAGE_SUMMARY_FILE : undefined; return override || publicFile; } diff --git a/homepage/e2e/fixture-summary.ts b/homepage/e2e/fixture-summary.ts @@ -8,7 +8,7 @@ import type { VideoStat } from "../../common/lib/stats"; // The pages' numbers come from homepage-summary.json, which compose writes into // public/ from a real corpus — gitignored, so a fresh clone or worktree has // none, and the data specs used to skip there (release 10: 8 of 29). So the -// e2e dev server reads THIS instead (HOMEPAGE_SUMMARY_FILE, app/lib/summary.ts, +// e2e dev server reads THIS instead (E2E_HOMEPAGE_SUMMARY_FILE, app/lib/summary.ts, // honoured only outside a production build): the real builder // (buildHomepageSummary) over made-up recordings, deterministic, written by // playwright.config.ts to e2e/.e2e-summary.json (gitignored, not linked) before diff --git a/homepage/playwright.config.ts b/homepage/playwright.config.ts @@ -1,5 +1,6 @@ import path from "node:path"; import { defineConfig, devices } from "@playwright/test"; +import { portFor } from "yt-dlp-transcript-common/lib/ports.mjs"; import { FIXTURE_SUMMARY_NAME, writeFixtureSummary, @@ -9,10 +10,17 @@ import { // source. Kill any stale dev server on the port between runs. // // The port comes from HOMEPAGE_E2E_PORT, the block scripts/worktree.mjs already -// allocates per worktree (3040 on main). It used to read `PORT`, which is the -// EDITOR's port variable — so a worktree run pointed this suite at whatever the -// editor had been given, and the per-worktree offset never applied here. -const PORT = Number(process.env.HOMEPAGE_E2E_PORT ?? 3040); +// allocates per worktree (common/lib/ports.mjs; 3040 on main). It used to read +// `PORT`, which is the EDITOR's port variable — so a worktree run pointed this +// suite at whatever the editor had been given, and the per-worktree offset never +// applied here. +// +// Test-only environment, declared here (one-core Phase 4 slice 3): +// E2E_HOMEPAGE_SUMMARY_FILE set below on the dev server: the synthetic +// summary it reads instead of +// public/homepage-summary.json (app/lib/summary.ts, +// ignored by a production build) +const PORT = portFor("HOMEPAGE_E2E_PORT"); const baseURL = `http://localhost:${PORT}`; // The dev server reads a SYNTHETIC summary (e2e/fixture-summary.ts), never @@ -36,7 +44,7 @@ export default defineConfig({ url: baseURL, timeout: 120_000, reuseExistingServer: !process.env.CI, - env: { HOMEPAGE_SUMMARY_FILE: FIXTURE_SUMMARY }, + env: { E2E_HOMEPAGE_SUMMARY_FILE: FIXTURE_SUMMARY }, }, use: { baseURL, diff --git a/scripts/run-sharded-e2e.mjs b/scripts/run-sharded-e2e.mjs @@ -10,7 +10,7 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url)); const REPO_ROOT = path.resolve(__dirname, ".."); const EDITOR_DIR = path.join(REPO_ROOT, "editor"); const REPORT_DIR = path.join(EDITOR_DIR, "blob-report"); -const IMAGE = process.env.IMAGE ?? "yt-dlp-transcript-browser-e2e"; +const IMAGE = process.env.E2E_IMAGE ?? "yt-dlp-transcript-browser-e2e"; // Flags this script consumes itself; everything else on the command line is // forwarded verbatim to each shard's `playwright test` (see parseArgs). @@ -35,7 +35,7 @@ function parseIntArg(flag, envVar, fallback, min) { function parseShards() { return parseIntArg( "--shards", - "SHARDS", + "E2E_SHARDS", Math.min(Math.max(2, Math.floor(os.cpus().length / 2)), 8), 1, ); @@ -75,7 +75,7 @@ const SHARDS = parseShards(); const RETRIES = parseRetries(); const PASSTHROUGH = parsePassthrough(); const SKIP_BUILD = - process.argv.includes("--no-build") || process.env.SKIP_BUILD === "1"; + process.argv.includes("--no-build") || process.env.E2E_SKIP_BUILD === "1"; function run(cmd, args, opts = {}) { return new Promise((resolve, reject) => { diff --git a/umtool/bin/cut-from-cache.mjs b/umtool/bin/cut-from-cache.mjs @@ -45,7 +45,7 @@ const reencode = argv.includes("--reencode") // middle of -- and "Stop abandons the REST, and the next run resumes rather // than re-cutting" is the whole argument for one step per clip. The e2e // fixture's server sets this; nothing else ever should. -const delay = Number(process.env.UMTOOL_CUT_DELAY_MS ?? 0); +const delay = Number(process.env.E2E_UMTOOL_CUT_DELAY_MS ?? 0); if (delay > 0) await new Promise((r) => setTimeout(r, delay)); const res = await cutClipFromCache(r.project, clipId, { reencode }); diff --git a/umtool/e2e/deliver.spec.ts b/umtool/e2e/deliver.spec.ts @@ -237,7 +237,7 @@ test("rebuild runs build.py once per content variant present", async ({ page }) // mtimes of the files the first job wrote, which must not have moved. // // Deterministic rather than racy: the fixture's server sets -// UMTOOL_CUT_DELAY_MS=2000 (playwright.config.ts), the job is six clips long, +// E2E_UMTOOL_CUT_DELAY_MS=2000 (playwright.config.ts), the job is six clips long, // and the click waits for the first CUT-OK. Three clips of work -- six // seconds -- therefore cannot have happened by the time Stop lands. // --------------------------------------------------------------------------- diff --git a/umtool/e2e/fixtures/editor-stub.mjs b/umtool/e2e/fixtures/editor-stub.mjs @@ -19,12 +19,13 @@ import http from "node:http"; import { spawnSync } from "node:child_process"; import { mkdirSync, writeFileSync, statSync } from "node:fs"; import path from "node:path"; +import { portFor } from "yt-dlp-transcript-common/lib/ports.mjs"; -const PORT = Number(process.env.EDITOR_STUB_PORT ?? 3052); +const PORT = portFor("EDITOR_STUB_PORT"); const CHANNELS = process.env.CHANNELS_DIR ?? ""; const TOKEN = process.env.WORKER_TOKEN ?? ""; // Where the spec reads what was asked for. -const LOG = process.env.EDITOR_STUB_LOG ?? path.join(process.cwd(), "editor-stub.requests.json"); +const LOG = process.env.E2E_EDITOR_STUB_LOG ?? path.join(process.cwd(), "editor-stub.requests.json"); const requests = []; const jobs = new Map(); diff --git a/umtool/e2e/fixtures/make-fixture.mjs b/umtool/e2e/fixtures/make-fixture.mjs @@ -1224,7 +1224,7 @@ ff([ // "stopped after k of n" a measurement rather than a race. // // Six confirmed clips, none of them cut, all inside the same cached window. -// With UMTOOL_CUT_DELAY_MS set (playwright.config.ts) each step takes about +// With E2E_UMTOOL_CUT_DELAY_MS set (playwright.config.ts) each step takes about // two seconds, so the spec can see the first CUT-OK, press Stop, and know that // at least three clips could not possibly have been reached. const STOP = writeProject( diff --git a/umtool/e2e/projects.spec.ts b/umtool/e2e/projects.spec.ts @@ -280,7 +280,7 @@ test("the reserved names are exactly the static pages under app/browse", () => { }); test("a kind the registry has never seen appears everywhere, with no code edit", () => { - // The extensibility claim, tested rather than asserted. UMTOOL_EXTRA_KINDS is + // The extensibility claim, tested rather than asserted. E2E_UMTOOL_EXTRA_KINDS is // read only by kinds.mjs; if a new kind needs an edit anywhere else, this // fails. const extra = JSON.stringify([ @@ -295,7 +295,7 @@ test("a kind the registry has never seen appears everywhere, with no code edit", "meta:m.KIND_META().map(k=>k.badge)," + "detected:m.detectKind(new Set(['FIXTURE.marker']))})))", ], - { cwd: UMTOOL, encoding: "utf8", env: { ...process.env, UMTOOL_EXTRA_KINDS: extra } }, + { cwd: UMTOOL, encoding: "utf8", env: { ...process.env, E2E_UMTOOL_EXTRA_KINDS: extra } }, ); const j = JSON.parse(out.trim()); diff --git a/umtool/lib/projects/kinds.mjs b/umtool/lib/projects/kinds.mjs @@ -137,9 +137,9 @@ export const PROJECT_KINDS = [ // asserts the index, the chips, the CLI and the project page all pick it up // with no code edit anywhere. If adding a kind needs an edit outside this file, // that spec fails loudly. -if (process.env.UMTOOL_EXTRA_KINDS) { +if (process.env.E2E_UMTOOL_EXTRA_KINDS) { try { - for (const k of JSON.parse(process.env.UMTOOL_EXTRA_KINDS)) { + for (const k of JSON.parse(process.env.E2E_UMTOOL_EXTRA_KINDS)) { PROJECT_KINDS.push({ stages: [{ id: "draft", label: "draft" }], decisionKinds: [], diff --git a/umtool/playwright.config.ts b/umtool/playwright.config.ts @@ -1,14 +1,26 @@ import { defineConfig, devices } from "@playwright/test"; import path from "node:path"; import { fileURLToPath } from "node:url"; +import { portFor } from "yt-dlp-transcript-common/lib/ports.mjs"; -const PORT = Number(process.env.UMTOOL_E2E_PORT ?? 3051); +// Test-only environment, declared here (one-core Phase 4 slice 3); every one +// carries the E2E_ prefix: +// E2E_UMTOOL_CUT_DELAY_MS set below on the app server: two seconds of pacing +// per cut (bin/cut-from-cache.mjs), so the deliver +// spec can press Stop mid-job +// E2E_EDITOR_STUB_LOG set below on the editor stub: where it records what +// it was asked (e2e/fixtures/editor-stub.mjs) +// E2E_UMTOOL_EXTRA_KINDS set by projects.spec.ts on the CLI it runs: a kind +// injected into the registry (lib/projects/kinds.mjs) +// The ports are common/lib/ports.mjs's (offset per worktree by +// scripts/worktree.mjs), not test-only, and keep their names. +const PORT = portFor("UMTOOL_E2E_PORT"); // The editor stub's port. Named, because the queue lock's port PREFLIGHT only // checks the ports it is given: a bare PORT+1 was outside it, so a second // checkout's stub could already hold the port and this run would drive it. It // is in common/lib/ports.mjs (so scripts/worktree.mjs gives a worktree its own) // and in package.json's --ports spec (so the preflight sees it). -const STUB_PORT = Number(process.env.EDITOR_STUB_PORT ?? PORT + 1); +const STUB_PORT = portFor("EDITOR_STUB_PORT"); // The package is "type": "module", so there is no __dirname here. const FIXTURE = path.join(path.dirname(fileURLToPath(import.meta.url)), ".e2e-song"); @@ -49,7 +61,7 @@ export default defineConfig({ EDITOR_STUB_PORT: String(STUB_PORT), CHANNELS_DIR: `${FIXTURE}/channels`, WORKER_TOKEN: "umtool-e2e-token", - EDITOR_STUB_LOG: `${FIXTURE}/editor-stub.requests.json`, + E2E_EDITOR_STUB_LOG: `${FIXTURE}/editor-stub.requests.json`, }, }, { @@ -74,7 +86,7 @@ export default defineConfig({ // Two seconds of pacing per CUT, so the deliver spec can press Stop in // the middle of a job and prove that cancelling abandons the rest while // the next run resumes. Read only by bin/cut-from-cache.mjs. - `UMTOOL_CUT_DELAY_MS=2000 ` + + `E2E_UMTOOL_CUT_DELAY_MS=2000 ` + `NEXT_DIST_DIR=.next-e2e pnpm exec next dev --port ${PORT}`, port: PORT, reuseExistingServer: false,