@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.
- package/dist/agent-governance.d.ts +285 -0
- package/dist/agent-governance.js +438 -0
- package/dist/agent-governance.js.map +1 -0
- package/dist/agent-registry.d.ts +103 -0
- package/dist/agent-registry.js +758 -0
- package/dist/agent-registry.js.map +1 -0
- package/dist/agent-types.d.ts +433 -0
- package/dist/agent-types.js +480 -0
- package/dist/agent-types.js.map +1 -0
- package/dist/authority.d.ts +83 -0
- package/dist/authority.js +170 -0
- package/dist/authority.js.map +1 -0
- package/dist/boundary-conformance.d.ts +51 -0
- package/dist/boundary-conformance.js +364 -0
- package/dist/boundary-conformance.js.map +1 -0
- package/dist/control-conformance.d.ts +68 -0
- package/dist/control-conformance.js +1131 -0
- package/dist/control-conformance.js.map +1 -0
- package/dist/control-in-memory.d.ts +257 -0
- package/dist/control-in-memory.js +886 -0
- package/dist/control-in-memory.js.map +1 -0
- package/dist/control-types.d.ts +1017 -0
- package/dist/control-types.js +779 -0
- package/dist/control-types.js.map +1 -0
- package/dist/durable-run.d.ts +73 -0
- package/dist/durable-run.js +262 -0
- package/dist/durable-run.js.map +1 -0
- package/dist/effect-policy.d.ts +22 -0
- package/dist/effect-policy.js +45 -0
- package/dist/effect-policy.js.map +1 -0
- package/dist/errors.d.ts +36 -0
- package/dist/errors.js +71 -0
- package/dist/errors.js.map +1 -0
- package/dist/in-memory-stores.d.ts +16 -0
- package/dist/in-memory-stores.js +522 -0
- package/dist/in-memory-stores.js.map +1 -0
- package/dist/index.d.ts +41 -0
- package/dist/index.js +26 -0
- package/dist/index.js.map +1 -0
- package/dist/operator-config.d.ts +141 -0
- package/dist/operator-config.js +289 -0
- package/dist/operator-config.js.map +1 -0
- package/dist/orchestration-activation.d.ts +116 -0
- package/dist/orchestration-activation.js +223 -0
- package/dist/orchestration-activation.js.map +1 -0
- package/dist/orchestration-canary-conformance.d.ts +10 -0
- package/dist/orchestration-canary-conformance.js +726 -0
- package/dist/orchestration-canary-conformance.js.map +1 -0
- package/dist/orchestration-commands.d.ts +47 -0
- package/dist/orchestration-commands.js +538 -0
- package/dist/orchestration-commands.js.map +1 -0
- package/dist/orchestration-conformance.d.ts +59 -0
- package/dist/orchestration-conformance.js +594 -0
- package/dist/orchestration-conformance.js.map +1 -0
- package/dist/orchestration-driver-types.d.ts +163 -0
- package/dist/orchestration-driver-types.js +21 -0
- package/dist/orchestration-driver-types.js.map +1 -0
- package/dist/orchestration-driver.d.ts +48 -0
- package/dist/orchestration-driver.js +1030 -0
- package/dist/orchestration-driver.js.map +1 -0
- package/dist/orchestration-in-memory.d.ts +18 -0
- package/dist/orchestration-in-memory.js +1313 -0
- package/dist/orchestration-in-memory.js.map +1 -0
- package/dist/orchestration-join-conformance.d.ts +20 -0
- package/dist/orchestration-join-conformance.js +542 -0
- package/dist/orchestration-join-conformance.js.map +1 -0
- package/dist/orchestration-plan.d.ts +52 -0
- package/dist/orchestration-plan.js +265 -0
- package/dist/orchestration-plan.js.map +1 -0
- package/dist/orchestration-race-conformance.d.ts +35 -0
- package/dist/orchestration-race-conformance.js +1265 -0
- package/dist/orchestration-race-conformance.js.map +1 -0
- package/dist/orchestration-remediation-conformance.d.ts +54 -0
- package/dist/orchestration-remediation-conformance.js +632 -0
- package/dist/orchestration-remediation-conformance.js.map +1 -0
- package/dist/orchestration-store-types.d.ts +568 -0
- package/dist/orchestration-store-types.js +2 -0
- package/dist/orchestration-store-types.js.map +1 -0
- package/dist/pack-conformance.d.ts +59 -0
- package/dist/pack-conformance.js +222 -0
- package/dist/pack-conformance.js.map +1 -0
- package/dist/pack-install.d.ts +44 -0
- package/dist/pack-install.js +126 -0
- package/dist/pack-install.js.map +1 -0
- package/dist/registry.d.ts +107 -0
- package/dist/registry.js +481 -0
- package/dist/registry.js.map +1 -0
- package/dist/runtime.d.ts +158 -0
- package/dist/runtime.js +1341 -0
- package/dist/runtime.js.map +1 -0
- package/dist/serialization.d.ts +26 -0
- package/dist/serialization.js +162 -0
- package/dist/serialization.js.map +1 -0
- package/dist/store-conformance.d.ts +45 -0
- package/dist/store-conformance.js +876 -0
- package/dist/store-conformance.js.map +1 -0
- package/dist/store-errors.d.ts +49 -0
- package/dist/store-errors.js +48 -0
- package/dist/store-errors.js.map +1 -0
- package/dist/store-types.d.ts +331 -0
- package/dist/store-types.js +14 -0
- package/dist/store-types.js.map +1 -0
- package/dist/store-validation.d.ts +63 -0
- package/dist/store-validation.js +164 -0
- package/dist/store-validation.js.map +1 -0
- package/dist/stream-hub.d.ts +151 -0
- package/dist/stream-hub.js +402 -0
- package/dist/stream-hub.js.map +1 -0
- package/dist/testing.d.ts +25 -0
- package/dist/testing.js +19 -0
- package/dist/testing.js.map +1 -0
- package/dist/types.d.ts +221 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- 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
|