@stigmer/runner 3.22.0 → 3.22.1-dev.20260922095755

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.
Files changed (135) hide show
  1. package/README.md +1 -1
  2. package/dist/.build-fingerprint +1 -1
  3. package/dist/activities/attach-session.js +0 -1
  4. package/dist/activities/attach-session.js.map +1 -1
  5. package/dist/activities/call-agent-status.d.ts +18 -11
  6. package/dist/activities/call-agent-status.js +21 -24
  7. package/dist/activities/call-agent-status.js.map +1 -1
  8. package/dist/activities/call-agent.d.ts +2 -2
  9. package/dist/activities/call-agent.js +4 -6
  10. package/dist/activities/call-agent.js.map +1 -1
  11. package/dist/activities/call-function.d.ts +4 -2
  12. package/dist/activities/call-function.js +13 -4
  13. package/dist/activities/call-function.js.map +1 -1
  14. package/dist/activities/classify-tool-approvals.js +1 -1
  15. package/dist/activities/classify-tool-approvals.js.map +1 -1
  16. package/dist/activities/discover-mcp-server.js +0 -1
  17. package/dist/activities/discover-mcp-server.js.map +1 -1
  18. package/dist/activities/emit-event.d.ts +6 -1
  19. package/dist/activities/emit-event.js +5 -14
  20. package/dist/activities/emit-event.js.map +1 -1
  21. package/dist/activities/execute-cursor/__test-utils__/hermetic-cursor.js +1 -1
  22. package/dist/activities/execute-cursor/__test-utils__/hermetic-cursor.js.map +1 -1
  23. package/dist/activities/execute-cursor/adapter.js +0 -1
  24. package/dist/activities/execute-cursor/adapter.js.map +1 -1
  25. package/dist/activities/execute-cursor/turn-setup.d.ts +1 -1
  26. package/dist/activities/execute-cursor/turn-setup.js +5 -6
  27. package/dist/activities/execute-cursor/turn-setup.js.map +1 -1
  28. package/dist/activities/execute-deep-agent/__test-utils__/hermetic-deep-agent.js +1 -1
  29. package/dist/activities/execute-deep-agent/__test-utils__/hermetic-deep-agent.js.map +1 -1
  30. package/dist/activities/execute-deep-agent/adapter.js +1 -1
  31. package/dist/activities/execute-deep-agent/adapter.js.map +1 -1
  32. package/dist/activities/execute-deep-agent/turn-setup.d.ts +1 -1
  33. package/dist/activities/execute-deep-agent/turn-setup.js +5 -3
  34. package/dist/activities/execute-deep-agent/turn-setup.js.map +1 -1
  35. package/dist/activities/generate-session-subject.js +1 -2
  36. package/dist/activities/generate-session-subject.js.map +1 -1
  37. package/dist/activities/hydrate-workflow-execution.js +0 -1
  38. package/dist/activities/hydrate-workflow-execution.js.map +1 -1
  39. package/dist/activities/promote-task-output.d.ts +2 -1
  40. package/dist/activities/promote-task-output.js +10 -14
  41. package/dist/activities/promote-task-output.js.map +1 -1
  42. package/dist/activities/workflow-event-activities.d.ts +9 -7
  43. package/dist/activities/workflow-event-activities.js +14 -18
  44. package/dist/activities/workflow-event-activities.js.map +1 -1
  45. package/dist/client/stigmer-client.d.ts +13 -13
  46. package/dist/client/stigmer-client.js +4 -7
  47. package/dist/client/stigmer-client.js.map +1 -1
  48. package/dist/config.d.ts +40 -20
  49. package/dist/config.js +25 -9
  50. package/dist/config.js.map +1 -1
  51. package/dist/harness/registry.js +0 -1
  52. package/dist/harness/registry.js.map +1 -1
  53. package/dist/harness/turn-context.js +2 -4
  54. package/dist/harness/turn-context.js.map +1 -1
  55. package/dist/main.js +13 -6
  56. package/dist/main.js.map +1 -1
  57. package/dist/runner-manager.d.ts +8 -6
  58. package/dist/runner-manager.js +20 -10
  59. package/dist/runner-manager.js.map +1 -1
  60. package/dist/runner-token-coordinator.d.ts +8 -3
  61. package/dist/runner-token-coordinator.js +8 -3
  62. package/dist/runner-token-coordinator.js.map +1 -1
  63. package/dist/runner.d.ts +10 -2
  64. package/dist/runner.js +31 -25
  65. package/dist/runner.js.map +1 -1
  66. package/dist/shared/artifact-storage.d.ts +6 -6
  67. package/dist/shared/artifact-storage.js +5 -12
  68. package/dist/shared/artifact-storage.js.map +1 -1
  69. package/dist/shared/checkpointer/factory.js +5 -1
  70. package/dist/shared/checkpointer/factory.js.map +1 -1
  71. package/dist/shared/checkpointer/http-saver.d.ts +13 -2
  72. package/dist/shared/checkpointer/http-saver.js +17 -5
  73. package/dist/shared/checkpointer/http-saver.js.map +1 -1
  74. package/dist/shared/checkpointer/types.d.ts +10 -1
  75. package/dist/shared/checkpointer/types.js +16 -0
  76. package/dist/shared/checkpointer/types.js.map +1 -1
  77. package/dist/shared/extract-structured-output.js +1 -1
  78. package/dist/shared/extract-structured-output.js.map +1 -1
  79. package/package.json +5 -5
  80. package/src/__test-utils__/config-fixture.ts +2 -2
  81. package/src/__test-utils__/module-specifiers.ts +45 -3
  82. package/src/__tests__/config-boundary.test.ts +135 -0
  83. package/src/__tests__/config.test.ts +7 -16
  84. package/src/__tests__/runner-manager.test.ts +28 -6
  85. package/src/__tests__/runner.test.ts +28 -4
  86. package/src/activities/__tests__/attach-session.test.ts +1 -1
  87. package/src/activities/__tests__/call-agent-contracts.test.ts +4 -7
  88. package/src/activities/__tests__/call-agent-status.test.ts +17 -16
  89. package/src/activities/__tests__/call-agent.test.ts +28 -7
  90. package/src/activities/__tests__/call-function.test.ts +5 -4
  91. package/src/activities/__tests__/classify-tool-approvals.test.ts +1 -1
  92. package/src/activities/__tests__/discover-mcp-server.test.ts +1 -1
  93. package/src/activities/__tests__/generate-session-subject-wire.test.ts +1 -1
  94. package/src/activities/__tests__/workflow-event-activities.test.ts +21 -23
  95. package/src/activities/attach-session.ts +0 -1
  96. package/src/activities/call-agent-status.ts +28 -20
  97. package/src/activities/call-agent.ts +5 -6
  98. package/src/activities/call-function.ts +15 -3
  99. package/src/activities/classify-tool-approvals.ts +1 -1
  100. package/src/activities/discover-mcp-server.ts +0 -1
  101. package/src/activities/emit-event.ts +7 -16
  102. package/src/activities/execute-cursor/__test-utils__/hermetic-cursor.ts +1 -1
  103. package/src/activities/execute-cursor/adapter.ts +0 -1
  104. package/src/activities/execute-cursor/turn-setup.ts +6 -7
  105. package/src/activities/execute-deep-agent/__test-utils__/hermetic-deep-agent.ts +1 -1
  106. package/src/activities/execute-deep-agent/__tests__/adapter.test.ts +2 -2
  107. package/src/activities/execute-deep-agent/__tests__/summarization-verification.test.ts +1 -1
  108. package/src/activities/execute-deep-agent/adapter.ts +1 -1
  109. package/src/activities/execute-deep-agent/turn-setup.ts +6 -4
  110. package/src/activities/generate-session-subject.ts +1 -2
  111. package/src/activities/hydrate-workflow-execution.ts +0 -1
  112. package/src/activities/promote-task-output.ts +11 -15
  113. package/src/activities/workflow-event-activities.ts +17 -18
  114. package/src/client/__tests__/stigmer-client.test.ts +8 -19
  115. package/src/client/stigmer-client.ts +18 -20
  116. package/src/config.ts +51 -23
  117. package/src/harness/registry.ts +0 -1
  118. package/src/harness/turn-context.ts +2 -4
  119. package/src/main.ts +13 -6
  120. package/src/runner-manager.ts +25 -15
  121. package/src/runner-token-coordinator.ts +8 -3
  122. package/src/runner.ts +39 -30
  123. package/src/shared/__tests__/artifact-storage-extended.test.ts +9 -9
  124. package/src/shared/__tests__/artifact-storage.test.ts +33 -30
  125. package/src/shared/__tests__/extract-structured-output.test.ts +2 -2
  126. package/src/shared/artifact-storage.ts +12 -17
  127. package/src/shared/checkpointer/__tests__/factory.test.ts +23 -3
  128. package/src/shared/checkpointer/__tests__/http-saver.test.ts +25 -2
  129. package/src/shared/checkpointer/factory.ts +5 -1
  130. package/src/shared/checkpointer/http-saver.ts +20 -6
  131. package/src/shared/checkpointer/types.ts +12 -1
  132. package/src/shared/extract-structured-output.ts +1 -1
  133. package/src/shared/filereview/__tests__/cas-substrate.test.ts +1 -1
  134. package/src/shared/filereview/__tests__/proxy-reconcile.test.ts +1 -1
  135. package/src/workflow-engine/__tests__/tasks/emit-event.test.ts +32 -54
@@ -1,9 +1,19 @@
1
+ /**
2
+ * HttpCheckpointSaver: the proxy wire shape ($binary transport, the routes,
3
+ * the header), the bounded retry loop and its classification, and the one
4
+ * thing that must never regress silently — the bearer credential is read
5
+ * from the shared ref on every request, so a rotation between two requests
6
+ * of one saver reaches the second (a pool member claimed for a session; a
7
+ * sandbox token renewed in place).
8
+ */
9
+
1
10
  import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
11
  import { HttpCheckpointSaver } from "../http-saver.js";
12
+ import type { TokenRef } from "../../../config.js";
3
13
  import type { Checkpoint, CheckpointMetadata } from "@langchain/langgraph-checkpoint";
4
14
 
5
15
  const PROXY = "https://proxy.test";
6
- const TOKEN = "test-token";
16
+ const TOKEN: TokenRef = { current: "test-token" };
7
17
 
8
18
  function makeConfig(overrides: Record<string, string> = {}) {
9
19
  return {
@@ -71,6 +81,19 @@ describe("HttpCheckpointSaver", () => {
71
81
  expect(opts.headers.Authorization).toBe("Bearer test-token");
72
82
  expect(url).toContain("/v1/proxy/checkpoints/checkpoint");
73
83
  });
84
+
85
+ it("presents the credential the ref holds NOW: a rotation between two requests reaches the second", async () => {
86
+ const ref: TokenRef = { current: "boot-credential" };
87
+ const rotating = new HttpCheckpointSaver(PROXY, ref);
88
+ fetchSpy.mockResolvedValue({ status: 404, ok: false });
89
+
90
+ await rotating.getTuple(makeConfig());
91
+ ref.current = "session-credential";
92
+ await rotating.getTuple(makeConfig());
93
+
94
+ expect(fetchSpy.mock.calls[0][1].headers.Authorization).toBe("Bearer boot-credential");
95
+ expect(fetchSpy.mock.calls[1][1].headers.Authorization).toBe("Bearer session-credential");
96
+ });
74
97
  });
75
98
 
76
99
  describe("put", () => {
@@ -360,7 +383,7 @@ describe("HttpCheckpointSaver", () => {
360
383
 
361
384
  describe("URL construction", () => {
362
385
  it("strips trailing slashes from proxy endpoint", () => {
363
- const s = new HttpCheckpointSaver("https://proxy.test///", "tok");
386
+ const s = new HttpCheckpointSaver("https://proxy.test///", { current: "tok" });
364
387
  fetchSpy.mockResolvedValue({ status: 404, ok: false });
365
388
  s.getTuple(makeConfig());
366
389
  const [url] = fetchSpy.mock.calls[0];
@@ -68,7 +68,11 @@ export async function createCheckpointer(
68
68
  "proxyEndpoint is required for HTTP checkpointer",
69
69
  );
70
70
  }
71
- if (!config.authToken) {
71
+ // The ref must be present AND hold a credential at open: a saver that
72
+ // opened without one would fail its first request with a bare 401
73
+ // instead of this named error, and a proxy-mode runner always has one
74
+ // (loadConfig refuses to boot without STIGMER_TOKEN).
75
+ if (!config.authToken?.current) {
72
76
  throw new CheckpointerCreationError(
73
77
  "http",
74
78
  "authToken is required for HTTP checkpointer",
@@ -26,6 +26,14 @@
26
26
  * upserts. Budget exhaustion still fails loudly — checkpoints are not
27
27
  * lossy-tolerable (unlike status updates, see status.ts), and silently
28
28
  * dropping one would corrupt resume state.
29
+ *
30
+ * The bearer credential is a shared ref read on EVERY request, never a
31
+ * header frozen at construction: a saver lives for a whole turn, and the
32
+ * runner's control-plane credential rotates underneath it (a pool member
33
+ * claimed for a session, a sandbox token renewed in place). A frozen header
34
+ * kept presenting the boot credential and every read after the rotation
35
+ * was refused (config.ts header; ProxyArtifactStorage holds its credential
36
+ * the same way).
29
37
  */
30
38
 
31
39
  import type { RunnableConfig } from "@langchain/core/runnables";
@@ -38,6 +46,7 @@ import {
38
46
  type ChannelVersions,
39
47
  } from "@langchain/langgraph-checkpoint";
40
48
  import type { CheckpointMetadata, PendingWrite } from "@langchain/langgraph-checkpoint";
49
+ import type { TokenRef } from "../../config.js";
41
50
  import type { RetryOptions } from "../grpc-retry.js";
42
51
  import { fetchWithRetry, type FetchRetryPolicy } from "../http-retry.js";
43
52
 
@@ -119,20 +128,17 @@ const DEFAULT_RETRY = {
119
128
 
120
129
  export class HttpCheckpointSaver extends BaseCheckpointSaver {
121
130
  private readonly baseUrl: string;
122
- private readonly headers: Record<string, string>;
131
+ private readonly authToken: Readonly<TokenRef>;
123
132
  private readonly retryPolicy: FetchRetryPolicy;
124
133
 
125
134
  constructor(
126
135
  proxyEndpoint: string,
127
- authToken: string,
136
+ authToken: Readonly<TokenRef>,
128
137
  options: HttpCheckpointSaverOptions = {},
129
138
  ) {
130
139
  super();
131
140
  this.baseUrl = `${proxyEndpoint.replace(/\/+$/, "")}/v1/proxy/checkpoints`;
132
- this.headers = {
133
- Authorization: `Bearer ${authToken}`,
134
- "Content-Type": "application/json",
135
- };
141
+ this.authToken = authToken;
136
142
  this.retryPolicy = {
137
143
  label: "HttpCheckpointSaver",
138
144
  baseDelayMs: options.baseDelayMs ?? DEFAULT_RETRY.baseDelayMs,
@@ -143,6 +149,14 @@ export class HttpCheckpointSaver extends BaseCheckpointSaver {
143
149
  };
144
150
  }
145
151
 
152
+ /** The request headers as of now: the credential is resolved here, per request (the header). */
153
+ private get headers(): Record<string, string> {
154
+ return {
155
+ Authorization: `Bearer ${this.authToken.current ?? ""}`,
156
+ "Content-Type": "application/json",
157
+ };
158
+ }
159
+
146
160
  private fetchWithRetry(url: string, init?: RequestInit): Promise<Response> {
147
161
  return fetchWithRetry(url, init, this.retryPolicy);
148
162
  }
@@ -6,11 +6,22 @@
6
6
  * ExecuteDeepAgent invocations so HITL/pause/transient-recovery truly resume.
7
7
  * - http: routes through the Stigmer Side-Channel Proxy (cloud / managed).
8
8
  * - memory: ephemeral in-process storage — explicit opt-in, used by tests.
9
+ *
10
+ * The http backend's credential is a shared ref, never a string: the saver
11
+ * reads it per request, so a rotation of the runner's control-plane
12
+ * credential (a pool claim, a sandbox-token renewal) reaches a checkpoint
13
+ * written after it — including mid-turn, since a saver lives for a whole
14
+ * turn (config.ts header; the artifact store holds its credential the same
15
+ * way).
9
16
  */
17
+
18
+ import type { TokenRef } from "../../config.js";
19
+
10
20
  export interface CheckpointerConfig {
11
21
  readonly type: "memory" | "http" | "sqlite";
12
22
  readonly proxyEndpoint?: string;
13
- readonly authToken?: string;
23
+ /** The credential the http saver presents, read per request. Required when type is "http". */
24
+ readonly authToken?: Readonly<TokenRef>;
14
25
  /** Absolute path to the SQLite database file. Required when type is "sqlite". */
15
26
  readonly sqlitePath?: string;
16
27
  }
@@ -60,7 +60,7 @@ export async function extractStructuredOutput(
60
60
  const { model: llm } = await buildChatModel({
61
61
  modelName: extractionModel,
62
62
  proxyEndpoint,
63
- stigmerToken: config.stigmerToken ?? undefined,
63
+ stigmerToken: config.stigmerTokenRef.current ?? undefined,
64
64
  maxTokens: 4096,
65
65
  });
66
66
 
@@ -412,7 +412,7 @@ describe("casBlobReader — ProxyArtifactStorage presigned+fetch path (cloud)",
412
412
  });
413
413
  vi.stubGlobal("fetch", fetchMock);
414
414
  try {
415
- const storage = new ProxyArtifactStorage("https://proxy.example.com", "tok");
415
+ const storage = new ProxyArtifactStorage("https://proxy.example.com", { current: "tok" });
416
416
  const reader = casBlobReader(storage);
417
417
  const got = await reader(key);
418
418
  expect(got.equals(payload)).toBe(true);
@@ -186,7 +186,7 @@ describe("proxy-mode file-review reconcile (real ProxyArtifactStorage)", () => {
186
186
 
187
187
  beforeEach(async () => {
188
188
  proxy = await startMockProxy();
189
- storage = new ProxyArtifactStorage(proxy.endpoint, "tok");
189
+ storage = new ProxyArtifactStorage(proxy.endpoint, { current: "tok" });
190
190
  });
191
191
  afterEach(async () => {
192
192
  await proxy.close();
@@ -1,5 +1,6 @@
1
1
  import { describe, it, expect, vi, afterEach, beforeEach } from "vitest";
2
2
  import { emitEventAction, type EmitEventConfig, type SignalDeliveryTarget } from "../../../activities/emit-event.js";
3
+ import type { StigmerClient } from "../../../client/stigmer-client.js";
3
4
 
4
5
  // ─── Server-mediated signal-delivery mocks ───────────────────────────────────
5
6
  // deliverSignal routes through StigmerClient.sendWorkflowSignal (the server's
@@ -12,25 +13,9 @@ const mockSendWorkflowSignal = vi.fn(async (
12
13
  _payload: unknown,
13
14
  _options?: { timeoutMs?: number },
14
15
  ) => ({}));
15
- const mockClientCtor = vi.fn((_opts: { endpoint: string; token?: string | null }) => ({
16
- sendWorkflowSignal: mockSendWorkflowSignal,
17
- }));
18
-
19
- vi.mock("../../../client/stigmer-client.js", () => ({
20
- StigmerClient: vi.fn().mockImplementation(
21
- (opts: { endpoint: string; token?: string | null }) => mockClientCtor(opts),
22
- ),
23
- }));
24
-
25
- // Mutable so each test can vary what config.ts resolves. deliverSignal builds
26
- // its client from the canonical stigmerBackendEndpoint resolution.
27
- let mockBackendEndpoint = "http://localhost:7234";
28
- vi.mock("../../../config.js", () => ({
29
- loadConfig: () => ({
30
- stigmerBackendEndpoint: mockBackendEndpoint,
31
- stigmerToken: null,
32
- }),
33
- }));
16
+ // The client is the caller's (call-function.ts builds it from the runner's
17
+ // Config); this module constructs none, so the test hands in a fake.
18
+ const client = { sendWorkflowSignal: mockSendWorkflowSignal } as unknown as StigmerClient;
34
19
 
35
20
  describe("emitEventAction", () => {
36
21
  describe("envelope construction", () => {
@@ -39,7 +24,7 @@ describe("emitEventAction", () => {
39
24
  event: { type: "workflow.step.completed" },
40
25
  };
41
26
 
42
- const result = await emitEventAction(config, "exec-123");
27
+ const result = await emitEventAction(config, "exec-123", undefined, client);
43
28
 
44
29
  expect(result.specversion).toBe("1.0");
45
30
  expect(result.type).toBe("workflow.step.completed");
@@ -55,7 +40,7 @@ describe("emitEventAction", () => {
55
40
  event: { type: "user.created", source: "/services/auth" },
56
41
  };
57
42
 
58
- const result = await emitEventAction(config, "exec-456");
43
+ const result = await emitEventAction(config, "exec-456", undefined, client);
59
44
 
60
45
  expect(result.source).toBe("/services/auth");
61
46
  });
@@ -65,7 +50,7 @@ describe("emitEventAction", () => {
65
50
  event: { type: "order.shipped", subject: "order-789" },
66
51
  };
67
52
 
68
- const result = await emitEventAction(config, "exec-1");
53
+ const result = await emitEventAction(config, "exec-1", undefined, client);
69
54
 
70
55
  expect(result.subject).toBe("order-789");
71
56
  });
@@ -78,7 +63,7 @@ describe("emitEventAction", () => {
78
63
  },
79
64
  };
80
65
 
81
- const result = await emitEventAction(config, "exec-1");
66
+ const result = await emitEventAction(config, "exec-1", undefined, client);
82
67
 
83
68
  expect(result.data).toEqual({ build_id: "b-123", status: "success", duration_ms: 4500 });
84
69
  });
@@ -88,7 +73,7 @@ describe("emitEventAction", () => {
88
73
  event: { type: "ping", data: {} },
89
74
  };
90
75
 
91
- const result = await emitEventAction(config, "exec-1");
76
+ const result = await emitEventAction(config, "exec-1", undefined, client);
92
77
 
93
78
  expect(result.data).toBeUndefined();
94
79
  });
@@ -98,31 +83,31 @@ describe("emitEventAction", () => {
98
83
  event: { type: "test" },
99
84
  };
100
85
 
101
- const result = await emitEventAction(config, "exec-1");
86
+ const result = await emitEventAction(config, "exec-1", undefined, client);
102
87
 
103
88
  expect("subject" in result).toBe(false);
104
89
  });
105
90
 
106
91
  it("throws when event field is missing", async () => {
107
92
  const config = {} as EmitEventConfig;
108
- await expect(emitEventAction(config, "exec-1")).rejects.toThrow("'event' field is required");
93
+ await expect(emitEventAction(config, "exec-1", undefined, client)).rejects.toThrow("'event' field is required");
109
94
  });
110
95
 
111
96
  it("throws when event.type is missing", async () => {
112
97
  const config = { event: {} } as unknown as EmitEventConfig;
113
- await expect(emitEventAction(config, "exec-1")).rejects.toThrow("'event.type' field is required");
98
+ await expect(emitEventAction(config, "exec-1", undefined, client)).rejects.toThrow("'event.type' field is required");
114
99
  });
115
100
 
116
101
  it("generates unique IDs for each invocation", async () => {
117
102
  const config: EmitEventConfig = { event: { type: "test" } };
118
- const r1 = await emitEventAction(config, "e1");
119
- const r2 = await emitEventAction(config, "e1");
103
+ const r1 = await emitEventAction(config, "e1", undefined, client);
104
+ const r2 = await emitEventAction(config, "e1", undefined, client);
120
105
  expect(r1.id).not.toBe(r2.id);
121
106
  });
122
107
 
123
108
  it("time field is a valid ISO 8601 string", async () => {
124
109
  const config: EmitEventConfig = { event: { type: "test" } };
125
- const result = await emitEventAction(config, "e1");
110
+ const result = await emitEventAction(config, "e1", undefined, client);
126
111
  const parsed = new Date(result.time as string);
127
112
  expect(parsed.toISOString()).toBe(result.time);
128
113
  });
@@ -134,7 +119,7 @@ describe("emitEventAction", () => {
134
119
  event: { type: "test.event" },
135
120
  };
136
121
 
137
- const result = await emitEventAction(config, "exec-1");
122
+ const result = await emitEventAction(config, "exec-1", undefined, client);
138
123
 
139
124
  expect(result.type).toBe("test.event");
140
125
  expect(result.delivery_errors).toBeUndefined();
@@ -146,7 +131,7 @@ describe("emitEventAction", () => {
146
131
  delivery: [],
147
132
  };
148
133
 
149
- const result = await emitEventAction(config, "exec-1");
134
+ const result = await emitEventAction(config, "exec-1", undefined, client);
150
135
 
151
136
  expect(result.type).toBe("test.event");
152
137
  expect(result.delivery_errors).toBeUndefined();
@@ -171,7 +156,7 @@ describe("emitEventAction", () => {
171
156
  delivery: [{ webhook: { url: "https://events.example.com/ingest" } }],
172
157
  };
173
158
 
174
- const result = await emitEventAction(config, "exec-1");
159
+ const result = await emitEventAction(config, "exec-1", undefined, client);
175
160
 
176
161
  expect(fetchSpy).toHaveBeenCalledTimes(1);
177
162
  const call = fetchSpy.mock.calls[0] as unknown as [string, RequestInit];
@@ -202,7 +187,7 @@ describe("emitEventAction", () => {
202
187
  }],
203
188
  };
204
189
 
205
- await emitEventAction(config, "exec-1", { API_TOKEN: "secret123" });
190
+ await emitEventAction(config, "exec-1", { API_TOKEN: "secret123" }, client);
206
191
 
207
192
  const call = fetchSpy.mock.calls[0] as unknown as [string, RequestInit];
208
193
  expect((call[1].headers as Record<string, string>)["Authorization"]).toBe("Bearer secret123");
@@ -218,7 +203,7 @@ describe("emitEventAction", () => {
218
203
  delivery: [{ webhook: { url: "https://down.example.com" } }],
219
204
  };
220
205
 
221
- const result = await emitEventAction(config, "exec-1");
206
+ const result = await emitEventAction(config, "exec-1", undefined, client);
222
207
 
223
208
  expect(result.delivery_errors).toHaveLength(1);
224
209
  expect((result.delivery_errors as any)[0].target).toBe("webhook:https://down.example.com");
@@ -235,7 +220,7 @@ describe("emitEventAction", () => {
235
220
  delivery: [{ webhook: { url: "https://unreachable.example.com" } }],
236
221
  };
237
222
 
238
- const result = await emitEventAction(config, "exec-1");
223
+ const result = await emitEventAction(config, "exec-1", undefined, client);
239
224
 
240
225
  expect(result.delivery_errors).toHaveLength(1);
241
226
  expect((result.delivery_errors as any)[0].error).toContain("ECONNREFUSED");
@@ -255,7 +240,7 @@ describe("emitEventAction", () => {
255
240
  ],
256
241
  };
257
242
 
258
- const result = await emitEventAction(config, "exec-1");
243
+ const result = await emitEventAction(config, "exec-1", undefined, client);
259
244
 
260
245
  expect(fetchSpy).toHaveBeenCalledTimes(2);
261
246
  expect(result.delivery_errors).toBeUndefined();
@@ -265,13 +250,10 @@ describe("emitEventAction", () => {
265
250
  describe("signal delivery (server-mediated SendSignal lane)", () => {
266
251
  beforeEach(() => {
267
252
  mockSendWorkflowSignal.mockClear();
268
- mockClientCtor.mockClear();
269
253
  mockSendWorkflowSignal.mockResolvedValue({});
270
- mockBackendEndpoint = "http://localhost:7234";
271
254
  });
272
255
 
273
- it("routes through the server resolved by config, addressed by execution_id", async () => {
274
- mockBackendEndpoint = "https://api.stigmer.example.com";
256
+ it("routes through the client it is handed, addressed by execution_id", async () => {
275
257
 
276
258
  const config: EmitEventConfig = {
277
259
  event: { type: "approval.granted" },
@@ -280,11 +262,8 @@ describe("emitEventAction", () => {
280
262
  ],
281
263
  };
282
264
 
283
- const result = await emitEventAction(config, "exec-1");
265
+ const result = await emitEventAction(config, "exec-1", undefined, client);
284
266
 
285
- expect(mockClientCtor).toHaveBeenCalledWith(
286
- expect.objectContaining({ endpoint: "https://api.stigmer.example.com" }),
287
- );
288
267
  expect(mockSendWorkflowSignal).toHaveBeenCalledTimes(1);
289
268
  const [executionId, signalName, envelope, options] =
290
269
  mockSendWorkflowSignal.mock.calls[0] as unknown as [
@@ -314,7 +293,7 @@ describe("emitEventAction", () => {
314
293
  ],
315
294
  };
316
295
 
317
- const result = await emitEventAction(config, "exec-1");
296
+ const result = await emitEventAction(config, "exec-1", undefined, client);
318
297
 
319
298
  expect(result.delivery_errors).toHaveLength(1);
320
299
  expect((result.delivery_errors as any)[0].target).toBe("signal:wfx_done/ping");
@@ -334,7 +313,7 @@ describe("emitEventAction", () => {
334
313
  ],
335
314
  };
336
315
 
337
- const result = await emitEventAction(config, "exec-1");
316
+ const result = await emitEventAction(config, "exec-1", undefined, client);
338
317
 
339
318
  expect(mockSendWorkflowSignal).not.toHaveBeenCalled();
340
319
  expect(result.delivery_errors).toHaveLength(1);
@@ -354,14 +333,14 @@ describe("emitEventAction", () => {
354
333
  ],
355
334
  };
356
335
 
357
- const result = await emitEventAction(config, "exec-1");
336
+ const result = await emitEventAction(config, "exec-1", undefined, client);
358
337
 
359
338
  expect(mockSendWorkflowSignal).not.toHaveBeenCalled();
360
339
  expect(result.delivery_errors).toHaveLength(1);
361
340
  expect((result.delivery_errors as any)[0].error).toContain("'signal_name'");
362
341
  });
363
342
 
364
- it("reuses one client across multiple signal targets", async () => {
343
+ it("sends every signal target through the one client", async () => {
365
344
  const config: EmitEventConfig = {
366
345
  event: { type: "fanout" },
367
346
  delivery: [
@@ -370,14 +349,13 @@ describe("emitEventAction", () => {
370
349
  ],
371
350
  };
372
351
 
373
- const result = await emitEventAction(config, "exec-1");
352
+ const result = await emitEventAction(config, "exec-1", undefined, client);
374
353
 
375
- expect(mockClientCtor).toHaveBeenCalledTimes(1);
376
354
  expect(mockSendWorkflowSignal).toHaveBeenCalledTimes(2);
377
355
  expect(result.delivery_errors).toBeUndefined();
378
356
  });
379
357
 
380
- it("constructs no client for webhook-only delivery", async () => {
358
+ it("never touches the client for webhook-only delivery", async () => {
381
359
  const originalFetch = globalThis.fetch;
382
360
  globalThis.fetch = vi.fn(() =>
383
361
  Promise.resolve(new Response(null, { status: 200 })),
@@ -389,9 +367,9 @@ describe("emitEventAction", () => {
389
367
  delivery: [{ webhook: { url: "https://events.example.com" } }],
390
368
  };
391
369
 
392
- await emitEventAction(config, "exec-1");
370
+ await emitEventAction(config, "exec-1", undefined, client);
393
371
 
394
- expect(mockClientCtor).not.toHaveBeenCalled();
372
+ expect(mockSendWorkflowSignal).not.toHaveBeenCalled();
395
373
  } finally {
396
374
  globalThis.fetch = originalFetch;
397
375
  }