@nanobpm/urban 0.85.0 → 0.86.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.
@@ -13,6 +13,11 @@ import {
13
13
  type NanoSdkJobWorkerConfig,
14
14
  } from "./nanosdk.ts";
15
15
  import { BpmnError } from "../core/host.ts";
16
+ import {
17
+ assertDeployedWaitStateType,
18
+ DEPLOYED_WAIT_STATE_TYPES,
19
+ UnsupportedWaitStateTypeError,
20
+ } from "../core/host.ts";
16
21
  import { __resetExecStoreForTests, installExecStore, runInJobContext } from "../core/execContext.ts";
17
22
  import { readLineage } from "../core/lineage.ts";
18
23
  import { createNodeHost } from "../adapters/node.ts";
@@ -498,6 +503,27 @@ test("normalizeWaitStateType maps the wait-state kinds (case-insensitive) and re
498
503
  assert.equal(normalizeWaitStateType(3), undefined);
499
504
  });
500
505
 
506
+ test("assertDeployedWaitStateType: allows the floor and an absent selector, rejects the rest", () => {
507
+ // The deployed floor is exactly JOB|MESSAGE — the single source of truth both adapters gate on.
508
+ assert.deepEqual([...DEPLOYED_WAIT_STATE_TYPES], ["JOB", "MESSAGE"]);
509
+ // In-floor and absent selectors are no-ops (no throw).
510
+ assert.doesNotThrow(() => assertDeployedWaitStateType(undefined));
511
+ assert.doesNotThrow(() => assertDeployedWaitStateType("JOB"));
512
+ assert.doesNotThrow(() => assertDeployedWaitStateType("MESSAGE"));
513
+ // Every canonical type outside the floor is rejected with a typed, descriptive error.
514
+ for (const waitStateType of ["USER_TASK", "TIMER", "SIGNAL", "CONDITION"] as const) {
515
+ assert.throws(
516
+ () => assertDeployedWaitStateType(waitStateType),
517
+ (err: unknown) => {
518
+ assert.ok(err instanceof UnsupportedWaitStateTypeError);
519
+ assert.equal(err.waitStateType, waitStateType);
520
+ assert.match(err.message, /HTTP 422/);
521
+ return true;
522
+ },
523
+ );
524
+ }
525
+ });
526
+
501
527
  test("searchElementInstances passes zero-wait consistency, maps rows, and forwards selectors", async () => {
502
528
  let seenInput: unknown;
503
529
  let seenConsistency: unknown;
@@ -650,35 +676,67 @@ test("searchElementInstanceWaitStates forwards a waitStateType selector and read
650
676
  {
651
677
  elementInstanceKey: "9",
652
678
  processInstanceKey: "3",
653
- elementId: "ut",
654
- waitStateType: "USER_TASK",
655
- taskKey: "42",
679
+ elementId: "catch",
680
+ waitStateType: "MESSAGE",
681
+ messageName: "Go",
682
+ correlationKey: "K1",
656
683
  },
657
684
  {
658
685
  elementInstanceKey: "11",
659
686
  processInstanceKey: "3",
660
- elementId: "ut2",
661
- waitStateType: "USER_TASK",
662
- }, // USER_TASK with no taskKey/userTaskKey → dropped (invalid identity)
687
+ elementId: "catch2",
688
+ waitStateType: "MESSAGE",
689
+ }, // MESSAGE with no messageName → dropped (invalid identity)
663
690
  { elementInstanceKey: "10", processInstanceKey: "3", elementId: "?" }, // no waitStateType → dropped
664
691
  ],
665
692
  };
666
693
  },
667
694
  });
668
695
  const engine = new SdkEngineClient(client);
669
- const out = await engine.searchElementInstanceWaitStates({ waitStateType: "USER_TASK" });
670
- assert.deepEqual(seenInput, { filter: { waitStateType: "USER_TASK" } });
696
+ const out = await engine.searchElementInstanceWaitStates({ waitStateType: "MESSAGE" });
697
+ assert.deepEqual(seenInput, { filter: { waitStateType: "MESSAGE" } });
671
698
  assert.deepEqual(out, [
672
699
  {
673
700
  elementInstanceKey: "9",
674
701
  processInstanceKey: "3",
675
- elementId: "ut",
676
- waitStateType: "USER_TASK",
677
- userTaskKey: "42",
702
+ elementId: "catch",
703
+ waitStateType: "MESSAGE",
704
+ messageName: "Go",
705
+ correlationKey: "K1",
678
706
  },
679
707
  ]);
680
708
  });
681
709
 
710
+ test("searchElementInstanceWaitStates rejects a waitStateType outside the deployed floor without calling the gateway", async () => {
711
+ // The deployed gateway's read model implements only JOB|MESSAGE and 422s any other filter
712
+ // value. The SDK adapter must fail fast client-side (not issue the call), so an app authoring
713
+ // `waitStateType: "USER_TASK"` cannot ship green then blow up at runtime (issue
714
+ // nanobpm/nano-ide#497).
715
+ let called = false;
716
+ const client = fakeSdkClient({
717
+ searchElementInstanceWaitStates: async () => {
718
+ called = true;
719
+ return { items: [] };
720
+ },
721
+ });
722
+ const engine = new SdkEngineClient(client);
723
+ for (const waitStateType of ["USER_TASK", "TIMER", "SIGNAL", "CONDITION"] as const) {
724
+ await assert.rejects(
725
+ engine.searchElementInstanceWaitStates({ waitStateType }),
726
+ (err: unknown) => {
727
+ assert.ok(err instanceof Error);
728
+ assert.match(err.message, /unsupported waitStateType/);
729
+ assert.match(err.message, new RegExp(waitStateType));
730
+ return true;
731
+ },
732
+ );
733
+ }
734
+ assert.equal(called, false, "the gateway must not be called for an out-of-floor filter");
735
+ // An in-floor filter still reaches the gateway.
736
+ await engine.searchElementInstanceWaitStates({ waitStateType: "JOB" });
737
+ assert.equal(called, true);
738
+ });
739
+
682
740
  test("searchElementInstanceWaitStates drops parks missing their required discriminator field", async () => {
683
741
  const client = fakeSdkClient({
684
742
  searchElementInstanceWaitStates: async () => ({
@@ -32,7 +32,7 @@ import type {
32
32
  UserTaskFilter,
33
33
  WorkerSubscription,
34
34
  } from "../core/host.ts";
35
- import { isBpmnError } from "../core/host.ts";
35
+ import { assertDeployedWaitStateType, isBpmnError } from "../core/host.ts";
36
36
  import {
37
37
  buildFormSchema,
38
38
  parseFormSchema,
@@ -709,10 +709,14 @@ export class SdkEngineClient implements EngineClient {
709
709
  async searchElementInstanceWaitStates(
710
710
  filter?: ElementInstanceWaitStateFilter,
711
711
  ): Promise<ElementInstanceWaitState[]> {
712
- // The wait-states search surfaces *every* park (job/message/timer/signal/condition/user
713
- // task), not just user tasks. Same zero-wait read + per-row mapping-gate shape as
714
- // `searchElementInstances`; the SDK nests park-specific fields under `details`, which
715
- // `mapElementInstanceWaitStateRow` unwraps.
712
+ // The wait-states search surfaces the parks the deployed engine's read model serves.
713
+ // A `waitStateType` selector outside the deployed floor (`JOB | MESSAGE`) is rejected by
714
+ // the gateway with HTTP 422; fail fast client-side with a clear error instead, so the
715
+ // divergence is caught offline and identically to the WASM adapter (No Drift Surfaces).
716
+ assertDeployedWaitStateType(filter?.waitStateType);
717
+ // Same zero-wait read + per-row mapping-gate shape as `searchElementInstances`; the SDK
718
+ // nests park-specific fields under `details`, which `mapElementInstanceWaitStateRow`
719
+ // unwraps.
716
720
  const f: Record<string, unknown> = {};
717
721
  if (filter?.processInstanceKey) f.processInstanceKey = filter.processInstanceKey;
718
722
  if (filter?.elementId) f.elementId = filter.elementId;
@@ -133,6 +133,14 @@ export { BpmnError, isBpmnError } from "./core/host.ts";
133
133
  // double / adapter iterates it to prove at runtime that it implements the full interface,
134
134
  // catching the "fake lags the SDK" seam-lag class categorically.
135
135
  export { ENGINE_CLIENT_METHODS } from "./core/host.ts";
136
+ // Deployed wait-state floor (issue nanobpm/nano-ide#497): the `waitStateType` values the
137
+ // deployed gateway actually serves, plus the shared guard both EngineClient adapters use so
138
+ // a filter outside the floor fails identically in emulation and against a live engine.
139
+ export {
140
+ assertDeployedWaitStateType,
141
+ DEPLOYED_WAIT_STATE_TYPES,
142
+ UnsupportedWaitStateTypeError,
143
+ } from "./core/host.ts";
136
144
 
137
145
  // Connector packs (ADR 0050, in-process port)
138
146
  export {