Co-authored-by: Carlos Florencio <carlosmflorencio@gmail.com>
12 KiB
id, title, status, wave, depends_on, plan, spec
| id | title | status | wave | depends_on | plan | spec | |
|---|---|---|---|---|---|---|---|
| session-hostname-resolution-04 | E2E and final verification | pending | 4 |
|
docs/plans/session-hostname-resolution/plan.md | docs/specs/session-hostname-resolution/spec.md |
E2E and final verification
Inputs
Tasks 01-03 and the spec
acceptance criteria. Existing E2E patterns:
apps/web/e2e/tests/auth/auth-screenshots.spec.ts (auth-mode restart +
account security page), apps/web/e2e/helpers/auth.ts (setupAdmin, login,
mintPAT), apps/web/e2e/helpers/causal-waits.ts (causal wait primitives),
and apps/web/e2e/README.md for projects/commands.
Project placement (verified against apps/web/e2e/playwright.config.ts)
tests/auth/*.spec.tsruns in theauthproject: the backend is restarted with auth enabled and the whole API is locked behind a login;mobile-*.spec.tsfiles undertests/auth/run in themobile-chromeproject instead (the auth project'stestIgnoreexcludes them). The new spec must not claim to run on the default project.
Implementation
Desktop spec — apps/web/e2e/tests/auth/account-security-hostnames.spec.ts
Setup (mirror auth-screenshots.spec.ts for the restart shape, plus
mobile-users-self-actions.spec.ts for DB isolation):
beforeAll:backend.restart({ KANDEV_FEATURES_AUTH: "true", KANDEV_DATABASE_PATH: path.join(backend.tmpDir, "kandev-hostnames-desktop.db") })— an isolated DB path is required, exactly as in the mobile spec. The auth project's worker is serial/shared andbackend.restartpreserves the existing SQLite DB, so without an isolated path this spec's second login (focus-refetch step), hostname cache rows, and admin state leak into later auth-project tests, andsetupAdminmay find the instance already past setup mode. The isolated DB also makes this spec's own row-count assertions deterministic across runs.- Lifecycle (pinned): call
setupAdmin(...)only to create the admin and authenticate the page context —/auth/setupalready creates the admin's session and sets thekandev_sessioncookie on the context, so the baseline sessions table has exactly one row. Do NOT calllogin(...)for the main flow (it would create a second baseline row and skew the isolation proof).login(...)is used only in the focus-refetch step to create the second session row. The page context must carry the authenticated session cookie for both the settings page and theafterEachcleanup PATCH. afterAll:backend.restart()to return to the baseline env (this reverts the DB path too).afterEach: PATCHresolve_session_hostnamesback tofalseusing the authenticated API client (the E2E worker does not reset user settings between tests).- Isolation model (single test, no false claims): the spec is one
test(...)containing the whole flow below — a second test cannot assert a fresh one-row baseline because the isolated DB persists across tests within the worker (setupAdmin runs once inbeforeAll, and the focus-refetchloginadds a session that noafterEachremoves). Cross-spec isolation comes from the isolatedKANDEV_DATABASE_PATHitself (other auth-project specs use their own/backend's DB), not from an in-spec two-test proof. Within the single test, the row-count assertions are: exactly one row aftersetupAdmin(fresh DB), and exactly two after the focus-refetchlogin— these are deterministic because the DB is dedicated to this spec.
Flow (assertions are env-robust; DNS answers vary — never assert an exact
hostname). The E2E never depends on auth.session.hostname.resolved
events: the worker-scoped backend keeps its DB (and therefore the hostname
cache) across tests, and every e2e login comes from the same loopback IP, so
a first-resolution or changed-result event is not guaranteed for any toggle.
Streaming behavior is covered by the resolver/WS-handler/component unit
tests; the E2E validates the UI contract (column, hostname-or-N/A cells,
cached rendering on re-enable, persistence, focus-refetch):
- Before navigation:
const watch = watchWs(page);thenpage.goto(...)(percausal-waits.ts,watchWsmust precede the firstgoto). - Navigate to
/settings/account/security; assert the sessions table has no Hostname column (toggle is off by default). - Toggle the switch on; assert the column appears and every row's cell is
either
N/Aor a non-empty hostname immediately (empty always rendersN/A; there is no pending state). - Toggle off; assert the column is hidden.
- Toggle on again; assert the column reappears with values (cached from the
in-memory map and/or the enable-transition refetch's
hostnamevalues —N/Aor a non-empty hostname). Do NOT wait for a WS event here: an unchanged or fresh-cache result intentionally emits none. - Reload the page; assert the toggle is still on and the column is shown
with hostname-or-
N/Acells (persistence through user settings). - New-session discovery (required, not skippable): with the page still
open, call
login(...)again through the same BrowserContext's request client — that is what creates a secondauth_sessionsrow (navigating alone only reuses the cookie and adds nothing; the baseline is the singlesetupAdminsession, so the table must grow from one row to two). Then: a.const page2 = await ctx.newPage();b. Explicitly activate page 2 first:await page2.bringToFront();—gotoalone is navigation, not activation, and the focus handler on page 1 must deterministically fire from a real blur/focus transition. c.await page2.goto("/")(shared cookies authenticate it); wait for the app shell to render on page 2. d. Arm a causal wait for the sessions refetch (watch.waitForHttporpage.waitForResponseonGET /api/v1/auth/sessions), thenawait page.bringToFront()on the original page; await the sessions response — this proves the focus handler fired and refetched. e. Assert the sessions table now has two rows (thesetupAdminsession plus the new login) and the new row's hostname cell is hostname-or-N/A. If the auth fixture cannot create a second session for the same admin, fix the fixture rather than dropping the assertion.
Mobile spec — apps/web/e2e/tests/auth/mobile-account-security-hostnames.spec.ts
Runs in the mobile-chrome project (routed away from the desktop auth
project via its testIgnore), which does NOT inherit the auth project's
backend restart — the spec must set it up itself, mirroring
mobile-users-self-actions.spec.ts:
beforeAll:backend.restart({ KANDEV_FEATURES_AUTH: "true", KANDEV_DATABASE_PATH: path.join(backend.tmpDir, "kandev-hostnames-mobile.db") })(an isolated DB so the hostname cache does not leak between the desktop and mobile workers);afterAll:backend.restart()to the baseline env.- Create a manual context:
browser.newContext({ ...devices["Pixel 5"], baseURL: backend.frontendUrl })(manual contexts do not inherit project device options), thensetupAdmin+loginbefore navigating. afterEach: PATCHresolve_session_hostnamesback tofalsewith the authenticated client.
Flow at the Pixel 5 viewport: enable → column appears → cells are
hostname-or-N/A (no WS waits, per the desktop flow), disable → hidden,
re-enable → restored. Assert the toggle switch row is reachable/tappable and
the page has no document-level horizontal overflow with the Hostname column
rendered.
Final verification
(cd apps/backend && go test -race ./internal/auth/... ./internal/user/... ./internal/gateway/websocket/... ./internal/backendapp/... ./pkg/websocket/...)
make -C apps/backend lint
(cd apps/web && pnpm run i18n:check)
(cd apps/web && pnpm run i18n:ratchet)
(cd apps/web && pnpm run typecheck)
(cd apps/web && pnpm run lint)
(cd apps/web && pnpm exec vitest run lib/ssr/user-settings.test.ts lib/ws/handlers/users.test.ts lib/ws/handlers/session-hostnames.test.ts lib/state/slices/auth/auth-slice.test.ts components/settings/account/security-settings.test.tsx)
(cd apps/web && pnpm e2e:run --project auth tests/auth/account-security-hostnames.spec.ts)
(cd apps/web && pnpm e2e:run --project mobile-chrome tests/auth/mobile-account-security-hostnames.spec.ts)
git diff --check
Update the plan's wave table and this task's status to done when green.
Acceptance
- The desktop spec (auth project) proves default-off, enable → column +
hostname-or-
N/Acells (no WS-event waits), disable → hidden, re-enable → restored with cached values, reload persistence, and focus-refetch discovery of a second login, with authenticated setup/cleanup. - The mobile spec (mobile-chrome project) proves the same flow at Pixel 5
width with no horizontal overflow, using its own auth-enabled backend
restart (isolated DB),
setupAdmin/login, and authenticated cleanup. - All focused backend and frontend checks pass;
git diff --checkis clean. - The spec/plan/task statuses are updated to match reality.
Verification
The commands in "Final verification" are the acceptance gate.
Dependencies
Tasks 01-03.
Risks
- Running the spec without the auth project's env/helpers:
GET /api/v1/auth/sessionsis behindRequireRealIdentity, so an unauthenticated page cannot load sessions. Always restart withKANDEV_FEATURES_AUTH: "true"and log in first. - Exact-hostname assertions flake across environments (no PTR,
localhost, synthesized.in-addr.arpa). Assert column presence/absence and hostname-or-N/Acell text only; becauseN/Arenders immediately there is no pending state to wait for. - Never await
auth.session.hostname.resolvedin E2E: the worker backend keeps its cache across tests and e2e logins share one loopback IP, so a first-resolution or changed-result event is not guaranteed (fresh cache and unchanged results intentionally emit nothing). Streaming is proven by unit/component tests. - Focus-refetch coverage must call
loginagain through the same context's request client (only setup/login createauth_sessionsrows; navigation reuses the cookie), then navigate a second same-context page so page 1 really loses focus, thenbringToFrontpage 1 and assert the row count grew by exactly one. It must not be skippable; fixture limitations are fixture bugs. - The desktop spec must restart with an isolated
KANDEV_DATABASE_PATH(the auth worker is serial/shared andbackend.restartpreserves the DB); without it, the focus-refetch second login, hostname cache, and admin state leak into later auth-project tests andsetupAdminmay not be in setup mode. Keep the spec a single test with deterministic 1-row → 2-row assertions; cross-spec isolation comes from the dedicated DB path, not an in-spec two-test proof (the DB persists across tests, so a second test cannot claim a fresh baseline). - The mobile spec runs in the
mobile-chromeproject, which does not inherit the auth project's backend restart; it must restart auth-enabled with an isolated DB itself (permobile-users-self-actions.spec.ts) or the page hits setup/unauthenticated mode and sessions never load. - A leftover enabled setting leaks into later specs; always reset in
afterEachwith the authenticated client. - The managed E2E runner must rebuild production assets on the first run;
re-run with
--no-buildonly after a successful prior build from the same tree.