digital-workers 2.1.3 → 2.4.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 (183) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +17 -0
  3. package/README.md +2 -0
  4. package/dist/actions.d.ts.map +1 -1
  5. package/dist/actions.js +33 -21
  6. package/dist/actions.js.map +1 -1
  7. package/dist/agent-comms.d.ts.map +1 -1
  8. package/dist/agent-comms.js +36 -25
  9. package/dist/agent-comms.js.map +1 -1
  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.js +3 -3
  23. package/dist/capability-tiers.js.map +1 -1
  24. package/dist/cascade-context.d.ts +28 -28
  25. package/dist/client.d.ts +162 -0
  26. package/dist/client.d.ts.map +1 -0
  27. package/dist/client.js +64 -0
  28. package/dist/client.js.map +1 -0
  29. package/dist/decide.d.ts +42 -6
  30. package/dist/decide.d.ts.map +1 -1
  31. package/dist/decide.js +54 -11
  32. package/dist/decide.js.map +1 -1
  33. package/dist/do.d.ts +36 -7
  34. package/dist/do.d.ts.map +1 -1
  35. package/dist/do.js +82 -39
  36. package/dist/do.js.map +1 -1
  37. package/dist/error-escalation.d.ts.map +1 -1
  38. package/dist/error-escalation.js +38 -38
  39. package/dist/error-escalation.js.map +1 -1
  40. package/dist/generate.d.ts +48 -7
  41. package/dist/generate.d.ts.map +1 -1
  42. package/dist/generate.js +49 -8
  43. package/dist/generate.js.map +1 -1
  44. package/dist/goals.d.ts +10 -9
  45. package/dist/goals.d.ts.map +1 -1
  46. package/dist/goals.js +30 -24
  47. package/dist/goals.js.map +1 -1
  48. package/dist/image.d.ts +189 -0
  49. package/dist/image.d.ts.map +1 -0
  50. package/dist/image.js +528 -0
  51. package/dist/image.js.map +1 -0
  52. package/dist/index.d.ts +49 -2
  53. package/dist/index.d.ts.map +1 -1
  54. package/dist/index.js +58 -2
  55. package/dist/index.js.map +1 -1
  56. package/dist/is.d.ts +45 -10
  57. package/dist/is.d.ts.map +1 -1
  58. package/dist/is.js +56 -21
  59. package/dist/is.js.map +1 -1
  60. package/dist/kpis.d.ts +24 -15
  61. package/dist/kpis.d.ts.map +1 -1
  62. package/dist/kpis.js +16 -14
  63. package/dist/kpis.js.map +1 -1
  64. package/dist/load-balancing.d.ts.map +1 -1
  65. package/dist/load-balancing.js +124 -38
  66. package/dist/load-balancing.js.map +1 -1
  67. package/dist/logger.d.ts +76 -0
  68. package/dist/logger.d.ts.map +1 -0
  69. package/dist/logger.js +39 -0
  70. package/dist/logger.js.map +1 -0
  71. package/dist/notify.d.ts +38 -9
  72. package/dist/notify.d.ts.map +1 -1
  73. package/dist/notify.js +72 -17
  74. package/dist/notify.js.map +1 -1
  75. package/dist/role.d.ts +5 -4
  76. package/dist/role.d.ts.map +1 -1
  77. package/dist/role.js +13 -10
  78. package/dist/role.js.map +1 -1
  79. package/dist/runtime.d.ts +310 -0
  80. package/dist/runtime.d.ts.map +1 -0
  81. package/dist/runtime.js +510 -0
  82. package/dist/runtime.js.map +1 -0
  83. package/dist/team.d.ts +11 -6
  84. package/dist/team.d.ts.map +1 -1
  85. package/dist/team.js +22 -15
  86. package/dist/team.js.map +1 -1
  87. package/dist/transports/email.d.ts +318 -0
  88. package/dist/transports/email.d.ts.map +1 -0
  89. package/dist/transports/email.js +779 -0
  90. package/dist/transports/email.js.map +1 -0
  91. package/dist/transports/slack.d.ts +515 -0
  92. package/dist/transports/slack.d.ts.map +1 -0
  93. package/dist/transports/slack.js +844 -0
  94. package/dist/transports/slack.js.map +1 -0
  95. package/dist/transports.d.ts.map +1 -1
  96. package/dist/transports.js +44 -25
  97. package/dist/transports.js.map +1 -1
  98. package/dist/types.d.ts +141 -19
  99. package/dist/types.d.ts.map +1 -1
  100. package/dist/types.js +5 -0
  101. package/dist/types.js.map +1 -1
  102. package/dist/utils/id.d.ts +19 -0
  103. package/dist/utils/id.d.ts.map +1 -0
  104. package/dist/utils/id.js +21 -0
  105. package/dist/utils/id.js.map +1 -0
  106. package/dist/video.d.ts +203 -0
  107. package/dist/video.d.ts.map +1 -0
  108. package/dist/video.js +528 -0
  109. package/dist/video.js.map +1 -0
  110. package/dist/worker.d.ts +343 -0
  111. package/dist/worker.d.ts.map +1 -0
  112. package/dist/worker.js +698 -0
  113. package/dist/worker.js.map +1 -0
  114. package/package.json +32 -14
  115. package/src/actions.ts +39 -30
  116. package/src/agent-comms.ts +54 -92
  117. package/src/approve.ts +91 -20
  118. package/src/ask.ts +99 -25
  119. package/src/browse.ts +627 -0
  120. package/src/capability-tiers.ts +5 -5
  121. package/src/client.ts +221 -0
  122. package/src/decide.ts +81 -35
  123. package/src/do.ts +98 -52
  124. package/src/error-escalation.ts +55 -67
  125. package/src/generate.ts +52 -18
  126. package/src/goals.ts +36 -27
  127. package/src/image.ts +816 -0
  128. package/src/index.ts +187 -2
  129. package/src/is.ts +59 -25
  130. package/src/kpis.ts +41 -36
  131. package/src/load-balancing.ts +132 -46
  132. package/src/logger.ts +93 -0
  133. package/src/notify.ts +78 -17
  134. package/src/role.ts +30 -20
  135. package/src/runtime.ts +796 -0
  136. package/src/team.ts +24 -19
  137. package/src/transports/email.ts +1160 -0
  138. package/src/transports/slack.ts +1320 -0
  139. package/src/transports.ts +58 -43
  140. package/src/types.ts +174 -46
  141. package/src/utils/id.ts +21 -0
  142. package/src/video.ts +906 -0
  143. package/src/worker.ts +1007 -0
  144. package/test/approve.test.ts +305 -0
  145. package/test/ask.test.ts +274 -0
  146. package/test/browse.test.ts +361 -0
  147. package/test/decide.test.ts +252 -0
  148. package/test/do.test.ts +144 -0
  149. package/test/error-logging.test.ts +357 -0
  150. package/test/generate.test.ts +319 -0
  151. package/test/image.test.ts +398 -0
  152. package/test/is.test.ts +287 -0
  153. package/test/load-balancing-safety.test.ts +404 -0
  154. package/test/notify.test.ts +434 -0
  155. package/test/primitives.test.ts +320 -0
  156. package/test/runtime-integration.test.ts +892 -0
  157. package/test/transports/crypto.test.ts +230 -0
  158. package/test/transports/email.test.ts +866 -0
  159. package/test/transports/id-generation.test.ts +91 -0
  160. package/test/transports/slack.test.ts +760 -0
  161. package/test/type-safety.test.ts +834 -0
  162. package/test/types.test.ts +60 -2
  163. package/test/video.test.ts +530 -0
  164. package/test/worker.test.ts +1433 -0
  165. package/tsconfig.json +4 -1
  166. package/vitest.config.ts +42 -0
  167. package/wrangler.jsonc +36 -0
  168. package/LICENSE +0 -21
  169. package/src/actions.js +0 -436
  170. package/src/approve.js +0 -234
  171. package/src/ask.js +0 -226
  172. package/src/decide.js +0 -244
  173. package/src/do.js +0 -227
  174. package/src/generate.js +0 -298
  175. package/src/goals.js +0 -205
  176. package/src/index.js +0 -68
  177. package/src/is.js +0 -317
  178. package/src/kpis.js +0 -270
  179. package/src/notify.js +0 -219
  180. package/src/role.js +0 -110
  181. package/src/team.js +0 -130
  182. package/src/transports.js +0 -357
  183. package/src/types.js +0 -71
@@ -0,0 +1,834 @@
1
+ /**
2
+ * Type Safety Tests for digital-workers
3
+ *
4
+ * Tests verifying proper handling of optional properties with
5
+ * `exactOptionalPropertyTypes: true` TypeScript configuration.
6
+ *
7
+ * With this setting, optional properties (prop?: T) cannot have `undefined`
8
+ * explicitly assigned. Instead, use either:
9
+ * 1. Conditional assignment pattern: `if (value !== undefined) result.prop = value`
10
+ * 2. Spread pattern: `...(value !== undefined && { prop: value })`
11
+ *
12
+ * @packageDocumentation
13
+ */
14
+
15
+ import { describe, it, expect, expectTypeOf } from 'vitest'
16
+ import type {
17
+ Worker,
18
+ WorkerRef,
19
+ Contacts,
20
+ ContactPreferences,
21
+ EmailContact,
22
+ SlackContact,
23
+ PhoneContact,
24
+ NotifyResult,
25
+ AskResult,
26
+ ApprovalResult,
27
+ DecideResult,
28
+ DoResult,
29
+ WorkerTeam,
30
+ WorkerRole,
31
+ WorkerGoals,
32
+ WorkerKPI,
33
+ WorkerOKR,
34
+ NotifyOptions,
35
+ AskOptions,
36
+ ApproveOptions,
37
+ DecideOptions,
38
+ DoOptions,
39
+ GenerateOptions,
40
+ GenerateResult,
41
+ IsOptions,
42
+ TypeCheckResult,
43
+ } from '../src/types.js'
44
+ import type {
45
+ RouteResult,
46
+ AgentInfo,
47
+ TaskRequest,
48
+ RoutingMetrics,
49
+ AgentAvailability,
50
+ RoutingRule,
51
+ EscalationPath,
52
+ CompositeBalancerConfig,
53
+ } from '../src/load-balancing.js'
54
+ import type {
55
+ ClassifiedError,
56
+ ErrorContext,
57
+ EscalationPolicy,
58
+ RetryConfig,
59
+ RetryState,
60
+ FallbackConfig,
61
+ AgentForFallback,
62
+ RecoveryState,
63
+ EscalationResult,
64
+ HandleErrorOptions,
65
+ } from '../src/error-escalation.js'
66
+ import type {
67
+ EmailMessage,
68
+ EmailSendResult,
69
+ ApprovalRequestData,
70
+ ParsedEmailReply,
71
+ EmailTransportConfig,
72
+ } from '../src/transports/email.js'
73
+ import type {
74
+ SlackMessage,
75
+ SlackTransportConfig,
76
+ WebhookHandlerResult,
77
+ } from '../src/transports/slack.js'
78
+ import type { TransportConfig, MessagePayload, DeliveryResult, Address } from '../src/transports.js'
79
+
80
+ // ============================================================================
81
+ // Helper Type Assertions
82
+ // ============================================================================
83
+
84
+ /**
85
+ * Type-level assertion that T is assignable to U
86
+ */
87
+ type AssertAssignable<T, U> = T extends U ? true : false
88
+
89
+ /**
90
+ * Creates an object with conditional property assignment
91
+ * This is the pattern used throughout the codebase
92
+ */
93
+ function createWithConditionalProps<T extends object>(base: T, optionals: Partial<T>): T {
94
+ const result = { ...base }
95
+ for (const [key, value] of Object.entries(optionals)) {
96
+ if (value !== undefined) {
97
+ ;(result as Record<string, unknown>)[key] = value
98
+ }
99
+ }
100
+ return result
101
+ }
102
+
103
+ // ============================================================================
104
+ // types.ts - Worker Types
105
+ // ============================================================================
106
+
107
+ describe('types.ts - Optional Property Handling', () => {
108
+ describe('Worker interface', () => {
109
+ it('should create Worker with conditional optional properties', () => {
110
+ const id = 'worker_1'
111
+ const name = 'Test Worker'
112
+ const role = undefined // Simulating optional value that might be undefined
113
+
114
+ // Pattern: conditional assignment
115
+ const worker: Worker = {
116
+ id,
117
+ name,
118
+ type: 'human',
119
+ status: 'available',
120
+ contacts: {},
121
+ }
122
+
123
+ if (role !== undefined) {
124
+ worker.role = role as never // This branch won't execute
125
+ }
126
+
127
+ expect(worker.id).toBe('worker_1')
128
+ expect('role' in worker).toBe(false)
129
+ })
130
+
131
+ it('should create Worker with spread pattern for optional properties', () => {
132
+ const capabilityTier: 'code' | undefined = 'code'
133
+ const skills: string[] | undefined = undefined
134
+
135
+ const worker: Worker = {
136
+ id: 'worker_2',
137
+ name: 'Agent',
138
+ type: 'agent',
139
+ status: 'available',
140
+ contacts: {},
141
+ ...(capabilityTier !== undefined && { capabilityTier }),
142
+ ...(skills !== undefined && { skills }),
143
+ }
144
+
145
+ expect(worker.capabilityTier).toBe('code')
146
+ expect('skills' in worker).toBe(false)
147
+ })
148
+
149
+ it('should handle nested optional contact properties', () => {
150
+ const slackUser: string | undefined = 'U12345'
151
+ const slackChannel: string | undefined = undefined
152
+
153
+ const slackContact: SlackContact = {}
154
+ if (slackUser !== undefined) slackContact.user = slackUser
155
+ if (slackChannel !== undefined) slackContact.channel = slackChannel
156
+
157
+ const worker: Worker = {
158
+ id: 'worker_3',
159
+ name: 'Worker',
160
+ type: 'human',
161
+ status: 'available',
162
+ contacts: {
163
+ slack: slackContact,
164
+ },
165
+ }
166
+
167
+ expect((worker.contacts.slack as SlackContact).user).toBe('U12345')
168
+ expect('channel' in (worker.contacts.slack as SlackContact)).toBe(false)
169
+ })
170
+ })
171
+
172
+ describe('WorkerRef interface', () => {
173
+ it('should create WorkerRef with only defined optional properties', () => {
174
+ const type: 'human' | 'agent' | undefined = 'human'
175
+ const name: string | undefined = undefined
176
+ const role: string | undefined = 'Engineer'
177
+
178
+ const ref: WorkerRef = { id: 'ref_1' }
179
+ if (type !== undefined) ref.type = type
180
+ if (name !== undefined) ref.name = name
181
+ if (role !== undefined) ref.role = role
182
+
183
+ expect(ref.type).toBe('human')
184
+ expect('name' in ref).toBe(false)
185
+ expect(ref.role).toBe('Engineer')
186
+ })
187
+ })
188
+
189
+ describe('ContactPreferences interface', () => {
190
+ it('should handle all optional properties correctly', () => {
191
+ const primary: 'email' | undefined = 'email'
192
+ const urgent: 'slack' | undefined = undefined
193
+ const fallback: ('email' | 'slack')[] | undefined = ['email', 'slack']
194
+
195
+ const prefs: ContactPreferences = {}
196
+ if (primary !== undefined) prefs.primary = primary
197
+ if (urgent !== undefined) prefs.urgent = urgent
198
+ if (fallback !== undefined) prefs.fallback = fallback
199
+
200
+ expect(prefs.primary).toBe('email')
201
+ expect('urgent' in prefs).toBe(false)
202
+ expect(prefs.fallback).toEqual(['email', 'slack'])
203
+ })
204
+ })
205
+
206
+ describe('Result types', () => {
207
+ it('should create NotifyResult with conditional optional properties', () => {
208
+ const messageId: string | undefined = 'msg_123'
209
+ const sentAt: Date | undefined = undefined
210
+ const recipients: WorkerRef[] | undefined = [{ id: 'alice' }]
211
+
212
+ const result: NotifyResult = {
213
+ sent: true,
214
+ via: ['email'],
215
+ }
216
+ if (messageId !== undefined) result.messageId = messageId
217
+ if (sentAt !== undefined) result.sentAt = sentAt
218
+ if (recipients !== undefined) result.recipients = recipients
219
+
220
+ expect(result.messageId).toBe('msg_123')
221
+ expect('sentAt' in result).toBe(false)
222
+ expect(result.recipients).toHaveLength(1)
223
+ })
224
+
225
+ it('should create ApprovalResult with conditional optional properties', () => {
226
+ const approvedBy: WorkerRef | undefined = { id: 'manager' }
227
+ const notes: string | undefined = undefined
228
+ const via: 'email' | undefined = 'email'
229
+
230
+ const result: ApprovalResult = { approved: true }
231
+ if (approvedBy !== undefined) result.approvedBy = approvedBy
232
+ if (notes !== undefined) result.notes = notes
233
+ if (via !== undefined) result.via = via
234
+
235
+ expect(result.approvedBy?.id).toBe('manager')
236
+ expect('notes' in result).toBe(false)
237
+ expect(result.via).toBe('email')
238
+ })
239
+ })
240
+
241
+ describe('Options types', () => {
242
+ it('should create NotifyOptions with conditional properties', () => {
243
+ const via: 'slack' | undefined = 'slack'
244
+ const priority: 'high' | undefined = undefined
245
+ const timeout: number | undefined = 5000
246
+
247
+ const options: NotifyOptions = {}
248
+ if (via !== undefined) options.via = via
249
+ if (priority !== undefined) options.priority = priority
250
+ if (timeout !== undefined) options.timeout = timeout
251
+
252
+ expect(options.via).toBe('slack')
253
+ expect('priority' in options).toBe(false)
254
+ expect(options.timeout).toBe(5000)
255
+ })
256
+ })
257
+ })
258
+
259
+ // ============================================================================
260
+ // load-balancing.ts - Route and Agent Types
261
+ // ============================================================================
262
+
263
+ describe('load-balancing.ts - Optional Property Handling', () => {
264
+ describe('RouteResult interface', () => {
265
+ it('should create RouteResult with conditional optional properties', () => {
266
+ const reason: string | undefined = undefined
267
+ const matchScore: number | undefined = 0.95
268
+ const matchedRule: string | null | undefined = 'skill-match'
269
+
270
+ const result: RouteResult = {
271
+ agent: {
272
+ id: 'agent_1',
273
+ name: 'Test Agent',
274
+ type: 'agent',
275
+ status: 'available',
276
+ skills: ['typescript'],
277
+ currentLoad: 0,
278
+ maxLoad: 10,
279
+ contacts: {},
280
+ metadata: {},
281
+ },
282
+ task: {
283
+ id: 'task_1',
284
+ name: 'Test Task',
285
+ requiredSkills: ['typescript'],
286
+ priority: 5,
287
+ metadata: {},
288
+ },
289
+ strategy: 'capability',
290
+ timestamp: new Date(),
291
+ }
292
+ if (reason !== undefined) result.reason = reason
293
+ if (matchScore !== undefined) result.matchScore = matchScore
294
+ if (matchedRule !== undefined) result.matchedRule = matchedRule
295
+
296
+ expect('reason' in result).toBe(false)
297
+ expect(result.matchScore).toBe(0.95)
298
+ expect(result.matchedRule).toBe('skill-match')
299
+ })
300
+
301
+ it('should handle usedDefault and usedFallback optional flags', () => {
302
+ const usedDefault: boolean | undefined = true
303
+ const usedFallback: boolean | undefined = undefined
304
+
305
+ const result: RouteResult = {
306
+ agent: null,
307
+ task: {
308
+ id: 'task_2',
309
+ name: 'Failed Task',
310
+ requiredSkills: [],
311
+ priority: 1,
312
+ metadata: {},
313
+ },
314
+ strategy: 'round-robin',
315
+ timestamp: new Date(),
316
+ reason: 'no-available-agents',
317
+ }
318
+ if (usedDefault !== undefined) result.usedDefault = usedDefault
319
+ if (usedFallback !== undefined) result.usedFallback = usedFallback
320
+
321
+ expect(result.usedDefault).toBe(true)
322
+ expect('usedFallback' in result).toBe(false)
323
+ })
324
+ })
325
+
326
+ describe('TaskRequest interface', () => {
327
+ it('should create TaskRequest with conditional enqueuedAt', () => {
328
+ const enqueuedAt: Date | undefined = new Date()
329
+
330
+ const task: TaskRequest = {
331
+ id: 'task_1',
332
+ name: 'Test',
333
+ requiredSkills: [],
334
+ priority: 5,
335
+ metadata: {},
336
+ }
337
+ if (enqueuedAt !== undefined) task.enqueuedAt = enqueuedAt
338
+
339
+ expect(task.enqueuedAt).toBeInstanceOf(Date)
340
+ })
341
+ })
342
+
343
+ describe('AgentAvailability interface', () => {
344
+ it('should handle optional load tracking', () => {
345
+ const currentLoad: number | undefined = 5
346
+ const maxLoad: number | undefined = undefined
347
+
348
+ const availability: AgentAvailability = {
349
+ status: 'available',
350
+ lastSeen: new Date(),
351
+ }
352
+ if (currentLoad !== undefined) availability.currentLoad = currentLoad
353
+ if (maxLoad !== undefined) availability.maxLoad = maxLoad
354
+
355
+ expect(availability.currentLoad).toBe(5)
356
+ expect('maxLoad' in availability).toBe(false)
357
+ })
358
+ })
359
+
360
+ describe('RoutingRule interface', () => {
361
+ it('should handle optional enabled and priority', () => {
362
+ const enabled: boolean | undefined = true
363
+ const priority: number | undefined = undefined
364
+
365
+ const rule: RoutingRule = {
366
+ name: 'test-rule',
367
+ priority: 1,
368
+ fromTier: 'code',
369
+ toTier: 'generative',
370
+ condition: () => true,
371
+ action: () => null,
372
+ }
373
+ if (enabled !== undefined) rule.enabled = enabled
374
+ if (priority !== undefined) rule.priority = priority
375
+
376
+ expect(rule.enabled).toBe(true)
377
+ })
378
+ })
379
+
380
+ describe('CompositeBalancerConfig interface', () => {
381
+ it('should handle optional fallbackBehavior and customStrategies', () => {
382
+ const fallbackBehavior: 'next-strategy' | 'none' | undefined = 'next-strategy'
383
+ const customStrategies:
384
+ | Record<string, (t: TaskRequest, a: AgentInfo[]) => AgentInfo | null>
385
+ | undefined = undefined
386
+
387
+ const config: CompositeBalancerConfig = {
388
+ strategies: ['round-robin', 'least-busy'],
389
+ }
390
+ if (fallbackBehavior !== undefined) config.fallbackBehavior = fallbackBehavior
391
+ if (customStrategies !== undefined) config.customStrategies = customStrategies
392
+
393
+ expect(config.fallbackBehavior).toBe('next-strategy')
394
+ expect('customStrategies' in config).toBe(false)
395
+ })
396
+ })
397
+ })
398
+
399
+ // ============================================================================
400
+ // error-escalation.ts - Error and Recovery Types
401
+ // ============================================================================
402
+
403
+ describe('error-escalation.ts - Optional Property Handling', () => {
404
+ describe('ClassifiedError interface', () => {
405
+ it('should create ClassifiedError with conditional optional properties', () => {
406
+ const tier: 'code' | undefined = 'code'
407
+ const agentId: string | undefined = undefined
408
+ const taskId: string | undefined = 'task_123'
409
+ const stack: string | undefined = 'Error: test\n at test.js:1'
410
+ const context: ErrorContext | undefined = { workflowId: 'wf_1' }
411
+
412
+ const error: ClassifiedError = {
413
+ id: 'err_1',
414
+ original: new Error('Test error'),
415
+ severity: 'medium',
416
+ category: 'transient',
417
+ timestamp: new Date(),
418
+ }
419
+ if (tier !== undefined) error.tier = tier
420
+ if (agentId !== undefined) error.agentId = agentId
421
+ if (taskId !== undefined) error.taskId = taskId
422
+ if (stack !== undefined) error.stack = stack
423
+ if (context !== undefined) error.context = context
424
+
425
+ expect(error.tier).toBe('code')
426
+ expect('agentId' in error).toBe(false)
427
+ expect(error.taskId).toBe('task_123')
428
+ expect(error.context?.workflowId).toBe('wf_1')
429
+ })
430
+ })
431
+
432
+ describe('ErrorContext interface', () => {
433
+ it('should handle all optional fields', () => {
434
+ const workflowId: string | undefined = 'wf_1'
435
+ const stepId: string | undefined = undefined
436
+ const attemptNumber: number | undefined = 3
437
+ const metadata: Record<string, unknown> | undefined = { key: 'value' }
438
+
439
+ const context: ErrorContext = {}
440
+ if (workflowId !== undefined) context.workflowId = workflowId
441
+ if (stepId !== undefined) context.stepId = stepId
442
+ if (attemptNumber !== undefined) context.attemptNumber = attemptNumber
443
+ if (metadata !== undefined) context.metadata = metadata
444
+
445
+ expect(context.workflowId).toBe('wf_1')
446
+ expect('stepId' in context).toBe(false)
447
+ expect(context.attemptNumber).toBe(3)
448
+ })
449
+ })
450
+
451
+ describe('EscalationPolicy interface', () => {
452
+ it('should handle optional skipTierThreshold and tierPolicies', () => {
453
+ const skipTierThreshold: 'high' | undefined = 'high'
454
+ const tierPolicies: Record<string, { maxRetries?: number }> | undefined = undefined
455
+
456
+ const policy: EscalationPolicy = {
457
+ maxEscalationDepth: 5,
458
+ allowSkipTiers: true,
459
+ rules: [],
460
+ }
461
+ if (skipTierThreshold !== undefined) policy.skipTierThreshold = skipTierThreshold
462
+ if (tierPolicies !== undefined) policy.tierPolicies = tierPolicies
463
+
464
+ expect(policy.skipTierThreshold).toBe('high')
465
+ expect('tierPolicies' in policy).toBe(false)
466
+ })
467
+ })
468
+
469
+ describe('RetryState interface', () => {
470
+ it('should handle nullable Date properties', () => {
471
+ // Note: These are explicitly typed as Date | null, not optional
472
+ // This is correct usage - null means "not set yet"
473
+ const state: RetryState = {
474
+ attemptNumber: 0,
475
+ lastAttemptTime: null,
476
+ nextRetryTime: null,
477
+ exhausted: false,
478
+ }
479
+
480
+ expect(state.lastAttemptTime).toBeNull()
481
+ expect(state.nextRetryTime).toBeNull()
482
+ })
483
+ })
484
+
485
+ describe('FallbackConfig interface', () => {
486
+ it('should handle optional configuration fields', () => {
487
+ const requiredSkills: string[] | undefined = ['typescript']
488
+ const currentTier: 'code' | undefined = undefined
489
+ const excludeAgentIds: string[] | undefined = ['agent_1']
490
+
491
+ const config: FallbackConfig = {
492
+ strategy: 'capability-match',
493
+ }
494
+ if (requiredSkills !== undefined) config.requiredSkills = requiredSkills
495
+ if (currentTier !== undefined) config.currentTier = currentTier
496
+ if (excludeAgentIds !== undefined) config.excludeAgentIds = excludeAgentIds
497
+
498
+ expect(config.requiredSkills).toEqual(['typescript'])
499
+ expect('currentTier' in config).toBe(false)
500
+ })
501
+ })
502
+
503
+ describe('RecoveryState interface', () => {
504
+ it('should handle optional agentId and resolution', () => {
505
+ const agentId: string | undefined = 'agent_1'
506
+ const resolution: string | undefined = undefined
507
+ const isTerminal: boolean | undefined = false
508
+
509
+ const state: RecoveryState = {
510
+ errorId: 'err_1',
511
+ tier: 'code',
512
+ retryState: {
513
+ attemptNumber: 0,
514
+ lastAttemptTime: null,
515
+ nextRetryTime: null,
516
+ exhausted: false,
517
+ },
518
+ escalated: false,
519
+ resolved: false,
520
+ escalationPath: ['code'],
521
+ fallbackHistory: [],
522
+ }
523
+ if (agentId !== undefined) state.agentId = agentId
524
+ if (resolution !== undefined) state.resolution = resolution
525
+ if (isTerminal !== undefined) state.isTerminal = isTerminal
526
+
527
+ expect(state.agentId).toBe('agent_1')
528
+ expect('resolution' in state).toBe(false)
529
+ expect(state.isTerminal).toBe(false)
530
+ })
531
+ })
532
+
533
+ describe('EscalationResult interface', () => {
534
+ it('should handle conditional result properties', () => {
535
+ const retryDelay: number | undefined = 1000
536
+ const escalationPath: EscalationPath | undefined = undefined
537
+ const degradationLevel: 'partial' | undefined = 'partial'
538
+
539
+ const result: EscalationResult = {
540
+ handled: true,
541
+ action: 'retry',
542
+ classifiedError: {
543
+ id: 'err_1',
544
+ original: new Error('Test'),
545
+ severity: 'low',
546
+ category: 'transient',
547
+ timestamp: new Date(),
548
+ },
549
+ }
550
+ if (retryDelay !== undefined) result.retryDelay = retryDelay
551
+ if (escalationPath !== undefined) result.escalationPath = escalationPath
552
+ if (degradationLevel !== undefined) result.degradationLevel = degradationLevel
553
+
554
+ expect(result.retryDelay).toBe(1000)
555
+ expect('escalationPath' in result).toBe(false)
556
+ expect(result.degradationLevel).toBe('partial')
557
+ })
558
+ })
559
+ })
560
+
561
+ // ============================================================================
562
+ // transports/*.ts - Transport Types
563
+ // ============================================================================
564
+
565
+ describe('transports/email.ts - Optional Property Handling', () => {
566
+ describe('EmailMessage interface', () => {
567
+ it('should create EmailMessage with conditional optional properties', () => {
568
+ const replyTo: string | undefined = 'reply@example.com'
569
+ const text: string | undefined = undefined
570
+ const html: string | undefined = '<p>Hello</p>'
571
+
572
+ const message: EmailMessage = {
573
+ to: 'test@example.com',
574
+ from: 'sender@example.com',
575
+ subject: 'Test',
576
+ }
577
+ if (replyTo !== undefined) message.replyTo = replyTo
578
+ if (text !== undefined) message.text = text
579
+ if (html !== undefined) message.html = html
580
+
581
+ expect(message.replyTo).toBe('reply@example.com')
582
+ expect('text' in message).toBe(false)
583
+ expect(message.html).toBe('<p>Hello</p>')
584
+ })
585
+ })
586
+
587
+ describe('EmailSendResult interface', () => {
588
+ it('should handle optional messageId and error', () => {
589
+ const messageId: string | undefined = 'msg_123'
590
+ const error: string | undefined = undefined
591
+
592
+ const result: EmailSendResult = { success: true }
593
+ if (messageId !== undefined) result.messageId = messageId
594
+ if (error !== undefined) result.error = error
595
+
596
+ expect(result.messageId).toBe('msg_123')
597
+ expect('error' in result).toBe(false)
598
+ })
599
+ })
600
+
601
+ describe('ParsedEmailReply interface', () => {
602
+ it('should handle multiple optional fields', () => {
603
+ const approved: boolean | undefined = true
604
+ const requestId: string | undefined = 'req_123'
605
+ const notes: string | undefined = undefined
606
+ const from: string | undefined = 'user@example.com'
607
+
608
+ const reply: ParsedEmailReply = {
609
+ isApprovalResponse: true,
610
+ }
611
+ if (approved !== undefined) reply.approved = approved
612
+ if (requestId !== undefined) reply.requestId = requestId
613
+ if (notes !== undefined) reply.notes = notes
614
+ if (from !== undefined) reply.from = from
615
+
616
+ expect(reply.approved).toBe(true)
617
+ expect(reply.requestId).toBe('req_123')
618
+ expect('notes' in reply).toBe(false)
619
+ expect(reply.from).toBe('user@example.com')
620
+ })
621
+ })
622
+ })
623
+
624
+ describe('transports/slack.ts - Optional Property Handling', () => {
625
+ describe('SlackMessage interface', () => {
626
+ it('should handle optional thread and metadata', () => {
627
+ const thread_ts: string | undefined = '1234567890.123456'
628
+ const reply_broadcast: boolean | undefined = undefined
629
+
630
+ const message: SlackMessage = {
631
+ channel: '#general',
632
+ text: 'Hello',
633
+ }
634
+ if (thread_ts !== undefined) message.thread_ts = thread_ts
635
+ if (reply_broadcast !== undefined) message.reply_broadcast = reply_broadcast
636
+
637
+ expect(message.thread_ts).toBe('1234567890.123456')
638
+ expect('reply_broadcast' in message).toBe(false)
639
+ })
640
+ })
641
+
642
+ describe('WebhookHandlerResult interface', () => {
643
+ it('should handle multiple optional fields', () => {
644
+ const actionId: string | undefined = 'approve_123'
645
+ const userId: string | undefined = 'U12345'
646
+ const channelId: string | undefined = undefined
647
+ const error: string | undefined = undefined
648
+
649
+ const result: WebhookHandlerResult = { success: true }
650
+ if (actionId !== undefined) result.actionId = actionId
651
+ if (userId !== undefined) result.userId = userId
652
+ if (channelId !== undefined) result.channelId = channelId
653
+ if (error !== undefined) result.error = error
654
+
655
+ expect(result.actionId).toBe('approve_123')
656
+ expect(result.userId).toBe('U12345')
657
+ expect('channelId' in result).toBe(false)
658
+ expect('error' in result).toBe(false)
659
+ })
660
+ })
661
+ })
662
+
663
+ describe('transports.ts - Optional Property Handling', () => {
664
+ describe('DeliveryResult interface', () => {
665
+ it('should handle optional messageId, error, and metadata', () => {
666
+ const messageId: string | undefined = 'msg_123'
667
+ const error: string | undefined = undefined
668
+ const metadata: Record<string, unknown> | undefined = { provider: 'resend' }
669
+
670
+ const result: DeliveryResult = {
671
+ success: true,
672
+ transport: 'email',
673
+ }
674
+ if (messageId !== undefined) result.messageId = messageId
675
+ if (error !== undefined) result.error = error
676
+ if (metadata !== undefined) result.metadata = metadata
677
+
678
+ expect(result.messageId).toBe('msg_123')
679
+ expect('error' in result).toBe(false)
680
+ expect(result.metadata?.['provider']).toBe('resend')
681
+ })
682
+ })
683
+
684
+ describe('Address interface', () => {
685
+ it('should handle optional name and metadata', () => {
686
+ const name: string | undefined = 'Test User'
687
+ const metadata: Record<string, unknown> | undefined = undefined
688
+
689
+ const address: Address = {
690
+ transport: 'email',
691
+ value: 'test@example.com',
692
+ }
693
+ if (name !== undefined) address.name = name
694
+ if (metadata !== undefined) address.metadata = metadata
695
+
696
+ expect(address.name).toBe('Test User')
697
+ expect('metadata' in address).toBe(false)
698
+ })
699
+ })
700
+
701
+ describe('MessagePayload interface', () => {
702
+ it('should handle many optional message properties', () => {
703
+ const from: string | undefined = 'system'
704
+ const subject: string | undefined = undefined
705
+ const priority: 'high' | undefined = 'high'
706
+ const threadId: string | undefined = undefined
707
+
708
+ const payload: MessagePayload = {
709
+ to: 'user@example.com',
710
+ body: 'Message content',
711
+ type: 'notification',
712
+ }
713
+ if (from !== undefined) payload.from = from
714
+ if (subject !== undefined) payload.subject = subject
715
+ if (priority !== undefined) payload.priority = priority
716
+ if (threadId !== undefined) payload.threadId = threadId
717
+
718
+ expect(payload.from).toBe('system')
719
+ expect('subject' in payload).toBe(false)
720
+ expect(payload.priority).toBe('high')
721
+ expect('threadId' in payload).toBe(false)
722
+ })
723
+ })
724
+ })
725
+
726
+ // ============================================================================
727
+ // Spread Pattern Tests
728
+ // ============================================================================
729
+
730
+ describe('Spread Pattern for Optional Properties', () => {
731
+ it('should use spread pattern for object literals', () => {
732
+ const optional1: string | undefined = 'value1'
733
+ const optional2: string | undefined = undefined
734
+ const optional3: number | undefined = 42
735
+
736
+ interface TestInterface {
737
+ required: string
738
+ optional1?: string
739
+ optional2?: string
740
+ optional3?: number
741
+ }
742
+
743
+ const result: TestInterface = {
744
+ required: 'base',
745
+ ...(optional1 !== undefined && { optional1 }),
746
+ ...(optional2 !== undefined && { optional2 }),
747
+ ...(optional3 !== undefined && { optional3 }),
748
+ }
749
+
750
+ expect(result.required).toBe('base')
751
+ expect(result.optional1).toBe('value1')
752
+ expect('optional2' in result).toBe(false)
753
+ expect(result.optional3).toBe(42)
754
+ })
755
+
756
+ it('should work with nested optional properties', () => {
757
+ const nestedProp: { inner?: string } | undefined = { inner: 'nested' }
758
+ const anotherNested: { value?: number } | undefined = undefined
759
+
760
+ interface OuterInterface {
761
+ id: string
762
+ nested?: { inner?: string }
763
+ another?: { value?: number }
764
+ }
765
+
766
+ const result: OuterInterface = {
767
+ id: 'test',
768
+ ...(nestedProp !== undefined && { nested: nestedProp }),
769
+ ...(anotherNested !== undefined && { another: anotherNested }),
770
+ }
771
+
772
+ expect(result.nested?.inner).toBe('nested')
773
+ expect('another' in result).toBe(false)
774
+ })
775
+ })
776
+
777
+ // ============================================================================
778
+ // Type-Level Tests
779
+ // ============================================================================
780
+
781
+ describe('Type-Level Assertions', () => {
782
+ it('should verify Worker type structure', () => {
783
+ // Type-level test - ensures the interface structure is correct
784
+ type WorkerKeys = keyof Worker
785
+ expectTypeOf<WorkerKeys>().toMatchTypeOf<
786
+ | 'id'
787
+ | 'name'
788
+ | 'type'
789
+ | 'status'
790
+ | 'contacts'
791
+ | 'preferences'
792
+ | 'role'
793
+ | 'teams'
794
+ | 'skills'
795
+ | 'tools'
796
+ | 'capabilityTier'
797
+ | 'capabilityProfile'
798
+ | 'metadata'
799
+ >()
800
+ })
801
+
802
+ it('should verify RouteResult optional properties are not required', () => {
803
+ // This should compile - reason is optional
804
+ const minimalResult: RouteResult = {
805
+ agent: null,
806
+ task: {
807
+ id: '1',
808
+ name: 'test',
809
+ requiredSkills: [],
810
+ priority: 1,
811
+ metadata: {},
812
+ },
813
+ strategy: 'round-robin',
814
+ timestamp: new Date(),
815
+ }
816
+
817
+ expect(minimalResult).toBeDefined()
818
+ expect('reason' in minimalResult).toBe(false)
819
+ })
820
+
821
+ it('should verify ClassifiedError optional properties are not required', () => {
822
+ // This should compile - tier, agentId, etc. are optional
823
+ const minimalError: ClassifiedError = {
824
+ id: 'err_1',
825
+ original: new Error('test'),
826
+ severity: 'low',
827
+ category: 'transient',
828
+ timestamp: new Date(),
829
+ }
830
+
831
+ expect(minimalError).toBeDefined()
832
+ expect('tier' in minimalError).toBe(false)
833
+ })
834
+ })