// Adversarial completeness & regression for the mock layer (epic #296, S5). // // This suite proves the *merged* mock layer — job-worker mocking (S1+S2, `worker-mock.ts`) — and // native call-activity execution (the engine now runs call activities directly; the testkit no // longer rewrites them) are airtight along the axes a future regression is most likely to break // silently: // // 1. Negative / isolation — a mock on one type/process must not leak onto another, un-mocked // types/processes must still run real code, and removing a mock must restore real behaviour. // 2. Outcome fidelity — each worker outcome must actually surface in observable instance state // (completion variables merged, incident raised, BPMN error boundary taken). // 3. Condition correctness — `when(predicate)` matches only the intended jobs, first-match wins // in registration order, and a non-matching job falls through to the next rule / real handler. // // Everything is asserted through the engine's *observable* surface (process-instance state + // snapshot incidents/variables) rather than mock internals, and everything is deterministic (the // drain reaches a fixpoint under the virtual clock). Runs on Node and Deno. // // A separate `mock-completeness-guard.test.ts` proves the mock layer's outcome inventory and // `when(...)`-matchable fields are DERIVED (a new engine outcome without a builder+test is a red // build); `adversarial-determinism.test.ts` proves — at runtime and statically — that the mock // layer introduces no wall-clock/randomness. import { test } from "node:test"; import assert from "node:assert/strict"; import type { EngineJob } from "@nanobpm/urban/runtime"; import { createWasmEngineClient, type WasmEngineClient } from "./wasm-engine.ts"; import { MockWorkerBuilder, type MockOutcome } from "./worker-mock.ts"; // --------------------------------------------------------------------------------------------- // Fixtures (mirror the styles the S1/S2 and S3 suites use). // --------------------------------------------------------------------------------------------- /** A single service task (`work`) — completes the whole instance when `work` completes. */ const SVC_BPMN = ` `; /** A service task (`work`) then a user task (`review`): the instance parks ACTIVE on `review` * after `work` completes, so its merged variables stay observable in the snapshot. */ const WAIT_BPMN = ` `; /** A service task (`work`) with an error boundary catching errorCode BOOM → alternate end. */ const BOUNDARY_BPMN = ` `; /** Three sequential service tasks (`a` → `b` → `c`) then a user task — used to prove per-type * isolation across several types at once while keeping the instance ACTIVE. */ const THREE_BPMN = ` `; /** Parent with two call activities in sequence (childA → childB) then a user task — for * native call-activity execution of multiple, distinct children. */ const PARENT_TWO = ` `; /** A real child process carrying a marker service task, so a test can prove the native call * activity actually runs the called process (the worker counter increments). */ function childModel(processId: string, taskType: string): string { return ` `; } // --------------------------------------------------------------------------------------------- // Helpers. // --------------------------------------------------------------------------------------------- function res(...models: { name: string; xml: string }[]): { name: string; content: string; contentType: string }[] { return models.map((m) => ({ name: m.name, content: m.xml, contentType: "text/xml" })); } /** Boot an engine, deploy `models`, run `body`, and always close. */ async function withEngine( models: { name: string; xml: string }[], body: (engine: WasmEngineClient) => Promise, ): Promise { const engine = await createWasmEngineClient(); try { await engine.deployResources(res(...models)); await body(engine); } finally { await engine.close(); } } /** The `reason` strings of a snapshot's incidents. */ function incidentReasons(engine: WasmEngineClient): string[] { const raw = engine.snapshot().incidents; if (!Array.isArray(raw)) return []; const out: string[] = []; for (const inc of raw) { if (inc && typeof inc === "object") { const reason = Reflect.get(inc, "reason"); if (typeof reason === "string") out.push(reason); } } return out; } /** A single instance's variables from the snapshot. */ function instanceVariables(engine: WasmEngineClient, key: string): Record { const raw = engine.snapshot().instances; if (!Array.isArray(raw)) return {}; for (const inst of raw) { if (inst && typeof inst === "object" && String(Reflect.get(inst, "key")) === key) { const vars = Reflect.get(inst, "variables"); if (vars && typeof vars === "object" && !Array.isArray(vars)) return { ...vars }; } } return {}; } /** Resolve a `failWith(...)` to its {@link MockOutcome} without a drain — a fast probe of the retry * budget the engine would receive, dodging the non-quiescing positive-retry drive-through. */ function engine_failWithProbe(retries: number | undefined, message: string | undefined): MockOutcome { const opts = retries === undefined && message === undefined ? undefined : { retries, message }; const job: EngineJob = { jobKey: "probe", jobType: "work", variables: {} }; const outcome = new MockWorkerBuilder(() => {}).failWith(opts).resolve(job); assert.ok(outcome !== undefined, "failWith always yields an outcome"); return outcome; } // ============================================================================================= // 1. Negative / isolation. // ============================================================================================= test("isolation: a mock on type A leaves B and C running real code (multi-type)", async () => { await withEngine([{ name: "three.bpmn", xml: THREE_BPMN }], async (engine) => { const realRan: string[] = []; await engine.registerWorker("a", () => { throw new Error("real A must NOT run — it is mocked"); }); await engine.registerWorker("b", () => { realRan.push("b"); return { bDone: true }; }); await engine.registerWorker("c", () => { realRan.push("c"); return { cDone: true }; }); // Only A is mocked. engine.mockWorker("a").completeWith({ aMocked: true }); const { processInstanceKey } = await engine.createInstance({ processDefinitionId: "three" }); const [inst] = await engine.searchProcessInstances({ processInstanceKeys: [processInstanceKey] }); assert.equal(inst?.state, "ACTIVE", "A mocked, B and C real — the instance advanced to the user task"); assert.deepEqual(realRan, ["b", "c"], "the un-mocked types B and C both executed real code, in order"); const vars = instanceVariables(engine, processInstanceKey); assert.equal(vars.aMocked, true, "A's mocked completion merged"); assert.equal(vars.bDone, true); assert.equal(vars.cDone, true); }); }); test("isolation: removing one of several mocks restores only that type's real behaviour", async () => { await withEngine([{ name: "three.bpmn", xml: THREE_BPMN }], async (engine) => { const realRan: string[] = []; for (const type of ["a", "b", "c"]) { await engine.registerWorker(type, () => { realRan.push(type); return { [`${type}Real`]: true }; }); } engine.mockWorker("a").completeWith({ aMocked: true }); const mockB = engine.mockWorker("b").completeWith({ bMocked: true }); engine.mockWorker("c").completeWith({ cMocked: true }); // Remove only B's mock → B resumes real, A and C stay mocked. mockB.reset(); const { processInstanceKey } = await engine.createInstance({ processDefinitionId: "three" }); assert.deepEqual(realRan, ["b"], "only B's real handler ran after its mock was removed"); const vars = instanceVariables(engine, processInstanceKey); assert.equal(vars.aMocked, true, "A stayed mocked"); assert.equal(vars.bReal, true, "B ran real code"); assert.equal(vars.bMocked, undefined, "B's mock no longer applied"); assert.equal(vars.cMocked, true, "C stayed mocked"); }); }); test("native call activity: two un-mocked call activities each instantiate and run their deployed child", async () => { await withEngine( [ { name: "parent.bpmn", xml: PARENT_TWO }, { name: "childA.bpmn", xml: childModel("childA", "childAwork") }, { name: "childB.bpmn", xml: childModel("childB", "childBwork") }, ], async (engine) => { let childAReal = 0; let childBReal = 0; await engine.registerWorker("childAwork", () => { childAReal += 1; return { fromA: 1 }; }); await engine.registerWorker("childBwork", () => { childBReal += 1; return { childBRealRan: true }; }); const { processInstanceKey } = await engine.createInstance({ processDefinitionId: "parent" }); const [inst] = await engine.searchProcessInstances({ processInstanceKeys: [processInstanceKey] }); assert.equal(inst?.state, "ACTIVE", "both call activities resolved natively; the parent parked on the user task"); assert.deepEqual(engine.snapshot().activeElementIds, ["review"], "the parent parked past both call activities"); // Both children ran their REAL called processes exactly once — native execution, not a rewrite // (a deploy-time rewrite would have auto-completed the call activities and run neither worker). assert.equal(childAReal, 1, "child A's real called process ran exactly once (native execution)"); assert.equal(childBReal, 1, "child B's real called process ran exactly once (native execution)"); // NOTE: child→parent variable propagation (`propagateAllChildVariables`) is tracked separately // (Magikcraft/nano-bpm#1057) and intentionally not asserted here. }, ); }); test("native call activity: a call activity to an un-deployed child raises an incident (no silent auto-complete)", async () => { // Removing the deploy-time rewrite means an un-deployed called process is a real incident, not a // silent pass-through: the engine parks the parent on the call activity with a recoverable // `CalledElementError`. Consumers must deploy the child (production-faithful behaviour). await withEngine([{ name: "parent.bpmn", xml: PARENT_TWO }], async (engine) => { const { processInstanceKey } = await engine.createInstance({ processDefinitionId: "parent" }); const [inst] = await engine.searchProcessInstances({ processInstanceKeys: [processInstanceKey] }); assert.equal(inst?.state, "ACTIVE", "the parent parked on the call activity rather than completing"); assert.deepEqual(engine.snapshot().activeElementIds, ["caA"], "the parent parked on the first call activity"); assert.ok(incidentReasons(engine).length > 0, "the un-deployed called process raised an incident"); }); }); // ============================================================================================= // 2. Outcome fidelity — each outcome surfaces in observable instance state. // ============================================================================================= test("fidelity: completeWith surfaces the exact merged variables on the instance", async () => { await withEngine([{ name: "wait.bpmn", xml: WAIT_BPMN }], async (engine) => { engine.mockWorker("work").completeWith({ a: 1, nested: { deep: [true, "x"] }, kept: "yes" }); const { processInstanceKey } = await engine.createInstance({ processDefinitionId: "wait", variables: { seed: "keep-me" }, }); const vars = instanceVariables(engine, processInstanceKey); assert.equal(vars.a, 1); assert.deepEqual(vars.nested, { deep: [true, "x"] }, "structured completion variables round-trip verbatim"); assert.equal(vars.kept, "yes"); assert.equal(vars.seed, "keep-me", "pre-existing instance variables are preserved (merge, not replace)"); assert.equal(incidentReasons(engine).length, 0); }); }); test("fidelity: failWith carries its retry budget on the resolved outcome (retries preserved, message set)", () => { // Determinism boundary (documented, not a defect to fix here): a worker `failWith` applies a // *fixed* retry budget every time it resolves. When it drives a mock-only type (no advancing real // handler), the engine re-activates the failed job with no backoff under the virtual clock and the // mock re-applies the same fixed budget, so a POSITIVE-retry failure never reaches a drain // fixpoint (the drain's MAX_DRAIN_ITERATIONS guard is the safety net). This mirrors the merged // child-process slice, whose `failWith` is deliberately incident-only for exactly this reason. // Terminal, deterministically-drivable failures therefore use `retries: 0` (→ incident), asserted // in the two tests below. Here we assert the retry budget is faithfully carried on the resolved // outcome — fast and drain-free — so the value the engine would receive is correct. const b = engine_failWithProbe(2, "retry me"); assert.equal(b.kind, "fail"); assert.equal(b.kind === "fail" && b.retries, 2, "the positive retry budget is preserved on the fail outcome"); assert.equal(b.kind === "fail" && b.message, "retry me", "the message is carried through"); const dflt = engine_failWithProbe(undefined, undefined); assert.equal(dflt.kind === "fail" && dflt.retries, 0, "failWith defaults to a zero-retry (incident) failure"); }); test("fidelity: failWith default (retries 0) raises an incident carrying the message", async () => { await withEngine([{ name: "svc.bpmn", xml: SVC_BPMN }], async (engine) => { engine.mockWorker("work").failWith({ message: "hard stop" }); const { processInstanceKey } = await engine.createInstance({ processDefinitionId: "svc" }); const [inst] = await engine.searchProcessInstances({ processInstanceKeys: [processInstanceKey] }); assert.equal(inst?.state, "ACTIVE", "a zero-retry failure must not complete the instance"); assert.ok(incidentReasons(engine).includes("hard stop"), "the incident carries the failWith message"); }); }); test("fidelity: raiseIncident yields an incident visible in the snapshot", async () => { await withEngine([{ name: "svc.bpmn", xml: SVC_BPMN }], async (engine) => { engine.mockWorker("work").raiseIncident({ message: "explicit incident" }); const { processInstanceKey } = await engine.createInstance({ processDefinitionId: "svc" }); const [inst] = await engine.searchProcessInstances({ processInstanceKeys: [processInstanceKey] }); assert.equal(inst?.state, "ACTIVE"); assert.ok(incidentReasons(engine).includes("explicit incident"), "raiseIncident surfaces its message"); }); }); test("fidelity: throwBpmnError(code) takes the matching error-boundary flow (not an incident)", async () => { await withEngine([{ name: "boundary.bpmn", xml: BOUNDARY_BPMN }], async (engine) => { engine.mockWorker("work").throwBpmnError("BOOM", "mocked boom"); const { processInstanceKey } = await engine.createInstance({ processDefinitionId: "boundary" }); const [inst] = await engine.searchProcessInstances({ processInstanceKeys: [processInstanceKey] }); assert.equal(inst?.state, "COMPLETED", "the BPMN error was caught by the boundary and drove the instance to completion"); assert.equal(incidentReasons(engine).length, 0, "a caught BPMN error raises no incident"); }); }); test("fidelity: throwBpmnError with a NON-matching code raises an incident instead of being caught", async () => { await withEngine([{ name: "boundary.bpmn", xml: BOUNDARY_BPMN }], async (engine) => { // The boundary only catches BOOM; an unmatched error code is an uncaught BPMN error → incident, // NOT a silent completion. This proves throwBpmnError routes by code, not unconditionally. engine.mockWorker("work").throwBpmnError("NOPE", "unmatched boom"); const { processInstanceKey } = await engine.createInstance({ processDefinitionId: "boundary" }); const [inst] = await engine.searchProcessInstances({ processInstanceKeys: [processInstanceKey] }); assert.notEqual(inst?.state, "COMPLETED", "an unmatched error code must not be silently caught by the BOOM boundary"); }); }); // ============================================================================================= // 3. Condition correctness. // ============================================================================================= test("conditions: first-match wins over three tiers, in registration order", async () => { await withEngine([{ name: "wait.bpmn", xml: WAIT_BPMN }], async (engine) => { engine .mockWorker("work") .when((job) => job.variables.tier === "gold").completeWith({ picked: "gold" }) .when((job) => job.variables.tier === "silver").completeWith({ picked: "silver" }) .when((job) => job.variables.tier === "bronze").completeWith({ picked: "bronze" }); for (const tier of ["gold", "silver", "bronze"]) { const { processInstanceKey } = await engine.createInstance({ processDefinitionId: "wait", variables: { tier }, }); assert.equal(instanceVariables(engine, processInstanceKey).picked, tier, `${tier} matched its own clause`); } }); }); test("conditions: an earlier clause shadows a later one that would also match", async () => { await withEngine([{ name: "wait.bpmn", xml: WAIT_BPMN }], async (engine) => { // Both clauses match a vip gold job; the FIRST registered must win. engine .mockWorker("work") .when((job) => job.variables.vip === true).completeWith({ by: "vip-clause" }) .when((job) => job.variables.tier === "gold").completeWith({ by: "gold-clause" }); const { processInstanceKey } = await engine.createInstance({ processDefinitionId: "wait", variables: { vip: true, tier: "gold" }, }); assert.equal(instanceVariables(engine, processInstanceKey).by, "vip-clause", "the earlier matching clause wins"); }); }); test("conditions: a job matching no clause falls through to the real handler", async () => { await withEngine([{ name: "wait.bpmn", xml: WAIT_BPMN }], async (engine) => { let realRan = false; await engine.registerWorker("work", (job) => { realRan = true; return { by: "real", echo: job.variables.echo }; }); engine.mockWorker("work").when((job) => job.variables.tier === "gold").completeWith({ by: "mock" }); const { processInstanceKey } = await engine.createInstance({ processDefinitionId: "wait", variables: { tier: "bronze", echo: 7 }, }); const vars = instanceVariables(engine, processInstanceKey); assert.equal(realRan, true, "an unmatched job fell through to the real handler"); assert.equal(vars.by, "real"); assert.equal(vars.echo, 7); }); }); test("conditions: a when(...) clause can fall through to a later unconditional default", async () => { await withEngine([{ name: "wait.bpmn", xml: WAIT_BPMN }], async (engine) => { engine .mockWorker("work") .when((job) => job.variables.special === true).completeWith({ path: "special" }) .completeWith({ path: "default" }); const special = await engine.createInstance({ processDefinitionId: "wait", variables: { special: true } }); assert.equal(instanceVariables(engine, special.processInstanceKey).path, "special"); const plain = await engine.createInstance({ processDefinitionId: "wait", variables: { special: false } }); assert.equal(instanceVariables(engine, plain.processInstanceKey).path, "default", "the default caught the rest"); }); }); // The following four tests exercise a `when(...)` predicate over EACH matchable `EngineJob` field // (jobType, variables, elementId, processInstanceKey, jobKey). `mock-completeness-guard.test.ts` // scans these predicates and FAILS if any EngineJob field is not reachable by a tested predicate — // so adding a new matchable field to EngineJob forces a predicate + test here. test("conditions: predicate over job.jobType and job.variables selects the right job", async () => { await withEngine([{ name: "wait.bpmn", xml: WAIT_BPMN }], async (engine) => { engine .mockWorker("work") .when((job) => job.jobType === "work" && job.variables.pick === true).completeWith({ matched: true }); const hit = await engine.createInstance({ processDefinitionId: "wait", variables: { pick: true } }); assert.equal(instanceVariables(engine, hit.processInstanceKey).matched, true, "jobType+variables predicate matched"); let realRan = false; await engine.registerWorker("work", () => { realRan = true; return { matched: false }; }); const miss = await engine.createInstance({ processDefinitionId: "wait", variables: { pick: false } }); assert.equal(realRan, true, "a job whose variables don't match fell through to the real handler"); assert.equal(instanceVariables(engine, miss.processInstanceKey).matched, false); }); }); test("conditions: predicate over job.elementId selects by the BPMN element", async () => { await withEngine([{ name: "wait.bpmn", xml: WAIT_BPMN }], async (engine) => { // `work` is the serviceTask element id; matching on it proves elementId is populated & matchable. engine.mockWorker("work").when((job) => job.elementId === "work").completeWith({ byElement: true }); const { processInstanceKey } = await engine.createInstance({ processDefinitionId: "wait" }); assert.equal(instanceVariables(engine, processInstanceKey).byElement, true, "elementId predicate matched the serviceTask"); }); }); test("conditions: predicate over job.processInstanceKey and job.jobKey sees real, non-empty keys", async () => { await withEngine([{ name: "wait.bpmn", xml: WAIT_BPMN }], async (engine) => { const seen: { pik: string | undefined; jk: string }[] = []; engine .mockWorker("work") .when((job) => { seen.push({ pik: job.processInstanceKey, jk: job.jobKey }); return job.processInstanceKey !== undefined && job.jobKey.length > 0; }) .completeWith({ keyed: true }); const { processInstanceKey } = await engine.createInstance({ processDefinitionId: "wait" }); assert.equal(instanceVariables(engine, processInstanceKey).keyed, true, "the key predicate matched"); assert.equal(seen.length >= 1, true, "the predicate was evaluated"); assert.equal(seen[0]?.jk.length ? true : false, true, "jobKey is a non-empty string on the dispatched job"); assert.equal(seen[0]?.pik, processInstanceKey, "processInstanceKey on the job is the parent instance key"); }); });