import type { FormResult } from "@nanobpm/engine-wasm/readmodel-types"; import { type AgentHistoryFilter, type AgentHistoryRecord, type AgentInstanceFilter, type AgentInstanceSummary, type EngineClient, type ElementInstanceSummary, type ElementInstanceFilter, type ElementInstanceWaitState, type ElementInstanceWaitStateFilter, type IncidentFilter, type IncidentSummary, type JobFilter, type JobHandler, type JobSummary, type UserTaskState, type UserTaskFilter, type VariableFilter, type VariableSummary, type WorkerSubscription } from "@nanobpm/urban/runtime"; import { MockWorkerBuilder } from "./worker-mock.ts"; import { type ProcessInstanceState, wasmStateToProcessInstanceState } from "@nanobpm/engine-testkit"; export { type ProcessInstanceState, wasmStateToProcessInstanceState }; /** A single process instance's lifecycle snapshot, as returned by * {@link EngineClient.searchProcessInstances}. Structurally identical to urban's. */ export interface ProcessInstanceSnapshot { readonly processInstanceKey: string; readonly state: ProcessInstanceState; readonly processDefinitionKey?: string; readonly parentProcessInstanceKey?: string; readonly rootProcessInstanceKey?: string; } /** Defensively narrow an untyped read-model search response to its object rows. The DTO * annotation on a `JSON.parse`d body is a shape *claim*, not a runtime guarantee, so guard * the body, its `items` array, and each row: a malformed/changed engine response (a non-object * body, a non-array `items`, or `null`/non-object rows) yields `[]`/drops the bad row instead * of throwing downstream. Keeps the caller's DTO row type — the single source of truth both * `searchUserTasks` and `searchProcessInstances` extract through, so they cannot drift. */ export declare function searchRows(body: { items: T[]; }): T[]; /** * An {@link EngineClient} backed by the in-process `@nanobpm/engine-wasm` * engine. Construct via {@link createWasmEngineClient}. Beyond the interface it * exposes {@link advanceTime}, {@link snapshot}, and {@link now} for the settle * loop and assertions (S2). */ export declare class WasmEngineClient implements EngineClient { #private; /** A shutdown {@link AbortSignal} that aborts when {@link close} begins. The test kit threads it * into the scheduler backing `app.wait()`, so a worker handler parked on the virtual clock is * cancelled at teardown rather than wedging `close()` on a timer that will never fire (the * virtual-timer sibling of the issue #446 real-time use-after-free). */ get shutdownSignal(): AbortSignal; private constructor(); /** Boot the wasm module (idempotent) and return a fresh, empty engine. */ static create(): Promise; deployResources(resources: { name: string; content: string; contentType: string; }[]): Promise<{ deployed: number; }>; createInstance(input: { processDefinitionId: string; variables?: Record; awaitCompletion?: boolean; }): Promise<{ processInstanceKey: string; variables?: Record; }>; cancelInstance(input: { processInstanceKey: string; }): Promise; publishMessage(input: { name: string; correlationKey?: string; variables?: Record; }): Promise; searchUserTasks(filter?: UserTaskFilter & { state?: UserTaskState; }): Promise<{ userTaskKey: string; elementId?: string; variables?: Record; formKey?: string; externalFormReference?: string; processInstanceKey?: string; parentProcessInstanceKey?: string; rootProcessInstanceKey?: string; }[]>; /** The open (answerable) user tasks — `searchUserTasks` pinned to `state: "CREATED"`. * Mirrors `SdkEngineClient.openUserTasks`: the single safe accessor for reconcile/ * affordance paths, derived from `searchUserTasks` so the two cannot drift. */ openUserTasks(filter?: UserTaskFilter): Promise<{ userTaskKey: string; elementId?: string; variables?: Record; formKey?: string; externalFormReference?: string; processInstanceKey?: string; parentProcessInstanceKey?: string; rootProcessInstanceKey?: string; }[]>; /** Fetch a deployed form's form-js schema from the engine's real read model * (`GET /forms/{formKey}` via `getFormByKey`). Structurally matches urban's * `EngineClient.getForm`; returns `null` when no such form exists in the read model. */ getForm(input: { formKey?: string; formId?: string; }): Promise<{ formKey?: string; formId?: string; version?: number; schema: Record; } | null>; completeUserTask(userTaskKey: string, variables?: Record): Promise; searchProcessInstances(filter?: { processInstanceKeys?: string[]; state?: ProcessInstanceState; parentProcessInstanceKey?: string; rootProcessInstanceKey?: string; }): Promise; /** * Search element instances ("tokens") — derived from the primary-state snapshot, which the * WASM engine keeps in lieu of a `/v2/element-instances/search` read channel. Each running * instance records its *active* element instances (`instances[].activeElements[]`), so this * surfaces the ACTIVE set — "the furthest element reached", including active non-user-task * elements a user-task search cannot see. A completed/terminated element instance is not * retained with a key in the snapshot, so a caller filtering `state: "COMPLETED"` / * `"TERMINATED"` gets an empty result here (unlike the gateway-backed `SdkEngineClient`, * which the engine serves those from server-side). `elementType` is likewise absent — the * snapshot does not carry the BPMN type — matching `ElementInstanceSummary`'s optional field. */ searchElementInstances(filter?: ElementInstanceFilter): Promise; /** * Search element-instance *wait states* — the parks the deployed engine's read model * serves, derived from the snapshot's `jobs` (JOB), `messageSubscriptions` (MESSAGE) and * `userTasks` (USER_TASK). A JOB/MESSAGE park row carries its `elementId` + owning process * instance but not the element-instance key, so it is joined to the same instance's * `activeElements` to resolve the key `searchElementInstances` reports; a USER_TASK row * instead carries its `elementInstanceKey` directly — either way the two stay consistent. * * **Deployed floor (`JOB | MESSAGE | USER_TASK`).** This synthesizes only the park kinds the * deployed nanobpmn gateway implements ({@link DEPLOYED_WAIT_STATE_TYPES}), and rejects a * `waitStateType` filter outside it with {@link UnsupportedWaitStateTypeError} — exactly as * the gateway-backed `SdkEngineClient` does (the live gateway answers HTTP 422). A * `USER_TASK` park is synthesized here (Magikcraft/nano-bpm#1042 shipped it in the gateway's * read model) and can also be read via `searchUserTasks`; `TIMER`/`SIGNAL`/`CONDITION` are * still *not* synthesized (the 8.10 follow-on), so the emulation cannot report a park a real * engine would not — a query that passes here passes against a live engine too. Ref: * Magikcraft/nano-bpm#1042. */ searchElementInstanceWaitStates(filter?: ElementInstanceWaitStateFilter): Promise; /** Fetch a single element instance by key — the snapshot-derived active set (see * {@link searchElementInstances}) narrowed to the one whose key matches, or `null` when no * such active element instance exists (mirrors `SdkEngineClient.getElementInstance`, which * returns `null` on a 404). A blank key can address nothing → `null`. */ getElementInstance(elementInstanceKey: string): Promise; /** Incidents the WASM engine currently records. The engine retains an incident in its snapshot * only while it is *open* (a resolved incident drops out), so every reported incident is * `ACTIVE`; a request for any other lifecycle state therefore yields nothing. Each snapshot * incident carries `key`/`instanceKey`/`elementId`/`kind`/`reason` but no job key, so `jobKey` * is left absent (matching `IncidentSummary`'s optional field); the element-instance key is * resolved by joining `(instanceKey, elementId)` to the owning instance's `activeElements` — * the same index `searchElementInstanceWaitStates` uses, so the two cannot drift. The * `processInstanceKey`/`state` selectors are applied client-side, mirroring the effective * behaviour of the gateway-backed `SdkEngineClient`. */ searchIncidents(filter?: IncidentFilter): Promise; /** Search process variables — the read counterpart to {@link setVariables}. Delegates to the * engine's real REST read channel (`POST /v2/variables/search`) and parses through the derived * `VariableSearchQueryResult` DTO. The read model does not yet honour filter/sort/page fields * server-side (it returns every variable), so the `processInstanceKey`/`scopeKey`/`name` * selectors are applied client-side — mirroring the *effective* behaviour of the gateway-backed * `SdkEngineClient`, which gets them honoured server-side; only the filtering site differs. Each * row carries its serialized JSON `value` and the engine's `isTruncated` flag. */ searchVariables(filter?: VariableFilter): Promise; /** Search jobs — the "is it actually stuck" read (a `CREATED` job WITH a `worker` set is leased, * one with NONE set is queued) and the on-tool source of a `jobKey` for {@link updateJobRetries}. * Derived from the snapshot's `jobs` (the WASM engine has no `/jobs/search` read channel): each * snapshot job carries `key`/`jobType`/`instanceKey`/`elementId`/`retries`/`state`. The pull-based * WASM engine locks a job to a worker only transiently inside {@link drain}, so a parked job has * no `worker`/`deadline` — both are emitted only when the snapshot carries them (matching * `JobSummary`'s optional fields). The `processInstanceKey`/`state`/`type`/`elementId`/`worker` * selectors are applied client-side, mirroring the effective behaviour of the gateway-backed * `SdkEngineClient`. */ searchJobs(filter?: JobFilter): Promise; /** Fetch the deployed BPMN XML of a process definition by key — the deployed routing model (with * its FEEL gateway conditions), the source of truth for WHY an instance routed where it did. The * WASM engine has no `/v2/process-definitions/{key}/xml` read channel, but its event log retains a * `ProcessDeployed` event carrying `process_definition_key` + the full `process.xml`, so this * scans the log for the matching key — engine truth, no side-channel capture. Returns `null` for * a blank/unknown key (mirroring `SdkEngineClient.getProcessDefinitionXml`, which returns `null` * on a 404). */ getProcessDefinitionXml(processDefinitionKey: string): Promise; /** Engine-native agent instances the WASM double records. The bundled μ-nano.wasm read model has * no AgentInstance/AgentHistory channel (it exposes none of the `/agent-instances/*` endpoints * the `SdkEngineClient` reads), so this emulates the floor a consumer needs to boot against the * testkit without a `TypeError`: no agent instance is ever recorded, so the search is empty. * This keeps the `EngineClient` surface complete (issue #341 conformance) and lets the historical * transcript consumer (nanobpm/nano-workforce#747) exercise its read path here; the *behavioural* * agent-history parity is validated against a live engine, which this in-process double omits. * The read-as-absence shape (empty list / `null`) matches `SdkEngineClient`, so the two adapters * agree on the "no such instance" answer. */ searchAgentInstances(_filter?: AgentInstanceFilter): Promise; /** One agent instance's conversation history — see {@link searchAgentInstances} for why the WASM * double records none. A blank key addresses nothing (parity with `SdkEngineClient`, which * short-circuits it); every key yields the empty list here. */ searchAgentInstanceHistory(_agentInstanceKey: string, _filter?: AgentHistoryFilter): Promise; /** Fetch a single agent instance by key — always `null` here (the WASM double records none; see * {@link searchAgentInstances}), matching `SdkEngineClient`'s read-as-absence on a 404 and its * blank-key short-circuit. */ getAgentInstance(_agentInstanceKey: string): Promise; /** Resolve an open incident by key, returning the parked token to progress (a job incident * returns its job to the activatable pool — which must have retries left, see * {@link updateJobRetries}). Drains afterward so a re-activated job is served, mirroring a live * push engine (parity with `SdkEngineClient`, whose engine serves it server-side). */ resolveIncident(input: { incidentKey: string; }): Promise; /** Set a failed job's remaining retries — the "retry a failed job" operation. Bumping retries * back above zero makes the job activatable again so a paired {@link resolveIncident} can * return it to the pool. Drains afterward for the same push-parity reason as the other * mutating calls. */ updateJobRetries(input: { jobKey: string; retries: number; }): Promise; /** Set (merge) variables into a scope (`scopeKey` is a process-instance or element-instance * key). `local` merges strictly into that local scope; the default (`false`) propagates to the * outermost scope — the same semantics the live SDK adapter's `createElementInstanceVariables` * honours. Drains afterward so a variable that unblocks a waiting worker takes effect. */ setVariables(input: { scopeKey: string; variables: Record; local?: boolean; }): Promise; registerWorker(jobType: string, handler: JobHandler, options?: { workerName?: string; maxParallelJobs?: number; fetchVariables?: string[]; }): Promise; close(): Promise; /** * Register (or fetch the existing) job-worker mock for `taskType` (epic #296, S1). * A mocked type is resolved by its {@link MockWorkerBuilder} at the dispatch seam * ({@link drain} → `#runJob`) — completed / failed / errored by the mock's matching * clause **instead of** the app's real handler — while un-mocked types, and jobs a * mock's `when(...)` clauses don't match, still run real code. The builder shadows * the real handler for the app's lifetime; call `.reset()` on it (or * {@link clearWorkerMock}) to remove the mock and restore real behaviour. * * Idempotent per type: repeated calls return the SAME builder, so conditional * clauses accumulate in registration order across calls. Purely opt-in — no mock * bookkeeping happens on any dispatch until this is first called for a type. */ mockWorker(taskType: string): MockWorkerBuilder; /** * Remove any job-worker mock for `taskType`, restoring its real handler. No-op if * unmocked. Delegates to the builder's {@link MockWorkerBuilder.reset} so a test that * still holds the builder reference sees its clauses and pending predicate cleared too * — not just the registry entry dropped. */ clearWorkerMock(taskType: string): void; /** Advance the virtual clock by `ms`, firing due timers, then drain workers so * any jobs the timers created are served. */ advanceTime(ms: number): Promise; /** The current virtual clock (ms). */ get now(): number; /** * Register an observer notified with the `jobType` each time a job is dispatched to a * worker handler (before the handler runs, so a failing handler still counts as * "exercised"). The second argument reports whether that dispatch was satisfied by a * job-worker mock (epic #296, S4) — the coverage gate records the type as exercised * either way, and additionally flags it as mocked so a mocked worker stays an honest, * visible entry rather than a silently-hidden gap. Returns an unsubscribe. A single * observer is held — a later call replaces the earlier one — which is all the coverage * gate needs; pass `undefined` (or call the returned unsubscribe) to clear it. The * observer is a passive spectator: a throw from it is swallowed and never affects job * completion, the drain, or incidents. */ observeJobs(onJob: ((jobType: string, mocked: boolean) => void) | undefined): () => void; /** The raw engine snapshot (parsed). */ snapshot(): Record; /** * Serve every registered worker's — and every mock-only type's — activatable jobs to * quiescence: repeatedly activate + run + acknowledge until a full pass activates * nothing. A handler that returns a value completes the job with it; a * {@link isBpmnError} throw raises a BPMN error; any other throw fails the job * (decrementing retries, raising an incident at zero) — parity with the live adapter. * * A **mock-only** type (one with a {@link mockWorker} mock but no real * `registerWorker`) is also drained, so a test can mock a worker the app never * registered (e.g. an unimplemented external integration). Its jobs are activated and * resolved by the mock; a job the mock's clauses don't match is left locked (no real * handler exists to run it), which keeps the drain a fixpoint rather than spinning. */ drain(): Promise; } /** Boot the wasm engine (idempotent) and return a fresh {@link WasmEngineClient}. */ export declare function createWasmEngineClient(): Promise; /** Derive the ACTIVE element instances the WASM snapshot records (see * {@link WasmEngineClient.searchElementInstances}). Applies the `processInstanceKey`/ * `elementId`/`state` selectors client-side, matching the effective behaviour of the * gateway-backed `SdkEngineClient`. */ export declare function deriveElementInstances(snapshot: Record, filter?: ElementInstanceFilter): ElementInstanceSummary[]; /** Derive element-instance wait states from the snapshot's park collections (see * {@link WasmEngineClient.searchElementInstanceWaitStates}). Synthesizes only the deployed * floor (`JOB | MESSAGE | USER_TASK`, {@link DEPLOYED_WAIT_STATE_TYPES}) — the park kinds the * deployed gateway's read model serves — so the emulation cannot report a park a live engine * would not. A `waitStateType` filter outside the floor is rejected with * {@link UnsupportedWaitStateTypeError} (the gateway answers HTTP 422). A JOB/MESSAGE park's * element-instance key is resolved through {@link activeElementKeyIndex}, while a USER_TASK * park carries its `elementInstanceKey` directly on the task row; the * `processInstanceKey`/`elementId`/`waitStateType` selectors are applied client-side. */ export declare function deriveWaitStates(snapshot: Record, filter?: ElementInstanceWaitStateFilter): ElementInstanceWaitState[]; /** Presence-check a possibly-numeric form key under the shared trim rule — mirrors the urban * form contract's `presentKey` (`packages/urban/src/runtime/core/form-contract.ts`). The * read-model body may carry a `formKey` as a number, so coerce a *number* to a string; a string * is taken as-is; **any other type is absent**. It deliberately never `String(...)`-coerces an * arbitrary value, so a non-string (e.g. an object) can't leak in as a truthy `"[object Object]"` * identifier. Re-declared locally (not imported) for the same long-published-API-floor reason as * `present`/`str` above. Exported for the coercion-defect-class guard in the test suite. */ export declare function presentKey(v: unknown): string | undefined; /** Presence-check a string-only identifier (`externalFormReference`/`formId`) under the shared * trim rule — mirrors the urban form contract, which treats these as present only when a string. * A number or any other type is absent, and it never `String(...)`-coerces, so a non-string can't * leak in as a garbage identifier. Exported for the coercion-defect-class guard in the tests. */ export declare function presentString(v: unknown): string | undefined; /** The non-empty, trimmed string form of a *required* engine key, or throws when the value is * absent/blank (including a whitespace-only or padded string). A mutating seam op addresses a * single entity by key, so a blank key is a caller bug that must fail fast with an actionable * message rather than reach the WASM engine as a garbage identifier and surface an opaque error — * matching `SdkEngineClient`'s `requireEngineKey` so the two adapters stay behaviour-identical (No * Drift Surfaces). Reuses {@link presentKey}'s trim-and-coerce rule so a padded key like `" 5 "` * is normalized identically to how read paths resolve it. Exported for the guard in the tests. */ export declare function requireKey(value: unknown, name: string): string; /** Build the typed `getForm` result from an untyped read-model body (the `getFormByKey` JSON * boundary). The `FormResult` DTO annotation on a `JSON.parse`d body is a shape *claim*, not a * runtime guarantee, so guard the body with `isRecord` — which, unlike `typeof === "object"`, * also excludes arrays — before reading its fields. A missing/invalid schema is treated as "no * such form" (`null`). Identifier presence is type-aware (mirrors the shared form contract's * `buildFormSchema`): a `formKey` counts only when a string/number, a `formId` only when a string, * so a non-string value can never coerce into a garbage `"[object Object]"` identifier. The single * source of truth `getForm` extracts through, so the boundary's tolerance cannot drift. */ export declare function parseForm(body: FormResult | null): { formKey?: string; formId?: string; version?: number; schema: Record; } | null;