first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,88 @@
# `runTenantProviderConformance` — reusable conformance harness
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Every concrete `TenantProvider` — the in-memory reference impl
(`api-tenant-provider-memory`, ), the host-DB-backed adapter,
any directory-service adapter — MUST pass the same behavioural suite.
Putting the suite here, importable as `runTenantProviderConformance`,
guarantees that adding a new provider source is a one-line job in the
adapter's test file and that every adapter agrees on the contract's
edge cases (active-only default, includeSuspended, lookup-returns-null,
cursor invalidation, dispose idempotency).
The harness uses `describe` / `it` / `expect` from
`@suaveplan/testing/runner` so it works under both the Vitest default
runner and the Bun-conditional runner (rule 11).
## Features
- Eight tests covering `list` default filter, `includeSuspended`, `count`, `lookup`-returns-record, `lookup`-returns-null, pagination round-trip, invalid-cursor, and dispose idempotency.
- `factory(seed)` callback constructs a fresh provider per test — no shared singleton, no leaked state.
- Compatible with both Vitest and Bun via `@suaveplan/testing/runner`.
- Every `it(...)` carries `{ timeout: 5000 }` per CLAUDE.md.
## Usage: Basic
```ts
// In api-tenant-provider-memory/src/memory.test.ts (Stage 4):
import { runTenantProviderConformance } from "@suaveplan/api-tenant-provider/testing";
import { MemoryTenantProvider } from "./memory.js";
runTenantProviderConformance({
label: "in-memory",
factory: (seed) => new MemoryTenantProvider(seed),
});
```
## Usage: Advanced
```ts
import { runTenantProviderConformance } from "@suaveplan/api-tenant-provider/testing";
import { SurrealTenantProvider } from "./surreal.js";
import { startSurrealdb } from "@suaveplan/testing-integration-surrealdb";
// Spin up a real SurrealDB container per provider instance — the
// factory can be async, so heavyweight fixtures are first-class.
runTenantProviderConformance({
label: "surrealdb",
factory: async (seed) => {
const db = await startSurrealdb();
const provider = new SurrealTenantProvider(db);
for (const record of seed) await provider.put(record);
// Wrap dispose so the container also stops when the test ends.
const originalDispose = provider.dispose.bind(provider);
provider.dispose = async () => {
await originalDispose();
await db.stop();
};
return provider;
},
});
```
## API Reference
- `interface TenantProviderConformanceOptions { readonly label: string; readonly factory: (seed: readonly TenantRecord[]) => Promise<TenantProvider> | TenantProvider }`.
- `function runTenantProviderConformance(options: TenantProviderConformanceOptions): void` — registers a `describe` block with the documented tests.
## Implementation Notes
The harness is intentionally minimal: it does not test telemetry,
metrics, or implementation-specific properties — those belong in the
adapter's own test file. It only pins the cross-adapter contract that
every consumer of `TenantProvider` relies on.
The `factory` callback receives a `readonly TenantRecord[]` seed and
MUST return a provider in which `list({ includeSuspended: true })`
yields exactly the active and suspended subset of `seed` (deterministic
order is not required — assertions normalise via sort).
`dispose` idempotency is tested by calling `dispose()` twice and
asserting the second call resolves. Adapters whose `dispose` reasonably
fails on double-call must reset internal state on the first call to
make the second a no-op.