@fluidgrids/workflows 2026.1028.2 → 2026.1030.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.
Files changed (154) hide show
  1. package/README.md +181 -11
  2. package/dist/{activation-BBh1p82z.d.cts → activation-DqUfpHzk.d.cts} +1 -1
  3. package/dist/{activation-DnhG7SmF.d.ts → activation-xpx1oWuo.d.ts} +1 -1
  4. package/dist/{chunk-VRHHBPT6.js → chunk-2D3MYIIF.js} +1212 -128
  5. package/dist/chunk-3U2NU7YC.cjs +124 -0
  6. package/dist/{chunk-NEIUQASE.cjs → chunk-4QKB4ARC.cjs} +3 -3
  7. package/dist/{chunk-GBQ33SIZ.cjs → chunk-54FLQNUM.cjs} +12 -1
  8. package/dist/chunk-5HIVIV53.js +330 -0
  9. package/dist/chunk-5MFVJM76.js +1222 -0
  10. package/dist/{chunk-JD2VQIDZ.cjs → chunk-672KMMBK.cjs} +2 -2
  11. package/dist/{chunk-C5OLWZ42.js → chunk-6A2FGSUS.js} +22 -14
  12. package/dist/{chunk-XKJA7KX3.js → chunk-6OLN6BMB.js} +52 -3
  13. package/dist/{chunk-6FSHT5IL.js → chunk-7EZIANC7.js} +1 -1
  14. package/dist/{chunk-2J6TF7YI.cjs → chunk-A5SXCA62.cjs} +10 -3
  15. package/dist/{chunk-BO6OVILK.js → chunk-AC6ZEXVR.js} +10 -4
  16. package/dist/{chunk-ABFZWYHT.js → chunk-AE5Q6ZAJ.js} +315 -50
  17. package/dist/chunk-C4F5KJTI.js +293 -0
  18. package/dist/{chunk-IISBRUWJ.js → chunk-G5H32GIG.js} +1 -1
  19. package/dist/chunk-GEQDGUGB.cjs +2719 -0
  20. package/dist/{chunk-XHXIDVEC.js → chunk-HCM7ZC7Q.js} +1 -1
  21. package/dist/chunk-HMZUGSMU.cjs +1554 -0
  22. package/dist/{chunk-SAOBPYYJ.cjs → chunk-IQPXHQQQ.cjs} +8 -4
  23. package/dist/{chunk-P6TA5EZT.cjs → chunk-ISOIA7NV.cjs} +335 -58
  24. package/dist/chunk-JSC75O5C.cjs +167 -0
  25. package/dist/chunk-M272AHEX.js +25 -0
  26. package/dist/{chunk-BD7ZPG53.js → chunk-NWQ5WRLE.js} +10 -2
  27. package/dist/chunk-ODZ2IAR2.cjs +1247 -0
  28. package/dist/chunk-Q4MWQNJK.cjs +28 -0
  29. package/dist/{chunk-HWJZVOG7.js → chunk-SR7TR5KX.js} +6 -2
  30. package/dist/{chunk-FEW3JBYA.cjs → chunk-TCTE7DX5.cjs} +3 -3
  31. package/dist/{chunk-623FVQSN.cjs → chunk-TYF56RGB.cjs} +123 -0
  32. package/dist/chunk-VA4OFR72.cjs +324 -0
  33. package/dist/chunk-VU5S5A7V.js +150 -0
  34. package/dist/chunk-YEHR4NNE.js +1522 -0
  35. package/dist/{chunk-FMSW2HDF.cjs → chunk-ZCGKGDCL.cjs} +23 -15
  36. package/dist/codegen/index.d.cts +2 -2
  37. package/dist/codegen/index.d.ts +2 -2
  38. package/dist/collect-6NIEufHd.d.ts +103 -0
  39. package/dist/collect-CcTCKqx6.d.cts +103 -0
  40. package/dist/{file-BOoHZgoQ.d.cts → file-BxYpwjRQ.d.cts} +1007 -21
  41. package/dist/{file-yuPCFU5b.d.ts → file-CFdPQJI9.d.ts} +1007 -21
  42. package/dist/history/index.cjs +375 -86
  43. package/dist/history/index.d.cts +590 -12
  44. package/dist/history/index.d.ts +590 -12
  45. package/dist/history/index.js +50 -4
  46. package/dist/index.cjs +156 -50
  47. package/dist/index.d.cts +38 -58
  48. package/dist/index.d.ts +38 -58
  49. package/dist/index.js +6 -4
  50. package/dist/interceptors-CNcr2dlk.d.ts +159 -0
  51. package/dist/interceptors-CqkNZVfI.d.cts +159 -0
  52. package/dist/ir/index.cjs +362 -38
  53. package/dist/ir/index.d.cts +282 -7
  54. package/dist/ir/index.d.ts +282 -7
  55. package/dist/ir/index.js +329 -23
  56. package/dist/{lower-BzmUz0sq.d.cts → lower-BtRfIW2A.d.cts} +56 -8
  57. package/dist/{lower-_-iT4HYs.d.ts → lower-CpsMFOFX.d.ts} +56 -8
  58. package/dist/{manifest-C8N6HDYl.d.ts → manifest-D-I_twys.d.ts} +1 -1
  59. package/dist/{manifest-0OLw0i7k.d.cts → manifest-DahBQVM8.d.cts} +1 -1
  60. package/dist/node-evaluator-DGQDPSPI.cjs +24 -0
  61. package/dist/node-evaluator-LEEVD7H7.js +14 -0
  62. package/dist/payload-fields-BVRQn2yH.d.cts +122 -0
  63. package/dist/payload-fields-oOBWxDiG.d.ts +122 -0
  64. package/dist/quickjs/index.cjs +28 -27
  65. package/dist/quickjs/index.d.cts +6 -6
  66. package/dist/quickjs/index.d.ts +6 -6
  67. package/dist/quickjs/index.js +10 -9
  68. package/dist/roundtrip/index.cjs +259 -51
  69. package/dist/roundtrip/index.d.cts +100 -9
  70. package/dist/roundtrip/index.d.ts +100 -9
  71. package/dist/roundtrip/index.js +177 -11
  72. package/dist/runtime/index.cjs +21 -20
  73. package/dist/runtime/index.d.cts +184 -15
  74. package/dist/runtime/index.d.ts +184 -15
  75. package/dist/runtime/index.js +8 -7
  76. package/dist/testing/index.cjs +1122 -115
  77. package/dist/testing/index.d.cts +294 -13
  78. package/dist/testing/index.d.ts +294 -13
  79. package/dist/testing/index.js +1086 -84
  80. package/dist/{types-DV-HX_Gt.d.ts → types-Bv2i4cBX.d.cts} +244 -33
  81. package/dist/{types-CpBBjaGX.d.cts → types-DGx-b9eq.d.ts} +244 -33
  82. package/dist/{types-BGoPzgcl.d.ts → types-V7jEEUaL.d.cts} +20 -1
  83. package/dist/{types-BGoPzgcl.d.cts → types-V7jEEUaL.d.ts} +20 -1
  84. package/dist/{workflow-Bpr3vnA_.d.ts → workflow-B18OeF7g.d.ts} +2 -2
  85. package/dist/workflow-BGnv8SS6.d.ts +803 -0
  86. package/dist/{workflow-FXUkpIBc.d.cts → workflow-BJPFv3cp.d.cts} +2 -2
  87. package/dist/workflow-DS32Gtws.d.cts +803 -0
  88. package/fixtures/histories/activity-options-updated.history.json +159 -0
  89. package/fixtures/histories/activity-paused-unpaused.history.json +160 -0
  90. package/fixtures/histories/auto-upgrade-patched.history.json +276 -0
  91. package/fixtures/histories/benign-failure.history.json +146 -0
  92. package/fixtures/histories/cancel-activity-try-cancel.history.json +150 -0
  93. package/fixtures/histories/child-cancel-wait-requested.history.json +217 -0
  94. package/fixtures/histories/child-options-roundtrip.history.json +192 -0
  95. package/fixtures/histories/codec-opaque.history.json +229 -0
  96. package/fixtures/histories/continue-as-new-last-result.history.json +86 -0
  97. package/fixtures/histories/continue-as-new-options.history.json +99 -0
  98. package/fixtures/histories/continue-as-new-r1-long-task-queue.history.json +78 -0
  99. package/fixtures/histories/continue-as-new-r1-retry-options.history.json +83 -0
  100. package/fixtures/histories/dynamic-workflow.history.json +150 -0
  101. package/fixtures/histories/execution-timeout-chain.history.json +98 -0
  102. package/fixtures/histories/external-cancel-resolved.history.json +123 -0
  103. package/fixtures/histories/external-cancel-unknown-target.history.json +127 -0
  104. package/fixtures/histories/failure-error-types-fails-run.history.json +144 -0
  105. package/fixtures/histories/handler-drain-warn.history.json +222 -0
  106. package/fixtures/histories/heartbeat-details-on-retry.history.json +145 -0
  107. package/fixtures/histories/interceptor-headers.history.json +339 -0
  108. package/fixtures/histories/local-activity-abandoned-at-completion.history.json +73 -0
  109. package/fixtures/histories/local-activity-cancelled-in-scope.history.json +118 -0
  110. package/fixtures/histories/local-activity-completed.history.json +157 -0
  111. package/fixtures/histories/local-activity-failed.history.json +93 -0
  112. package/fixtures/histories/local-activity-retry-in-task.history.json +88 -0
  113. package/fixtures/histories/query-custom.history.json +213 -0
  114. package/fixtures/histories/query-default-handler.history.json +120 -0
  115. package/fixtures/histories/reset-forked-reapplied-signals.history.json +239 -0
  116. package/fixtures/histories/reset-reapplied-update-admitted.history.json +228 -0
  117. package/fixtures/histories/run-retry-last-failure.history.json +90 -0
  118. package/fixtures/histories/run-timeout.history.json +159 -0
  119. package/fixtures/histories/schedule-to-start-timeout.history.json +132 -0
  120. package/fixtures/histories/search-attributes-upserted.history.json +177 -0
  121. package/fixtures/histories/start-delay-signal-ends-delay.history.json +155 -0
  122. package/fixtures/histories/typeerror-task-retry-100.history.json +137 -0
  123. package/fixtures/histories/update-accepted-completed.history.json +45 -30
  124. package/fixtures/histories/update-default-handler.history.json +246 -0
  125. package/fixtures/histories/update-handler-awaits-activity.history.json +196 -0
  126. package/fixtures/histories/update-not-handled.history.json +138 -0
  127. package/fixtures/histories/update-rejected-no-event.history.json +190 -0
  128. package/fixtures/histories/update-with-start-admitted.history.json +150 -0
  129. package/fixtures/histories/user-activity-cancel.history.json +149 -0
  130. package/fixtures/histories/user-activity-retry.history.json +143 -0
  131. package/fixtures/histories/user-activity.history.json +149 -0
  132. package/package.json +1 -1
  133. package/schema/bridge-v2.schema.json +1062 -0
  134. package/schema/history-file.schema.json +538 -83
  135. package/schema/ir-v2.schema.json +290 -0
  136. package/schema/payload-fields.json +162 -0
  137. package/schema/run-event.schema.json +503 -82
  138. package/schema/workflow-command.schema.json +372 -69
  139. package/schema/workflow-task.schema.json +924 -171
  140. package/dist/assemble-BLqQ8lSu.d.cts +0 -39
  141. package/dist/assemble-DWgaXHd-.d.ts +0 -39
  142. package/dist/chunk-DHTLALBL.cjs +0 -80
  143. package/dist/chunk-DJYMDLV5.js +0 -227
  144. package/dist/chunk-I5LR3D2K.js +0 -869
  145. package/dist/chunk-M4BBAERY.cjs +0 -1635
  146. package/dist/chunk-N23SKIG4.js +0 -73
  147. package/dist/chunk-PZCVF76Q.cjs +0 -868
  148. package/dist/chunk-QQ6ULXHI.cjs +0 -885
  149. package/dist/chunk-UK3WM7YZ.cjs +0 -75
  150. package/dist/chunk-VOKMAAJP.js +0 -848
  151. package/dist/node-evaluator-4MSDKWOG.cjs +0 -22
  152. package/dist/node-evaluator-SX4LEYQC.js +0 -12
  153. package/dist/workflow-C2a05WUJ.d.ts +0 -444
  154. package/dist/workflow-J-yYixny.d.cts +0 -444
@@ -1,28 +1,32 @@
1
- import { failedTaskResult } from '../chunk-IISBRUWJ.js';
2
- import { isTier1Workflow, toIR } from '../chunk-HWJZVOG7.js';
3
- import { irHash, CODE_WORKFLOW_NODE_TYPE, normalizeDefinition } from '../chunk-BD7ZPG53.js';
1
+ import { failedTaskResult } from '../chunk-G5H32GIG.js';
2
+ import { isTier1Workflow, toIR } from '../chunk-SR7TR5KX.js';
3
+ import { irHash, CODE_WORKFLOW_NODE_TYPE, normalizeDefinition } from '../chunk-NWQ5WRLE.js';
4
4
  import '../chunk-5LUIDMF4.js';
5
5
  import '../chunk-JZ4TOOLN.js';
6
6
  import '../chunk-E2VK7J2C.js';
7
7
  import '../chunk-7AAZGVKD.js';
8
- import { createHistoryFile, validateCommands, parseHistoryFile } from '../chunk-VOKMAAJP.js';
8
+ import { createHistoryFile, validateCommands, parseHistoryFile } from '../chunk-YEHR4NNE.js';
9
+ import '../chunk-VU5S5A7V.js';
9
10
  import { isRecord } from '../chunk-PMCM2OH4.js';
10
- import { buildWorkflowRegistry, createWorkflowRuntime } from '../chunk-VRHHBPT6.js';
11
- import { failureToError, signalName, toMs, errorToFailure, ApplicationError, CancelledFailure, DeterminismViolation } from '../chunk-C5OLWZ42.js';
12
- import '../chunk-LNUQE6D6.js';
11
+ import { buildWorkflowRegistry, createWorkflowRuntime } from '../chunk-2D3MYIIF.js';
12
+ import { failureToError, signalName, ApplicationError, errorToFailure, CancelledFailure, DeterminismViolation } from '../chunk-6A2FGSUS.js';
13
+ import '../chunk-M272AHEX.js';
13
14
  import { TASK_PRELUDE } from '../chunk-4LYSEANA.js';
14
- import { isDurableWorkflow } from '../chunk-N23SKIG4.js';
15
- import { initialRunState, applyEvent, eventsForCommands, isActivityOpen, computeRetryDelay, isChildOpen } from '../chunk-ABFZWYHT.js';
16
- import { isInputEvent, HISTORY_PROTOCOL, TERMINAL_COMMANDS } from '../chunk-DJYMDLV5.js';
17
- import '../chunk-BO6OVILK.js';
15
+ import { isActivityImplementation, isActivityDefinition, ACTIVITY_IMPL_MARKER, ACTIVITY_MARKER, runRetryPolicyOf, toMs, findActivityImplementation, executeActivity, isDurableWorkflow } from '../chunk-C4F5KJTI.js';
16
+ import '../chunk-LNUQE6D6.js';
17
+ import { COMMAND_FEATURES, codeFeaturesEnabledBy, applyEvent, projectRunState, isActivityOpen, hasUnprocessedInput, initialRunState, eventsForCommands, continueAsNewRetryShape, isChildOpen } from '../chunk-AE5Q6ZAJ.js';
18
+ import '../chunk-AC6ZEXVR.js';
19
+ import { RUNTIME_RELEASE, HISTORY_PROTOCOL, isCommandEvent, TERMINAL_EVENTS, isInputEvent, TERMINAL_COMMANDS, computeRetryDelay, computeRunRetryDelay } from '../chunk-5HIVIV53.js';
18
20
  import { createContext, Script } from 'vm';
19
21
 
20
22
  // src/testing/host.ts
21
23
  function drainJobs() {
22
24
  return new Promise((resolve) => setImmediate(resolve));
23
25
  }
24
- function registryOf(workflows) {
25
- return buildWorkflowRegistry([["in-process", { ...workflows }]]);
26
+ function registryOf(workflows, interceptors) {
27
+ return buildWorkflowRegistry([
28
+ ["in-process", { ...workflows, ...interceptors !== void 0 ? { interceptors } : {} }]
29
+ ]);
26
30
  }
27
31
  async function hostLoop(abi, requestJson) {
28
32
  abi.start(requestJson);
@@ -38,6 +42,58 @@ async function runWorkflowTaskInProcess(registry, request) {
38
42
  }
39
43
 
40
44
  // src/testing/environment.ts
45
+ var QueryRejectedError = class extends Error {
46
+ constructor(runId, queryName, message) {
47
+ super(message);
48
+ this.runId = runId;
49
+ this.queryName = queryName;
50
+ this.name = "QueryRejectedError";
51
+ }
52
+ runId;
53
+ queryName;
54
+ };
55
+ var QueryFailedError = class extends Error {
56
+ constructor(runId, queryName, message) {
57
+ super(message);
58
+ this.runId = runId;
59
+ this.queryName = queryName;
60
+ this.name = "QueryFailedError";
61
+ }
62
+ runId;
63
+ queryName;
64
+ };
65
+ var UpdateRefusedError = class extends Error {
66
+ constructor(message) {
67
+ super(message);
68
+ this.name = "UpdateRefusedError";
69
+ }
70
+ };
71
+ var WorkflowAlreadyStartedError = class extends Error {
72
+ constructor(workflowKey, workflowId, existingRunId) {
73
+ super(
74
+ `Workflow ${workflowKey} already has a run with workflow id "${workflowId}" (${existingRunId})`
75
+ );
76
+ this.workflowKey = workflowKey;
77
+ this.workflowId = workflowId;
78
+ this.existingRunId = existingRunId;
79
+ this.name = "WorkflowAlreadyStartedError";
80
+ }
81
+ workflowKey;
82
+ workflowId;
83
+ existingRunId;
84
+ };
85
+ var UnfinishedHandlersError = class extends Error {
86
+ constructor(runId, handlers) {
87
+ super(
88
+ `Run ${runId} finished while handlers were still running: ${handlers.map((handler) => `${handler.kind} ${handler.name} x${handler.count}`).join(", ")} (await ctx.allHandlersFinished(), or set unfinishedPolicy: 'ABANDON')`
89
+ );
90
+ this.runId = runId;
91
+ this.handlers = handlers;
92
+ this.name = "UnfinishedHandlersError";
93
+ }
94
+ runId;
95
+ handlers;
96
+ };
41
97
  var WorkflowFailedError = class extends Error {
42
98
  constructor(runId, failure) {
43
99
  super(`Run ${runId} failed: ${failure.message}`);
@@ -69,7 +125,7 @@ var RunBlockedError = class extends Error {
69
125
  }
70
126
  runId;
71
127
  };
72
- var TestRunHandle = class {
128
+ var TestRunHandle = class _TestRunHandle {
73
129
  constructor(env, runId) {
74
130
  this.env = env;
75
131
  this.runId = runId;
@@ -99,6 +155,22 @@ var TestRunHandle = class {
99
155
  this.env.terminate(this.runId, reason);
100
156
  await this.env.runUntilIdle(false);
101
157
  }
158
+ /** Send an update and run until idle (Phase C). */
159
+ update(update, args, options = {}) {
160
+ return this.env.update(this.runId, update, args, options);
161
+ }
162
+ /** Answer a query from the run's history (Phase C custom queries and the built-ins). */
163
+ query(query, args) {
164
+ return this.env.query(this.runId, query, args);
165
+ }
166
+ /** Reset the run (11 §2.7): a new run forked at a completed task; this one is terminated. */
167
+ async reset(options = {}) {
168
+ return new _TestRunHandle(this.env, await this.env.reset(this.runId, options));
169
+ }
170
+ /** Sink calls the run's tasks made (Phase C). */
171
+ sinkCalls() {
172
+ return this.env.sinkCallsOf(this.runId);
173
+ }
102
174
  /** Run until this run (or the last run of its continue-as-new chain) ends; its output. */
103
175
  async result() {
104
176
  return this.env.resultOf(this.runId);
@@ -113,6 +185,23 @@ function toDate(value) {
113
185
  if (value instanceof Date) return value.getTime();
114
186
  return typeof value === "number" ? value : Date.parse(value);
115
187
  }
188
+ function optionalDuration(value, what) {
189
+ return value === void 0 ? void 0 : toMs(value, what);
190
+ }
191
+ var FAILED_STATUSES = /* @__PURE__ */ new Set([
192
+ "FAILED",
193
+ "CANCELLED",
194
+ "TERMINATED",
195
+ "TIMED_OUT"
196
+ ]);
197
+ function admittedPayload(update) {
198
+ return {
199
+ updateId: update.updateId,
200
+ name: update.name,
201
+ args: update.args,
202
+ ...update.headers !== void 0 ? { headers: { ...update.headers } } : {}
203
+ };
204
+ }
116
205
  function toJsonValue(value) {
117
206
  if (value === void 0) return null;
118
207
  const text = JSON.stringify(value);
@@ -122,6 +211,12 @@ var TestWorkflowEnvironment = class _TestWorkflowEnvironment {
122
211
  clock;
123
212
  timeSkipping;
124
213
  enabled;
214
+ /** The bundle features the gate's flags enable, sent with every task (`request.features`). */
215
+ codeFeatures;
216
+ failOnUnfinishedHandlers;
217
+ historyRelease;
218
+ dues = [];
219
+ held = /* @__PURE__ */ new Set();
125
220
  workspaceId;
126
221
  runs = /* @__PURE__ */ new Map();
127
222
  runOrder = [];
@@ -133,17 +228,65 @@ var TestWorkflowEnvironment = class _TestWorkflowEnvironment {
133
228
  timers = [];
134
229
  mockedChildren = [];
135
230
  runCounter = 0;
231
+ updateCounter = 0;
136
232
  executor;
137
233
  workflowKeys;
234
+ interceptors;
235
+ activityImpls;
236
+ activityInterceptors;
237
+ connections;
238
+ sinks;
239
+ /** Attempts that handed their completion to an outside system (`completeAsync`), by token. */
240
+ asyncAttempts = /* @__PURE__ */ new Map();
138
241
  constructor(options) {
139
242
  this.clock = toDate(options.startTime);
140
243
  this.timeSkipping = options.timeSkipping !== false;
141
- this.enabled = options.enabled ?? "parity";
244
+ this.enabled = options.enabled ?? new Set(COMMAND_FEATURES);
245
+ this.codeFeatures = codeFeaturesEnabledBy(this.enabled);
246
+ this.failOnUnfinishedHandlers = options.failOnUnfinishedHandlers !== false;
247
+ this.historyRelease = options.historyRelease ?? RUNTIME_RELEASE;
142
248
  this.workspaceId = options.workspaceId ?? "test-workspace";
143
249
  this.workflows = { ...options.workflows ?? {} };
144
- this.registry = registryOf(this.workflows);
250
+ this.interceptors = options.interceptors;
251
+ this.registry = registryOf(this.workflows, this.interceptors);
145
252
  this.executor = options.taskExecutor;
146
253
  this.workflowKeys = { ...options.workflowKeys ?? {} };
254
+ this.activityImpls = [...options.activities ?? []];
255
+ for (const impl of this.activityImpls)
256
+ if (!isActivityImplementation(impl))
257
+ throw new TypeError("activities must be implementActivity() or defineActivity() values");
258
+ this.activityInterceptors = [...options.activityInterceptors ?? []];
259
+ this.connections = { ...options.connections ?? {} };
260
+ this.sinks = { ...options.sinks ?? {} };
261
+ }
262
+ /** Register a user-activity implementation (Phase C). A later one replaces an earlier of the same name. */
263
+ registerActivity(implementation) {
264
+ if (!isActivityImplementation(implementation))
265
+ throw new TypeError(
266
+ "registerActivity needs an implementActivity() or defineActivity() value"
267
+ );
268
+ const index = this.activityImpls.findIndex(
269
+ (impl) => impl.name === implementation.name && impl.dynamic === implementation.dynamic
270
+ );
271
+ if (index >= 0) this.activityImpls.splice(index, 1);
272
+ this.activityImpls.push(implementation);
273
+ }
274
+ /** Mock a user activity by name or declaration (Phase C). */
275
+ mockActivity(activity, fn) {
276
+ const name = typeof activity === "string" ? activity : activity.name;
277
+ if (typeof activity !== "string" && !isActivityDefinition(activity))
278
+ throw new TypeError("mockActivity needs an activity name or a declareActivity() value");
279
+ this.registerActivity(
280
+ Object.freeze({
281
+ __fluidgrids: ACTIVITY_MARKER,
282
+ name,
283
+ options: typeof activity === "string" ? {} : activity.options,
284
+ implementation: ACTIVITY_IMPL_MARKER,
285
+ // The mock receives the activity's JSON args, typed by the declaration.
286
+ fn: (ctx, args) => fn(args, ctx),
287
+ dynamic: false
288
+ })
289
+ );
147
290
  }
148
291
  /** Create an environment. */
149
292
  static async create(options = {}) {
@@ -162,6 +305,26 @@ var TestWorkflowEnvironment = class _TestWorkflowEnvironment {
162
305
  impl
163
306
  );
164
307
  }
308
+ /**
309
+ * Hold the activity queue of a node type (every node type when omitted):
310
+ * attempts wait instead of starting, and a `scheduleToStartTimeout` expires
311
+ * (`ACTIVITY_TIMED_OUT { timeoutType: SCHEDULE_TO_START }`, not retried).
312
+ */
313
+ holdActivities(nodeType = "*") {
314
+ this.held.add(nodeType);
315
+ }
316
+ /** Release held activity queues (all when no node type is given). */
317
+ releaseActivities(nodeType) {
318
+ if (nodeType === void 0) this.held.clear();
319
+ else this.held.delete(nodeType);
320
+ }
321
+ /** The hold key of an attempt: its node type, or the task queue of a user activity. */
322
+ holdKeyOf(entry) {
323
+ return entry.kind === "user" ? entry.taskQueue ?? "" : entry.nodeType;
324
+ }
325
+ isHeld(nodeType) {
326
+ return this.held.has("*") || this.held.has(nodeType);
327
+ }
165
328
  /** Mock a child workflow started by key (graph or durable). */
166
329
  mockChild(workflowKey, impl) {
167
330
  this.childMocks.set(workflowKey, impl);
@@ -172,17 +335,327 @@ var TestWorkflowEnvironment = class _TestWorkflowEnvironment {
172
335
  // A declared workflow is stored by its declaration; its input type is the caller's.
173
336
  this.ensureRegistered(definition)
174
337
  );
338
+ const declared = this.workflows[workflowFn]?.options ?? {};
339
+ const workflowKey = options.workflowKey ?? (typeof definition === "string" ? this.workflows[workflowFn]?.name ?? workflowFn : definition.name);
340
+ if (options.update !== void 0 && options.startDelay !== void 0)
341
+ throw new UpdateRefusedError(
342
+ "Update-with-start cannot be combined with a start delay (11 \xA72.6)"
343
+ );
344
+ if (options.workflowId !== void 0) {
345
+ const existing = this.useExisting(workflowKey, options);
346
+ if (existing !== void 0) {
347
+ for (const signal of options.signals ?? [])
348
+ this.deliverSignal(existing, signal.name, signal.payload);
349
+ if (options.update !== void 0)
350
+ this.addUpdate(this.run(existing), options.update, "PENDING");
351
+ await this.runUntilIdle(false);
352
+ return new TestRunHandle(this, existing);
353
+ }
354
+ }
355
+ const executionTimeoutMs = optionalDuration(
356
+ options.executionTimeout ?? declared.executionTimeout,
357
+ "executionTimeout"
358
+ );
359
+ const retry = options.retry ?? declared.retry;
360
+ const config = {
361
+ workflowKey,
362
+ runAttempt: 1,
363
+ ...options.workflowId !== void 0 ? { businessId: options.workflowId } : {},
364
+ ...executionTimeoutMs !== void 0 ? { executionTimeoutMs, executionDeadline: this.clock + executionTimeoutMs } : {},
365
+ ...this.durationConfig(options.runTimeout ?? declared.runTimeout, "runTimeoutMs"),
366
+ ...this.durationConfig(
367
+ options.workflowTaskTimeout ?? declared.workflowTaskTimeout,
368
+ "workflowTaskTimeoutMs"
369
+ ),
370
+ ...this.durationConfig(options.startDelay, "startDelayMs"),
371
+ ...retry !== void 0 ? { retry: runRetryPolicyOf(retry) } : {},
372
+ ...options.summary !== void 0 ? { summary: options.summary } : {},
373
+ ...options.details !== void 0 ? { details: options.details } : {},
374
+ ...options.memo !== void 0 ? { memo: toJsonValue(options.memo) } : {},
375
+ ...(options.versioningBehavior ?? declared.versioningBehavior) !== void 0 ? { versioningBehavior: options.versioningBehavior ?? declared.versioningBehavior } : {},
376
+ ...options.headers !== void 0 ? { headers: { ...options.headers } } : {},
377
+ ...options.searchAttributes !== void 0 ? { searchAttributes: toJsonValue(options.searchAttributes) } : {},
378
+ ...options.fairnessKey !== void 0 ? { fairnessKey: options.fairnessKey } : {},
379
+ ...options.fairnessWeight !== void 0 ? { fairnessWeight: options.fairnessWeight } : {}
380
+ };
175
381
  const runId = this.createRun(
176
382
  workflowFn,
177
383
  toJsonValue(options.input),
178
384
  { ...options.info, ...options.runId !== void 0 ? { runId: options.runId } : {} },
179
- void 0
385
+ void 0,
386
+ {},
387
+ config,
388
+ options.update !== void 0 ? [this.newUpdate(options.update, "ADMITTED")] : []
180
389
  );
181
390
  for (const signal of options.signals ?? [])
182
391
  this.deliverSignal(runId, signal.name, signal.payload);
183
392
  await this.runUntilIdle(false);
184
393
  return new TestRunHandle(this, runId);
185
394
  }
395
+ /**
396
+ * Update-with-start (11 §2.6): start the run (or, with
397
+ * `idConflictPolicy: USE_EXISTING`, reach the open one) and deliver the
398
+ * update before any other task runs.
399
+ */
400
+ async startWithUpdate(definition, options) {
401
+ const updateId = options.update.updateId ?? this.nextUpdateId();
402
+ const handle = await this.start(definition, {
403
+ ...options,
404
+ update: { ...options.update, updateId }
405
+ });
406
+ return { handle, update: this.updateResultOf(handle.runId, updateId) };
407
+ }
408
+ nextUpdateId() {
409
+ this.updateCounter += 1;
410
+ return `u${this.updateCounter}`;
411
+ }
412
+ /** A new update row (not attached to a run yet). */
413
+ newUpdate(update, status) {
414
+ return {
415
+ updateId: update.updateId ?? this.nextUpdateId(),
416
+ name: update.name,
417
+ args: toJsonValue(update.args),
418
+ ...update.headers !== void 0 ? { headers: { ...update.headers } } : {},
419
+ status
420
+ };
421
+ }
422
+ /** Record an update on a run: pending (delivered by the next task) or admitted into history. */
423
+ addUpdate(run, update, status) {
424
+ const existing = update.updateId !== void 0 ? run.updates.get(update.updateId) : void 0;
425
+ if (existing) return existing;
426
+ const entry = this.newUpdate(update, status);
427
+ run.updates.set(entry.updateId, entry);
428
+ if (status === "ADMITTED")
429
+ this.append(run, { type: "UPDATE_ADMITTED", payload: admittedPayload(entry) });
430
+ else {
431
+ if (run.delayedUntil !== void 0) this.endDelay(run);
432
+ else this.scheduleTask(run);
433
+ }
434
+ return entry;
435
+ }
436
+ /** Schedule a workflow task for a run that has none (an update needs no input event). */
437
+ scheduleTask(run) {
438
+ if (run.state.status !== "RUNNING" || run.state.task.state !== "NONE") return;
439
+ if (run.delayedUntil !== void 0) return;
440
+ run.taskCounter += 1;
441
+ const taskId = `${run.runId}-task-${run.taskCounter}`;
442
+ const scheduled = {
443
+ seq: run.history.length + 1,
444
+ type: "WORKFLOW_TASK_SCHEDULED",
445
+ payload: { taskId },
446
+ createdAt: new Date(this.clock).toISOString()
447
+ };
448
+ applyEvent(run.state, scheduled);
449
+ run.history.push(scheduled);
450
+ }
451
+ /**
452
+ * Send an update to a run and run until idle (Phase C, 05 §6). The result
453
+ * is `REJECTED` (the validator refused it or no handler took it; nothing is
454
+ * written to history), `COMPLETED`, or `ACCEPTED` while its handler still
455
+ * waits (for a timer without time skipping, or with `waitFor: ACCEPTED`).
456
+ */
457
+ async update(runId, update, args, options = {}) {
458
+ const run = this.run(runId);
459
+ const name = typeof update === "string" ? update : update.name;
460
+ if (options.updateId !== void 0 && run.updates.has(options.updateId))
461
+ return this.updateResultOf(runId, options.updateId);
462
+ if (run.state.status !== "RUNNING")
463
+ throw new UpdateRefusedError(`Run ${runId} is ${run.state.status}; it takes no updates`);
464
+ const entry = this.addUpdate(
465
+ run,
466
+ {
467
+ name,
468
+ args,
469
+ ...options.updateId !== void 0 ? { updateId: options.updateId } : {},
470
+ ...options.headers !== void 0 ? { headers: options.headers } : {}
471
+ },
472
+ "PENDING"
473
+ );
474
+ await this.runUntilIdle(false);
475
+ return this.updateResultOf(runId, entry.updateId);
476
+ }
477
+ /** The current result of an update. */
478
+ updateResultOf(runId, updateId) {
479
+ const entry = this.run(runId).updates.get(updateId);
480
+ if (!entry) throw new Error(`Run ${runId} has no update ${updateId}`);
481
+ return {
482
+ updateId,
483
+ status: entry.status === "ADMITTED" ? "PENDING" : entry.status,
484
+ ...entry.result !== void 0 ? { result: entry.result } : {},
485
+ ...entry.failure !== void 0 ? { failure: entry.failure } : {}
486
+ };
487
+ }
488
+ /** Answer a query from the run's history (a replay-only `query` task). */
489
+ async query(runId, query, args) {
490
+ const run = this.run(runId);
491
+ const name = typeof query === "string" ? query : query.name;
492
+ const request = {
493
+ protocol: HISTORY_PROTOCOL,
494
+ kind: "query",
495
+ workflowFn: run.workflowFn,
496
+ info: run.info,
497
+ history: run.history,
498
+ nowMs: this.clock,
499
+ queries: [{ queryId: "q1", name, args: toJsonValue(args) }]
500
+ };
501
+ const result = await this.execute(request);
502
+ const answer = result.queryResults?.[0];
503
+ if (answer?.rejected === true)
504
+ throw new QueryRejectedError(runId, name, answer.error ?? `No handler for query ${name}`);
505
+ if (answer === void 0 || answer.error !== void 0)
506
+ throw new QueryFailedError(
507
+ runId,
508
+ name,
509
+ answer?.error ?? result.failure?.message ?? "The query was not answered"
510
+ );
511
+ return answer.value ?? null;
512
+ }
513
+ /** Sink calls the run's tasks made. */
514
+ sinkCallsOf(runId) {
515
+ return this.run(runId).sinkCalls.map((call) => ({ ...call, args: [...call.args] }));
516
+ }
517
+ /**
518
+ * Reset a run (11 §2.7): a new run whose history is the original's up to
519
+ * and including the chosen completed workflow task (with its commands),
520
+ * then `WORKFLOW_RESET`, then the signals (and, as `UPDATE_ADMITTED`, the
521
+ * accepted updates) received after that point. The original is terminated
522
+ * with reason `RESET`; children it started before the point follow the new
523
+ * run, the others get their parent-close policy. Returns the new run id.
524
+ */
525
+ async reset(runId, options = {}) {
526
+ const original = this.run(runId);
527
+ const history = original.history;
528
+ const points = [];
529
+ for (const event of history) {
530
+ if (event.type !== "WORKFLOW_TASK_COMPLETED") continue;
531
+ const taskId = typeof event.payload["taskId"] === "string" ? event.payload["taskId"] : "";
532
+ let end = event.seq;
533
+ let closes = false;
534
+ while (end < history.length) {
535
+ const next = history[end];
536
+ if (!next || !isCommandEvent(next) || next.taskId !== taskId) break;
537
+ if (TERMINAL_EVENTS.has(next.type)) closes = true;
538
+ end += 1;
539
+ }
540
+ if (!closes) points.push({ seq: event.seq, cut: end });
541
+ }
542
+ const point = options.toSeq !== void 0 ? [...points].reverse().find((entry) => entry.seq <= (options.toSeq ?? 0)) : options.to === "FIRST_TASK" ? points[0] : points[points.length - 1];
543
+ if (!point) throw new Error(`Run ${runId} has no completed workflow task to reset to`);
544
+ const cut = point.cut;
545
+ const copied = history.slice(0, cut).map((event) => ({ ...event, payload: { ...event.payload } }));
546
+ const later = history.slice(cut);
547
+ const reapplySignals = options.reapplySignals !== false;
548
+ const reapplyUpdates = options.reapplyUpdates !== false;
549
+ const keptChildren = new Set(
550
+ copied.filter((event) => event.type === "CHILD_RUN_INITIATED").map((event) => String(event.payload["childId"]))
551
+ );
552
+ this.runCounter += 1;
553
+ const newRunId = `run-${this.runCounter}`;
554
+ if (original.state.status === "RUNNING") {
555
+ original.resetTo = newRunId;
556
+ this.append(original, { type: "WORKFLOW_TERMINATED", payload: { reason: "RESET" } });
557
+ this.onClosed(original, keptChildren);
558
+ } else original.resetTo = newRunId;
559
+ const fork = {
560
+ runId: newRunId,
561
+ workflowFn: original.workflowFn,
562
+ history: copied,
563
+ state: projectRunState(copied),
564
+ taskCounter: copied.filter((event) => event.type === "WORKFLOW_TASK_SCHEDULED").length,
565
+ info: { ...original.info, runId: newRunId },
566
+ children: new Map([...original.children].filter(([childId]) => keptChildren.has(childId))),
567
+ config: { ...original.config },
568
+ ...original.parent ? { parent: original.parent } : {},
569
+ updates: /* @__PURE__ */ new Map(),
570
+ sinkCalls: []
571
+ };
572
+ this.runs.set(newRunId, fork);
573
+ this.runOrder.push(newRunId);
574
+ for (const [childId, childRunId] of fork.children) {
575
+ const child = childRunId !== void 0 ? this.runs.get(childRunId) : void 0;
576
+ if (child) child.parent = { runId: newRunId, childId };
577
+ }
578
+ this.append(fork, {
579
+ type: "WORKFLOW_RESET",
580
+ payload: {
581
+ fromRunId: runId,
582
+ toSeq: cut,
583
+ reapplySignals,
584
+ reapplyUpdates,
585
+ ...options.reason !== void 0 ? { reason: options.reason } : {}
586
+ }
587
+ });
588
+ for (const [activityId, activity] of Object.entries(fork.state.activities)) {
589
+ if (!isActivityOpen(activity)) continue;
590
+ const scheduled = copied[activity.scheduledSeq - 1];
591
+ if (scheduled?.type !== "ACTIVITY_SCHEDULED") continue;
592
+ this.afterCommand(fork, {
593
+ // The run-event schema checked the payload; it is the command that scheduled it.
594
+ ...scheduled.payload,
595
+ type: "SCHEDULE_ACTIVITY",
596
+ activityId
597
+ });
598
+ }
599
+ for (const [timerId, timer] of Object.entries(fork.state.timers))
600
+ if (timer.status === "PENDING")
601
+ this.timers.push({ runId: newRunId, timerId, fireAt: Date.parse(timer.fireAt) });
602
+ for (const event of later) {
603
+ if (event.type === "SIGNAL_RECEIVED" && reapplySignals)
604
+ this.append(fork, { type: "SIGNAL_RECEIVED", payload: { ...event.payload } });
605
+ if (reapplyUpdates && (event.type === "UPDATE_ACCEPTED" || event.type === "UPDATE_ADMITTED") && !fork.updates.has(String(event.payload["updateId"])) && fork.state.updates[String(event.payload["updateId"])] === void 0) {
606
+ const headers = event.payload["headers"];
607
+ this.addUpdate(
608
+ fork,
609
+ {
610
+ updateId: String(event.payload["updateId"]),
611
+ name: String(event.payload["name"]),
612
+ args: event.payload["args"] ?? null,
613
+ ...typeof headers === "object" && headers !== null && !Array.isArray(headers) ? (
614
+ // The run-event schema checked the headers map.
615
+ { headers }
616
+ ) : {}
617
+ },
618
+ "ADMITTED"
619
+ );
620
+ }
621
+ }
622
+ if (hasUnprocessedInput(fork.history)) this.scheduleTask(fork);
623
+ await this.runUntilIdle(false);
624
+ return newRunId;
625
+ }
626
+ durationConfig(value, key) {
627
+ const ms = optionalDuration(value, key);
628
+ if (ms === void 0) return {};
629
+ const out = {};
630
+ out[key] = ms;
631
+ return out;
632
+ }
633
+ /**
634
+ * The workflow-id policies of a start (11 §2.1): the open run to use, or
635
+ * undefined to start a new run (after terminating the open one for
636
+ * `TERMINATE_EXISTING`). Throws {@link WorkflowAlreadyStartedError}.
637
+ */
638
+ useExisting(workflowKey, options) {
639
+ const workflowId = options.workflowId ?? "";
640
+ const runs = this.runOrder.map((id) => this.run(id)).filter(
641
+ (run) => run.config.workflowKey === workflowKey && run.config.businessId === workflowId
642
+ );
643
+ const open = runs.find((run) => run.state.status === "RUNNING");
644
+ if (open) {
645
+ const conflict = options.idConflictPolicy ?? "FAIL";
646
+ if (conflict === "USE_EXISTING") return open.runId;
647
+ if (conflict === "FAIL")
648
+ throw new WorkflowAlreadyStartedError(workflowKey, workflowId, open.runId);
649
+ this.terminate(open.runId, "ID_CONFLICT");
650
+ return void 0;
651
+ }
652
+ const last = runs[runs.length - 1];
653
+ if (!last) return void 0;
654
+ const reuse = options.idReusePolicy ?? "ALLOW_DUPLICATE";
655
+ if (reuse === "REJECT_DUPLICATE" || reuse === "ALLOW_DUPLICATE_FAILED_ONLY" && !FAILED_STATUSES.has(last.state.status))
656
+ throw new WorkflowAlreadyStartedError(workflowKey, workflowId, last.runId);
657
+ return void 0;
658
+ }
186
659
  /** Advance the clock by `duration`, firing timers and running work as it comes due. */
187
660
  async sleep(duration) {
188
661
  const until = this.clock + toMs(duration, "sleep duration");
@@ -225,7 +698,7 @@ var TestWorkflowEnvironment = class _TestWorkflowEnvironment {
225
698
  if (value === definition) return name2;
226
699
  const name = Object.hasOwn(this.workflows, definition.name) ? `${definition.name}#${this.runCounter}` : definition.name;
227
700
  this.workflows[name] = definition;
228
- this.registry = registryOf(this.workflows);
701
+ this.registry = registryOf(this.workflows, this.interceptors);
229
702
  return name;
230
703
  }
231
704
  run(runId) {
@@ -233,7 +706,7 @@ var TestWorkflowEnvironment = class _TestWorkflowEnvironment {
233
706
  if (!run) throw new Error(`Unknown run ${runId}`);
234
707
  return run;
235
708
  }
236
- createRun(workflowFn, input, info, parent, extraStarted = {}) {
709
+ createRun(workflowFn, input, info, parent, extraStarted = {}, config = { workflowKey: workflowFn, runAttempt: 1 }, admitted = []) {
237
710
  this.runCounter += 1;
238
711
  const runId = info.runId ?? `run-${this.runCounter}`;
239
712
  const snapshot = {
@@ -254,19 +727,80 @@ var TestWorkflowEnvironment = class _TestWorkflowEnvironment {
254
727
  taskCounter: 0,
255
728
  info: snapshot,
256
729
  children: /* @__PURE__ */ new Map(),
257
- ...parent ? { parent } : {}
730
+ config,
731
+ ...parent ? { parent } : {},
732
+ updates: /* @__PURE__ */ new Map(),
733
+ sinkCalls: []
258
734
  };
735
+ const firstTaskDelay = config.startDelayMs ?? config.firstTaskDelayMs;
736
+ if (firstTaskDelay !== void 0 && firstTaskDelay > 0) {
737
+ run.delayedUntil = this.clock + firstTaskDelay;
738
+ this.dues.push({ runId, kind: "start", at: run.delayedUntil });
739
+ }
740
+ if (config.runTimeoutMs !== void 0)
741
+ this.dues.push({ runId, kind: "runTimeout", at: this.clock + config.runTimeoutMs });
742
+ if (config.executionDeadline !== void 0)
743
+ this.dues.push({ runId, kind: "executionTimeout", at: config.executionDeadline });
259
744
  this.runs.set(runId, run);
260
745
  this.runOrder.push(runId);
261
- this.append(run, {
262
- type: "WORKFLOW_STARTED",
263
- // The service writes the seed of ctx.random()/ctx.uuid() into the start
264
- // event so the history replays on its own; it uses the run id.
265
- payload: { workflowFn, input, taskQueue: "managed", randomnessSeed: runId, ...extraStarted }
266
- });
746
+ this.append(
747
+ run,
748
+ {
749
+ type: "WORKFLOW_STARTED",
750
+ // The service writes the seed of ctx.random()/ctx.uuid() into the start
751
+ // event so the history replays on its own; it uses the run id.
752
+ payload: {
753
+ workflowFn,
754
+ input,
755
+ taskQueue: config.taskQueue ?? "managed",
756
+ randomnessSeed: runId,
757
+ ...extraStarted,
758
+ ...this.startedFields(config)
759
+ }
760
+ },
761
+ admitted.length === 0
762
+ );
763
+ if (admitted.length > 0) {
764
+ for (const update of admitted) {
765
+ run.updates.set(update.updateId, update);
766
+ this.append(run, { type: "UPDATE_ADMITTED", payload: admittedPayload(update) }, false);
767
+ }
768
+ this.scheduleTask(run);
769
+ }
267
770
  return runId;
268
771
  }
269
- append(run, draft) {
772
+ /** The R2 fields of `WORKFLOW_STARTED`, each written only when it is set. */
773
+ startedFields(config) {
774
+ const out = {};
775
+ if (config.businessId !== void 0) {
776
+ out["businessId"] = config.businessId;
777
+ out["workflowId"] = config.businessId;
778
+ }
779
+ if (config.startDelayMs !== void 0) out["startDelayMs"] = config.startDelayMs;
780
+ if (config.retry !== void 0 || config.runAttempt > 1) out["runAttempt"] = config.runAttempt;
781
+ if (config.summary !== void 0) out["summary"] = config.summary;
782
+ if (config.details !== void 0) out["details"] = config.details;
783
+ if (config.memo !== void 0) out["context"] = config.memo;
784
+ const timeouts = {};
785
+ if (config.executionTimeoutMs !== void 0)
786
+ timeouts["executionTimeout"] = config.executionTimeoutMs;
787
+ if (config.runTimeoutMs !== void 0) timeouts["runTimeout"] = config.runTimeoutMs;
788
+ if (config.workflowTaskTimeoutMs !== void 0)
789
+ timeouts["workflowTaskTimeout"] = config.workflowTaskTimeoutMs;
790
+ if (Object.keys(timeouts).length > 0) out["timeouts"] = timeouts;
791
+ if (config.retry !== void 0) out["retry"] = toJsonValue(config.retry);
792
+ if (config.versioningBehavior !== void 0)
793
+ out["versioningBehavior"] = config.versioningBehavior;
794
+ if (config.lastCompletionResult !== void 0 && this.historyRelease >= 2)
795
+ out["lastCompletionResult"] = config.lastCompletionResult;
796
+ if (config.lastFailure !== void 0) out["lastFailure"] = toJsonValue(config.lastFailure);
797
+ if (config.headers !== void 0) out["headers"] = { ...config.headers };
798
+ if (config.searchAttributes !== void 0) out["searchAttributes"] = config.searchAttributes;
799
+ if (config.fairnessKey !== void 0) out["fairnessKey"] = config.fairnessKey;
800
+ if (config.fairnessWeight !== void 0) out["fairnessWeight"] = config.fairnessWeight;
801
+ return out;
802
+ }
803
+ append(run, draft, schedule = true) {
270
804
  const event = {
271
805
  seq: run.history.length + 1,
272
806
  type: draft.type,
@@ -276,7 +810,8 @@ var TestWorkflowEnvironment = class _TestWorkflowEnvironment {
276
810
  };
277
811
  applyEvent(run.state, event);
278
812
  run.history.push(event);
279
- if (isInputEvent(event) && run.state.status === "RUNNING" && run.state.task.state === "NONE") {
813
+ if (run.delayedUntil !== void 0 && event.type === "SIGNAL_RECEIVED") this.endDelay(run);
814
+ if (schedule && run.delayedUntil === void 0 && isInputEvent(event) && run.state.status === "RUNNING" && run.state.task.state === "NONE") {
280
815
  run.taskCounter += 1;
281
816
  const taskId = `${run.runId}-task-${run.taskCounter}`;
282
817
  const scheduled = {
@@ -290,7 +825,13 @@ var TestWorkflowEnvironment = class _TestWorkflowEnvironment {
290
825
  }
291
826
  return event;
292
827
  }
293
- deliverSignal(runId, name, payload) {
828
+ /** End a start delay: the first workflow task is scheduled now. */
829
+ endDelay(run) {
830
+ run.delayedUntil = void 0;
831
+ this.dues = this.dues.filter((due) => !(due.runId === run.runId && due.kind === "start"));
832
+ this.scheduleTask(run);
833
+ }
834
+ deliverSignal(runId, name, payload, headers) {
294
835
  const run = this.runs.get(runId);
295
836
  if (!run || run.state.status !== "RUNNING") return false;
296
837
  this.append(run, {
@@ -298,7 +839,8 @@ var TestWorkflowEnvironment = class _TestWorkflowEnvironment {
298
839
  payload: {
299
840
  name,
300
841
  payload: toJsonValue(payload),
301
- receivedAt: new Date(this.clock).toISOString()
842
+ receivedAt: new Date(this.clock).toISOString(),
843
+ ...headers !== void 0 ? { headers: { ...headers } } : {}
302
844
  }
303
845
  });
304
846
  return true;
@@ -311,6 +853,11 @@ var TestWorkflowEnvironment = class _TestWorkflowEnvironment {
311
853
  type: "WORKFLOW_CANCEL_REQUESTED",
312
854
  payload: reason !== void 0 ? { reason } : {}
313
855
  });
856
+ if (run.delayedUntil !== void 0) {
857
+ run.delayedUntil = void 0;
858
+ this.append(run, { type: "WORKFLOW_CANCELLED", payload: {} });
859
+ this.onClosed(run);
860
+ }
314
861
  return true;
315
862
  }
316
863
  terminate(runId, reason) {
@@ -330,7 +877,9 @@ var TestWorkflowEnvironment = class _TestWorkflowEnvironment {
330
877
  await this.runTask(task);
331
878
  continue;
332
879
  }
333
- const attempt = this.attempts.find((entry) => entry.dueAt <= this.clock);
880
+ const attempt = this.attempts.find(
881
+ (entry) => entry.dueAt <= this.clock && !this.isHeld(this.holdKeyOf(entry))
882
+ );
334
883
  if (attempt) {
335
884
  this.attempts = this.attempts.filter((entry) => entry !== attempt);
336
885
  await this.runAttempt(attempt);
@@ -349,16 +898,78 @@ var TestWorkflowEnvironment = class _TestWorkflowEnvironment {
349
898
  }
350
899
  throw new Error("The test environment did not settle after 100000 steps");
351
900
  }
901
+ /** When a held attempt's schedule-to-start timeout expires, if it has one. */
902
+ heldDeadline(entry) {
903
+ return this.isHeld(this.holdKeyOf(entry)) && entry.scheduleToStart !== void 0 ? entry.dueAt + entry.scheduleToStart : void 0;
904
+ }
352
905
  nextDue() {
353
- const due = [
354
- ...this.timers.map((timer) => timer.fireAt),
355
- ...this.attempts.map((entry) => entry.dueAt)
356
- ];
906
+ const due = [...this.timers.map((timer) => timer.fireAt)];
907
+ for (const entry of this.attempts) {
908
+ if (!this.isHeld(this.holdKeyOf(entry))) due.push(entry.dueAt);
909
+ const deadline = this.heldDeadline(entry);
910
+ if (deadline !== void 0) due.push(deadline);
911
+ }
912
+ for (const entry of this.dues) due.push(entry.at);
357
913
  return due.length === 0 ? void 0 : Math.min(...due);
358
914
  }
915
+ /** Expire held attempts whose schedule-to-start timeout passed (not retried, 11 §2.3). */
916
+ expireHeld() {
917
+ const expired = this.attempts.filter((entry) => {
918
+ const deadline = this.heldDeadline(entry);
919
+ return deadline !== void 0 && deadline <= this.clock;
920
+ });
921
+ if (expired.length === 0) return false;
922
+ this.attempts = this.attempts.filter((entry) => !expired.includes(entry));
923
+ for (const entry of expired) {
924
+ const run = this.runs.get(entry.runId);
925
+ const activity = run?.state.activities[entry.activityId];
926
+ if (!run || run.state.status !== "RUNNING" || !activity || !isActivityOpen(activity))
927
+ continue;
928
+ this.append(run, {
929
+ type: "ACTIVITY_TIMED_OUT",
930
+ payload: {
931
+ activityId: entry.activityId,
932
+ attempt: entry.attempt,
933
+ timeoutType: "SCHEDULE_TO_START"
934
+ }
935
+ });
936
+ }
937
+ return true;
938
+ }
939
+ /** Fire run-level deadlines: the end of a start delay, run and execution timeouts. */
940
+ fireRunDues() {
941
+ const due = this.dues.filter((entry) => entry.at <= this.clock).sort((a, b) => a.at - b.at);
942
+ if (due.length === 0) return false;
943
+ this.dues = this.dues.filter((entry) => !due.includes(entry));
944
+ for (const entry of due) {
945
+ const run = this.runs.get(entry.runId);
946
+ if (!run || run.state.status !== "RUNNING") continue;
947
+ if (entry.kind === "start") {
948
+ if (run.delayedUntil !== void 0) this.endDelay(run);
949
+ continue;
950
+ }
951
+ const timeoutType = entry.kind === "runTimeout" ? "WORKFLOW_RUN" : "WORKFLOW_EXECUTION";
952
+ const failure = {
953
+ errorType: "TIMEOUT",
954
+ message: timeoutType === "WORKFLOW_RUN" ? "The run timeout expired" : "The execution timeout of the chain expired",
955
+ type: "TimeoutFailure",
956
+ timeoutType,
957
+ source: "engine"
958
+ };
959
+ run.delayedUntil = void 0;
960
+ this.append(run, {
961
+ type: "WORKFLOW_TIMED_OUT",
962
+ payload: { timeoutType, failure: toJsonValue(failure) }
963
+ });
964
+ this.onClosed(run);
965
+ }
966
+ return true;
967
+ }
359
968
  fireDue() {
969
+ const expired = this.expireHeld();
970
+ const fired = this.fireRunDues();
360
971
  const due = this.timers.filter((timer) => timer.fireAt <= this.clock).sort((a, b) => a.fireAt - b.fireAt);
361
- if (due.length === 0) return false;
972
+ if (due.length === 0) return expired || fired;
362
973
  this.timers = this.timers.filter((timer) => !due.includes(timer));
363
974
  for (const timer of due) {
364
975
  const run = this.runs.get(timer.runId);
@@ -371,15 +982,27 @@ var TestWorkflowEnvironment = class _TestWorkflowEnvironment {
371
982
  async runTask(run) {
372
983
  const taskId = run.state.task.taskId ?? `${run.runId}-task-${run.taskCounter}`;
373
984
  this.append(run, { type: "WORKFLOW_TASK_STARTED", payload: { taskId, attempt: 1 } });
985
+ const delivered = [...run.updates.values()].filter(
986
+ (update) => update.status === "PENDING" || update.status === "ADMITTED"
987
+ );
988
+ const updates = delivered.map((update) => ({
989
+ updateId: update.updateId,
990
+ name: update.name,
991
+ args: update.args,
992
+ ...update.headers !== void 0 ? { headers: { ...update.headers } } : {},
993
+ ...update.status === "ADMITTED" ? { admitted: true } : {}
994
+ }));
374
995
  const request = {
375
996
  protocol: HISTORY_PROTOCOL,
376
997
  kind: "task",
377
998
  workflowFn: run.workflowFn,
378
999
  info: run.info,
379
1000
  history: run.history,
380
- nowMs: this.clock
1001
+ nowMs: this.clock,
1002
+ ...updates.length > 0 ? { updates } : {},
1003
+ ...this.codeFeatures.length > 0 ? { features: [...this.codeFeatures] } : {}
381
1004
  };
382
- const result = this.executor ? await this.executor(request) : await runWorkflowTaskInProcess(this.registry, request);
1005
+ const result = await this.executeWithLocalActivities(run, request);
383
1006
  const fail = (cause, message) => {
384
1007
  this.append(run, {
385
1008
  type: "WORKFLOW_TASK_FAILED",
@@ -392,7 +1015,11 @@ var TestWorkflowEnvironment = class _TestWorkflowEnvironment {
392
1015
  run.taskFailure = result;
393
1016
  return;
394
1017
  }
395
- const checked = validateCommands(result.commands, { enabled: this.enabled });
1018
+ const relief = {
1019
+ grantedLocalActivityIds: [],
1020
+ deliveredUpdateIds: delivered.map((update) => update.updateId)
1021
+ };
1022
+ const checked = validateCommands(result.commands, { enabled: this.enabled, relief });
396
1023
  if (!checked.ok) {
397
1024
  fail(checked.cause, checked.issues.map((issue) => issue.message).join("; "));
398
1025
  return;
@@ -400,21 +1027,133 @@ var TestWorkflowEnvironment = class _TestWorkflowEnvironment {
400
1027
  const drafts = eventsForCommands(run.state, checked.commands, {
401
1028
  taskId,
402
1029
  nowMs: this.clock,
403
- enabled: this.enabled
1030
+ enabled: this.enabled,
1031
+ relief,
1032
+ updates: Object.fromEntries(
1033
+ delivered.map((update) => [
1034
+ update.updateId,
1035
+ {
1036
+ name: update.name,
1037
+ args: update.args,
1038
+ ...update.headers !== void 0 ? { headers: update.headers } : {}
1039
+ }
1040
+ ])
1041
+ )
404
1042
  });
405
1043
  if (!drafts.ok) {
406
1044
  fail(drafts.cause, drafts.issues.map((issue) => issue.message).join("; "));
407
1045
  return;
408
1046
  }
1047
+ if (result.unfinishedHandlers !== void 0 && result.unfinishedHandlers.length > 0)
1048
+ run.unfinishedHandlers = result.unfinishedHandlers;
1049
+ this.recordUpdateReplies(run, checked.commands, delivered);
1050
+ this.runSinks(run, result.sinkCalls ?? []);
409
1051
  this.append(run, { type: "WORKFLOW_TASK_COMPLETED", taskId, payload: { taskId } });
410
1052
  for (const draft of drafts.events) this.append(run, draft);
411
1053
  for (const command of checked.commands)
412
1054
  if (run.state.status === "RUNNING" || TERMINAL_COMMANDS.has(command.type))
413
1055
  this.afterCommand(run, command);
414
1056
  }
1057
+ /** What a task's update replies mean for the run's update rows. */
1058
+ recordUpdateReplies(run, commands, delivered) {
1059
+ const answered = /* @__PURE__ */ new Set();
1060
+ for (const command of commands) {
1061
+ if (command.type !== "ACCEPT_UPDATE" && command.type !== "REJECT_UPDATE" && command.type !== "COMPLETE_UPDATE")
1062
+ continue;
1063
+ const entry = run.updates.get(command.updateId);
1064
+ if (!entry) continue;
1065
+ answered.add(command.updateId);
1066
+ if (command.type === "ACCEPT_UPDATE") entry.status = "ACCEPTED";
1067
+ else if (command.type === "REJECT_UPDATE") {
1068
+ entry.status = "REJECTED";
1069
+ entry.failure = command.failure;
1070
+ } else {
1071
+ entry.status = "COMPLETED";
1072
+ if (command.failure !== void 0) entry.failure = command.failure;
1073
+ else entry.result = command.result ?? null;
1074
+ }
1075
+ }
1076
+ for (const update of delivered)
1077
+ if (!answered.has(update.updateId) && update.status === "PENDING") {
1078
+ update.status = "REJECTED";
1079
+ update.failure = {
1080
+ errorType: "VALIDATION",
1081
+ type: "UpdateNotHandled",
1082
+ message: `The workflow task did not answer update "${update.name}"`
1083
+ };
1084
+ }
1085
+ }
1086
+ /** Keep a task's sink calls and call the configured sinks with them. */
1087
+ runSinks(run, calls) {
1088
+ for (const call of calls) {
1089
+ run.sinkCalls.push(call);
1090
+ const fn = this.sinks[call.sink]?.[call.fn];
1091
+ if (fn === void 0) continue;
1092
+ try {
1093
+ fn(...call.args);
1094
+ } catch {
1095
+ }
1096
+ }
1097
+ }
1098
+ execute(request) {
1099
+ return this.executor ? this.executor(request) : runWorkflowTaskInProcess(this.registry, request);
1100
+ }
1101
+ /**
1102
+ * Run a workflow task as the worker does (11 §2.12): while it asks for
1103
+ * local activities, run each through its mock and run the same task again
1104
+ * with the outcomes, up to 1,000 rounds.
1105
+ */
1106
+ async executeWithLocalActivities(run, request) {
1107
+ const outcomes = /* @__PURE__ */ new Map();
1108
+ let result = await this.execute(request);
1109
+ for (let round = 0; result.outcome === "localActivityRequested"; round++) {
1110
+ if (round >= 1e3)
1111
+ return {
1112
+ ...result,
1113
+ outcome: "failed",
1114
+ commands: [],
1115
+ failure: { cause: "INTERNAL", message: "The task asked for local activities 1000 times" }
1116
+ };
1117
+ for (const wanted of result.localActivityRequests ?? [])
1118
+ outcomes.set(wanted.localActivityId, await this.runLocalActivity(run, wanted));
1119
+ result = await this.execute({ ...request, localActivityResults: [...outcomes.values()] });
1120
+ }
1121
+ return result;
1122
+ }
1123
+ async runLocalActivity(run, wanted) {
1124
+ const mock = this.mocks.get(`${wanted.nodeType}#${wanted.operation}`) ?? this.mocks.get(wanted.nodeType);
1125
+ try {
1126
+ if (!mock)
1127
+ throw new ApplicationError(
1128
+ `No mock for ${wanted.nodeType} ${wanted.operation} (use env.mock)`,
1129
+ { errorType: "VALIDATION", nonRetryable: true, type: "UnmockedActivity" }
1130
+ );
1131
+ const result = toJsonValue(
1132
+ await mock(wanted.args, {
1133
+ runId: run.runId,
1134
+ activityId: wanted.localActivityId,
1135
+ attempt: wanted.attempt,
1136
+ nodeType: wanted.nodeType,
1137
+ operation: wanted.operation,
1138
+ local: true,
1139
+ ...wanted.connection !== void 0 ? { connection: wanted.connection } : {}
1140
+ })
1141
+ );
1142
+ return { localActivityId: wanted.localActivityId, result, attempts: wanted.attempt };
1143
+ } catch (error) {
1144
+ const failure = errorToFailure(error, `node:${wanted.nodeType}`);
1145
+ delete failure.stackTrace;
1146
+ return {
1147
+ localActivityId: wanted.localActivityId,
1148
+ failure: toJsonValue(failure),
1149
+ attempts: wanted.attempt
1150
+ };
1151
+ }
1152
+ }
415
1153
  afterCommand(run, command) {
416
1154
  switch (command.type) {
417
1155
  case "SCHEDULE_ACTIVITY": {
1156
+ const user = command.kind === "user";
418
1157
  const attempt = {
419
1158
  runId: run.runId,
420
1159
  activityId: command.activityId,
@@ -422,12 +1161,16 @@ var TestWorkflowEnvironment = class _TestWorkflowEnvironment {
422
1161
  dueAt: this.clock,
423
1162
  scheduledAt: this.clock,
424
1163
  args: command.args,
425
- nodeType: command.nodeType,
426
- operation: command.operation,
1164
+ kind: command.kind,
1165
+ nodeType: user ? "" : command.nodeType,
1166
+ operation: user ? command.activityName : command.operation,
427
1167
  retry: command.retry,
428
1168
  cancelRequested: false,
429
- ...command.connection !== void 0 ? { connection: command.connection } : {},
430
- ...command.scheduleToCloseTimeout !== void 0 ? { scheduleToClose: command.scheduleToCloseTimeout } : {}
1169
+ ...!user && command.connection !== void 0 ? { connection: command.connection } : {},
1170
+ ...user ? { taskQueue: command.taskQueue } : {},
1171
+ ...command.headers !== void 0 ? { headers: { ...command.headers } } : {},
1172
+ ...command.scheduleToCloseTimeout !== void 0 ? { scheduleToClose: command.scheduleToCloseTimeout } : {},
1173
+ ...command.scheduleToStartTimeout !== void 0 ? { scheduleToStart: command.scheduleToStartTimeout } : {}
431
1174
  };
432
1175
  this.attempts.push(attempt);
433
1176
  return;
@@ -502,18 +1245,25 @@ var TestWorkflowEnvironment = class _TestWorkflowEnvironment {
502
1245
  return;
503
1246
  }
504
1247
  case "CONTINUE_AS_NEW": {
505
- const next = this.createRun(
506
- run.workflowFn,
507
- command.input,
508
- {
509
- ...run.info,
510
- runId: void 0,
511
- continuedFromRunId: run.runId,
512
- firstRunId: run.info.firstRunId ?? run.runId
513
- },
514
- run.parent
515
- );
516
- run.continuedTo = next;
1248
+ const options = command.options;
1249
+ const config = {
1250
+ ...this.chainConfig(run.config),
1251
+ // The run timeout restarts and the attempt count resets; the
1252
+ // execution timeout of the chain carries over (11 §2.20).
1253
+ lastCompletionResult: command.input
1254
+ };
1255
+ if (typeof options["taskQueue"] === "string") config.taskQueue = options["taskQueue"];
1256
+ if (typeof options["workflowTaskTimeout"] === "number")
1257
+ config.workflowTaskTimeoutMs = options["workflowTaskTimeout"];
1258
+ if (options["versioningBehavior"] === "PINNED" || options["versioningBehavior"] === "AUTO_UPGRADE")
1259
+ config.versioningBehavior = options["versioningBehavior"];
1260
+ const memo = options["context"];
1261
+ if (typeof memo === "object" && memo !== null && !Array.isArray(memo)) config.memo = memo;
1262
+ else if (Object.keys(run.state.context).length > 0) config.memo = { ...run.state.context };
1263
+ const retry = options["retry"];
1264
+ if (continueAsNewRetryShape(options) === "run" && typeof retry === "object" && retry !== null && !Array.isArray(retry))
1265
+ config.retry = retry;
1266
+ this.startSuccessor(run, command.input, config);
517
1267
  this.onClosed(run);
518
1268
  return;
519
1269
  }
@@ -526,11 +1276,62 @@ var TestWorkflowEnvironment = class _TestWorkflowEnvironment {
526
1276
  return;
527
1277
  }
528
1278
  }
1279
+ /** What a successor of a run inherits (continue-as-new, run retry). */
1280
+ chainConfig(config) {
1281
+ const out = { workflowKey: config.workflowKey, runAttempt: 1 };
1282
+ for (const key of [
1283
+ "businessId",
1284
+ "executionDeadline",
1285
+ "executionTimeoutMs",
1286
+ "runTimeoutMs",
1287
+ "workflowTaskTimeoutMs",
1288
+ "retry",
1289
+ "versioningBehavior",
1290
+ "taskQueue",
1291
+ "memo"
1292
+ ])
1293
+ if (config[key] !== void 0) Object.assign(out, { [key]: config[key] });
1294
+ return out;
1295
+ }
1296
+ /** Start the next run of a chain; the closed run's parent and handles follow it. */
1297
+ startSuccessor(run, input, config) {
1298
+ const next = this.createRun(
1299
+ run.workflowFn,
1300
+ input,
1301
+ {
1302
+ ...run.info,
1303
+ runId: void 0,
1304
+ continuedFromRunId: run.runId,
1305
+ firstRunId: run.info.firstRunId ?? run.runId
1306
+ },
1307
+ run.parent,
1308
+ {},
1309
+ config
1310
+ );
1311
+ run.continuedTo = next;
1312
+ return next;
1313
+ }
529
1314
  resolveTarget(run, target) {
530
1315
  if ("runId" in target) return target.runId;
531
1316
  if ("parent" in target) return run.parent?.runId ?? run.info.parentRunId ?? void 0;
532
- if ("childId" in target) return run.children.get(target.childId);
533
- return void 0;
1317
+ if ("childId" in target) {
1318
+ let childRunId = run.children.get(target.childId);
1319
+ while (childRunId !== void 0) {
1320
+ const next = this.runs.get(childRunId)?.continuedTo;
1321
+ if (next === void 0) break;
1322
+ childRunId = next;
1323
+ }
1324
+ return childRunId;
1325
+ }
1326
+ return this.resolveWorkflowId(target.workflowKey, target.workflowId, target.firstRunId);
1327
+ }
1328
+ /** The open run of a workflow id, else its latest run (11 §2.1). */
1329
+ resolveWorkflowId(workflowKey, workflowId, firstRunId) {
1330
+ const matching = this.runOrder.map((id) => this.run(id)).filter(
1331
+ (candidate) => candidate.config.workflowKey === workflowKey && candidate.config.businessId === workflowId && (firstRunId === void 0 || (candidate.info.firstRunId ?? candidate.runId) === firstRunId)
1332
+ );
1333
+ const open = matching.find((candidate) => candidate.state.status === "RUNNING");
1334
+ return (open ?? matching[matching.length - 1])?.runId;
534
1335
  }
535
1336
  startChild(run, command) {
536
1337
  const byFn = command.workflowFn !== void 0 && (this.executor !== void 0 || this.registry.byName.has(command.workflowFn));
@@ -563,6 +1364,28 @@ var TestWorkflowEnvironment = class _TestWorkflowEnvironment {
563
1364
  });
564
1365
  return;
565
1366
  }
1367
+ const options = command.options;
1368
+ const ms = (key) => typeof options[key] === "number" ? options[key] : void 0;
1369
+ const executionTimeoutMs = ms("executionTimeout");
1370
+ const childKey = command.workflowKey ?? this.registry.byName.get(workflowFn)?.name ?? workflowFn;
1371
+ const config = { workflowKey: childKey, runAttempt: 1 };
1372
+ if (typeof options.workflowId === "string") config.businessId = options.workflowId;
1373
+ if (executionTimeoutMs !== void 0) {
1374
+ config.executionTimeoutMs = executionTimeoutMs;
1375
+ config.executionDeadline = this.clock + executionTimeoutMs;
1376
+ }
1377
+ const runTimeoutMs = ms("runTimeout");
1378
+ if (runTimeoutMs !== void 0) config.runTimeoutMs = runTimeoutMs;
1379
+ const taskTimeoutMs = ms("workflowTaskTimeout");
1380
+ if (taskTimeoutMs !== void 0) config.workflowTaskTimeoutMs = taskTimeoutMs;
1381
+ const startDelayMs = ms("startDelay");
1382
+ if (startDelayMs !== void 0) config.startDelayMs = startDelayMs;
1383
+ if (typeof options.summary === "string") config.summary = options.summary;
1384
+ if (typeof options.details === "string") config.details = options.details;
1385
+ if (options.context !== void 0) config.memo = options.context;
1386
+ if (options.versioningBehavior !== void 0)
1387
+ config.versioningBehavior = options.versioningBehavior;
1388
+ if (options.retry !== void 0) config.retry = options.retry;
566
1389
  const childRunId = this.createRun(
567
1390
  workflowFn,
568
1391
  command.input,
@@ -571,7 +1394,8 @@ var TestWorkflowEnvironment = class _TestWorkflowEnvironment {
571
1394
  {
572
1395
  parentRunId: run.runId,
573
1396
  parentClosePolicy: command.options.parentClosePolicy
574
- }
1397
+ },
1398
+ config
575
1399
  );
576
1400
  run.children.set(command.childId, childRunId);
577
1401
  this.append(run, {
@@ -625,35 +1449,151 @@ var TestWorkflowEnvironment = class _TestWorkflowEnvironment {
625
1449
  });
626
1450
  return;
627
1451
  }
628
- const mock = this.mocks.get(`${attempt.nodeType}#${attempt.operation}`) ?? this.mocks.get(attempt.nodeType);
629
1452
  let failure;
630
1453
  let result = null;
1454
+ let heartbeatDetails = attempt.heartbeatDetails;
1455
+ let asyncToken;
631
1456
  try {
632
- if (!mock)
633
- throw new ApplicationError(
634
- `No mock for ${attempt.nodeType} ${attempt.operation} (use env.mock)`,
635
- {
636
- errorType: "VALIDATION",
637
- nonRetryable: true,
638
- type: "UnmockedActivity"
1457
+ if (attempt.kind === "user") {
1458
+ const impl = findActivityImplementation(this.activityImpls, attempt.operation);
1459
+ if (!impl)
1460
+ throw new ApplicationError(
1461
+ `No implementation of activity ${attempt.operation} (use env.registerActivity or env.mockActivity)`,
1462
+ { errorType: "VALIDATION", nonRetryable: true, type: "ActivityNotRegistered" }
1463
+ );
1464
+ const ctx = this.activityContext(
1465
+ attempt,
1466
+ (details) => {
1467
+ heartbeatDetails = details;
1468
+ },
1469
+ (token) => {
1470
+ asyncToken = token;
639
1471
  }
640
1472
  );
641
- result = toJsonValue(
642
- await mock(attempt.args, {
643
- runId: attempt.runId,
644
- activityId: attempt.activityId,
645
- attempt: attempt.attempt,
646
- nodeType: attempt.nodeType,
647
- operation: attempt.operation,
648
- ...attempt.connection !== void 0 ? { connection: attempt.connection } : {}
649
- })
650
- );
1473
+ result = toJsonValue(
1474
+ await executeActivity(impl, ctx, attempt.args, this.activityInterceptors)
1475
+ );
1476
+ } else {
1477
+ const mock = this.mocks.get(`${attempt.nodeType}#${attempt.operation}`) ?? this.mocks.get(attempt.nodeType);
1478
+ if (!mock)
1479
+ throw new ApplicationError(
1480
+ `No mock for ${attempt.nodeType} ${attempt.operation} (use env.mock)`,
1481
+ {
1482
+ errorType: "VALIDATION",
1483
+ nonRetryable: true,
1484
+ type: "UnmockedActivity"
1485
+ }
1486
+ );
1487
+ result = toJsonValue(
1488
+ await mock(attempt.args, {
1489
+ runId: attempt.runId,
1490
+ activityId: attempt.activityId,
1491
+ attempt: attempt.attempt,
1492
+ nodeType: attempt.nodeType,
1493
+ operation: attempt.operation,
1494
+ ...attempt.connection !== void 0 ? { connection: attempt.connection } : {},
1495
+ ...attempt.heartbeatDetails !== void 0 ? { heartbeatDetails: attempt.heartbeatDetails } : {},
1496
+ heartbeat: (details) => {
1497
+ heartbeatDetails = toJsonValue(details);
1498
+ }
1499
+ })
1500
+ );
1501
+ }
651
1502
  } catch (error) {
652
- failure = errorToFailure(error, `node:${attempt.nodeType}`);
1503
+ failure = errorToFailure(
1504
+ error,
1505
+ attempt.kind === "user" ? "sdk-ts" : `node:${attempt.nodeType}`
1506
+ );
653
1507
  delete failure.stackTrace;
654
1508
  }
655
1509
  if (run.state.status !== "RUNNING" || !isActivityOpen(run.state.activities[attempt.activityId] ?? activity))
656
1510
  return;
1511
+ if (asyncToken !== void 0 && failure === void 0) {
1512
+ this.asyncAttempts.set(asyncToken, {
1513
+ ...attempt,
1514
+ ...heartbeatDetails !== void 0 ? { heartbeatDetails } : {}
1515
+ });
1516
+ return;
1517
+ }
1518
+ this.finishAttempt(run, attempt, failure, result, heartbeatDetails);
1519
+ }
1520
+ /** The `ActivityContext` of one user-activity attempt. */
1521
+ activityContext(attempt, onHeartbeat, onAsync) {
1522
+ const run = this.runs.get(attempt.runId);
1523
+ const listeners = /* @__PURE__ */ new Set();
1524
+ const signal = {
1525
+ get aborted() {
1526
+ return attempt.cancelRequested;
1527
+ },
1528
+ addEventListener: (_type, listener) => {
1529
+ listeners.add(listener);
1530
+ },
1531
+ removeEventListener: (_type, listener) => {
1532
+ listeners.delete(listener);
1533
+ }
1534
+ };
1535
+ return {
1536
+ info: {
1537
+ runId: attempt.runId,
1538
+ workspaceId: this.workspaceId,
1539
+ ...run?.config.workflowKey !== void 0 ? { workflowKey: run.config.workflowKey } : {},
1540
+ activityId: attempt.activityId,
1541
+ activityName: attempt.operation,
1542
+ taskQueue: attempt.taskQueue ?? "managed",
1543
+ attempt: attempt.attempt,
1544
+ ...attempt.heartbeatDetails !== void 0 ? { heartbeatDetails: attempt.heartbeatDetails } : {},
1545
+ headers: { ...attempt.headers ?? {} }
1546
+ },
1547
+ heartbeat: (details) => {
1548
+ onHeartbeat(toJsonValue(details));
1549
+ return Promise.resolve({ cancelRequested: attempt.cancelRequested, paused: false });
1550
+ },
1551
+ cancellationSignal: signal,
1552
+ completeAsync: () => {
1553
+ const token = `test-task-token:${attempt.runId}:${attempt.activityId}:${attempt.attempt}`;
1554
+ onAsync(token);
1555
+ return Promise.resolve(token);
1556
+ },
1557
+ connection: (name) => {
1558
+ const found = this.connections[name];
1559
+ if (found === void 0)
1560
+ return Promise.reject(
1561
+ new ApplicationError(`No connection "${name}" in the test environment`, {
1562
+ errorType: "AUTHORIZATION",
1563
+ nonRetryable: true,
1564
+ type: "ConnectionNotReleased"
1565
+ })
1566
+ );
1567
+ return Promise.resolve({ ...found });
1568
+ }
1569
+ };
1570
+ }
1571
+ /** Complete an activity that called `completeAsync()` (`completeActivityAsync`). */
1572
+ async completeActivityAsync(token, result) {
1573
+ const attempt = this.takeAsync(token);
1574
+ const run = this.run(attempt.runId);
1575
+ this.finishAttempt(run, attempt, void 0, toJsonValue(result), attempt.heartbeatDetails);
1576
+ await this.runUntilIdle(false);
1577
+ }
1578
+ /** Fail an activity that called `completeAsync()` (`failActivityAsync`); the retry policy applies. */
1579
+ async failActivityAsync(token, error) {
1580
+ const attempt = this.takeAsync(token);
1581
+ const run = this.run(attempt.runId);
1582
+ const failure = errorToFailure(error, "sdk-ts");
1583
+ delete failure.stackTrace;
1584
+ this.finishAttempt(run, attempt, failure, null, attempt.heartbeatDetails);
1585
+ await this.runUntilIdle(false);
1586
+ }
1587
+ takeAsync(token) {
1588
+ const attempt = this.asyncAttempts.get(token);
1589
+ if (!attempt) throw new Error(`Unknown or used activity task token ${token}`);
1590
+ this.asyncAttempts.delete(token);
1591
+ return attempt;
1592
+ }
1593
+ /** Record an attempt's outcome: completion, a final failure or timeout, or the next attempt. */
1594
+ finishAttempt(run, attempt, failure, result, heartbeatDetails) {
1595
+ const open = run.state.activities[attempt.activityId];
1596
+ if (run.state.status !== "RUNNING" || !open || !isActivityOpen(open)) return;
657
1597
  const started = () => {
658
1598
  this.append(run, {
659
1599
  type: "ACTIVITY_STARTED",
@@ -686,7 +1626,8 @@ var TestWorkflowEnvironment = class _TestWorkflowEnvironment {
686
1626
  this.attempts.push({
687
1627
  ...attempt,
688
1628
  attempt: attempt.attempt + 1,
689
- dueAt: this.clock + decision2.delayMs
1629
+ dueAt: this.clock + decision2.delayMs,
1630
+ ...heartbeatDetails !== void 0 ? { heartbeatDetails } : {}
690
1631
  });
691
1632
  return;
692
1633
  }
@@ -695,7 +1636,8 @@ var TestWorkflowEnvironment = class _TestWorkflowEnvironment {
695
1636
  this.attempts.push({
696
1637
  ...attempt,
697
1638
  attempt: attempt.attempt + 1,
698
- dueAt: this.clock + decision.delayMs
1639
+ dueAt: this.clock + decision.delayMs,
1640
+ ...heartbeatDetails !== void 0 ? { heartbeatDetails } : {}
699
1641
  });
700
1642
  return;
701
1643
  }
@@ -709,16 +1651,57 @@ var TestWorkflowEnvironment = class _TestWorkflowEnvironment {
709
1651
  }
710
1652
  });
711
1653
  }
712
- onClosed(run) {
1654
+ /**
1655
+ * Run-level retry (11 §2.17): a failed or run-timed-out run whose policy
1656
+ * allows another attempt is followed by a new run of the chain after the
1657
+ * backoff, with `runAttempt + 1` and the failure as `lastFailure`.
1658
+ */
1659
+ retryRun(run) {
1660
+ if (run.continuedTo !== void 0) return;
1661
+ const last = run.history[run.history.length - 1];
1662
+ if (!last || last.type !== "WORKFLOW_FAILED" && last.type !== "WORKFLOW_TIMED_OUT") return;
1663
+ const failure = last.payload["failure"] ?? { errorType: "TIMEOUT", message: "timed out", timeoutType: "WORKFLOW_RUN" };
1664
+ const decision = computeRunRetryDelay(run.config.retry, run.config.runAttempt, failure);
1665
+ if (!decision.retry) return;
1666
+ const config = {
1667
+ ...this.chainConfig(run.config),
1668
+ runAttempt: run.config.runAttempt + 1,
1669
+ lastFailure: failure
1670
+ };
1671
+ if (decision.delayMs > 0) config.firstTaskDelayMs = decision.delayMs;
1672
+ this.startSuccessor(run, run.state.input ?? null, config);
1673
+ }
1674
+ /**
1675
+ * A run closed: drop its pending work, retry it, apply the parent-close
1676
+ * policy to its children (except `keptChildren`, which follow a reset
1677
+ * fork), settle its open updates and notify its parent.
1678
+ */
1679
+ onClosed(run, keptChildren = /* @__PURE__ */ new Set()) {
713
1680
  this.timers = this.timers.filter((timer) => timer.runId !== run.runId);
714
1681
  this.attempts = this.attempts.filter((entry) => entry.runId !== run.runId);
1682
+ this.dues = this.dues.filter((entry) => entry.runId !== run.runId);
1683
+ for (const [token, entry] of this.asyncAttempts)
1684
+ if (entry.runId === run.runId) this.asyncAttempts.delete(token);
1685
+ this.retryRun(run);
715
1686
  for (const [childId2, childRunId] of run.children) {
1687
+ if (keptChildren.has(childId2)) continue;
716
1688
  const child = run.state.children[childId2];
717
1689
  if (!child || !isChildOpen(child) || childRunId === void 0) continue;
718
1690
  if (child.parentClosePolicy === "TERMINATE") this.terminate(childRunId, "parent closed");
719
1691
  else if (child.parentClosePolicy === "REQUEST_CANCEL")
720
1692
  this.requestCancel(childRunId, "parent closed");
721
1693
  }
1694
+ for (const update of run.updates.values()) {
1695
+ if (update.status !== "PENDING" && update.status !== "ACCEPTED" && update.status !== "ADMITTED")
1696
+ continue;
1697
+ const accepted = update.status === "ACCEPTED";
1698
+ update.status = accepted ? "COMPLETED" : "REJECTED";
1699
+ update.failure = {
1700
+ errorType: accepted ? "PERMANENT" : "VALIDATION",
1701
+ type: "RunClosed",
1702
+ message: `The run closed before update "${update.name}" ${accepted ? "completed" : "was accepted"}`
1703
+ };
1704
+ }
722
1705
  if (!run.parent) return;
723
1706
  const parent = this.runs.get(run.parent.runId);
724
1707
  if (!parent || parent.state.status !== "RUNNING") return;
@@ -726,6 +1709,10 @@ var TestWorkflowEnvironment = class _TestWorkflowEnvironment {
726
1709
  if (!parent.state.children[childId] || !isChildOpen(parent.state.children[childId])) return;
727
1710
  const last = run.history[run.history.length - 1];
728
1711
  if (!last) return;
1712
+ if (run.resetTo !== void 0) {
1713
+ parent.children.set(childId, run.resetTo);
1714
+ return;
1715
+ }
729
1716
  if (run.continuedTo !== void 0) {
730
1717
  const next = this.runs.get(run.continuedTo);
731
1718
  if (next) {
@@ -766,19 +1753,34 @@ var TestWorkflowEnvironment = class _TestWorkflowEnvironment {
766
1753
  default:
767
1754
  this.append(parent, {
768
1755
  type: "CHILD_RUN_TIMED_OUT",
769
- payload: { ...base, timeoutType: "WORKFLOW_RUN" }
1756
+ payload: {
1757
+ ...base,
1758
+ timeoutType: last.payload["timeoutType"] === "WORKFLOW_EXECUTION" ? "WORKFLOW_EXECUTION" : "WORKFLOW_RUN"
1759
+ }
770
1760
  });
771
1761
  }
772
1762
  }
773
1763
  async resultOf(runId) {
774
1764
  await this.runUntilIdle(this.timeSkipping);
775
1765
  let run = this.run(runId);
776
- while (run.continuedTo !== void 0) run = this.run(run.continuedTo);
1766
+ while (run.continuedTo !== void 0 || run.resetTo !== void 0)
1767
+ run = this.run(run.continuedTo ?? run.resetTo ?? run.runId);
777
1768
  if (run.taskFailure) throw new WorkflowTaskFailedError(run.runId, run.taskFailure);
1769
+ if (this.failOnUnfinishedHandlers && run.unfinishedHandlers !== void 0)
1770
+ throw new UnfinishedHandlersError(run.runId, run.unfinishedHandlers);
778
1771
  const last = run.history[run.history.length - 1];
779
1772
  switch (run.state.status) {
780
1773
  case "COMPLETED":
781
1774
  return run.state.output ?? null;
1775
+ case "TIMED_OUT":
1776
+ throw new WorkflowFailedError(
1777
+ run.runId,
1778
+ last?.payload["failure"] ?? {
1779
+ errorType: "TIMEOUT",
1780
+ message: "timed out",
1781
+ type: "TimeoutFailure"
1782
+ }
1783
+ );
782
1784
  case "FAILED":
783
1785
  throw new WorkflowFailedError(
784
1786
  run.runId,
@@ -1034,4 +2036,4 @@ async function runWorkflowTaskInVm(script, request, options = {}) {
1034
2036
  }
1035
2037
  }
1036
2038
 
1037
- export { HistoryAssertionError, ReplayError, RunBlockedError, TestRunHandle, TestWorkflowEnvironment, WorkflowFailedError, WorkflowTaskFailedError, assertHistory, bundleToScript, drainJobs, expectIR, jsonCopy, registryOf, replay, replayRequest, runWorkflowTaskInProcess, runWorkflowTaskInVm, simulate };
2039
+ export { HistoryAssertionError, QueryFailedError, QueryRejectedError, ReplayError, RunBlockedError, TestRunHandle, TestWorkflowEnvironment, UnfinishedHandlersError, UpdateRefusedError, WorkflowAlreadyStartedError, WorkflowFailedError, WorkflowTaskFailedError, assertHistory, bundleToScript, drainJobs, expectIR, jsonCopy, registryOf, replay, replayRequest, runWorkflowTaskInProcess, runWorkflowTaskInVm, simulate };