5.3 KiB
Capability Spec Delta: api-tenant-provider (api-tenant-provider)
Package: @suaveplan/api-tenant-provider
Status: PROPOSED
Every requirement carries a
**Verified by:**line.
ADDED Requirements
Requirement: TenantProvider contract
The package SHALL export TenantProvider: list(opts?: ListOpts): AsyncIterable<TenantRecord>, count(): Promise<number>, lookup(tenantId: TenantId): Promise<TenantRecord | null>, dispose(): Promise<void>. lookup MUST return null (not throw) when the tenant is not found.
Scenario: every method present
- WHEN
expectTypeOf<TenantProvider>()is checked - THEN all four methods are declared
Scenario: lookup returns null for missing tenant
- WHEN a stub provider with no tenants is given an arbitrary id
- THEN
lookup(id)resolves tonull
Verified by: src/provider/provider.spec-conformance.test.ts::"TenantProvider declares list count lookup dispose" AND src/provider/provider.test.ts::"lookup returns null for missing tenant"
Requirement: import z from "zod" everywhere
Every Zod import MUST use the default form.
Scenario: no named import of z
- WHEN the test greps
import zfrom"zod" - THEN zero matches
Verified by: tests/zod-import-form.test.ts::"every src file uses default-import z"
Requirement: TenantRecord schema
The package SHALL export tenantRecordSchema: z.ZodSchema<TenantRecord> describing { id: TenantId, displayName?: string, status: "active" | "suspended" | "archived", createdAt: string }. id MUST be branded as TenantId (not plain string). Unknown keys MUST be rejected.
Scenario: minimal record parses
- WHEN
{ id: "t1", status: "active", createdAt: "2026-05-13T..." }is parsed - THEN it succeeds AND
idis typed asTenantId
Scenario: unknown status rejected
- WHEN
{ id: "t1", status: "weird", createdAt: "..." }is parsed - THEN Zod reports an issue at
status
Verified by: src/tenant-record/tenant-record.test.ts::"minimal record parses" AND src/tenant-record/tenant-record.test.ts::"unknown status rejected"
Requirement: Status filter — active-by-default
list(opts) MUST default to enumerating only tenants whose status === "active". list({ includeSuspended: true }) MUST also enumerate suspended tenants. archived tenants are NEVER enumerated by list — they are only reachable via lookup.
Scenario: default excludes suspended and archived
- WHEN the provider holds [active, suspended, archived] AND
list()is consumed - THEN only the active tenant is yielded
Scenario: includeSuspended surfaces suspended
- WHEN
list({ includeSuspended: true })is consumed - THEN both active and suspended tenants are yielded; archived is NOT yielded
Verified by: src/provider/status-filter.test.ts::"default list excludes suspended and archived" AND src/provider/status-filter.test.ts::"includeSuspended surfaces suspended but not archived"
Requirement: Pagination cursor round-trip
ListOpts.cursor MUST round-trip — consuming part of list({ pageSize: 10 }), capturing a cursor, and resuming with list({ pageSize: 10, cursor }) MUST yield the remainder without duplication or omission. Invalid cursors MUST throw TENANT_PROVIDER.INVALID_CURSOR.
Scenario: cursor resumes without gap or overlap
- WHEN 25 tenants exist AND the caller pulls 10 then resumes with the cursor
- THEN the resumption yields exactly 15 tenants AND no duplicates
Scenario: invalid cursor rejected
- WHEN an arbitrary string is passed as cursor
- THEN
TENANT_PROVIDER.INVALID_CURSORis thrown
Verified by: src/pagination/pagination.test.ts::"cursor resumes without gap or overlap" AND src/pagination/pagination.test.ts::"invalid cursor rejected"
Requirement: CapabilityId augmentation contributes "tenant.provider"
The package SHALL augment the CapabilityId union to include "tenant.provider".
Scenario: tenant.provider assignable to CapabilityId
- WHEN
expectTypeOf<"tenant.provider">().toMatchTypeOf<CapabilityId>()is checked - THEN the assertion passes
Verified by: src/capability-id/capability-id.spec-conformance.test.ts::"tenant.provider is a valid CapabilityId after augmentation"
Requirement: Error codes registry frozen
The package SHALL export TENANT_PROVIDER_CODES containing exactly: TENANT_NOT_FOUND, SOURCE_UNAVAILABLE, INVALID_CURSOR.
Scenario: every code reachable
- WHEN the test enumerates
TENANT_PROVIDER_CODES - THEN every code has a production throw site
Verified by: src/error-codes/error-codes.spec-conformance.test.ts::"every TENANT_PROVIDER_CODES entry thrown by production path"
Requirement: Conformance harness for providers
The package SHALL export runTenantProviderConformance({ factory, label }). Every concrete provider (in-memory in Stage 4, DB-backed downstream) MUST pass it.
Scenario: harness covers list, count, lookup, status filter, pagination, dispose
- WHEN the harness runs against a stub provider
- THEN test categories exist for each documented behavior
Verified by: src/testing/conformance.test.ts::"harness covers list count lookup status-filter pagination dispose"