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.
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +17 -0
- package/README.md +2 -0
- package/dist/actions.d.ts.map +1 -1
- package/dist/actions.js +33 -21
- package/dist/actions.js.map +1 -1
- package/dist/agent-comms.d.ts.map +1 -1
- package/dist/agent-comms.js +36 -25
- package/dist/agent-comms.js.map +1 -1
- package/dist/approve.d.ts +40 -8
- package/dist/approve.d.ts.map +1 -1
- package/dist/approve.js +86 -20
- package/dist/approve.js.map +1 -1
- package/dist/ask.d.ts +38 -7
- package/dist/ask.d.ts.map +1 -1
- package/dist/ask.js +85 -25
- package/dist/ask.js.map +1 -1
- package/dist/browse.d.ts +223 -0
- package/dist/browse.d.ts.map +1 -0
- package/dist/browse.js +392 -0
- package/dist/browse.js.map +1 -0
- package/dist/capability-tiers.js +3 -3
- package/dist/capability-tiers.js.map +1 -1
- package/dist/cascade-context.d.ts +28 -28
- package/dist/client.d.ts +162 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +64 -0
- package/dist/client.js.map +1 -0
- package/dist/decide.d.ts +42 -6
- package/dist/decide.d.ts.map +1 -1
- package/dist/decide.js +54 -11
- package/dist/decide.js.map +1 -1
- package/dist/do.d.ts +36 -7
- package/dist/do.d.ts.map +1 -1
- package/dist/do.js +82 -39
- package/dist/do.js.map +1 -1
- package/dist/error-escalation.d.ts.map +1 -1
- package/dist/error-escalation.js +38 -38
- package/dist/error-escalation.js.map +1 -1
- package/dist/generate.d.ts +48 -7
- package/dist/generate.d.ts.map +1 -1
- package/dist/generate.js +49 -8
- package/dist/generate.js.map +1 -1
- package/dist/goals.d.ts +10 -9
- package/dist/goals.d.ts.map +1 -1
- package/dist/goals.js +30 -24
- package/dist/goals.js.map +1 -1
- package/dist/image.d.ts +189 -0
- package/dist/image.d.ts.map +1 -0
- package/dist/image.js +528 -0
- package/dist/image.js.map +1 -0
- package/dist/index.d.ts +49 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +58 -2
- package/dist/index.js.map +1 -1
- package/dist/is.d.ts +45 -10
- package/dist/is.d.ts.map +1 -1
- package/dist/is.js +56 -21
- package/dist/is.js.map +1 -1
- package/dist/kpis.d.ts +24 -15
- package/dist/kpis.d.ts.map +1 -1
- package/dist/kpis.js +16 -14
- package/dist/kpis.js.map +1 -1
- package/dist/load-balancing.d.ts.map +1 -1
- package/dist/load-balancing.js +124 -38
- package/dist/load-balancing.js.map +1 -1
- package/dist/logger.d.ts +76 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +39 -0
- package/dist/logger.js.map +1 -0
- package/dist/notify.d.ts +38 -9
- package/dist/notify.d.ts.map +1 -1
- package/dist/notify.js +72 -17
- package/dist/notify.js.map +1 -1
- package/dist/role.d.ts +5 -4
- package/dist/role.d.ts.map +1 -1
- package/dist/role.js +13 -10
- package/dist/role.js.map +1 -1
- package/dist/runtime.d.ts +310 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +510 -0
- package/dist/runtime.js.map +1 -0
- package/dist/team.d.ts +11 -6
- package/dist/team.d.ts.map +1 -1
- package/dist/team.js +22 -15
- package/dist/team.js.map +1 -1
- package/dist/transports/email.d.ts +318 -0
- package/dist/transports/email.d.ts.map +1 -0
- package/dist/transports/email.js +779 -0
- package/dist/transports/email.js.map +1 -0
- package/dist/transports/slack.d.ts +515 -0
- package/dist/transports/slack.d.ts.map +1 -0
- package/dist/transports/slack.js +844 -0
- package/dist/transports/slack.js.map +1 -0
- package/dist/transports.d.ts.map +1 -1
- package/dist/transports.js +44 -25
- package/dist/transports.js.map +1 -1
- package/dist/types.d.ts +141 -19
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -1
- package/dist/utils/id.d.ts +19 -0
- package/dist/utils/id.d.ts.map +1 -0
- package/dist/utils/id.js +21 -0
- package/dist/utils/id.js.map +1 -0
- package/dist/video.d.ts +203 -0
- package/dist/video.d.ts.map +1 -0
- package/dist/video.js +528 -0
- package/dist/video.js.map +1 -0
- package/dist/worker.d.ts +343 -0
- package/dist/worker.d.ts.map +1 -0
- package/dist/worker.js +698 -0
- package/dist/worker.js.map +1 -0
- package/package.json +32 -14
- package/src/actions.ts +39 -30
- package/src/agent-comms.ts +54 -92
- package/src/approve.ts +91 -20
- package/src/ask.ts +99 -25
- package/src/browse.ts +627 -0
- package/src/capability-tiers.ts +5 -5
- package/src/client.ts +221 -0
- package/src/decide.ts +81 -35
- package/src/do.ts +98 -52
- package/src/error-escalation.ts +55 -67
- package/src/generate.ts +52 -18
- package/src/goals.ts +36 -27
- package/src/image.ts +816 -0
- package/src/index.ts +187 -2
- package/src/is.ts +59 -25
- package/src/kpis.ts +41 -36
- package/src/load-balancing.ts +132 -46
- package/src/logger.ts +93 -0
- package/src/notify.ts +78 -17
- package/src/role.ts +30 -20
- package/src/runtime.ts +796 -0
- package/src/team.ts +24 -19
- package/src/transports/email.ts +1160 -0
- package/src/transports/slack.ts +1320 -0
- package/src/transports.ts +58 -43
- package/src/types.ts +174 -46
- package/src/utils/id.ts +21 -0
- package/src/video.ts +906 -0
- package/src/worker.ts +1007 -0
- package/test/approve.test.ts +305 -0
- package/test/ask.test.ts +274 -0
- package/test/browse.test.ts +361 -0
- package/test/decide.test.ts +252 -0
- package/test/do.test.ts +144 -0
- package/test/error-logging.test.ts +357 -0
- package/test/generate.test.ts +319 -0
- package/test/image.test.ts +398 -0
- package/test/is.test.ts +287 -0
- package/test/load-balancing-safety.test.ts +404 -0
- package/test/notify.test.ts +434 -0
- package/test/primitives.test.ts +320 -0
- package/test/runtime-integration.test.ts +892 -0
- package/test/transports/crypto.test.ts +230 -0
- package/test/transports/email.test.ts +866 -0
- package/test/transports/id-generation.test.ts +91 -0
- package/test/transports/slack.test.ts +760 -0
- package/test/type-safety.test.ts +834 -0
- package/test/types.test.ts +60 -2
- package/test/video.test.ts +530 -0
- package/test/worker.test.ts +1433 -0
- package/tsconfig.json +4 -1
- package/vitest.config.ts +42 -0
- package/wrangler.jsonc +36 -0
- package/LICENSE +0 -21
- package/src/actions.js +0 -436
- package/src/approve.js +0 -234
- package/src/ask.js +0 -226
- package/src/decide.js +0 -244
- package/src/do.js +0 -227
- package/src/generate.js +0 -298
- package/src/goals.js +0 -205
- package/src/index.js +0 -68
- package/src/is.js +0 -317
- package/src/kpis.js +0 -270
- package/src/notify.js +0 -219
- package/src/role.js +0 -110
- package/src/team.js +0 -130
- package/src/transports.js +0 -357
- package/src/types.js +0 -71
package/test/do.test.ts
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for do() - Task execution primitive
|
|
3
|
+
*
|
|
4
|
+
* The do() function routes tasks to appropriate Workers (AI Agents or Humans)
|
|
5
|
+
* based on capability matching. Unlike ai-functions.do() which directly calls
|
|
6
|
+
* the LLM, this function provides worker coordination with retries and timeouts.
|
|
7
|
+
*
|
|
8
|
+
* These tests use real AI calls via the Cloudflare AI Gateway.
|
|
9
|
+
* Tests are skipped if AI_GATEWAY_URL is not configured.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { describe, it, expect } from 'vitest'
|
|
13
|
+
import { do as doTask } from '../src/index.js'
|
|
14
|
+
|
|
15
|
+
// Skip tests if no gateway configured
|
|
16
|
+
const hasGateway = !!process.env.AI_GATEWAY_URL || !!process.env.ANTHROPIC_API_KEY
|
|
17
|
+
|
|
18
|
+
describe('do() - Task Execution Primitive', () => {
|
|
19
|
+
describe('Unit Tests (no AI)', () => {
|
|
20
|
+
it('should be exported from index', () => {
|
|
21
|
+
expect(doTask).toBeDefined()
|
|
22
|
+
expect(typeof doTask).toBe('function')
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('should have parallel method', () => {
|
|
26
|
+
expect(doTask.parallel).toBeDefined()
|
|
27
|
+
expect(typeof doTask.parallel).toBe('function')
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('should have sequence method', () => {
|
|
31
|
+
expect(doTask.sequence).toBeDefined()
|
|
32
|
+
expect(typeof doTask.sequence).toBe('function')
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('should have withDependencies method', () => {
|
|
36
|
+
expect(doTask.withDependencies).toBeDefined()
|
|
37
|
+
expect(typeof doTask.withDependencies).toBe('function')
|
|
38
|
+
})
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
describe.skipIf(!hasGateway)('Integration Tests (with AI)', () => {
|
|
42
|
+
it('should execute a simple task', async () => {
|
|
43
|
+
const result = await doTask('Calculate 2 + 2 and return the result', {
|
|
44
|
+
timeout: 30000,
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
expect(result).toBeDefined()
|
|
48
|
+
expect(typeof result.success).toBe('boolean')
|
|
49
|
+
expect(typeof result.duration).toBe('number')
|
|
50
|
+
expect(result.duration).toBeGreaterThan(0)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('should execute task with context', async () => {
|
|
54
|
+
const result = await doTask('Summarize the provided text', {
|
|
55
|
+
timeout: 30000,
|
|
56
|
+
context: {
|
|
57
|
+
text: 'Hello world. This is a simple test message.',
|
|
58
|
+
},
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
expect(result).toBeDefined()
|
|
62
|
+
expect(result.success).toBeDefined()
|
|
63
|
+
expect(result.duration).toBeGreaterThan(0)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('should return steps array', async () => {
|
|
67
|
+
const result = await doTask('List 3 colors', {
|
|
68
|
+
timeout: 30000,
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
expect(result).toBeDefined()
|
|
72
|
+
expect(Array.isArray(result.steps)).toBe(true)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('should handle task with structured context', async () => {
|
|
76
|
+
const result = await doTask<{ name: string; greeting: string }>(
|
|
77
|
+
'Generate a greeting for the user',
|
|
78
|
+
{
|
|
79
|
+
timeout: 30000,
|
|
80
|
+
context: {
|
|
81
|
+
user: { name: 'Alice', language: 'English' },
|
|
82
|
+
},
|
|
83
|
+
}
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
expect(result).toBeDefined()
|
|
87
|
+
expect(result.success).toBeDefined()
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('should handle timeout option', async () => {
|
|
91
|
+
// Very short timeout to test timeout behavior
|
|
92
|
+
const result = await doTask('Count from 1 to 5', {
|
|
93
|
+
timeout: 1, // 1ms timeout - will likely fail
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
// Either succeeds quickly or fails due to timeout
|
|
97
|
+
expect(result).toBeDefined()
|
|
98
|
+
expect(typeof result.success).toBe('boolean')
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('should execute parallel tasks', async () => {
|
|
102
|
+
const results = await doTask.parallel(['What is 1+1?', 'What is 2+2?', 'What is 3+3?'], {
|
|
103
|
+
timeout: 30000,
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
expect(results).toBeDefined()
|
|
107
|
+
expect(Array.isArray(results)).toBe(true)
|
|
108
|
+
expect(results.length).toBe(3)
|
|
109
|
+
results.forEach((result) => {
|
|
110
|
+
expect(result.success).toBeDefined()
|
|
111
|
+
expect(result.duration).toBeDefined()
|
|
112
|
+
})
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('should execute sequential tasks', async () => {
|
|
116
|
+
const results = await doTask.sequence(['Say hello', 'Say goodbye'], { timeout: 30000 })
|
|
117
|
+
|
|
118
|
+
expect(results).toBeDefined()
|
|
119
|
+
expect(Array.isArray(results)).toBe(true)
|
|
120
|
+
expect(results.length).toBe(2)
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it('should handle task with retries option', async () => {
|
|
124
|
+
const result = await doTask('Return the word "success"', {
|
|
125
|
+
maxRetries: 1,
|
|
126
|
+
timeout: 30000,
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
expect(result).toBeDefined()
|
|
130
|
+
expect(result.success).toBeDefined()
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
it('should support background execution option', async () => {
|
|
134
|
+
const result = await doTask('Generate a random number', {
|
|
135
|
+
background: true,
|
|
136
|
+
timeout: 30000,
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
expect(result).toBeDefined()
|
|
140
|
+
// Background tasks still return a result
|
|
141
|
+
expect(result.success).toBeDefined()
|
|
142
|
+
})
|
|
143
|
+
})
|
|
144
|
+
})
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error Logging Tests
|
|
3
|
+
*
|
|
4
|
+
* TDD tests verifying that catch blocks log errors instead of silently returning null.
|
|
5
|
+
* Following Red-Green-Refactor methodology.
|
|
6
|
+
*
|
|
7
|
+
* Bead issue: aip-prsc
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
11
|
+
import type { Logger } from '../src/logger.js'
|
|
12
|
+
import { createSlackTransport } from '../src/transports/slack.js'
|
|
13
|
+
import type { SlackTransportConfig } from '../src/transports/slack.js'
|
|
14
|
+
|
|
15
|
+
// ============================================================================
|
|
16
|
+
// Test Fixtures
|
|
17
|
+
// ============================================================================
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Creates a mock logger for testing
|
|
21
|
+
*/
|
|
22
|
+
function createMockLogger(): Logger & {
|
|
23
|
+
calls: {
|
|
24
|
+
debug: Array<{ msg: string; meta?: object }>
|
|
25
|
+
info: Array<{ msg: string; meta?: object }>
|
|
26
|
+
warn: Array<{ msg: string; meta?: object }>
|
|
27
|
+
error: Array<{ msg: string; error?: Error; meta?: object }>
|
|
28
|
+
}
|
|
29
|
+
} {
|
|
30
|
+
const calls = {
|
|
31
|
+
debug: [] as Array<{ msg: string; meta?: object }>,
|
|
32
|
+
info: [] as Array<{ msg: string; meta?: object }>,
|
|
33
|
+
warn: [] as Array<{ msg: string; meta?: object }>,
|
|
34
|
+
error: [] as Array<{ msg: string; error?: Error; meta?: object }>,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
calls,
|
|
39
|
+
debug(msg: string, meta?: object) {
|
|
40
|
+
calls.debug.push({ msg, meta })
|
|
41
|
+
},
|
|
42
|
+
info(msg: string, meta?: object) {
|
|
43
|
+
calls.info.push({ msg, meta })
|
|
44
|
+
},
|
|
45
|
+
warn(msg: string, meta?: object) {
|
|
46
|
+
calls.warn.push({ msg, meta })
|
|
47
|
+
},
|
|
48
|
+
error(msg: string, error?: Error, meta?: object) {
|
|
49
|
+
calls.error.push({ msg, error, meta })
|
|
50
|
+
},
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Test configuration
|
|
55
|
+
const testConfig: Omit<SlackTransportConfig, 'transport'> = {
|
|
56
|
+
botToken: 'xoxb-test-token',
|
|
57
|
+
signingSecret: 'test-signing-secret',
|
|
58
|
+
apiUrl: 'https://slack.test/api',
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ============================================================================
|
|
62
|
+
// Slack Transport Error Logging Tests
|
|
63
|
+
// ============================================================================
|
|
64
|
+
|
|
65
|
+
describe('SlackTransport Error Logging', () => {
|
|
66
|
+
let mockLogger: ReturnType<typeof createMockLogger>
|
|
67
|
+
let mockFetch: ReturnType<typeof vi.fn>
|
|
68
|
+
let originalFetch: typeof globalThis.fetch
|
|
69
|
+
|
|
70
|
+
beforeEach(() => {
|
|
71
|
+
mockLogger = createMockLogger()
|
|
72
|
+
mockFetch = vi.fn()
|
|
73
|
+
originalFetch = globalThis.fetch
|
|
74
|
+
globalThis.fetch = mockFetch
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
afterEach(() => {
|
|
78
|
+
globalThis.fetch = originalFetch
|
|
79
|
+
vi.restoreAllMocks()
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
describe('lookupUserByEmail', () => {
|
|
83
|
+
it('should log error when API call throws', async () => {
|
|
84
|
+
// Arrange: Make fetch throw an error
|
|
85
|
+
const apiError = new Error('Network error')
|
|
86
|
+
mockFetch.mockRejectedValue(apiError)
|
|
87
|
+
|
|
88
|
+
const transport = createSlackTransport({
|
|
89
|
+
...testConfig,
|
|
90
|
+
logger: mockLogger,
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
// Act
|
|
94
|
+
const result = await transport.lookupUserByEmail('test@example.com')
|
|
95
|
+
|
|
96
|
+
// Assert: Original behavior preserved
|
|
97
|
+
expect(result).toBeNull()
|
|
98
|
+
|
|
99
|
+
// Assert: Error was logged with context
|
|
100
|
+
expect(mockLogger.calls.error.length).toBe(1)
|
|
101
|
+
expect(mockLogger.calls.error[0].msg).toContain('lookupUserByEmail')
|
|
102
|
+
expect(mockLogger.calls.error[0].error).toBe(apiError)
|
|
103
|
+
expect(mockLogger.calls.error[0].meta).toMatchObject({
|
|
104
|
+
email: 'test@example.com',
|
|
105
|
+
})
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it('should not log when no error occurs', async () => {
|
|
109
|
+
// Arrange: Successful API response
|
|
110
|
+
mockFetch.mockResolvedValue({
|
|
111
|
+
ok: true,
|
|
112
|
+
json: () => Promise.resolve({ ok: true, user: { id: 'U123' } }),
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
const transport = createSlackTransport({
|
|
116
|
+
...testConfig,
|
|
117
|
+
logger: mockLogger,
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
// Act
|
|
121
|
+
const result = await transport.lookupUserByEmail('test@example.com')
|
|
122
|
+
|
|
123
|
+
// Assert
|
|
124
|
+
expect(result).toBe('U123')
|
|
125
|
+
expect(mockLogger.calls.error.length).toBe(0)
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it('should work without logger (backward compatibility)', async () => {
|
|
129
|
+
// Arrange: Make fetch throw an error
|
|
130
|
+
mockFetch.mockRejectedValue(new Error('Network error'))
|
|
131
|
+
|
|
132
|
+
const transport = createSlackTransport(testConfig)
|
|
133
|
+
|
|
134
|
+
// Act & Assert: Should not throw when no logger provided
|
|
135
|
+
const result = await transport.lookupUserByEmail('test@example.com')
|
|
136
|
+
expect(result).toBeNull()
|
|
137
|
+
})
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
describe('parseWebhookPayload', () => {
|
|
141
|
+
it('should log error when JSON parsing fails', () => {
|
|
142
|
+
// Arrange
|
|
143
|
+
const transport = createSlackTransport({
|
|
144
|
+
...testConfig,
|
|
145
|
+
logger: mockLogger,
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
// Create invalid JSON payload
|
|
149
|
+
const invalidRequest = {
|
|
150
|
+
body: 'payload={invalid-json}',
|
|
151
|
+
headers: {},
|
|
152
|
+
timestamp: Date.now().toString(),
|
|
153
|
+
signature: 'v0=invalid',
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Act
|
|
157
|
+
const result = transport.parseWebhookPayloadForTesting(invalidRequest)
|
|
158
|
+
|
|
159
|
+
// Assert: Original behavior preserved
|
|
160
|
+
expect(result).toBeNull()
|
|
161
|
+
|
|
162
|
+
// Assert: Error was logged
|
|
163
|
+
expect(mockLogger.calls.error.length).toBe(1)
|
|
164
|
+
expect(mockLogger.calls.error[0].msg).toContain('parseWebhookPayload')
|
|
165
|
+
expect(mockLogger.calls.error[0].error).toBeInstanceOf(SyntaxError)
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
it('should not log when parsing succeeds', () => {
|
|
169
|
+
// Arrange
|
|
170
|
+
const transport = createSlackTransport({
|
|
171
|
+
...testConfig,
|
|
172
|
+
logger: mockLogger,
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
const validPayload = { type: 'block_actions', user: { id: 'U123' } }
|
|
176
|
+
const validRequest = {
|
|
177
|
+
body: `payload=${encodeURIComponent(JSON.stringify(validPayload))}`,
|
|
178
|
+
headers: {},
|
|
179
|
+
timestamp: Date.now().toString(),
|
|
180
|
+
signature: 'v0=test',
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Act
|
|
184
|
+
const result = transport.parseWebhookPayloadForTesting(validRequest)
|
|
185
|
+
|
|
186
|
+
// Assert
|
|
187
|
+
expect(result).toEqual(validPayload)
|
|
188
|
+
expect(mockLogger.calls.error.length).toBe(0)
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
it('should work without logger (backward compatibility)', () => {
|
|
192
|
+
// Arrange
|
|
193
|
+
const transport = createSlackTransport(testConfig)
|
|
194
|
+
|
|
195
|
+
const invalidRequest = {
|
|
196
|
+
body: 'payload={invalid-json}',
|
|
197
|
+
headers: {},
|
|
198
|
+
timestamp: Date.now().toString(),
|
|
199
|
+
signature: 'v0=invalid',
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Act & Assert: Should not throw when no logger provided
|
|
203
|
+
const result = transport.parseWebhookPayloadForTesting(invalidRequest)
|
|
204
|
+
expect(result).toBeNull()
|
|
205
|
+
})
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
describe('parseActionValue', () => {
|
|
209
|
+
it('should log debug when JSON parsing fails (expected for string values)', () => {
|
|
210
|
+
// Arrange
|
|
211
|
+
const transport = createSlackTransport({
|
|
212
|
+
...testConfig,
|
|
213
|
+
logger: mockLogger,
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
// Act: Parse a plain string (not JSON)
|
|
217
|
+
const result = transport.parseActionValueForTesting('plain-string-value')
|
|
218
|
+
|
|
219
|
+
// Assert: Original behavior preserved (returns original value)
|
|
220
|
+
expect(result).toBe('plain-string-value')
|
|
221
|
+
|
|
222
|
+
// Assert: Debug was logged (this is expected behavior, not an error)
|
|
223
|
+
expect(mockLogger.calls.debug.length).toBe(1)
|
|
224
|
+
expect(mockLogger.calls.debug[0].msg).toContain('parseActionValue')
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
it('should not log when JSON parsing succeeds', () => {
|
|
228
|
+
// Arrange
|
|
229
|
+
const transport = createSlackTransport({
|
|
230
|
+
...testConfig,
|
|
231
|
+
logger: mockLogger,
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
const jsonValue = { action: 'approve', data: 123 }
|
|
235
|
+
|
|
236
|
+
// Act
|
|
237
|
+
const result = transport.parseActionValueForTesting(JSON.stringify(jsonValue))
|
|
238
|
+
|
|
239
|
+
// Assert
|
|
240
|
+
expect(result).toEqual(jsonValue)
|
|
241
|
+
expect(mockLogger.calls.debug.length).toBe(0)
|
|
242
|
+
})
|
|
243
|
+
})
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
// ============================================================================
|
|
247
|
+
// Logger Interface Tests
|
|
248
|
+
// ============================================================================
|
|
249
|
+
|
|
250
|
+
describe('Logger Interface', () => {
|
|
251
|
+
it('should define all required log levels', async () => {
|
|
252
|
+
// Import the Logger interface and verify it has all required methods
|
|
253
|
+
const { Logger } = await import('../src/logger.js')
|
|
254
|
+
|
|
255
|
+
// This test verifies the interface exists - actual type checking is done by TypeScript
|
|
256
|
+
const mockLogger: Logger = {
|
|
257
|
+
debug: vi.fn(),
|
|
258
|
+
info: vi.fn(),
|
|
259
|
+
warn: vi.fn(),
|
|
260
|
+
error: vi.fn(),
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Verify methods are callable
|
|
264
|
+
mockLogger.debug('test debug')
|
|
265
|
+
mockLogger.info('test info', { key: 'value' })
|
|
266
|
+
mockLogger.warn('test warn')
|
|
267
|
+
mockLogger.error('test error', new Error('test'), { context: 'test' })
|
|
268
|
+
|
|
269
|
+
expect(mockLogger.debug).toHaveBeenCalledTimes(1)
|
|
270
|
+
expect(mockLogger.info).toHaveBeenCalledTimes(1)
|
|
271
|
+
expect(mockLogger.warn).toHaveBeenCalledTimes(1)
|
|
272
|
+
expect(mockLogger.error).toHaveBeenCalledTimes(1)
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
it('should allow optional meta parameter', async () => {
|
|
276
|
+
const { Logger } = await import('../src/logger.js')
|
|
277
|
+
|
|
278
|
+
const mockLogger: Logger = {
|
|
279
|
+
debug: vi.fn(),
|
|
280
|
+
info: vi.fn(),
|
|
281
|
+
warn: vi.fn(),
|
|
282
|
+
error: vi.fn(),
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// All methods should work without meta
|
|
286
|
+
mockLogger.debug('no meta')
|
|
287
|
+
mockLogger.info('no meta')
|
|
288
|
+
mockLogger.warn('no meta')
|
|
289
|
+
mockLogger.error('no meta')
|
|
290
|
+
|
|
291
|
+
// Error method should work with error but no meta
|
|
292
|
+
mockLogger.error('with error', new Error('test'))
|
|
293
|
+
|
|
294
|
+
expect(mockLogger.debug).toHaveBeenCalledWith('no meta')
|
|
295
|
+
expect(mockLogger.error).toHaveBeenCalledWith('with error', new Error('test'))
|
|
296
|
+
})
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
// ============================================================================
|
|
300
|
+
// Error Context Preservation Tests
|
|
301
|
+
// ============================================================================
|
|
302
|
+
|
|
303
|
+
describe('Error Context Preservation', () => {
|
|
304
|
+
let mockLogger: ReturnType<typeof createMockLogger>
|
|
305
|
+
let mockFetch: ReturnType<typeof vi.fn>
|
|
306
|
+
let originalFetch: typeof globalThis.fetch
|
|
307
|
+
|
|
308
|
+
beforeEach(() => {
|
|
309
|
+
mockLogger = createMockLogger()
|
|
310
|
+
mockFetch = vi.fn()
|
|
311
|
+
originalFetch = globalThis.fetch
|
|
312
|
+
globalThis.fetch = mockFetch
|
|
313
|
+
})
|
|
314
|
+
|
|
315
|
+
afterEach(() => {
|
|
316
|
+
globalThis.fetch = originalFetch
|
|
317
|
+
})
|
|
318
|
+
|
|
319
|
+
it('should preserve error stack trace', async () => {
|
|
320
|
+
// Arrange
|
|
321
|
+
const apiError = new Error('API failure')
|
|
322
|
+
mockFetch.mockRejectedValue(apiError)
|
|
323
|
+
|
|
324
|
+
const transport = createSlackTransport({
|
|
325
|
+
...testConfig,
|
|
326
|
+
logger: mockLogger,
|
|
327
|
+
})
|
|
328
|
+
|
|
329
|
+
// Act
|
|
330
|
+
await transport.lookupUserByEmail('test@example.com')
|
|
331
|
+
|
|
332
|
+
// Assert: Error object with stack trace is passed to logger
|
|
333
|
+
const loggedError = mockLogger.calls.error[0].error
|
|
334
|
+
expect(loggedError).toBe(apiError)
|
|
335
|
+
expect(loggedError?.stack).toBeDefined()
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
it('should include operation context in log metadata', async () => {
|
|
339
|
+
// Arrange
|
|
340
|
+
mockFetch.mockRejectedValue(new Error('Network error'))
|
|
341
|
+
|
|
342
|
+
const transport = createSlackTransport({
|
|
343
|
+
...testConfig,
|
|
344
|
+
logger: mockLogger,
|
|
345
|
+
})
|
|
346
|
+
|
|
347
|
+
// Act
|
|
348
|
+
await transport.lookupUserByEmail('user@example.com')
|
|
349
|
+
|
|
350
|
+
// Assert: Context metadata is included
|
|
351
|
+
const meta = mockLogger.calls.error[0].meta
|
|
352
|
+
expect(meta).toMatchObject({
|
|
353
|
+
email: 'user@example.com',
|
|
354
|
+
operation: 'lookupUserByEmail',
|
|
355
|
+
})
|
|
356
|
+
})
|
|
357
|
+
})
|