import { AppContext } from '@voltro/runtime'; import { AuthStrategy } from '@voltro/protocol'; import { changeDelete } from '@voltro/database'; import { ChangeEvent } from '@voltro/database'; import { changeInsert } from '@voltro/database'; import { changeSoftDelete } from '@voltro/database'; import { changeUpdate } from '@voltro/database'; import { Effect } from 'effect'; import { EventBus } from '@voltro/runtime'; import { EventDescriptor } from '@voltro/protocol'; import { EventPublisher } from '@voltro/runtime'; import { InspectedStep } from '@voltro/workflow'; import { InspectedWorkflow } from '@voltro/workflow'; import { Layer } from 'effect'; import { ParseResult } from 'effect'; import { ProcedureDescriptor } from '@voltro/protocol'; import { PublicApiDescriptor } from '@voltro/protocol/rest'; import { PublicApiProfileConfig } from '@voltro/protocol'; import { PublicApiSpec } from '@voltro/protocol'; import { RelationsSpec } from '@voltro/database'; import { RestRouteDescriptor } from '@voltro/protocol/rest'; import { RestServeBindings } from '@voltro/protocol/rest'; import { Row } from '@voltro/database'; import { RowFilter } from '@voltro/runtime'; import { RpcConnection } from '@voltro/runtime'; import { RpcInterceptor } from '@voltro/protocol'; import { RpcKind } from '@voltro/protocol'; import { Schema } from 'effect'; import { Scope } from 'effect'; import { Subject } from '@voltro/protocol'; import { SubscribeContext } from '@voltro/runtime'; import { SyncLogger } from '@voltro/logger'; import { TableLike } from '@voltro/database'; import { VoltroPlugin } from '@voltro/protocol'; /** An unauthenticated caller, optionally carrying a tenant (what the * `x-tenant` header resolves to when no auth strategy matches). Delegates to * the protocol's own constructor so the harness cannot drift from it. */ export declare const anonymous: (tenantId?: string | null) => Subject; /** An API-key subject — what a `Authorization: Bearer ` call resolves to. * Same shape as `user()`; the `type` is what an app's own branching reads. */ export declare const apiKey: (id: string, options?: TestSubjectOptions) => Subject; export { changeDelete } export { ChangeEvent } export { changeInsert } export { changeSoftDelete } export { changeUpdate } /** * A named factory for `table`. * * ```ts * const users = defineFactory(usersTable, { * defaults: { email: (seq) => `user-${seq}@test.local`, role: 'member' }, * traits: { admin: { role: 'admin' } }, * }) * const posts = defineFactory(postsTable, { associations: { authorId: users } }) * * const admin = await users.with('admin').create(ctx.store) * const post = await posts.create(ctx.store, { authorId: admin.id }) // no extra user * const orphanFree = await posts.create(ctx.store) // an author IS created * ``` */ export declare const defineFactory: (table: TableLike, options?: DefineFactoryOptions) => Factory; export declare interface DefineFactoryOptions { /** Column defaults applied before the caller's per-call overrides. */ readonly defaults?: FactoryDefaults; /** Named override bundles, selected with `.with('name')`. */ readonly traits?: Readonly>; /** * Factories for this table's ancestors, keyed by the REFERENCE COLUMN name. * `create()` uses one when it has to build a parent, so the parent gets its * own defaults and its own ancestors rather than a bare `fixtureRow`. * * ```ts * const posts = defineFactory(postsTable, { * associations: { authorId: users }, // `users` is another factory * }) * ``` * * Omitted for a column → the parent is built with a plain `fixtureRow` (plus * ITS own required ancestors, recursively). That is enough for a parent * nothing asserts on, and the escape hatch when it is not is to pass the id * explicitly, which always wins over both. */ readonly associations?: Readonly>; } /** * `describe` a suite whose dependency is checked by ITS OWN probe. * * The general form. A suite may need two services up (a primary AND its * replica), a docker daemon, or a redis in cluster mode — conditions a * host/port pair cannot express. It keeps whatever probe the suite already had * and only changes what an unmet condition PRODUCES: a vitest skip instead of a * test body that returns early and reports as passed. * * `dependency` is what must be up, in words, and it goes in the skip label — so * a skipped line in the reporter says what was missing rather than leaving you * to open the file. */ export declare const describeIfAvailable: (label: string, dependency: string, probe: () => boolean | Promise, suite: () => void) => Promise; /** * `describe` a suite that needs a live service, skipped (not passed) when the * service is not there. * * Await it at module scope — vitest test files support top-level await, and the * probe has to resolve before `describe.skipIf` is called: * * await describeIfReachable('pg introspection — live', PG, () => { * test('…', async () => { … }) * }) * * The suite name carries the target, so a skipped line in the reporter names * what was missing instead of leaving you to guess. */ export declare const describeIfReachable: (label: string, target: ReachableTarget, suite: () => void) => Promise; /** One recorded emission. `event` is the descriptor's id when a descriptor was * passed, so an assertion reads the same string the handler wrote. */ export declare interface EmittedWebhook { readonly event: string; readonly payload: unknown; readonly emittedAt: Date; } /** Effect shape the runner erases a workflow's success/error/requirement * channels down to. `unknown` for value/error (covariant — anything * assigns), `never` for the requirement (the runner provides the engine * layer itself). Callers pass concrete workflows cast via `as never` — * `never` assigns to any field type, so the object-literal form stays * ergonomic without an `any` in the public type. */ declare type ErasedWorkflowEffect = Effect.Effect; export declare interface Factory { /** The table this factory builds rows for. */ readonly table: TableLike; /** A complete row — defaults, then traits, then `overrides`, then * `fixtureRow` for whatever is still required. Pure: writes nothing, and * therefore creates NO parent rows (a `reference()` column with no override * gets `fixtureRow`'s placeholder). Use `create` when the relations matter. */ readonly build: (overrides?: Row) => Row; /** `count` built rows. `overrides` may be a function of the index when the * rows must differ. */ readonly buildList: (count: number, overrides?: Row | ((index: number) => Row)) => ReadonlyArray; /** * Insert a row AND every ancestor it requires, in dependency order, and * return the inserted row (the store's, so auto-stamped ids/audit columns are * on it). * * An ancestor is created only for a required `reference()` column the merged * overrides leave unset — pass `{ authorId: existing.id }` and nothing extra * is written. */ readonly create: (store: FactoryStore, overrides?: Row) => Promise; /** `count` created rows. Each gets its OWN ancestors unless the override * pins them, which is usually what a list test wants — pass the parent id in * `overrides` to share one. */ readonly createList: (store: FactoryStore, count: number, overrides?: Row | ((index: number) => Row)) => Promise>; /** A derived factory with these traits' defaults merged in, in the order * named (later wins). Throws for a trait this factory does not declare — * a mistyped trait name silently building the BASE row is the failure mode * this rules out. */ readonly with: (...traits: ReadonlyArray) => Factory; /** A derived factory with extra defaults merged in — the ad-hoc form of a * trait, for a one-off variation not worth naming. */ readonly extend: (defaults: FactoryDefaults) => Factory; } /** Column → default. Every key is optional; anything omitted falls through to * `fixtureRow`'s placeholder or, for a `reference()`, to an association. */ export declare type FactoryDefaults = Readonly>; /** The narrow slice of a store a factory writes through — `ctx.store` and any * transactional view of it satisfy it structurally. Typed as a shape rather * than as `DataStore` so a factory can also be handed the mixin-wrapped store * a handler sees, which is the one a test actually holds. */ export declare interface FactoryStore { insert(table: string, row: Row): Promise; } /** * One column's default: a function handed a monotonic sequence number * (`(seq) => \`user-${seq}@test.local\``) for a unique column, or a plain value. * * The arms are spelled out rather than written `unknown | ((seq: number) => …)`, * which is the obvious version and is silently WRONG: `unknown` absorbs every * other member of a union, so that type collapses to `unknown`, the function * arm disappears, and a caller writing `(seq) => …` gets `seq: any` with no * contextual type at all. The function arm is FIRST so it wins contextual * typing for an arrow literal. */ export declare type FactoryValue = ((sequence: number) => unknown) | string | number | boolean | bigint | Date | null | undefined | ReadonlyArray | Readonly>; /** * Complete a partial row for `table` so it satisfies the required-column insert * validation. Fills every NOT-NULL, no-default, non-auto-stamped column that * `overrides` doesn't already supply, then merges `overrides` on top (an explicit * value — including `null` — always wins). Returns a plain row for the loose * `store.insert(name, row)` path. */ export declare const fixtureRow: (table: TableLike, overrides?: Row) => Row; /** * The Effect-native form: global time is frozen for the SCOPE's lifetime and * restored on release — on success, on failure, and on interruption. * * ```ts * Effect.scoped(Effect.gen(function* () { * const clock = yield* frozenTime('2026-03-01T12:00:00Z') * yield* handler // every `new Date()` inside reads the mock * clock.advance('1d') * })) * ``` */ export declare const frozenTime: (at: Date | number | string | MockClock) => Effect.Effect; /** * What `invoke` resolves to for a handler returning `Result`: an `Effect`'s * SUCCESS value, an awaited `Promise`, or the value itself. * * Inferring from the executor's whole return type — rather than making `Output` * a naked type parameter that also appears inside `Effect` — is what * keeps `invoke(d, () => Effect.succeed('x'), …)` resolving to `string` instead * of to `Effect`: with two inference sites for one parameter TypeScript * would pick the bare candidate and hand the caller back the un-run Effect in * the TYPE as well as at runtime. */ export declare type HandlerOutput = Result extends Effect.Effect ? A : Awaited; /** * Run `executor` as the dispatcher would: enforce the descriptor's `guards:` * against `ctx.request.subject`, decode `rawInput` through the descriptor's * `input` Schema, then call the handler with the decoded value — and, when * the descriptor is a MUTATION, inside a real `store.transactional(...)`, so * everything the handler writes through `ctx.store` commits together and a * handler that throws leaves NOTHING written. Queries and actions run * untransacted, matching production (an action's external I/O can't roll * back, so it is deliberately never wrapped). * * Rejects with a `ParseError` when the input doesn't satisfy the schema, and * with the typed `ScopeError` when a guard denies — the same error a client * would receive. Guards run after the decode and before the transaction, as in * production; plugin interceptors registered via * `makeTestContext({ plugins })` wrap the whole thing from outside. * * ```ts * import { invoke, makeTestContext, user } from '@voltro/testing' * import { createNote } from './notes.mutation' // descriptor * import { createNoteHandler } from './notes.mutation.server' // executor * * const ctx = makeTestContext({ subject: user('A', { scopes: ['notes:write'] }) }) * const note = await invoke(createNote, createNoteHandler, { title: 'hi' }, ctx) * * // a caller without the scope is refused before the handler runs: * const outsider = makeTestContext({ subject: user('B') }) * await expect(invoke(createNote, createNoteHandler, { title: 'hi' }, outsider)) * .rejects.toMatchObject({ _tag: 'ScopeError' }) * * // a mutation that throws half-way leaves nothing behind: * await expect(invoke(createNote, failingHandler, { title: 'hi' }, ctx)).rejects.toThrow() * expect(await ctx.store.select('notes').all()).toHaveLength(0) * ``` */ export declare const invoke: , Result>(descriptor: D, executor: (input: Schema.Schema.Type, ctx: TestContext) => Result, rawInput: unknown, ctx: TestContext) => Promise>; /** Can we open a TCP connection to `host:port` within `timeoutMs`? */ export declare const isTcpReachable: (target: ReachableTarget) => Promise; /** * A `SubscribeContext` typed as the real one. * * Every field a subscriber can reach either comes from the caller or **throws * naming itself** when touched. That is deliberate and it is the half a plain * object literal cannot give you: a default that returns `undefined` reproduces * the failure this constructor exists to remove — the handler proceeds against * nothing and the test passes. A default that throws turns "this test did not * provide a store" into a sentence rather than into a wrong assertion. */ export declare const makeSubscribeContext: (options?: MakeSubscribeContextOptions) => SubscribeContext; export declare interface MakeSubscribeContextOptions { /** Subscriber id — defaults to `'test'`, the shape discovery derives from a filename. */ readonly id?: string; /** * The store the handler reads and writes through. * * REQUIRED when the handler touches it, and there is no default. A silent * empty store would let a subscriber that reads the wrong table pass — the * absent-default is the point: `makeTestContext().store` is the store to pass * here, so the subscriber and the handlers under test share one. */ readonly store?: SubscribeContext['store']; /** The declared-event publisher, when the subscriber bridges to one. */ readonly publish?: EventPublisher['publish']; readonly log?: SyncLogger; } /** * Build a request-level harness over `ctx`. * * The subject a request runs under is resolved PER REQUEST — by the strategy * chain from the request's own headers, or by `actingAs` — and the procedure * then runs on `ctx.withSubject(resolved, …)`. That is the whole point of the * layer: two calls to the same app with different headers are two different * callers reading through the same store, exactly as two HTTP requests are. */ export declare const makeTestApp: (options: MakeTestAppOptions) => TestApp; export declare interface MakeTestAppOptions { /** The context every request runs against. Its store is the app's store; the * request's RESOLVED subject re-scopes it per call via `withSubject`, so a * request genuinely reads through the identity the transport produced rather * than the one the context was built with. */ readonly ctx: TestContext; /** * Hand-authored `defineRestRoute` descriptors (an app's `restRoutes`). * * Typed `` to match `app.config.ts`'s own `restRoutes` field and * `restRoutesToHttpRoutes`'s parameter: each descriptor fixes its OWN input * and output types at `defineRestRoute`, and a heterogeneous array of them * has no single narrower element type — `` rejects every real * route (the handler's return type is contravariant into it). */ readonly restRoutes?: ReadonlyArray>; /** Procedures carrying a `publicApi:` annotation, with their executors. */ readonly publicApi?: ReadonlyArray; /** * The app's auth strategies, composed by the framework's own * `composeAuthStrategies`. With none supplied the composer's DEFAULT applies: * an anonymous subject carrying the `x-tenant` header's tenant — which is * itself the behaviour most worth testing, and the one `invoke` cannot reach. */ readonly strategies?: ReadonlyArray; /** Refuse an anonymous caller that carries no tenant (`Unauthenticated`). * Passed straight to `composeAuthStrategies`. */ readonly anonymousTenantRequired?: boolean; /** HTTP idempotency binding — the same `{ store, header, ttlMs }` the serve * path builds from `app.config.ts`'s `idempotency` field. */ readonly idempotency?: RestServeBindings['idempotency']; /** The api's `publicApi:` config — the REST profile the projections speak, * the same object `app.config.ts` declares. Absent → `'rpc'`. */ readonly publicApiProfile?: PublicApiProfileConfig; } export declare const makeTestContext: (options?: MakeTestContextOptions) => TestContext; export declare interface MakeTestContextOptions { /** The acting subject. Default: anonymous, no tenant. Read it back at * `ctx.request.subject` (the same place a live handler reads it). */ readonly subject?: Subject; /** Optional connection fixture, or the actual RuntimeContext.connection from * a listener test. Shared by transaction/subject re-scopes, never invented by * the harness; omitting it models an internal invocation. */ readonly connection?: RpcConnection; /** Tables to wire into the store's schema. Default: all globally * registered tables (whatever the test imported). */ readonly tables?: ReadonlyArray; /** * `relations(...)` specs to register for this context — the harness's stand-in * for the boot sweep, so `.with({ ... })` eager loads resolve under test. * * Needed because `relations()` is PURE: it RETURNS a spec, it does not * register one. In production `voltro dev` discovers `*.relations.ts` and * feeds each export to `registerDiscoveredRelations`; a unit test runs no * boot, so importing the module registers nothing and the first eager load * fails with "no relations registered". Hand the specs over instead: * * ```ts * import { teamRelations } from '../db/teams.relations' * const ctx = makeTestContext({ relations: [teamRelations] }) * ``` * * SEMANTICS, because the registry is process-global (a `Symbol.for` map shared * by every copy of `@voltro/database`) and that global is a leak waiting to * happen: passing `relations` REPLACES the registry with exactly these specs * (`clearRelationsRegistry()` then `registerRelations` each). It is a * declaration of what this context's relations ARE, not an addition to * whatever a previous test left behind. That is what makes two * `makeTestContext({ relations: [...] })` calls in one file independent — * additive registration would make the second call throw * `duplicate relation '…'` on a re-registered spec and would silently carry * the first test's relations into the second's store. * * Omitting the option touches the registry NOT AT ALL, so a suite that calls * `registerRelations` itself (or one whose app registers at import time) keeps * working unchanged. */ readonly relations?: ReadonlyArray; /** * Seed rows keyed by table name (use `mockStore({...})`). * * COPIED, not adopted. The context builds its own `InMemoryDataStore` from * these rows, so the object you pass is never written to and two contexts * built from ONE seed do not share data — which is what keeps two tests in a * file independent. * * For two actors over ONE store, re-scope a single context rather than * building a second one: `ctx.asSubject(other)` (or `ctx.withSubject(other, * fn)`) shares the store, the event bus, the cache and the clock, and moves * the acting identity for real. Passing the same seed twice reads like * sharing and is not. */ readonly store?: Record>; /** Frozen clock start. Default 2026-01-01T00:00:00Z. */ readonly clockStart?: Date; /** Injected AI mock (plan 05's `mockAi({...})`). */ readonly ai?: MockAi; /** Queued LLM responses for the bundled `MockLLM`. */ readonly llmResponses?: ReadonlyArray; /** Env values sealed into the boot snapshot so handler code that reads * `getSecret('X')` / `serverEnv.X` resolves under test. Without a snapshot * those accessors throw ("read before the boot env gate ran") because the * test harness never runs the real boot gate. Merged OVER `process.env` * (these win). Default: just the ambient `process.env`. */ readonly env?: Record; /** * The key for `.encrypted()` columns under test. Registers the field cipher * the boot would derive from `VOLTRO_FIELD_ENCRYPTION_KEY`, so a handler that * writes such a column runs here instead of failing with "no field cipher is * registered". Process-global like the boot's (`setFieldCipher`), and left * alone when omitted — `env` alone does NOT do this: the harness runs no boot * gate, and the gate is what turns the variable into a cipher. */ readonly fieldEncryptionKey?: string; /** * Plugins whose rpc interceptors (`interceptMutation` / `interceptQuery` / * `interceptAction`) wrap every `invoke` on this context — the same list an * app declares in `app.config.ts`, composed the same way (`[A, B, C]` → * `A(B(C(handler)))`, first listed outermost). * * Deliberately the PLUGIN objects, not a bare interceptor function: the * harness then makes the same kind-selection production does, so a hook * filed under the wrong name (`interceptMutation` on a query-only plugin) * fails here exactly as it silently no-ops in production. And it lives on * the CONTEXT, not on `invoke`, because production composes the chain once * at boot — an app cannot vary its plugin set per call, so neither can this. * * Their `services:` LAYER is provided too — `invoke` builds the same handler * service stack the serve pipeline does, so an Effect-mode handler that * `yield* MailService`s (or `StorageService`, or any tag a plugin owns) * resolves it under test instead of dying with "Service not found". That is * the whole reason the mail assertion surface is the plugin's own memory * provider rather than a mock this package invents: * * ```ts * import { mailPlugin, readMailBuffer, clearMailBuffer } from '@voltro/plugin-mail' * * const ctx = makeTestContext({ plugins: [mailPlugin({ provider: 'memory', from: 'a@b.c' })] }) * await invoke(sendWelcome, sendWelcomeHandler, { to: 'x@y.z' }, ctx) * expect(readMailBuffer().map((m) => m.to)).toEqual(['x@y.z']) * ``` * * Lifecycle hooks (`onActivate`), `schema`, routes and dashboard mounts are * boot concerns with no meaning for a single handler call, and are ignored — * so a plugin whose service is built INSIDE `onActivate` will not resolve * here. Every first-party plugin builds its service eagerly in the factory. */ readonly plugins?: ReadonlyArray; /** * The app's own `layers:` from `app.config.ts` — user-defined Effect * services a handler `yield*`s. * * Passed EXPLICITLY, never discovered: the harness runs no boot, so it * cannot read `app.config.ts`, and inventing a stand-in service would assert * against the stand-in. Handing over the real layer is not faking — it is the * same object the boot path merges. * * Merged LAST, so a user layer overrides a plugin layer declaring the same * Tag — the ordering `makeHandlerServiceLayer` uses on both boot paths. */ readonly layers?: ReadonlyArray>; /** * Row-level security for this context, WITHOUT touching the process global. * * `ctx.store` applies the app's row filter either way: with this option * omitted the harness resolves whatever `setRowFilter(...)` registered, so a * test that boots its app's real filter needs no option at all. Passing one * here overrides the global FOR THIS CONTEXT — which is what a test usually * wants, because `setRowFilter` is process-global: registered in one test it * silently constrains every later test in the same worker, and forgetting the * `afterEach` that clears it produces a failure in an unrelated file. * * Resolution goes through `resolveRowFilterScopeFor` — the same function * `resolveRowFilterScope` (and therefore the serve pipeline) is built on — so * the system-subject bypass, the `retry` schedule, and the `onLoadError` * decision are inherited rather than re-implemented. A filter whose `load` * fails refuses here exactly as it refuses in production. */ readonly rowFilter?: RowFilter; } export declare const makeWorkflowRunner: (opts: MakeWorkflowRunnerOptions) => WorkflowRunner; export declare interface MakeWorkflowRunnerOptions { readonly ctx: TestContext; readonly workflows?: ReadonlyArray; } export declare const markRestInvocation: (ctx: TestContext, publicApi: PublicApiSpec) => void; /** Minimal AI mock surface. The full `mockAi({...})` helper is owned by * the AI package (plan 05); typed here as an optional injected interface * so `@voltro/testing` carries no hard `@voltro/ai` dependency. */ export declare interface MockAi { readonly generate?: (...args: ReadonlyArray) => unknown; readonly generateObject?: (...args: ReadonlyArray) => unknown; } export declare class MockClock { private currentMs; constructor(initial?: Date | number | string); /** Current mock time as an instant in ms. */ now(): number; /** Current mock time as a Date. */ date(): Date; /** Advance time by a duration (ms / seconds / minutes / hours / days). */ advance(amount: number | string): void; /** Jump to an ABSOLUTE instant. `advance` moves relative; this is `travel_to`. */ set(at: Date | number | string): void; /** Is THIS clock the one currently faking global time? */ get installed(): boolean; /** * Take over `Date.now()` / `new Date()` for the whole realm. Returns the * uninstall — call it, or prefer `withFrozenTime` / `frozenTime`, which call * it for you even when the body throws. * * Throws if any clock is already installed. See the header: an inner * uninstall would otherwise restore the outer FAKE and leave the realm frozen * with nothing naming the cause. */ install(): () => void; /** * Give the realm its real clock back. * * A no-op when this clock is not the installed one, so an uninstall in a * `finally` is safe to run twice. The loud direction is the INSTALL — that is * the one that corrupts the next test. */ uninstall(): void; } export declare class MockLLM { private readonly queue; readonly calls: MockLLMCall[]; constructor(responses: ReadonlyArray); next(call: MockLLMCall): MockResponse; remaining(): number; } export declare interface MockLLMCall { readonly model: string; readonly messages: ReadonlyArray; readonly tools?: ReadonlyArray; } export declare type MockResponse = { readonly text: string; } | { readonly toolCall: { readonly name: string; readonly input: unknown; }; } | { readonly error: { readonly code: string; readonly message?: string; }; }; /** Seed helper: `makeTestContext({ store: mockStore({ docs: [...] }) })`. * Identity over the table→rows map; exists so the call site reads well. */ export declare const mockStore: (seed: Record>) => Record>; export declare class MockWebhooks { readonly emitted: EmittedWebhook[]; /** Mirrors `WebhooksServiceShape.emit`. Accepts the event id OR a * declared event, exactly as the real service does — the * handler under test should not have to be written differently to be * testable. */ emit

(event: string | { readonly id?: string; readonly event?: string; }, payload: P): Promise<{ readonly delivered: number; }>; /** Every payload emitted for one event, oldest first. */ payloadsFor(event: string): ReadonlyArray; /** The most recent emission of an event, or undefined. */ last(event: string): EmittedWebhook | undefined; clear(): void; } /** * The next value of that same counter — for a caller writing their OWN unique * default (`email: () => \`user-${nextSequence()}@test.local\``). * * Sharing ONE counter with the auto-filler is the point, not an implementation * detail: two sources of "unique enough" numbers in one process will collide * eventually, and the collision surfaces as a constraint violation in a test * that looks unrelated to either. Monotonic per process, never reset — a reset * between tests would re-issue values that rows from an earlier test in the * same worker still hold. */ export declare const nextSequence: () => number; /** Delivery nudges emitted through `ctx.outbox` on this context. */ export declare const outboxNudgesOf: (ctx: TestContext) => ReadonlyArray; export { ParseResult } /** The plugins a context was built with — for the REST harness, which hands * their cross-cutting errors to the `publicApi` projection the way both boot * paths do. */ export declare const pluginsOf: (ctx: TestContext) => ReadonlyArray; /** Any procedure executor: takes the DECODED input + a context, returns a * result — sync, `Promise`, or `Effect`. Mirrors the `(input, ctx) => …` shape * a real query / mutation / action handler has, INCLUDING the Effect form: the * framework's contract is "async or Effect, your choice per handler", and the * dispatcher runs both (`Effect.isEffect(result) ? … : …` in `servePipeline`). * `E` is the handler's typed error channel; it defaults to `never` for the * common `Effect.gen` that only succeeds. `R` is the services the handler * `yield*`s — `MailService`, a plugin Tag, one of the app's own `layers:`. * `invoke` provides those from the context's `plugins:` / `layers:`, so a * handler with a non-`never` `R` is callable; it defaults to `never` for the * common handler that resolves nothing. */ export declare type ProcedureExecutor = (input: Input, ctx: TestContext) => Output | Promise | Effect.Effect; /** One `publicApi`-annotated procedure plus the executor that implements it — * the same descriptor/handler pair `invoke` takes. The harness runs the * handler THROUGH `invoke`, so the procedure's `guards:`, input decode, * mutation transaction and plugin interceptors all still apply underneath the * REST hop. */ export declare interface PublicApiBinding { readonly descriptor: PublicApiDescriptor; readonly handler: (input: never, ctx: TestContext) => unknown; } export declare interface ReachableTarget { readonly host: string; readonly port: number; /** Shown in the skip label so a skipped run says WHAT was missing. */ readonly name: string; readonly timeoutMs?: number; } /** One delivery a test subscriber saw. */ export declare interface RecordedDelivery

{ readonly payload: P; readonly origin: string; readonly n: number; } /** * The engine BUILD behind a target, printed once per file. * * A dialect suite that is green on a developer machine and red in CI is only * comparable if both runs name the software they ran against — and the compose * file uses moving tags (`mariadb:11`), so "the same tag" is not the same * build. A failure took seven eliminated hypotheses partly because the first one * — the image version — was tested by pulling the tag LOCALLY, which observes * what the tag points at today and not what the CI runner had resolved. * * So: the suite says it, in both places, unconditionally. It asserts nothing — * a version gate would be a second thing to maintain, and the value here is * purely that a red log carries the number. * * Failures are swallowed on purpose. This is a diagnostic; a probe that took the * file down would trade a useful line for a collection error. */ export declare const reportEngineVersion: (label: string, probe: () => Promise) => Promise; /** * Drop queued post-commit work WITHOUT running it. * * What a transaction REPLAY does: the previous attempt rolled back, so the * callbacks it queued describe writes that no longer exist and must never * fire. `invoke` calls this at the top of every deadlock-replay attempt, * mirroring the serve pipeline's `afterCommit.length = 0`. Exported so a test * driving a handler directly can reproduce a replay by hand. */ export declare const resetAfterCommit: (ctx: TestContext) => void; export declare const restInvocationOf: (ctx: TestContext) => { readonly publicApi: PublicApiSpec; } | undefined; export { Row } /** * The composed plugin interceptor `invoke` runs for a procedure of `kind` on * this context — `undefined` when no registered plugin ships that hook. * * Composition is `composeRpcInterceptors`, the SAME function the serve * entrypoints use, so the wrap order (first plugin outermost) is not a second * opinion about how a chain nests. Exported so a test can assert the chain a * context carries without going through a handler. */ export declare const rpcInterceptorFor: (ctx: TestContext, kind: RpcKind) => RpcInterceptor | undefined; /** * Run the post-commit work a handler queued, then clear it. * * Called by `invoke` after a mutation's transaction COMMITS — and deliberately * not after a rollback, since the point of `afterCommit` is that the work only * happens if the write did. Exported so a test driving a handler directly can * reproduce the same ordering. */ export declare const runAfterCommit: (ctx: TestContext) => Promise; /** * Run `work` inside a REAL store transaction: every write the work performs * through `txCtx.store` commits together when it resolves, and ALL of them * roll back when it rejects. The rollback is the store's own — an * `InMemoryDataStore` transactional view with a private overlay that is * discarded on throw (`buffered ChangeEvents drain only on commit`), the same * mechanism `store.transactional(...)` gives a mutation in production. Nothing * here simulates it. * * `txCtx` is a full `TestContext` re-derived over the transaction: its * `withSubject` / `withTenant` re-scopers and its `load` / `loadMany` batcher * read and write through the SAME transaction, so a handler cannot * accidentally escape it mid-mutation. * * Nested calls are NOT supported — the in-memory store rejects a nested * transaction, exactly as it does at runtime. */ export declare const runInStoreTransaction: (ctx: TestContext, work: (txCtx: TestContext) => Promise) => Promise; /** A machine subject scoped to ONE tenant (a per-tenant integration, a * `storeForTenant` view). NOT the cross-tenant `system()` below. */ export declare const serviceAccount: (id: string, options?: TestSubjectOptions) => Subject; /** * The handler service layer for this context — every registered plugin's * `services:` layer, then the app's own `layers:`, merged in that order. * * The ORDER is the serve pipeline's (`makeHandlerServiceLayer`): user layers * last, so an app layer declaring the same Tag as a plugin's wins here exactly * as it does in production. `undefined` when the context registered neither — * a handler that `yield*`s a Tag nobody provided must still die with "Service * not found", because that is what it would do at runtime. * * What is deliberately NOT in here: `Cache` / `Kv` / `AnalyticsSink` / the * aggregate + standing-primitive registries / the outbound `HttpClient`. Those * are built by the CLI at boot from an app's config, and `@voltro/testing` * cannot depend on the CLI. Nothing about that is faked — a handler needing one * gets the honest "Service not found" instead of a stand-in whose behaviour the * test would then be asserting. * * Exported so a test can assert the layer set a context carries without going * through a handler. */ export declare const serviceLayerFor: (ctx: TestContext) => Layer.Layer | undefined; /** * The CROSS-TENANT machine actor — cron, workflows, backfills. `tenantId` is * `null` by construction and the tenant read-scope mixin reads that as "every * tenant" rather than "no rows", so this is not a tenant-less `user()`. * Defaults to `['admin:full']`, matching `systemSubject`. */ export declare const system: (id?: string, scopes?: ReadonlyArray) => Subject; /** The tenant every `user()` / `apiKey()` / `serviceAccount()` belongs to * unless told otherwise. Exported so a test can seed rows under the same * tenant the default subject reads through. */ export declare const TEST_TENANT_ID = "test-tenant"; export declare interface TestApp { /** A view of this app that resolves EVERY request to `subject`, bypassing the * strategy chain — the same `resolveSubject` seam the serve pipeline fills * with its own auth resolver. Use it to test what a given identity may do; * use `withHeaders({ 'x-tenant': … })` (and no `actingAs`) to test how an * identity is RESOLVED. */ readonly actingAs: (subject: Subject) => TestApp; /** A view of this app that sends `headers` on every request. Merged over * whatever is already set; later calls win per key. */ readonly withHeaders: (headers: Readonly>) => TestApp; readonly get: (path: string, options?: TestRequestOptions) => Promise; readonly delete: (path: string, options?: TestRequestOptions) => Promise; readonly post: (path: string, body?: unknown, options?: TestRequestOptions) => Promise; readonly put: (path: string, body?: unknown, options?: TestRequestOptions) => Promise; readonly patch: (path: string, body?: unknown, options?: TestRequestOptions) => Promise; /** The general form the five verbs above are sugar over. */ readonly request: (method: string, path: string, options?: TestRequestOptions & { readonly body?: unknown; }) => Promise; /** Every path this app mounts, in mount order. A 404 names them, and a test * asserting the mounted surface can read them without one. */ readonly paths: ReadonlyArray; } /** The test context IS an `AppContext` (store + request + cache), plus the * deterministic doubles and the subject/tenant re-scopers. Because it * extends `AppContext`, you can pass it straight into an executor typed * `(input, ctx: AppContext) => …`. */ export declare interface TestContext extends AppContext { readonly clock: MockClock; /** Outgoing webhooks, recorded. `ctx.webhooks` is a field production supplies * and the harness did not, so a mutation written the documented way — * `useWebhooks(ctx).emit(...)` — threw in every unit test. */ readonly webhooks: MockWebhooks; /** * The event publisher, backed by a real in-process bus. * * `ctx.events` is a field PRODUCTION supplies on every `AppContext`, and this * harness did not — so an executor containing `ctx.events.publish(...)` could * not be unit-tested at all: it died on `Cannot read properties of undefined * (reading 'publish')`. The shipped `api-durable` template demonstrates * exactly that pattern (publish inside the mutation's transaction), so the * example and the test helper contradicted each other, and the template's own * test only passed because it awaited the Effect without running it. * * Real, not a stub: `makeEventPublisher` is the same constructor production * uses, over a `testEventBus`. A fake would re-implement the validation and * the tenant stamping, and would be wrong the first time either gains a case. * `ctx.eventBus.received` is where a test reads what was published. */ readonly events: EventPublisher; /** The bus behind `ctx.events` — subscribe to it to assert what a handler * published. */ readonly eventBus: TestEventBus; readonly llm: MockLLM; readonly ai?: MockAi; /** Re-scope to a different subject for one block (real subject swap, not * a closure stub). Shares the underlying data so cross-subject reads * exercise real tenant scoping. */ withSubject(subject: Subject, fn: (ctx: TestContext) => T | Promise): Promise; /** Re-scope to a different tenant (keeps the current subject identity). */ withTenant(tenantId: string, fn: (ctx: TestContext) => T | Promise): Promise; /** * The same context acting as another subject, returned rather than passed to * a block — for a test with two actors alive side by side. * * `withSubject` nests, which reads well for "write as A, then assert B cannot * see it". It reads badly for a conflict: two editors touching one row, a * `FOR UPDATE` contest, an undo the other person's write must refuse. Those * want two named contexts and interleaved calls, and nesting a block per * alternation buries the sequence being tested. * * Everything is shared exactly as `withSubject` shares it — one store, one * event bus, one cache, one clock. Only the acting identity differs. * * **Do not build this by spreading the context.** `{ ...ctx, request: { * ...ctx.request, subject } }` looks equivalent and is not: `ctx.store` was * wrapped for the ORIGINAL subject, so tenant scoping, row filters and the * `audit()` stamp all keep resolving to that first identity. A write made * through such a context lands with the wrong `updatedBy` — measured — so a * test asserting "the other person touched this row" can pass while the row * says otherwise. */ asSubject(subject: Subject): TestContext; /** The same context acting for another tenant, returned rather than passed * to a block. The `asSubject` counterpart to `withTenant`. */ asTenant(tenantId: string): TestContext; } export declare interface TestEventBus { /** * Publish exactly as a handler would — through the real validation, the real * size gate and the real serial assignment. A payload that does not match the * descriptor fails here, which is the point: a harness that skipped validation * would let a test pass on a payload production rejects. */ publish(descriptor: EventDescriptor, key: Schema.Schema.Type, payload: Schema.Schema.Type): Promise<{ readonly n: number; }>; /** Publish and ASSERT it succeeded — the common case, one line. */ subscribe(descriptor: EventDescriptor, key: Schema.Schema.Type, options?: { readonly tenantId?: string | null; readonly resume?: ReadonlyArray<{ origin: string; n: number; }>; }): TestSubscriber>; /** * Force a gap WITHOUT waiting for a buffer to overflow. * * A deployment's recovery path (`onMissed` → resync) is the hardest thing to * test honestly, because provoking a real loss means racing a queue. This * injects a delivery `count` serials ahead, so the next subscriber to resume * is owed messages the ring never held — exactly the shape a slow consumer * produces, deterministically. */ skipSerials(descriptor: EventDescriptor, key: Schema.Schema.Type, count: number, options?: { readonly tenantId?: string | null; }): void; /** The underlying bus, for a test that needs something not exposed here. */ readonly bus: EventBus; /** Drop every subscriber and buffer. */ readonly reset: () => void; } /** * A running event bus with no transport. * * ```ts * const events = testEventBus() * const display = events.subscribe(gameStarted, { arenaId: 'a1' }) * await events.publish(gameStarted, { arenaId: 'a1' }, { gameId: 'g1' }) * expect(display.received).toEqual([{ gameId: 'g1' }]) * ``` */ export declare const testEventBus: (options?: TestEventBusOptions) => TestEventBus; export declare interface TestEventBusOptions { /** Default tenant for publishes and subscriptions. `null` = system. */ readonly tenantId?: string | null; /** Ring depth — a small one makes an eviction test deterministic. */ readonly ringSize?: number; readonly origin?: string; } export declare const TESTING_PRESET_VERSION: 1; export declare interface TestRequestOptions { /** Extra headers for this call, merged over the app's (`actingAs` / * `withHeaders`) headers. Names are lowercased, as the transport delivers * them. */ readonly headers?: Readonly>; } export declare interface TestResponse { readonly status: number; /** Response headers, names lowercased. */ /** Response headers, lower-cased. A header the route repeated — two * `set-cookie` lines — is an array, so a test can assert on both. */ readonly headers: Readonly>>; /** The raw response body as text (empty string when there was none). */ readonly text: string; /** The body parsed as JSON when the route answered with a JSON content-type, * otherwise `undefined`. Every framework REST response is JSON; a streaming * (SSE) route answers with neither — see `stream`. */ readonly body: unknown; /** `true` for a streaming (SSE) route: the response has no buffered body, so * `text` is empty and `body` is `undefined`. Stated rather than silently * looking like an empty 200. */ readonly stream: boolean; } export declare interface TestSubjectOptions { /** Default `TEST_TENANT_ID`. */ readonly tenantId?: string; /** Default `[]` — no authority. Guards deny. */ readonly scopes?: ReadonlyArray; /** Strategy-specific claims (`metadata.provider`, org ids, raw JWT claims). */ readonly metadata?: Record; } export declare interface TestSubscriber

{ /** Payloads delivered so far, in order. */ readonly received: ReadonlyArray

; /** Full envelopes, when a test cares about serials or origin. */ readonly deliveries: ReadonlyArray>; /** Losses the server could PROVE, with their cause. */ readonly missed: ReadonlyArray<{ readonly count: number; readonly reason: 'buffer' | 'resume'; }>; /** Total proven losses. */ readonly missedCount: number; readonly stop: () => void; } /** * An end-user subject — what a signed-in session resolves to. * * ```ts * const ctx = makeTestContext({ subject: user('u1', { scopes: ['notes:write'] }) }) * const outsider = makeTestContext({ subject: user('u2') }) // no scopes → guards deny * ``` */ export declare const user: (id: string, options?: TestSubjectOptions) => Subject; /** * Run `body` with global time frozen at `at`, then restore — whether the body * returns, throws, resolves or rejects. * * ```ts * withFrozenTime('2026-03-01T12:00:00Z', (clock) => { * expect(new Date().toISOString()).toBe('2026-03-01T12:00:00.000Z') * clock.advance('1h') // global time moves with it * }) * ``` * * An ASYNC body is awaited before the restore. A plain `finally` around a * promise-returning call would put the clock back while the body was still * running, which is the failure this helper exists to make impossible. */ export declare const withFrozenTime: (at: Date | number | string | MockClock, body: (clock: MockClock) => A) => A; /** Model another native RPC connection against the SAME test store/services. * The connection may come from a real listener. Passing undefined models an * internal call; no identity is fabricated or copied from another caller. */ export declare const withRpcConnection: (ctx: TestContext, connection: RpcConnection | undefined) => TestContext; /** A workflow + its execute function, as the framework pairs them via * `workflow.toLayer(execute)`. Structural (no `@effect/workflow` generic * signature) so callers can pass the value from `workflow({ name, ... })` * plus its `(payload, executionId) => Effect` execute directly — type-erased * over the workflow's payload/success/error/requirement shapes. */ export declare interface WorkflowEntry { readonly workflow: { readonly name: string; readonly execute: (payload: unknown, options?: unknown) => ErasedWorkflowEffect; readonly toLayer: (execute: (payload: unknown, executionId: string) => ErasedWorkflowEffect) => Layer.Layer; }; readonly execute: (payload: unknown, executionId: string) => ErasedWorkflowEffect; } export declare interface WorkflowRunner { /** Start a workflow by its `name` (tag) with a payload and block until * terminal. Records every step attempt so `result.steps[i].attempts` * reflects real retries. */ start(workflowName: string, payload: unknown): Promise; /** Inspect a previously-started run by id without re-running it. Returns * `null` for an unknown run id. */ inspect(runId: string): Promise; } export declare interface WorkflowRunResult { readonly status: 'succeeded' | 'failed'; readonly output: unknown; readonly error: { readonly tag: string | null; readonly message: string; } | null; readonly steps: ReadonlyArray; readonly runId: string; } export { }