1.4.8 Story-level E2E: cross-project work-item isolation + dependency scenarios (closes the Story)
Estimate: 16m · Depends on: 1.4.5, 1.4.7
The Story-closing E2E test that proves Story 1.4's structural invariants hold end-to-end across realistic multi-workspace, multi-project scenarios. Same shape as Story 1.2.7 (workspace isolation) and Story 1.3.6 (project isolation) — a Playwright (or Vitest-driven HTTP) test that exercises the data layer via a thin throwaway test endpoint (since Epic 2 hasn't shipped the production routes yet). The test endpoint lives under app/api/_test/work-items/ and is gated by a build-time NODE_ENV !== 'production' flag.
Scenario: User A in workspace W1 with projects P1 + P2; User B in workspace W2 with project P3. Each creates 3 work items in each of their projects. Then:
- A queries P1's list — sees exactly 3 items.
- A queries P2's list — sees exactly 3 items.
- A tries to query P3 by ID — gets 404 (NOT 403 — leaking existence is the bug 1.2.7 caught).
- A tries to update one of B's items by ID — 404.
- A tries to create a work item with P3's projectId — server rejects with NotInSameProjectError before the DB even sees it (service-layer assertion); if A could bypass the service, the RLS WITH CHECK clause would also reject.
- A creates an epic in P1, adds a story under it, adds a subtask under the story (3-level chain). Verifies the subtree query returns the full tree.
- A archives the epic — children remain visible at top level (Linear-shape choice from 1.4.4 documented).
- A's revision feed for an updated item shows the changes; B cannot read those revisions.
- Dependency scenario: A creates issues X, Y, Z in P1. Links X is_blocked_by Y; links X is_blocked_by Z.
getBlockers(X)returns [Y, Z];getBlocking(Y)returns [X];isReady(X)returns false. Marks Y status=done; isReady(X) still false. Marks Z status=done; isReady(X) returns true. Unlinks X→Y (the link, not the item); isReady(X) stays true (only Z remained, already done). - Cross-project dependency scenario: A creates issue X in P1 and Y in P2 (same workspace W1). Links X is_blocked_by Y — succeeds. getBlockers(X) returns Y even when called under P1 project-context. B cannot see the link at all (cross-workspace gate).
- Link cycle prevention E2E: A creates X is_blocked_by Y; attempts Y is_blocked_by X via the HTTP endpoint — gets a 4xx with WI_LINK_CYCLE in the error body.
- Symmetric link UI contract: A links X relates_to Y; from the issue-detail GET on both X and Y, the "Related issues" field surfaces the counterpart (one logical link, two row writes, both endpoints render symmetrically).
- Explanation-source state machine E2E: create an issue via POST (no explanation); GET confirms
explanationMd = null,explanationSource = user_authored. PATCH with{ explanationMd: "…", explanationSource: "ai_draft" }(simulating what Epic 7's AI-drafting endpoint will do); GET confirms both. PATCH with{ explanationMd: "…edited" }(no explicit source); GET confirms source auto-transitioned touser_edited. Revision feed shows three rows in order with the right diffs (including the source transition on the third update). - Markdown rendering smoke: create an issue with
descriptionMdcontaining a fenced code block, a GFM table, and an inline<script>tag. GET-with-render returns sanitized HTML — script stripped, table rendered, code block with syntax-highlighting markup.
What you'll do: Add the test endpoint app/api/_test/work-items/route.ts (POST = createWorkItem, GET = list/get, PATCH = update, DELETE = archive) AND app/api/_test/work-item-links/route.ts (POST = linkWorkItems, DELETE = unlinkWorkItems, GET with ?workItemId=&direction=blockers|blocking wraps getBlockers / getBlocking; GET with ?workItemId=&ready=1 wraps isReady). Wire both through the service layer (NOT raw Prisma) so the tests exercise the production code path. Add the Playwright test tests/e2e/work-items-isolation.spec.ts following the 1.2.7 / 1.3.6 pattern. CI runs E2E against the docker-compose'd Postgres + a built Next server.
Acceptance criteria
- Test endpoints
app/api/_test/work-items/route.tsANDapp/api/_test/work-item-links/route.tsexpose CRUD + linking via the service layer; both gated behindNODE_ENV !== 'production'(return 404 in production builds). - E2E spec
tests/e2e/work-items-isolation.spec.tscovers every bullet from the scenario list above, including the dependency / cross-project link / cycle scenarios. - Cross-workspace queries return 404 (not 403), preserving the "no existence leak" contract from 1.2.7.
- Cross-project queries within the same workspace return 404 too (project RLS narrowing).
- The Story-level verification recipe (below) reproduces the scenario locally in <10 minutes.
- All quality gates green; CI green; existing E2E suite stays green.
Context refs
tests/e2e/workspaces-isolation.spec.tsfrom 1.2.7 — the exact patterntests/e2e/projects-isolation.spec.tsfrom 1.3.6 — the second pattern instanceapp/api/_test/*existing test endpoints from 1.2.7 / 1.3.6 — the gating conventionlib/services/workItemsService.ts— the service the route delegates to- This Story page — the scenario specification