@nanobpm/urban 0.91.0 → 0.92.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/runtime/core/host.d.ts +17 -13
- package/dist/runtime/core/host.js +7 -5
- package/dist/runtime/core/modules/mcp.js +7 -7
- package/dist/runtime/engine/nanosdk.js +4 -3
- package/package.json +1 -1
- package/src/runtime/core/host.ts +18 -14
- package/src/runtime/core/modules/mcp.test.ts +61 -11
- package/src/runtime/core/modules/mcp.ts +7 -6
- package/src/runtime/engine/nanosdk.test.ts +58 -10
- package/src/runtime/engine/nanosdk.ts +4 -3
|
@@ -314,16 +314,20 @@ export interface ElementInstanceSummary {
|
|
|
314
314
|
* {@link ElementInstanceWaitStateFilter} whose `waitStateType` falls outside that floor is
|
|
315
315
|
* rejected (the gateway answers HTTP 422); {@link assertDeployedWaitStateType} enforces the
|
|
316
316
|
* same floor client-side so the divergence surfaces as a clear error offline, in tests, and
|
|
317
|
-
* against a live engine alike.
|
|
317
|
+
* against a live engine alike. The floor now covers `JOB | MESSAGE | USER_TASK`
|
|
318
|
+
* (Magikcraft/nano-bpm#1042 shipped `USER_TASK` parks); `TIMER`/`SIGNAL`/`CONDITION` remain
|
|
319
|
+
* the follow-on. Ref: Magikcraft/nano-bpm#1042.
|
|
318
320
|
*/
|
|
319
321
|
export type WaitStateType = "JOB" | "MESSAGE" | "USER_TASK" | "TIMER" | "SIGNAL" | "CONDITION";
|
|
320
322
|
/**
|
|
321
323
|
* The `waitStateType` values the *currently deployed* nanobpmn gateway's wait-state read
|
|
322
324
|
* model actually implements — the **deployed floor**, a strict subset of the canonical
|
|
323
|
-
* {@link WaitStateType} union. The gateway's read model
|
|
324
|
-
* taken 2026-06-09
|
|
325
|
-
*
|
|
326
|
-
* with
|
|
325
|
+
* {@link WaitStateType} union. The gateway's read model started as a snapshot of the Zeebe
|
|
326
|
+
* contract taken 2026-06-09 that recognized only `JOB | MESSAGE`; Magikcraft/nano-bpm#1042
|
|
327
|
+
* has since shipped `USER_TASK` parks (added on user-task CREATED, removed on
|
|
328
|
+
* COMPLETED/CANCELED, with the park identity under `details.taskKey`), moving the floor up to
|
|
329
|
+
* `JOB | MESSAGE | USER_TASK`. `TIMER`/`SIGNAL`/`CONDITION` are still rejected with HTTP 422
|
|
330
|
+
* (the 8.10 follow-on tracked in Magikcraft/nano-bpm#1042).
|
|
327
331
|
*
|
|
328
332
|
* This constant is the **single source of truth** for that floor: the SDK-backed and
|
|
329
333
|
* WASM-backed {@link EngineClient} adapters both gate through {@link assertDeployedWaitStateType},
|
|
@@ -645,14 +649,14 @@ export interface EngineClient {
|
|
|
645
649
|
* (see {@link ElementInstanceWaitState}), so a consumer can read the job/message parks a
|
|
646
650
|
* {@link searchUserTasks} cannot surface.
|
|
647
651
|
*
|
|
648
|
-
* **Deployed floor.** The currently deployed nanobpmn gateway implements
|
|
649
|
-
* `JOB | MESSAGE` ({@link DEPLOYED_WAIT_STATE_TYPES})
|
|
650
|
-
* {@link searchUserTasks}
|
|
651
|
-
* with {@link UnsupportedWaitStateTypeError} (the
|
|
652
|
-
* unfiltered search returns only in-floor parks. The
|
|
653
|
-
* forward-looking (richer engines may serve more), but this adapter
|
|
654
|
-
* floor so a query cannot pass in emulation while a real engine rejects it.
|
|
655
|
-
* Magikcraft/nano-bpm#1042.
|
|
652
|
+
* **Deployed floor.** The currently deployed nanobpmn gateway implements
|
|
653
|
+
* `JOB | MESSAGE | USER_TASK` ({@link DEPLOYED_WAIT_STATE_TYPES}); a `USER_TASK` park can be
|
|
654
|
+
* read here (or through {@link searchUserTasks}). A `waitStateType` filter outside the floor
|
|
655
|
+
* (`TIMER`/`SIGNAL`/`CONDITION`) is rejected with {@link UnsupportedWaitStateTypeError} (the
|
|
656
|
+
* live gateway answers HTTP 422); an unfiltered search returns only in-floor parks. The
|
|
657
|
+
* `waitStateType` union is forward-looking (richer engines may serve more), but this adapter
|
|
658
|
+
* contract pins the floor so a query cannot pass in emulation while a real engine rejects it.
|
|
659
|
+
* Ref: Magikcraft/nano-bpm#1042.
|
|
656
660
|
*/
|
|
657
661
|
searchElementInstanceWaitStates(filter?: ElementInstanceWaitStateFilter): Promise<ElementInstanceWaitState[]>;
|
|
658
662
|
/**
|
|
@@ -45,10 +45,12 @@ export function presentFormIdentifier(value) {
|
|
|
45
45
|
/**
|
|
46
46
|
* The `waitStateType` values the *currently deployed* nanobpmn gateway's wait-state read
|
|
47
47
|
* model actually implements — the **deployed floor**, a strict subset of the canonical
|
|
48
|
-
* {@link WaitStateType} union. The gateway's read model
|
|
49
|
-
* taken 2026-06-09
|
|
50
|
-
*
|
|
51
|
-
* with
|
|
48
|
+
* {@link WaitStateType} union. The gateway's read model started as a snapshot of the Zeebe
|
|
49
|
+
* contract taken 2026-06-09 that recognized only `JOB | MESSAGE`; Magikcraft/nano-bpm#1042
|
|
50
|
+
* has since shipped `USER_TASK` parks (added on user-task CREATED, removed on
|
|
51
|
+
* COMPLETED/CANCELED, with the park identity under `details.taskKey`), moving the floor up to
|
|
52
|
+
* `JOB | MESSAGE | USER_TASK`. `TIMER`/`SIGNAL`/`CONDITION` are still rejected with HTTP 422
|
|
53
|
+
* (the 8.10 follow-on tracked in Magikcraft/nano-bpm#1042).
|
|
52
54
|
*
|
|
53
55
|
* This constant is the **single source of truth** for that floor: the SDK-backed and
|
|
54
56
|
* WASM-backed {@link EngineClient} adapters both gate through {@link assertDeployedWaitStateType},
|
|
@@ -57,7 +59,7 @@ export function presentFormIdentifier(value) {
|
|
|
57
59
|
* backed by the real gateway) only once the engine actually serves the richer type — the
|
|
58
60
|
* tracking gap is Magikcraft/nano-bpm#1042.
|
|
59
61
|
*/
|
|
60
|
-
export const DEPLOYED_WAIT_STATE_TYPES = ["JOB", "MESSAGE"];
|
|
62
|
+
export const DEPLOYED_WAIT_STATE_TYPES = ["JOB", "MESSAGE", "USER_TASK"];
|
|
61
63
|
/**
|
|
62
64
|
* Thrown when a wait-state search is asked to filter on a {@link WaitStateType} the deployed
|
|
63
65
|
* gateway's read model does not implement (outside {@link DEPLOYED_WAIT_STATE_TYPES}). It
|
|
@@ -41,7 +41,6 @@ import { resolveBindMode } from "../manifest.js";
|
|
|
41
41
|
import { json, makeRouter } from "../router.js";
|
|
42
42
|
import { API_BASE, readApiBinding } from "./api.js";
|
|
43
43
|
import { resolveAppPath } from "./datasource.js";
|
|
44
|
-
import { InstanceStateStore } from "./instance-state-store.js";
|
|
45
44
|
import { OpenUserTasksStore } from "./open-user-tasks-store.js";
|
|
46
45
|
/** The mount path of the MCP endpoint — a sibling of `/app/api` / `/app/agent`, mounted
|
|
47
46
|
* unconditionally so an MCP client always has a stable address to connect to. */
|
|
@@ -682,18 +681,19 @@ function buildDebugTools(app) {
|
|
|
682
681
|
},
|
|
683
682
|
{
|
|
684
683
|
name: `${DEBUG_PREFIX}instance_state`,
|
|
685
|
-
description: "
|
|
684
|
+
description: "Engine truth: the live lifecycle state of a process instance (ACTIVE/COMPLETED/TERMINATED) via searchProcessInstances, plus `waitingOnHuman` from its open user tasks. `null` means the engine has no such instance (not merely 'not yet terminal').",
|
|
686
685
|
inputSchema: {
|
|
687
686
|
type: "object",
|
|
688
687
|
properties: { processInstanceKey: OPTIONAL_STRING },
|
|
689
688
|
required: ["processInstanceKey"],
|
|
690
689
|
},
|
|
691
|
-
run: (args) => {
|
|
690
|
+
run: async (args) => {
|
|
692
691
|
const key = requireString(args, "processInstanceKey");
|
|
693
|
-
const
|
|
694
|
-
if (!
|
|
695
|
-
return
|
|
696
|
-
|
|
692
|
+
const [snapshot] = await app.engine.searchProcessInstances({ processInstanceKeys: [key] });
|
|
693
|
+
if (!snapshot)
|
|
694
|
+
return null;
|
|
695
|
+
const openTasks = await app.engine.openUserTasks({ processInstanceKey: key });
|
|
696
|
+
return { processInstanceKey: key, state: snapshot.state, waitingOnHuman: openTasks.length > 0 };
|
|
697
697
|
},
|
|
698
698
|
},
|
|
699
699
|
{
|
|
@@ -640,9 +640,10 @@ export class SdkEngineClient {
|
|
|
640
640
|
}
|
|
641
641
|
async searchElementInstanceWaitStates(filter) {
|
|
642
642
|
// The wait-states search surfaces the parks the deployed engine's read model serves.
|
|
643
|
-
// A `waitStateType` selector outside the deployed floor (`JOB | MESSAGE`) is
|
|
644
|
-
// the gateway with HTTP 422; fail fast client-side with a clear error instead,
|
|
645
|
-
// divergence is caught offline and identically to the WASM adapter (No Drift
|
|
643
|
+
// A `waitStateType` selector outside the deployed floor (`JOB | MESSAGE | USER_TASK`) is
|
|
644
|
+
// rejected by the gateway with HTTP 422; fail fast client-side with a clear error instead,
|
|
645
|
+
// so the divergence is caught offline and identically to the WASM adapter (No Drift
|
|
646
|
+
// Surfaces).
|
|
646
647
|
assertDeployedWaitStateType(filter?.waitStateType);
|
|
647
648
|
// Same zero-wait read + per-row mapping-gate shape as `searchElementInstances`; the SDK
|
|
648
649
|
// nests park-specific fields under `details`, which `mapElementInstanceWaitStateRow`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nanobpm/urban",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.92.1",
|
|
4
4
|
"description": "Urban: build and run code-first apps on Nano — runtime, derivation toolkit, and CLI in one. Author durable processes with @nanobpm/workflow (defineFlow), a typed datasource, forms, triggers and surfaces from a nano.app.json manifest, on Node or Deno.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
package/src/runtime/core/host.ts
CHANGED
|
@@ -350,7 +350,9 @@ export interface ElementInstanceSummary {
|
|
|
350
350
|
* {@link ElementInstanceWaitStateFilter} whose `waitStateType` falls outside that floor is
|
|
351
351
|
* rejected (the gateway answers HTTP 422); {@link assertDeployedWaitStateType} enforces the
|
|
352
352
|
* same floor client-side so the divergence surfaces as a clear error offline, in tests, and
|
|
353
|
-
* against a live engine alike.
|
|
353
|
+
* against a live engine alike. The floor now covers `JOB | MESSAGE | USER_TASK`
|
|
354
|
+
* (Magikcraft/nano-bpm#1042 shipped `USER_TASK` parks); `TIMER`/`SIGNAL`/`CONDITION` remain
|
|
355
|
+
* the follow-on. Ref: Magikcraft/nano-bpm#1042.
|
|
354
356
|
*/
|
|
355
357
|
export type WaitStateType =
|
|
356
358
|
| "JOB"
|
|
@@ -363,10 +365,12 @@ export type WaitStateType =
|
|
|
363
365
|
/**
|
|
364
366
|
* The `waitStateType` values the *currently deployed* nanobpmn gateway's wait-state read
|
|
365
367
|
* model actually implements — the **deployed floor**, a strict subset of the canonical
|
|
366
|
-
* {@link WaitStateType} union. The gateway's read model
|
|
367
|
-
* taken 2026-06-09
|
|
368
|
-
*
|
|
369
|
-
* with
|
|
368
|
+
* {@link WaitStateType} union. The gateway's read model started as a snapshot of the Zeebe
|
|
369
|
+
* contract taken 2026-06-09 that recognized only `JOB | MESSAGE`; Magikcraft/nano-bpm#1042
|
|
370
|
+
* has since shipped `USER_TASK` parks (added on user-task CREATED, removed on
|
|
371
|
+
* COMPLETED/CANCELED, with the park identity under `details.taskKey`), moving the floor up to
|
|
372
|
+
* `JOB | MESSAGE | USER_TASK`. `TIMER`/`SIGNAL`/`CONDITION` are still rejected with HTTP 422
|
|
373
|
+
* (the 8.10 follow-on tracked in Magikcraft/nano-bpm#1042).
|
|
370
374
|
*
|
|
371
375
|
* This constant is the **single source of truth** for that floor: the SDK-backed and
|
|
372
376
|
* WASM-backed {@link EngineClient} adapters both gate through {@link assertDeployedWaitStateType},
|
|
@@ -375,7 +379,7 @@ export type WaitStateType =
|
|
|
375
379
|
* backed by the real gateway) only once the engine actually serves the richer type — the
|
|
376
380
|
* tracking gap is Magikcraft/nano-bpm#1042.
|
|
377
381
|
*/
|
|
378
|
-
export const DEPLOYED_WAIT_STATE_TYPES: readonly WaitStateType[] = ["JOB", "MESSAGE"];
|
|
382
|
+
export const DEPLOYED_WAIT_STATE_TYPES: readonly WaitStateType[] = ["JOB", "MESSAGE", "USER_TASK"];
|
|
379
383
|
|
|
380
384
|
/**
|
|
381
385
|
* Thrown when a wait-state search is asked to filter on a {@link WaitStateType} the deployed
|
|
@@ -701,14 +705,14 @@ export interface EngineClient {
|
|
|
701
705
|
* (see {@link ElementInstanceWaitState}), so a consumer can read the job/message parks a
|
|
702
706
|
* {@link searchUserTasks} cannot surface.
|
|
703
707
|
*
|
|
704
|
-
* **Deployed floor.** The currently deployed nanobpmn gateway implements
|
|
705
|
-
* `JOB | MESSAGE` ({@link DEPLOYED_WAIT_STATE_TYPES})
|
|
706
|
-
* {@link searchUserTasks}
|
|
707
|
-
* with {@link UnsupportedWaitStateTypeError} (the
|
|
708
|
-
* unfiltered search returns only in-floor parks. The
|
|
709
|
-
* forward-looking (richer engines may serve more), but this adapter
|
|
710
|
-
* floor so a query cannot pass in emulation while a real engine rejects it.
|
|
711
|
-
* Magikcraft/nano-bpm#1042.
|
|
708
|
+
* **Deployed floor.** The currently deployed nanobpmn gateway implements
|
|
709
|
+
* `JOB | MESSAGE | USER_TASK` ({@link DEPLOYED_WAIT_STATE_TYPES}); a `USER_TASK` park can be
|
|
710
|
+
* read here (or through {@link searchUserTasks}). A `waitStateType` filter outside the floor
|
|
711
|
+
* (`TIMER`/`SIGNAL`/`CONDITION`) is rejected with {@link UnsupportedWaitStateTypeError} (the
|
|
712
|
+
* live gateway answers HTTP 422); an unfiltered search returns only in-floor parks. The
|
|
713
|
+
* `waitStateType` union is forward-looking (richer engines may serve more), but this adapter
|
|
714
|
+
* contract pins the floor so a query cannot pass in emulation while a real engine rejects it.
|
|
715
|
+
* Ref: Magikcraft/nano-bpm#1042.
|
|
712
716
|
*/
|
|
713
717
|
searchElementInstanceWaitStates(
|
|
714
718
|
filter?: ElementInstanceWaitStateFilter,
|
|
@@ -29,7 +29,6 @@ import { makeRouter } from "../router.ts";
|
|
|
29
29
|
import { mountApi } from "./api.ts";
|
|
30
30
|
import { makeGateway } from "./gateway.ts";
|
|
31
31
|
import { DataLayer, type ProvisionedSource } from "./datasource.ts";
|
|
32
|
-
import { InstanceStateStore } from "./instance-state-store.ts";
|
|
33
32
|
import { OpenUserTasksStore } from "./open-user-tasks-store.ts";
|
|
34
33
|
import { evictExcessSessions, isLoopbackRequest, missingRequiredArgs, mountMcp, newSessionId, readHeader, readMcpConfig } from "./mcp.ts";
|
|
35
34
|
import { collectMcpToolProjection, diffMcpToolProjection, parseSpec } from "../../../openapi/spec.ts";
|
|
@@ -769,13 +768,11 @@ test("the reserved-namespace drop warns once, not on every tools/list and tools/
|
|
|
769
768
|
assert.equal(drops.length, 1, "the reserved-namespace drop must warn exactly once across many requests");
|
|
770
769
|
});
|
|
771
770
|
|
|
772
|
-
test("
|
|
771
|
+
test("urban_debug_open_user_tasks reads the ADR 0065 canonical projection", async () => {
|
|
773
772
|
const dir = await mkdtemp(join(tmpdir(), "urban-mcp-proj-"));
|
|
774
773
|
const host = createNodeHost({ cwd: dir, log: () => {} });
|
|
775
774
|
const db: SqliteDb = host.openSqlite(join(dir, "test.db"));
|
|
776
775
|
try {
|
|
777
|
-
new InstanceStateStore(db, { clock: { now: () => 0 } }).ensureSchema();
|
|
778
|
-
new InstanceStateStore(db, { clock: { now: () => 0 } }).recordState("pi-7", "ACTIVE");
|
|
779
776
|
const openStore = new OpenUserTasksStore(db, { clock: { now: () => 0 } });
|
|
780
777
|
openStore.ensureSchema();
|
|
781
778
|
openStore.recordOpenTask("pi-7", { userTaskKey: "ut-1", elementId: "Task_A" });
|
|
@@ -792,13 +789,6 @@ test("the projection debug tools read the ADR 0065 canonical stores", async () =
|
|
|
792
789
|
const { router } = buildHarness({ data });
|
|
793
790
|
const session = await connect(router);
|
|
794
791
|
|
|
795
|
-
const stateCall = await rpc(router, session, "tools/call", {
|
|
796
|
-
name: "urban_debug_instance_state",
|
|
797
|
-
arguments: { processInstanceKey: "pi-7" },
|
|
798
|
-
});
|
|
799
|
-
const state = JSON.parse(toolContentText(stateCall.result));
|
|
800
|
-
assert.equal(Reflect.get(state, "state"), "ACTIVE");
|
|
801
|
-
|
|
802
792
|
const tasksCall = await rpc(router, session, "tools/call", {
|
|
803
793
|
name: "urban_debug_open_user_tasks",
|
|
804
794
|
arguments: { processInstanceKey: "pi-7" },
|
|
@@ -812,6 +802,66 @@ test("the projection debug tools read the ADR 0065 canonical stores", async () =
|
|
|
812
802
|
}
|
|
813
803
|
});
|
|
814
804
|
|
|
805
|
+
test("urban_debug_instance_state returns engine truth for a live (ACTIVE) instance", async () => {
|
|
806
|
+
const snapshot: ProcessInstanceSnapshot = { processInstanceKey: "pi-7", state: "ACTIVE" };
|
|
807
|
+
const engine = fakeEngine({
|
|
808
|
+
searchProcessInstances: async (filter) => {
|
|
809
|
+
assert.deepEqual(filter?.processInstanceKeys, ["pi-7"]);
|
|
810
|
+
return [snapshot];
|
|
811
|
+
},
|
|
812
|
+
openUserTasks: async () => [],
|
|
813
|
+
});
|
|
814
|
+
const { router } = buildHarness({ engine });
|
|
815
|
+
const session = await connect(router);
|
|
816
|
+
|
|
817
|
+
const call = await rpc(router, session, "tools/call", {
|
|
818
|
+
name: "urban_debug_instance_state",
|
|
819
|
+
arguments: { processInstanceKey: "pi-7" },
|
|
820
|
+
});
|
|
821
|
+
const state = JSON.parse(toolContentText(call.result));
|
|
822
|
+
assert.deepEqual(state, { processInstanceKey: "pi-7", state: "ACTIVE", waitingOnHuman: false });
|
|
823
|
+
});
|
|
824
|
+
|
|
825
|
+
test("urban_debug_instance_state reports waitingOnHuman from open user tasks", async () => {
|
|
826
|
+
const snapshot: ProcessInstanceSnapshot = { processInstanceKey: "pi-7", state: "ACTIVE" };
|
|
827
|
+
const engine = fakeEngine({
|
|
828
|
+
searchProcessInstances: async () => [snapshot],
|
|
829
|
+
openUserTasks: async (filter) => {
|
|
830
|
+
assert.equal(filter?.processInstanceKey, "pi-7");
|
|
831
|
+
return [{ userTaskKey: "ut-1", processInstanceKey: "pi-7" }];
|
|
832
|
+
},
|
|
833
|
+
});
|
|
834
|
+
const { router } = buildHarness({ engine });
|
|
835
|
+
const session = await connect(router);
|
|
836
|
+
|
|
837
|
+
const call = await rpc(router, session, "tools/call", {
|
|
838
|
+
name: "urban_debug_instance_state",
|
|
839
|
+
arguments: { processInstanceKey: "pi-7" },
|
|
840
|
+
});
|
|
841
|
+
const state = JSON.parse(toolContentText(call.result));
|
|
842
|
+
assert.deepEqual(state, { processInstanceKey: "pi-7", state: "ACTIVE", waitingOnHuman: true });
|
|
843
|
+
});
|
|
844
|
+
|
|
845
|
+
test("urban_debug_instance_state returns null when the engine has no such instance", async () => {
|
|
846
|
+
let openUserTasksCalled = false;
|
|
847
|
+
const engine = fakeEngine({
|
|
848
|
+
searchProcessInstances: async () => [],
|
|
849
|
+
openUserTasks: async () => {
|
|
850
|
+
openUserTasksCalled = true;
|
|
851
|
+
return [];
|
|
852
|
+
},
|
|
853
|
+
});
|
|
854
|
+
const { router } = buildHarness({ engine });
|
|
855
|
+
const session = await connect(router);
|
|
856
|
+
|
|
857
|
+
const call = await rpc(router, session, "tools/call", {
|
|
858
|
+
name: "urban_debug_instance_state",
|
|
859
|
+
arguments: { processInstanceKey: "nope" },
|
|
860
|
+
});
|
|
861
|
+
assert.equal(toolContentText(call.result), "null");
|
|
862
|
+
assert.equal(openUserTasksCalled, false, "must not probe user tasks for an instance the engine doesn't know");
|
|
863
|
+
});
|
|
864
|
+
|
|
815
865
|
// ---- mutation guard (Slice 3) -----------------------------------------------------------------
|
|
816
866
|
|
|
817
867
|
// A repeated credential header must read the SAME under the MCP guard as under OpenAPI route
|
|
@@ -82,7 +82,6 @@ import { resolveBindMode } from "../manifest.ts";
|
|
|
82
82
|
import { json, makeRouter, type Route } from "../router.ts";
|
|
83
83
|
import { API_BASE, readApiBinding } from "./api.ts";
|
|
84
84
|
import { resolveAppPath } from "./datasource.ts";
|
|
85
|
-
import { InstanceStateStore } from "./instance-state-store.ts";
|
|
86
85
|
import { OpenUserTasksStore } from "./open-user-tasks-store.ts";
|
|
87
86
|
|
|
88
87
|
/** The mount path of the MCP endpoint — a sibling of `/app/api` / `/app/agent`, mounted
|
|
@@ -785,17 +784,19 @@ function buildDebugTools(app: AppApi): DebugTool[] {
|
|
|
785
784
|
},
|
|
786
785
|
{
|
|
787
786
|
name: `${DEBUG_PREFIX}instance_state`,
|
|
788
|
-
description:
|
|
787
|
+
description:
|
|
788
|
+
"Engine truth: the live lifecycle state of a process instance (ACTIVE/COMPLETED/TERMINATED) via searchProcessInstances, plus `waitingOnHuman` from its open user tasks. `null` means the engine has no such instance (not merely 'not yet terminal').",
|
|
789
789
|
inputSchema: {
|
|
790
790
|
type: "object",
|
|
791
791
|
properties: { processInstanceKey: OPTIONAL_STRING },
|
|
792
792
|
required: ["processInstanceKey"],
|
|
793
793
|
},
|
|
794
|
-
run: (args) => {
|
|
794
|
+
run: async (args) => {
|
|
795
795
|
const key = requireString(args, "processInstanceKey");
|
|
796
|
-
const
|
|
797
|
-
if (!
|
|
798
|
-
|
|
796
|
+
const [snapshot] = await app.engine.searchProcessInstances({ processInstanceKeys: [key] });
|
|
797
|
+
if (!snapshot) return null;
|
|
798
|
+
const openTasks = await app.engine.openUserTasks({ processInstanceKey: key });
|
|
799
|
+
return { processInstanceKey: key, state: snapshot.state, waitingOnHuman: openTasks.length > 0 };
|
|
799
800
|
},
|
|
800
801
|
},
|
|
801
802
|
{
|
|
@@ -640,14 +640,16 @@ test("normalizeWaitStateType maps the wait-state kinds (case-insensitive) and re
|
|
|
640
640
|
});
|
|
641
641
|
|
|
642
642
|
test("assertDeployedWaitStateType: allows the floor and an absent selector, rejects the rest", () => {
|
|
643
|
-
// The deployed floor is exactly JOB|MESSAGE — the single source of truth both
|
|
644
|
-
|
|
643
|
+
// The deployed floor is exactly JOB|MESSAGE|USER_TASK — the single source of truth both
|
|
644
|
+
// adapters gate on (USER_TASK shipped in Magikcraft/nano-bpm#1042).
|
|
645
|
+
assert.deepEqual([...DEPLOYED_WAIT_STATE_TYPES], ["JOB", "MESSAGE", "USER_TASK"]);
|
|
645
646
|
// In-floor and absent selectors are no-ops (no throw).
|
|
646
647
|
assert.doesNotThrow(() => assertDeployedWaitStateType(undefined));
|
|
647
648
|
assert.doesNotThrow(() => assertDeployedWaitStateType("JOB"));
|
|
648
649
|
assert.doesNotThrow(() => assertDeployedWaitStateType("MESSAGE"));
|
|
649
|
-
|
|
650
|
-
|
|
650
|
+
assert.doesNotThrow(() => assertDeployedWaitStateType("USER_TASK"));
|
|
651
|
+
// Every canonical type still outside the floor is rejected with a typed, descriptive error.
|
|
652
|
+
for (const waitStateType of ["TIMER", "SIGNAL", "CONDITION"] as const) {
|
|
651
653
|
assert.throws(
|
|
652
654
|
() => assertDeployedWaitStateType(waitStateType),
|
|
653
655
|
(err: unknown) => {
|
|
@@ -843,11 +845,56 @@ test("searchElementInstanceWaitStates forwards a waitStateType selector and read
|
|
|
843
845
|
]);
|
|
844
846
|
});
|
|
845
847
|
|
|
848
|
+
test("searchElementInstanceWaitStates maps a USER_TASK park, unwrapping details.taskKey → userTaskKey (mock SDK client, not a live gateway)", async () => {
|
|
849
|
+
// MOCK-ONLY LEG: this exercises the SDK adapter's row mapping against a `fakeSdkClient`, not
|
|
850
|
+
// a live gateway — the canonical parity assertion against a real engine lives in the
|
|
851
|
+
// adapter-agnostic `runEngineClientContract` USER_TASK leg (contract.ts), run against the
|
|
852
|
+
// live `SdkEngineClient` in the integration lane. Here we only pin the shape the engine's
|
|
853
|
+
// wait-state read model returns for a USER_TASK park (Magikcraft/nano-bpm#1042): the park's
|
|
854
|
+
// `taskKey` under `details` unwraps to the seam's `userTaskKey`.
|
|
855
|
+
let seenInput: unknown;
|
|
856
|
+
const client = fakeSdkClient({
|
|
857
|
+
searchElementInstanceWaitStates: async (input) => {
|
|
858
|
+
seenInput = input;
|
|
859
|
+
return {
|
|
860
|
+
items: [
|
|
861
|
+
{
|
|
862
|
+
elementInstanceKey: 7,
|
|
863
|
+
processInstanceKey: 3,
|
|
864
|
+
elementId: "review",
|
|
865
|
+
type: "USER_TASK",
|
|
866
|
+
details: { waitStateType: "USER_TASK", taskKey: 90 },
|
|
867
|
+
},
|
|
868
|
+
{
|
|
869
|
+
elementInstanceKey: 8,
|
|
870
|
+
processInstanceKey: 3,
|
|
871
|
+
elementId: "review2",
|
|
872
|
+
details: { waitStateType: "USER_TASK" },
|
|
873
|
+
}, // USER_TASK with no taskKey → dropped (missing required identity)
|
|
874
|
+
],
|
|
875
|
+
};
|
|
876
|
+
},
|
|
877
|
+
});
|
|
878
|
+
const engine = new SdkEngineClient(client);
|
|
879
|
+
const out = await engine.searchElementInstanceWaitStates({ processInstanceKey: "3", waitStateType: "USER_TASK" });
|
|
880
|
+
assert.deepEqual(seenInput, { filter: { processInstanceKey: "3", waitStateType: "USER_TASK" } });
|
|
881
|
+
assert.deepEqual(out, [
|
|
882
|
+
{
|
|
883
|
+
elementInstanceKey: "7",
|
|
884
|
+
processInstanceKey: "3",
|
|
885
|
+
elementId: "review",
|
|
886
|
+
elementType: "USER_TASK",
|
|
887
|
+
waitStateType: "USER_TASK",
|
|
888
|
+
userTaskKey: "90",
|
|
889
|
+
},
|
|
890
|
+
]);
|
|
891
|
+
});
|
|
892
|
+
|
|
846
893
|
test("searchElementInstanceWaitStates rejects a waitStateType outside the deployed floor without calling the gateway", async () => {
|
|
847
|
-
// The deployed gateway's read model implements
|
|
848
|
-
// value. The SDK adapter must fail fast client-side (not issue the call), so an app
|
|
849
|
-
// `waitStateType: "
|
|
850
|
-
// nanobpm/nano-ide#497).
|
|
894
|
+
// The deployed gateway's read model implements JOB|MESSAGE|USER_TASK and 422s any other
|
|
895
|
+
// filter value. The SDK adapter must fail fast client-side (not issue the call), so an app
|
|
896
|
+
// authoring `waitStateType: "TIMER"` cannot ship green then blow up at runtime (issues
|
|
897
|
+
// nanobpm/nano-ide#497, #498).
|
|
851
898
|
let called = false;
|
|
852
899
|
const client = fakeSdkClient({
|
|
853
900
|
searchElementInstanceWaitStates: async () => {
|
|
@@ -856,7 +903,7 @@ test("searchElementInstanceWaitStates rejects a waitStateType outside the deploy
|
|
|
856
903
|
},
|
|
857
904
|
});
|
|
858
905
|
const engine = new SdkEngineClient(client);
|
|
859
|
-
for (const waitStateType of ["
|
|
906
|
+
for (const waitStateType of ["TIMER", "SIGNAL", "CONDITION"] as const) {
|
|
860
907
|
await assert.rejects(
|
|
861
908
|
engine.searchElementInstanceWaitStates({ waitStateType }),
|
|
862
909
|
(err: unknown) => {
|
|
@@ -868,8 +915,9 @@ test("searchElementInstanceWaitStates rejects a waitStateType outside the deploy
|
|
|
868
915
|
);
|
|
869
916
|
}
|
|
870
917
|
assert.equal(called, false, "the gateway must not be called for an out-of-floor filter");
|
|
871
|
-
//
|
|
918
|
+
// Every in-floor filter still reaches the gateway.
|
|
872
919
|
await engine.searchElementInstanceWaitStates({ waitStateType: "JOB" });
|
|
920
|
+
await engine.searchElementInstanceWaitStates({ waitStateType: "USER_TASK" });
|
|
873
921
|
assert.equal(called, true);
|
|
874
922
|
});
|
|
875
923
|
|
|
@@ -867,9 +867,10 @@ export class SdkEngineClient implements EngineClient {
|
|
|
867
867
|
filter?: ElementInstanceWaitStateFilter,
|
|
868
868
|
): Promise<ElementInstanceWaitState[]> {
|
|
869
869
|
// The wait-states search surfaces the parks the deployed engine's read model serves.
|
|
870
|
-
// A `waitStateType` selector outside the deployed floor (`JOB | MESSAGE`) is
|
|
871
|
-
// the gateway with HTTP 422; fail fast client-side with a clear error instead,
|
|
872
|
-
// divergence is caught offline and identically to the WASM adapter (No Drift
|
|
870
|
+
// A `waitStateType` selector outside the deployed floor (`JOB | MESSAGE | USER_TASK`) is
|
|
871
|
+
// rejected by the gateway with HTTP 422; fail fast client-side with a clear error instead,
|
|
872
|
+
// so the divergence is caught offline and identically to the WASM adapter (No Drift
|
|
873
|
+
// Surfaces).
|
|
873
874
|
assertDeployedWaitStateType(filter?.waitStateType);
|
|
874
875
|
// Same zero-wait read + per-row mapping-gate shape as `searchElementInstances`; the SDK
|
|
875
876
|
// nests park-specific fields under `details`, which `mapElementInstanceWaitStateRow`
|