digital-workers 2.1.1 → 2.3.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 (197) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +136 -180
  3. package/dist/actions.d.ts.map +1 -1
  4. package/dist/actions.js +34 -21
  5. package/dist/actions.js.map +1 -1
  6. package/dist/agent-comms.d.ts +438 -0
  7. package/dist/agent-comms.d.ts.map +1 -0
  8. package/dist/agent-comms.js +677 -0
  9. package/dist/agent-comms.js.map +1 -0
  10. package/dist/approve.d.ts +40 -8
  11. package/dist/approve.d.ts.map +1 -1
  12. package/dist/approve.js +86 -20
  13. package/dist/approve.js.map +1 -1
  14. package/dist/ask.d.ts +38 -7
  15. package/dist/ask.d.ts.map +1 -1
  16. package/dist/ask.js +85 -25
  17. package/dist/ask.js.map +1 -1
  18. package/dist/browse.d.ts +223 -0
  19. package/dist/browse.d.ts.map +1 -0
  20. package/dist/browse.js +392 -0
  21. package/dist/browse.js.map +1 -0
  22. package/dist/capability-tiers.d.ts +230 -0
  23. package/dist/capability-tiers.d.ts.map +1 -0
  24. package/dist/capability-tiers.js +388 -0
  25. package/dist/capability-tiers.js.map +1 -0
  26. package/dist/cascade-context.d.ts +523 -0
  27. package/dist/cascade-context.d.ts.map +1 -0
  28. package/dist/cascade-context.js +494 -0
  29. package/dist/cascade-context.js.map +1 -0
  30. package/dist/client.d.ts +162 -0
  31. package/dist/client.d.ts.map +1 -0
  32. package/dist/client.js +64 -0
  33. package/dist/client.js.map +1 -0
  34. package/dist/decide.d.ts +42 -6
  35. package/dist/decide.d.ts.map +1 -1
  36. package/dist/decide.js +54 -11
  37. package/dist/decide.js.map +1 -1
  38. package/dist/do.d.ts +36 -7
  39. package/dist/do.d.ts.map +1 -1
  40. package/dist/do.js +82 -39
  41. package/dist/do.js.map +1 -1
  42. package/dist/error-escalation.d.ts +416 -0
  43. package/dist/error-escalation.d.ts.map +1 -0
  44. package/dist/error-escalation.js +656 -0
  45. package/dist/error-escalation.js.map +1 -0
  46. package/dist/generate.d.ts +48 -7
  47. package/dist/generate.d.ts.map +1 -1
  48. package/dist/generate.js +49 -8
  49. package/dist/generate.js.map +1 -1
  50. package/dist/goals.d.ts +10 -9
  51. package/dist/goals.d.ts.map +1 -1
  52. package/dist/goals.js +30 -24
  53. package/dist/goals.js.map +1 -1
  54. package/dist/image.d.ts +189 -0
  55. package/dist/image.d.ts.map +1 -0
  56. package/dist/image.js +528 -0
  57. package/dist/image.js.map +1 -0
  58. package/dist/index.d.ts +59 -2
  59. package/dist/index.d.ts.map +1 -1
  60. package/dist/index.js +92 -2
  61. package/dist/index.js.map +1 -1
  62. package/dist/is.d.ts +45 -10
  63. package/dist/is.d.ts.map +1 -1
  64. package/dist/is.js +56 -21
  65. package/dist/is.js.map +1 -1
  66. package/dist/kpis.d.ts +24 -15
  67. package/dist/kpis.d.ts.map +1 -1
  68. package/dist/kpis.js +16 -14
  69. package/dist/kpis.js.map +1 -1
  70. package/dist/load-balancing.d.ts +395 -0
  71. package/dist/load-balancing.d.ts.map +1 -0
  72. package/dist/load-balancing.js +991 -0
  73. package/dist/load-balancing.js.map +1 -0
  74. package/dist/logger.d.ts +76 -0
  75. package/dist/logger.d.ts.map +1 -0
  76. package/dist/logger.js +39 -0
  77. package/dist/logger.js.map +1 -0
  78. package/dist/notify.d.ts +38 -9
  79. package/dist/notify.d.ts.map +1 -1
  80. package/dist/notify.js +72 -17
  81. package/dist/notify.js.map +1 -1
  82. package/dist/role.d.ts +5 -4
  83. package/dist/role.d.ts.map +1 -1
  84. package/dist/role.js +13 -10
  85. package/dist/role.js.map +1 -1
  86. package/dist/runtime.d.ts +310 -0
  87. package/dist/runtime.d.ts.map +1 -0
  88. package/dist/runtime.js +510 -0
  89. package/dist/runtime.js.map +1 -0
  90. package/dist/team.d.ts +11 -6
  91. package/dist/team.d.ts.map +1 -1
  92. package/dist/team.js +22 -15
  93. package/dist/team.js.map +1 -1
  94. package/dist/transports/email.d.ts +318 -0
  95. package/dist/transports/email.d.ts.map +1 -0
  96. package/dist/transports/email.js +779 -0
  97. package/dist/transports/email.js.map +1 -0
  98. package/dist/transports/slack.d.ts +515 -0
  99. package/dist/transports/slack.d.ts.map +1 -0
  100. package/dist/transports/slack.js +844 -0
  101. package/dist/transports/slack.js.map +1 -0
  102. package/dist/transports.d.ts.map +1 -1
  103. package/dist/transports.js +44 -25
  104. package/dist/transports.js.map +1 -1
  105. package/dist/types.d.ts +149 -19
  106. package/dist/types.d.ts.map +1 -1
  107. package/dist/types.js +6 -0
  108. package/dist/types.js.map +1 -1
  109. package/dist/utils/id.d.ts +19 -0
  110. package/dist/utils/id.d.ts.map +1 -0
  111. package/dist/utils/id.js +21 -0
  112. package/dist/utils/id.js.map +1 -0
  113. package/dist/video.d.ts +203 -0
  114. package/dist/video.d.ts.map +1 -0
  115. package/dist/video.js +528 -0
  116. package/dist/video.js.map +1 -0
  117. package/dist/worker.d.ts +343 -0
  118. package/dist/worker.d.ts.map +1 -0
  119. package/dist/worker.js +698 -0
  120. package/dist/worker.js.map +1 -0
  121. package/package.json +24 -5
  122. package/src/actions.ts +48 -38
  123. package/src/agent-comms.ts +1200 -0
  124. package/src/approve.ts +91 -20
  125. package/src/ask.ts +99 -25
  126. package/src/browse.ts +627 -0
  127. package/src/capability-tiers.ts +545 -0
  128. package/src/cascade-context.ts +648 -0
  129. package/src/client.ts +221 -0
  130. package/src/decide.ts +81 -35
  131. package/src/do.ts +98 -52
  132. package/src/error-escalation.ts +1123 -0
  133. package/src/generate.ts +52 -18
  134. package/src/goals.ts +36 -27
  135. package/src/image.ts +816 -0
  136. package/src/index.ts +410 -2
  137. package/src/is.ts +59 -25
  138. package/src/kpis.ts +41 -36
  139. package/src/load-balancing.ts +1467 -0
  140. package/src/logger.ts +93 -0
  141. package/src/notify.ts +78 -17
  142. package/src/role.ts +30 -20
  143. package/src/runtime.ts +796 -0
  144. package/src/team.ts +24 -19
  145. package/src/transports/email.ts +1160 -0
  146. package/src/transports/slack.ts +1320 -0
  147. package/src/transports.ts +58 -43
  148. package/src/types.ts +182 -46
  149. package/src/utils/id.ts +21 -0
  150. package/src/video.ts +906 -0
  151. package/src/worker.ts +1007 -0
  152. package/test/agent-comms.test.ts +1397 -0
  153. package/test/approve.test.ts +305 -0
  154. package/test/ask.test.ts +274 -0
  155. package/test/browse.test.ts +361 -0
  156. package/test/capability-tiers.test.ts +631 -0
  157. package/test/cascade-context.test.ts +692 -0
  158. package/test/decide.test.ts +252 -0
  159. package/test/do.test.ts +144 -0
  160. package/test/error-escalation.test.ts +1205 -0
  161. package/test/error-logging.test.ts +357 -0
  162. package/test/generate.test.ts +319 -0
  163. package/test/image.test.ts +398 -0
  164. package/test/is.test.ts +287 -0
  165. package/test/load-balancing-safety.test.ts +404 -0
  166. package/test/load-balancing-thread-safety.test.ts +464 -0
  167. package/test/load-balancing.test.ts +1145 -0
  168. package/test/notify.test.ts +434 -0
  169. package/test/primitives.test.ts +320 -0
  170. package/test/runtime-integration.test.ts +892 -0
  171. package/test/transports/crypto.test.ts +230 -0
  172. package/test/transports/email.test.ts +866 -0
  173. package/test/transports/id-generation.test.ts +91 -0
  174. package/test/transports/slack.test.ts +760 -0
  175. package/test/type-safety.test.ts +834 -0
  176. package/test/types.test.ts +95 -2
  177. package/test/video.test.ts +530 -0
  178. package/test/worker.test.ts +1433 -0
  179. package/tsconfig.json +4 -1
  180. package/vitest.config.ts +42 -0
  181. package/wrangler.jsonc +36 -0
  182. package/.turbo/turbo-build.log +0 -5
  183. package/src/actions.js +0 -436
  184. package/src/approve.js +0 -234
  185. package/src/ask.js +0 -226
  186. package/src/decide.js +0 -244
  187. package/src/do.js +0 -227
  188. package/src/generate.js +0 -298
  189. package/src/goals.js +0 -205
  190. package/src/index.js +0 -68
  191. package/src/is.js +0 -317
  192. package/src/kpis.js +0 -270
  193. package/src/notify.js +0 -219
  194. package/src/role.js +0 -110
  195. package/src/team.js +0 -130
  196. package/src/transports.js +0 -357
  197. package/src/types.js +0 -71
@@ -0,0 +1,692 @@
1
+ /**
2
+ * Tests for AgentCascadeContext - Type-safe cascade context schema for agent coordination
3
+ *
4
+ * TDD RED Phase: These tests define the expected behavior before implementation.
5
+ */
6
+
7
+ import { describe, it, expect, expectTypeOf } from 'vitest'
8
+ import { z } from 'zod'
9
+
10
+ // These imports will fail initially (RED phase)
11
+ import {
12
+ // Types
13
+ type AgentCascadeContext,
14
+ type AgentTier,
15
+ type ContextVersion,
16
+ type ContextEnrichment,
17
+ type ValidationResult,
18
+ // Functions
19
+ createCascadeContext,
20
+ validateContext,
21
+ enrichContext,
22
+ serializeContext,
23
+ deserializeContext,
24
+ mergeContexts,
25
+ diffContexts,
26
+ createContextVersion,
27
+ // Schemas
28
+ AgentCascadeContextSchema,
29
+ AgentTierSchema,
30
+ } from '../src/cascade-context.js'
31
+
32
+ // ============================================================================
33
+ // Test Fixtures
34
+ // ============================================================================
35
+
36
+ const mockBaseContext: AgentCascadeContext = {
37
+ id: 'ctx_123',
38
+ version: {
39
+ major: 1,
40
+ minor: 0,
41
+ patch: 0,
42
+ timestamp: new Date('2026-01-11T00:00:00Z'),
43
+ },
44
+ originAgent: {
45
+ id: 'agent_coordinator',
46
+ tier: 'coordinator',
47
+ name: 'Task Coordinator',
48
+ },
49
+ currentAgent: {
50
+ id: 'agent_worker',
51
+ tier: 'worker',
52
+ name: 'Code Worker',
53
+ },
54
+ task: {
55
+ id: 'task_456',
56
+ type: 'code_review',
57
+ priority: 'high',
58
+ description: 'Review pull request #123',
59
+ },
60
+ state: {
61
+ phase: 'execution',
62
+ startedAt: new Date('2026-01-11T00:00:00Z'),
63
+ attempts: 1,
64
+ },
65
+ data: {
66
+ pullRequest: { id: 123, branch: 'feature/cascade' },
67
+ },
68
+ trace: [],
69
+ metadata: {},
70
+ }
71
+
72
+ // ============================================================================
73
+ // AgentCascadeContext Interface Tests
74
+ // ============================================================================
75
+
76
+ describe('AgentCascadeContext Interface', () => {
77
+ describe('type safety', () => {
78
+ it('should have required id field', () => {
79
+ const context: AgentCascadeContext = { ...mockBaseContext }
80
+ expect(context.id).toBe('ctx_123')
81
+ expectTypeOf(context.id).toBeString()
82
+ })
83
+
84
+ it('should have required version field with proper structure', () => {
85
+ const context: AgentCascadeContext = { ...mockBaseContext }
86
+ expect(context.version.major).toBe(1)
87
+ expect(context.version.minor).toBe(0)
88
+ expect(context.version.patch).toBe(0)
89
+ expect(context.version.timestamp).toBeInstanceOf(Date)
90
+ })
91
+
92
+ it('should have required originAgent with tier', () => {
93
+ const context: AgentCascadeContext = { ...mockBaseContext }
94
+ expect(context.originAgent.id).toBe('agent_coordinator')
95
+ expect(context.originAgent.tier).toBe('coordinator')
96
+ })
97
+
98
+ it('should have required currentAgent with tier', () => {
99
+ const context: AgentCascadeContext = { ...mockBaseContext }
100
+ expect(context.currentAgent.id).toBe('agent_worker')
101
+ expect(context.currentAgent.tier).toBe('worker')
102
+ })
103
+
104
+ it('should have required task information', () => {
105
+ const context: AgentCascadeContext = { ...mockBaseContext }
106
+ expect(context.task.id).toBe('task_456')
107
+ expect(context.task.type).toBe('code_review')
108
+ })
109
+
110
+ it('should have required state information', () => {
111
+ const context: AgentCascadeContext = { ...mockBaseContext }
112
+ expect(context.state.phase).toBe('execution')
113
+ expect(context.state.attempts).toBe(1)
114
+ })
115
+
116
+ it('should support optional data payload', () => {
117
+ const context: AgentCascadeContext = { ...mockBaseContext }
118
+ expect(context.data).toBeDefined()
119
+ expect(context.data?.pullRequest).toBeDefined()
120
+ })
121
+
122
+ it('should support trace array for debugging', () => {
123
+ const context: AgentCascadeContext = { ...mockBaseContext }
124
+ expect(Array.isArray(context.trace)).toBe(true)
125
+ })
126
+
127
+ it('should support metadata for extensions', () => {
128
+ const context: AgentCascadeContext = { ...mockBaseContext }
129
+ expect(context.metadata).toBeDefined()
130
+ })
131
+ })
132
+
133
+ describe('AgentTier type', () => {
134
+ it('should support coordinator tier', () => {
135
+ const tier: AgentTier = 'coordinator'
136
+ expect(tier).toBe('coordinator')
137
+ })
138
+
139
+ it('should support supervisor tier', () => {
140
+ const tier: AgentTier = 'supervisor'
141
+ expect(tier).toBe('supervisor')
142
+ })
143
+
144
+ it('should support worker tier', () => {
145
+ const tier: AgentTier = 'worker'
146
+ expect(tier).toBe('worker')
147
+ })
148
+
149
+ it('should support specialist tier', () => {
150
+ const tier: AgentTier = 'specialist'
151
+ expect(tier).toBe('specialist')
152
+ })
153
+
154
+ it('should support executor tier', () => {
155
+ const tier: AgentTier = 'executor'
156
+ expect(tier).toBe('executor')
157
+ })
158
+ })
159
+ })
160
+
161
+ // ============================================================================
162
+ // Context Validation Tests
163
+ // ============================================================================
164
+
165
+ describe('Context Validation', () => {
166
+ describe('validateContext()', () => {
167
+ it('should validate a correct context', () => {
168
+ const result = validateContext(mockBaseContext)
169
+ expect(result.success).toBe(true)
170
+ expect(result.errors).toBeUndefined()
171
+ })
172
+
173
+ it('should reject context without id', () => {
174
+ const invalid = { ...mockBaseContext, id: undefined } as any
175
+ const result = validateContext(invalid)
176
+ expect(result.success).toBe(false)
177
+ expect(result.errors).toBeDefined()
178
+ expect(result.errors).toContainEqual(expect.objectContaining({
179
+ path: ['id'],
180
+ }))
181
+ })
182
+
183
+ it('should reject context without version', () => {
184
+ const invalid = { ...mockBaseContext, version: undefined } as any
185
+ const result = validateContext(invalid)
186
+ expect(result.success).toBe(false)
187
+ expect(result.errors).toContainEqual(expect.objectContaining({
188
+ path: ['version'],
189
+ }))
190
+ })
191
+
192
+ it('should reject invalid agent tier', () => {
193
+ const invalid = {
194
+ ...mockBaseContext,
195
+ currentAgent: { ...mockBaseContext.currentAgent, tier: 'invalid_tier' },
196
+ } as any
197
+ const result = validateContext(invalid)
198
+ expect(result.success).toBe(false)
199
+ expect(result.errors).toContainEqual(expect.objectContaining({
200
+ path: expect.arrayContaining(['currentAgent', 'tier']),
201
+ }))
202
+ })
203
+
204
+ it('should reject invalid task priority', () => {
205
+ const invalid = {
206
+ ...mockBaseContext,
207
+ task: { ...mockBaseContext.task, priority: 'invalid_priority' },
208
+ } as any
209
+ const result = validateContext(invalid)
210
+ expect(result.success).toBe(false)
211
+ })
212
+
213
+ it('should provide descriptive error messages', () => {
214
+ const invalid = { id: 123 } as any // Wrong type for id
215
+ const result = validateContext(invalid)
216
+ expect(result.success).toBe(false)
217
+ expect(result.errors?.[0]?.message).toBeDefined()
218
+ expect(typeof result.errors?.[0]?.message).toBe('string')
219
+ })
220
+
221
+ it('should validate nested context data', () => {
222
+ const withNestedData: AgentCascadeContext = {
223
+ ...mockBaseContext,
224
+ data: {
225
+ deeply: { nested: { value: 'test' } },
226
+ },
227
+ }
228
+ const result = validateContext(withNestedData)
229
+ expect(result.success).toBe(true)
230
+ })
231
+ })
232
+
233
+ describe('Zod Schema Validation', () => {
234
+ it('should export AgentCascadeContextSchema', () => {
235
+ expect(AgentCascadeContextSchema).toBeDefined()
236
+ expect(AgentCascadeContextSchema.parse).toBeTypeOf('function')
237
+ })
238
+
239
+ it('should export AgentTierSchema', () => {
240
+ expect(AgentTierSchema).toBeDefined()
241
+ expect(AgentTierSchema.parse).toBeTypeOf('function')
242
+ })
243
+
244
+ it('should validate context with Zod schema directly', () => {
245
+ const parsed = AgentCascadeContextSchema.safeParse(mockBaseContext)
246
+ expect(parsed.success).toBe(true)
247
+ })
248
+
249
+ it('should reject invalid context with Zod schema', () => {
250
+ const parsed = AgentCascadeContextSchema.safeParse({ invalid: true })
251
+ expect(parsed.success).toBe(false)
252
+ })
253
+ })
254
+ })
255
+
256
+ // ============================================================================
257
+ // Context Enrichment Tests
258
+ // ============================================================================
259
+
260
+ describe('Context Enrichment', () => {
261
+ describe('enrichContext()', () => {
262
+ it('should add enrichment data from agent tier', () => {
263
+ const enrichment: ContextEnrichment = {
264
+ agentId: 'agent_worker',
265
+ tier: 'worker',
266
+ timestamp: new Date('2026-01-11T01:00:00Z'),
267
+ data: { analysisResult: 'approved' },
268
+ }
269
+
270
+ const enriched = enrichContext(mockBaseContext, enrichment)
271
+
272
+ expect(enriched.data?.analysisResult).toBe('approved')
273
+ })
274
+
275
+ it('should add trace entry when enriching', () => {
276
+ const enrichment: ContextEnrichment = {
277
+ agentId: 'agent_worker',
278
+ tier: 'worker',
279
+ timestamp: new Date(),
280
+ data: { result: 'done' },
281
+ }
282
+
283
+ const enriched = enrichContext(mockBaseContext, enrichment)
284
+
285
+ expect(enriched.trace).toHaveLength(1)
286
+ expect(enriched.trace[0]?.agentId).toBe('agent_worker')
287
+ })
288
+
289
+ it('should preserve existing data when enriching', () => {
290
+ const enrichment: ContextEnrichment = {
291
+ agentId: 'agent_worker',
292
+ tier: 'worker',
293
+ timestamp: new Date(),
294
+ data: { newField: 'value' },
295
+ }
296
+
297
+ const enriched = enrichContext(mockBaseContext, enrichment)
298
+
299
+ expect(enriched.data?.pullRequest).toEqual({ id: 123, branch: 'feature/cascade' })
300
+ expect(enriched.data?.newField).toBe('value')
301
+ })
302
+
303
+ it('should update currentAgent when enriching', () => {
304
+ const enrichment: ContextEnrichment = {
305
+ agentId: 'agent_specialist',
306
+ tier: 'specialist',
307
+ timestamp: new Date(),
308
+ data: {},
309
+ }
310
+
311
+ const enriched = enrichContext(mockBaseContext, enrichment)
312
+
313
+ expect(enriched.currentAgent.id).toBe('agent_specialist')
314
+ expect(enriched.currentAgent.tier).toBe('specialist')
315
+ })
316
+
317
+ it('should increment version patch on enrichment', () => {
318
+ const enrichment: ContextEnrichment = {
319
+ agentId: 'agent_worker',
320
+ tier: 'worker',
321
+ timestamp: new Date(),
322
+ data: {},
323
+ }
324
+
325
+ const enriched = enrichContext(mockBaseContext, enrichment)
326
+
327
+ expect(enriched.version.patch).toBe(mockBaseContext.version.patch + 1)
328
+ })
329
+
330
+ it('should maintain immutability of original context', () => {
331
+ const original = { ...mockBaseContext }
332
+ const enrichment: ContextEnrichment = {
333
+ agentId: 'agent_new',
334
+ tier: 'worker',
335
+ timestamp: new Date(),
336
+ data: { modified: true },
337
+ }
338
+
339
+ const enriched = enrichContext(original, enrichment)
340
+
341
+ expect(original.data?.modified).toBeUndefined()
342
+ expect(enriched.data?.modified).toBe(true)
343
+ expect(original.trace).toHaveLength(0)
344
+ })
345
+ })
346
+ })
347
+
348
+ // ============================================================================
349
+ // Context Serialization Tests
350
+ // ============================================================================
351
+
352
+ describe('Context Serialization', () => {
353
+ describe('serializeContext()', () => {
354
+ it('should serialize context to JSON string', () => {
355
+ const serialized = serializeContext(mockBaseContext)
356
+ expect(typeof serialized).toBe('string')
357
+ expect(() => JSON.parse(serialized)).not.toThrow()
358
+ })
359
+
360
+ it('should serialize Date objects correctly', () => {
361
+ const serialized = serializeContext(mockBaseContext)
362
+ const parsed = JSON.parse(serialized)
363
+ expect(parsed.version.timestamp).toBe('2026-01-11T00:00:00.000Z')
364
+ expect(parsed.state.startedAt).toBe('2026-01-11T00:00:00.000Z')
365
+ })
366
+
367
+ it('should preserve all context fields in serialization', () => {
368
+ const serialized = serializeContext(mockBaseContext)
369
+ const parsed = JSON.parse(serialized)
370
+
371
+ expect(parsed.id).toBe(mockBaseContext.id)
372
+ expect(parsed.originAgent.id).toBe(mockBaseContext.originAgent.id)
373
+ expect(parsed.task.type).toBe(mockBaseContext.task.type)
374
+ })
375
+ })
376
+
377
+ describe('deserializeContext()', () => {
378
+ it('should deserialize JSON string to context', () => {
379
+ const serialized = serializeContext(mockBaseContext)
380
+ const deserialized = deserializeContext(serialized)
381
+
382
+ expect(deserialized.id).toBe(mockBaseContext.id)
383
+ })
384
+
385
+ it('should restore Date objects', () => {
386
+ const serialized = serializeContext(mockBaseContext)
387
+ const deserialized = deserializeContext(serialized)
388
+
389
+ expect(deserialized.version.timestamp).toBeInstanceOf(Date)
390
+ expect(deserialized.state.startedAt).toBeInstanceOf(Date)
391
+ })
392
+
393
+ it('should validate deserialized context', () => {
394
+ const serialized = serializeContext(mockBaseContext)
395
+ const deserialized = deserializeContext(serialized)
396
+
397
+ const result = validateContext(deserialized)
398
+ expect(result.success).toBe(true)
399
+ })
400
+
401
+ it('should throw on invalid JSON', () => {
402
+ expect(() => deserializeContext('invalid json')).toThrow()
403
+ })
404
+
405
+ it('should throw on invalid context structure', () => {
406
+ const invalidJson = JSON.stringify({ invalid: true })
407
+ expect(() => deserializeContext(invalidJson)).toThrow()
408
+ })
409
+ })
410
+
411
+ describe('round-trip serialization', () => {
412
+ it('should preserve context through serialize/deserialize cycle', () => {
413
+ const serialized = serializeContext(mockBaseContext)
414
+ const deserialized = deserializeContext(serialized)
415
+
416
+ expect(deserialized.id).toBe(mockBaseContext.id)
417
+ expect(deserialized.version.major).toBe(mockBaseContext.version.major)
418
+ expect(deserialized.originAgent.id).toBe(mockBaseContext.originAgent.id)
419
+ expect(deserialized.currentAgent.tier).toBe(mockBaseContext.currentAgent.tier)
420
+ expect(deserialized.task.priority).toBe(mockBaseContext.task.priority)
421
+ expect(deserialized.data?.pullRequest).toEqual(mockBaseContext.data?.pullRequest)
422
+ })
423
+ })
424
+ })
425
+
426
+ // ============================================================================
427
+ // Context Version Tracking Tests
428
+ // ============================================================================
429
+
430
+ describe('Context Version Tracking', () => {
431
+ describe('ContextVersion type', () => {
432
+ it('should have major, minor, patch fields', () => {
433
+ const version: ContextVersion = {
434
+ major: 1,
435
+ minor: 2,
436
+ patch: 3,
437
+ timestamp: new Date(),
438
+ }
439
+
440
+ expect(version.major).toBe(1)
441
+ expect(version.minor).toBe(2)
442
+ expect(version.patch).toBe(3)
443
+ })
444
+
445
+ it('should have timestamp field', () => {
446
+ const version: ContextVersion = {
447
+ major: 1,
448
+ minor: 0,
449
+ patch: 0,
450
+ timestamp: new Date('2026-01-11T00:00:00Z'),
451
+ }
452
+
453
+ expect(version.timestamp).toBeInstanceOf(Date)
454
+ })
455
+
456
+ it('should support optional hash for integrity', () => {
457
+ const version: ContextVersion = {
458
+ major: 1,
459
+ minor: 0,
460
+ patch: 0,
461
+ timestamp: new Date(),
462
+ hash: 'abc123',
463
+ }
464
+
465
+ expect(version.hash).toBe('abc123')
466
+ })
467
+ })
468
+
469
+ describe('createContextVersion()', () => {
470
+ it('should create initial version 1.0.0', () => {
471
+ const version = createContextVersion()
472
+
473
+ expect(version.major).toBe(1)
474
+ expect(version.minor).toBe(0)
475
+ expect(version.patch).toBe(0)
476
+ expect(version.timestamp).toBeInstanceOf(Date)
477
+ })
478
+
479
+ it('should create version with current timestamp', () => {
480
+ const before = new Date()
481
+ const version = createContextVersion()
482
+ const after = new Date()
483
+
484
+ expect(version.timestamp.getTime()).toBeGreaterThanOrEqual(before.getTime())
485
+ expect(version.timestamp.getTime()).toBeLessThanOrEqual(after.getTime())
486
+ })
487
+
488
+ it('should accept custom version numbers', () => {
489
+ const version = createContextVersion({ major: 2, minor: 1, patch: 5 })
490
+
491
+ expect(version.major).toBe(2)
492
+ expect(version.minor).toBe(1)
493
+ expect(version.patch).toBe(5)
494
+ })
495
+ })
496
+ })
497
+
498
+ // ============================================================================
499
+ // Context Factory Tests
500
+ // ============================================================================
501
+
502
+ describe('Context Factory', () => {
503
+ describe('createCascadeContext()', () => {
504
+ it('should create context with required fields', () => {
505
+ const context = createCascadeContext({
506
+ originAgent: {
507
+ id: 'agent_1',
508
+ tier: 'coordinator',
509
+ name: 'Coordinator',
510
+ },
511
+ task: {
512
+ id: 'task_1',
513
+ type: 'analysis',
514
+ priority: 'normal',
515
+ description: 'Analyze data',
516
+ },
517
+ })
518
+
519
+ expect(context.id).toBeDefined()
520
+ expect(context.version).toBeDefined()
521
+ expect(context.originAgent.id).toBe('agent_1')
522
+ expect(context.currentAgent.id).toBe('agent_1')
523
+ expect(context.task.id).toBe('task_1')
524
+ })
525
+
526
+ it('should generate unique context id', () => {
527
+ const ctx1 = createCascadeContext({
528
+ originAgent: { id: 'a', tier: 'worker', name: 'A' },
529
+ task: { id: 't', type: 'test', priority: 'low', description: 'd' },
530
+ })
531
+ const ctx2 = createCascadeContext({
532
+ originAgent: { id: 'a', tier: 'worker', name: 'A' },
533
+ task: { id: 't', type: 'test', priority: 'low', description: 'd' },
534
+ })
535
+
536
+ expect(ctx1.id).not.toBe(ctx2.id)
537
+ })
538
+
539
+ it('should initialize empty trace array', () => {
540
+ const context = createCascadeContext({
541
+ originAgent: { id: 'a', tier: 'worker', name: 'A' },
542
+ task: { id: 't', type: 'test', priority: 'low', description: 'd' },
543
+ })
544
+
545
+ expect(context.trace).toEqual([])
546
+ })
547
+
548
+ it('should set currentAgent to originAgent by default', () => {
549
+ const context = createCascadeContext({
550
+ originAgent: { id: 'origin', tier: 'coordinator', name: 'Origin' },
551
+ task: { id: 't', type: 'test', priority: 'low', description: 'd' },
552
+ })
553
+
554
+ expect(context.currentAgent.id).toBe('origin')
555
+ expect(context.currentAgent.tier).toBe('coordinator')
556
+ })
557
+
558
+ it('should initialize state with planning phase', () => {
559
+ const context = createCascadeContext({
560
+ originAgent: { id: 'a', tier: 'worker', name: 'A' },
561
+ task: { id: 't', type: 'test', priority: 'low', description: 'd' },
562
+ })
563
+
564
+ expect(context.state.phase).toBe('planning')
565
+ expect(context.state.attempts).toBe(0)
566
+ expect(context.state.startedAt).toBeInstanceOf(Date)
567
+ })
568
+
569
+ it('should accept optional initial data', () => {
570
+ const context = createCascadeContext({
571
+ originAgent: { id: 'a', tier: 'worker', name: 'A' },
572
+ task: { id: 't', type: 'test', priority: 'low', description: 'd' },
573
+ data: { key: 'value' },
574
+ })
575
+
576
+ expect(context.data?.key).toBe('value')
577
+ })
578
+
579
+ it('should accept optional metadata', () => {
580
+ const context = createCascadeContext({
581
+ originAgent: { id: 'a', tier: 'worker', name: 'A' },
582
+ task: { id: 't', type: 'test', priority: 'low', description: 'd' },
583
+ metadata: { source: 'api' },
584
+ })
585
+
586
+ expect(context.metadata?.source).toBe('api')
587
+ })
588
+ })
589
+ })
590
+
591
+ // ============================================================================
592
+ // Context Diff/Merge Utilities Tests
593
+ // ============================================================================
594
+
595
+ describe('Context Diff/Merge Utilities', () => {
596
+ describe('mergeContexts()', () => {
597
+ it('should merge two contexts preferring the newer one', () => {
598
+ const older: AgentCascadeContext = {
599
+ ...mockBaseContext,
600
+ version: { ...mockBaseContext.version, patch: 0 },
601
+ }
602
+ const newer: AgentCascadeContext = {
603
+ ...mockBaseContext,
604
+ version: { ...mockBaseContext.version, patch: 1 },
605
+ data: { newData: true },
606
+ }
607
+
608
+ const merged = mergeContexts(older, newer)
609
+
610
+ expect(merged.version.patch).toBe(1)
611
+ expect(merged.data?.newData).toBe(true)
612
+ })
613
+
614
+ it('should combine trace arrays', () => {
615
+ const ctx1: AgentCascadeContext = {
616
+ ...mockBaseContext,
617
+ trace: [{ agentId: 'a1', tier: 'worker', timestamp: new Date(), action: 'process' }],
618
+ }
619
+ const ctx2: AgentCascadeContext = {
620
+ ...mockBaseContext,
621
+ trace: [{ agentId: 'a2', tier: 'specialist', timestamp: new Date(), action: 'analyze' }],
622
+ }
623
+
624
+ const merged = mergeContexts(ctx1, ctx2)
625
+
626
+ expect(merged.trace).toHaveLength(2)
627
+ })
628
+
629
+ it('should merge data objects deeply', () => {
630
+ const ctx1: AgentCascadeContext = {
631
+ ...mockBaseContext,
632
+ data: { a: 1, nested: { x: 1 } },
633
+ }
634
+ const ctx2: AgentCascadeContext = {
635
+ ...mockBaseContext,
636
+ data: { b: 2, nested: { y: 2 } },
637
+ }
638
+
639
+ const merged = mergeContexts(ctx1, ctx2)
640
+
641
+ expect(merged.data?.a).toBe(1)
642
+ expect(merged.data?.b).toBe(2)
643
+ expect(merged.data?.nested?.x).toBe(1)
644
+ expect(merged.data?.nested?.y).toBe(2)
645
+ })
646
+ })
647
+
648
+ describe('diffContexts()', () => {
649
+ it('should return empty diff for identical contexts', () => {
650
+ const diff = diffContexts(mockBaseContext, mockBaseContext)
651
+
652
+ expect(diff.changes).toHaveLength(0)
653
+ })
654
+
655
+ it('should detect version changes', () => {
656
+ const modified = {
657
+ ...mockBaseContext,
658
+ version: { ...mockBaseContext.version, patch: 5 },
659
+ }
660
+
661
+ const diff = diffContexts(mockBaseContext, modified)
662
+
663
+ expect(diff.changes).toContainEqual(expect.objectContaining({
664
+ path: ['version', 'patch'],
665
+ oldValue: 0,
666
+ newValue: 5,
667
+ }))
668
+ })
669
+
670
+ it('should detect data changes', () => {
671
+ const modified = {
672
+ ...mockBaseContext,
673
+ data: { ...mockBaseContext.data, newField: 'new' },
674
+ }
675
+
676
+ const diff = diffContexts(mockBaseContext, modified)
677
+
678
+ expect(diff.changes.some(c => c.path.includes('data'))).toBe(true)
679
+ })
680
+
681
+ it('should detect agent changes', () => {
682
+ const modified = {
683
+ ...mockBaseContext,
684
+ currentAgent: { id: 'new_agent', tier: 'specialist' as AgentTier, name: 'New' },
685
+ }
686
+
687
+ const diff = diffContexts(mockBaseContext, modified)
688
+
689
+ expect(diff.changes.some(c => c.path.includes('currentAgent'))).toBe(true)
690
+ })
691
+ })
692
+ })