Archilyzer · Source

archilyzer

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

commit 505340fd59f5fea3ccf5adfe0f033ad43296b0c5
parent b8c2811d82500d2cd7c78c13a0c41a8e3aa1cc6b
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Sat, 10 Oct 2026 02:00:53 -0400

plans: e2e speed S1–S3, as shipped (Track E)

The record under plans/e2e-speed.md "As it went", FACTS amended (start mode
is the default and its build is stamped), and the editor changelog's
[Unreleased] bullet.

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

Diffstat:
Meditor/CHANGELOG.md | 1+
Mplans/FACTS.md | 8++++++++
Mplans/e2e-speed.md | 89++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
3 files changed, 97 insertions(+), 1 deletion(-)

diff --git a/editor/CHANGELOG.md b/editor/CHANGELOG.md @@ -1,6 +1,7 @@ # Changelog ## [Unreleased] +- **`pnpm e2e` runs against a production build, and rebuilds it when the code changed.** The editor and umtool suites now run under `next start` by default (release 19's full editor suite: 24 min, against 71 under `next dev`). Before a run starts, the build's stamp — a fingerprint of the files the build reads, uncommitted edits included — is checked against the tree, and a stale build is rebuilt first through the heavy slot under a 5 GB cap, with the reason printed (`e2e build: rebuilding editor — common changed since the build …`). The test build has its own directory (`editor/.next/e2e`, `umtool/.next-e2e-start`), so it never replaces the build a running editor or umtool serves. `E2E_MODE=dev` runs `next dev` for iterating on one spec. The export and homepage suites stay on `next dev` (they are static exports). Every suite also writes `test-results/timings.json`, and `node scripts/e2e-timings.mjs` prints each spec file's time against the branch's last run. Four slow tests no longer wait on real clocks: the first rate-limit cooldown is 20 s and the clip-window gap 2 s on the test server only (`E2E_BACKOFF_BASE_MS`, `E2E_CLIP_WINDOW_GAP_MS`). - **An X fetch with a `limit` stops at that many posts.** "Fetch posts" with `limit` (`pnpm ops fetch-posts {"limit": 400}`) on a gallery-dl X channel read the whole history instead — a new channel walked 3,803 posts under the rate limit and held the platform queue for hours — because the cap counted media files, which a metadata-only read has almost none of. It now caps the posts themselves. - **A home seeder of last resort, behind a VPN.** `archilyzer seed` seeds the playable torrents of the sites named in the new `settings.seeder` (`sites`, `trackers`, `maxUploadKiBps`, `maxConnections`, `pollSeconds`, `standbyAfterSeconds`, `bindInterface`; SETTINGS.md) to desktop clients over TCP and to browsers over WebRTC — but each torrent only while no other seeder has it: other seeders seen on every poll for `standbyAfterSeconds` puts that torrent on standby (it stops announcing and closes its peers, keeping the data), and it comes back at once when a leecher is waiting with no other source, or after the same window with no other seeder. Every change is logged with its reason. No DHT, no local discovery, no UPnP. `archilyzer tracker` is a self-hosted HTTP + WebSocket tracker that tracks only those torrents. `docker-compose.seeder.yml` (profile `seeder`) runs both inside a WireGuard container's network namespace (gluetun, its firewall always on), so a tunnel that is down means no network, never the home connection; the WireGuard config is yours (`SEEDER_WG_CONF`, required, mounted read-only). `archilyzer doctor` compares the seeder's egress address with the host's and fails when they are the same; it says "seeder not configured" until `seeder.sites` names a site. - **Saved videos can be made browser-playable, with a torrent each.** `pnpm ops prepare-playable` (`POST /api/ops/prepare-playable`) and `archilyzer media playable <slug>` remux each of a channel's saved containers — without re-encoding (`-c copy`) — into an mp4 with its index in front, or a webm when it already is one (VP9/AV1 with Opus), drop subtitles, metadata and chapters, and make one single-file torrent of the copy: named `<id>.<ext>`, no web seed, no comment, no "created by", 256 KiB–1 MiB pieces. They go to `playable/<slug>/<id>/` beside the saved-video store, listed in `playable/<slug>/playable.json` with each infohash. `"trackers"` is the announce list written into each torrent (none by default); it is not part of the infohash, so the same torrent can be announced elsewhere later. A video already prepared from the same source (by sha256) is skipped, so a re-run is a no-op; a codec a browser cannot play without re-encoding (HEVC, MPEG-4 Part 2) is listed and left alone. diff --git a/plans/FACTS.md b/plans/FACTS.md @@ -999,6 +999,12 @@ seed again (`plans/tools/implementer-rules.md`); the export config re-copies `sw - `editor/playwright.config.ts` — editor on `PORT ?? 3011`, export on `EXPORT_PORT ?? 3010`. `E2E_MODE=start` → `pnpm start:test`, otherwise `pnpm dev:test` (`:10-11`). `fullyParallel: false`, `workers: 1`, `timeout: 30_000`. + **AMENDED 2026-10-10 (e2e speed S1):** `start` is the DEFAULT (`E2E_MODE=dev` for `next dev`). + Before the servers start, `scripts/e2e-stamp.mjs ensure editor` checks the build's stamp + (`editor/.next/e2e/e2e-stamp.json`) and rebuilds through the heavy slot when the tree moved; + the test server serves `.next/e2e` (`E2E_NEXT_DIST_DIR`), never `.next`. `CI` keeps `.next` + and skips the stamp (the sharded image builds its own). umtool likewise (`.next-e2e-start`); + export and homepage stay `next dev`. - `editor/e2e/helpers.ts` — `resetData` (`:32`), `writeSettings` (`:49`), `readJson` (`:90`). - Fake binaries live in `editor/e2e/fixtures/bin/`. The **`SLOWOP` sentinel** (checked case-insensitively against cwd or video id) makes an instant fake emit paced progress — @@ -1080,6 +1086,8 @@ not reset `timestampMode`/`promptVariant`. Run `pnpm e2e` in **default dev mode** — `E2E_MODE=start` serves a stale build. Kill stale dev servers by port between runs. +**AMENDED 2026-10-10 (e2e speed S1):** no longer true — start mode is the default and its build +is stamped; a stale build is rebuilt before the run (see the playwright.config entry above). ### The 22 reds: what they actually were diff --git a/plans/e2e-speed.md b/plans/e2e-speed.md @@ -96,4 +96,91 @@ apply throughout; no slice weakens the memory floor. ## As it went -(Each slice adds its record here.) +### S1–S3, as shipped (2026-10-10, Track E of the overnight batch, branch `r20/e2e-speed`) + +**S1 — start mode by default, the build stamped.** +- `scripts/e2e-stamp.mjs` (`ensure | check | build <editor|umtool>`). The stamp is a FINGERPRINT, not HEAD: the + index's blob id of every file the build reads, and git's blob id of the working-tree bytes of every dirty or + untracked one, over the package, `common/` and `pnpm-lock.yaml`, minus what no build reads (`<pkg>/e2e/`, the + package's `playwright.config.ts`, `*.test.*`, `CHANGELOG.md`). An edit and its commit fingerprint the same; a + plans-only commit costs no rebuild. A stale stamp names where the tree moved (`common, editor changed since + the build (4 uncommitted files there, e.g. common/controller/fetchWindows.ts)`). The build runs through + `queue-lock.mjs --heavy` and `systemd-run --user --scope -p MemoryMax=5G -p MemorySwapMax=0` (inside an e2e run + the slot is already held and passes through), and the stamp written is the tree the build STARTED from. +- **The build has its own directory** — ruled: the primary checkout's `.next` is what the live editor and umtool + serve. Editor: `.next/e2e` through `E2E_NEXT_DIST_DIR` (a one-line `distDir` in `editor/next.config.ts`; inside + the ignored `.next/`, so Tailwind never scans it). umtool: `.next-e2e-start` (its config already reads + `NEXT_DIST_DIR`; ignored by `umtool/.next-*/`). The stamp is `editor/.next/e2e/e2e-stamp.json`, not + `editor/.next/e2e-stamp.json` as briefed. A `next build` with the custom dist dir leaves `tsconfig.json` + alone and rewrites only the ignored `next-env.d.ts`. +- `editor/playwright.config.ts` and `umtool/playwright.config.ts` run `ensure` before any server starts + (`E2E_BUILD_CHECKED` keeps a worker's second load from checking again). `E2E_MODE=dev` builds nothing. + `CI` (the sharded image, which builds its own `.next`) keeps today's path. +- **The export (default, hub) and homepage suites take the switch and stay `next dev`**, printing so for + `E2E_MODE=start` — ruled: they are static exports, several export specs (header, transcript-downloads, brand, + site-branding, …) rewrite the fixture site mid-run and assert the next page, and the homepage's fixture + summary and publish are read only outside a production build. **The editor suite's export server stays `next + dev` too** — ruled: its two spec files took 42 s in all in release 19's start run, under one export build's + cost, and a static build would bake in the `test-settings.json` export-search.spec rewrites. +- **Flakes from release 19's start run:** `channel-work.spec:208` was a real race — `fs.cp` mkdirs each + directory after finding it absent, and a write still landing from the previous spec's work created + `test-transcripts/channels` in between. `resetData` now retries the clear-and-copy on `EEXIST` (four + attempts). `sites-crud.spec:392` (the first Save's status not seen in 5 s) and `whisper.spec:184` (the clear + job's output still "Waiting for output…" at 15 s) are recorded as load flakes: both pass alone, and nothing in + either names a race. +- **Stale-safety, by hand** (the S1 tree, before its commit): first run `e2e build: rebuilding editor — no + build stamp here yet` → `done in 54s, stamped f534007e1d7a`, `channel-work.spec` 11 passed. Then a comment + appended to `common/lib/project.ts`: `check editor` → `stale — common changed since the build …`; the runner → + `e2e build: rebuilding editor — common changed since the build …` (stopped before the suite). Killing that + build left NO stamp (`next build` empties its dist dir first), so the next run rebuilt rather than trusting a + half-written build: `done in 84s, stamped f534007e1d7a` — the same key for the same tree. + +**S2 — a timing record per run.** Every suite's config writes Playwright's `json` report beside `list` +(`test-results*/timings.json`; editor, umtool, export default/hub/report/2origin, homepage). +`scripts/e2e-timings.mjs [<timings.json>]` (default: the editor's) totals it per spec file (every result, +retries included), prints the files slowest first, each against the last run on the same branch that ran it, +and the wall time against the last run of the same spec files; it records the run in the git common dir +(`.git/e2e-timings/<suite>/<branch>.json`, the last 30), untracked and shared by every worktree. + +**S3 — the four timers.** Two were product timers and take a test-only duration from `E2E_SERVER_ENV`, +declared `test` in `common/lib/envVars.ts`: +- `E2E_BACKOFF_BASE_MS=20000` — the first rate-limit cooldown, read in `nextBackoff` + (`common/jobs/platformBackoff.ts`); the cap and the hold arithmetic (`FAILS_TO_REACH_CAP`) keep the real + constants. 20 s, not less — ruled: rumble-sweep, metadata-scan-softblock and fetch-window assert a cooldown + still in force a page load after it was recorded. +- `E2E_CLIP_WINDOW_GAP_MS=2000` — the pause between two clip-window fetches (`common/controller/fetchWindows.ts`: + `testGapMs()` beside `CLIP_WINDOW_PLATFORM_MIN_GAP_SECONDS`, and one line, `testGapMs() ??`, in the gap + expression). fetch-window.spec now asserts the one pause the batch owes, from the job log. + +The other two were never product timers: publish-lane.spec's 45 s and channel-storage.spec's 25 s were the +stuck-job holder's own `releaseAfterMs`, sized for a cold dev server. `GET /api/test/stuck-job?release=<id>` +finishes a holder now, detached from the request exactly as `releaseAfterMs` does (one helper, +`armDetached`); the two specs release it once they have seen what it holds (publish-lane also asserts the +stage is still queued at that point). + +Before/after, start mode, same worktree, per test (and per spec file, from `e2e-timings.mjs`): + +| test | before | after | file before → after | +|---|---|---|---| +| `pacing.spec:195` (a live 429 backs youtube off …) | 1.1 m | 22.4 s | 66.5 s → 24.0 s | +| `publish-lane.spec:78` (a hold mid-stage …) | 49.7 s | 5.6 s | 51.2 s → 7.0 s | +| `fetch-window.spec:337` (a batch skips what is cached …) | 34.1 s | 3.1 s | 45.2 s → 6.4 s | +| `channel-storage.spec:872` (a job cancelled but still stopping …) | 28.1 s | 2.8 s | 85.4 s → 35.3 s | + +Four files: 248 s → 73 s of test time. + +Commits: `d1e80e42` S1 · `5ef76aa0` S2 · `977c8e72` S3 · `0f509eac` a comment. + +Gates: tsc clean before each commit (3 runs, 3.6–4.7 min); `scripts/e2e-stamp.test.mjs` 9/9, +`scripts/e2e-timings.test.mjs` 8/8; `test:scripts` 725 passed + 3 skipped (728); editor unit 218/218; common +3635/3636 — the one, `fetchPosts.test.ts`'s drain-mid-page case, passes 3/3 alone and touches nothing here; +`platformBackoff.test.ts` + `fetchWindows.test.ts` 33/33 with a case for each knob; `docs env --check` clean. +e2e, single files in the foreground: `channel-work.spec` 11/11 (twice, each after a build); umtool +`article-notes` + `usage` 15/15 in start mode (first umtool build 43 s); export `header.spec` 33/33 under +`E2E_MODE=start` (prints the dev line); homepage `source.spec` 6/6; the four S3 files 4/4 + 24/24 before and +after. With the knobs on: `rumble-sweep` + `metadata-scan-softblock` 2/2 and `rate-limit` + `ops-api` 18/18 (start; +ops-api's `detached` holder still detached); `pacing` + `publish-lane` 4/4 under `E2E_MODE=dev` (1.0 m; pacing:195 +21.7 s), and the stamp still fresh after it — a `next dev` in `.next` leaves `.next/e2e` alone. A commit that +changed only `editor/playwright.config.ts` (`0f509eac`) cost no rebuild. + +Not done here, by the brief: the full suites (the orchestrator runs them, start then dev). S4–S6.