MotirBuilding in public
MOTIR · moooon
onMotir
You’re viewing a public project. Anyone can view it — no account needed. Sign in to submit, upvote, or comment on requests.View-only — you can’t edit work items
MOTIR-51

1.4.8 Story-level E2E: cross-project work-item isolation + dependency scenarios (closes the Story)

Done
Description

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 to user_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 descriptionMd containing 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.ts AND app/api/_test/work-item-links/route.ts expose CRUD + linking via the service layer; both gated behind NODE_ENV !== 'production' (return 404 in production builds).
  • E2E spec tests/e2e/work-items-isolation.spec.ts covers 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.ts from 1.2.7 — the exact pattern
  • tests/e2e/projects-isolation.spec.ts from 1.3.6 — the second pattern instance
  • app/api/_test/* existing test endpoints from 1.2.7 / 1.3.6 — the gating convention
  • lib/services/workItemsService.ts — the service the route delegates to
  • This Story page — the scenario specification