@fluidgrids/workflows 2026.1029.1 → 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 (118) hide show
  1. package/README.md +112 -4
  2. package/dist/{activation-CESehfYi.d.cts → activation-DqUfpHzk.d.cts} +1 -1
  3. package/dist/{activation-BnVQMv9y.d.ts → activation-xpx1oWuo.d.ts} +1 -1
  4. package/dist/{chunk-MJ5TGYVG.js → chunk-2D3MYIIF.js} +969 -153
  5. package/dist/{chunk-2B37XAFQ.cjs → chunk-3U2NU7YC.cjs} +24 -11
  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-2DDULZFM.js → chunk-5HIVIV53.js} +86 -75
  9. package/dist/chunk-5MFVJM76.js +1222 -0
  10. package/dist/{chunk-EWYADREB.cjs → chunk-672KMMBK.cjs} +2 -2
  11. package/dist/{chunk-KXJDRQ6G.js → chunk-6A2FGSUS.js} +22 -2
  12. package/dist/{chunk-OIX4O6UR.js → chunk-6OLN6BMB.js} +16 -3
  13. package/dist/{chunk-6FSHT5IL.js → chunk-7EZIANC7.js} +1 -1
  14. package/dist/chunk-A5SXCA62.cjs +93 -0
  15. package/dist/chunk-AC6ZEXVR.js +85 -0
  16. package/dist/{chunk-FWBRQWRX.js → chunk-AE5Q6ZAJ.js} +167 -10
  17. package/dist/{chunk-7ILGCUI6.js → chunk-C4F5KJTI.js} +142 -3
  18. package/dist/{chunk-WXXNCHUY.js → chunk-G5H32GIG.js} +1 -1
  19. package/dist/{chunk-OMY3CKUZ.cjs → chunk-GEQDGUGB.cjs} +1042 -226
  20. package/dist/{chunk-25BKWR2O.js → chunk-HCM7ZC7Q.js} +1 -1
  21. package/dist/{chunk-I47EENR4.cjs → chunk-HMZUGSMU.cjs} +673 -156
  22. package/dist/{chunk-NXFBVDOI.cjs → chunk-IQPXHQQQ.cjs} +8 -4
  23. package/dist/{chunk-RUQIMTN5.cjs → chunk-ISOIA7NV.cjs} +178 -15
  24. package/dist/chunk-JSC75O5C.cjs +167 -0
  25. package/dist/{chunk-BD7ZPG53.js → chunk-NWQ5WRLE.js} +10 -2
  26. package/dist/chunk-ODZ2IAR2.cjs +1247 -0
  27. package/dist/{chunk-FQSJI5NV.js → chunk-SR7TR5KX.js} +6 -2
  28. package/dist/{chunk-OKX3FYIM.cjs → chunk-TCTE7DX5.cjs} +3 -3
  29. package/dist/{chunk-3FIDHONL.cjs → chunk-TYF56RGB.cjs} +99 -80
  30. package/dist/{chunk-J3PYFGKA.cjs → chunk-VA4OFR72.cjs} +162 -4
  31. package/dist/chunk-VU5S5A7V.js +150 -0
  32. package/dist/{chunk-A36CCOWS.js → chunk-YEHR4NNE.js} +633 -128
  33. package/dist/{chunk-ELL2RBML.cjs → chunk-ZCGKGDCL.cjs} +23 -1
  34. package/dist/codegen/index.d.cts +2 -2
  35. package/dist/codegen/index.d.ts +2 -2
  36. package/dist/{collect-BTHjQaSp.d.ts → collect-6NIEufHd.d.ts} +20 -3
  37. package/dist/{collect-CSyOg5XC.d.cts → collect-CcTCKqx6.d.cts} +20 -3
  38. package/dist/{file-cgL9l9l8.d.cts → file-BxYpwjRQ.d.cts} +250 -15
  39. package/dist/{file-olQudcx5.d.ts → file-CFdPQJI9.d.ts} +250 -15
  40. package/dist/history/index.cjs +283 -106
  41. package/dist/history/index.d.cts +549 -10
  42. package/dist/history/index.d.ts +549 -10
  43. package/dist/history/index.js +5 -4
  44. package/dist/index.cjs +146 -53
  45. package/dist/index.d.cts +38 -14
  46. package/dist/index.d.ts +38 -14
  47. package/dist/index.js +6 -5
  48. package/dist/interceptors-CNcr2dlk.d.ts +159 -0
  49. package/dist/interceptors-CqkNZVfI.d.cts +159 -0
  50. package/dist/ir/index.cjs +339 -36
  51. package/dist/ir/index.d.cts +244 -8
  52. package/dist/ir/index.d.ts +244 -8
  53. package/dist/ir/index.js +306 -21
  54. package/dist/{lower-3S6PsqjJ.d.cts → lower-BtRfIW2A.d.cts} +23 -9
  55. package/dist/{lower-DmZpQ8hJ.d.ts → lower-CpsMFOFX.d.ts} +23 -9
  56. package/dist/{manifest-BJjrQt77.d.ts → manifest-D-I_twys.d.ts} +1 -1
  57. package/dist/{manifest-gRI2Q5Fj.d.cts → manifest-DahBQVM8.d.cts} +1 -1
  58. package/dist/node-evaluator-DGQDPSPI.cjs +24 -0
  59. package/dist/node-evaluator-LEEVD7H7.js +14 -0
  60. package/dist/payload-fields-BVRQn2yH.d.cts +122 -0
  61. package/dist/payload-fields-oOBWxDiG.d.ts +122 -0
  62. package/dist/quickjs/index.cjs +27 -27
  63. package/dist/quickjs/index.d.cts +6 -6
  64. package/dist/quickjs/index.d.ts +6 -6
  65. package/dist/quickjs/index.js +9 -9
  66. package/dist/roundtrip/index.cjs +72 -66
  67. package/dist/roundtrip/index.d.cts +25 -11
  68. package/dist/roundtrip/index.d.ts +25 -11
  69. package/dist/roundtrip/index.js +13 -11
  70. package/dist/runtime/index.cjs +19 -19
  71. package/dist/runtime/index.d.cts +139 -12
  72. package/dist/runtime/index.d.ts +139 -12
  73. package/dist/runtime/index.js +6 -6
  74. package/dist/testing/index.cjs +688 -121
  75. package/dist/testing/index.d.cts +174 -10
  76. package/dist/testing/index.d.ts +174 -10
  77. package/dist/testing/index.js +646 -82
  78. package/dist/{types-CnIOinoz.d.ts → types-Bv2i4cBX.d.cts} +126 -23
  79. package/dist/{types-CQKOhPTa.d.cts → types-DGx-b9eq.d.ts} +126 -23
  80. package/dist/{types-Bb2wRnJ5.d.cts → types-V7jEEUaL.d.cts} +1 -1
  81. package/dist/{types-Bb2wRnJ5.d.ts → types-V7jEEUaL.d.ts} +1 -1
  82. package/dist/{workflow-8uRU9rhL.d.ts → workflow-B18OeF7g.d.ts} +2 -2
  83. package/dist/{workflow-DWTPHFbt.d.ts → workflow-BGnv8SS6.d.ts} +261 -14
  84. package/dist/{workflow-CZOXUlQ5.d.cts → workflow-BJPFv3cp.d.cts} +2 -2
  85. package/dist/{workflow-O6e64ZcG.d.cts → workflow-DS32Gtws.d.cts} +261 -14
  86. package/fixtures/histories/activity-options-updated.history.json +159 -0
  87. package/fixtures/histories/activity-paused-unpaused.history.json +160 -0
  88. package/fixtures/histories/codec-opaque.history.json +229 -0
  89. package/fixtures/histories/dynamic-workflow.history.json +150 -0
  90. package/fixtures/histories/interceptor-headers.history.json +339 -0
  91. package/fixtures/histories/query-custom.history.json +213 -0
  92. package/fixtures/histories/query-default-handler.history.json +120 -0
  93. package/fixtures/histories/reset-forked-reapplied-signals.history.json +239 -0
  94. package/fixtures/histories/reset-reapplied-update-admitted.history.json +228 -0
  95. package/fixtures/histories/search-attributes-upserted.history.json +177 -0
  96. package/fixtures/histories/update-accepted-completed.history.json +45 -30
  97. package/fixtures/histories/update-default-handler.history.json +246 -0
  98. package/fixtures/histories/update-handler-awaits-activity.history.json +196 -0
  99. package/fixtures/histories/update-not-handled.history.json +138 -0
  100. package/fixtures/histories/update-rejected-no-event.history.json +190 -0
  101. package/fixtures/histories/update-with-start-admitted.history.json +150 -0
  102. package/fixtures/histories/user-activity-cancel.history.json +149 -0
  103. package/fixtures/histories/user-activity-retry.history.json +143 -0
  104. package/fixtures/histories/user-activity.history.json +149 -0
  105. package/package.json +1 -1
  106. package/schema/bridge-v2.schema.json +1062 -0
  107. package/schema/history-file.schema.json +255 -81
  108. package/schema/ir-v2.schema.json +246 -1
  109. package/schema/payload-fields.json +162 -0
  110. package/schema/run-event.schema.json +239 -80
  111. package/schema/workflow-command.schema.json +210 -84
  112. package/schema/workflow-task.schema.json +532 -167
  113. package/dist/chunk-6QPOJZT6.cjs +0 -1180
  114. package/dist/chunk-A2OHX3UQ.js +0 -1156
  115. package/dist/chunk-PWIUEWC3.js +0 -81
  116. package/dist/chunk-YHBVMOMK.cjs +0 -87
  117. package/dist/node-evaluator-SOADN23V.js +0 -14
  118. package/dist/node-evaluator-YGDEXK4U.cjs +0 -24
@@ -1,29 +1,32 @@
1
- import { failedTaskResult } from '../chunk-WXXNCHUY.js';
2
- import { isTier1Workflow, toIR } from '../chunk-FQSJI5NV.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-A36CCOWS.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-MJ5TGYVG.js';
11
- import { failureToError, signalName, ApplicationError, errorToFailure, CancelledFailure, DeterminismViolation } from '../chunk-KXJDRQ6G.js';
11
+ import { buildWorkflowRegistry, createWorkflowRuntime } from '../chunk-2D3MYIIF.js';
12
+ import { failureToError, signalName, ApplicationError, errorToFailure, CancelledFailure, DeterminismViolation } from '../chunk-6A2FGSUS.js';
12
13
  import '../chunk-M272AHEX.js';
13
14
  import { TASK_PRELUDE } from '../chunk-4LYSEANA.js';
14
- import { runRetryPolicyOf, toMs, isDurableWorkflow } from '../chunk-7ILGCUI6.js';
15
+ import { isActivityImplementation, isActivityDefinition, ACTIVITY_IMPL_MARKER, ACTIVITY_MARKER, runRetryPolicyOf, toMs, findActivityImplementation, executeActivity, isDurableWorkflow } from '../chunk-C4F5KJTI.js';
15
16
  import '../chunk-LNUQE6D6.js';
16
- import { initialRunState, applyEvent, isActivityOpen, eventsForCommands, continueAsNewRetryShape, isChildOpen } from '../chunk-FWBRQWRX.js';
17
- import { RUNTIME_RELEASE, isInputEvent, HISTORY_PROTOCOL, TERMINAL_COMMANDS } from '../chunk-2DDULZFM.js';
18
- import { computeRetryDelay, computeRunRetryDelay } from '../chunk-PWIUEWC3.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';
19
20
  import { createContext, Script } from 'vm';
20
21
 
21
22
  // src/testing/host.ts
22
23
  function drainJobs() {
23
24
  return new Promise((resolve) => setImmediate(resolve));
24
25
  }
25
- function registryOf(workflows) {
26
- return buildWorkflowRegistry([["in-process", { ...workflows }]]);
26
+ function registryOf(workflows, interceptors) {
27
+ return buildWorkflowRegistry([
28
+ ["in-process", { ...workflows, ...interceptors !== void 0 ? { interceptors } : {} }]
29
+ ]);
27
30
  }
28
31
  async function hostLoop(abi, requestJson) {
29
32
  abi.start(requestJson);
@@ -39,6 +42,32 @@ async function runWorkflowTaskInProcess(registry, request) {
39
42
  }
40
43
 
41
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
+ };
42
71
  var WorkflowAlreadyStartedError = class extends Error {
43
72
  constructor(workflowKey, workflowId, existingRunId) {
44
73
  super(
@@ -96,7 +125,7 @@ var RunBlockedError = class extends Error {
96
125
  }
97
126
  runId;
98
127
  };
99
- var TestRunHandle = class {
128
+ var TestRunHandle = class _TestRunHandle {
100
129
  constructor(env, runId) {
101
130
  this.env = env;
102
131
  this.runId = runId;
@@ -126,6 +155,22 @@ var TestRunHandle = class {
126
155
  this.env.terminate(this.runId, reason);
127
156
  await this.env.runUntilIdle(false);
128
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
+ }
129
174
  /** Run until this run (or the last run of its continue-as-new chain) ends; its output. */
130
175
  async result() {
131
176
  return this.env.resultOf(this.runId);
@@ -149,6 +194,14 @@ var FAILED_STATUSES = /* @__PURE__ */ new Set([
149
194
  "TERMINATED",
150
195
  "TIMED_OUT"
151
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
+ }
152
205
  function toJsonValue(value) {
153
206
  if (value === void 0) return null;
154
207
  const text = JSON.stringify(value);
@@ -158,6 +211,8 @@ var TestWorkflowEnvironment = class _TestWorkflowEnvironment {
158
211
  clock;
159
212
  timeSkipping;
160
213
  enabled;
214
+ /** The bundle features the gate's flags enable, sent with every task (`request.features`). */
215
+ codeFeatures;
161
216
  failOnUnfinishedHandlers;
162
217
  historyRelease;
163
218
  dues = [];
@@ -173,19 +228,65 @@ var TestWorkflowEnvironment = class _TestWorkflowEnvironment {
173
228
  timers = [];
174
229
  mockedChildren = [];
175
230
  runCounter = 0;
231
+ updateCounter = 0;
176
232
  executor;
177
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();
178
241
  constructor(options) {
179
242
  this.clock = toDate(options.startTime);
180
243
  this.timeSkipping = options.timeSkipping !== false;
181
- this.enabled = options.enabled ?? "parity";
244
+ this.enabled = options.enabled ?? new Set(COMMAND_FEATURES);
245
+ this.codeFeatures = codeFeaturesEnabledBy(this.enabled);
182
246
  this.failOnUnfinishedHandlers = options.failOnUnfinishedHandlers !== false;
183
247
  this.historyRelease = options.historyRelease ?? RUNTIME_RELEASE;
184
248
  this.workspaceId = options.workspaceId ?? "test-workspace";
185
249
  this.workflows = { ...options.workflows ?? {} };
186
- this.registry = registryOf(this.workflows);
250
+ this.interceptors = options.interceptors;
251
+ this.registry = registryOf(this.workflows, this.interceptors);
187
252
  this.executor = options.taskExecutor;
188
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
+ );
189
290
  }
190
291
  /** Create an environment. */
191
292
  static async create(options = {}) {
@@ -217,6 +318,10 @@ var TestWorkflowEnvironment = class _TestWorkflowEnvironment {
217
318
  if (nodeType === void 0) this.held.clear();
218
319
  else this.held.delete(nodeType);
219
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
+ }
220
325
  isHeld(nodeType) {
221
326
  return this.held.has("*") || this.held.has(nodeType);
222
327
  }
@@ -232,11 +337,17 @@ var TestWorkflowEnvironment = class _TestWorkflowEnvironment {
232
337
  );
233
338
  const declared = this.workflows[workflowFn]?.options ?? {};
234
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
+ );
235
344
  if (options.workflowId !== void 0) {
236
345
  const existing = this.useExisting(workflowKey, options);
237
346
  if (existing !== void 0) {
238
347
  for (const signal of options.signals ?? [])
239
348
  this.deliverSignal(existing, signal.name, signal.payload);
349
+ if (options.update !== void 0)
350
+ this.addUpdate(this.run(existing), options.update, "PENDING");
240
351
  await this.runUntilIdle(false);
241
352
  return new TestRunHandle(this, existing);
242
353
  }
@@ -261,7 +372,11 @@ var TestWorkflowEnvironment = class _TestWorkflowEnvironment {
261
372
  ...options.summary !== void 0 ? { summary: options.summary } : {},
262
373
  ...options.details !== void 0 ? { details: options.details } : {},
263
374
  ...options.memo !== void 0 ? { memo: toJsonValue(options.memo) } : {},
264
- ...(options.versioningBehavior ?? declared.versioningBehavior) !== void 0 ? { versioningBehavior: options.versioningBehavior ?? declared.versioningBehavior } : {}
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 } : {}
265
380
  };
266
381
  const runId = this.createRun(
267
382
  workflowFn,
@@ -269,13 +384,245 @@ var TestWorkflowEnvironment = class _TestWorkflowEnvironment {
269
384
  { ...options.info, ...options.runId !== void 0 ? { runId: options.runId } : {} },
270
385
  void 0,
271
386
  {},
272
- config
387
+ config,
388
+ options.update !== void 0 ? [this.newUpdate(options.update, "ADMITTED")] : []
273
389
  );
274
390
  for (const signal of options.signals ?? [])
275
391
  this.deliverSignal(runId, signal.name, signal.payload);
276
392
  await this.runUntilIdle(false);
277
393
  return new TestRunHandle(this, runId);
278
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
+ }
279
626
  durationConfig(value, key) {
280
627
  const ms = optionalDuration(value, key);
281
628
  if (ms === void 0) return {};
@@ -351,7 +698,7 @@ var TestWorkflowEnvironment = class _TestWorkflowEnvironment {
351
698
  if (value === definition) return name2;
352
699
  const name = Object.hasOwn(this.workflows, definition.name) ? `${definition.name}#${this.runCounter}` : definition.name;
353
700
  this.workflows[name] = definition;
354
- this.registry = registryOf(this.workflows);
701
+ this.registry = registryOf(this.workflows, this.interceptors);
355
702
  return name;
356
703
  }
357
704
  run(runId) {
@@ -359,7 +706,7 @@ var TestWorkflowEnvironment = class _TestWorkflowEnvironment {
359
706
  if (!run) throw new Error(`Unknown run ${runId}`);
360
707
  return run;
361
708
  }
362
- createRun(workflowFn, input, info, parent, extraStarted = {}, config = { workflowKey: workflowFn, runAttempt: 1 }) {
709
+ createRun(workflowFn, input, info, parent, extraStarted = {}, config = { workflowKey: workflowFn, runAttempt: 1 }, admitted = []) {
363
710
  this.runCounter += 1;
364
711
  const runId = info.runId ?? `run-${this.runCounter}`;
365
712
  const snapshot = {
@@ -381,7 +728,9 @@ var TestWorkflowEnvironment = class _TestWorkflowEnvironment {
381
728
  info: snapshot,
382
729
  children: /* @__PURE__ */ new Map(),
383
730
  config,
384
- ...parent ? { parent } : {}
731
+ ...parent ? { parent } : {},
732
+ updates: /* @__PURE__ */ new Map(),
733
+ sinkCalls: []
385
734
  };
386
735
  const firstTaskDelay = config.startDelayMs ?? config.firstTaskDelayMs;
387
736
  if (firstTaskDelay !== void 0 && firstTaskDelay > 0) {
@@ -394,19 +743,30 @@ var TestWorkflowEnvironment = class _TestWorkflowEnvironment {
394
743
  this.dues.push({ runId, kind: "executionTimeout", at: config.executionDeadline });
395
744
  this.runs.set(runId, run);
396
745
  this.runOrder.push(runId);
397
- this.append(run, {
398
- type: "WORKFLOW_STARTED",
399
- // The service writes the seed of ctx.random()/ctx.uuid() into the start
400
- // event so the history replays on its own; it uses the run id.
401
- payload: {
402
- workflowFn,
403
- input,
404
- taskQueue: config.taskQueue ?? "managed",
405
- randomnessSeed: runId,
406
- ...extraStarted,
407
- ...this.startedFields(config)
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);
408
767
  }
409
- });
768
+ this.scheduleTask(run);
769
+ }
410
770
  return runId;
411
771
  }
412
772
  /** The R2 fields of `WORKFLOW_STARTED`, each written only when it is set. */
@@ -434,9 +794,13 @@ var TestWorkflowEnvironment = class _TestWorkflowEnvironment {
434
794
  if (config.lastCompletionResult !== void 0 && this.historyRelease >= 2)
435
795
  out["lastCompletionResult"] = config.lastCompletionResult;
436
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;
437
801
  return out;
438
802
  }
439
- append(run, draft) {
803
+ append(run, draft, schedule = true) {
440
804
  const event = {
441
805
  seq: run.history.length + 1,
442
806
  type: draft.type,
@@ -447,7 +811,7 @@ var TestWorkflowEnvironment = class _TestWorkflowEnvironment {
447
811
  applyEvent(run.state, event);
448
812
  run.history.push(event);
449
813
  if (run.delayedUntil !== void 0 && event.type === "SIGNAL_RECEIVED") this.endDelay(run);
450
- if (run.delayedUntil === void 0 && isInputEvent(event) && run.state.status === "RUNNING" && run.state.task.state === "NONE") {
814
+ if (schedule && run.delayedUntil === void 0 && isInputEvent(event) && run.state.status === "RUNNING" && run.state.task.state === "NONE") {
451
815
  run.taskCounter += 1;
452
816
  const taskId = `${run.runId}-task-${run.taskCounter}`;
453
817
  const scheduled = {
@@ -465,19 +829,9 @@ var TestWorkflowEnvironment = class _TestWorkflowEnvironment {
465
829
  endDelay(run) {
466
830
  run.delayedUntil = void 0;
467
831
  this.dues = this.dues.filter((due) => !(due.runId === run.runId && due.kind === "start"));
468
- if (run.state.status !== "RUNNING" || run.state.task.state !== "NONE") return;
469
- run.taskCounter += 1;
470
- const taskId = `${run.runId}-task-${run.taskCounter}`;
471
- const scheduled = {
472
- seq: run.history.length + 1,
473
- type: "WORKFLOW_TASK_SCHEDULED",
474
- payload: { taskId },
475
- createdAt: new Date(this.clock).toISOString()
476
- };
477
- applyEvent(run.state, scheduled);
478
- run.history.push(scheduled);
832
+ this.scheduleTask(run);
479
833
  }
480
- deliverSignal(runId, name, payload) {
834
+ deliverSignal(runId, name, payload, headers) {
481
835
  const run = this.runs.get(runId);
482
836
  if (!run || run.state.status !== "RUNNING") return false;
483
837
  this.append(run, {
@@ -485,7 +839,8 @@ var TestWorkflowEnvironment = class _TestWorkflowEnvironment {
485
839
  payload: {
486
840
  name,
487
841
  payload: toJsonValue(payload),
488
- receivedAt: new Date(this.clock).toISOString()
842
+ receivedAt: new Date(this.clock).toISOString(),
843
+ ...headers !== void 0 ? { headers: { ...headers } } : {}
489
844
  }
490
845
  });
491
846
  return true;
@@ -523,7 +878,7 @@ var TestWorkflowEnvironment = class _TestWorkflowEnvironment {
523
878
  continue;
524
879
  }
525
880
  const attempt = this.attempts.find(
526
- (entry) => entry.dueAt <= this.clock && !this.isHeld(entry.nodeType)
881
+ (entry) => entry.dueAt <= this.clock && !this.isHeld(this.holdKeyOf(entry))
527
882
  );
528
883
  if (attempt) {
529
884
  this.attempts = this.attempts.filter((entry) => entry !== attempt);
@@ -545,12 +900,12 @@ var TestWorkflowEnvironment = class _TestWorkflowEnvironment {
545
900
  }
546
901
  /** When a held attempt's schedule-to-start timeout expires, if it has one. */
547
902
  heldDeadline(entry) {
548
- return this.isHeld(entry.nodeType) && entry.scheduleToStart !== void 0 ? entry.dueAt + entry.scheduleToStart : void 0;
903
+ return this.isHeld(this.holdKeyOf(entry)) && entry.scheduleToStart !== void 0 ? entry.dueAt + entry.scheduleToStart : void 0;
549
904
  }
550
905
  nextDue() {
551
906
  const due = [...this.timers.map((timer) => timer.fireAt)];
552
907
  for (const entry of this.attempts) {
553
- if (!this.isHeld(entry.nodeType)) due.push(entry.dueAt);
908
+ if (!this.isHeld(this.holdKeyOf(entry))) due.push(entry.dueAt);
554
909
  const deadline = this.heldDeadline(entry);
555
910
  if (deadline !== void 0) due.push(deadline);
556
911
  }
@@ -627,13 +982,25 @@ var TestWorkflowEnvironment = class _TestWorkflowEnvironment {
627
982
  async runTask(run) {
628
983
  const taskId = run.state.task.taskId ?? `${run.runId}-task-${run.taskCounter}`;
629
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
+ }));
630
995
  const request = {
631
996
  protocol: HISTORY_PROTOCOL,
632
997
  kind: "task",
633
998
  workflowFn: run.workflowFn,
634
999
  info: run.info,
635
1000
  history: run.history,
636
- nowMs: this.clock
1001
+ nowMs: this.clock,
1002
+ ...updates.length > 0 ? { updates } : {},
1003
+ ...this.codeFeatures.length > 0 ? { features: [...this.codeFeatures] } : {}
637
1004
  };
638
1005
  const result = await this.executeWithLocalActivities(run, request);
639
1006
  const fail = (cause, message) => {
@@ -648,7 +1015,11 @@ var TestWorkflowEnvironment = class _TestWorkflowEnvironment {
648
1015
  run.taskFailure = result;
649
1016
  return;
650
1017
  }
651
- 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 });
652
1023
  if (!checked.ok) {
653
1024
  fail(checked.cause, checked.issues.map((issue) => issue.message).join("; "));
654
1025
  return;
@@ -656,7 +1027,18 @@ var TestWorkflowEnvironment = class _TestWorkflowEnvironment {
656
1027
  const drafts = eventsForCommands(run.state, checked.commands, {
657
1028
  taskId,
658
1029
  nowMs: this.clock,
659
- 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
+ )
660
1042
  });
661
1043
  if (!drafts.ok) {
662
1044
  fail(drafts.cause, drafts.issues.map((issue) => issue.message).join("; "));
@@ -664,12 +1046,55 @@ var TestWorkflowEnvironment = class _TestWorkflowEnvironment {
664
1046
  }
665
1047
  if (result.unfinishedHandlers !== void 0 && result.unfinishedHandlers.length > 0)
666
1048
  run.unfinishedHandlers = result.unfinishedHandlers;
1049
+ this.recordUpdateReplies(run, checked.commands, delivered);
1050
+ this.runSinks(run, result.sinkCalls ?? []);
667
1051
  this.append(run, { type: "WORKFLOW_TASK_COMPLETED", taskId, payload: { taskId } });
668
1052
  for (const draft of drafts.events) this.append(run, draft);
669
1053
  for (const command of checked.commands)
670
1054
  if (run.state.status === "RUNNING" || TERMINAL_COMMANDS.has(command.type))
671
1055
  this.afterCommand(run, command);
672
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
+ }
673
1098
  execute(request) {
674
1099
  return this.executor ? this.executor(request) : runWorkflowTaskInProcess(this.registry, request);
675
1100
  }
@@ -728,6 +1153,7 @@ var TestWorkflowEnvironment = class _TestWorkflowEnvironment {
728
1153
  afterCommand(run, command) {
729
1154
  switch (command.type) {
730
1155
  case "SCHEDULE_ACTIVITY": {
1156
+ const user = command.kind === "user";
731
1157
  const attempt = {
732
1158
  runId: run.runId,
733
1159
  activityId: command.activityId,
@@ -735,11 +1161,14 @@ var TestWorkflowEnvironment = class _TestWorkflowEnvironment {
735
1161
  dueAt: this.clock,
736
1162
  scheduledAt: this.clock,
737
1163
  args: command.args,
738
- nodeType: command.nodeType,
739
- operation: command.operation,
1164
+ kind: command.kind,
1165
+ nodeType: user ? "" : command.nodeType,
1166
+ operation: user ? command.activityName : command.operation,
740
1167
  retry: command.retry,
741
1168
  cancelRequested: false,
742
- ...command.connection !== void 0 ? { connection: command.connection } : {},
1169
+ ...!user && command.connection !== void 0 ? { connection: command.connection } : {},
1170
+ ...user ? { taskQueue: command.taskQueue } : {},
1171
+ ...command.headers !== void 0 ? { headers: { ...command.headers } } : {},
743
1172
  ...command.scheduleToCloseTimeout !== void 0 ? { scheduleToClose: command.scheduleToCloseTimeout } : {},
744
1173
  ...command.scheduleToStartTimeout !== void 0 ? { scheduleToStart: command.scheduleToStartTimeout } : {}
745
1174
  };
@@ -1020,40 +1449,151 @@ var TestWorkflowEnvironment = class _TestWorkflowEnvironment {
1020
1449
  });
1021
1450
  return;
1022
1451
  }
1023
- const mock = this.mocks.get(`${attempt.nodeType}#${attempt.operation}`) ?? this.mocks.get(attempt.nodeType);
1024
1452
  let failure;
1025
1453
  let result = null;
1026
1454
  let heartbeatDetails = attempt.heartbeatDetails;
1455
+ let asyncToken;
1027
1456
  try {
1028
- if (!mock)
1029
- throw new ApplicationError(
1030
- `No mock for ${attempt.nodeType} ${attempt.operation} (use env.mock)`,
1031
- {
1032
- errorType: "VALIDATION",
1033
- nonRetryable: true,
1034
- 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;
1035
1471
  }
1036
1472
  );
1037
- result = toJsonValue(
1038
- await mock(attempt.args, {
1039
- runId: attempt.runId,
1040
- activityId: attempt.activityId,
1041
- attempt: attempt.attempt,
1042
- nodeType: attempt.nodeType,
1043
- operation: attempt.operation,
1044
- ...attempt.connection !== void 0 ? { connection: attempt.connection } : {},
1045
- ...attempt.heartbeatDetails !== void 0 ? { heartbeatDetails: attempt.heartbeatDetails } : {},
1046
- heartbeat: (details) => {
1047
- heartbeatDetails = toJsonValue(details);
1048
- }
1049
- })
1050
- );
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
+ }
1051
1502
  } catch (error) {
1052
- failure = errorToFailure(error, `node:${attempt.nodeType}`);
1503
+ failure = errorToFailure(
1504
+ error,
1505
+ attempt.kind === "user" ? "sdk-ts" : `node:${attempt.nodeType}`
1506
+ );
1053
1507
  delete failure.stackTrace;
1054
1508
  }
1055
1509
  if (run.state.status !== "RUNNING" || !isActivityOpen(run.state.activities[attempt.activityId] ?? activity))
1056
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;
1057
1597
  const started = () => {
1058
1598
  this.append(run, {
1059
1599
  type: "ACTIVITY_STARTED",
@@ -1131,18 +1671,37 @@ var TestWorkflowEnvironment = class _TestWorkflowEnvironment {
1131
1671
  if (decision.delayMs > 0) config.firstTaskDelayMs = decision.delayMs;
1132
1672
  this.startSuccessor(run, run.state.input ?? null, config);
1133
1673
  }
1134
- onClosed(run) {
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()) {
1135
1680
  this.timers = this.timers.filter((timer) => timer.runId !== run.runId);
1136
1681
  this.attempts = this.attempts.filter((entry) => entry.runId !== run.runId);
1137
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);
1138
1685
  this.retryRun(run);
1139
1686
  for (const [childId2, childRunId] of run.children) {
1687
+ if (keptChildren.has(childId2)) continue;
1140
1688
  const child = run.state.children[childId2];
1141
1689
  if (!child || !isChildOpen(child) || childRunId === void 0) continue;
1142
1690
  if (child.parentClosePolicy === "TERMINATE") this.terminate(childRunId, "parent closed");
1143
1691
  else if (child.parentClosePolicy === "REQUEST_CANCEL")
1144
1692
  this.requestCancel(childRunId, "parent closed");
1145
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
+ }
1146
1705
  if (!run.parent) return;
1147
1706
  const parent = this.runs.get(run.parent.runId);
1148
1707
  if (!parent || parent.state.status !== "RUNNING") return;
@@ -1150,6 +1709,10 @@ var TestWorkflowEnvironment = class _TestWorkflowEnvironment {
1150
1709
  if (!parent.state.children[childId] || !isChildOpen(parent.state.children[childId])) return;
1151
1710
  const last = run.history[run.history.length - 1];
1152
1711
  if (!last) return;
1712
+ if (run.resetTo !== void 0) {
1713
+ parent.children.set(childId, run.resetTo);
1714
+ return;
1715
+ }
1153
1716
  if (run.continuedTo !== void 0) {
1154
1717
  const next = this.runs.get(run.continuedTo);
1155
1718
  if (next) {
@@ -1200,7 +1763,8 @@ var TestWorkflowEnvironment = class _TestWorkflowEnvironment {
1200
1763
  async resultOf(runId) {
1201
1764
  await this.runUntilIdle(this.timeSkipping);
1202
1765
  let run = this.run(runId);
1203
- 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);
1204
1768
  if (run.taskFailure) throw new WorkflowTaskFailedError(run.runId, run.taskFailure);
1205
1769
  if (this.failOnUnfinishedHandlers && run.unfinishedHandlers !== void 0)
1206
1770
  throw new UnfinishedHandlersError(run.runId, run.unfinishedHandlers);
@@ -1472,4 +2036,4 @@ async function runWorkflowTaskInVm(script, request, options = {}) {
1472
2036
  }
1473
2037
  }
1474
2038
 
1475
- export { HistoryAssertionError, ReplayError, RunBlockedError, TestRunHandle, TestWorkflowEnvironment, UnfinishedHandlersError, WorkflowAlreadyStartedError, 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 };