mirror of
https://github.com/EstrellaXD/Auto_Bangumi.git
synced 2026-11-01 21:26:08 +08:00
docs: plan issue-driven e2e implementation
This commit is contained in:
@@ -0,0 +1,708 @@
|
||||
# Issue-Driven Hermetic E2E Implementation Plan
|
||||
|
||||
## Status
|
||||
|
||||
Ready for execution. This plan implements the approved
|
||||
[`2026-07-10-issue-driven-e2e-test-design.md`](../specs/2026-07-10-issue-driven-e2e-test-design.md).
|
||||
|
||||
## Objective
|
||||
|
||||
Replace the ordered, stateful E2E workflow with deterministic runtime,
|
||||
real-browser, real-qBittorrent, and packaged-image checks. Fix the two current
|
||||
production regressions exposed by issues #1075 and #1072, make recent issue
|
||||
fixes executable as acceptance tests, and add four parallel pull-request checks
|
||||
whose p95 wall-clock time stays below 15 minutes.
|
||||
|
||||
## Guardrails
|
||||
|
||||
- Preserve unrelated working-tree files. In particular, never stage
|
||||
`backend/uv.lock`, `AGENTS.md`, or `PRODUCT.md` unless their owner explicitly
|
||||
asks for it.
|
||||
- Commit the already-verified authentication implementation separately before
|
||||
beginning E2E work; do not mix it into issue or test-infrastructure commits.
|
||||
- Do not add a production test-only endpoint. Tests use public APIs, mock
|
||||
service admin APIs, or a database fixture written before app startup.
|
||||
- During test execution, AutoBangumi and qBittorrent run on a Docker internal
|
||||
network. Playwright additionally rejects requests outside the dynamically
|
||||
assigned local origins.
|
||||
- No fixed host port, fixed container name, fixed Compose project name, fixed
|
||||
sleep, test retry, numbered test name, or cross-test state dictionary is
|
||||
allowed.
|
||||
- A required test must have a positive or negative control so a broad
|
||||
suppression cannot make it pass accidentally.
|
||||
- Each task uses a red/green cycle: add the narrow failing assertion, confirm
|
||||
the expected failure, implement the minimum behavior, then run the targeted
|
||||
and adjacent suites.
|
||||
|
||||
## Commit Sequence
|
||||
|
||||
1. Existing authentication review implementation.
|
||||
2. `fix: preserve tmdb language and versioned episodes`.
|
||||
3. `fix: make release classification deterministic`.
|
||||
4. `test: add hermetic e2e support services`.
|
||||
5. `test: replace ordered api e2e coverage`.
|
||||
6. `test: add browser issue regressions`.
|
||||
7. `test: exercise real qbittorrent and release images`.
|
||||
8. `ci: run hermetic e2e checks`.
|
||||
|
||||
The sequence keeps production fixes, reusable infrastructure, scenarios, and
|
||||
CI policy independently reviewable.
|
||||
|
||||
## Task 1: Stabilize the Existing Authentication Work
|
||||
|
||||
**Files**
|
||||
|
||||
- Existing modified files under `backend/src/module/{api,application,database,mcp,models,ports,security}`.
|
||||
- Existing modified and new auth tests under `backend/src/test`.
|
||||
- Existing modified files under `webui/src/{api,components,hooks,store,utils}` and `webui/types`.
|
||||
- `docs/superpowers/specs/2026-07-10-auth-review-fixes-design.md`.
|
||||
|
||||
**Steps**
|
||||
|
||||
1. Re-run the focused backend auth, migration, and MCP suites and the focused
|
||||
WebUI auth/access suites against the current working tree.
|
||||
2. Run backend Ruff, Black check, and mypy on the changed Python files.
|
||||
3. Run WebUI type checking, ESLint, Prettier check, and the full Vitest suite.
|
||||
4. Stage only the known authentication implementation, tests, and updated auth
|
||||
design. Explicitly inspect `git diff --cached --name-only` before commit.
|
||||
5. Commit the authentication work without staging the three protected local
|
||||
files listed in Guardrails.
|
||||
|
||||
**Verification**
|
||||
|
||||
```bash
|
||||
cd backend && uv run pytest \
|
||||
src/test/test_api_auth.py \
|
||||
src/test/test_api_passkey.py \
|
||||
src/test/test_api_users.py \
|
||||
src/test/test_auth.py \
|
||||
src/test/test_auth_persistence.py \
|
||||
src/test/test_auth_service_transactions.py \
|
||||
src/test/test_mcp_security.py \
|
||||
src/test/test_migrations_module.py \
|
||||
src/test/test_session_store.py \
|
||||
src/test/test_setup.py -q
|
||||
cd backend && uv run pytest -q
|
||||
cd backend && uv run ruff check src
|
||||
cd backend && uv run mypy src
|
||||
cd webui && pnpm test --run
|
||||
cd webui && pnpm test:build
|
||||
cd webui && pnpm lint
|
||||
cd webui && pnpm format:check
|
||||
```
|
||||
|
||||
## Task 2: Fix TMDB Search Localization (#1075)
|
||||
|
||||
**Files**
|
||||
|
||||
- Modify `backend/src/test/test_tmdb.py`.
|
||||
- Modify `backend/src/module/parser/analyser/tmdb_parser.py`.
|
||||
|
||||
**Steps**
|
||||
|
||||
1. Add table-driven URL tests for TV and movie searches in `zh`, `jp`, and
|
||||
`en`. Parse the URL and assert exact `language`, `query`, `api_key`,
|
||||
`page=1`, and `include_adult=false` values rather than comparing an encoded
|
||||
string manually.
|
||||
2. Add async tests proving both the whitespace-removal retry and the TV-to-movie
|
||||
fallback preserve the requested language.
|
||||
3. Change `search_url` and `search_movie_url` to accept `language` and construct
|
||||
query strings through `urllib.parse.urlencode`.
|
||||
4. Thread `language` through `_search_movie` and every initial, retry, and
|
||||
fallback call. Keep the existing language-bearing cache key.
|
||||
5. Run TMDB, RSS analyser, searcher, and config-reload tests to catch signature
|
||||
and cache regressions.
|
||||
|
||||
**Verification**
|
||||
|
||||
```bash
|
||||
cd backend && uv run pytest \
|
||||
src/test/test_tmdb.py \
|
||||
src/test/test_searcher.py \
|
||||
src/test/test_config.py -q
|
||||
cd backend && uv run ruff check src/module/parser/analyser/tmdb_parser.py src/test/test_tmdb.py
|
||||
cd backend && uv run black --check src/module/parser/analyser/tmdb_parser.py src/test/test_tmdb.py
|
||||
```
|
||||
|
||||
## Task 3: Parse Versioned Episodes Before CRC Tokens (#1072)
|
||||
|
||||
**Files**
|
||||
|
||||
- Modify `backend/src/test/test_torrent_parser.py`.
|
||||
- Modify `backend/src/test/test_offset_scanner.py`.
|
||||
- Modify `backend/src/module/parser/analyser/raw_parser.py`.
|
||||
|
||||
**Steps**
|
||||
|
||||
1. Add the exact reported title as a parser regression:
|
||||
|
||||
```text
|
||||
[SubsPlease] Haibara-kun no Tsuyokute Seishun New Game - 06v2 (1080p) [E931DD98].mkv
|
||||
```
|
||||
|
||||
Assert season 1, episode 6, group `SubsPlease`, and episode type `episode`.
|
||||
2. Add controls for plain `06`, bracketed `06v2`, `13v2` plus a CRC token,
|
||||
explicit `E13`, and a title with no episode signal.
|
||||
3. Update the regular expression so an unbracketed numeric episode may carry a
|
||||
one- or two-digit `vN` suffix. The suffix belongs to the episode token and
|
||||
must be matched before a later `[Ehhhhhhhh]` CRC-like group.
|
||||
4. Give explicit `E`/`EP` episode tokens an alphanumeric boundary so a CRC such
|
||||
as `E931DD98` cannot be accepted as an episode, while `E13` and `EP13`
|
||||
controls continue to parse.
|
||||
5. Add scanner tests with a real temporary database for the exact E6/CRC title,
|
||||
a true-positive E13 against a 12-episode TMDB season, and an unparseable
|
||||
no-signal title. Assert both the boolean result and persisted review fields.
|
||||
6. Run all parser, offset detector, offset scanner, and issue regression tests.
|
||||
|
||||
**Verification**
|
||||
|
||||
```bash
|
||||
cd backend && uv run pytest \
|
||||
src/test/test_torrent_parser.py \
|
||||
src/test/test_title_parser.py \
|
||||
src/test/test_offset_detector.py \
|
||||
src/test/test_offset_scanner.py \
|
||||
src/test/test_issue_bugs.py -q
|
||||
cd backend && uv run ruff check src/module/parser/analyser/raw_parser.py \
|
||||
src/test/test_torrent_parser.py src/test/test_offset_scanner.py
|
||||
```
|
||||
|
||||
## Task 4: Extract and Test Release Classification (#1065)
|
||||
|
||||
**Files**
|
||||
|
||||
- Create `scripts/classify_release.py`.
|
||||
- Create `backend/src/test/test_release_classification.py`.
|
||||
- Modify `.github/workflows/build.yml`.
|
||||
|
||||
**Steps**
|
||||
|
||||
1. Put classification in a pure Python function accepting event name, ref
|
||||
type, ref name, head ref, PR title, and whether the tagged commit is on
|
||||
`main`. The PR title is accepted only to prove it has no effect on version
|
||||
selection.
|
||||
2. Cover normal branch pushes, a PR titled/headed `Doc update`, tag
|
||||
`Doc-update`, a stable tag on and off `main`, and `X.Y.Z-beta.N`.
|
||||
3. Make the CLI print `release`, `dev`, `build_test`, and `version` as GitHub
|
||||
output lines. A stable tag off `main` exits non-zero with a specific error.
|
||||
4. Replace the embedded shell branching in `version-info` with the script while
|
||||
retaining the current job outputs and downstream job interface.
|
||||
5. Confirm a non-SemVer PR title can never become `VERSION`.
|
||||
|
||||
**Verification**
|
||||
|
||||
```bash
|
||||
cd backend && uv run pytest src/test/test_release_classification.py -q
|
||||
cd backend && uv run ruff check ../scripts/classify_release.py \
|
||||
src/test/test_release_classification.py
|
||||
python scripts/classify_release.py --event pull_request --ref-type branch \
|
||||
--ref-name main --head-ref Doc-update --on-main false
|
||||
```
|
||||
|
||||
## Task 5: Build Deterministic Fixture Assets
|
||||
|
||||
**Files**
|
||||
|
||||
- Create `e2e/scripts/generate_fixtures.py`.
|
||||
- Create generated `e2e/fixtures/files/tiny-media.mkv`.
|
||||
- Create generated `e2e/fixtures/torrents/tiny-media.torrent`.
|
||||
- Create RSS fixtures under `e2e/fixtures/rss`.
|
||||
- Create TMDB response fixtures under `e2e/fixtures/tmdb`.
|
||||
- Create a small local player document under `e2e/fixtures/player/index.html`.
|
||||
- Create `backend/src/test/test_e2e_fixture_generation.py`.
|
||||
|
||||
**Steps**
|
||||
|
||||
1. Generate a deterministic 64 KiB payload from a fixed byte pattern; do not
|
||||
store random output.
|
||||
2. Implement the small bencode encoder needed for a single-file torrent using
|
||||
only the Python standard library. Set piece length, SHA-1 pieces, filename,
|
||||
length, and `url-list` deterministically.
|
||||
3. Use the internal URL
|
||||
`http://mock-upstream:18888/files/tiny-media.mkv` as the web seed and an RSS
|
||||
enclosure URL on the same service for the torrent metainfo.
|
||||
4. Check in SHA-256 expectations for both generated files and make the unit
|
||||
test regenerate into a temporary directory and compare bytes.
|
||||
5. Add RSS cases for normal download, duplicate refresh, #1072, and localized
|
||||
TV/movie searches. TMDB fixtures use `poster_path: null`, which proves the
|
||||
parser never reaches its hard-coded public image host during E2E. Fixtures
|
||||
contain no public URL.
|
||||
|
||||
**Verification**
|
||||
|
||||
```bash
|
||||
python e2e/scripts/generate_fixtures.py --check
|
||||
cd backend && uv run pytest src/test/test_e2e_fixture_generation.py -q
|
||||
rg -n 'https?://' e2e/fixtures
|
||||
```
|
||||
|
||||
The URL scan may match only the two approved internal service URLs and the
|
||||
local player fixture's loopback placeholder.
|
||||
|
||||
## Task 6: Add Mock-Upstream and Fake-qB Services
|
||||
|
||||
**Files**
|
||||
|
||||
- Create `e2e/mock-upstream/server.py` and `e2e/mock-upstream/Dockerfile`.
|
||||
- Create `e2e/fake-qb/server.py` and `e2e/fake-qb/Dockerfile`.
|
||||
- Create `backend/src/test/e2e/runtime/test_support_services.py`.
|
||||
|
||||
**Steps**
|
||||
|
||||
1. Implement both services with the Python standard library so their images do
|
||||
not download packages at build time. Pin each Python base image with both a
|
||||
patch tag and immutable digest.
|
||||
2. Mock-upstream serves `/health`, `/rss/*`, `/tmdb/*`, `/images/*`,
|
||||
`/notifications/*`, `/files/*`, `/torrents/*`, and `/player/*`.
|
||||
3. File and torrent responses implement `HEAD` and single-byte-range `GET`,
|
||||
returning correct `Accept-Ranges`, `Content-Range`, `Content-Length`, 206,
|
||||
and 416 semantics. This is required by qBittorrent's web-seed client.
|
||||
4. Its `/__admin/reset` selects a named scenario and clears the request journal;
|
||||
`/__admin/requests` and `/__admin/state` return recorded data. Unknown routes
|
||||
return 501 and remain in the journal.
|
||||
5. Fake-qB implements the qB Web API subset AutoBangumi uses: login/logout,
|
||||
application preferences/version, torrent info, and torrent delete. Admin
|
||||
state selects Windows-path, neighbor-path, and forced-delete-failure cases.
|
||||
6. Redact authorization, cookie, password, and token values before journaling.
|
||||
7. Contract-test health, range requests, scenario reset, exact query/form
|
||||
capture, unknown-route failure, and redaction for both services.
|
||||
|
||||
**Verification**
|
||||
|
||||
```bash
|
||||
cd backend && uv run pytest \
|
||||
src/test/e2e/runtime/test_support_services.py -m e2e -q
|
||||
cd backend && uv run ruff check ../e2e/mock-upstream/server.py \
|
||||
../e2e/fake-qb/server.py src/test/e2e/runtime/test_support_services.py
|
||||
```
|
||||
|
||||
## Task 7: Add the Hermetic Stack and Test Controls
|
||||
|
||||
**Files**
|
||||
|
||||
- Create `e2e/compose/browser.yml`.
|
||||
- Create `e2e/compose/downloader.yml`.
|
||||
- Create `e2e/fixtures/qbittorrent/qBittorrent.conf`.
|
||||
- Create `e2e/scripts/build_test_image.py`.
|
||||
- Create `e2e/scripts/stack.py`.
|
||||
- Create `e2e/scripts/collect_artifacts.py`.
|
||||
- Create `e2e/scripts/audit_sources.py`.
|
||||
- Create `backend/src/test/e2e/support/{client.py,database.py,poll.py,run_offset_scan.py,stack.py}`.
|
||||
- Replace `backend/src/test/e2e/conftest.py` with isolated fixtures.
|
||||
|
||||
**Steps**
|
||||
|
||||
1. Build the WebUI, then assemble a temporary Docker build context containing
|
||||
the tracked backend plus the built `dist` and a generated
|
||||
`module/__version__.py` set to `3.3.999-e2e.1`. Do not write either generated
|
||||
artifact into the working tree. Build the native app image and assert
|
||||
`/app/IMAGE_VERSION` and the Python version module use the same value.
|
||||
2. `browser.yml` starts the app, mock-upstream, and fake-qB on one internal
|
||||
network. Publish app and admin ports dynamically on `127.0.0.1`.
|
||||
`stack.py` creates temporary bind-mounted config, data, download, and log
|
||||
directories so tests can seed a database before startup and retain failure
|
||||
state after containers stop.
|
||||
3. `downloader.yml` overlays real qBittorrent and shared download volumes. The
|
||||
image reference contains a patch tag and immutable `sha256` digest; a stack
|
||||
validation command rejects floating references.
|
||||
4. Mount a fixed, test-only qB configuration. Disable DHT, PeX, LSD, UPnP, NAT
|
||||
traversal, RSS, and update checks.
|
||||
5. `stack.py` creates a unique project name, starts services with health waits,
|
||||
discovers published ports, emits an environment file, restarts one service,
|
||||
and tears down volumes in `finally`/signal handlers.
|
||||
6. `collect_artifacts.py` records Compose status/logs, service journals, qB
|
||||
state, backend logs, and a copy of the failed SQLite database with secret
|
||||
values redacted where output is textual.
|
||||
7. Support polling helpers accept a deadline and include the last observed
|
||||
state in assertion messages. They never sleep without rechecking a bounded
|
||||
condition.
|
||||
8. The pytest fixtures consume `AB_E2E_BASE_URL`, mock/fake admin URLs, Compose
|
||||
files, project name, and artifact directory. The environment file exposes
|
||||
separate internal-service and dynamically published loopback mock URLs so
|
||||
the backend and browser never need to resolve the other's address. Fixtures
|
||||
create unique resource prefixes and an idempotent baseline administrator
|
||||
without relying on test definition order.
|
||||
9. Provide two app fixture types: a dedicated fresh stack for first-run and
|
||||
pre-start database scenarios, and one baseline stack for independent CRUD
|
||||
scenarios. A bootstrap test never shares its config/data directory with a
|
||||
baseline test.
|
||||
A database-seeded scenario first completes Setup with the mock downloader,
|
||||
stops the app, writes fixtures into the bind-mounted current-schema SQLite
|
||||
database, runs any one-shot service operation, and restarts the app. It never
|
||||
pre-creates the database before the initial Setup boot.
|
||||
10. `audit_sources.py` enforces the public-host allowlist, immutable image
|
||||
references, absence of fixed ports/container names and Playwright retries,
|
||||
and absence of blind waits outside the bounded polling helpers.
|
||||
|
||||
**Verification**
|
||||
|
||||
```bash
|
||||
python e2e/scripts/build_test_image.py --version 3.3.999-e2e.1
|
||||
python e2e/scripts/stack.py validate --profile browser
|
||||
python e2e/scripts/stack.py validate --profile downloader
|
||||
python e2e/scripts/stack.py smoke --profile browser
|
||||
```
|
||||
|
||||
## Task 8: Replace the Ordered Runtime E2E Suite
|
||||
|
||||
**Files**
|
||||
|
||||
- Create `backend/src/test/e2e/runtime/test_bootstrap_auth.py`.
|
||||
- Create `backend/src/test/e2e/runtime/test_session_access.py`.
|
||||
- Create `backend/src/test/e2e/runtime/test_restart_persistence.py`.
|
||||
- Create `backend/src/test/e2e/runtime/test_tmdb_language.py`.
|
||||
- Create `backend/src/test/e2e/runtime/test_offset_review.py`.
|
||||
- Create `backend/src/test/e2e/runtime/test_windows_path_delete.py`.
|
||||
- Modify `backend/pyproject.toml` marker declarations if lane-specific markers
|
||||
are introduced.
|
||||
|
||||
**Steps**
|
||||
|
||||
1. Bootstrap/auth runs on its dedicated fresh stack and verifies first-run
|
||||
setup, exact
|
||||
`{ "authenticated": true }` bodies, absence of token material, HttpOnly and
|
||||
SameSite session cookie attributes, POST refresh, wrong-password failure,
|
||||
logout, and deterministic 401 responses without development bypass.
|
||||
2. Session/access creates a secondary user so primary fixture credentials do
|
||||
not mutate. Use two cookie jars, update credentials, and prove old sessions
|
||||
and password fail while the replacement session works.
|
||||
3. Create API and MCP tokens; prove one-time secret display, prefix-only list
|
||||
responses, scope isolation, Authorization precedence, account-control denial
|
||||
for Bearer credentials, and immediate revocation.
|
||||
4. Restart persistence records user, session, tokens, RSS, and Bangumi IDs,
|
||||
restarts only the app service, and checks that valid data remains while
|
||||
revoked credentials stay revoked.
|
||||
5. TMDB language selects mock scenarios and exercises real analysis for TV,
|
||||
movie, whitespace retry, and same-keyword language changes. Assert both the
|
||||
user-facing result and upstream journal.
|
||||
6. Offset review completes Setup on a dedicated fresh stack, stops the app,
|
||||
seeds the real database, runs `run_offset_scan.py` against that work
|
||||
directory, and restarts the app. It covers E6/CRC false-positive, E13
|
||||
true-positive, and no-signal control cases.
|
||||
7. Windows delete points the configured downloader at fake-qB. Exercise both
|
||||
delete and disable through the public API, assert exact selected hash and
|
||||
`deleteFiles`, check database outcomes, and make forced qB failure visible.
|
||||
8. Every test establishes its own preconditions and uses unique resource names;
|
||||
test order can be reversed without changing results.
|
||||
|
||||
**Verification**
|
||||
|
||||
```bash
|
||||
python e2e/scripts/stack.py run --profile browser -- \
|
||||
uv run --directory backend pytest src/test/e2e/runtime -m e2e -q
|
||||
```
|
||||
|
||||
As an isolation audit, run every collected runtime node ID once by itself and
|
||||
run modules in reverse order through the stack runner. Do not add a random-order
|
||||
dependency solely for this check.
|
||||
|
||||
## Task 9: Install and Configure Playwright for Hermetic Test Runtime
|
||||
|
||||
**Files**
|
||||
|
||||
- Modify `webui/package.json` and `webui/pnpm-lock.yaml`.
|
||||
- Modify `webui/unocss.config.ts`.
|
||||
- Create `webui/playwright.config.ts`.
|
||||
- Create `webui/tsconfig.e2e.json`.
|
||||
- Create `webui/e2e/fixtures/{api.ts,auth.ts,network.ts,test.ts}`.
|
||||
- Create `webui/e2e/setup/{bootstrap-chromium.setup.ts,bootstrap-webkit.setup.ts}`.
|
||||
- Create `webui/e2e/specs/production-smoke.spec.ts`.
|
||||
- Modify `.gitignore` for Playwright output directories.
|
||||
|
||||
**Steps**
|
||||
|
||||
1. Use `pnpm add -D --save-exact` for `@playwright/test` and
|
||||
`@iconify-json/simple-icons`, recording exact versions in both package and
|
||||
lock files.
|
||||
2. Remove the UnoCSS `https://esm.sh/` CDN fallback and load simple-icons from
|
||||
the installed JSON package. Prove `pnpm build` succeeds with outbound access
|
||||
blocked.
|
||||
3. Define separate `setup-chromium`/`chromium-desktop` and
|
||||
`setup-webkit`/`webkit-mobile` dependency pairs plus `firefox-nightly`.
|
||||
WebKit uses a 390×844 iPhone-sized viewport. PR jobs select one pair;
|
||||
nightly selects Firefox. Set locale to `en-US`, one worker, zero retries,
|
||||
`serviceWorkers: 'block'`, trace/video/screenshot on failure, and a base URL
|
||||
supplied by `AB_E2E_BASE_URL`.
|
||||
Use `AB_E2E_PROFILE` and spec tags so normal browser jobs exclude the
|
||||
`@downloader` scenario, the downloader job selects it explicitly, and
|
||||
desktop projects exclude the `@mobile` geometry scenario.
|
||||
4. Each setup project completes the real wizard through accessible UI controls,
|
||||
validates the response contract and cookie flags, then writes its own
|
||||
browser-specific storage state. Chromium and WebKit never share one auth
|
||||
file. The browser profile configures fake-qB; the downloader profile uses the
|
||||
real internal qB endpoint. The auth path includes both browser and stack
|
||||
profile, so a downloader invocation never reuses browser-profile state.
|
||||
5. API fixtures create secondary users and unique resources through public
|
||||
endpoints. Network fixtures allow only the app, mock admin, and fake-qB
|
||||
origins; an unexpected request fails the test with method and URL.
|
||||
6. Capture browser console errors and failed responses into per-test artifact
|
||||
files. Redact secrets before writing logs.
|
||||
7. Add a production smoke that checks `/health`, `/#/setup`, and hashed static
|
||||
assets before any feature spec runs. This catches a missing version module or
|
||||
`dist` staging error directly.
|
||||
|
||||
**Verification**
|
||||
|
||||
```bash
|
||||
cd webui && pnpm install --frozen-lockfile
|
||||
cd webui && pnpm build
|
||||
cd webui && pnpm exec playwright test --list
|
||||
cd webui && pnpm test:e2e:typecheck
|
||||
cd webui && pnpm test:build
|
||||
cd webui && pnpm lint
|
||||
cd webui && pnpm format:check
|
||||
```
|
||||
|
||||
## Task 10: Add Browser Authentication and Secret-Lifetime Scenarios
|
||||
|
||||
**Files**
|
||||
|
||||
- Create `webui/e2e/specs/bootstrap-auth.spec.ts`.
|
||||
- Create `webui/e2e/specs/session-access.spec.ts`.
|
||||
- Create `webui/e2e/specs/token-secret.spec.ts`.
|
||||
- Add accessible labels only where needed in
|
||||
`webui/src/components/setup/wizard-container.vue`,
|
||||
`webui/src/components/setup/wizard-step-*.vue`,
|
||||
`webui/src/components/setting/config-access.vue`.
|
||||
|
||||
**Steps**
|
||||
|
||||
1. The browser-specific setup project is the sole owner of first-run Setup.
|
||||
`bootstrap-auth.spec.ts` starts from its resulting account and covers login
|
||||
response secrecy, cookie persistence across reload, wrong password, logout,
|
||||
and browser-back protection. It never attempts Setup a second time.
|
||||
2. Session access uses two independent contexts and a secondary user to prove
|
||||
credential updates invalidate prior sessions without destabilizing the
|
||||
baseline admin.
|
||||
3. Token secret creates a token in the Access panel and confirms the full value
|
||||
appears only in the reveal modal while the list exposes only its prefix.
|
||||
4. Close the reveal, navigate away, unmount the component, and return; the
|
||||
secret must never reappear.
|
||||
5. Delay the real create-token response with Playwright routing, navigate away,
|
||||
then continue the request. Assert the late promise does not restore secret
|
||||
UI or sensitive component state.
|
||||
6. Setup fields, account/token rows, and dialogs use roles, labels, and
|
||||
accessible names; no test ID is added for these flows.
|
||||
|
||||
**Verification**
|
||||
|
||||
```bash
|
||||
python e2e/scripts/stack.py run --profile browser -- \
|
||||
pnpm --dir webui exec playwright test \
|
||||
bootstrap-auth.spec.ts session-access.spec.ts token-secret.spec.ts \
|
||||
--project=chromium-desktop
|
||||
python e2e/scripts/stack.py run --profile browser -- \
|
||||
pnpm --dir webui exec playwright test \
|
||||
bootstrap-auth.spec.ts token-secret.spec.ts --project=webkit-mobile
|
||||
```
|
||||
|
||||
## Task 11: Add Browser Regressions for Recent UI and Parser Issues
|
||||
|
||||
**Files**
|
||||
|
||||
- Create `webui/e2e/specs/tmdb-language.spec.ts`.
|
||||
- Create `webui/e2e/specs/offset-review.spec.ts`.
|
||||
- Create `webui/e2e/specs/mobile-rule-editor.spec.ts`.
|
||||
- Create `webui/e2e/specs/player-local-settings.spec.ts`.
|
||||
- Modify `webui/src/components/basic/ab-bottom-sheet.vue`.
|
||||
- Modify `webui/src/components/bangumi-preview.vue`.
|
||||
- Modify `webui/src/components/ab-edit-rule.vue`,
|
||||
`webui/src/components/ab-add-rss.vue`,
|
||||
`webui/src/components/basic/ab-search.vue`,
|
||||
`webui/src/components/search/ab-search-modal.vue`,
|
||||
`webui/src/components/setting/config-player.vue`,
|
||||
`webui/src/pages/index/config.vue`, and
|
||||
`webui/src/pages/index/player.vue`.
|
||||
|
||||
**Steps**
|
||||
|
||||
1. TMDB language changes parser language from `zh` to `jp`, searches the same
|
||||
keyword, and proves TV, movie, and whitespace retry calls use the expected
|
||||
language without cross-language cache reuse.
|
||||
2. Offset review creates two Bangumi through the real analysis/subscribe APIs,
|
||||
then PATCHes their complete public update payloads so the E13 rule is marked
|
||||
for review and the exact `06v2`/`E931` rule is not. The UI displays a warning
|
||||
only for the true mismatch; runtime tests remain responsible for proving how
|
||||
those persisted states are calculated.
|
||||
3. Mobile rule editor runs at 390×844 in WebKit. Assert full-screen panel
|
||||
geometry, focus the title, contract viewport height, and prove the panel is
|
||||
not translated. Edit, save, reload, and assert persistence.
|
||||
4. Player settings assert immediate localStorage writes, no config-update or
|
||||
restart request, same-context reload persistence, new-context defaults, and
|
||||
an unchanged iframe URL loading the dynamically published loopback
|
||||
`/player/index.html` fixture marker. Give the iframe a localized accessible
|
||||
`title`.
|
||||
5. Name the bottom-sheet dialog/title and add the single geometry-only
|
||||
`data-testid="bottom-sheet-panel"`. Add accessible labels to Bangumi and RSS
|
||||
fields, named regions to config sections, a named search dialog/textbox/list,
|
||||
and a localized iframe title. Correct any control whose explicit ARIA role
|
||||
masks its native button semantics. All other locators use roles, labels, and
|
||||
visible names.
|
||||
|
||||
**Verification**
|
||||
|
||||
```bash
|
||||
python e2e/scripts/stack.py run --profile browser -- \
|
||||
pnpm --dir webui exec playwright test \
|
||||
tmdb-language.spec.ts offset-review.spec.ts player-local-settings.spec.ts \
|
||||
--project=chromium-desktop
|
||||
python e2e/scripts/stack.py run --profile browser -- \
|
||||
pnpm --dir webui exec playwright test \
|
||||
mobile-rule-editor.spec.ts player-local-settings.spec.ts \
|
||||
--project=webkit-mobile
|
||||
```
|
||||
|
||||
## Task 12: Exercise Real qBittorrent and Packaged Images
|
||||
|
||||
**Files**
|
||||
|
||||
- Create `webui/e2e/specs/rss-download-rename.spec.ts`.
|
||||
- Create `backend/src/test/e2e/downloader/test_release_image_smoke.py`.
|
||||
- Create `backend/src/test/e2e/downloader/test_qb_image_contract.py`.
|
||||
- Modify `e2e/fixtures/qbittorrent/qBittorrent.conf`.
|
||||
- Modify `e2e/fixtures/rss/tiny-download.xml`.
|
||||
|
||||
**Steps**
|
||||
|
||||
1. Validate the pinned qB image starts with the mounted credentials and all
|
||||
peer-discovery/update features disabled. Resolve one supported LinuxServer
|
||||
image once, record the complete `<patch-tag>@sha256:<digest>` reference in
|
||||
Compose, and make the image-contract test compare that exact shape.
|
||||
2. Complete Setup with the internal qB endpoint and shared download path.
|
||||
This occurs in the downloader invocation's dedicated setup project and fresh
|
||||
stack; the feature spec consumes its profile-specific storage state and does
|
||||
not run Setup again.
|
||||
3. Add the local RSS, call the real refresh endpoint, and poll qB until the
|
||||
web-seed torrent is complete. No tracker, DHT, or public peer may appear in
|
||||
qB state.
|
||||
4. Poll AutoBangumi API/database/filesystem until the media file is renamed and
|
||||
organized. Assert the expected season and episode in both API and path.
|
||||
5. Refresh the same RSS again and prove the torrent/task count does not grow.
|
||||
6. Open the WebUI and prove completed state is visible, then delete with files
|
||||
enabled and verify qB state, AutoBangumi state, and the shared file are gone.
|
||||
7. Build the native AutoBangumi image with `3.3.999-e2e.1`, start it with fresh
|
||||
volumes, and assert `/health` remains healthy, `db_ok=true`, and the health
|
||||
version equals `docker exec ... cat /app/IMAGE_VERSION`.
|
||||
8. The image test must inspect the locally built image ID and fail if Docker
|
||||
attempts to pull `latest` or any public AutoBangumi image.
|
||||
|
||||
**Verification**
|
||||
|
||||
```bash
|
||||
python e2e/scripts/stack.py run --profile downloader -- \
|
||||
pnpm --dir webui exec playwright test rss-download-rename.spec.ts \
|
||||
--project=chromium-desktop
|
||||
python e2e/scripts/stack.py run --profile downloader -- \
|
||||
uv run --directory backend pytest src/test/e2e/downloader -m e2e -q
|
||||
```
|
||||
|
||||
## Task 13: Add Pull-Request and Nightly Workflows
|
||||
|
||||
**Files**
|
||||
|
||||
- Create `.github/workflows/e2e.yml`.
|
||||
- Create `.github/workflows/e2e-nightly.yml`.
|
||||
- Modify `.github/workflows/build.yml`.
|
||||
- Create `e2e/README.md`.
|
||||
|
||||
**Steps**
|
||||
|
||||
1. Add `e2e-runtime`, `e2e-browser (chromium)`,
|
||||
`e2e-browser (webkit-mobile)`, and `e2e-downloader-image` jobs on every pull
|
||||
request without branch or path filters. Set `timeout-minutes: 15` on each
|
||||
job.
|
||||
2. Change the existing backend unit-test job to run
|
||||
`pytest -m "not e2e"`. Keep the nested E2E skip guard as defense in depth so
|
||||
a normal `pytest` invocation cannot start Docker unexpectedly.
|
||||
3. Cache uv, pnpm, Playwright browser binaries, frontend output, and Buildx
|
||||
layers with lockfile/source-sensitive keys. Install dependencies before
|
||||
starting the internal test network.
|
||||
4. Give every job a unique Compose project and artifact directory derived from
|
||||
workflow, job, run ID, attempt, and matrix project.
|
||||
5. Wrap execution so stack logs and failure artifacts upload with `if: always()`
|
||||
even when setup or tests fail. Use 14-day retention.
|
||||
6. Run zero PR retries. The nightly workflow runs Firefox and repeated browser
|
||||
and downloader passes, uploads the same artifacts, and remains non-blocking.
|
||||
7. Document exact local commands, prerequisites, expected duration, artifact
|
||||
locations, mock scenario authoring, fixture regeneration, and qB digest
|
||||
update procedure.
|
||||
8. Keep branch protection unchanged during ten shadow PR runs. Record run URLs,
|
||||
result, and wall-clock duration; after 10/10 passes and p95 below 15 minutes,
|
||||
make the four named checks required in repository settings.
|
||||
|
||||
**Verification**
|
||||
|
||||
```bash
|
||||
rg -n 'timeout-minutes: 15|retention-days: 14' \
|
||||
.github/workflows/e2e.yml .github/workflows/e2e-nightly.yml
|
||||
ruby -e "require 'yaml'; YAML.load_file('.github/workflows/e2e.yml')"
|
||||
ruby -e "require 'yaml'; YAML.load_file('.github/workflows/e2e-nightly.yml')"
|
||||
```
|
||||
|
||||
## Task 14: Remove the Legacy Workflow and Run the Full Gate
|
||||
|
||||
**Files**
|
||||
|
||||
- Delete `backend/src/test/e2e/test_e2e_workflow.py`.
|
||||
- Delete `backend/src/test/e2e/docker-compose.test.yml`.
|
||||
- Delete `backend/src/test/e2e/mock_rss_server.py`.
|
||||
- Delete `backend/src/test/e2e/Dockerfile.mock-rss`.
|
||||
- Delete migrated files under `backend/src/test/e2e/fixtures` after confirming
|
||||
their only replacement is under root `e2e/fixtures`.
|
||||
- Update `backend/src/test/e2e/__init__.py` to describe the new lane split.
|
||||
- Update `e2e/README.md` with the final command names and artifact paths.
|
||||
|
||||
**Steps**
|
||||
|
||||
1. Map every meaningful old assertion to a unit, runtime, browser, downloader,
|
||||
or image test. Preserve documented compatibility assertions; discard only
|
||||
order checks, permissive `(200, 401)` checks, and no-op assertions that do
|
||||
not describe a contract.
|
||||
2. Remove the old session-scoped process, fixed ports/container names,
|
||||
`e2e_state`, and numbered methods.
|
||||
3. Run `audit_sources.py` against the new tree for blind sleeps, public upstream
|
||||
hosts, floating image tags, raw secret logging, fixed ports/container names,
|
||||
and Playwright retries.
|
||||
4. Intentionally trigger one mock 501, one browser assertion failure, and one qB
|
||||
timeout locally. Confirm the artifact bundle identifies the last observed
|
||||
state without an interactive rerun, then restore the assertions.
|
||||
5. Run backend unit/runtime/downloader suites, WebUI unit/type/lint/build suites,
|
||||
both PR browser projects, and the native image smoke.
|
||||
6. Ask a subagent to review the final diff for correctness, isolation, secret
|
||||
handling, flake sources, and divergence from the approved design. Fix every
|
||||
actionable finding and rerun the affected lanes.
|
||||
|
||||
**Verification**
|
||||
|
||||
```bash
|
||||
python e2e/scripts/audit_sources.py
|
||||
cd backend && uv run pytest -q
|
||||
cd backend && uv run ruff check src ../e2e ../scripts/classify_release.py
|
||||
cd backend && uv run mypy src
|
||||
cd webui && pnpm test --run
|
||||
cd webui && pnpm test:build
|
||||
cd webui && pnpm lint
|
||||
cd webui && pnpm format:check
|
||||
python e2e/scripts/stack.py run --profile browser -- \
|
||||
uv run --directory backend pytest src/test/e2e/runtime -m e2e -q
|
||||
python e2e/scripts/stack.py run --profile browser -- \
|
||||
pnpm --dir webui exec playwright test --project=chromium-desktop
|
||||
python e2e/scripts/stack.py run --profile browser -- \
|
||||
pnpm --dir webui exec playwright test --project=webkit-mobile
|
||||
python e2e/scripts/stack.py run --profile downloader -- \
|
||||
pnpm --dir webui exec playwright test rss-download-rename.spec.ts \
|
||||
--project=chromium-desktop
|
||||
python e2e/scripts/stack.py run --profile downloader -- \
|
||||
uv run --directory backend pytest src/test/e2e/downloader -m e2e -q
|
||||
```
|
||||
|
||||
## Completion Criteria
|
||||
|
||||
- #1075 and #1072 are fixed with focused unit/integration regressions.
|
||||
- #1069, #1068, #1067, #1065, and #1062 have deterministic acceptance tests.
|
||||
- The old ordered E2E workflow and duplicate fixtures are gone.
|
||||
- Runtime, Chromium, WebKit mobile, real-qB, and image tests pass locally.
|
||||
- Test execution produces no public request and uses no floating container tag.
|
||||
- Failure artifacts contain enough state to diagnose injected failures.
|
||||
- The four PR jobs complete within 15 minutes and enter the ten-run shadow
|
||||
period before branch-protection enforcement.
|
||||
Reference in New Issue
Block a user