feat(e2e): add tests for real-time updates and token lifecycle by pikann · Pull Request #621 · Paca-AI/paca · GitHub
Skip to content

feat(e2e): add tests for real-time updates and token lifecycle - #621

Merged
pikann merged 3 commits into
masterfrom
feature/add-e2e-tests-for-realtime
Oct 22, 2026
Merged

pikann merged 3 commits into
masterfrom
feature/add-e2e-tests-for-realtime

Conversation

@pikann

@pikann pikann commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Adds a realtime E2E area covering the per-event authorization work merged in #620, and fixes one UI gap those tests found.

The area has 68 cases in four specs (each with its .feature file, following the repo's convention). The specs open real socket.io connections as signed-in members (the protocol the web app speaks), change data as the admin over the API, and assert on what each socket receives. A negative assertion always waits for a socket that IS allowed to receive the same event first, so "nothing arrived" never means "nothing has arrived yet".

Spec Cases Covers
event-authorization 38 (5 fixme) Handshake refusals (no credentials, garbage, expired, wrongly signed, refresh-kind, deleted-user tokens; cookie vs handshake token). Joining (exact rooms per role, non-member, cross-project, malformed joins, leave and rejoin). Events: a Deny on one task, document, sprint, automation or environment; sprint, folder and per-view conditions; single-resource grants; Allow plus Deny across roles; unioned roles; event ordering; two sockets of one account; assignee-only notifications; views:read vs sprints:read.
iam-changes 20 Role edits, attaching and detaching roles, a workspace role naming the project, deleting a role, removing and re-adding a member, deleting a user or project, other sockets, a burst of edits. Every case keeps one socket open and checks its id never changes.
token-lifecycle 2 reauth about a minute before expiry and disconnect at expiry, using tokens minted with the stack's JWT secret. Two lifetimes run concurrently (about 80s).
live-updates-ui 10 Real browser: tasks appear, rename and disappear live; a Deny; the UI following role edits; one stable connection; the full reauth round trip with real websocket frames; catch-up after going offline.

Shared helpers are in tests/helpers/realtime.ts (socket probe, token minter, cleanup tracking). socket.io-client is added as a dev dependency of apps/e2e, and the README lists the new area.

Fix found by the tests

After a member lost tasks:read, the page kept showing the task: the refetch is refused with a 403, and React Query keeps the previous data on error. use-project-realtime.ts now calls resetQueries instead of invalidateQueries when a namespace is lost, so refused data does not stay on screen. Granting or changing permissions without losing a namespace still only invalidates. Two unit tests cover both branches.

Placeholders

Five cases are test.fixme:

  • Three need an LLM-backed agent, which the e2e stack does not have (conversation events).
  • Two depend on a design decision still open: a task that moves out of a member's scope, and a deletion under a conditional grant (see feat: enhance-realtime-authorization #620).

Type of Change

  • Other (E2E tests for realtime authorization, plus a web hook fix)

Testing

  • Run against the isolated e2e stack (deploy/docker-compose.e2e.yml, Chromium, 3 workers, no retries): 63 passed, 5 skipped, 0 failed.
  • Mutation check: with the realtime service's per-event access check disabled, exactly the 15 entity-level denial tests fail and the room-level ones still pass, so the denial tests are not vacuous.
  • Timing assertions allow for the server's 5-second expiry scan, and the environment test joins its sockets before triggering events; both flaked before those two fixes, and the whole area was re-run afterwards.
  • Web: tsc -b, biome and the related vitest files pass.
  • Not run: Firefox, WebKit and the mobile projects. The new specs are not added to any CI workflow, and e2e-tests.yml is manual-trigger only.

Checklist

  • The change is focused and scoped.
  • Related documentation is updated.
  • New structure or direction is explained clearly.
  • I avoided unnecessary detail or premature abstraction.

🤖 Generated with Claude Code

- Implement end-to-end tests for real-time updates in the UI, ensuring tasks appear, update, and disappear without page reloads.
- Add tests for token lifecycle management, verifying that sockets handle token expiration and reauthentication correctly.
- Enhance the project real-time hook to reset cached data when access is lost, preventing stale data from displaying.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No critical issues — one tradeoff worth a look.

Reviewed changes — the new realtime E2E area plus a one-line production fix in the project realtime hook.

  • Realtime probe helpers — new tests/helpers/realtime.ts opens real socket.io connections as signed-in members, mints short-lived JWTs with the stack's JWT_SECRET, and records events/reauth/disconnects for the specs.
  • Event-authorization specs — 38 cases covering handshake refusals, per-role room joins, and per-resource Allow/Deny (sprint/folder/view conditions, role unions, ordering).
  • IAM-change specs — 20 cases keeping one socket open across role edits, attach/detach, and member/user/project deletion, asserting permissions:changed and room re-sync.
  • Token-lifecycle specs — reauth ~60s before expiry and disconnect at expiry on minted short-TTL tokens.
  • Live-UI specs — real-browser checks for live create/rename/delete, role-edit reflection, the reauth round trip, and offline catch-up.
  • Web hook fix — use-project-realtime.ts now resetQueries the project when a namespace is lost, so a refetch the server 403s does not leave refused data on screen. Verified against TanStack Query v5 (resetQueries resets to initial state then refetches active matches) and query-client.ts (shouldRetryQuery skips 4xx), so the fix behaves as described.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

Comment thread apps/web/src/hooks/use-project-realtime.ts

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No critical issues — the prior reset-scope concern is addressed; one completeness note on the new mapping.

Reviewed changes — the delta since the prior pullfrog review (6baefe5): the narrowed lost-namespace reset plus new gained-namespace handling in the project realtime hook and socket client.

  • Scoped the lost-namespace reset — use-project-realtime.ts now resets through a holdsDataOf predicate driven by a new NAMESPACE_QUERY_ROOTS table, so only the lost namespace's data is cleared and the just-invalidated members/permissions queries are spared.
  • Added a gained-namespace refetch — NamespacesChange now carries added (empty on the first report for a project); the hook invalidates a regained namespace's queries, which may be stuck in the error state from a prior rejection.
  • Refined recordNamespaces — the socket client computes gained vs. lost namespaces against the previous grant and reports both.
  • Expanded unit tests — new hook cases exercising the predicate (per-namespace, task data under sprints/views, unknown namespaces, permissions spared) and a socket-client case for added.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new table deliberately errs safe for a namespace it does not know, but for the seven known namespaces it uses a fixed list, and that list misses a few ["projects", projectId, …] caches gated by the namespace's own read action: task-types/task-statuses/custom-fields (tasks:read) and automation-plugin-node-types (workflows:read). After losing tasks:read/workflows:read those caches are neither reset nor cleared, whereas the broad reset this replaced did clear them. Low impact — the screens that render them are gated by the same permission — but worth listing for completeness.

Technical details
# `NAMESPACE_QUERY_ROOTS` omits role-gated schema queries

## Affected sites
- `apps/web/src/hooks/use-project-realtime.ts:92` — `tasks: ["tasks", "agentActivities"]` omits `task-types`, `task-statuses`, `custom-fields`.
- `apps/web/src/hooks/use-project-realtime.ts:96` — `workflows: ["workflows", "automations", "automation-dependency-map"]` omits `automation-plugin-node-types`.

## Evidence
- `task-types` / `task-statuses` / `custom-fields` keys: `apps/web/src/lib/project-api.ts:679`, `:685`, `:691`. Their GET routes require `iam.ActionTasksRead`: `services/api/internal/transport/http/router/router.go:539`, `:555`, `:672`.
- `automation-plugin-node-types` key: `apps/web/src/lib/automation-api.ts:704`. Its GET route requires `iam.ActionWorkflowsRead`: `router.go:590`.
- With `removed = ["tasks"]`, `holdsDataOf(["projects", id, "task-types"], id, ["tasks"])` sees `root = "task-types"`, which is not in the `tasks` roots and does not hit the sprints/views special case, so it returns `false`.
- The `"workflows"` entry matches no query in `apps/web/src` (automation list/detail queries live under `automations`), so that root is inert today.

## Required outcome
- A namespace's role-gated schema/config caches count as that namespace's data, so losing the namespace clears them too (add the missing roots to the respective arrays).

## Open questions for the human
- Whether "namespace data" is intended to include task/automation schema, or only the rows behind each namespace's room.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No new issues in this delta. These commits touch only the e2e helpers and unit tests, plus comments — no production behavior change. The one prior note on NAMESPACE_QUERY_ROOTS completeness is untouched by these commits and still open, so this is not an approval.

Reviewed changes — the delta since the prior pullfrog review (5a57238): realtime test-helper hardening and new hook unit tests.

  • Guarded token minting — mintToken now calls a new assertE2eStack() that refuses to mint unless BASE_URL is loopback (localhost/127.0.0.1/[::1]) or E2E_JWT_SECRET was set explicitly.
  • Tightened event matching — Probe.eventsAbout now matches the id as a whole JSON string value (JSON.stringify(id)) rather than a bare substring, so it cannot match inside another value.
  • Awaitable cleanup — closeAll is now async and awaits each tracked close(); the three socket specs await it in afterEach.
  • Expanded unit tests — new cases for queries outside the lost namespace's roots, the real viewTaskPositionsQueryOptions key matching the predicate, and the gained-namespace refetch path.
  • Comment refreshes — eventsAbout/waitForEvent JSDoc and the realtime hook's header/reset comments now match the scoped-reset behavior.

Pullfrog  | Fix it ➔ | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

@pikann
pikann merged commit 95b432b into master Oct 22, 2026
3 checks passed
@pikann
pikann deleted the feature/add-e2e-tests-for-realtime branch October 22, 2026 03:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant