@beignet/core
Version:
Core framework primitives for Beignet
841 lines • 25.6 kB
TypeScript
import { type MemoryErrorReporterPort } from "../error-reporting/index.js";
import { type MemoryFlagsPort } from "../flags/index.js";
import { type MemoryIdempotencyStore } from "../idempotency/index.js";
import { type MemoryLocksPort } from "../locks/index.js";
import { type MemoryMailerPort } from "../mail/index.js";
import { type MemoryNotificationPort } from "../notifications/index.js";
import { type MemoryOutboxPort } from "../outbox/index.js";
import { type MemoryPaymentsPort } from "../payments/index.js";
import { type ActivityActor, type ActivityTenant, type AuditLogPort, type MemoryAuditLogPort } from "../ports/audit.js";
import type { BestEffortWork, BestEffortWorkPort } from "../ports/best-effort-work.js";
import { type CachePort } from "../ports/cache.js";
import { type ClockPort } from "../ports/clock.js";
import type { EventBusPort, JobDispatcherPort } from "../ports/events.js";
import { type IdGeneratorPort } from "../ports/id-generator.js";
import { type AnyPorts, type BoundGate, type GatePort, type LoggerPort, type RateLimitPort, type StoragePort, type UnitOfWorkPort } from "../ports/index.js";
import { type RecordedEvent, type RecordedJobDispatch } from "../ports/testing.js";
import type { AnyServiceProvider, ProviderSetupResult } from "../providers/provider.js";
import { type MemorySearchPort } from "../search/index.js";
export * from "../ports/testing.js";
/**
* Value that may be returned synchronously or asynchronously.
*/
export type MaybePromise<T> = T | Promise<T>;
/**
* Common Beignet ports created by `createTestPorts(...)`.
*/
export interface CommonTestPorts<TxPorts = AnyPorts> {
[key: string]: unknown;
/**
* Ambient-enriched in-memory audit log. Entries recorded through this port
* inherit actor, tenant, request ID, and trace ID from the active request
* context, matching production server behavior. Use the fixture's `audit`
* for entry assertions.
*/
audit: AuditLogPort;
/**
* In-memory string cache.
*/
cache: CachePort;
/**
* Mutable deterministic test clock.
*/
clock: ClockPort;
/**
* Recording event bus.
*/
eventBus: EventBusPort;
/**
* Recording non-durable best-effort work.
*/
bestEffortWork: BestEffortWorkPort;
/**
* In-memory error reporter.
*/
errorReporter: MemoryErrorReporterPort;
/**
* Authorization gate. Apps with real policies should provide their own gate
* through `overrides`.
*/
gate: GatePort<unknown, []>;
/**
* In-memory feature flags.
*/
flags: MemoryFlagsPort;
/**
* In-memory idempotency store.
*/
idempotency: MemoryIdempotencyStore;
/**
* Sequence or UUID generator used by tests.
*/
ids: IdGeneratorPort;
/**
* Recording job dispatcher.
*/
jobs: JobDispatcherPort;
/**
* No-op logger.
*/
logger: LoggerPort;
/**
* In-memory lease-backed locks.
*/
locks: MemoryLocksPort;
/**
* In-memory mailer.
*/
mailer: MemoryMailerPort;
/**
* In-memory notification port.
*/
notifications: MemoryNotificationPort;
/**
* In-memory outbox.
*/
outbox: MemoryOutboxPort;
/**
* In-memory payments port.
*/
payments: MemoryPaymentsPort;
/**
* In-memory search port.
*/
search: MemorySearchPort;
/**
* In-memory rate limiter.
*/
rateLimit: RateLimitPort;
/**
* In-memory object storage.
*/
storage: StoragePort;
/**
* No-op Unit of Work over the configured transaction ports.
*/
uow: UnitOfWorkPort<TxPorts>;
}
/**
* Captured state and ports returned by `createTestPorts(...)`.
*/
export interface TestPortsFixture<Ports extends AnyPorts = CommonTestPorts, TxPorts = Ports> {
/**
* App ports to pass into a use-case or route test context.
*/
ports: Ports;
/**
* Memory audit port, exposed for assertions.
*/
audit: MemoryAuditLogPort;
/**
* Recording event bus port.
*/
eventBus: EventBusPort;
/**
* Recording best-effort-work port.
*/
bestEffortWork: BestEffortWorkPort;
/**
* Work currently waiting in the recording best-effort-work adapter.
*/
pendingBestEffortWork: BestEffortWork[];
/**
* Run the currently pending best-effort-work batch in FIFO order.
*/
flushBestEffortWork(): Promise<void>;
/**
* Recorded events published through `eventBus`.
*/
events: RecordedEvent[];
/**
* Memory error reporter, exposed for assertions.
*/
errorReporter: MemoryErrorReporterPort;
/**
* Recording job dispatcher port.
*/
jobs: JobDispatcherPort;
/**
* Jobs dispatched through `jobs`.
*/
dispatchedJobs: RecordedJobDispatch[];
/**
* Memory mailer, exposed for delivery assertions.
*/
mailer: MemoryMailerPort;
/**
* Memory notification port, exposed for delivery assertions.
*/
notifications: MemoryNotificationPort;
/**
* Memory outbox, exposed for durable workflow assertions.
*/
outbox: MemoryOutboxPort;
/**
* Memory payments port.
*/
payments: MemoryPaymentsPort;
/**
* Memory storage port.
*/
storage: StoragePort;
/**
* Memory idempotency store.
*/
idempotency: MemoryIdempotencyStore;
/**
* Test cache port.
*/
cache: CachePort;
/**
* Test feature flags port.
*/
flags: MemoryFlagsPort;
/**
* Test rate-limit port.
*/
rateLimit: RateLimitPort;
/**
* Test logger port.
*/
logger: LoggerPort;
/**
* Test locks port.
*/
locks: MemoryLocksPort;
/**
* Test search port.
*/
search: MemorySearchPort;
/**
* Test clock port.
*/
clock: ClockPort;
/**
* Test ID generator.
*/
ids: IdGeneratorPort;
/**
* Unit of Work port installed on `ports`.
*/
uow: UnitOfWorkPort<TxPorts>;
}
/**
* Unit of Work behavior for `createTestPorts(...)`.
*/
export interface CreateTestPortsTransactionOptions<Ports extends AnyPorts, TxPorts> {
/**
* Transaction-scoped ports or a function that derives them from final ports.
*
* Defaults to the final `ports` object.
*/
ports?: TxPorts | ((ports: Ports) => TxPorts);
/**
* Hook run after the transaction callback resolves.
*/
afterCommit?: (tx: TxPorts) => MaybePromise<void>;
/**
* Hook run after the transaction callback throws.
*/
afterRollback?: (error: unknown, tx: TxPorts) => MaybePromise<void>;
/**
* Enqueue `tx.events` (a buffered domain event recorder) to `ports.outbox`
* after the transaction commits, and clear it after a rollback.
*
* Requires `transaction.ports` to include an `events` recorder created by
* `createDomainEventRecorder()`.
*/
outbox?: boolean;
}
/**
* Override shape for one test port.
*
* Object-valued ports may be supplied one level deep as partials: the missing
* members are completed behind a proxy that throws a named error on use.
* Function-valued ports must be supplied whole, so the check for functions
* happens before the object branch.
*/
export type TestPortOverride<Port> = Port extends (...args: never[]) => unknown ? Port : Port extends object ? Partial<Port> : Port;
/**
* Typed partial port overrides accepted by `createTestPorts(...)` and
* `createTestContext(...)`.
*/
export type TestPortsOverrides<Ports extends AnyPorts> = {
[K in keyof Ports]?: TestPortOverride<Ports[K]>;
};
/**
* Options for `createTestPorts(...)`.
*/
export interface CreateTestPortsOptions<Ports extends AnyPorts, TxPorts = Ports> {
/**
* App-owned default ports, usually imported from `infra/port-wiring`. Common
* Beignet test defaults replace matching keys from `base`; use `overrides`
* for app ports that should win.
*
* Values are intentionally loose: boot-time app ports may bind wider types
* (or deferred placeholders) than the provider-contributed runtime ports.
*/
base?: {
[K in keyof Ports]?: unknown;
};
/**
* Test-specific ports that should replace generated defaults or `base`.
*
* Plain-object ports may be supplied as one-level-deep partials; missing
* members throw a named error when called. Function-valued ports, class
* instances, and other exotic objects are passed through whole.
*/
overrides?: TestPortsOverrides<Ports>;
/**
* Clock implementation. Defaults to a frozen clock at the Unix epoch.
*/
clock?: ClockPort;
/**
* ID generator. Defaults to UUIDs.
*/
ids?: IdGeneratorPort;
/**
* Unit of Work configuration. A no-op UOW is installed by default.
*/
transaction?: CreateTestPortsTransactionOptions<Ports, TxPorts>;
}
/**
* Create standard memory/fake Beignet ports for tests.
*
* Use this as the starting point for use-case and route tests, then layer
* app-owned repositories or provider fakes through `base` and `overrides`.
*
* @param options - Optional app ports, overrides, and Unit of Work behavior.
* @returns Ports plus captured state for assertions.
*/
export declare function createTestPorts<Ports extends AnyPorts = CommonTestPorts, TxPorts = Ports>(options?: CreateTestPortsOptions<Ports, TxPorts>): TestPortsFixture<Ports, TxPorts>;
/**
* Bound gate type inferred from a ports object.
*/
export type TestBoundGateForPorts<Ports extends AnyPorts> = Ports extends {
gate?: {
bind: (ctx: never) => infer Bound;
};
} ? Bound : BoundGate<readonly []>;
/**
* Common app context fields built by `createTestContextFactory(...)`.
*/
export type TestContextFields<Ports extends AnyPorts> = {
/**
* Actor under test.
*/
actor: ActivityActor;
/**
* Optional tenant under test.
*/
tenant?: ActivityTenant;
/**
* Stable request ID.
*/
requestId: string;
/**
* Stable trace ID.
*/
traceId?: string;
/**
* Optional auth/session value.
*/
auth?: unknown;
/**
* App ports under test.
*/
ports: Ports;
/**
* Bound authorization gate when `ports.gate` is available.
*/
gate?: TestBoundGateForPorts<Ports>;
};
/**
* Options for `createTestContextFactory(...)`.
*/
export interface CreateTestContextFactoryOptions<Ctx, Ports extends AnyPorts> {
/**
* Ports or a callback returning ports for each created context.
*/
ports: Ports | (() => Ports);
/**
* Actor used by default.
*
* @default createTestUserActor()
*/
actor?: ActivityActor;
/**
* Tenant used by default. Pass `null` to omit tenant context.
*/
tenant?: ActivityTenant | null;
/**
* Request ID used by default.
*/
requestId?: string;
/**
* Trace ID used by default.
*/
traceId?: string;
/**
* Auth/session value added to the context.
*/
auth?: unknown;
/**
* Additional app-specific context fields.
*/
extra?: Partial<Ctx> | ((args: TestContextFields<Ports>) => Partial<Ctx>);
}
/**
* Per-call overrides for a test context factory.
*/
export type TestContextFactoryOverrides<Ctx, Ports extends AnyPorts> = Partial<Pick<CreateTestContextFactoryOptions<Ctx, Ports>, "actor" | "tenant" | "requestId" | "traceId" | "auth" | "extra">> & {
/**
* Ports for this context call.
*/
ports?: Ports;
};
/**
* Create a repeatable app context factory for use-case and route tests.
*
* The factory adds actor, tenant, request ID, trace ID, ports, and auth fields,
* and attaches a live `ctx.gate` automatically when `ports.gate` exposes
* `bind(...)`. The gate is attached after all `extra` and override fields are
* merged, so it always authorizes against the final context identity.
*
* @param options - Default context values.
* @returns A function that creates one app context.
*/
export declare function createTestContextFactory<Ctx extends object, Ports extends AnyPorts>(options: CreateTestContextFactoryOptions<Ctx, Ports>): (overrides?: TestContextFactoryOverrides<Ctx, Ports>) => Ctx;
declare const disposeSymbol: typeof Symbol.dispose;
/**
* Options for `createTestContext(...)`.
*/
export interface CreateTestContextOptions<Ctx, Ports extends AnyPorts> {
/**
* Actor under test.
*
* @default createTestSystemActor("test-system")
*/
actor?: ActivityActor;
/**
* Tenant under test. Pass `null` to omit tenant context.
*
* @default createTestTenant()
*/
tenant?: ActivityTenant | null;
/**
* Request ID exposed on the context.
*
* @default "test-request"
*/
requestId?: string;
/**
* Trace ID exposed on the context.
*
* @default "test-trace"
*/
traceId?: string;
/**
* Auth/session value added to the context.
*
* @default null
*/
auth?: unknown;
/**
* App-owned default ports, usually imported from `infra/port-wiring`. Common
* Beignet test defaults replace matching keys from `base`; use `ports` for
* app ports that should win.
*/
base?: {
[K in keyof Ports]?: unknown;
};
/**
* Typed partial port overrides that replace generated defaults or `base`.
*/
ports?: TestPortsOverrides<Ports>;
/**
* Clock implementation. Defaults to a frozen clock at the Unix epoch.
*/
clock?: ClockPort;
/**
* ID generator. Defaults to UUIDs.
*/
ids?: IdGeneratorPort;
/**
* Unit of Work configuration. A no-op UOW is installed by default.
*/
transaction?: CreateTestPortsTransactionOptions<Ports, unknown>;
/**
* Additional app-specific context fields. An explicit `gate` here wins over
* the kit-attached gate.
*/
extra?: Partial<Ctx>;
/**
* Enter the ambient request context with the test actor, tenant, request
* ID, and trace ID so ambient enrichment (such as the default audit port)
* works like production. Call `dispose()` (or use `using`) to clear it.
*
* @default true
*/
ambient?: boolean;
}
/**
* Test context fixture returned by `createTestContext(...)`.
*/
export interface TestContextFixture<Ctx, Ports extends AnyPorts> extends TestPortsFixture<Ports, unknown> {
/**
* Assembled app context under test.
*/
ctx: Ctx;
/**
* Clear the ambient request context entered by this fixture.
*/
dispose(): void;
/**
* Explicit-resource-management alias for `dispose()`, so fixtures work with
* `using`.
*/
[disposeSymbol](): void;
}
/**
* Create a one-call test context fixture for jobs, listeners, schedules,
* notifications, payments, tasks, and use-case tests.
*
* The fixture builds common memory ports through `createTestPorts(...)`,
* assembles an app context with actor, tenant, request ID, trace ID, auth,
* and a live bound gate, and enters the ambient request context so ambient
* enrichment matches production. Reading an app port that is neither a kit
* default nor supplied throws a named error on use.
*
* @example
* ```ts
* const makeContext = createTestContext<AppContext>();
*
* using fixture = makeContext({
* ports: { issues: { create: async (input) => issueRecord(input) } },
* });
* await runCreateIssue(fixture.ctx);
* ```
*
* @returns A factory that creates one disposable test context fixture.
*/
export declare function createTestContext<Ctx extends {
ports: AnyPorts;
}>(): (options?: CreateTestContextOptions<Ctx, Ctx["ports"]>) => TestContextFixture<Ctx, Ctx["ports"]>;
/**
* Options for `installProviderForTest(...)`.
*/
export interface InstallProviderForTestOptions {
/**
* Environment variables used to resolve the provider's config through the
* same loader the server uses (including the provider's `overrides`).
* Ignored when `config` is passed explicitly.
*/
env?: Record<string, string | undefined>;
/**
* Base app ports visible to provider setup, such as `{ devtools }`.
*/
ports?: Record<string, unknown>;
/**
* Config passed to provider setup as-is. The provider config schema is not
* run, matching server startup where config is validated before setup.
*/
config?: unknown;
/**
* Service-context factory passed to setup and lifecycle hooks. The default
* factory rejects, mirroring the late-bound runtime factory before all
* providers have started.
*/
createServiceContext?: (input?: unknown) => Promise<unknown>;
}
/**
* Installed provider fixture returned by `installProviderForTest(...)`.
*/
export interface InstalledProviderFixture {
/**
* Base ports merged with the ports contributed by provider setup.
*/
ports: Record<string, unknown>;
/**
* Raw provider setup result, exposed for assertions on contributed ports
* and optional lifecycle hooks.
*/
result: ProviderSetupResult<Record<string, unknown>, Record<string, unknown>>;
/**
* Run the provider `start` hook when the provider declares one.
*/
start(): Promise<void>;
/**
* Run the provider `stop` hook when the provider declares one.
*/
stop(): Promise<void>;
}
/**
* Run provider setup against test ports and return the merged ports plus
* lifecycle runners.
*
* Use this in provider tests instead of hand-rolling the setup, port merge,
* and lifecycle plumbing around `provider.setup(...)`.
*
* @param provider - Provider under test.
* @param options - Base ports, raw config, and service-context factory.
* @returns Merged ports, the raw setup result, and start/stop runners.
*/
export declare function installProviderForTest(provider: AnyServiceProvider, options?: InstallProviderForTestOptions): Promise<InstalledProviderFixture>;
/**
* Arguments passed to a factory default builder.
*/
export interface FactoryBuildArgs<Name extends string = string> {
/**
* Factory name.
*/
readonly name: Name;
/**
* Current sequence value.
*/
readonly sequence: number;
/**
* ID generator available to the factory.
*/
readonly ids: IdGeneratorPort;
/**
* Clock available to the factory.
*/
readonly clock: ClockPort;
}
/**
* Arguments passed to a factory persistence function.
*/
export type FactoryPersistArgs<Name extends string = string> = FactoryBuildArgs<Name>;
/**
* Overrides accepted by factory build and create methods.
*/
export type FactoryOverrides<Value extends object, Name extends string = string> = Partial<Value> | ((value: Value, args: FactoryBuildArgs<Name>) => Partial<Value>);
/**
* Options for declaring a test data factory.
*/
export interface CreateFactoryOptions<Name extends string, Value extends object, Ctx = unknown, Created = Value> {
/**
* Build the default value for the current sequence.
*/
defaults(args: FactoryBuildArgs<Name>): Value;
/**
* Persist a built value and return the created record.
*/
persist?(ctx: Ctx, value: Value, args: FactoryPersistArgs<Name>): MaybePromise<Created>;
/**
* ID generator to expose to the factory.
*/
ids?: IdGeneratorPort;
/**
* Clock to expose to the factory.
*/
clock?: ClockPort;
/**
* Initial sequence number. Defaults to `1`.
*/
start?: number;
}
/**
* Declared test data factory.
*/
export interface FactoryDef<Name extends string = string, Value extends object = object, Ctx = unknown, Created = Value> {
/**
* Discriminator for factory definitions.
*/
readonly kind: "factory";
/**
* Factory name.
*/
readonly name: Name;
/**
* Build an in-memory value.
*/
build(overrides?: FactoryOverrides<Value, Name>): Value;
/**
* Build multiple in-memory values.
*/
buildList(count: number, overrides?: FactoryOverrides<Value, Name>): Value[];
/**
* Build and persist a value.
*/
create(ctx: Ctx, overrides?: FactoryOverrides<Value, Name>): Promise<Created>;
/**
* Build and persist multiple values.
*/
createList(ctx: Ctx, count: number, overrides?: FactoryOverrides<Value, Name>): Promise<Created[]>;
/**
* Reset the factory sequence.
*/
resetSequence(next?: number): void;
}
/**
* Options for declaring a seed.
*/
export interface DefineSeedOptions<Ctx> {
/**
* Optional human-readable seed description.
*/
description?: string;
/**
* Execute the seed.
*/
run(ctx: Ctx): MaybePromise<void>;
}
/**
* Declared seed that can be run with `runSeeds(...)`.
*/
export interface SeedDef<Ctx = unknown, Name extends string = string> {
/**
* Discriminator for seed definitions.
*/
readonly kind: "seed";
/**
* Seed name.
*/
readonly name: Name;
/**
* Optional human-readable seed description.
*/
readonly description?: string;
/**
* Execute the seed.
*/
run(ctx: Ctx): MaybePromise<void>;
}
/**
* Options for running a list of seeds.
*/
export interface RunSeedsOptions<Ctx> {
/**
* Context passed to every seed.
*/
ctx: Ctx;
/**
* Seeds to run in order.
*/
seeds: readonly SeedDef<Ctx>[];
}
/**
* Options for creating a database test harness.
*/
export interface CreateDatabaseTestHarnessOptions<Database, Ctx> {
/**
* Create an isolated app-owned database fixture.
*/
create(): MaybePromise<Database>;
/**
* Build the context passed to factories and seeds from the database fixture.
*/
ctx(database: Database): Ctx;
/**
* Reset the database fixture, when supported by the app.
*/
reset?(database: Database): MaybePromise<void>;
/**
* Close and dispose the database fixture.
*/
close?(database: Database): MaybePromise<void>;
/**
* Factories whose sequences should reset before each setup and reset.
*/
factories?: readonly Pick<FactoryDef, "resetSequence">[];
/**
* Default seeds available to `setup({ seed: true })` and `session.runSeeds()`.
*/
seeds?: readonly SeedDef<Ctx>[];
}
/**
* Options for setting up one database test session.
*/
export interface DatabaseTestSetupOptions<Ctx> {
/**
* Run seeds after creating the database session.
*
* Pass `true` to use the harness default seeds, or pass an explicit seed
* list for this setup.
*/
seed?: boolean | readonly SeedDef<Ctx>[];
}
/**
* Active database fixture created by `createDatabaseTestHarness(...)`.
*/
export interface DatabaseTestSession<Database, Ctx> {
/**
* App-owned database fixture returned by the harness `create` function.
*/
readonly database: Database;
/**
* Context built from the database fixture for factories and seeds.
*/
readonly ctx: Ctx;
/**
* Run the default harness seeds or an explicit seed list.
*/
runSeeds(seeds?: readonly SeedDef<Ctx>[]): Promise<void>;
/**
* Reset factory sequences and the database fixture.
*/
reset(): Promise<void>;
/**
* Close and dispose this session.
*/
close(): Promise<void>;
}
/**
* Database test harness that coordinates app-owned database fixtures with
* Beignet factories and seeds.
*/
export interface DatabaseTestHarness<Database, Ctx> {
/**
* Create one isolated database test session.
*/
setup(options?: DatabaseTestSetupOptions<Ctx>): Promise<DatabaseTestSession<Database, Ctx>>;
/**
* Reset all configured factory sequences.
*/
resetFactories(): void;
/**
* Close all sessions that have not already been closed.
*/
cleanup(): Promise<void>;
}
/**
* Error thrown when a seed fails.
*/
export declare class SeedRunError extends Error {
/**
* Seed that failed.
*/
readonly seed: SeedDef;
/**
* Original thrown value.
*/
readonly cause: unknown;
constructor(seed: SeedDef, cause: unknown);
}
/**
* Reset one or more factory sequences to their configured starting values.
*/
export declare function resetFactories(...factories: readonly Pick<FactoryDef, "resetSequence">[]): void;
/**
* Create a typed test data factory with deterministic sequence support.
*/
export declare function createFactory<const Name extends string, Value extends object, Ctx = unknown, Created = Value>(name: Name, options: CreateFactoryOptions<Name, Value, Ctx, Created>): FactoryDef<Name, Value, Ctx, Created>;
/**
* Define a named seed.
*/
export declare function defineSeed<const Name extends string, Ctx = unknown>(name: Name, options: DefineSeedOptions<Ctx>): SeedDef<Ctx, Name>;
/**
* Run seeds in order and wrap failures with the seed name.
*/
export declare function runSeeds<Ctx>(options: RunSeedsOptions<Ctx>): Promise<void>;
/**
* Create a database test harness for app-owned database fixtures.
*
* The harness does not know about an ORM or database provider. Apps provide
* creation, reset, close, and context mapping functions, while Beignet handles
* the repetitive test lifecycle around factory sequences and seed execution.
*/
export declare function createDatabaseTestHarness<Database, Ctx>(options: CreateDatabaseTestHarnessOptions<Database, Ctx>): DatabaseTestHarness<Database, Ctx>;
//# sourceMappingURL=index.d.ts.map