Skip to content

moooon

Motir

Vibe your whole project. Bring an idea — Motir's three AI layers plan it, track it, and ship it, end to end. You're looking at Motir, built in Motir.

  • Vibe Project
  • Open Source
  • AI Agent
  • AI Loop
1
requests
0
upvotes
145
planned
1,361
shipped

Motir · Work items

MOTIR-2264Done

Story test gate (Vitest) — coverage floor, the writer→consumer seams, and the architecture guards the coverage number cannot see

The story-level Vitest gate for this story, run after its feature cards merge, against their real merged surface. It does three things the per-card unit tests structurally cannot.

⚠️ RE-SCOPED 2026-08-07. This card was authored as the gate for the model story before the page was re-sliced into its own story, so its changed-surface list still named that story's files — lib/permissions/*, lib/projects/access.ts, lib/dto/permissions.ts — most of which shipped and are done. The gate measures the surface of the story it belongs to; that surface is now the two settings screens plus the widened read. The capability-agreement matrix and the architecture guards survive unchanged — they were always about the whole permission seam, and they remain the most valuable thing here. See the story's re-plan note.

1 · Coverage floor. Run coverage over THIS story's changed surface — the widened permissionsByDomain arm, lib/dto/permissions.ts, lib/mappers/permissionMappers.ts, the new projectMembershipRepository grouped count, getRoleCatalog, and the two settings screens with their components — and write the missing unit and branch tests wherever a file sits below the project's ≥90% per-file floor. Each card's own tests are the floor; this tops up the seams between them.

2 · Integration seams — drive real output through the real consumer. The per-card tests each mock the other side; these do not:

  • The widened RoleCatalogDTO, built from real ProjectMembership rows in Postgres, read back by the screens' own consumer — the key drift the unit tests mask on both sides, and the member count is the field most exposed to it because it is the only one that crosses from a different table.
  • resolvePermissions' output set, taken from real memberships, threaded through the DTO mapper and read back by the detail screen's consumer.
  • Every one of the five existing getXCapabilities methods asserted against getPermissions on the SAME actor and project: for each boolean they return, the corresponding permission key must be present in the set exactly when the boolean is true. This is the seam where the general read and the five specialised ones could silently disagree, and nothing else compares them.
  • The public-project path end to end with a genuinely anonymous actor — no session, no workspace membership — resolved through the service, not the pure layer.

3 · Architecture and contract guards, which a coverage percentage cannot see.

  • The screens draw the role-gated set, not the catalog: the DTO the settings screens consume contains exactly ROLE_GATED_PERMISSIONS, asserted against that constant rather than a literal count, so the guard stays true when the set grows from 20 keys to 28.
  • No component re-derives the model: nothing under the settings screens imports lib/permissions/catalog or builtinRoles directly — the screens read the service's DTO, so custom roles change one read rather than every component.
  • The catalog has no orphans: every PermissionKey marked enforced is referenced by at least one production consumer outside lib/permissions/* and outside tests/ — a key nothing enforces is exactly the lie the model exists to prevent.
  • No enforcement outside the seam: no module in lib/services or app decides access by comparing a role string directly (role === 'admin' and friends) instead of going through the predicates — an inline comparison is a policy that the catalog cannot see and a custom role cannot change.
  • The pure layer stays pure: lib/permissions/* and lib/projects/access.ts import no Prisma client and perform no IO.
  • Both i18n catalogs stay total over the role-gated permission keys.

Real Postgres, not mocks, for everything in section 2.

Acceptance criteria

  • Every file in this story's changed surface meets the ≥90% per-file branch/function/line floor, and the run that proves it is over the merged surface of this story's cards, not a single card's diff.
  • The role-gated guard asserts the consumed DTO's key set against ROLE_GATED_PERMISSIONS itself, so it neither hardcodes 20 nor hardcodes 28.
  • The capability-agreement matrix covers all five existing getXCapabilities methods against getPermissions, for each of admin / member / viewer / non-member / workspace-owner, on each of the four access levels.
  • The member-count seam is proven against real rows: seeded memberships in, the same numbers out of getRoleCatalog, and a role with no members resolves to 0.
  • The anonymous public-project case passes through the service layer with a null actor.
  • The architecture guards above each fail loudly when deliberately violated — each guard's test includes the negative case that proves it can fail.
  • The suite runs against real Postgres and is green in CI.

Context refs

  • motir-core/CLAUDE.md § coverage — the ≥90% per-file floor this gate enforces.
  • tests/settings/settings-area-access-matrix.test.ts — the shipped precedent for an access-matrix test in this repo.
  • lib/services/projectAccessService.ts — the five capability methods the agreement matrix compares against, and getRoleCatalog.
  • lib/permissions/builtinRoles.tsROLE_GATED_PERMISSIONS, the constant the row-set guard asserts against.
  • The widened read · the screens — the cards whose merged surface this gate measures.
  • The model story — done; its files are the CONTEXT this gate guards, not the surface it measures.