Skip to content

test(pwa): browser-level regression for the install-to-activation cache re-poisoning window - #2632

Closed
Chris0Jeky wants to merge 2 commits into
mainfrom
issue-2475/pwa-activation-race
Closed

test(pwa): browser-level regression for the install-to-activation cache re-poisoning window#2632
Chris0Jeky wants to merge 2 commits into
mainfrom
issue-2475/pwa-activation-race

Conversation

@Chris0Jeky

Copy link
Copy Markdown
Owner

Summary

Adds one gated Playwright case to frontend/taskdeck-web/tests/e2e/pwa-proof-strict.spec.ts that pins the install-to-activation re-poisoning window described in #2475 at browser level, and marks it with test.fail() because it is red against the current production build.

The case installs the #2350-era worker exactly as the existing strict case does, then holds the migration window open with the browser rather than racing a clock: registerType: 'prompt' parks the replacement worker in waiting until a page sends taskdeck:skip-waiting, and the real sign-in migration is what sends it. Inside that window the case proves the install-time sweep already finished (its marker cache taskdeck-pwa-cache-policy-v2 is present), proves the old worker is still the controller (it still answers the pre-#2350 policy handshake) and proves the replacement is still waiting. It then seeds taskdeck-static-assets through the OLD worker's own CacheFirst fetch handler, from a real 200 response, not by page script writing into CacheStorage. After sign-in completes the migration it asserts the seeded entry is gone.

No production code changes. The stub worker, the route interception and the policy handshake are all test-side.

Root cause

The seeded entry survives the migration on the current build, so the case fails. The reason is already recorded on main in frontend/taskdeck-web/public/api-cache-cleanup.js (the comment at the end of the file, landed with #2416): the generated worker loads that file with importScripts() from inside vite-plugin-pwa's asynchronous AMD define() factory, which runs in a promise continuation rather than during the worker's synchronous initial evaluation. By the time the activate listener is attached, the activate event has already been dispatched, so the forced re-sweep inside event.waitUntil never fires. Only the memoised evaluation-time sweep runs, and that sweep has already resolved by the time this case seeds.

That is the defect #2475 asks for evidence of, and repairing it needs a production change (an activation hook that registers synchronously, or the injectManifest move), which is out of scope for this branch. So the case ships marked rather than unmarked and red:

Nothing in CI runs this file. It is gated behind TASKDECK_E2E_PWA_PREVIEW=1 and nothing in the repository sets that variable, so the marker governs only the manually driven lane.

Verification

Environment: backend from this worktree on http://localhost:5031 (dotnet run --project backend/src/Taskdeck.Api/Taskdeck.Api.csproj -c Debug --no-launch-profile, ASPNETCORE_ENVIRONMENT=Development), production build served by npx vite preview --port 4173 --strictPort from frontend/taskdeck-web. All Playwright runs from frontend/taskdeck-web with TASKDECK_E2E_PWA_PREVIEW=1, TASKDECK_E2E_API_BASE_URL=http://localhost:5031/api, TASKDECK_E2E_FRONTEND_BASE_URL=http://localhost:4173.

Lane command, run three consecutive times:

npx playwright test tests/e2e/pwa-proof-strict.spec.ts --config playwright.pwa-proof.config.ts --project=chromium --workers=1 --reporter=line

Results: 2 passed (5.8s) exit 0, 2 passed (4.4s) exit 0, 2 passed (3.9s) exit 0. A fourth run after the marker-semantics check below: 2 passed (5.0s) exit 0.

Diagnostics printed by the new case on run 1, which are the actual red evidence for #2475:

PROOF raceReplacementState = installed
PROOF raceWindowOpen = {"waiting":true,"activeState":"activated","controllerPresent":true}
PROOF raceSeededEntries = ["http://localhost:4173/icons/icon-192x192.png?td-race-seed=1"]
PROOF raceEntriesAfter = ["http://localhost:4173/icons/icon-192x192.png?td-race-seed=1"]
PROOF raceKeysAfter = ["taskdeck-pwa-cache-policy-v2","workbox-precache-v2-http://localhost:4173/","taskdeck-static-assets"]

The seeded entry is present before the migration and still present after it, and the failing assertion under the marker is expect(entriesAfter.some((u) => u.includes('td-race-seed'))).toBe(false).

Marker-semantics check, to prove the marker does not hide a future regression: the final assertion was temporarily inverted to .toBe(true) so the case would pass, and the same lane command was run. Output: Expected to fail, but passed. then 1 failed, 1 passed (5.2s), exit 1. The assertion was restored from a backup copy and the lane was re-run green (the fourth run above). The working tree diff after restoring contains only the marker and its comment.

Other checks, all from frontend/taskdeck-web in this worktree:

  • npx eslint tests/e2e/pwa-proof-strict.spec.ts exit 0, no output.
  • npm run typecheck (vue-tsc -b) exit 0.
  • git diff --check exit 0, no output.

Not verified

  • No backend test project was run. This branch touches no backend code.
  • The vitest suites were not run. No file under src/ changed, so src/tests/pwa/ and test:pwa-generated-worker were not exercised.
  • The case was only run on Chromium on this machine. Firefox and WebKit service worker behaviour is untested, and the pwa-proof config only defines a chromium project.
  • The behaviour of the case against a build where the activation-time sweep actually runs was not measured. The marker-semantics check above simulates the passing outcome by inverting the assertion, not by repairing production code, so the case has never been observed passing for the right reason.
  • Whether the case is stable on a heavily loaded machine was not measured. The four runs above completed in about four to six seconds each on an otherwise quiet box.
  • No CI run has exercised this file, because nothing in CI sets TASKDECK_E2E_PWA_PREVIEW=1.

Risk notes

  • The case is a standing expected failure. If a future change repairs the activation sweep, the pwa-proof lane goes red with "Expected to fail, but passed" until someone removes the test.fail(). That is deliberate and the comment on the marker says so, but it is a red an operator has to read rather than react to.
  • The post-migration assertion reads taskdeck-static-assets once rather than polling. If a repair lands that sweeps inside a real activate waitUntil, the read could in principle happen while that sweep is still in flight and report a false red. It cannot produce a false green, so it does not weaken the evidence this branch carries, but the assertion should become an expect.poll when the repair lands.
  • The nominal worst-case wait budget of the new case is close to the 90 second per-test timeout in playwright.pwa-proof.config.ts, so on a slow box a failure could surface as an opaque timeout instead of the diagnostics above. Not observed in any of the runs here.
  • The stub legacy worker body and the askPolicy helper are duplicated between the two cases in the file. If the handshake constants in public/api-cache-cleanup.js or src/pwa/legacyApiCacheWorker.ts change, both copies need updating.
  • The header comment above the new case describes the forced activate-time sweep as the mechanism under test without repeating the inertness finding; the marker comment immediately inside the case body carries that finding.

Closes #2475

Holds the window open with a waiting replacement instead of racing a poll: the seed is written by the old worker's own CacheFirst handler strictly after the install-time sweep (proven by the marker cache) and strictly before activation (proven by registration.waiting and the pre-#2350 policy answer). This case is RED against current main - see the record on #2475. Not for merge as-is.
…ilure

The case is red against the current production build because the forced activate-time re-sweep never runs on the generated worker. Marking it with test.fail keeps the pwa-proof lane exit code meaningful and turns the case red again the moment a production repair makes it pass.
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@Chris0Jeky

Copy link
Copy Markdown
Owner Author

Superseded, closing: this PR was opened by the fix round of the bounded #2475 attempt after its review asked for an expected-failure marker; it carries only the test.fail() case. The production repair is #2639, whose branch already cherry-picks this commit and will land the case without the marker once the sweep really runs. The attempt record stays on #2475.

@Chris0Jeky Chris0Jeky closed this Sep 5, 2026
@github-project-automation github-project-automation Bot moved this from Pending to Done in Taskdeck Execution Sep 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

[Frontend][PWA] Browser-level regression for the install-to-activation cache re-poisoning window

1 participant