@stigmer/runner 3.40.1 → 3.41.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.
Files changed (248) hide show
  1. package/README.md +3 -1
  2. package/dist/.build-fingerprint +1 -1
  3. package/dist/__test-utils__/execution-record-fixture.d.ts +2 -1
  4. package/dist/__test-utils__/execution-record-fixture.js.map +1 -1
  5. package/dist/activities/call-agent.js +1 -1
  6. package/dist/activities/discover-mcp-server.d.ts +7 -1
  7. package/dist/activities/discover-mcp-server.js +13 -29
  8. package/dist/activities/discover-mcp-server.js.map +1 -1
  9. package/dist/activities/execute-cursor/__test-utils__/fold.d.ts +4 -2
  10. package/dist/activities/execute-cursor/__test-utils__/fold.js +3 -4
  11. package/dist/activities/execute-cursor/__test-utils__/fold.js.map +1 -1
  12. package/dist/activities/execute-cursor/__test-utils__/hermetic-cursor.js +1 -0
  13. package/dist/activities/execute-cursor/__test-utils__/hermetic-cursor.js.map +1 -1
  14. package/dist/activities/execute-cursor/approval-policy.d.ts +1 -29
  15. package/dist/activities/execute-cursor/approval-policy.js +11 -35
  16. package/dist/activities/execute-cursor/approval-policy.js.map +1 -1
  17. package/dist/activities/execute-cursor/boundary-rows.js +4 -4
  18. package/dist/activities/execute-cursor/boundary-rows.js.map +1 -1
  19. package/dist/activities/execute-cursor/translator.d.ts +12 -17
  20. package/dist/activities/execute-cursor/translator.js +13 -30
  21. package/dist/activities/execute-cursor/translator.js.map +1 -1
  22. package/dist/activities/execute-cursor/turn-stream.js +4 -2
  23. package/dist/activities/execute-cursor/turn-stream.js.map +1 -1
  24. package/dist/activities/execute-deep-agent/__test-utils__/hermetic-deep-agent.d.ts +11 -1
  25. package/dist/activities/execute-deep-agent/__test-utils__/hermetic-deep-agent.js +22 -3
  26. package/dist/activities/execute-deep-agent/__test-utils__/hermetic-deep-agent.js.map +1 -1
  27. package/dist/activities/execute-deep-agent/inline-publisher.d.ts +33 -2
  28. package/dist/activities/execute-deep-agent/inline-publisher.js +33 -7
  29. package/dist/activities/execute-deep-agent/inline-publisher.js.map +1 -1
  30. package/dist/activities/execute-deep-agent/platform-route.d.ts +48 -13
  31. package/dist/activities/execute-deep-agent/platform-route.js +87 -19
  32. package/dist/activities/execute-deep-agent/platform-route.js.map +1 -1
  33. package/dist/activities/execute-deep-agent/streaming-side-effects.d.ts +20 -3
  34. package/dist/activities/execute-deep-agent/streaming-side-effects.js +26 -4
  35. package/dist/activities/execute-deep-agent/streaming-side-effects.js.map +1 -1
  36. package/dist/activities/execute-deep-agent/translator.d.ts +5 -4
  37. package/dist/activities/execute-deep-agent/translator.js +5 -4
  38. package/dist/activities/execute-deep-agent/translator.js.map +1 -1
  39. package/dist/activities/execute-deep-agent/turn-settle.js +6 -6
  40. package/dist/activities/execute-deep-agent/turn-settle.js.map +1 -1
  41. package/dist/activities/execute-deep-agent/turn-setup.js +7 -7
  42. package/dist/activities/execute-deep-agent/turn-setup.js.map +1 -1
  43. package/dist/activities/execute-deep-agent/turn-stream.d.ts +10 -6
  44. package/dist/activities/execute-deep-agent/turn-stream.js +6 -16
  45. package/dist/activities/execute-deep-agent/turn-stream.js.map +1 -1
  46. package/dist/activities/execute-deep-agent/turn.d.ts +4 -1
  47. package/dist/activities/execute-deep-agent/turn.js +15 -6
  48. package/dist/activities/execute-deep-agent/turn.js.map +1 -1
  49. package/dist/activities/workflow-event-activities.js +2 -0
  50. package/dist/activities/workflow-event-activities.js.map +1 -1
  51. package/dist/config.d.ts +9 -0
  52. package/dist/config.js +2 -0
  53. package/dist/config.js.map +1 -1
  54. package/dist/exit-backstop.d.ts +64 -0
  55. package/dist/exit-backstop.js +79 -0
  56. package/dist/exit-backstop.js.map +1 -0
  57. package/dist/harness/terminal-table.d.ts +12 -5
  58. package/dist/harness/terminal-table.js +13 -13
  59. package/dist/harness/terminal-table.js.map +1 -1
  60. package/dist/harness/transcript/builder.d.ts +21 -0
  61. package/dist/harness/transcript/builder.js +53 -31
  62. package/dist/harness/transcript/builder.js.map +1 -1
  63. package/dist/harness/transcript/events.d.ts +22 -26
  64. package/dist/harness/transcript/events.js +6 -5
  65. package/dist/harness/transcript/events.js.map +1 -1
  66. package/dist/harness/turn-context.d.ts +3 -1
  67. package/dist/harness/turn-context.js +5 -3
  68. package/dist/harness/turn-context.js.map +1 -1
  69. package/dist/main.js +10 -4
  70. package/dist/main.js.map +1 -1
  71. package/dist/middleware/cost-advisory.d.ts +6 -3
  72. package/dist/middleware/cost-advisory.js +6 -3
  73. package/dist/middleware/cost-advisory.js.map +1 -1
  74. package/dist/middleware/execution-budget.d.ts +22 -7
  75. package/dist/middleware/execution-budget.js +57 -35
  76. package/dist/middleware/execution-budget.js.map +1 -1
  77. package/dist/middleware/index.d.ts +10 -7
  78. package/dist/middleware/index.js +10 -7
  79. package/dist/middleware/index.js.map +1 -1
  80. package/dist/middleware/types.d.ts +6 -1
  81. package/dist/runner-manager.js +3 -2
  82. package/dist/runner-manager.js.map +1 -1
  83. package/dist/runner.d.ts +25 -0
  84. package/dist/runner.js +41 -14
  85. package/dist/runner.js.map +1 -1
  86. package/dist/shared/approval-policy.d.ts +23 -13
  87. package/dist/shared/approval-policy.js +47 -14
  88. package/dist/shared/approval-policy.js.map +1 -1
  89. package/dist/shared/caller-identity.d.ts +4 -5
  90. package/dist/shared/caller-identity.js +4 -5
  91. package/dist/shared/caller-identity.js.map +1 -1
  92. package/dist/shared/channel-attachment.js +3 -2
  93. package/dist/shared/channel-attachment.js.map +1 -1
  94. package/dist/shared/connect-backfill.d.ts +2 -1
  95. package/dist/shared/connect-backfill.js +2 -2
  96. package/dist/shared/connect-backfill.js.map +1 -1
  97. package/dist/shared/mcp-resolver.d.ts +10 -1
  98. package/dist/shared/mcp-resolver.js +11 -2
  99. package/dist/shared/mcp-resolver.js.map +1 -1
  100. package/dist/shared/memory-attachment.js +3 -2
  101. package/dist/shared/memory-attachment.js.map +1 -1
  102. package/dist/shared/model-error.d.ts +25 -3
  103. package/dist/shared/model-error.js +78 -10
  104. package/dist/shared/model-error.js.map +1 -1
  105. package/dist/shared/platform-server-address.d.ts +71 -0
  106. package/dist/shared/platform-server-address.js +103 -0
  107. package/dist/shared/platform-server-address.js.map +1 -0
  108. package/dist/shared/synthesized-attachment.d.ts +0 -5
  109. package/dist/shared/synthesized-attachment.js +0 -12
  110. package/dist/shared/synthesized-attachment.js.map +1 -1
  111. package/dist/shared/tool-rounds.d.ts +51 -23
  112. package/dist/shared/tool-rounds.js +67 -24
  113. package/dist/shared/tool-rounds.js.map +1 -1
  114. package/dist/shared/workspace/platform-dir.d.ts +15 -0
  115. package/dist/shared/workspace/platform-dir.js +18 -1
  116. package/dist/shared/workspace/platform-dir.js.map +1 -1
  117. package/dist/shared/workspace/stigmer-link.d.ts +21 -14
  118. package/dist/shared/workspace/stigmer-link.js +91 -29
  119. package/dist/shared/workspace/stigmer-link.js.map +1 -1
  120. package/dist/tools/url-guard.d.ts +5 -1
  121. package/dist/tools/url-guard.js +9 -2
  122. package/dist/tools/url-guard.js.map +1 -1
  123. package/dist/worker.d.ts +11 -2
  124. package/dist/worker.js +1 -1
  125. package/dist/worker.js.map +1 -1
  126. package/dist/workflow-engine/tasks/human-input.d.ts +16 -0
  127. package/dist/workflow-engine/tasks/human-input.js +79 -37
  128. package/dist/workflow-engine/tasks/human-input.js.map +1 -1
  129. package/dist/workflow-engine/types.d.ts +14 -4
  130. package/dist/workflow-engine/types.js.map +1 -1
  131. package/dist/workflows/call-agent-orchestrator.d.ts +13 -0
  132. package/dist/workflows/call-agent-orchestrator.js.map +1 -1
  133. package/dist/workflows/engine-core.d.ts +25 -0
  134. package/dist/workflows/engine-core.js.map +1 -1
  135. package/dist/workflows/execute-from-execution.d.ts +3 -0
  136. package/dist/workflows/execute-from-execution.js.map +1 -1
  137. package/dist/workflows/human-input-orchestrator.d.ts +9 -12
  138. package/dist/workflows/human-input-orchestrator.js +12 -44
  139. package/dist/workflows/human-input-orchestrator.js.map +1 -1
  140. package/package.json +5 -5
  141. package/src/__test-utils__/config-fixture.ts +1 -0
  142. package/src/__test-utils__/execution-record-fixture.ts +2 -1
  143. package/src/__test-utils__/harness-contract/runtime-contract.ts +5 -3
  144. package/src/__test-utils__/test-gate.ts +8 -0
  145. package/src/__tests__/exit-backstop.test.ts +91 -0
  146. package/src/__tests__/golden-e2e.test.ts +80 -62
  147. package/src/__tests__/history-encryption-e2e.test.ts +51 -27
  148. package/src/__tests__/runner.test.ts +60 -2
  149. package/src/activities/__tests__/classify-tool-approvals.test.ts +1 -0
  150. package/src/activities/__tests__/discover-mcp-server.hang.test.ts +5 -1
  151. package/src/activities/__tests__/discover-mcp-server.test.ts +90 -49
  152. package/src/activities/__tests__/workflow-event-activities.test.ts +28 -7
  153. package/src/activities/call-agent.ts +1 -1
  154. package/src/activities/discover-mcp-server.ts +27 -37
  155. package/src/activities/execute-cursor/__test-utils__/fold.ts +6 -4
  156. package/src/activities/execute-cursor/__test-utils__/hermetic-cursor.ts +1 -0
  157. package/src/activities/execute-cursor/__tests__/approval-gate.test.ts +4 -29
  158. package/src/activities/execute-cursor/__tests__/hermetic/bypassed-tool-call.test.ts +120 -0
  159. package/src/activities/execute-cursor/__tests__/hermetic/file-review-capture.test.ts +5 -5
  160. package/src/activities/execute-cursor/__tests__/hermetic/goldens/file-review-capture.status.json +0 -2
  161. package/src/activities/execute-cursor/__tests__/hermetic/goldens/resolution-error.status.json +1 -1
  162. package/src/activities/execute-cursor/__tests__/hermetic/goldens/shell-output-delta.error-delta.status.json +0 -3
  163. package/src/activities/execute-cursor/__tests__/hermetic/goldens/shell-output-delta.output-first.status.json +0 -2
  164. package/src/activities/execute-cursor/__tests__/hermetic/goldens/shell-output-delta.status.json +0 -2
  165. package/src/activities/execute-cursor/__tests__/hermetic/goldens/stall.status.json +0 -2
  166. package/src/activities/execute-cursor/__tests__/hermetic/goldens/unattributed-hook-block.status.json +0 -3
  167. package/src/activities/execute-cursor/__tests__/hermetic/shell-output-delta.test.ts +9 -7
  168. package/src/activities/execute-cursor/__tests__/hermetic/thrown-error-arms.test.ts +6 -1
  169. package/src/activities/execute-cursor/__tests__/hitl-resume-history.test.ts +8 -11
  170. package/src/activities/execute-cursor/__tests__/sequential-gate-resume.test.ts +1 -3
  171. package/src/activities/execute-cursor/__tests__/translator.test.ts +68 -15
  172. package/src/activities/execute-cursor/approval-policy.ts +11 -36
  173. package/src/activities/execute-cursor/boundary-rows.ts +3 -3
  174. package/src/activities/execute-cursor/translator.ts +15 -32
  175. package/src/activities/execute-cursor/turn-stream.ts +4 -1
  176. package/src/activities/execute-deep-agent/__test-utils__/hermetic-deep-agent.ts +33 -2
  177. package/src/activities/execute-deep-agent/__tests__/adapter.test.ts +1 -0
  178. package/src/activities/execute-deep-agent/__tests__/hermetic/file-review-capture.test.ts +4 -0
  179. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/file-review-capture.status.json +1 -1
  180. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/inline-artifact.turn1.status.json +1 -1
  181. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/inline-artifact.turn2.status.json +1 -1
  182. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/recursion-limit.persisted.status.json +8 -73
  183. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/resolution-error.status.json +1 -1
  184. package/src/activities/execute-deep-agent/__tests__/hermetic/inline-artifact-across-gate.test.ts +17 -0
  185. package/src/activities/execute-deep-agent/__tests__/hermetic/recursion-limit.test.ts +86 -26
  186. package/src/activities/execute-deep-agent/__tests__/hermetic/resolution-error.test.ts +9 -5
  187. package/src/activities/execute-deep-agent/__tests__/hermetic/thrown-turn-drains-publishes.test.ts +159 -0
  188. package/src/activities/execute-deep-agent/__tests__/inline-publisher.test.ts +84 -0
  189. package/src/activities/execute-deep-agent/__tests__/platform-route.test.ts +47 -10
  190. package/src/activities/execute-deep-agent/__tests__/streaming-side-effects.test.ts +34 -0
  191. package/src/activities/execute-deep-agent/__tests__/turn-stream.test.ts +13 -5
  192. package/src/activities/execute-deep-agent/inline-publisher.ts +47 -7
  193. package/src/activities/execute-deep-agent/platform-route.ts +109 -20
  194. package/src/activities/execute-deep-agent/streaming-side-effects.ts +27 -4
  195. package/src/activities/execute-deep-agent/translator.ts +5 -4
  196. package/src/activities/execute-deep-agent/turn-settle.ts +6 -6
  197. package/src/activities/execute-deep-agent/turn-setup.ts +7 -7
  198. package/src/activities/execute-deep-agent/turn-stream.ts +15 -22
  199. package/src/activities/execute-deep-agent/turn.ts +16 -7
  200. package/src/activities/workflow-event-activities.ts +2 -0
  201. package/src/config.ts +11 -0
  202. package/src/exit-backstop.ts +97 -0
  203. package/src/harness/README.md +5 -5
  204. package/src/harness/terminal-table.ts +13 -13
  205. package/src/harness/transcript/__tests__/builder.test.ts +93 -21
  206. package/src/harness/transcript/builder.ts +53 -30
  207. package/src/harness/transcript/events.ts +22 -27
  208. package/src/harness/turn-context.ts +5 -3
  209. package/src/main.ts +9 -5
  210. package/src/middleware/__tests__/execution-budget.test.ts +181 -103
  211. package/src/middleware/cost-advisory.ts +6 -3
  212. package/src/middleware/execution-budget.ts +60 -34
  213. package/src/middleware/index.ts +10 -7
  214. package/src/middleware/types.ts +6 -1
  215. package/src/runner-manager.ts +3 -2
  216. package/src/runner.ts +57 -15
  217. package/src/shared/__tests__/approval-policy.test.ts +41 -0
  218. package/src/shared/__tests__/artifact-storage.test.ts +1 -0
  219. package/src/shared/__tests__/connect-backfill.test.ts +15 -11
  220. package/src/shared/__tests__/mcp-resolver.test.ts +72 -10
  221. package/src/shared/__tests__/model-error.test.ts +71 -3
  222. package/src/shared/__tests__/platform-server-address.test.ts +122 -0
  223. package/src/shared/__tests__/tool-rounds.test.ts +64 -14
  224. package/src/shared/approval-policy.ts +57 -14
  225. package/src/shared/caller-identity.ts +4 -5
  226. package/src/shared/channel-attachment.ts +2 -2
  227. package/src/shared/connect-backfill.ts +5 -1
  228. package/src/shared/mcp-resolver.ts +11 -1
  229. package/src/shared/memory-attachment.ts +2 -2
  230. package/src/shared/model-error.ts +87 -9
  231. package/src/shared/platform-server-address.ts +123 -0
  232. package/src/shared/synthesized-attachment.ts +0 -12
  233. package/src/shared/tool-rounds.ts +70 -24
  234. package/src/shared/workspace/__tests__/stigmer-link.test.ts +107 -10
  235. package/src/shared/workspace/platform-dir.ts +19 -1
  236. package/src/shared/workspace/stigmer-link.ts +99 -29
  237. package/src/tools/__tests__/url-guard.test.ts +19 -1
  238. package/src/tools/url-guard.ts +9 -1
  239. package/src/worker.ts +12 -2
  240. package/src/workflow-engine/__tests__/golden-execution.test.ts +2 -1
  241. package/src/workflow-engine/__tests__/loader.test.ts +1 -1
  242. package/src/workflow-engine/__tests__/tasks/human-input.test.ts +133 -57
  243. package/src/workflow-engine/tasks/human-input.ts +92 -42
  244. package/src/workflow-engine/types.ts +16 -4
  245. package/src/workflows/call-agent-orchestrator.ts +7 -0
  246. package/src/workflows/engine-core.ts +18 -0
  247. package/src/workflows/execute-from-execution.ts +2 -1
  248. package/src/workflows/human-input-orchestrator.ts +15 -56
@@ -0,0 +1,91 @@
1
+ import { afterEach, describe, expect, it, vi } from "vitest";
2
+ import { EXIT_BACKSTOP_MS, armExitBackstop, describeResources } from "../exit-backstop.js";
3
+
4
+ /**
5
+ * The exit backstop's contract (exit-backstop.ts): it never holds the loop,
6
+ * it stays silent before its bound, and at the bound it names what is still
7
+ * alive and exits 0, once. The real-process behaviour (a healthy runner
8
+ * exits in milliseconds, a leaked handle is cut at the bound) is proven
9
+ * against the built runner and by the execution conformance harness's
10
+ * slow-exit line; these pin the logic.
11
+ */
12
+
13
+ afterEach(() => {
14
+ vi.useRealTimers();
15
+ });
16
+
17
+ function arm(overrides: { boundMs?: number; resources?: string[] } = {}) {
18
+ const exit = vi.fn<(code: number) => void>();
19
+ const log = vi.fn<(line: string) => void>();
20
+ const timer = armExitBackstop({
21
+ boundMs: overrides.boundMs,
22
+ exit,
23
+ log,
24
+ activeResources: () => overrides.resources ?? ["TCPSocketWrap", "Timeout", "TCPSocketWrap"],
25
+ });
26
+ return { exit, log, timer };
27
+ }
28
+
29
+ describe("armExitBackstop", () => {
30
+ it("does not hold the event loop", () => {
31
+ const { timer } = arm();
32
+ try {
33
+ expect(timer.hasRef()).toBe(false);
34
+ } finally {
35
+ clearTimeout(timer);
36
+ }
37
+ });
38
+
39
+ it("stays silent before the bound", () => {
40
+ vi.useFakeTimers();
41
+ const { exit, log } = arm();
42
+
43
+ vi.advanceTimersByTime(EXIT_BACKSTOP_MS - 1);
44
+
45
+ expect(exit).not.toHaveBeenCalled();
46
+ expect(log).not.toHaveBeenCalled();
47
+ });
48
+
49
+ it("at the bound, names what is still alive and exits 0, once", () => {
50
+ vi.useFakeTimers();
51
+ const { exit, log } = arm();
52
+
53
+ vi.advanceTimersByTime(EXIT_BACKSTOP_MS);
54
+ vi.advanceTimersByTime(EXIT_BACKSTOP_MS);
55
+
56
+ expect(log).toHaveBeenCalledTimes(1);
57
+ expect(log.mock.calls[0]?.[0]).toBe(
58
+ `[runner] Process still alive ${EXIT_BACKSTOP_MS}ms after shutdown; forcing exit. ` +
59
+ "Active resources: TCPSocketWrap×2, Timeout×1",
60
+ );
61
+ expect(exit).toHaveBeenCalledTimes(1);
62
+ expect(exit).toHaveBeenCalledWith(0);
63
+ });
64
+
65
+ it("honours an explicit bound", () => {
66
+ vi.useFakeTimers();
67
+ const { exit } = arm({ boundMs: 250 });
68
+
69
+ vi.advanceTimersByTime(249);
70
+ expect(exit).not.toHaveBeenCalled();
71
+ vi.advanceTimersByTime(1);
72
+ expect(exit).toHaveBeenCalledWith(0);
73
+ });
74
+
75
+ it("keeps the bound above the conformance harness's 2 s slow-exit line", () => {
76
+ // Below it, the harness would stop reporting the very leaks this cuts.
77
+ expect(EXIT_BACKSTOP_MS).toBeGreaterThan(2_000);
78
+ });
79
+ });
80
+
81
+ describe("describeResources", () => {
82
+ it("groups and sorts, so the same leak reads the same on every exit", () => {
83
+ expect(describeResources(["Timeout", "TCPSocketWrap", "PipeWrap", "TCPSocketWrap"])).toBe(
84
+ "PipeWrap×1, TCPSocketWrap×2, Timeout×1",
85
+ );
86
+ });
87
+
88
+ it("says so when Node reports nothing", () => {
89
+ expect(describeResources([])).toBe("none reported");
90
+ });
91
+ });
@@ -7,14 +7,21 @@
7
7
  * - Expression evaluation via local activities (jq-wasm)
8
8
  * - Timer handling (sleep/wait)
9
9
  *
10
- * Prerequisites:
11
- * - @temporalio/testing ephemeral server binary (installed via npm)
12
- * - Temporal CLI (`temporal`) on PATH
10
+ * Prerequisites: none beyond `npm install`. `TestWorkflowEnvironment.createTimeSkipping()`
11
+ * downloads and runs Temporal's own test server, which skips a timer as soon as
12
+ * the workflow waits on it, so the goldens that wait minutes (#16 wait-delay,
13
+ * #21 retry-backoff) finish inside the per-test budget.
13
14
  *
14
15
  * The workflow-engine uses `deepClone` from `clone.ts` which falls
15
16
  * back to JSON round-trip when `structuredClone` is unavailable in
16
- * the Temporal deterministic V8 sandbox. Tests are skipped if the
17
- * environment can't start a Temporal test server.
17
+ * the Temporal deterministic V8 sandbox.
18
+ *
19
+ * Only one failure skips: the Temporal test server cannot boot (no network
20
+ * for the download, say). Each golden then skips by name with the boot
21
+ * error. Anything after the boot (the worker, the workflow bundle, a
22
+ * golden itself) fails. The suite once swallowed every error and returned
23
+ * early from each test, which vitest counts as a pass, so it reported
24
+ * sixteen passes without running a golden (stigmer#1300).
18
25
  *
19
26
  * Run with: npx vitest run src/__tests__/golden-e2e.test.ts
20
27
  */
@@ -24,8 +31,9 @@ import { readFileSync } from "node:fs";
24
31
  import { join, dirname } from "node:path";
25
32
  import { fileURLToPath } from "node:url";
26
33
  import { loadWorkflowFromYaml } from "../workflow-engine/loader.js";
34
+ import { IN_TEST_GATE } from "../__test-utils__/test-gate.js";
27
35
  import { evaluateExpressionBatch } from "../workflow-engine/expression.js";
28
- import type { ExecuteServerlessWorkflowInput } from "../workflows/engine-core.js";
36
+ import type { EngineActivities, ExecuteServerlessWorkflowInput } from "../workflows/engine-core.js";
29
37
  import type { WorkflowModel } from "../workflow-engine/types.js";
30
38
  import { INLINE_MODEL_WORKFLOW_TYPE } from "../__test-utils__/workflows/inline-model.js";
31
39
 
@@ -48,7 +56,8 @@ type Worker = import("@temporalio/worker").Worker;
48
56
  let env: TestWorkflowEnvironment | null = null;
49
57
  let worker: Worker | null = null;
50
58
  let workerRunPromise: Promise<void> | null = null;
51
- let envReady = false;
59
+ /** Why the Temporal test server could not boot; `null` once it has. */
60
+ let bootError: string | null = "not attempted";
52
61
 
53
62
  function createMockActivities() {
54
63
  return {
@@ -81,7 +90,12 @@ function createMockActivities() {
81
90
  ClearWorkflowApprovalStatus: async (): Promise<void> => {},
82
91
  UpdateWorkflowFileReviewStatus: async (): Promise<void> => {},
83
92
  GetAwaitingFileReviewChangeSetIds: async (): Promise<string[]> => [],
84
- };
93
+ GetAgentExecutionProgress: async (): Promise<null> => null,
94
+ ResetEventSequence: async (): Promise<number> => 0,
95
+ EmitWorkflowEvents: async (): Promise<void> => {},
96
+ LoadRecoveryContext: async (): Promise<unknown[]> => [],
97
+ PromoteTaskOutput: async (): Promise<void> => {},
98
+ } satisfies Record<keyof EngineActivities, unknown>;
85
99
  }
86
100
 
87
101
  async function runGoldenWorkflow(model: WorkflowModel, workflowInput: unknown = null): Promise<unknown> {
@@ -96,34 +110,36 @@ async function runGoldenWorkflow(model: WorkflowModel, workflowInput: unknown =
96
110
  taskQueue: TASK_QUEUE,
97
111
  workflowId: `golden-e2e-${Date.now()}-${Math.random().toString(36).slice(2)}`,
98
112
  args: [input],
99
- workflowExecutionTimeout: "30s",
113
+ // Workflow time, which the test server skips through: #16 alone waits 96s of
114
+ // timers. Real time stays bounded by vitest's per-test timeout.
115
+ workflowExecutionTimeout: "10m",
100
116
  });
101
117
  }
102
118
 
103
119
  describe("Golden E2E — Temporal TestWorkflowEnvironment", () => {
104
120
  beforeAll(async () => {
121
+ const { TestWorkflowEnvironment: TWE } = await import("@temporalio/testing");
122
+ const { Worker: W } = await import("@temporalio/worker");
123
+
105
124
  try {
106
- const { TestWorkflowEnvironment: TWE } = await import("@temporalio/testing");
107
- const { Worker: W } = await import("@temporalio/worker");
108
-
109
- env = await TWE.createLocal();
110
- worker = await W.create({
111
- connection: env.nativeConnection,
112
- taskQueue: TASK_QUEUE,
113
- workflowsPath: WORKFLOWS_PATH,
114
- activities: createMockActivities(),
115
- });
116
- workerRunPromise = worker.run();
117
-
118
- // Smoke test: execute simplest workflow to verify sandbox compatibility
119
- const smokeModel = loadWorkflowFromYaml(loadGolden("01-operation-basic.yaml"));
120
- await runGoldenWorkflow(smokeModel);
121
- envReady = true;
125
+ env = await TWE.createTimeSkipping();
122
126
  } catch (err: unknown) {
123
- const msg = err instanceof Error ? err.message : String(err);
124
- console.warn(`Temporal E2E smoke test failed (tests will be skipped): ${msg}`);
125
- envReady = false;
127
+ // Inside a gate the test server is a provided dependency: not booting is a failure.
128
+ if (IN_TEST_GATE) throw err;
129
+ bootError = err instanceof Error ? err.message : String(err);
130
+ console.warn(`Temporal test server cannot boot; every golden skips: ${bootError}`);
131
+ return;
126
132
  }
133
+ bootError = null;
134
+
135
+ // Past the boot, a failure is the suite's to report, not to skip.
136
+ worker = await W.create({
137
+ connection: env.nativeConnection,
138
+ taskQueue: TASK_QUEUE,
139
+ workflowsPath: WORKFLOWS_PATH,
140
+ activities: createMockActivities(),
141
+ });
142
+ workerRunPromise = worker.run();
127
143
  }, 60_000);
128
144
 
129
145
  afterAll(async () => {
@@ -135,117 +151,119 @@ describe("Golden E2E — Temporal TestWorkflowEnvironment", () => {
135
151
  }, 30_000);
136
152
 
137
153
  // ─────────────────────────────────────────────────────────────────
138
- // Each test skips gracefully if the Temporal test server
139
- // cannot start or the smoke test fails.
154
+ // Each golden skips by name when the test server could not boot.
140
155
  // ─────────────────────────────────────────────────────────────────
141
156
 
142
- it("#01 operation-basic — sequential set tasks", async () => {
143
- if (!envReady) return;
157
+ it("#01 operation-basic — sequential set tasks", async (testCtx) => {
158
+ if (bootError !== null) return testCtx.skip(`Temporal test server cannot boot: ${bootError}`);
144
159
  const model = loadWorkflowFromYaml(loadGolden("01-operation-basic.yaml"));
145
160
  const result = await runGoldenWorkflow(model);
146
161
  expect(result).toEqual({ workflow_completed: true });
147
162
  });
148
163
 
149
- it("#14 try-catch-raise — error handling pipeline", async () => {
150
- if (!envReady) return;
164
+ it("#14 try-catch-raise — error handling pipeline", async (testCtx) => {
165
+ if (bootError !== null) return testCtx.skip(`Temporal test server cannot boot: ${bootError}`);
151
166
  const model = loadWorkflowFromYaml(loadGolden("14-try-catch-raise.yaml"));
152
167
  const result = await runGoldenWorkflow(model);
153
168
  expect(result).toEqual({ phase: "completed" });
154
169
  });
155
170
 
156
- it("#15 fork-parallel — non-compete branches", async () => {
157
- if (!envReady) return;
171
+ it("#15 fork-parallel — non-compete branches", async (testCtx) => {
172
+ if (bootError !== null) return testCtx.skip(`Temporal test server cannot boot: ${bootError}`);
158
173
  const model = loadWorkflowFromYaml(loadGolden("15-fork-parallel.yaml"));
159
174
  const result = await runGoldenWorkflow(model);
160
175
  expect(result).toBeDefined();
161
176
  });
162
177
 
163
- it("#02 switch-conditional — HTTP + conditional branching", async () => {
164
- if (!envReady) return;
178
+ it("#02 switch-conditional — HTTP + conditional branching", async (testCtx) => {
179
+ if (bootError !== null) return testCtx.skip(`Temporal test server cannot boot: ${bootError}`);
165
180
  const model = loadWorkflowFromYaml(loadGolden("02-switch-conditional.yaml"));
166
181
  const result = await runGoldenWorkflow(model);
167
182
  expect(result).toBeDefined();
168
183
  });
169
184
 
170
- it("#03 foreach-loop — iteration with HTTP calls", async () => {
171
- if (!envReady) return;
185
+ it("#03 foreach-loop — iteration with HTTP calls", async (testCtx) => {
186
+ if (bootError !== null) return testCtx.skip(`Temporal test server cannot boot: ${bootError}`);
172
187
  const model = loadWorkflowFromYaml(loadGolden("03-foreach-loop.yaml"));
173
188
  const result = await runGoldenWorkflow(model, { items: ["a", "b", "c"] });
174
189
  expect(result).toBeDefined();
175
190
  });
176
191
 
177
- it("#04 parallel-concurrent — fork with HTTP branches", async () => {
178
- if (!envReady) return;
192
+ it("#04 parallel-concurrent — fork with HTTP branches", async (testCtx) => {
193
+ if (bootError !== null) return testCtx.skip(`Temporal test server cannot boot: ${bootError}`);
179
194
  const model = loadWorkflowFromYaml(loadGolden("04-parallel-concurrent.yaml"));
180
195
  const result = await runGoldenWorkflow(model);
181
196
  expect(result).toBeDefined();
182
197
  });
183
198
 
184
- it("#06 sleep-delay — wait + HTTP calls", async () => {
185
- if (!envReady) return;
199
+ it("#06 sleep-delay — wait + HTTP calls", async (testCtx) => {
200
+ if (bootError !== null) return testCtx.skip(`Temporal test server cannot boot: ${bootError}`);
186
201
  const model = loadWorkflowFromYaml(loadGolden("06-sleep-delay.yaml"));
187
202
  const result = await runGoldenWorkflow(model);
188
203
  expect(result).toBeDefined();
189
204
  });
190
205
 
191
- it("#07 inject-transform — expressions + HTTP", async () => {
192
- if (!envReady) return;
206
+ it("#07 inject-transform — expressions + HTTP", async (testCtx) => {
207
+ if (bootError !== null) return testCtx.skip(`Temporal test server cannot boot: ${bootError}`);
193
208
  const model = loadWorkflowFromYaml(loadGolden("07-inject-transform.yaml"));
194
209
  const result = await runGoldenWorkflow(model, { a: 10, b: 20 });
195
210
  expect(result).toBeDefined();
196
211
  });
197
212
 
198
- it("#08 error-retry — try/catch with HTTP", async () => {
199
- if (!envReady) return;
213
+ it("#08 error-retry — try/catch with HTTP", async (testCtx) => {
214
+ if (bootError !== null) return testCtx.skip(`Temporal test server cannot boot: ${bootError}`);
200
215
  const model = loadWorkflowFromYaml(loadGolden("08-error-retry.yaml"));
201
216
  const result = await runGoldenWorkflow(model);
202
217
  expect(result).toBeDefined();
203
218
  });
204
219
 
205
- it("#11 claimcheck-large-payload — sequential HTTP calls", async () => {
206
- if (!envReady) return;
220
+ it("#11 claimcheck-large-payload — sequential HTTP calls", async (testCtx) => {
221
+ if (bootError !== null) return testCtx.skip(`Temporal test server cannot boot: ${bootError}`);
207
222
  const model = loadWorkflowFromYaml(loadGolden("11-claimcheck-large-payload.yaml"));
208
223
  const result = await runGoldenWorkflow(model);
209
224
  expect(result).toBeDefined();
210
225
  });
211
226
 
212
- it("#12 claimcheck-between-steps — HTTP chain", async () => {
213
- if (!envReady) return;
227
+ it("#12 claimcheck-between-steps — HTTP chain", async (testCtx) => {
228
+ if (bootError !== null) return testCtx.skip(`Temporal test server cannot boot: ${bootError}`);
214
229
  const model = loadWorkflowFromYaml(loadGolden("12-claimcheck-between-steps.yaml"));
215
230
  const result = await runGoldenWorkflow(model);
216
231
  expect(result).toBeDefined();
217
232
  });
218
233
 
219
- it("#16 wait-delay — timer durations", async () => {
220
- if (!envReady) return;
234
+ it("#16 wait-delay — timer durations", async (testCtx) => {
235
+ if (bootError !== null) return testCtx.skip(`Temporal test server cannot boot: ${bootError}`);
221
236
  const model = loadWorkflowFromYaml(loadGolden("16-wait-delay.yaml"));
222
237
  const result = await runGoldenWorkflow(model);
223
238
  expect(result).toEqual({ completed: true });
224
239
  });
225
240
 
226
- it("#18 run-task — script + shell + workflow", async () => {
227
- if (!envReady) return;
241
+ // quarantined: stigmer#1322 — `run.workflow` starts its child by the workflow's
242
+ // name, which no worker registers; the kind is refused at save until that issue
243
+ // lands, and this golden is its end-to-end reproduction.
244
+ it.skip("#18 run-task — script + shell + workflow", async (testCtx) => {
245
+ if (bootError !== null) return testCtx.skip(`Temporal test server cannot boot: ${bootError}`);
228
246
  const model = loadWorkflowFromYaml(loadGolden("18-run-task.yaml"));
229
247
  const result = await runGoldenWorkflow(model);
230
248
  expect(result).toEqual({ all_runs_done: true });
231
249
  });
232
250
 
233
- it("#19 emit-event — CloudEvents envelope", async () => {
234
- if (!envReady) return;
251
+ it("#19 emit-event — CloudEvents envelope", async (testCtx) => {
252
+ if (bootError !== null) return testCtx.skip(`Temporal test server cannot boot: ${bootError}`);
235
253
  const model = loadWorkflowFromYaml(loadGolden("19-emit-event.yaml"));
236
254
  const result = await runGoldenWorkflow(model);
237
255
  expect(result).toEqual({ events_emitted: true });
238
256
  });
239
257
 
240
- it("#21 retry-backoff — catch-level retry", async () => {
241
- if (!envReady) return;
258
+ it("#21 retry-backoff — catch-level retry", async (testCtx) => {
259
+ if (bootError !== null) return testCtx.skip(`Temporal test server cannot boot: ${bootError}`);
242
260
  const model = loadWorkflowFromYaml(loadGolden("21-retry-backoff.yaml"));
243
261
  const result = await runGoldenWorkflow(model);
244
262
  expect(result).toEqual({ phase: "completed" });
245
263
  });
246
264
 
247
- it("#23 notification — webhook notification", async () => {
248
- if (!envReady) return;
265
+ it("#23 notification — webhook notification", async (testCtx) => {
266
+ if (bootError !== null) return testCtx.skip(`Temporal test server cannot boot: ${bootError}`);
249
267
  const model = loadWorkflowFromYaml(loadGolden("23-notification.yaml"));
250
268
  const result = await runGoldenWorkflow(model);
251
269
  expect(result).toEqual({ notifications_sent: true });
@@ -15,8 +15,8 @@
15
15
  * converter has no Void special-case — a data-bearing encrypted result
16
16
  * would fail its converter lookup.
17
17
  *
18
- * Follows the golden-e2e pattern: tests skip gracefully when the
19
- * Temporal test server cannot start.
18
+ * Follows the golden-e2e pattern: the test skips by name only when the
19
+ * Temporal dev server cannot boot; any later failure fails it.
20
20
  */
21
21
 
22
22
  import { describe, it, expect, beforeAll, afterAll } from "vitest";
@@ -24,9 +24,11 @@ import { randomBytes } from "node:crypto";
24
24
  import { join, dirname } from "node:path";
25
25
  import { fileURLToPath } from "node:url";
26
26
  import { loadWorkflowFromYaml } from "../workflow-engine/loader.js";
27
+ import { IN_TEST_GATE } from "../__test-utils__/test-gate.js";
27
28
  import { evaluateExpressionBatch } from "../workflow-engine/expression.js";
28
29
  import { EncryptionPayloadCodec } from "@stigmer/temporal-codecs";
29
- import type { ExecuteServerlessWorkflowInput } from "../workflows/engine-core.js";
30
+ import type { EngineActivities, ExecuteServerlessWorkflowInput } from "../workflows/engine-core.js";
31
+ import type { HydrateActivities } from "../workflows/execute-from-execution.js";
30
32
 
31
33
  const __dirname = dirname(fileURLToPath(import.meta.url));
32
34
  const WORKFLOWS_PATH = join(__dirname, "../workflows/index.ts");
@@ -68,7 +70,8 @@ type Worker = import("@temporalio/worker").Worker;
68
70
  let env: TestWorkflowEnvironment | null = null;
69
71
  let worker: Worker | null = null;
70
72
  let workerRunPromise: Promise<void> | null = null;
71
- let envReady = false;
73
+ /** Why the Temporal dev server could not boot; `null` once it has. */
74
+ let bootError: string | null = "not attempted";
72
75
 
73
76
  function createMockActivities() {
74
77
  return {
@@ -90,6 +93,23 @@ function createMockActivities() {
90
93
  EmitWorkflowEvents: async (): Promise<void> => {},
91
94
  LoadRecoveryContext: async (): Promise<unknown[]> => [],
92
95
  PromoteTaskOutput: async (): Promise<void> => {},
96
+ CallGrpc: unscheduled("CallGrpc"),
97
+ CallFunction: unscheduled("CallFunction"),
98
+ RunScript: unscheduled("RunScript"),
99
+ RunShell: unscheduled("RunShell"),
100
+ CallAgent: unscheduled("CallAgent"),
101
+ UpdateWorkflowTaskApprovalStatus: unscheduled("UpdateWorkflowTaskApprovalStatus"),
102
+ ClearWorkflowApprovalStatus: unscheduled("ClearWorkflowApprovalStatus"),
103
+ UpdateWorkflowFileReviewStatus: unscheduled("UpdateWorkflowFileReviewStatus"),
104
+ GetAwaitingFileReviewChangeSetIds: unscheduled("GetAwaitingFileReviewChangeSetIds"),
105
+ GetAgentExecutionProgress: unscheduled("GetAgentExecutionProgress"),
106
+ } satisfies Record<keyof (EngineActivities & HydrateActivities), unknown>;
107
+ }
108
+
109
+ /** An activity the secret-bearing workflow never schedules; reaching it is a test bug. */
110
+ function unscheduled(name: string): () => Promise<never> {
111
+ return async () => {
112
+ throw new Error(`${name} is not scheduled by the secret-bearing workflow`);
93
113
  };
94
114
  }
95
115
 
@@ -110,31 +130,35 @@ function collectByteFields(value: unknown, out: Uint8Array[]): void {
110
130
 
111
131
  describe("History encryption tripwire — Temporal TestWorkflowEnvironment", () => {
112
132
  beforeAll(async () => {
113
- try {
114
- const { TestWorkflowEnvironment: TWE } = await import("@temporalio/testing");
115
- const { Worker: W } = await import("@temporalio/worker");
133
+ const { TestWorkflowEnvironment: TWE } = await import("@temporalio/testing");
134
+ const { Worker: W } = await import("@temporalio/worker");
116
135
 
136
+ try {
117
137
  env = await TWE.createLocal();
118
- worker = await W.create({
119
- connection: env.nativeConnection,
120
- taskQueue: TASK_QUEUE,
121
- workflowsPath: WORKFLOWS_PATH,
122
- activities: createMockActivities(),
123
- dataConverter: {
124
- payloadCodecs: [
125
- new EncryptionPayloadCodec({
126
- primary: { keyId: "tripwire-key", key: randomBytes(32) },
127
- }),
128
- ],
129
- },
130
- });
131
- workerRunPromise = worker.run();
132
- envReady = true;
133
138
  } catch (err: unknown) {
134
- const msg = err instanceof Error ? err.message : String(err);
135
- console.warn(`Temporal test server unavailable (tests will be skipped): ${msg}`);
136
- envReady = false;
139
+ // Inside a gate the test server is a provided dependency: not booting is a failure.
140
+ if (IN_TEST_GATE) throw err;
141
+ bootError = err instanceof Error ? err.message : String(err);
142
+ console.warn(`Temporal dev server cannot boot; the tripwire skips: ${bootError}`);
143
+ return;
137
144
  }
145
+ bootError = null;
146
+
147
+ // Past the boot, a failure is the suite's to report, not to skip.
148
+ worker = await W.create({
149
+ connection: env.nativeConnection,
150
+ taskQueue: TASK_QUEUE,
151
+ workflowsPath: WORKFLOWS_PATH,
152
+ activities: createMockActivities(),
153
+ dataConverter: {
154
+ payloadCodecs: [
155
+ new EncryptionPayloadCodec({
156
+ primary: { keyId: "tripwire-key", key: randomBytes(32) },
157
+ }),
158
+ ],
159
+ },
160
+ });
161
+ workerRunPromise = worker.run();
138
162
  }, 60_000);
139
163
 
140
164
  afterAll(async () => {
@@ -145,8 +169,8 @@ describe("History encryption tripwire — Temporal TestWorkflowEnvironment", ()
145
169
  if (env) await env.teardown();
146
170
  }, 30_000);
147
171
 
148
- it("records no plaintext secret bytes anywhere in workflow history", async () => {
149
- if (!envReady || !env) return;
172
+ it("records no plaintext secret bytes anywhere in workflow history", async (testCtx) => {
173
+ if (bootError !== null || !env) return testCtx.skip(`Temporal dev server cannot boot: ${bootError}`);
150
174
 
151
175
  const workflowId = `tripwire-${Date.now()}`;
152
176
  // The starting client has NO codec, mirroring the Java/Go
@@ -1,5 +1,5 @@
1
- import { describe, it, expect } from "vitest";
2
- import { mapOptionsToConfig } from "../runner.js";
1
+ import { describe, it, expect, vi } from "vitest";
2
+ import { mapOptionsToConfig, releaseAfterDrain } from "../runner.js";
3
3
  import type { StigmerRunnerOptions, StigmerRunner } from "../runner.js";
4
4
  import type { TokenRef } from "../config.js";
5
5
 
@@ -224,3 +224,61 @@ describe("public API exports", () => {
224
224
  expect(typeof mod.createStigmerRunner).toBe("function");
225
225
  });
226
226
  });
227
+
228
+ describe("releaseAfterDrain", () => {
229
+ function resources(opts: { harnessFails?: boolean; closeFails?: boolean } = {}) {
230
+ const order: string[] = [];
231
+ return {
232
+ order,
233
+ value: {
234
+ shutdownHarnesses: vi.fn(async () => {
235
+ order.push("harnesses");
236
+ if (opts.harnessFails) {
237
+ throw new Error("harness boom");
238
+ }
239
+ }),
240
+ connection: {
241
+ close: vi.fn(async () => {
242
+ order.push("connection");
243
+ if (opts.closeFails) {
244
+ throw new Error("close boom");
245
+ }
246
+ }),
247
+ },
248
+ },
249
+ };
250
+ }
251
+
252
+ it("releases the harnesses, then closes the Temporal connection", async () => {
253
+ const r = resources();
254
+ const logError = vi.fn<(line: string) => void>();
255
+
256
+ await releaseAfterDrain(r.value, logError);
257
+
258
+ expect(r.order).toEqual(["harnesses", "connection"]);
259
+ expect(logError).not.toHaveBeenCalled();
260
+ });
261
+
262
+ it("still closes the connection when a harness teardown fails, and logs it", async () => {
263
+ const r = resources({ harnessFails: true });
264
+ const logError = vi.fn<(line: string) => void>();
265
+
266
+ await releaseAfterDrain(r.value, logError);
267
+
268
+ expect(r.order).toEqual(["harnesses", "connection"]);
269
+ expect(logError).toHaveBeenCalledTimes(1);
270
+ expect(logError.mock.calls[0]?.[0]).toContain("Harness shutdown failed after the worker drained");
271
+ expect(logError.mock.calls[0]?.[0]).toContain("harness boom");
272
+ });
273
+
274
+ it("logs a failed close instead of throwing, so a clean stop stays clean", async () => {
275
+ const r = resources({ closeFails: true });
276
+ const logError = vi.fn<(line: string) => void>();
277
+
278
+ await expect(releaseAfterDrain(r.value, logError)).resolves.toBeUndefined();
279
+
280
+ expect(logError).toHaveBeenCalledTimes(1);
281
+ expect(logError.mock.calls[0]?.[0]).toContain("Temporal connection close failed after the worker drained");
282
+ expect(logError.mock.calls[0]?.[0]).toContain("close boom");
283
+ });
284
+ });
@@ -727,6 +727,7 @@ function makeConfig() {
727
727
  temporalConnection: {},
728
728
  stigmerBackendEndpoint: "http://localhost:7234",
729
729
  mcpBridgeEndpoint: null,
730
+ mcpPublicEndpoint: null,
730
731
  stigmerTokenRef: { current: "test-token" },
731
732
  cursorApiKey: "",
732
733
  workspaceRootDir: "/tmp/test",
@@ -14,6 +14,10 @@
14
14
  */
15
15
  import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
16
16
  import { createServer, type Server } from "node:http";
17
+ import { testConfig } from "../../__test-utils__/config-fixture.js";
18
+
19
+ /** The runner endpoints discovery fills STIGMER_SERVER_ADDRESS from. */
20
+ const PLATFORM_ENDPOINTS = testConfig();
17
21
 
18
22
  vi.mock("../../idle-watchdog.js", () => ({
19
23
  activityStarted: vi.fn(),
@@ -85,7 +89,7 @@ describe("discovery against a 4xx-then-silent-SSE endpoint (issue #239)", () =>
85
89
 
86
90
  const promise = discoverMcpServer(
87
91
  { mcpServerId: "mcp-monday" },
88
- { stigmerClient: stigmerClient as never, transportPosture: "stdio-forbidden" },
92
+ { stigmerClient: stigmerClient as never, transportPosture: "stdio-forbidden", platformEndpoints: PLATFORM_ENDPOINTS },
89
93
  );
90
94
  // Attach the rejection expectation BEFORE advancing so the rejection is
91
95
  // never momentarily unhandled.