@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,876 @@
1
+ import { canonicalSemanticForm, computeActivationVersion, computeCapabilitySetVersion, computeGraphVersion, } from '@victframework/kernel';
2
+ import { ACTIVATION_MANIFEST_SCHEMA, RUN_EVENT_SCHEMA } from './store-types.js';
3
+ import { toCanonicalJson } from './serialization.js';
4
+ /**
5
+ * Build a fixture activation manifest whose identities are REAL: every
6
+ * content-derived identity is computed by the kernel's canonical identity
7
+ * functions, exactly as the runtime would produce it. Stores recompute and
8
+ * reject hand-made identities, so fixtures must be genuine.
9
+ */
10
+ function fixtureManifest(graphId, capabilityRevision = '1') {
11
+ const graph = {
12
+ schema: 'vict.graph@1',
13
+ id: graphId,
14
+ entry: 'n1',
15
+ nodes: [{ id: 'n1', capability: 'cap.a', input: null, output: null }],
16
+ edges: [],
17
+ };
18
+ const bindings = [
19
+ {
20
+ capability: 'cap.a',
21
+ revision: capabilityRevision,
22
+ effect: 'pure',
23
+ input: null,
24
+ output: null,
25
+ },
26
+ ];
27
+ const graphVersion = computeGraphVersion(graph);
28
+ const capabilitySetVersion = computeCapabilitySetVersion(bindings);
29
+ const activationVersion = computeActivationVersion(graphVersion, capabilitySetVersion);
30
+ return {
31
+ manifestSchema: ACTIVATION_MANIFEST_SCHEMA,
32
+ graphId,
33
+ graph: canonicalSemanticForm(graph),
34
+ graphVersion,
35
+ capabilitySetVersion,
36
+ activationVersion,
37
+ bindings,
38
+ contracts: [],
39
+ };
40
+ }
41
+ const GRAPH_A = fixtureManifest('conf-graph-a');
42
+ /** Same graph as GRAPH_A, different capability revision: a valid successor activation. */
43
+ const GRAPH_A_V2 = fixtureManifest('conf-graph-a', '2');
44
+ const GRAPH_B = fixtureManifest('conf-graph-b');
45
+ function makeEvent(seq, type, extra = {}, runId = 'conf-run-1') {
46
+ return {
47
+ seq,
48
+ runId,
49
+ graphId: GRAPH_A.graphId,
50
+ graphVersion: GRAPH_A.graphVersion,
51
+ capabilitySetVersion: GRAPH_A.capabilitySetVersion,
52
+ activationVersion: GRAPH_A.activationVersion,
53
+ timestamp: 1_000 + seq,
54
+ type,
55
+ ...extra,
56
+ };
57
+ }
58
+ function createCommand(overrides = {}) {
59
+ const command = {
60
+ runId: 'conf-run-1',
61
+ graphId: GRAPH_A.graphId,
62
+ graphVersion: GRAPH_A.graphVersion,
63
+ capabilitySetVersion: GRAPH_A.capabilitySetVersion,
64
+ activationVersion: GRAPH_A.activationVersion,
65
+ mode: 'normal',
66
+ retention: 'summary',
67
+ currentNodeId: 'n1',
68
+ steps: 0,
69
+ events: [makeEvent(0, 'run.started')],
70
+ timestamp: 1_000,
71
+ ...overrides,
72
+ };
73
+ // Events always belong to the run under test, whatever overrides change.
74
+ return {
75
+ ...command,
76
+ events: command.events.map((event) => ({ ...event, runId: command.runId })),
77
+ };
78
+ }
79
+ function transitionCommand(overrides = {}) {
80
+ const command = {
81
+ runId: 'conf-run-1',
82
+ expectedRecordRevision: 1,
83
+ expectedNextEventSeq: 1,
84
+ next: { currentNodeId: 'n1' },
85
+ events: [],
86
+ timestamp: 1_100,
87
+ ...overrides,
88
+ };
89
+ return {
90
+ ...command,
91
+ events: command.events.map((event) => ({ ...event, runId: command.runId })),
92
+ };
93
+ }
94
+ const SAFE_ERROR = {
95
+ code: 'VICT_RUNTIME_CAPABILITY_THREW',
96
+ message: 'Capability threw during invocation; the thrown message is not retained.',
97
+ details: { errorName: 'Error', errorId: 'err_conf-1' },
98
+ };
99
+ const SECRET_CANARY = 'conf-canary-9d1c2-secret';
100
+ function rejectsWithCode(expect, promise, code) {
101
+ return expect(promise).rejects.toMatchObject({ code });
102
+ }
103
+ /** Publish the fixture activation: runs must reference a published activation (RUN-001). */
104
+ async function publishFixture(s) {
105
+ await s.catalog.publish({ manifest: GRAPH_A, canonicalManifest: toCanonicalJson(GRAPH_A) });
106
+ }
107
+ /** Build and run the conformance suite against one backend factory. */
108
+ export function runStoreConformanceSuite(runner, factory) {
109
+ const { test, expect } = runner;
110
+ test(`[${factory.name}] activation publish/read round-trips immutably`, async () => {
111
+ const s = await factory.create();
112
+ try {
113
+ const result = await s.catalog.publish({
114
+ manifest: GRAPH_A,
115
+ canonicalManifest: toCanonicalJson(GRAPH_A),
116
+ });
117
+ expect(result.created).toBe(true);
118
+ const stored = await s.catalog.get(GRAPH_A.activationVersion);
119
+ expect(stored).toBeDefined();
120
+ expect(stored?.canonicalManifest).toBe(toCanonicalJson(GRAPH_A));
121
+ expect(stored?.graphId).toBe(GRAPH_A.graphId);
122
+ expect(Object.isFrozen(stored)).toBe(true);
123
+ expect((await s.catalog.list()).length).toBe(1);
124
+ }
125
+ finally {
126
+ await s.dispose();
127
+ }
128
+ });
129
+ test(`[${factory.name}] equivalent republish is idempotent`, async () => {
130
+ const s = await factory.create();
131
+ try {
132
+ const first = await s.catalog.publish({
133
+ manifest: GRAPH_A,
134
+ canonicalManifest: toCanonicalJson(GRAPH_A),
135
+ });
136
+ const second = await s.catalog.publish({
137
+ manifest: GRAPH_A,
138
+ canonicalManifest: toCanonicalJson(GRAPH_A),
139
+ });
140
+ expect(first.created).toBe(true);
141
+ expect(second.created).toBe(false);
142
+ expect((await s.catalog.list()).length).toBe(1);
143
+ }
144
+ finally {
145
+ await s.dispose();
146
+ }
147
+ });
148
+ test(`[${factory.name}] same version with different content is rejected as a collision`, async () => {
149
+ const s = await factory.create();
150
+ try {
151
+ await s.catalog.publish({ manifest: GRAPH_A, canonicalManifest: toCanonicalJson(GRAPH_A) });
152
+ const tamperedBinding = GRAPH_A.bindings[0];
153
+ const tampered = {
154
+ ...GRAPH_A,
155
+ bindings: [
156
+ {
157
+ capability: tamperedBinding.capability,
158
+ revision: '2',
159
+ effect: tamperedBinding.effect,
160
+ input: tamperedBinding.input,
161
+ output: tamperedBinding.output,
162
+ },
163
+ ],
164
+ };
165
+ await rejectsWithCode(expect, s.catalog.publish({
166
+ manifest: tampered,
167
+ canonicalManifest: toCanonicalJson(tampered),
168
+ }), 'VICT_STORE_ACTIVATION_COLLISION');
169
+ }
170
+ finally {
171
+ await s.dispose();
172
+ }
173
+ });
174
+ test(`[${factory.name}] selection uses optimistic concurrency`, async () => {
175
+ const s = await factory.create();
176
+ try {
177
+ await s.catalog.publish({ manifest: GRAPH_A, canonicalManifest: toCanonicalJson(GRAPH_A) });
178
+ await s.catalog.publish({
179
+ manifest: GRAPH_A_V2,
180
+ canonicalManifest: toCanonicalJson(GRAPH_A_V2),
181
+ });
182
+ // A valid activation of ANOTHER graph, for the cross-graph rejection below.
183
+ await s.catalog.publish({ manifest: GRAPH_B, canonicalManifest: toCanonicalJson(GRAPH_B) });
184
+ const first = await s.catalog.select({
185
+ graphId: GRAPH_A.graphId,
186
+ activationVersion: GRAPH_A.activationVersion,
187
+ });
188
+ expect(first.selectionRevision).toBe(1);
189
+ // Select the successor activation of the SAME graph.
190
+ const second = await s.catalog.select({
191
+ graphId: GRAPH_A.graphId,
192
+ activationVersion: GRAPH_A_V2.activationVersion,
193
+ });
194
+ expect(second.selectionRevision).toBe(2);
195
+ // An activation of a DIFFERENT graph cannot be selected for this one.
196
+ await rejectsWithCode(expect, s.catalog.select({
197
+ graphId: GRAPH_A.graphId,
198
+ activationVersion: GRAPH_B.activationVersion,
199
+ }), 'VICT_STORE_ACTIVATION_MISMATCH');
200
+ // Stale writer loses.
201
+ await rejectsWithCode(expect, s.catalog.select({
202
+ graphId: GRAPH_A.graphId,
203
+ activationVersion: GRAPH_A.activationVersion,
204
+ expectedSelectionRevision: 1,
205
+ }), 'VICT_STORE_SELECTION_CONFLICT');
206
+ // Current writer wins.
207
+ const third = await s.catalog.select({
208
+ graphId: GRAPH_A.graphId,
209
+ activationVersion: GRAPH_A.activationVersion,
210
+ expectedSelectionRevision: 2,
211
+ });
212
+ expect(third.selectionRevision).toBe(3);
213
+ expect((await s.catalog.getSelected(GRAPH_A.graphId))?.activationVersion).toBe(GRAPH_A.activationVersion);
214
+ }
215
+ finally {
216
+ await s.dispose();
217
+ }
218
+ });
219
+ test(`[${factory.name}] publishAndSelect is atomic and idempotent for equivalent content`, async () => {
220
+ const s = await factory.create();
221
+ try {
222
+ const first = await s.catalog.publishAndSelect({
223
+ publish: { manifest: GRAPH_A, canonicalManifest: toCanonicalJson(GRAPH_A) },
224
+ select: { graphId: GRAPH_A.graphId },
225
+ });
226
+ expect(first.created).toBe(true);
227
+ const second = await s.catalog.publishAndSelect({
228
+ publish: { manifest: GRAPH_A, canonicalManifest: toCanonicalJson(GRAPH_A) },
229
+ select: { graphId: GRAPH_A.graphId },
230
+ });
231
+ expect(second.created).toBe(false);
232
+ expect(second.selection.selectionRevision).toBe(2);
233
+ expect((await s.catalog.list()).length).toBe(1);
234
+ }
235
+ finally {
236
+ await s.dispose();
237
+ }
238
+ });
239
+ test(`[${factory.name}] createRun plus initial event is atomic`, async () => {
240
+ const s = await factory.create();
241
+ try {
242
+ await s.catalog.publish({ manifest: GRAPH_A, canonicalManifest: toCanonicalJson(GRAPH_A) });
243
+ const run = await s.execution.createRun(createCommand());
244
+ expect(run.status).toBe('running');
245
+ expect(run.recordRevision).toBe(1);
246
+ const events = await s.execution.listEvents('conf-run-1');
247
+ expect(events.length).toBe(1);
248
+ expect(events[0]?.type).toBe('run.started');
249
+ // Duplicate run id is a conflict, not an overwrite.
250
+ await rejectsWithCode(expect, s.execution.createRun(createCommand()), 'VICT_STORE_RUN_CONFLICT');
251
+ }
252
+ finally {
253
+ await s.dispose();
254
+ }
255
+ });
256
+ test(`[${factory.name}] transition plus event batch is atomic; stale revisions lose`, async () => {
257
+ const s = await factory.create();
258
+ try {
259
+ await publishFixture(s);
260
+ await s.execution.createRun(createCommand());
261
+ const updated = await s.execution.commitTransition(transitionCommand({
262
+ expectedRecordRevision: 1,
263
+ expectedNextEventSeq: 1,
264
+ next: { currentNodeId: 'n2', steps: 1 },
265
+ events: [makeEvent(1, 'node.started', { nodeId: 'n2', capabilityId: 'cap.a' })],
266
+ }));
267
+ expect(updated.recordRevision).toBe(2);
268
+ expect(updated.currentNodeId).toBe('n2');
269
+ // Stale revision (same transition replayed) is rejected.
270
+ await rejectsWithCode(expect, s.execution.commitTransition(transitionCommand({
271
+ expectedRecordRevision: 1,
272
+ expectedNextEventSeq: 2,
273
+ events: [makeEvent(2, 'node.completed', { nodeId: 'n2', capabilityId: 'cap.a' })],
274
+ })), 'VICT_STORE_RUN_CONFLICT');
275
+ // Wrong event sequence is a conflict, and nothing is appended.
276
+ await rejectsWithCode(expect, s.execution.commitTransition(transitionCommand({
277
+ expectedRecordRevision: 2,
278
+ expectedNextEventSeq: 3,
279
+ events: [makeEvent(2, 'node.completed', { nodeId: 'n2', capabilityId: 'cap.a' })],
280
+ })), 'VICT_STORE_EVENT_SEQUENCE_CONFLICT');
281
+ expect((await s.execution.listEvents('conf-run-1')).length).toBe(2);
282
+ }
283
+ finally {
284
+ await s.dispose();
285
+ }
286
+ });
287
+ test(`[${factory.name}] completed, failed, and blocked records round-trip`, async () => {
288
+ const s = await factory.create();
289
+ try {
290
+ const statuses = [
291
+ { status: 'completed', event: 'run.completed' },
292
+ { status: 'failed', error: SAFE_ERROR, event: 'run.failed' },
293
+ { status: 'blocked', event: 'run.blocked' },
294
+ ];
295
+ for (const [index, target] of statuses.entries()) {
296
+ const runId = `conf-run-${index}`;
297
+ await publishFixture(s);
298
+ await s.execution.createRun(createCommand({ runId }));
299
+ const terminal = await s.execution.commitTransition(transitionCommand({
300
+ runId,
301
+ expectedRecordRevision: 1,
302
+ expectedNextEventSeq: 1,
303
+ next: {
304
+ status: target.status,
305
+ steps: 3,
306
+ completedAt: 2_000,
307
+ ...(target.error ? { error: target.error } : {}),
308
+ },
309
+ events: [
310
+ makeEvent(1, target.event, {
311
+ steps: 3,
312
+ ...(target.error ? { error: target.error } : {}),
313
+ ...(target.status === 'completed'
314
+ ? { output: { shape: 'object', keys: [] } }
315
+ : {}),
316
+ ...(target.status === 'blocked' ? { reason: 'r', remediation: 'm' } : {}),
317
+ }, runId),
318
+ ],
319
+ }));
320
+ expect(terminal.status).toBe(target.status);
321
+ const read = await s.execution.getRun(runId);
322
+ expect(read?.status).toBe(target.status);
323
+ expect(read?.completedAt).toBe(2_000);
324
+ if (target.error) {
325
+ expect(read?.error?.code).toBe(SAFE_ERROR.code);
326
+ expect(JSON.stringify(read).includes(SECRET_CANARY)).toBe(false);
327
+ }
328
+ // Terminal runs reject further transitions.
329
+ await rejectsWithCode(expect, s.execution.commitTransition(transitionCommand({ runId, expectedRecordRevision: 2, expectedNextEventSeq: 2 })), 'VICT_STORE_RUN_CONFLICT');
330
+ }
331
+ }
332
+ finally {
333
+ await s.dispose();
334
+ }
335
+ });
336
+ test(`[${factory.name}] retention shapes: none, summary, full`, async () => {
337
+ const s = await factory.create();
338
+ try {
339
+ const output = { public: 'value', secretNote: SECRET_CANARY };
340
+ // none: no summary, no output.
341
+ await publishFixture(s);
342
+ await s.execution.createRun(createCommand({ runId: 'r-none', retention: 'none' }));
343
+ const none = await s.execution.commitTransition(transitionCommand({
344
+ runId: 'r-none',
345
+ expectedRecordRevision: 1,
346
+ expectedNextEventSeq: 1,
347
+ next: { status: 'completed', steps: 1, completedAt: 2_000 },
348
+ events: [
349
+ makeEvent(1, 'run.completed', {
350
+ steps: 1,
351
+ output: { shape: 'object', keys: ['public'] },
352
+ }),
353
+ ],
354
+ }));
355
+ expect('output' in none).toBe(false);
356
+ expect('outputSummary' in none).toBe(false);
357
+ // summary: summary only; no canary value anywhere.
358
+ await publishFixture(s);
359
+ await s.execution.createRun(createCommand({ runId: 'r-summary', retention: 'summary' }));
360
+ const summary = await s.execution.commitTransition(transitionCommand({
361
+ runId: 'r-summary',
362
+ expectedRecordRevision: 1,
363
+ expectedNextEventSeq: 1,
364
+ next: {
365
+ status: 'completed',
366
+ steps: 1,
367
+ completedAt: 2_000,
368
+ outputSummary: { shape: 'object', keys: ['public', '[redacted]'] },
369
+ },
370
+ events: [
371
+ makeEvent(1, 'run.completed', {
372
+ steps: 1,
373
+ output: { shape: 'object', keys: ['public'] },
374
+ }),
375
+ ],
376
+ }));
377
+ expect(summary.outputSummary).toEqual({ shape: 'object', keys: ['public', '[redacted]'] });
378
+ expect(JSON.stringify(summary).includes(SECRET_CANARY)).toBe(false);
379
+ // full: complete output stored by explicit configuration.
380
+ await publishFixture(s);
381
+ await s.execution.createRun(createCommand({ runId: 'r-full', retention: 'full' }));
382
+ const full = await s.execution.commitTransition(transitionCommand({
383
+ runId: 'r-full',
384
+ expectedRecordRevision: 1,
385
+ expectedNextEventSeq: 1,
386
+ next: {
387
+ status: 'completed',
388
+ steps: 1,
389
+ completedAt: 2_000,
390
+ outputSummary: { shape: 'object', keys: ['public'] },
391
+ output,
392
+ },
393
+ events: [
394
+ makeEvent(1, 'run.completed', {
395
+ steps: 1,
396
+ output: { shape: 'object', keys: ['public'] },
397
+ }),
398
+ ],
399
+ }));
400
+ expect(full.output).toEqual(output);
401
+ // Inputs are never part of the record shape at all.
402
+ expect('input' in full).toBe(false);
403
+ expect('input' in none).toBe(false);
404
+ }
405
+ finally {
406
+ await s.dispose();
407
+ }
408
+ });
409
+ test(`[${factory.name}] safe errors round-trip without raw causes`, async () => {
410
+ const s = await factory.create();
411
+ try {
412
+ await publishFixture(s);
413
+ await s.execution.createRun(createCommand({ runId: 'r-err', retention: 'summary' }));
414
+ const failed = await s.execution.commitTransition(transitionCommand({
415
+ runId: 'r-err',
416
+ expectedRecordRevision: 1,
417
+ expectedNextEventSeq: 1,
418
+ next: { status: 'failed', steps: 2, completedAt: 2_000, error: SAFE_ERROR },
419
+ events: [makeEvent(1, 'run.failed', { steps: 2, error: SAFE_ERROR })],
420
+ }));
421
+ const serialized = JSON.stringify(failed);
422
+ expect(serialized.includes('VICT_RUNTIME_CAPABILITY_THREW')).toBe(true);
423
+ // A store never invents driver detail into stored errors.
424
+ expect(serialized.includes('driverCause')).toBe(false);
425
+ expect(serialized.includes(SECRET_CANARY)).toBe(false);
426
+ }
427
+ finally {
428
+ await s.dispose();
429
+ }
430
+ });
431
+ test(`[${factory.name}] events are dense, append-only, and explicitly ordered`, async () => {
432
+ const s = await factory.create();
433
+ try {
434
+ await publishFixture(s);
435
+ await s.execution.createRun(createCommand());
436
+ let seq = 1;
437
+ let revision = 1;
438
+ for (const type of ['node.started', 'node.completed', 'signal.routed']) {
439
+ await s.execution.commitTransition(transitionCommand({
440
+ expectedRecordRevision: revision,
441
+ expectedNextEventSeq: seq,
442
+ events: [makeEvent(seq, type, { nodeId: 'n1', capabilityId: 'cap.a' })],
443
+ }));
444
+ revision += 1;
445
+ seq += 1;
446
+ }
447
+ const events = await s.execution.listEvents('conf-run-1');
448
+ expect(events.map((event) => event.seq)).toEqual([0, 1, 2, 3]);
449
+ // Ordered reads after a cursor.
450
+ const tail = await s.execution.listEvents('conf-run-1', 2);
451
+ expect(tail.map((event) => event.seq)).toEqual([3]);
452
+ // Frozen snapshots.
453
+ expect(Object.isFrozen(events[0])).toBe(true);
454
+ }
455
+ finally {
456
+ await s.dispose();
457
+ }
458
+ });
459
+ test(`[${factory.name}] returned records cannot mutate stored state`, async () => {
460
+ const s = await factory.create();
461
+ try {
462
+ await publishFixture(s);
463
+ await s.execution.createRun(createCommand());
464
+ const run = await s.execution.getRun('conf-run-1');
465
+ expect(run).toBeDefined();
466
+ // Attempts to mutate the returned snapshot must not affect the store.
467
+ const mutate = async () => {
468
+ const record = (await s.execution.getRun('conf-run-1'));
469
+ record.status = 'completed';
470
+ };
471
+ // Frozen records throw in strict mode; either way the store is unchanged.
472
+ try {
473
+ await mutate();
474
+ }
475
+ catch {
476
+ /* expected for frozen snapshots */
477
+ }
478
+ expect((await s.execution.getRun('conf-run-1'))?.status).toBe('running');
479
+ }
480
+ finally {
481
+ await s.dispose();
482
+ }
483
+ });
484
+ test(`[${factory.name}] recover-interrupted is idempotent and preserves identity`, async () => {
485
+ const s = await factory.create();
486
+ try {
487
+ await publishFixture(s);
488
+ await s.execution.createRun(createCommand());
489
+ const first = await s.execution.recoverInterruptedRuns({
490
+ code: 'VICT_RUN_INTERRUPTED_BY_RESTART',
491
+ reason: 'process ended',
492
+ remediation: 'start a new run',
493
+ timestamp: 5_000,
494
+ });
495
+ expect(first.scanned).toBe(1);
496
+ expect(first.blocked.length).toBe(1);
497
+ expect(first.blocked[0]?.activationVersion).toBe(GRAPH_A.activationVersion);
498
+ expect(first.blocked[0]?.eventSeq).toBe(1);
499
+ const run = await s.execution.getRun('conf-run-1');
500
+ expect(run?.status).toBe('blocked');
501
+ // Idempotent: the second recovery finds nothing and appends nothing.
502
+ const second = await s.execution.recoverInterruptedRuns({
503
+ code: 'VICT_RUN_INTERRUPTED_BY_RESTART',
504
+ reason: 'process ended',
505
+ remediation: 'start a new run',
506
+ timestamp: 6_000,
507
+ });
508
+ expect(second.scanned).toBe(0);
509
+ expect(second.blocked.length).toBe(0);
510
+ const events = await s.execution.listEvents('conf-run-1');
511
+ expect(events.length).toBe(2);
512
+ const blockedEvent = events.at(-1);
513
+ expect(blockedEvent?.type).toBe('run.blocked');
514
+ const payload = JSON.parse(blockedEvent?.payload ?? '{}');
515
+ expect(payload.code).toBe('VICT_RUN_INTERRUPTED_BY_RESTART');
516
+ }
517
+ finally {
518
+ await s.dispose();
519
+ }
520
+ });
521
+ test(`[${factory.name}] unknown and malformed records fail safely`, async () => {
522
+ const s = await factory.create();
523
+ try {
524
+ expect(await s.execution.getRun('ghost')).toBeUndefined();
525
+ expect(await s.catalog.get('ghost')).toBeUndefined();
526
+ expect(await s.catalog.getSelection('ghost')).toBeUndefined();
527
+ await rejectsWithCode(expect, s.execution.listEvents('ghost'), 'VICT_STORE_RUN_NOT_FOUND');
528
+ await rejectsWithCode(expect, s.execution.commitTransition(transitionCommand()), 'VICT_STORE_RUN_NOT_FOUND');
529
+ await rejectsWithCode(expect, s.execution.createRun(createCommand({ mode: 'bogus' })), 'VICT_STORE_INVALID_COMMAND');
530
+ await rejectsWithCode(expect, s.execution.createRun(createCommand({ retention: 'everything' })), 'VICT_STORE_INVALID_COMMAND');
531
+ }
532
+ finally {
533
+ await s.dispose();
534
+ }
535
+ });
536
+ test(`[${factory.name}] event schema versions are recorded`, async () => {
537
+ const s = await factory.create();
538
+ try {
539
+ await publishFixture(s);
540
+ await s.execution.createRun(createCommand());
541
+ const events = await s.execution.listEvents('conf-run-1');
542
+ expect(events[0]?.eventSchema).toBe(RUN_EVENT_SCHEMA);
543
+ }
544
+ finally {
545
+ await s.dispose();
546
+ }
547
+ });
548
+ test(`[${factory.name}] expectedNextEventSeq must equal the actual stored next sequence`, async () => {
549
+ const s = await factory.create();
550
+ try {
551
+ await publishFixture(s);
552
+ await s.execution.createRun(createCommand());
553
+ const events = await s.execution.listEvents('conf-run-1');
554
+ expect(events.length).toBe(1);
555
+ // The stored history ends at seq 0; an expectation of 5 with a
556
+ // matching event seq must be rejected against ACTUAL stored history.
557
+ await rejectsWithCode(expect, s.execution.commitTransition(transitionCommand({
558
+ expectedRecordRevision: 1,
559
+ expectedNextEventSeq: 5,
560
+ events: [makeEvent(5, 'node.completed', { nodeId: 'n1', capabilityId: 'cap.a' })],
561
+ })), 'VICT_STORE_EVENT_SEQUENCE_CONFLICT');
562
+ // A stale expectation is equally rejected.
563
+ await rejectsWithCode(expect, s.execution.commitTransition(transitionCommand({
564
+ expectedRecordRevision: 1,
565
+ expectedNextEventSeq: 0,
566
+ events: [makeEvent(0, 'node.completed', { nodeId: 'n1', capabilityId: 'cap.a' })],
567
+ })), 'VICT_STORE_EVENT_SEQUENCE_CONFLICT');
568
+ // Rollback proof: revision and history are untouched.
569
+ const run = await s.execution.getRun('conf-run-1');
570
+ expect(run?.recordRevision).toBe(1);
571
+ expect((await s.execution.listEvents('conf-run-1')).length).toBe(1);
572
+ // The correct expectation succeeds.
573
+ await s.execution.commitTransition(transitionCommand({
574
+ expectedRecordRevision: 1,
575
+ expectedNextEventSeq: 1,
576
+ events: [makeEvent(1, 'node.completed', { nodeId: 'n1', capabilityId: 'cap.a' })],
577
+ }));
578
+ expect((await s.execution.listEvents('conf-run-1')).length).toBe(2);
579
+ }
580
+ finally {
581
+ await s.dispose();
582
+ }
583
+ });
584
+ test(`[${factory.name}] createRun accepts only a dense initial batch beginning at zero`, async () => {
585
+ const s = await factory.create();
586
+ try {
587
+ await publishFixture(s);
588
+ // Batch starting at 1: rejected.
589
+ await rejectsWithCode(expect, s.execution.createRun(createCommand({ runId: 'r-gap1', events: [makeEvent(1, 'run.started', {}, 'r-gap1')] })), 'VICT_STORE_EVENT_SEQUENCE_CONFLICT');
590
+ // Dense prefix then gap [0, 2]: rejected.
591
+ await rejectsWithCode(expect, s.execution.createRun(createCommand({
592
+ runId: 'r-gap2',
593
+ events: [
594
+ makeEvent(0, 'run.started', {}, 'r-gap2'),
595
+ makeEvent(2, 'node.started', { nodeId: 'n1', capabilityId: 'cap.a' }, 'r-gap2'),
596
+ ],
597
+ })), 'VICT_STORE_EVENT_SEQUENCE_CONFLICT');
598
+ // No partial runs or events were left behind.
599
+ expect(await s.execution.getRun('r-gap1')).toBeUndefined();
600
+ expect(await s.execution.getRun('r-gap2')).toBeUndefined();
601
+ // A dense batch from zero is accepted.
602
+ await s.execution.createRun(createCommand({
603
+ runId: 'r-dense',
604
+ events: [
605
+ makeEvent(0, 'run.started', {}, 'r-dense'),
606
+ makeEvent(1, 'node.started', { nodeId: 'n1', capabilityId: 'cap.a' }, 'r-dense'),
607
+ ],
608
+ }));
609
+ expect((await s.execution.listEvents('r-dense')).map((event) => event.seq)).toEqual([0, 1]);
610
+ }
611
+ finally {
612
+ await s.dispose();
613
+ }
614
+ });
615
+ test(`[${factory.name}] publish rejects canonical content that is not the manifest's canonical form`, async () => {
616
+ const s = await factory.create();
617
+ try {
618
+ // Top-level identifiers look valid, but the canonical string does not
619
+ // correspond to the supplied manifest content.
620
+ const tampered = { ...GRAPH_A, graphVersion: 'v1_graph-a' };
621
+ await rejectsWithCode(expect, s.catalog.publish({ manifest: tampered, canonicalManifest: '{"tampered":true}' }), 'VICT_STORE_ACTIVATION_MISMATCH');
622
+ // Canonical string of a DIFFERENT manifest: rejected.
623
+ await rejectsWithCode(expect, s.catalog.publish({
624
+ manifest: GRAPH_A,
625
+ canonicalManifest: toCanonicalJson(GRAPH_B),
626
+ }), 'VICT_STORE_ACTIVATION_MISMATCH');
627
+ expect((await s.catalog.list()).length).toBe(0);
628
+ }
629
+ finally {
630
+ await s.dispose();
631
+ }
632
+ });
633
+ test(`[${factory.name}] publish rejects identities that do not recompute from content`, async () => {
634
+ const s = await factory.create();
635
+ try {
636
+ // The manifest is internally consistent JSON (canonical string
637
+ // matches), the columns agree — but the graph CONTENT was tampered
638
+ // with while keeping the original identity strings. The canonical
639
+ // identity functions must catch it.
640
+ const forgedGraph = {
641
+ schema: 'vict.graph@1',
642
+ id: 'conf-graph-a',
643
+ entry: 'n1',
644
+ nodes: [{ id: 'n1', capability: 'cap.OTHER', input: null, output: null }],
645
+ edges: [],
646
+ };
647
+ const forged = { ...GRAPH_A, graph: forgedGraph };
648
+ await rejectsWithCode(expect, s.catalog.publish({ manifest: forged, canonicalManifest: toCanonicalJson(forged) }), 'VICT_STORE_ACTIVATION_MISMATCH');
649
+ // Same trick for bindings: keep the capabilitySetVersion string, swap
650
+ // the binding revision.
651
+ const forgedBindings = {
652
+ ...GRAPH_A,
653
+ bindings: [
654
+ {
655
+ capability: 'cap.a',
656
+ revision: '999',
657
+ effect: 'pure',
658
+ input: null,
659
+ output: null,
660
+ },
661
+ ],
662
+ };
663
+ await rejectsWithCode(expect, s.catalog.publish({
664
+ manifest: forgedBindings,
665
+ canonicalManifest: toCanonicalJson(forgedBindings),
666
+ }), 'VICT_STORE_ACTIVATION_MISMATCH');
667
+ expect((await s.catalog.list()).length).toBe(0);
668
+ }
669
+ finally {
670
+ await s.dispose();
671
+ }
672
+ });
673
+ test(`[${factory.name}] selection requires the activation to belong to the graph`, async () => {
674
+ const s = await factory.create();
675
+ try {
676
+ await s.catalog.publish({ manifest: GRAPH_A, canonicalManifest: toCanonicalJson(GRAPH_A) });
677
+ await rejectsWithCode(expect, s.catalog.select({
678
+ graphId: GRAPH_B.graphId,
679
+ activationVersion: GRAPH_A.activationVersion,
680
+ }), 'VICT_STORE_ACTIVATION_MISMATCH');
681
+ expect(await s.catalog.getSelection(GRAPH_B.graphId)).toBeUndefined();
682
+ }
683
+ finally {
684
+ await s.dispose();
685
+ }
686
+ });
687
+ test(`[${factory.name}] createRun requires a coherent published activation identity`, async () => {
688
+ const s = await factory.create();
689
+ try {
690
+ await publishFixture(s);
691
+ // Wrong graph id.
692
+ await rejectsWithCode(expect, s.execution.createRun(createCommand({ runId: 'r-id1', graphId: 'conf-graph-OTHER' })), 'VICT_STORE_ACTIVATION_MISMATCH');
693
+ // Wrong graph version.
694
+ await rejectsWithCode(expect, s.execution.createRun(createCommand({ runId: 'r-id2', graphVersion: 'v1_graph-a-OTHER' })), 'VICT_STORE_ACTIVATION_MISMATCH');
695
+ // Wrong capability-set version.
696
+ await rejectsWithCode(expect, s.execution.createRun(createCommand({ runId: 'r-id3', capabilitySetVersion: 'v1_capset-a-OTHER' })), 'VICT_STORE_ACTIVATION_MISMATCH');
697
+ // No partial runs were created.
698
+ for (const runId of ['r-id1', 'r-id2', 'r-id3']) {
699
+ expect(await s.execution.getRun(runId)).toBeUndefined();
700
+ }
701
+ }
702
+ finally {
703
+ await s.dispose();
704
+ }
705
+ });
706
+ test(`[${factory.name}] appended events must carry the stored run's identity`, async () => {
707
+ const s = await factory.create();
708
+ try {
709
+ await publishFixture(s);
710
+ await s.execution.createRun(createCommand());
711
+ for (const [field, value] of [
712
+ ['graphId', 'conf-graph-OTHER'],
713
+ ['graphVersion', 'v1_graph-a-OTHER'],
714
+ ['capabilitySetVersion', 'v1_capset-a-OTHER'],
715
+ ['activationVersion', 'v1_act-a-OTHER'],
716
+ ]) {
717
+ const event = makeEvent(1, 'node.started', { nodeId: 'n1', capabilityId: 'cap.a' });
718
+ await rejectsWithCode(expect, s.execution.commitTransition(transitionCommand({
719
+ expectedRecordRevision: 1,
720
+ expectedNextEventSeq: 1,
721
+ events: [{ ...event, [field]: value }],
722
+ })), 'VICT_STORE_INVALID_COMMAND');
723
+ }
724
+ // Every rejection left no partial state.
725
+ expect((await s.execution.getRun('conf-run-1'))?.recordRevision).toBe(1);
726
+ expect((await s.execution.listEvents('conf-run-1')).length).toBe(1);
727
+ }
728
+ finally {
729
+ await s.dispose();
730
+ }
731
+ });
732
+ test(`[${factory.name}] publishAndSelect failure leaves catalog and selection untouched`, async () => {
733
+ const s = await factory.create();
734
+ try {
735
+ // Establish an unrelated selection so revision 0 is stale.
736
+ await s.catalog.publish({ manifest: GRAPH_B, canonicalManifest: toCanonicalJson(GRAPH_B) });
737
+ await s.catalog.select({
738
+ graphId: GRAPH_B.graphId,
739
+ activationVersion: GRAPH_B.activationVersion,
740
+ });
741
+ // Publish a previously ABSENT activation with a stale selection
742
+ // revision: the whole operation must fail atomically.
743
+ await rejectsWithCode(expect, s.catalog.publishAndSelect({
744
+ publish: { manifest: GRAPH_A, canonicalManifest: toCanonicalJson(GRAPH_A) },
745
+ select: { graphId: GRAPH_A.graphId, expectedSelectionRevision: 999 },
746
+ }), 'VICT_STORE_SELECTION_CONFLICT');
747
+ // The activation was NOT left behind.
748
+ expect(await s.catalog.get(GRAPH_A.activationVersion)).toBeUndefined();
749
+ expect((await s.catalog.list()).length).toBe(1);
750
+ expect(await s.catalog.getSelection(GRAPH_A.graphId)).toBeUndefined();
751
+ }
752
+ finally {
753
+ await s.dispose();
754
+ }
755
+ });
756
+ test(`[${factory.name}] publishAndSelect honors the selection revision guard`, async () => {
757
+ const s = await factory.create();
758
+ try {
759
+ const first = await s.catalog.publishAndSelect({
760
+ publish: { manifest: GRAPH_A, canonicalManifest: toCanonicalJson(GRAPH_A) },
761
+ select: { graphId: GRAPH_A.graphId },
762
+ });
763
+ expect(first.selection.selectionRevision).toBe(1);
764
+ // Stale guard (0) fails; nothing changes.
765
+ await rejectsWithCode(expect, s.catalog.publishAndSelect({
766
+ publish: { manifest: GRAPH_A, canonicalManifest: toCanonicalJson(GRAPH_A) },
767
+ select: { graphId: GRAPH_A.graphId, expectedSelectionRevision: 0 },
768
+ }), 'VICT_STORE_SELECTION_CONFLICT');
769
+ expect((await s.catalog.getSelection(GRAPH_A.graphId))?.selectionRevision).toBe(1);
770
+ // Current guard succeeds.
771
+ const again = await s.catalog.publishAndSelect({
772
+ publish: { manifest: GRAPH_A, canonicalManifest: toCanonicalJson(GRAPH_A) },
773
+ select: { graphId: GRAPH_A.graphId, expectedSelectionRevision: 1 },
774
+ });
775
+ expect(again.selection.selectionRevision).toBe(2);
776
+ expect(again.created).toBe(false);
777
+ }
778
+ finally {
779
+ await s.dispose();
780
+ }
781
+ });
782
+ test(`[${factory.name}] strict persisted-value domain: full-retention output cannot be silently altered`, async () => {
783
+ const s = await factory.create();
784
+ try {
785
+ await publishFixture(s);
786
+ await s.execution.createRun(createCommand({ runId: 'r-strict', retention: 'full' }));
787
+ // A Map collapses to {} under JSON.stringify — must be rejected.
788
+ await rejectsWithCode(expect, s.execution.commitTransition(transitionCommand({
789
+ runId: 'r-strict',
790
+ expectedRecordRevision: 1,
791
+ expectedNextEventSeq: 1,
792
+ next: {
793
+ status: 'completed',
794
+ steps: 1,
795
+ completedAt: 2_000,
796
+ output: { plain: 1, sneaky: new Map([['secret', 'value']]) },
797
+ },
798
+ events: [makeEvent(1, 'run.completed', { steps: 1 }, 'r-strict')],
799
+ })), 'VICT_STORE_INVALID_COMMAND');
800
+ // Explicit undefined in objects and arrays: rejected.
801
+ // NOTE: an absent/undefined `output` means 'no output' at the port
802
+ // level (the field is optional), so it is not in the rejected set;
803
+ // carrying `undefined` INSIDE an object or array is rejected.
804
+ for (const bad of [{ x: undefined }, [undefined], new Set([1]), Symbol('s')]) {
805
+ await rejectsWithCode(expect, s.execution.commitTransition(transitionCommand({
806
+ runId: 'r-strict',
807
+ expectedRecordRevision: 1,
808
+ expectedNextEventSeq: 1,
809
+ next: { status: 'completed', steps: 1, completedAt: 2_000, output: bad },
810
+ events: [makeEvent(1, 'run.completed', { steps: 1 }, 'r-strict')],
811
+ })), 'VICT_STORE_INVALID_COMMAND');
812
+ }
813
+ // No partial mutation: still running at revision 1 with one event.
814
+ const run = await s.execution.getRun('r-strict');
815
+ expect(run?.status).toBe('running');
816
+ expect(run?.recordRevision).toBe(1);
817
+ expect((await s.execution.listEvents('r-strict')).length).toBe(1);
818
+ // A plain, in-domain output is accepted and round-trips.
819
+ const completed = await s.execution.commitTransition(transitionCommand({
820
+ runId: 'r-strict',
821
+ expectedRecordRevision: 1,
822
+ expectedNextEventSeq: 1,
823
+ next: {
824
+ status: 'completed',
825
+ steps: 1,
826
+ completedAt: 2_000,
827
+ output: { plain: 1, nested: { b: 2, a: 1 }, when: new Date(1_500) },
828
+ },
829
+ events: [makeEvent(1, 'run.completed', { steps: 1 }, 'r-strict')],
830
+ }));
831
+ expect(completed.output).toEqual({
832
+ plain: 1,
833
+ nested: { b: 2, a: 1 },
834
+ when: '1970-01-01T00:00:01.500Z',
835
+ });
836
+ }
837
+ finally {
838
+ await s.dispose();
839
+ }
840
+ });
841
+ test(`[${factory.name}] fault between run update and event append leaves no half-state`, async () => {
842
+ const s = await factory.create();
843
+ try {
844
+ if (!s.faults) {
845
+ // Backend does not expose fault hooks; atomicity is still covered by
846
+ // the sequence-conflict and terminal-conflict cases above.
847
+ return;
848
+ }
849
+ await publishFixture(s);
850
+ await s.execution.createRun(createCommand());
851
+ const faults = s.faults;
852
+ faults.afterRunUpdate = () => {
853
+ throw new Error('injected failure after run update');
854
+ };
855
+ await rejectsWithCode(expect, s.execution.commitTransition(transitionCommand({
856
+ expectedRecordRevision: 1,
857
+ expectedNextEventSeq: 1,
858
+ next: { status: 'completed', steps: 1, completedAt: 9_999 },
859
+ events: [makeEvent(1, 'run.completed', { steps: 1 })],
860
+ })), 'VICT_STORE_UNAVAILABLE');
861
+ // No half-state: the run is still running at revision 1 with one event.
862
+ const run = await s.execution.getRun('conf-run-1');
863
+ expect(run?.status).toBe('running');
864
+ expect(run?.recordRevision).toBe(1);
865
+ expect((await s.execution.listEvents('conf-run-1')).length).toBe(1);
866
+ }
867
+ finally {
868
+ const faults = s.faults;
869
+ if (faults) {
870
+ faults.afterRunUpdate = undefined;
871
+ }
872
+ await s.dispose();
873
+ }
874
+ });
875
+ }
876
+ //# sourceMappingURL=store-conformance.js.map