@victframework/runtime 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/dist/agent-governance.d.ts +285 -0
  2. package/dist/agent-governance.js +438 -0
  3. package/dist/agent-governance.js.map +1 -0
  4. package/dist/agent-registry.d.ts +103 -0
  5. package/dist/agent-registry.js +758 -0
  6. package/dist/agent-registry.js.map +1 -0
  7. package/dist/agent-types.d.ts +433 -0
  8. package/dist/agent-types.js +480 -0
  9. package/dist/agent-types.js.map +1 -0
  10. package/dist/authority.d.ts +83 -0
  11. package/dist/authority.js +170 -0
  12. package/dist/authority.js.map +1 -0
  13. package/dist/boundary-conformance.d.ts +51 -0
  14. package/dist/boundary-conformance.js +364 -0
  15. package/dist/boundary-conformance.js.map +1 -0
  16. package/dist/control-conformance.d.ts +68 -0
  17. package/dist/control-conformance.js +1131 -0
  18. package/dist/control-conformance.js.map +1 -0
  19. package/dist/control-in-memory.d.ts +257 -0
  20. package/dist/control-in-memory.js +886 -0
  21. package/dist/control-in-memory.js.map +1 -0
  22. package/dist/control-types.d.ts +1017 -0
  23. package/dist/control-types.js +779 -0
  24. package/dist/control-types.js.map +1 -0
  25. package/dist/durable-run.d.ts +73 -0
  26. package/dist/durable-run.js +262 -0
  27. package/dist/durable-run.js.map +1 -0
  28. package/dist/effect-policy.d.ts +22 -0
  29. package/dist/effect-policy.js +45 -0
  30. package/dist/effect-policy.js.map +1 -0
  31. package/dist/errors.d.ts +36 -0
  32. package/dist/errors.js +71 -0
  33. package/dist/errors.js.map +1 -0
  34. package/dist/in-memory-stores.d.ts +16 -0
  35. package/dist/in-memory-stores.js +522 -0
  36. package/dist/in-memory-stores.js.map +1 -0
  37. package/dist/index.d.ts +41 -0
  38. package/dist/index.js +26 -0
  39. package/dist/index.js.map +1 -0
  40. package/dist/operator-config.d.ts +141 -0
  41. package/dist/operator-config.js +289 -0
  42. package/dist/operator-config.js.map +1 -0
  43. package/dist/orchestration-activation.d.ts +116 -0
  44. package/dist/orchestration-activation.js +223 -0
  45. package/dist/orchestration-activation.js.map +1 -0
  46. package/dist/orchestration-canary-conformance.d.ts +10 -0
  47. package/dist/orchestration-canary-conformance.js +726 -0
  48. package/dist/orchestration-canary-conformance.js.map +1 -0
  49. package/dist/orchestration-commands.d.ts +47 -0
  50. package/dist/orchestration-commands.js +538 -0
  51. package/dist/orchestration-commands.js.map +1 -0
  52. package/dist/orchestration-conformance.d.ts +59 -0
  53. package/dist/orchestration-conformance.js +594 -0
  54. package/dist/orchestration-conformance.js.map +1 -0
  55. package/dist/orchestration-driver-types.d.ts +163 -0
  56. package/dist/orchestration-driver-types.js +21 -0
  57. package/dist/orchestration-driver-types.js.map +1 -0
  58. package/dist/orchestration-driver.d.ts +48 -0
  59. package/dist/orchestration-driver.js +1030 -0
  60. package/dist/orchestration-driver.js.map +1 -0
  61. package/dist/orchestration-in-memory.d.ts +18 -0
  62. package/dist/orchestration-in-memory.js +1313 -0
  63. package/dist/orchestration-in-memory.js.map +1 -0
  64. package/dist/orchestration-join-conformance.d.ts +20 -0
  65. package/dist/orchestration-join-conformance.js +542 -0
  66. package/dist/orchestration-join-conformance.js.map +1 -0
  67. package/dist/orchestration-plan.d.ts +52 -0
  68. package/dist/orchestration-plan.js +265 -0
  69. package/dist/orchestration-plan.js.map +1 -0
  70. package/dist/orchestration-race-conformance.d.ts +35 -0
  71. package/dist/orchestration-race-conformance.js +1265 -0
  72. package/dist/orchestration-race-conformance.js.map +1 -0
  73. package/dist/orchestration-remediation-conformance.d.ts +54 -0
  74. package/dist/orchestration-remediation-conformance.js +632 -0
  75. package/dist/orchestration-remediation-conformance.js.map +1 -0
  76. package/dist/orchestration-store-types.d.ts +568 -0
  77. package/dist/orchestration-store-types.js +2 -0
  78. package/dist/orchestration-store-types.js.map +1 -0
  79. package/dist/pack-conformance.d.ts +59 -0
  80. package/dist/pack-conformance.js +222 -0
  81. package/dist/pack-conformance.js.map +1 -0
  82. package/dist/pack-install.d.ts +44 -0
  83. package/dist/pack-install.js +126 -0
  84. package/dist/pack-install.js.map +1 -0
  85. package/dist/registry.d.ts +107 -0
  86. package/dist/registry.js +481 -0
  87. package/dist/registry.js.map +1 -0
  88. package/dist/runtime.d.ts +158 -0
  89. package/dist/runtime.js +1341 -0
  90. package/dist/runtime.js.map +1 -0
  91. package/dist/serialization.d.ts +26 -0
  92. package/dist/serialization.js +162 -0
  93. package/dist/serialization.js.map +1 -0
  94. package/dist/store-conformance.d.ts +45 -0
  95. package/dist/store-conformance.js +876 -0
  96. package/dist/store-conformance.js.map +1 -0
  97. package/dist/store-errors.d.ts +49 -0
  98. package/dist/store-errors.js +48 -0
  99. package/dist/store-errors.js.map +1 -0
  100. package/dist/store-types.d.ts +331 -0
  101. package/dist/store-types.js +14 -0
  102. package/dist/store-types.js.map +1 -0
  103. package/dist/store-validation.d.ts +63 -0
  104. package/dist/store-validation.js +164 -0
  105. package/dist/store-validation.js.map +1 -0
  106. package/dist/stream-hub.d.ts +151 -0
  107. package/dist/stream-hub.js +402 -0
  108. package/dist/stream-hub.js.map +1 -0
  109. package/dist/testing.d.ts +25 -0
  110. package/dist/testing.js +19 -0
  111. package/dist/testing.js.map +1 -0
  112. package/dist/types.d.ts +221 -0
  113. package/dist/types.js +2 -0
  114. package/dist/types.js.map +1 -0
  115. package/package.json +45 -0
@@ -0,0 +1,632 @@
1
+ import { neutralJsonContract } from '@victframework/sdk';
2
+ function countEvent(events, type) {
3
+ return events.filter((event) => event.type === type).length;
4
+ }
5
+ function countTimeoutTimerEvents(events) {
6
+ return events.filter((event) => event.type === 'timer.scheduled' &&
7
+ event.kind === 'wait-timeout').length;
8
+ }
9
+ export function runOrchestrationRemediationSuite(runner, expect, factory) {
10
+ // The remediation suite is real-time driven (polling loops, real
11
+ // deadlines, live SQLite reopen). The tests get an explicit HARNESS
12
+ // budget so a loaded machine's scheduler latency cannot exceed the
13
+ // default per-test timeout — the assertions themselves are unchanged
14
+ // (no timing assertion of the system under test is weakened; a genuinely
15
+ // broken behavior still fails, only later).
16
+ const t = (name, fn) => {
17
+ void runner.test(name, fn, 20_000);
18
+ };
19
+ const label = (name) => `[${factory.name}] ${name}`;
20
+ t(label('HIGH-1: two sequential waits on one lineage each wait for their own signal'), async () => {
21
+ const handle = await factory.create();
22
+ try {
23
+ const { runtime, orchestration } = handle.stores;
24
+ let midCalls = 0;
25
+ let doneCalls = 0;
26
+ runtime
27
+ .registerCapability({
28
+ id: 'r-seed',
29
+ revision: '1',
30
+ effect: 'pure',
31
+ input: neutralJsonContract,
32
+ output: neutralJsonContract,
33
+ invoke: (i) => i,
34
+ })
35
+ .registerCapability({
36
+ id: 'r-mid',
37
+ revision: '1',
38
+ effect: 'pure',
39
+ input: neutralJsonContract,
40
+ output: neutralJsonContract,
41
+ invoke: (i) => {
42
+ midCalls += 1;
43
+ return i;
44
+ },
45
+ })
46
+ .registerCapability({
47
+ id: 'r-done',
48
+ revision: '1',
49
+ effect: 'pure',
50
+ input: neutralJsonContract,
51
+ output: neutralJsonContract,
52
+ invoke: (i) => {
53
+ doneCalls += 1;
54
+ return i;
55
+ },
56
+ });
57
+ const activated = await runtime.activate({
58
+ id: 'remediation-two-waits',
59
+ entry: 's',
60
+ nodes: [
61
+ { id: 's', capability: 'r-seed' },
62
+ { id: 'w1', kind: 'wait', wait: { kind: 'signal', name: 'first' } },
63
+ { id: 'm', capability: 'r-mid' },
64
+ { id: 'w2', kind: 'wait', wait: { kind: 'signal', name: 'second' } },
65
+ { id: 'd', capability: 'r-done' },
66
+ ],
67
+ edges: [
68
+ { from: 's', to: 'w1', kind: 'success' },
69
+ { from: 'w1', to: 'm', kind: 'success' },
70
+ { from: 'm', to: 'w2', kind: 'success' },
71
+ { from: 'w2', to: 'd', kind: 'success' },
72
+ ],
73
+ });
74
+ expect(activated.ok).toBeTruthy();
75
+ // (1) The run reaches the first signal wait.
76
+ const parked = await runtime.run('seed');
77
+ expect(parked.status).toBe('waiting');
78
+ expect(parked.waits?.length).toBe(1);
79
+ const firstWaitId = parked.waits?.[0]?.waitId;
80
+ // (2) The first signal resolves only that wait; execution advances
81
+ // to and parks at the second wait; the run stays non-terminal.
82
+ const signal1 = await runtime.signal({
83
+ runId: parked.runId,
84
+ waitId: firstWaitId,
85
+ signalId: 'sig-first-1',
86
+ signalName: 'first',
87
+ payload: 'p1',
88
+ });
89
+ expect(signal1.ok).toBeTruthy();
90
+ expect(signal1.ok ? signal1.status : '').toBe('accepted');
91
+ const afterFirst = await runtime.resumeRun(parked.runId);
92
+ expect(afterFirst.status).toBe('waiting');
93
+ expect(afterFirst.waits?.length).toBe(1);
94
+ const secondWaitId = afterFirst.waits?.[0]?.waitId;
95
+ expect(secondWaitId === undefined || secondWaitId !== firstWaitId).toBeTruthy();
96
+ expect(afterFirst.waits?.[0]?.signalName ?? '').toBe('second');
97
+ expect(midCalls).toBe(1);
98
+ expect(doneCalls).toBe(0);
99
+ // Durable facts after the first wake: the first wait is resolved,
100
+ // the second is open.
101
+ const waits = await orchestration.listWaits(parked.runId);
102
+ expect(waits.filter((wait) => wait.status === 'resolved').length).toBe(1);
103
+ expect(waits.filter((wait) => wait.status === 'open').length).toBe(1);
104
+ // (6) Replaying the first signal can NEVER resolve the second wait.
105
+ const replay = await runtime.signal({
106
+ runId: parked.runId,
107
+ waitId: firstWaitId,
108
+ signalId: 'sig-first-1',
109
+ signalName: 'first',
110
+ payload: 'p1',
111
+ });
112
+ expect(replay.ok).toBeTruthy();
113
+ expect(replay.ok ? replay.status : '').toBe('duplicate');
114
+ const replayNewId = await runtime.signal({
115
+ runId: parked.runId,
116
+ waitId: firstWaitId,
117
+ signalId: 'sig-first-2',
118
+ signalName: 'first',
119
+ payload: 'p1',
120
+ });
121
+ expect(replayNewId.ok).toBeTruthy();
122
+ expect(replayNewId.ok ? replayNewId.status : '').toBe('already_resolved');
123
+ const stillWaiting = await orchestration.getOrchestrationRun(parked.runId);
124
+ expect(stillWaiting?.status).toBe('waiting');
125
+ expect(doneCalls).toBe(0);
126
+ // (7) Only the second declared signal permits completion.
127
+ const signal2 = await runtime.signal({
128
+ runId: parked.runId,
129
+ waitId: secondWaitId,
130
+ signalId: 'sig-second-1',
131
+ signalName: 'second',
132
+ payload: 'p2',
133
+ });
134
+ expect(signal2.ok).toBeTruthy();
135
+ expect(signal2.ok ? signal2.status : '').toBe('accepted');
136
+ const final = await runtime.resumeRun(parked.runId);
137
+ expect(final.status).toBe('completed');
138
+ // (8) Event, receipt, and continuation counts remain exactly once.
139
+ const events = await orchestration.listOrchestrationEvents(parked.runId);
140
+ expect(countEvent(events, 'signal.received')).toBe(2);
141
+ expect(countEvent(events, 'run.waiting')).toBe(2);
142
+ expect(countEvent(events, 'run.resumed')).toBe(2);
143
+ expect(countEvent(events, 'run.completed')).toBe(1);
144
+ // s(1) + w1(park+wake: 2) + m(1) + w2(park+wake: 2) + d(1)
145
+ expect(countEvent(events, 'node.completed')).toBe(7);
146
+ const receipts = await orchestration.listSignalReceipts(parked.runId);
147
+ expect(receipts.length).toBe(2);
148
+ expect(midCalls).toBe(1);
149
+ expect(doneCalls).toBe(1);
150
+ // (9) SQLite close/reopen between the waits preserves the behavior
151
+ // (covered by the reopen variant below); here we additionally verify
152
+ // the completed run is stable across reopen.
153
+ const reopened = await handle.reopen();
154
+ const stable = await reopened.orchestration.getOrchestrationRun(parked.runId);
155
+ expect(stable?.status).toBe('completed');
156
+ const reopenedEvents = await reopened.orchestration.listOrchestrationEvents(parked.runId);
157
+ expect(reopenedEvents.length).toBe(events.length);
158
+ }
159
+ finally {
160
+ await handle.dispose();
161
+ }
162
+ });
163
+ t(label('HIGH-1: SQLite close/reopen between two sequential waits preserves wait sequencing'), async () => {
164
+ const handle = await factory.create();
165
+ try {
166
+ const { runtime } = handle.stores;
167
+ runtime
168
+ .registerCapability({
169
+ id: 'r-seed',
170
+ revision: '1',
171
+ effect: 'pure',
172
+ input: neutralJsonContract,
173
+ output: neutralJsonContract,
174
+ invoke: (i) => i,
175
+ })
176
+ .registerCapability({
177
+ id: 'r-mid',
178
+ revision: '1',
179
+ effect: 'pure',
180
+ input: neutralJsonContract,
181
+ output: neutralJsonContract,
182
+ invoke: (i) => i,
183
+ })
184
+ .registerCapability({
185
+ id: 'r-done',
186
+ revision: '1',
187
+ effect: 'pure',
188
+ input: neutralJsonContract,
189
+ output: neutralJsonContract,
190
+ invoke: (i) => i,
191
+ });
192
+ const activated = await runtime.activate({
193
+ id: 'remediation-two-waits-reopen',
194
+ entry: 's',
195
+ nodes: [
196
+ { id: 's', capability: 'r-seed' },
197
+ { id: 'w1', kind: 'wait', wait: { kind: 'signal', name: 'first' } },
198
+ { id: 'm', capability: 'r-mid' },
199
+ { id: 'w2', kind: 'wait', wait: { kind: 'signal', name: 'second' } },
200
+ { id: 'd', capability: 'r-done' },
201
+ ],
202
+ edges: [
203
+ { from: 's', to: 'w1', kind: 'success' },
204
+ { from: 'w1', to: 'm', kind: 'success' },
205
+ { from: 'm', to: 'w2', kind: 'success' },
206
+ { from: 'w2', to: 'd', kind: 'success' },
207
+ ],
208
+ });
209
+ expect(activated.ok).toBeTruthy();
210
+ const parked = await runtime.run('seed');
211
+ expect(parked.status).toBe('waiting');
212
+ const firstWaitId = parked.waits?.[0]?.waitId;
213
+ // Close and reopen BEFORE the first signal. The reopened runtime
214
+ // re-registers the identical pinned artifacts (revision-pinned
215
+ // registry lookups resolve the exact stored activation).
216
+ const reopened = await handle.reopen();
217
+ reopened.runtime
218
+ .registerCapability({
219
+ id: 'r-seed',
220
+ revision: '1',
221
+ effect: 'pure',
222
+ input: neutralJsonContract,
223
+ output: neutralJsonContract,
224
+ invoke: (i) => i,
225
+ })
226
+ .registerCapability({
227
+ id: 'r-mid',
228
+ revision: '1',
229
+ effect: 'pure',
230
+ input: neutralJsonContract,
231
+ output: neutralJsonContract,
232
+ invoke: (i) => i,
233
+ })
234
+ .registerCapability({
235
+ id: 'r-done',
236
+ revision: '1',
237
+ effect: 'pure',
238
+ input: neutralJsonContract,
239
+ output: neutralJsonContract,
240
+ invoke: (i) => i,
241
+ });
242
+ const signal1 = await reopened.runtime.signal({
243
+ runId: parked.runId,
244
+ waitId: firstWaitId,
245
+ signalId: 'sig-first-1',
246
+ signalName: 'first',
247
+ payload: 'p1',
248
+ });
249
+ expect(signal1.ok).toBeTruthy();
250
+ expect(signal1.ok ? signal1.status : '').toBe('accepted');
251
+ const afterFirst = await reopened.runtime.resumeRun(parked.runId);
252
+ expect(afterFirst.status).toBe('waiting');
253
+ const secondWaitId = afterFirst.waits?.[0]?.waitId;
254
+ expect(secondWaitId === undefined || secondWaitId !== firstWaitId).toBeTruthy();
255
+ expect(afterFirst.waits?.[0]?.signalName ?? '').toBe('second');
256
+ // The replayed first signal still cannot resolve the second wait
257
+ // after restart.
258
+ const replay = await reopened.runtime.signal({
259
+ runId: parked.runId,
260
+ waitId: firstWaitId,
261
+ signalId: 'sig-first-1',
262
+ signalName: 'first',
263
+ payload: 'p1',
264
+ });
265
+ expect(replay.ok).toBeTruthy();
266
+ expect(replay.ok ? replay.status : '').toBe('duplicate');
267
+ const signal2 = await reopened.runtime.signal({
268
+ runId: parked.runId,
269
+ waitId: secondWaitId,
270
+ signalId: 'sig-second-1',
271
+ signalName: 'second',
272
+ payload: 'p2',
273
+ });
274
+ expect(signal2.ok).toBeTruthy();
275
+ const final = await reopened.runtime.resumeRun(parked.runId);
276
+ expect(final.status).toBe('completed');
277
+ const events = await reopened.orchestration.listOrchestrationEvents(parked.runId);
278
+ expect(countEvent(events, 'signal.received')).toBe(2);
279
+ expect(countEvent(events, 'run.completed')).toBe(1);
280
+ }
281
+ finally {
282
+ await handle.dispose();
283
+ }
284
+ });
285
+ t(label('HIGH-2: a plain signal wait has no timeout timer and survives pumping and far clock advances'), async () => {
286
+ const { createManualOrchestrationClock } = await import('./orchestration-race-conformance.js');
287
+ const manual = createManualOrchestrationClock();
288
+ const handle = await factory.create(manual);
289
+ try {
290
+ const { runtime, orchestration } = handle.stores;
291
+ let doneCalls = 0;
292
+ runtime
293
+ .registerCapability({
294
+ id: 'r-seed',
295
+ revision: '1',
296
+ effect: 'pure',
297
+ input: neutralJsonContract,
298
+ output: neutralJsonContract,
299
+ invoke: (i) => i,
300
+ })
301
+ .registerCapability({
302
+ id: 'r-done',
303
+ revision: '1',
304
+ effect: 'pure',
305
+ input: neutralJsonContract,
306
+ output: neutralJsonContract,
307
+ invoke: (i) => {
308
+ doneCalls += 1;
309
+ return i;
310
+ },
311
+ });
312
+ const activated = await runtime.activate({
313
+ id: 'remediation-plain-wait',
314
+ entry: 's',
315
+ nodes: [
316
+ { id: 's', capability: 'r-seed' },
317
+ { id: 'w', kind: 'wait', wait: { kind: 'signal', name: 'go' } },
318
+ { id: 'd', capability: 'r-done' },
319
+ ],
320
+ edges: [
321
+ { from: 's', to: 'w', kind: 'success' },
322
+ { from: 'w', to: 'd', kind: 'success' },
323
+ ],
324
+ });
325
+ expect(activated.ok).toBeTruthy();
326
+ // (1) The plain signal wait parks with NO timeout timer of any kind.
327
+ const parked = await runtime.run('seed');
328
+ expect(parked.status).toBe('waiting');
329
+ const events = await orchestration.listOrchestrationEvents(parked.runId);
330
+ expect(countTimeoutTimerEvents(events)).toBe(0);
331
+ const snapshot = await orchestration.getOrchestrationSnapshot(parked.runId);
332
+ expect((snapshot?.timers ?? []).length).toBe(0);
333
+ const waits = await orchestration.listWaits(parked.runId);
334
+ expect(waits.length).toBe(1);
335
+ expect(waits[0]?.timeoutAt).toBeNull();
336
+ // (2) Pumping timers immediately does nothing.
337
+ const pump1 = await runtime.processDueTimers({ runId: parked.runId });
338
+ expect(pump1.fired).toBe(0);
339
+ expect((await orchestration.getOrchestrationRun(parked.runId))?.status).toBe('waiting');
340
+ // (3) Advancing the manual clock far into the future and pumping
341
+ // still does nothing.
342
+ manual.advance(365 * 24 * 60 * 60 * 1000);
343
+ const pump2 = await runtime.processDueTimers({ runId: parked.runId });
344
+ expect(pump2.fired).toBe(0);
345
+ // (4) The run remains parked and recoverable — and (6) close/reopen
346
+ // does not manufacture a timer.
347
+ expect((await orchestration.getOrchestrationRun(parked.runId))?.status).toBe('waiting');
348
+ const reopened = await handle.reopen();
349
+ const reopenedSnapshot = await reopened.orchestration.getOrchestrationSnapshot(parked.runId);
350
+ expect((reopenedSnapshot?.timers ?? []).length).toBe(0);
351
+ const reopenedEvents = await reopened.orchestration.listOrchestrationEvents(parked.runId);
352
+ expect(countTimeoutTimerEvents(reopenedEvents)).toBe(0);
353
+ const pump3 = await reopened.runtime.processDueTimers({ runId: parked.runId });
354
+ expect(pump3.fired).toBe(0);
355
+ expect((await reopened.orchestration.getOrchestrationRun(parked.runId))?.status).toBe('waiting');
356
+ // (5) The proper signal resumes it exactly once (artifacts
357
+ // re-registered identically on the reopened runtime).
358
+ reopened.runtime
359
+ .registerCapability({
360
+ id: 'r-seed',
361
+ revision: '1',
362
+ effect: 'pure',
363
+ input: neutralJsonContract,
364
+ output: neutralJsonContract,
365
+ invoke: (i) => i,
366
+ })
367
+ .registerCapability({
368
+ id: 'r-done',
369
+ revision: '1',
370
+ effect: 'pure',
371
+ input: neutralJsonContract,
372
+ output: neutralJsonContract,
373
+ invoke: (i) => {
374
+ doneCalls += 1;
375
+ return i;
376
+ },
377
+ });
378
+ const signal = await reopened.runtime.signal({
379
+ runId: parked.runId,
380
+ waitId: parked.waits?.[0]?.waitId,
381
+ signalId: 'sig-go-1',
382
+ signalName: 'go',
383
+ payload: 'resumed',
384
+ });
385
+ expect(signal.ok).toBeTruthy();
386
+ expect(signal.ok ? signal.status : '').toBe('accepted');
387
+ const final = await reopened.runtime.resumeRun(parked.runId);
388
+ expect(final.status).toBe('completed');
389
+ expect(doneCalls).toBe(1);
390
+ const finalEvents = await reopened.orchestration.listOrchestrationEvents(parked.runId);
391
+ expect(countEvent(finalEvents, 'signal.received')).toBe(1);
392
+ expect(countEvent(finalEvents, 'run.resumed')).toBe(1);
393
+ expect(countTimeoutTimerEvents(finalEvents)).toBe(0);
394
+ }
395
+ finally {
396
+ await handle.dispose();
397
+ }
398
+ });
399
+ t(label('HIGH-2: a signal wait with a declared timeout still times out through its timeout edge'), async () => {
400
+ const { createManualOrchestrationClock } = await import('./orchestration-race-conformance.js');
401
+ const manual = createManualOrchestrationClock();
402
+ const handle = await factory.create(manual);
403
+ try {
404
+ const { runtime, orchestration } = handle.stores;
405
+ let doneCalls = 0;
406
+ let fallbackCalls = 0;
407
+ runtime
408
+ .registerCapability({
409
+ id: 'r-seed',
410
+ revision: '1',
411
+ effect: 'pure',
412
+ input: neutralJsonContract,
413
+ output: neutralJsonContract,
414
+ invoke: (i) => i,
415
+ })
416
+ .registerCapability({
417
+ id: 'r-done',
418
+ revision: '1',
419
+ effect: 'pure',
420
+ input: neutralJsonContract,
421
+ output: neutralJsonContract,
422
+ invoke: (i) => {
423
+ doneCalls += 1;
424
+ return i;
425
+ },
426
+ })
427
+ .registerCapability({
428
+ id: 'r-fallback',
429
+ revision: '1',
430
+ effect: 'pure',
431
+ input: neutralJsonContract,
432
+ output: neutralJsonContract,
433
+ invoke: (i) => {
434
+ fallbackCalls += 1;
435
+ return i;
436
+ },
437
+ });
438
+ const activated = await runtime.activate({
439
+ id: 'remediation-timed-wait',
440
+ entry: 's',
441
+ nodes: [
442
+ { id: 's', capability: 'r-seed' },
443
+ {
444
+ id: 'w',
445
+ kind: 'wait',
446
+ wait: { kind: 'signal', name: 'tick', timeoutMs: 20 },
447
+ },
448
+ { id: 'd', capability: 'r-done' },
449
+ { id: 'fb', capability: 'r-fallback' },
450
+ ],
451
+ edges: [
452
+ { from: 's', to: 'w', kind: 'success' },
453
+ { from: 'w', to: 'd', kind: 'success' },
454
+ { from: 'w', to: 'fb', kind: 'timeout' },
455
+ ],
456
+ });
457
+ expect(activated.ok).toBeTruthy();
458
+ const parked = await runtime.run('seed');
459
+ expect(parked.status).toBe('waiting');
460
+ // A DECLARED timeout schedules exactly one wait-timeout timer.
461
+ const events = await orchestration.listOrchestrationEvents(parked.runId);
462
+ expect(countTimeoutTimerEvents(events)).toBe(1);
463
+ // Advancing past the deadline and pumping fires the timeout exactly
464
+ // once and routes through the timeout edge.
465
+ manual.advance(25);
466
+ const pumped = await runtime.processDueTimers({ runId: parked.runId });
467
+ expect(pumped.fired).toBe(1);
468
+ const final = await runtime.resumeRun(parked.runId);
469
+ expect(final.status).toBe('completed');
470
+ expect(fallbackCalls).toBe(1);
471
+ expect(doneCalls).toBe(0);
472
+ const finalEvents = await orchestration.listOrchestrationEvents(parked.runId);
473
+ expect(countEvent(finalEvents, 'timer.fired')).toBe(1);
474
+ expect(countEvent(finalEvents, 'signal.received')).toBe(0);
475
+ }
476
+ finally {
477
+ await handle.dispose();
478
+ }
479
+ });
480
+ t(label('HIGH-3: authorized operator fail resolves a blocked run to failed, atomically and idempotently'), async () => {
481
+ const handle = await factory.create();
482
+ try {
483
+ const stores = handle.stores;
484
+ let invokeCount = 0;
485
+ let release;
486
+ const gate = new Promise((resolve) => {
487
+ release = resolve;
488
+ });
489
+ stores.runtime
490
+ .registerCapability({
491
+ id: 'r-slow-write',
492
+ revision: '1',
493
+ effect: 'write',
494
+ input: neutralJsonContract,
495
+ output: neutralJsonContract,
496
+ invoke: async () => {
497
+ invokeCount += 1;
498
+ await gate;
499
+ return 'applied';
500
+ },
501
+ })
502
+ .registerCapability({
503
+ id: 'r-after',
504
+ revision: '1',
505
+ effect: 'pure',
506
+ input: neutralJsonContract,
507
+ output: neutralJsonContract,
508
+ invoke: (i) => i,
509
+ });
510
+ const activated = await stores.runtime.activate({
511
+ id: 'remediation-operator-fail',
512
+ entry: 'w',
513
+ nodes: [
514
+ { id: 'w', capability: 'r-slow-write', timeoutMs: 20 },
515
+ { id: 'z', capability: 'r-after' },
516
+ ],
517
+ edges: [{ from: 'w', to: 'z', kind: 'success' }],
518
+ });
519
+ expect(activated.ok).toBeTruthy();
520
+ // Drive into the blocked state: the write is in flight when the
521
+ // deadline passes (manual-style deterministic sequence via the
522
+ // invocation barrier; the timeout deadline is real-time here, as in
523
+ // the existing race suite).
524
+ const started = stores.runtime.run('seed');
525
+ await new Promise((resolve) => {
526
+ const check = setInterval(() => {
527
+ if (invokeCount === 1) {
528
+ clearInterval(check);
529
+ resolve();
530
+ }
531
+ }, 5);
532
+ });
533
+ await new Promise((resolve) => setTimeout(resolve, 40));
534
+ release?.();
535
+ const blocked = await started;
536
+ expect(blocked.status).toBe('blocked');
537
+ expect(invokeCount).toBe(1);
538
+ // Public surface denied by default.
539
+ const denied = await stores.runtime.resolveBlocked({
540
+ runId: blocked.runId,
541
+ resolutionId: 'res-fail-1',
542
+ action: 'fail',
543
+ reasonCode: 'operator_request',
544
+ failCode: 'VICT_ORCH_OPERATOR_FAILED',
545
+ });
546
+ expect(denied.ok).toBe(false);
547
+ expect(denied.ok ? '' : denied.code).toBe('VICT_ORCH_OPERATOR_DENIED');
548
+ // Authorized surface: same stores, explicit operator authorization.
549
+ const operator = await handle.createOperatorRuntime();
550
+ operator
551
+ .registerCapability({
552
+ id: 'r-slow-write',
553
+ revision: '1',
554
+ effect: 'write',
555
+ input: neutralJsonContract,
556
+ output: neutralJsonContract,
557
+ invoke: async () => 'applied',
558
+ })
559
+ .registerCapability({
560
+ id: 'r-after',
561
+ revision: '1',
562
+ effect: 'pure',
563
+ input: neutralJsonContract,
564
+ output: neutralJsonContract,
565
+ invoke: (i) => i,
566
+ });
567
+ // Stale resolution (fresh ID, wrong expected revision) is rejected
568
+ // while the run is still blocked.
569
+ const runBefore = await stores.orchestration.getOrchestrationRun(blocked.runId);
570
+ const stale = await operator.resolveBlocked({
571
+ runId: blocked.runId,
572
+ resolutionId: 'res-fail-stale',
573
+ action: 'fail',
574
+ reasonCode: 'operator_request',
575
+ failCode: 'VICT_ORCH_OPERATOR_FAILED',
576
+ expectedRunRevision: (runBefore?.recordRevision ?? 1) + 100,
577
+ });
578
+ expect(stale.ok).toBe(false);
579
+ expect(stale.ok ? '' : stale.code).toBe('VICT_ORCH_STALE_REVISION');
580
+ const accepted = await operator.resolveBlocked({
581
+ runId: blocked.runId,
582
+ resolutionId: 'res-fail-1',
583
+ action: 'fail',
584
+ reasonCode: 'operator_request',
585
+ failCode: 'VICT_ORCH_OPERATOR_FAILED',
586
+ expectedRunRevision: runBefore?.recordRevision,
587
+ });
588
+ expect(accepted.ok).toBeTruthy();
589
+ expect(accepted.ok ? accepted.status : '').toBe('accepted');
590
+ expect(accepted.ok ? accepted.runStatus : '').toBe('failed');
591
+ // Atomicity and exactly-once events: one operator record, one
592
+ // terminal failure, no downstream continuation.
593
+ const events = await stores.orchestration.listOrchestrationEvents(blocked.runId);
594
+ expect(countEvent(events, 'operator.intervened')).toBe(1);
595
+ expect(countEvent(events, 'run.failed')).toBe(1);
596
+ expect(countEvent(events, 'node.started')).toBe(1);
597
+ const tokens = (await stores.orchestration.getOrchestrationSnapshot(blocked.runId))?.tokens;
598
+ expect((tokens ?? []).some((token) => token.status === 'ready')).toBe(false);
599
+ // Idempotent for a repeated identical resolution.
600
+ const duplicate = await operator.resolveBlocked({
601
+ runId: blocked.runId,
602
+ resolutionId: 'res-fail-1',
603
+ action: 'fail',
604
+ reasonCode: 'operator_request',
605
+ failCode: 'VICT_ORCH_OPERATOR_FAILED',
606
+ });
607
+ expect(duplicate.ok).toBeTruthy();
608
+ expect(duplicate.ok ? duplicate.status : '').toBe('duplicate');
609
+ const eventsAfterDuplicate = await stores.orchestration.listOrchestrationEvents(blocked.runId);
610
+ expect(eventsAfterDuplicate.length).toBe(events.length);
611
+ // Conflicting resolution (same ID, different action) is rejected.
612
+ const conflict = await operator.resolveBlocked({
613
+ runId: blocked.runId,
614
+ resolutionId: 'res-fail-1',
615
+ action: 'cancel',
616
+ reasonCode: 'operator_request',
617
+ });
618
+ expect(conflict.ok).toBe(false);
619
+ expect(conflict.ok ? '' : conflict.code).toBe('VICT_ORCH_OPERATOR_CONFLICT');
620
+ // Terminal state survives close/reopen and stays identical.
621
+ const reopened = await handle.reopen();
622
+ expect((await reopened.orchestration.getOrchestrationRun(blocked.runId))?.status).toBe('failed');
623
+ const reopenedEvents = await reopened.orchestration.listOrchestrationEvents(blocked.runId);
624
+ expect(countEvent(reopenedEvents, 'operator.intervened')).toBe(1);
625
+ expect(countEvent(reopenedEvents, 'run.failed')).toBe(1);
626
+ }
627
+ finally {
628
+ await handle.dispose();
629
+ }
630
+ });
631
+ }
632
+ //# sourceMappingURL=orchestration-remediation-conformance.js.map