// Red/Green coverage for `assertThatInstance` (instance-assert slice).
//
// Every matcher is proven twice: it PASSES on the positive case and THROWS an
// `AssertionError` on the negative case (a matcher that cannot fail is worthless).
// A real app is booted via `bootTestApp`; instances are driven through the engine
// to ACTIVE / COMPLETED / TERMINATED and to a job-no-retries incident. Everything
// is deterministic — no wall-clock, no polling — and runs under both `node --test`
// and Deno.
import { test } from "node:test";
import assert from "node:assert/strict";
import { AssertionError } from "node:assert";
import { mkdtemp, mkdir, writeFile, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { bootTestApp, type TestApp } from "../boot-app.ts";
import { assertThatInstance } from "./instance.ts";
import { byKey, byProcessId } from "../index.ts";
// A single-service-task process. Deployed per-test through the engine so each
// case owns an isolated snapshot (the completed-element stats are aggregate).
function serviceProcess(id: string, taskType: string, retries = "3"): string {
return `
`;
}
// A minimal but complete app on disk: bootTestApp requires a mounted data layer,
// so the manifest declares a trivial SQLite source. Processes are deployed at
// runtime via `app.engine`, so the manifest needs no process/worker of its own.
const NOOP_MIGRATION = "CREATE TABLE _boot (id INTEGER PRIMARY KEY);";
async function makeFixture(): Promise {
const dir = await mkdtemp(join(tmpdir(), "urban-testkit-instance-"));
await mkdir(join(dir, "db", "migrations"), { recursive: true });
await writeFile(join(dir, "db", "migrations", "001_init.sql"), NOOP_MIGRATION);
const manifest = {
schemaVersion: 1,
id: "testkit-instance-fixture",
name: "Testkit Instance Fixture",
data: {
default: "app",
sources: { app: { driver: "sqlite", url: "file:./db/app.db", migrations: "db/migrations" } },
},
};
await writeFile(join(dir, "nano.app.json"), JSON.stringify(manifest, null, 2));
return dir;
}
async function withApp(fn: (app: TestApp) => Promise): Promise {
const dir = await makeFixture();
const app = await bootTestApp(dir);
try {
await fn(app);
} finally {
await app.stop();
await rm(dir, { recursive: true, force: true });
}
}
/** Assert `fn` throws a `node:assert` `AssertionError` whose message contains
* every fragment in `contains` (proves the message NAMES the actual state). */
function expectFailure(fn: () => unknown, contains: string[]): void {
let thrown: unknown;
try {
fn();
} catch (err) {
thrown = err;
}
assert.ok(thrown instanceof AssertionError, "matcher should throw an AssertionError on the negative case");
for (const fragment of contains) {
assert.ok(
thrown.message.includes(fragment),
`failure message should mention ${JSON.stringify(fragment)} — got:\n${thrown.message}`,
);
}
}
// --- State matchers: one per ProcessInstanceState member (ACTIVE/COMPLETED/TERMINATED) ---
test("isActive passes for an ACTIVE instance and fails otherwise", async () => {
await withApp(async (app) => {
await app.engine.deployResources([
{ name: "act.bpmn", content: serviceProcess("act", "act.work"), contentType: "application/bpmn+xml" },
]);
// No worker registered → the job parks the instance ACTIVE at `work`.
const { processInstanceKey } = await app.engine.createInstance({ processDefinitionId: "act" });
assertThatInstance(app, processInstanceKey).isActive();
expectFailure(() => assertThatInstance(app, processInstanceKey).hasCompleted(), [
"COMPLETED",
"ACTIVE",
]);
});
});
test("hasCompleted passes for a COMPLETED instance and fails otherwise", async () => {
await withApp(async (app) => {
await app.engine.deployResources([
{ name: "cmp.bpmn", content: serviceProcess("cmp", "cmp.work"), contentType: "application/bpmn+xml" },
]);
await app.engine.registerWorker("cmp.work", () => ({ ok: true }));
const { processInstanceKey } = await app.engine.createInstance({
processDefinitionId: "cmp",
awaitCompletion: true,
});
assertThatInstance(app, processInstanceKey).hasCompleted();
expectFailure(() => assertThatInstance(app, processInstanceKey).isTerminated(), [
"TERMINATED",
"COMPLETED",
]);
});
});
test("isTerminated passes for a cancelled instance and fails otherwise", async () => {
await withApp(async (app) => {
await app.engine.deployResources([
{ name: "trm.bpmn", content: serviceProcess("trm", "trm.work"), contentType: "application/bpmn+xml" },
]);
const { processInstanceKey } = await app.engine.createInstance({ processDefinitionId: "trm" });
await app.engine.cancelInstance({ processInstanceKey });
assertThatInstance(app, processInstanceKey).isTerminated();
expectFailure(() => assertThatInstance(app, processInstanceKey).isActive(), [
"ACTIVE",
"TERMINATED",
]);
});
});
// --- Element matchers: {active, completed} scope ---
test("hasActiveElement / hasActiveElements pass for live tokens and fail otherwise", async () => {
await withApp(async (app) => {
await app.engine.deployResources([
{ name: "el.bpmn", content: serviceProcess("el", "el.work"), contentType: "application/bpmn+xml" },
]);
const { processInstanceKey } = await app.engine.createInstance({ processDefinitionId: "el" });
assertThatInstance(app, processInstanceKey).hasActiveElement("work").hasActiveElements("work");
expectFailure(() => assertThatInstance(app, processInstanceKey).hasActiveElement("ghost"), [
"ghost",
"work",
]);
expectFailure(() => assertThatInstance(app, processInstanceKey).hasActiveElements("work", "ghost"), [
"ghost",
]);
});
});
test("hasCompletedElements passes for completed elements and fails otherwise", async () => {
await withApp(async (app) => {
await app.engine.deployResources([
{ name: "ce.bpmn", content: serviceProcess("ce", "ce.work"), contentType: "application/bpmn+xml" },
]);
await app.engine.registerWorker("ce.work", () => ({ ok: true }));
const { processInstanceKey } = await app.engine.createInstance({
processDefinitionId: "ce",
awaitCompletion: true,
});
// s, work and e all completed on the way to the end event.
assertThatInstance(app, processInstanceKey).hasCompletedElements("s", "work", "e");
expectFailure(() => assertThatInstance(app, processInstanceKey).hasCompletedElements("s", "ghost"), [
"ghost",
]);
});
});
test("element-set failures set the diff `expected` to the MISSING ids, not the full requested set", async () => {
// Guards a headline/diff coherence defect class: the failure headline names the
// MISSING element ids, so the structured `expected` diff value must be that same
// `missing` set — not the full requested `elementIds` (which would leave the diff
// internally inconsistent with the headline when some requested ids are present).
const captureExpected = (fn: () => unknown): unknown => {
try {
fn();
} catch (err) {
assert.ok(err instanceof AssertionError, "matcher should throw an AssertionError on the negative case");
return err.expected;
}
assert.fail("matcher should have thrown");
};
await withApp(async (app) => {
await app.engine.deployResources([
{ name: "elx.bpmn", content: serviceProcess("elx", "elx.work"), contentType: "application/bpmn+xml" },
]);
// Active: `work` is live, `ghost` is missing → expected diff is exactly ["ghost"].
const { processInstanceKey: activeKey } = await app.engine.createInstance({ processDefinitionId: "elx" });
assert.deepEqual(
captureExpected(() => assertThatInstance(app, activeKey).hasActiveElements("work", "ghost")),
["ghost"],
);
});
await withApp(async (app) => {
await app.engine.deployResources([
{ name: "cex.bpmn", content: serviceProcess("cex", "cex.work"), contentType: "application/bpmn+xml" },
]);
await app.engine.registerWorker("cex.work", () => ({ ok: true }));
const { processInstanceKey: doneKey } = await app.engine.createInstance({
processDefinitionId: "cex",
awaitCompletion: true,
});
// Completed: `s` completed, `ghost` is missing → expected diff is exactly ["ghost"].
assert.deepEqual(
captureExpected(() => assertThatInstance(app, doneKey).hasCompletedElements("s", "ghost")),
["ghost"],
);
});
});
test("hasCompletedElements fails fast when the snapshot holds more than one instance", async () => {
await withApp(async (app) => {
// Two DISTINCT processes so both instances coexist in the snapshot: one
// COMPLETED (its elements land in the aggregate `elementStats`) and one left
// ACTIVE. Because completion counts are snapshot-global, the matcher cannot
// attribute a completed element to a single instance here — it must refuse
// rather than silently borrow another instance's completions.
await app.engine.deployResources([
{ name: "m1.bpmn", content: serviceProcess("m1", "m1.work"), contentType: "application/bpmn+xml" },
{ name: "m2.bpmn", content: serviceProcess("m2", "m2.work"), contentType: "application/bpmn+xml" },
]);
await app.engine.registerWorker("m1.work", () => ({ ok: true }));
const { processInstanceKey: doneKey } = await app.engine.createInstance({
processDefinitionId: "m1",
awaitCompletion: true,
});
// No worker for m2 → this instance parks ACTIVE, so two instances coexist.
await app.engine.createInstance({ processDefinitionId: "m2" });
// Even asking about an element the resolved instance genuinely completed
// ("s") must throw, because the verdict would not be per-instance honest.
expectFailure(() => assertThatInstance(app, doneKey).hasCompletedElements("s"), [
"more than one instance",
"unsound",
]);
});
});
test("hasCompletedElements refuses when the resolved instance has vanished from the snapshot", async () => {
await withApp(async (app) => {
// A single COMPLETED instance: its elements land in the aggregate
// `elementStats`, and it is the snapshot's sole instance, so a normal
// `hasCompletedElements` verdict would be sound.
await app.engine.deployResources([
{ name: "vn.bpmn", content: serviceProcess("vn", "vn.work"), contentType: "application/bpmn+xml" },
]);
await app.engine.registerWorker("vn.work", () => ({ ok: true }));
const { processInstanceKey } = await app.engine.createInstance({
processDefinitionId: "vn",
awaitCompletion: true,
});
// Resolve the assertion against the real (correct) snapshot, THEN simulate the
// target instance vanishing while a lone UNRELATED instance remains — the exact
// shape the `instanceCount > 1` guard does NOT cover (count is 1, but it is the
// wrong instance). Reading snapshot-global `elementStats` here would borrow the
// vanished instance's completions and return an unsound verdict; the matcher must
// instead refuse because the resolved instance is no longer present.
const real = app.snapshot();
const doctored: Record = {
...real,
instances: [{ key: "OTHER", state: "Active", processId: "vn" }],
};
let vanished = false;
const doctoredApp: TestApp = { ...app, snapshot: () => (vanished ? doctored : real) };
const subject = assertThatInstance(doctoredApp, processInstanceKey);
vanished = true;
expectFailure(() => subject.hasCompletedElements("s"), [processInstanceKey, "no longer present"]);
});
});
// --- Variable matchers ---
test("hasVariable / hasVariables / hasNoVariable pass and fail as specified", async () => {
await withApp(async (app) => {
await app.engine.deployResources([
{ name: "vr.bpmn", content: serviceProcess("vr", "vr.work"), contentType: "application/bpmn+xml" },
]);
// Parked ACTIVE (no worker) so variables are retained in the snapshot.
const { processInstanceKey } = await app.engine.createInstance({
processDefinitionId: "vr",
variables: { who: "world", count: 3, nested: { a: 1, b: [2, 3] } },
});
const inst = () => assertThatInstance(app, processInstanceKey);
// hasVariable — deep equality.
inst().hasVariable("who", "world").hasVariable("nested", { a: 1, b: [2, 3] });
expectFailure(() => inst().hasVariable("who", "mars"), ['"world"', '"mars"']);
expectFailure(() => inst().hasVariable("missing", 1), ["missing"]);
// hasVariables — deep subset (extra vars, e.g. _urban lineage, ignored).
inst().hasVariables({ who: "world", count: 3 });
expectFailure(() => inst().hasVariables({ who: "world", count: 99 }), ["count"]);
// hasNoVariable.
inst().hasNoVariable("absent");
expectFailure(() => inst().hasNoVariable("who"), ["who", '"world"']);
});
});
// --- Instance-level incident matchers ---
test("hasIncident / hasNoIncident pass and fail, and hasIncident narrows by selector", async () => {
await withApp(async (app) => {
// Incident instance: worker throws with retries=1 → job-no-retries incident on `work`.
await app.engine.deployResources([
{ name: "inc.bpmn", content: serviceProcess("inc", "inc.work", "1"), contentType: "application/bpmn+xml" },
]);
await app.engine.registerWorker("inc.work", () => {
throw new Error("kaboom");
});
const { processInstanceKey: incKey } = await app.engine.createInstance({ processDefinitionId: "inc" });
await app.engine.drain();
// Healthy instance: parks ACTIVE with no incident.
await app.engine.deployResources([
{ name: "ok.bpmn", content: serviceProcess("ok", "ok.work"), contentType: "application/bpmn+xml" },
]);
const { processInstanceKey: okKey } = await app.engine.createInstance({ processDefinitionId: "ok" });
// hasIncident (unfiltered + narrowed by element id and by error message).
assertThatInstance(app, incKey)
.hasIncident()
.hasIncident({ elementId: "work" })
.hasIncident({ errorMessage: "kaboom" })
.hasIncident({ elementId: "work", errorMessage: "kaboom" });
expectFailure(() => assertThatInstance(app, okKey).hasIncident(), ["to have an incident", "it has no incidents"]);
// A selector that matches no incident on an instance that HAS one still fails, naming the actual incident.
expectFailure(() => assertThatInstance(app, incKey).hasIncident({ elementId: "s" }), ["kaboom", "work"]);
// hasNoIncident.
assertThatInstance(app, okKey).hasNoIncident();
expectFailure(() => assertThatInstance(app, incKey).hasNoIncident(), ["kaboom", "work"]);
});
});
// --- Selector forms all resolve through the shared resolver / convenience ---
test("assertThatInstance resolves by key, byKey(...), byProcessId(...), and the single-ACTIVE default", async () => {
await withApp(async (app) => {
await app.engine.deployResources([
{ name: "sel.bpmn", content: serviceProcess("order", "order.work"), contentType: "application/bpmn+xml" },
]);
const { processInstanceKey } = await app.engine.createInstance({ processDefinitionId: "order" });
assertThatInstance(app, processInstanceKey).isActive(); // bare string key
assertThatInstance(app, byKey(processInstanceKey)).isActive(); // byKey selector
assertThatInstance(app, byProcessId("order")).isActive(); // byProcessId selector
assertThatInstance(app).isActive(); // single-ACTIVE convenience
// Unresolvable selectors throw an intent-revealing AssertionError.
expectFailure(() => assertThatInstance(app, byProcessId("nope")), ["nope"]);
expectFailure(() => assertThatInstance(app, "does-not-exist"), ["does-not-exist"]);
});
});
// --- Chainability across matcher families ---
test("matchers chain across state, elements, variables and incidents", async () => {
await withApp(async (app) => {
await app.engine.deployResources([
{ name: "ch.bpmn", content: serviceProcess("ch", "ch.work"), contentType: "application/bpmn+xml" },
]);
const { processInstanceKey } = await app.engine.createInstance({
processDefinitionId: "ch",
variables: { who: "world" },
});
const returned = assertThatInstance(app, processInstanceKey)
.isActive()
.hasActiveElement("work")
.hasVariable("who", "world")
.hasVariables({ who: "world" })
.hasNoVariable("absent")
.hasNoIncident();
assert.equal(typeof returned.isActive, "function", "each matcher returns the fluent object");
});
});