@nanobpm/urban 0.82.1 → 0.83.0

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.
@@ -261,6 +261,104 @@ export interface UserTaskFilter {
261
261
  candidateGroup?: string;
262
262
  }
263
263
 
264
+ /**
265
+ * An element instance's lifecycle state, as the engine reports it on a
266
+ * `/v2/element-instances/search` result. Mirrors the engine's `ElementInstanceStateEnum`:
267
+ * `ACTIVE` (a token is currently *at* this element), `COMPLETED` (the token has left it), or
268
+ * `TERMINATED` (the element was interrupted). Structurally the same state set as
269
+ * {@link ProcessInstanceState} (one active state plus the `COMPLETED`/`TERMINATED` terminals),
270
+ * but a distinct concept — element instances are the tokens *within* a process instance — so it
271
+ * is kept as its own type rather than aliased.
272
+ */
273
+ export type ElementInstanceState = "ACTIVE" | "COMPLETED" | "TERMINATED";
274
+
275
+ /**
276
+ * A single element instance ("token") as {@link EngineClient.searchElementInstances} and
277
+ * {@link EngineClient.getElementInstance} report it — the finest-grained "how far has this
278
+ * process instance progressed" signal, below the process-instance lifecycle
279
+ * ({@link ProcessInstanceSnapshot}) and independent of whether the element is a user task
280
+ * ({@link UserTaskSummary}). It surfaces active *non-user-task* elements (a service task, a
281
+ * gateway, a catch event) a user-task search cannot see — "the furthest element reached".
282
+ * Carries the element instance's own key, its identity (`elementId`, its BPMN `elementType`
283
+ * when the engine reports it), the owning process instance, and its lifecycle `state`.
284
+ */
285
+ export interface ElementInstanceSummary {
286
+ readonly elementInstanceKey: string;
287
+ readonly processInstanceKey: string;
288
+ readonly elementId: string;
289
+ /** The BPMN element type (e.g. `"SERVICE_TASK"`, `"USER_TASK"`,
290
+ * `"INTERMEDIATE_CATCH_EVENT"`), when the engine reports it. Kept transport-agnostic (a
291
+ * bare string) so a new BPMN element type on the engine flows through without having to be
292
+ * enumerated here. Absent when the adapter's read model does not carry it. */
293
+ readonly elementType?: string;
294
+ readonly state: ElementInstanceState;
295
+ }
296
+
297
+ /**
298
+ * The kind of wait an element instance is parked in, as a
299
+ * `/v2/element-instances/wait-states/search` result reports it. Unlike a user-task search,
300
+ * this surfaces *every* park — a `JOB` (a service task awaiting a worker), a `MESSAGE`
301
+ * (an event awaiting correlation), a `TIMER`, a `SIGNAL`, a `CONDITION`, as well as a
302
+ * `USER_TASK` — so a consumer can read "what is this instance blocked on" beyond user tasks.
303
+ */
304
+ export type WaitStateType =
305
+ | "JOB"
306
+ | "MESSAGE"
307
+ | "USER_TASK"
308
+ | "TIMER"
309
+ | "SIGNAL"
310
+ | "CONDITION";
311
+
312
+ /**
313
+ * A single parked (waiting) element instance as
314
+ * {@link EngineClient.searchElementInstanceWaitStates} reports it. Every variant carries the
315
+ * element instance's identity; the `waitStateType` discriminates the park-specific fields — a
316
+ * `JOB` carries the `jobType`/`jobKey` it is parked on, a `MESSAGE` the awaited
317
+ * `messageName`/`correlationKey`, a `USER_TASK` the `userTaskKey`, a `SIGNAL` the
318
+ * `signalName` — so a caller narrows on `waitStateType` to reach them type-safely rather than
319
+ * probing a flat optional-field bag.
320
+ */
321
+ export type ElementInstanceWaitState =
322
+ & {
323
+ readonly elementInstanceKey: string;
324
+ readonly processInstanceKey: string;
325
+ readonly elementId: string;
326
+ readonly elementType?: string;
327
+ }
328
+ & (
329
+ | { readonly waitStateType: "JOB"; readonly jobType: string; readonly jobKey?: string }
330
+ | {
331
+ readonly waitStateType: "MESSAGE";
332
+ readonly messageName: string;
333
+ readonly correlationKey?: string;
334
+ }
335
+ | { readonly waitStateType: "USER_TASK"; readonly userTaskKey: string }
336
+ | { readonly waitStateType: "TIMER" }
337
+ | { readonly waitStateType: "SIGNAL"; readonly signalName: string }
338
+ | { readonly waitStateType: "CONDITION" }
339
+ );
340
+
341
+ /**
342
+ * Selectors for an element-instance search: which process instance, element, and/or lifecycle
343
+ * state to match. Used by {@link EngineClient.searchElementInstances}.
344
+ */
345
+ export interface ElementInstanceFilter {
346
+ processInstanceKey?: string;
347
+ elementId?: string;
348
+ state?: ElementInstanceState;
349
+ }
350
+
351
+ /**
352
+ * Selectors for an element-instance *wait-state* search: which process instance, element,
353
+ * and/or kind of wait to match. Used by {@link EngineClient.searchElementInstanceWaitStates}.
354
+ * There is deliberately no lifecycle-`state` selector — a wait state is always an active park.
355
+ */
356
+ export interface ElementInstanceWaitStateFilter {
357
+ processInstanceKey?: string;
358
+ elementId?: string;
359
+ waitStateType?: WaitStateType;
360
+ }
361
+
264
362
  /**
265
363
  * A deployed form's form-js schema plus its identifying metadata, as
266
364
  * {@link EngineClient.getForm} returns it. `schema` is the parsed form-js document
@@ -350,6 +448,41 @@ export interface EngineClient {
350
448
  processInstanceKeys?: string[];
351
449
  state?: ProcessInstanceState;
352
450
  }): Promise<ProcessInstanceSnapshot[]>;
451
+ /**
452
+ * Search element instances ("tokens") by process instance, element, and/or lifecycle state
453
+ * (`POST /v2/element-instances/search`). Where {@link searchProcessInstances} answers "is
454
+ * this instance still running" and {@link searchUserTasks} answers "what user tasks are
455
+ * open", this answers "what element has a token reached" — including the active
456
+ * *non-user-task* elements (a service task, a gateway, a catch event) that a user-task
457
+ * search cannot see, i.e. "the furthest element reached". An eventually consistent
458
+ * (zero-wait) read; each {@link ElementInstanceSummary} carries the element instance's key,
459
+ * its `elementId`/`elementType`, the owning process instance, and its `state`.
460
+ */
461
+ searchElementInstances(
462
+ filter?: ElementInstanceFilter,
463
+ ): Promise<ElementInstanceSummary[]>;
464
+ /**
465
+ * Search the *wait states* of element instances
466
+ * (`POST /v2/element-instances/wait-states/search`) — every park, not only user tasks. A
467
+ * `JOB` park (a service task awaiting a worker), a `MESSAGE` park (an event awaiting
468
+ * correlation), a `TIMER`/`SIGNAL`/`CONDITION`, plus `USER_TASK`. A zero-wait read; each
469
+ * result is discriminated by `waitStateType` (see {@link ElementInstanceWaitState}), so a
470
+ * consumer can read the job/message parks a {@link searchUserTasks} cannot surface.
471
+ */
472
+ searchElementInstanceWaitStates(
473
+ filter?: ElementInstanceWaitStateFilter,
474
+ ): Promise<ElementInstanceWaitState[]>;
475
+ /**
476
+ * Fetch a single element instance by its key (`GET /v2/element-instances/{elementInstanceKey}`),
477
+ * or `null` when it cannot be resolved — the key is blank, no such element instance exists
478
+ * (a 404), or the fetch otherwise fails. A read is treated as absence rather than propagating
479
+ * (mirroring {@link getForm}), so a caller distinguishes "have it" from "don't" without a
480
+ * try/catch. A zero-wait read returning the same {@link ElementInstanceSummary} shape as
481
+ * {@link searchElementInstances}.
482
+ */
483
+ getElementInstance(
484
+ elementInstanceKey: string,
485
+ ): Promise<ElementInstanceSummary | null>;
353
486
  /** Register a push worker for a job type. Draining is handled by the adapter. */
354
487
  registerWorker(
355
488
  jobType: string,
@@ -384,6 +517,9 @@ export const ENGINE_CLIENT_METHODS = [
384
517
  "getForm",
385
518
  "completeUserTask",
386
519
  "searchProcessInstances",
520
+ "searchElementInstances",
521
+ "searchElementInstanceWaitStates",
522
+ "getElementInstance",
387
523
  "registerWorker",
388
524
  "close",
389
525
  ] as const satisfies readonly (keyof EngineClient)[];
@@ -33,6 +33,9 @@ const engine: EngineClient = {
33
33
  getForm: async () => null,
34
34
  completeUserTask: async () => {},
35
35
  searchProcessInstances: async () => [],
36
+ searchElementInstances: async () => [],
37
+ searchElementInstanceWaitStates: async () => [],
38
+ getElementInstance: async () => null,
36
39
  registerWorker: async (jobType) => ({ jobType, unsubscribe: async () => {} }),
37
40
  close: async () => {},
38
41
  };
@@ -18,6 +18,9 @@ const fakeEngine: EngineClient = {
18
18
  getForm: async () => null,
19
19
  completeUserTask: async () => {},
20
20
  searchProcessInstances: async () => [],
21
+ searchElementInstances: async () => [],
22
+ searchElementInstanceWaitStates: async () => [],
23
+ getElementInstance: async () => null,
21
24
  registerWorker: async (jobType) => ({ jobType, unsubscribe: async () => {} }),
22
25
  close: async () => {},
23
26
  };
@@ -31,6 +31,9 @@ const engine: EngineClient = {
31
31
  openUserTasks: async () => [],
32
32
  getForm: async () => null,
33
33
  searchProcessInstances: async () => [],
34
+ searchElementInstances: async () => [],
35
+ searchElementInstanceWaitStates: async () => [],
36
+ getElementInstance: async () => null,
34
37
  completeUserTask: async () => {},
35
38
  registerWorker: async (jobType) => ({ jobType, unsubscribe: async () => {} }),
36
39
  close: async () => {},
@@ -85,6 +85,9 @@ function cancelEngine(opts: {
85
85
  openUserTasks: notUsed("openUserTasks"),
86
86
  getForm: notUsed("getForm"),
87
87
  completeUserTask: notUsed("completeUserTask"),
88
+ searchElementInstances: notUsed("searchElementInstances"),
89
+ searchElementInstanceWaitStates: notUsed("searchElementInstanceWaitStates"),
90
+ getElementInstance: notUsed("getElementInstance"),
88
91
  registerWorker: notUsed("registerWorker"),
89
92
  close: notUsed("close"),
90
93
  };
@@ -21,6 +21,9 @@ class MiniEngine implements EngineClient {
21
21
  async getForm(): Promise<null> { return null; }
22
22
  async completeUserTask(): Promise<void> {}
23
23
  async searchProcessInstances(): Promise<[]> { return []; }
24
+ async searchElementInstances(): Promise<[]> { return []; }
25
+ async searchElementInstanceWaitStates(): Promise<[]> { return []; }
26
+ async getElementInstance(): Promise<null> { return null; }
24
27
  async registerWorker(jobType: string, handler: JobHandler): Promise<WorkerSubscription> {
25
28
  this.workers.set(jobType, handler);
26
29
  return { jobType, unsubscribe: async () => void this.workers.delete(jobType) };
@@ -20,6 +20,9 @@ const noEngine: EngineClient = {
20
20
  getForm: async () => null,
21
21
  completeUserTask: async () => {},
22
22
  searchProcessInstances: async () => [],
23
+ searchElementInstances: async () => [],
24
+ searchElementInstanceWaitStates: async () => [],
25
+ getElementInstance: async () => null,
23
26
  registerWorker: async (jobType) => ({ jobType, unsubscribe: async () => {} }),
24
27
  close: async () => {},
25
28
  };
@@ -90,6 +90,9 @@ function fakeEngine(
90
90
  searchUserTasks: notUsed("searchUserTasks"),
91
91
  getForm: notUsed("getForm"),
92
92
  completeUserTask: notUsed("completeUserTask"),
93
+ searchElementInstances: notUsed("searchElementInstances"),
94
+ searchElementInstanceWaitStates: notUsed("searchElementInstanceWaitStates"),
95
+ getElementInstance: notUsed("getElementInstance"),
93
96
  registerWorker: notUsed("registerWorker"),
94
97
  close: notUsed("close"),
95
98
  };
@@ -48,6 +48,9 @@ function fakeEngine(): { engine: EngineClient; calls: FakeEngineCalls } {
48
48
  async getForm() { return null; },
49
49
  async completeUserTask() {},
50
50
  async searchProcessInstances() { return []; },
51
+ async searchElementInstances() { return []; },
52
+ async searchElementInstanceWaitStates() { return []; },
53
+ async getElementInstance() { return null; },
51
54
  async registerWorker(jobType) { return { jobType, unsubscribe: async () => {} }; },
52
55
  async close() {},
53
56
  };
@@ -34,6 +34,9 @@ function ctxWith(surfaces: Record<string, unknown>): Parameters<typeof mountSurf
34
34
  getForm: async () => null,
35
35
  completeUserTask: async () => {},
36
36
  searchProcessInstances: async () => [],
37
+ searchElementInstances: async () => [],
38
+ searchElementInstanceWaitStates: async () => [],
39
+ getElementInstance: async () => null,
37
40
  registerWorker: async (jobType) => ({ jobType, unsubscribe: async () => {} }),
38
41
  close: async () => {},
39
42
  };
@@ -50,6 +53,9 @@ const fakeEngine: EngineClient = {
50
53
  getForm: async () => null,
51
54
  completeUserTask: async () => {},
52
55
  searchProcessInstances: async () => [],
56
+ searchElementInstances: async () => [],
57
+ searchElementInstanceWaitStates: async () => [],
58
+ getElementInstance: async () => null,
53
59
  registerWorker: async (jobType) => ({ jobType, unsubscribe: async () => {} }),
54
60
  close: async () => {},
55
61
  };
@@ -35,6 +35,9 @@ class MiniEngine implements EngineClient {
35
35
  async getForm(): Promise<null> { return null; }
36
36
  async completeUserTask(): Promise<void> {}
37
37
  async searchProcessInstances(): Promise<[]> { return []; }
38
+ async searchElementInstances(): Promise<[]> { return []; }
39
+ async searchElementInstanceWaitStates(): Promise<[]> { return []; }
40
+ async getElementInstance(): Promise<null> { return null; }
38
41
  async registerWorker(jobType: string): Promise<{ jobType: string; unsubscribe(): Promise<void> }> {
39
42
  return { jobType, unsubscribe: async () => {} };
40
43
  }
@@ -32,6 +32,9 @@ class MiniEngine implements EngineClient {
32
32
  async getForm(): Promise<null> { return null; }
33
33
  async completeUserTask(): Promise<void> {}
34
34
  async searchProcessInstances(): Promise<[]> { return []; }
35
+ async searchElementInstances(): Promise<[]> { return []; }
36
+ async searchElementInstanceWaitStates(): Promise<[]> { return []; }
37
+ async getElementInstance(): Promise<null> { return null; }
35
38
  async close(): Promise<void> {}
36
39
  async registerWorker(jobType: string, handler: JobHandler): Promise<WorkerSubscription> {
37
40
  this.workers.set(jobType, handler);
@@ -32,6 +32,15 @@ class NoopEngine implements EngineClient {
32
32
  async searchProcessInstances() {
33
33
  return [];
34
34
  }
35
+ async searchElementInstances() {
36
+ return [];
37
+ }
38
+ async searchElementInstanceWaitStates() {
39
+ return [];
40
+ }
41
+ async getElementInstance() {
42
+ return null;
43
+ }
35
44
  async registerWorker(jobType: string): Promise<WorkerSubscription> {
36
45
  return { jobType, unsubscribe: async () => {} };
37
46
  }
@@ -53,6 +53,15 @@ class FakeEngine implements EngineClient {
53
53
  async searchProcessInstances() {
54
54
  return [];
55
55
  }
56
+ async searchElementInstances() {
57
+ return [];
58
+ }
59
+ async searchElementInstanceWaitStates() {
60
+ return [];
61
+ }
62
+ async getElementInstance() {
63
+ return null;
64
+ }
56
65
  async registerWorker(jobType: string, handler: JobHandler): Promise<WorkerSubscription> {
57
66
  this.workers.set(jobType, handler);
58
67
  return { jobType, unsubscribe: async () => void this.workers.delete(jobType) };
@@ -3,6 +3,8 @@ import assert from "node:assert/strict";
3
3
  import {
4
4
  createNanoSdkEngineClient,
5
5
  normalizeProcessInstanceState,
6
+ normalizeElementInstanceState,
7
+ normalizeWaitStateType,
6
8
  requireProcessInstanceKey,
7
9
  SdkEngineClient,
8
10
  type NanoSdkActivatedJob,
@@ -74,6 +76,18 @@ function fakeSdkClient(overrides: Partial<NanoSdkClient> = {}): NanoSdkClient &
74
76
  calls.push("searchProcessInstances");
75
77
  return { items: [] };
76
78
  },
79
+ async searchElementInstances() {
80
+ calls.push("searchElementInstances");
81
+ return { items: [] };
82
+ },
83
+ async searchElementInstanceWaitStates() {
84
+ calls.push("searchElementInstanceWaitStates");
85
+ return { items: [] };
86
+ },
87
+ async getElementInstance() {
88
+ calls.push("getElementInstance");
89
+ return {};
90
+ },
77
91
  async completeUserTask() {
78
92
  calls.push("completeUserTask");
79
93
  return {};
@@ -448,6 +462,273 @@ test("normalizeProcessInstanceState maps the terminal set and rejects others", (
448
462
  assert.equal(normalizeProcessInstanceState(undefined), undefined);
449
463
  });
450
464
 
465
+ test("normalizeElementInstanceState maps the recognized states (case-insensitive) and rejects others", () => {
466
+ assert.equal(normalizeElementInstanceState("ACTIVE"), "ACTIVE");
467
+ assert.equal(normalizeElementInstanceState("completed"), "COMPLETED");
468
+ assert.equal(normalizeElementInstanceState("Terminated"), "TERMINATED");
469
+ assert.equal(normalizeElementInstanceState("SKIPPED"), undefined);
470
+ assert.equal(normalizeElementInstanceState(7), undefined);
471
+ assert.equal(normalizeElementInstanceState(undefined), undefined);
472
+ });
473
+
474
+ test("normalizeWaitStateType maps the wait-state kinds (case-insensitive) and rejects others", () => {
475
+ assert.equal(normalizeWaitStateType("JOB"), "JOB");
476
+ assert.equal(normalizeWaitStateType("message"), "MESSAGE");
477
+ assert.equal(normalizeWaitStateType("User_Task"), "USER_TASK");
478
+ assert.equal(normalizeWaitStateType("TIMER"), "TIMER");
479
+ assert.equal(normalizeWaitStateType("signal"), "SIGNAL");
480
+ assert.equal(normalizeWaitStateType("CONDITION"), "CONDITION");
481
+ assert.equal(normalizeWaitStateType("ESCALATION"), undefined);
482
+ assert.equal(normalizeWaitStateType(3), undefined);
483
+ });
484
+
485
+ test("searchElementInstances passes zero-wait consistency, maps rows, and forwards selectors", async () => {
486
+ let seenInput: unknown;
487
+ let seenConsistency: unknown;
488
+ const client = fakeSdkClient({
489
+ searchElementInstances: async (input, c) => {
490
+ seenInput = input;
491
+ seenConsistency = c;
492
+ return {
493
+ items: [
494
+ {
495
+ elementInstanceKey: 5,
496
+ processInstanceKey: 3,
497
+ elementId: "task",
498
+ type: "SERVICE_TASK",
499
+ state: "active",
500
+ },
501
+ // elementType may arrive as `elementType` on a plain REST body.
502
+ {
503
+ elementInstanceKey: "6",
504
+ processInstanceKey: "3",
505
+ elementId: "gw",
506
+ elementType: "PARALLEL_GATEWAY",
507
+ state: "COMPLETED",
508
+ },
509
+ ],
510
+ };
511
+ },
512
+ });
513
+ const engine = new SdkEngineClient(client);
514
+ const out = await engine.searchElementInstances({
515
+ processInstanceKey: "3",
516
+ elementId: "task",
517
+ state: "ACTIVE",
518
+ });
519
+ assert.deepEqual(seenInput, {
520
+ filter: { processInstanceKey: "3", elementId: "task", state: "ACTIVE" },
521
+ });
522
+ assert.deepEqual(seenConsistency, { consistency: { waitUpToMs: 0 } });
523
+ assert.deepEqual(out, [
524
+ {
525
+ elementInstanceKey: "5",
526
+ processInstanceKey: "3",
527
+ elementId: "task",
528
+ elementType: "SERVICE_TASK",
529
+ state: "ACTIVE",
530
+ },
531
+ {
532
+ elementInstanceKey: "6",
533
+ processInstanceKey: "3",
534
+ elementId: "gw",
535
+ elementType: "PARALLEL_GATEWAY",
536
+ state: "COMPLETED",
537
+ },
538
+ ]);
539
+ });
540
+
541
+ test("searchElementInstances with no filter sends an empty filter", async () => {
542
+ let seenInput: unknown;
543
+ const client = fakeSdkClient({
544
+ searchElementInstances: async (input) => {
545
+ seenInput = input;
546
+ return { items: [] };
547
+ },
548
+ });
549
+ const engine = new SdkEngineClient(client);
550
+ await engine.searchElementInstances();
551
+ assert.deepEqual(seenInput, { filter: {} });
552
+ });
553
+
554
+ test("searchElementInstances skips malformed rows (missing key/elementId/state)", async () => {
555
+ const client = fakeSdkClient({
556
+ searchElementInstances: async () => ({
557
+ items: [
558
+ { elementInstanceKey: "", processInstanceKey: "3", elementId: "a", state: "ACTIVE" }, // keyless
559
+ { elementInstanceKey: " ", processInstanceKey: "3", elementId: "a", state: "ACTIVE" }, // whitespace key
560
+ { elementInstanceKey: "5", processInstanceKey: "", elementId: "a", state: "ACTIVE" }, // no PI key
561
+ { elementInstanceKey: "6", processInstanceKey: "3", elementId: "", state: "ACTIVE" }, // no elementId
562
+ { elementInstanceKey: "9", processInstanceKey: "3", elementId: " ", state: "ACTIVE" }, // whitespace elementId
563
+ { elementInstanceKey: "7", processInstanceKey: "3", elementId: "a", state: "SKIPPED" }, // bad state
564
+ { elementInstanceKey: "8", processInstanceKey: "3", elementId: "ok", state: "ACTIVE" }, // kept
565
+ ],
566
+ }),
567
+ });
568
+ const engine = new SdkEngineClient(client);
569
+ const out = await engine.searchElementInstances();
570
+ assert.deepEqual(out, [
571
+ { elementInstanceKey: "8", processInstanceKey: "3", elementId: "ok", state: "ACTIVE" },
572
+ ]);
573
+ });
574
+
575
+ test("searchElementInstanceWaitStates maps JOB/MESSAGE parks and unwraps SDK `details`", async () => {
576
+ let seenInput: unknown;
577
+ let seenConsistency: unknown;
578
+ const client = fakeSdkClient({
579
+ searchElementInstanceWaitStates: async (input, c) => {
580
+ seenInput = input;
581
+ seenConsistency = c;
582
+ return {
583
+ items: [
584
+ {
585
+ elementInstanceKey: 5,
586
+ processInstanceKey: 3,
587
+ elementId: "svc",
588
+ type: "SERVICE_TASK",
589
+ details: { waitStateType: "JOB", jobType: "work", jobKey: 6 },
590
+ },
591
+ {
592
+ elementInstanceKey: 8,
593
+ processInstanceKey: 3,
594
+ elementId: "catch",
595
+ details: { waitStateType: "MESSAGE", messageName: "Go", correlationKey: "K1" },
596
+ },
597
+ ],
598
+ };
599
+ },
600
+ });
601
+ const engine = new SdkEngineClient(client);
602
+ const out = await engine.searchElementInstanceWaitStates({ processInstanceKey: "3" });
603
+ assert.deepEqual(seenInput, { filter: { processInstanceKey: "3" } });
604
+ assert.deepEqual(seenConsistency, { consistency: { waitUpToMs: 0 } });
605
+ assert.deepEqual(out, [
606
+ {
607
+ elementInstanceKey: "5",
608
+ processInstanceKey: "3",
609
+ elementId: "svc",
610
+ elementType: "SERVICE_TASK",
611
+ waitStateType: "JOB",
612
+ jobType: "work",
613
+ jobKey: "6",
614
+ },
615
+ {
616
+ elementInstanceKey: "8",
617
+ processInstanceKey: "3",
618
+ elementId: "catch",
619
+ waitStateType: "MESSAGE",
620
+ messageName: "Go",
621
+ correlationKey: "K1",
622
+ },
623
+ ]);
624
+ });
625
+
626
+ test("searchElementInstanceWaitStates forwards a waitStateType selector and reads inlined details", async () => {
627
+ let seenInput: unknown;
628
+ const client = fakeSdkClient({
629
+ searchElementInstanceWaitStates: async (input) => {
630
+ seenInput = input;
631
+ // A plain REST body may inline the park fields rather than nesting them under `details`.
632
+ return {
633
+ items: [
634
+ {
635
+ elementInstanceKey: "9",
636
+ processInstanceKey: "3",
637
+ elementId: "ut",
638
+ waitStateType: "USER_TASK",
639
+ taskKey: "42",
640
+ },
641
+ {
642
+ elementInstanceKey: "11",
643
+ processInstanceKey: "3",
644
+ elementId: "ut2",
645
+ waitStateType: "USER_TASK",
646
+ }, // USER_TASK with no taskKey/userTaskKey → dropped (invalid identity)
647
+ { elementInstanceKey: "10", processInstanceKey: "3", elementId: "?" }, // no waitStateType → dropped
648
+ ],
649
+ };
650
+ },
651
+ });
652
+ const engine = new SdkEngineClient(client);
653
+ const out = await engine.searchElementInstanceWaitStates({ waitStateType: "USER_TASK" });
654
+ assert.deepEqual(seenInput, { filter: { waitStateType: "USER_TASK" } });
655
+ assert.deepEqual(out, [
656
+ {
657
+ elementInstanceKey: "9",
658
+ processInstanceKey: "3",
659
+ elementId: "ut",
660
+ waitStateType: "USER_TASK",
661
+ userTaskKey: "42",
662
+ },
663
+ ]);
664
+ });
665
+
666
+ test("searchElementInstanceWaitStates drops parks missing their required discriminator field", async () => {
667
+ const client = fakeSdkClient({
668
+ searchElementInstanceWaitStates: async () => ({
669
+ items: [
670
+ { elementInstanceKey: "5", processInstanceKey: "3", elementId: "j", waitStateType: "JOB" }, // no jobType
671
+ { elementInstanceKey: "6", processInstanceKey: "3", elementId: "m", waitStateType: "MESSAGE" }, // no messageName
672
+ { elementInstanceKey: "7", processInstanceKey: "3", elementId: "s", waitStateType: "SIGNAL" }, // no signalName
673
+ {
674
+ elementInstanceKey: "8",
675
+ processInstanceKey: "3",
676
+ elementId: "j2",
677
+ waitStateType: "JOB",
678
+ jobType: " ",
679
+ }, // blank jobType → dropped
680
+ {
681
+ elementInstanceKey: "9",
682
+ processInstanceKey: "3",
683
+ elementId: "t",
684
+ waitStateType: "TIMER",
685
+ }, // TIMER has no required field → kept
686
+ ],
687
+ }),
688
+ });
689
+ const engine = new SdkEngineClient(client);
690
+ const out = await engine.searchElementInstanceWaitStates();
691
+ assert.deepEqual(out, [
692
+ { elementInstanceKey: "9", processInstanceKey: "3", elementId: "t", waitStateType: "TIMER" },
693
+ ]);
694
+ });
695
+
696
+ test("getElementInstance returns a mapped summary, and null for a blank key or a 404", async () => {
697
+ const client = fakeSdkClient({
698
+ getElementInstance: async (input) => {
699
+ if (input.elementInstanceKey === "5") {
700
+ return {
701
+ elementInstanceKey: 5,
702
+ processInstanceKey: 3,
703
+ elementId: "task",
704
+ type: "SERVICE_TASK",
705
+ state: "ACTIVE",
706
+ };
707
+ }
708
+ throw new Error("404 not found");
709
+ },
710
+ });
711
+ const engine = new SdkEngineClient(client);
712
+ assert.deepEqual(await engine.getElementInstance("5"), {
713
+ elementInstanceKey: "5",
714
+ processInstanceKey: "3",
715
+ elementId: "task",
716
+ elementType: "SERVICE_TASK",
717
+ state: "ACTIVE",
718
+ });
719
+ // A padded-but-valid key is normalized before the fetch, so it addresses the same instance.
720
+ assert.deepEqual(await engine.getElementInstance(" 5 "), {
721
+ elementInstanceKey: "5",
722
+ processInstanceKey: "3",
723
+ elementId: "task",
724
+ elementType: "SERVICE_TASK",
725
+ state: "ACTIVE",
726
+ });
727
+ // A blank key short-circuits without hitting the engine; a 404 is treated as absence.
728
+ assert.equal(await engine.getElementInstance(" "), null);
729
+ assert.equal(await engine.getElementInstance("missing"), null);
730
+ });
731
+
451
732
  test("registerWorker creates a worker the SDK auto-starts, and dispatches + completes", async () => {
452
733
  const client = fakeSdkClient();
453
734
  const engine = new SdkEngineClient(client);