smithers-orchestrator 0.1.12 → 0.1.14
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/bunfig.toml +3 -4
- package/package.json +8 -1
- package/preload.ts +9 -0
- package/src/components/Claude.test.tsx +97 -0
- package/src/components/Phase.tsx +3 -2
- package/src/components/Ralph.test.tsx +111 -0
- package/src/components/Review.test.tsx +231 -0
- package/src/components/components.test.tsx +158 -0
- package/src/core/execute.test.ts +84 -0
- package/src/core/root.test.ts +83 -0
- package/src/core/serialize.test.ts +205 -0
- package/src/jsx-runtime.test.ts +237 -0
- package/src/orchestrator/monitor/output-parser.test.ts +165 -0
- package/src/orchestrator/monitor/stream-formatter.test.ts +224 -0
- package/src/orchestrator/tools/registry.test.ts +234 -0
- package/src/solid/h.test.ts +163 -0
- package/src/utils/vcs.test.ts +213 -0
package/bunfig.toml
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
# Bun configuration for Smithers
|
|
2
2
|
# https://bun.sh/docs/runtime/bunfig
|
|
3
|
-
# Usage: bun -c node_modules/smithers-orchestrator/bunfig.toml your-file.tsx
|
|
4
3
|
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
# Use bun-plugin-solid for Solid JSX transform
|
|
5
|
+
preload = ["./preload.ts"]
|
|
7
6
|
|
|
8
7
|
[install]
|
|
9
8
|
auto = "fallback"
|
|
10
9
|
|
|
11
10
|
[test]
|
|
12
|
-
preload = ["./test/preload.ts"]
|
|
11
|
+
preload = ["./preload.ts", "./test/preload.ts"]
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "smithers-orchestrator",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.14",
|
|
4
4
|
"description": "Build AI agents with Solid.js - Declarative JSX for Claude orchestration",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
},
|
|
10
10
|
"exports": {
|
|
11
11
|
".": "./src/index.ts",
|
|
12
|
+
"./jsx-runtime": "./src/jsx-runtime.ts",
|
|
13
|
+
"./jsx-dev-runtime": "./src/jsx-runtime.ts",
|
|
12
14
|
"./core": "./src/core/index.ts",
|
|
13
15
|
"./solid": "./src/solid/index.ts",
|
|
14
16
|
"./components": "./src/components/index.ts",
|
|
@@ -34,6 +36,7 @@
|
|
|
34
36
|
"skills",
|
|
35
37
|
"templates",
|
|
36
38
|
"bunfig.toml",
|
|
39
|
+
"preload.ts",
|
|
37
40
|
"plugin.json",
|
|
38
41
|
"README.md"
|
|
39
42
|
],
|
|
@@ -46,7 +49,11 @@
|
|
|
46
49
|
"dependencies": {
|
|
47
50
|
"@anthropic-ai/claude-agent-sdk": "^0.1.76",
|
|
48
51
|
"@anthropic-ai/sdk": "^0.71.2",
|
|
52
|
+
"@babel/core": "^7.28.6",
|
|
53
|
+
"@babel/preset-typescript": "^7.28.5",
|
|
54
|
+
"@dschz/bun-plugin-solid": "^1.0.4",
|
|
49
55
|
"@electric-sql/pglite": "^0.3.15",
|
|
56
|
+
"babel-preset-solid": "^1.9.10",
|
|
50
57
|
"commander": "^12.0.0",
|
|
51
58
|
"solid-js": "^1.9.10",
|
|
52
59
|
"zod": "^4.3.5"
|
package/preload.ts
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for Claude.tsx - Claude component interface tests.
|
|
3
|
+
* Tests the component's props and interface, not execution behavior.
|
|
4
|
+
*/
|
|
5
|
+
import { describe, test, expect, mock } from 'bun:test'
|
|
6
|
+
import type { ClaudeProps } from './Claude'
|
|
7
|
+
|
|
8
|
+
describe('ClaudeProps interface', () => {
|
|
9
|
+
test('model is optional string', () => {
|
|
10
|
+
const props: ClaudeProps = {}
|
|
11
|
+
expect(props.model).toBeUndefined()
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
test('model can be set', () => {
|
|
15
|
+
const props: ClaudeProps = { model: 'claude-opus-4' }
|
|
16
|
+
expect(props.model).toBe('claude-opus-4')
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
test('maxTurns is optional number', () => {
|
|
20
|
+
const props: ClaudeProps = { maxTurns: 5 }
|
|
21
|
+
expect(props.maxTurns).toBe(5)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
test('tools is optional string array', () => {
|
|
25
|
+
const props: ClaudeProps = { tools: ['Read', 'Edit', 'Bash'] }
|
|
26
|
+
expect(props.tools).toHaveLength(3)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
test('systemPrompt is optional string', () => {
|
|
30
|
+
const props: ClaudeProps = { systemPrompt: 'You are a helpful assistant' }
|
|
31
|
+
expect(props.systemPrompt).toBe('You are a helpful assistant')
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
test('onFinished is optional callback', () => {
|
|
35
|
+
const callback = mock(() => {})
|
|
36
|
+
const props: ClaudeProps = { onFinished: callback }
|
|
37
|
+
|
|
38
|
+
props.onFinished?.('result')
|
|
39
|
+
expect(callback).toHaveBeenCalledWith('result')
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
test('onError is optional callback', () => {
|
|
43
|
+
const callback = mock(() => {})
|
|
44
|
+
const props: ClaudeProps = { onError: callback }
|
|
45
|
+
|
|
46
|
+
const error = new Error('test')
|
|
47
|
+
props.onError?.(error)
|
|
48
|
+
expect(callback).toHaveBeenCalledWith(error)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
test('validate is optional async function', async () => {
|
|
52
|
+
const validate = mock(async () => true)
|
|
53
|
+
const props: ClaudeProps = { validate }
|
|
54
|
+
|
|
55
|
+
const result = await props.validate?.('test')
|
|
56
|
+
expect(result).toBe(true)
|
|
57
|
+
expect(validate).toHaveBeenCalledWith('test')
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
test('validate can return false', async () => {
|
|
61
|
+
const validate = mock(async () => false)
|
|
62
|
+
const props: ClaudeProps = { validate }
|
|
63
|
+
|
|
64
|
+
const result = await props.validate?.('invalid')
|
|
65
|
+
expect(result).toBe(false)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
test('allows arbitrary additional props', () => {
|
|
69
|
+
const props: ClaudeProps = {
|
|
70
|
+
customProp: 'value',
|
|
71
|
+
numberProp: 42,
|
|
72
|
+
boolProp: true,
|
|
73
|
+
objectProp: { key: 'value' },
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
expect(props.customProp).toBe('value')
|
|
77
|
+
expect(props.numberProp).toBe(42)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
test('children is optional', () => {
|
|
81
|
+
const props: ClaudeProps = {}
|
|
82
|
+
expect(props.children).toBeUndefined()
|
|
83
|
+
})
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
describe('Claude component behavior', () => {
|
|
87
|
+
test('exports Claude function', async () => {
|
|
88
|
+
const { Claude } = await import('./Claude')
|
|
89
|
+
expect(typeof Claude).toBe('function')
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
test('Claude is a valid Solid component', async () => {
|
|
93
|
+
const { Claude } = await import('./Claude')
|
|
94
|
+
// A component should accept props
|
|
95
|
+
expect(Claude.length).toBeLessThanOrEqual(1)
|
|
96
|
+
})
|
|
97
|
+
})
|
package/src/components/Phase.tsx
CHANGED
|
@@ -11,9 +11,10 @@ export interface PhaseProps {
|
|
|
11
11
|
* Provides semantic structure to agent workflows.
|
|
12
12
|
*/
|
|
13
13
|
export function Phase(props: PhaseProps): JSX.Element {
|
|
14
|
+
const { children, ...rest } = props
|
|
14
15
|
return (
|
|
15
|
-
<phase
|
|
16
|
-
{
|
|
16
|
+
<phase {...rest}>
|
|
17
|
+
{children}
|
|
17
18
|
</phase>
|
|
18
19
|
)
|
|
19
20
|
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for Ralph.tsx - Ralph orchestration component.
|
|
3
|
+
*/
|
|
4
|
+
import { describe, test, expect, mock } from 'bun:test'
|
|
5
|
+
import {
|
|
6
|
+
RalphContext,
|
|
7
|
+
createOrchestrationPromise,
|
|
8
|
+
signalOrchestrationComplete,
|
|
9
|
+
signalOrchestrationError,
|
|
10
|
+
} from './Ralph'
|
|
11
|
+
|
|
12
|
+
describe('RalphContext', () => {
|
|
13
|
+
test('RalphContext is exported', () => {
|
|
14
|
+
expect(RalphContext).toBeDefined()
|
|
15
|
+
})
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
describe('Orchestration promise functions', () => {
|
|
19
|
+
test('createOrchestrationPromise returns a promise', () => {
|
|
20
|
+
const promise = createOrchestrationPromise()
|
|
21
|
+
|
|
22
|
+
expect(promise).toBeInstanceOf(Promise)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
test('signalOrchestrationComplete resolves the promise', async () => {
|
|
26
|
+
const promise = createOrchestrationPromise()
|
|
27
|
+
|
|
28
|
+
// Signal completion in next tick
|
|
29
|
+
setTimeout(() => signalOrchestrationComplete(), 0)
|
|
30
|
+
|
|
31
|
+
await promise
|
|
32
|
+
// If we get here, the promise resolved
|
|
33
|
+
expect(true).toBe(true)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
test('signalOrchestrationError rejects the promise', async () => {
|
|
37
|
+
const promise = createOrchestrationPromise()
|
|
38
|
+
const error = new Error('Test error')
|
|
39
|
+
|
|
40
|
+
// Signal error in next tick
|
|
41
|
+
setTimeout(() => signalOrchestrationError(error), 0)
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
await promise
|
|
45
|
+
expect(true).toBe(false) // Should not reach here
|
|
46
|
+
} catch (e) {
|
|
47
|
+
expect(e).toBe(error)
|
|
48
|
+
}
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
test('signalOrchestrationComplete is safe to call without promise', () => {
|
|
52
|
+
// Should not throw even if no promise exists
|
|
53
|
+
signalOrchestrationComplete()
|
|
54
|
+
expect(true).toBe(true)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
test('signalOrchestrationError is safe to call without promise', () => {
|
|
58
|
+
// Should not throw even if no promise exists
|
|
59
|
+
signalOrchestrationError(new Error('Test'))
|
|
60
|
+
expect(true).toBe(true)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
test('calling complete twice is safe', async () => {
|
|
64
|
+
const promise = createOrchestrationPromise()
|
|
65
|
+
|
|
66
|
+
setTimeout(() => {
|
|
67
|
+
signalOrchestrationComplete()
|
|
68
|
+
signalOrchestrationComplete() // Second call should be no-op
|
|
69
|
+
}, 0)
|
|
70
|
+
|
|
71
|
+
await promise
|
|
72
|
+
expect(true).toBe(true)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
test('calling error after complete is no-op', async () => {
|
|
76
|
+
const promise = createOrchestrationPromise()
|
|
77
|
+
|
|
78
|
+
setTimeout(() => {
|
|
79
|
+
signalOrchestrationComplete()
|
|
80
|
+
signalOrchestrationError(new Error('Should not reject')) // Should be no-op
|
|
81
|
+
}, 0)
|
|
82
|
+
|
|
83
|
+
await promise
|
|
84
|
+
expect(true).toBe(true)
|
|
85
|
+
})
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
describe('RalphContextType interface', () => {
|
|
89
|
+
test('context value has registerTask and completeTask', () => {
|
|
90
|
+
// Create a mock context value
|
|
91
|
+
const contextValue = {
|
|
92
|
+
registerTask: mock(() => {}),
|
|
93
|
+
completeTask: mock(() => {}),
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
expect(contextValue.registerTask).toBeDefined()
|
|
97
|
+
expect(contextValue.completeTask).toBeDefined()
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
test('registerTask can be called', () => {
|
|
101
|
+
const registerTask = mock(() => {})
|
|
102
|
+
registerTask()
|
|
103
|
+
expect(registerTask).toHaveBeenCalled()
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
test('completeTask can be called', () => {
|
|
107
|
+
const completeTask = mock(() => {})
|
|
108
|
+
completeTask()
|
|
109
|
+
expect(completeTask).toHaveBeenCalled()
|
|
110
|
+
})
|
|
111
|
+
})
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for Review.tsx - Code review component interface tests.
|
|
3
|
+
*/
|
|
4
|
+
import { describe, test, expect, mock } from 'bun:test'
|
|
5
|
+
import type { ReviewTarget, ReviewResult, ReviewIssue, ReviewProps } from './Review'
|
|
6
|
+
|
|
7
|
+
describe('ReviewTarget interface', () => {
|
|
8
|
+
test('commit target with optional ref', () => {
|
|
9
|
+
const target: ReviewTarget = { type: 'commit' }
|
|
10
|
+
expect(target.type).toBe('commit')
|
|
11
|
+
expect(target.ref).toBeUndefined()
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
test('commit target with ref', () => {
|
|
15
|
+
const target: ReviewTarget = { type: 'commit', ref: 'abc123' }
|
|
16
|
+
expect(target.type).toBe('commit')
|
|
17
|
+
expect(target.ref).toBe('abc123')
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
test('diff target without ref', () => {
|
|
21
|
+
const target: ReviewTarget = { type: 'diff' }
|
|
22
|
+
expect(target.type).toBe('diff')
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
test('diff target with ref', () => {
|
|
26
|
+
const target: ReviewTarget = { type: 'diff', ref: 'feature-branch' }
|
|
27
|
+
expect(target.type).toBe('diff')
|
|
28
|
+
expect(target.ref).toBe('feature-branch')
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
test('pr target requires ref', () => {
|
|
32
|
+
const target: ReviewTarget = { type: 'pr', ref: '123' }
|
|
33
|
+
expect(target.type).toBe('pr')
|
|
34
|
+
expect(target.ref).toBe('123')
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
test('files target with files array', () => {
|
|
38
|
+
const target: ReviewTarget = {
|
|
39
|
+
type: 'files',
|
|
40
|
+
files: ['src/index.ts', 'src/utils.ts'],
|
|
41
|
+
}
|
|
42
|
+
expect(target.type).toBe('files')
|
|
43
|
+
expect(target.files).toHaveLength(2)
|
|
44
|
+
})
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
describe('ReviewResult interface', () => {
|
|
48
|
+
test('approved result structure', () => {
|
|
49
|
+
const result: ReviewResult = {
|
|
50
|
+
approved: true,
|
|
51
|
+
summary: 'Looks good!',
|
|
52
|
+
issues: [],
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
expect(result.approved).toBe(true)
|
|
56
|
+
expect(result.summary).toBe('Looks good!')
|
|
57
|
+
expect(result.issues).toHaveLength(0)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
test('rejected result with issues', () => {
|
|
61
|
+
const result: ReviewResult = {
|
|
62
|
+
approved: false,
|
|
63
|
+
summary: 'Found issues',
|
|
64
|
+
issues: [
|
|
65
|
+
{
|
|
66
|
+
severity: 'critical',
|
|
67
|
+
message: 'Security vulnerability',
|
|
68
|
+
file: 'auth.ts',
|
|
69
|
+
line: 42,
|
|
70
|
+
suggestion: 'Use parameterized queries',
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
expect(result.approved).toBe(false)
|
|
76
|
+
expect(result.issues).toHaveLength(1)
|
|
77
|
+
expect(result.issues[0].severity).toBe('critical')
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
test('result with multiple issues', () => {
|
|
81
|
+
const result: ReviewResult = {
|
|
82
|
+
approved: false,
|
|
83
|
+
summary: 'Multiple issues found',
|
|
84
|
+
issues: [
|
|
85
|
+
{ severity: 'critical', message: 'Issue 1' },
|
|
86
|
+
{ severity: 'major', message: 'Issue 2' },
|
|
87
|
+
{ severity: 'minor', message: 'Issue 3' },
|
|
88
|
+
],
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
expect(result.issues).toHaveLength(3)
|
|
92
|
+
})
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
describe('ReviewIssue interface', () => {
|
|
96
|
+
test('minimal issue', () => {
|
|
97
|
+
const issue: ReviewIssue = {
|
|
98
|
+
severity: 'minor',
|
|
99
|
+
message: 'Consider adding a comment',
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
expect(issue.severity).toBe('minor')
|
|
103
|
+
expect(issue.message).toBe('Consider adding a comment')
|
|
104
|
+
expect(issue.file).toBeUndefined()
|
|
105
|
+
expect(issue.line).toBeUndefined()
|
|
106
|
+
expect(issue.suggestion).toBeUndefined()
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
test('full issue with all fields', () => {
|
|
110
|
+
const issue: ReviewIssue = {
|
|
111
|
+
severity: 'major',
|
|
112
|
+
file: 'component.tsx',
|
|
113
|
+
line: 100,
|
|
114
|
+
message: 'Missing error handling',
|
|
115
|
+
suggestion: 'Add try-catch block',
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
expect(issue.severity).toBe('major')
|
|
119
|
+
expect(issue.file).toBe('component.tsx')
|
|
120
|
+
expect(issue.line).toBe(100)
|
|
121
|
+
expect(issue.suggestion).toBe('Add try-catch block')
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
test('critical severity', () => {
|
|
125
|
+
const issue: ReviewIssue = { severity: 'critical', message: 'Critical' }
|
|
126
|
+
expect(issue.severity).toBe('critical')
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
test('major severity', () => {
|
|
130
|
+
const issue: ReviewIssue = { severity: 'major', message: 'Major' }
|
|
131
|
+
expect(issue.severity).toBe('major')
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
test('minor severity', () => {
|
|
135
|
+
const issue: ReviewIssue = { severity: 'minor', message: 'Minor' }
|
|
136
|
+
expect(issue.severity).toBe('minor')
|
|
137
|
+
})
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
describe('ReviewProps interface', () => {
|
|
141
|
+
test('minimal props with target', () => {
|
|
142
|
+
const props: ReviewProps = {
|
|
143
|
+
target: { type: 'commit' },
|
|
144
|
+
}
|
|
145
|
+
expect(props.target.type).toBe('commit')
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
test('props with agent', () => {
|
|
149
|
+
const props: ReviewProps = {
|
|
150
|
+
target: { type: 'commit' },
|
|
151
|
+
agent: 'claude',
|
|
152
|
+
}
|
|
153
|
+
expect(props.agent).toBe('claude')
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
test('props with model', () => {
|
|
157
|
+
const props: ReviewProps = {
|
|
158
|
+
target: { type: 'commit' },
|
|
159
|
+
model: 'claude-opus-4',
|
|
160
|
+
}
|
|
161
|
+
expect(props.model).toBe('claude-opus-4')
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
test('props with blocking', () => {
|
|
165
|
+
const props: ReviewProps = {
|
|
166
|
+
target: { type: 'commit' },
|
|
167
|
+
blocking: true,
|
|
168
|
+
}
|
|
169
|
+
expect(props.blocking).toBe(true)
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
test('props with criteria', () => {
|
|
173
|
+
const props: ReviewProps = {
|
|
174
|
+
target: { type: 'commit' },
|
|
175
|
+
criteria: ['Check security', 'Check performance'],
|
|
176
|
+
}
|
|
177
|
+
expect(props.criteria).toHaveLength(2)
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
test('props with postToGitHub', () => {
|
|
181
|
+
const props: ReviewProps = {
|
|
182
|
+
target: { type: 'pr', ref: '123' },
|
|
183
|
+
postToGitHub: true,
|
|
184
|
+
}
|
|
185
|
+
expect(props.postToGitHub).toBe(true)
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
test('props with postToGitNotes', () => {
|
|
189
|
+
const props: ReviewProps = {
|
|
190
|
+
target: { type: 'commit' },
|
|
191
|
+
postToGitNotes: true,
|
|
192
|
+
}
|
|
193
|
+
expect(props.postToGitNotes).toBe(true)
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
test('onFinished callback', () => {
|
|
197
|
+
const callback = mock(() => {})
|
|
198
|
+
const props: ReviewProps = {
|
|
199
|
+
target: { type: 'commit' },
|
|
200
|
+
onFinished: callback,
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const result: ReviewResult = { approved: true, summary: 'Good', issues: [] }
|
|
204
|
+
props.onFinished?.(result)
|
|
205
|
+
expect(callback).toHaveBeenCalledWith(result)
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
test('onError callback', () => {
|
|
209
|
+
const callback = mock(() => {})
|
|
210
|
+
const props: ReviewProps = {
|
|
211
|
+
target: { type: 'commit' },
|
|
212
|
+
onError: callback,
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const error = new Error('test')
|
|
216
|
+
props.onError?.(error)
|
|
217
|
+
expect(callback).toHaveBeenCalledWith(error)
|
|
218
|
+
})
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
describe('Review component', () => {
|
|
222
|
+
test('exports Review function', async () => {
|
|
223
|
+
const { Review } = await import('./Review')
|
|
224
|
+
expect(typeof Review).toBe('function')
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
test('Review is a valid Solid component', async () => {
|
|
228
|
+
const { Review } = await import('./Review')
|
|
229
|
+
expect(Review.length).toBeLessThanOrEqual(1)
|
|
230
|
+
})
|
|
231
|
+
})
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for components - Claude, Phase, Step, Ralph, etc.
|
|
3
|
+
*/
|
|
4
|
+
import { describe, test, expect } from 'bun:test'
|
|
5
|
+
import { serialize } from '../core/serialize'
|
|
6
|
+
import { jsx } from '../jsx-runtime'
|
|
7
|
+
import { Claude } from './Claude'
|
|
8
|
+
import { Phase } from './Phase'
|
|
9
|
+
import { Step } from './Step'
|
|
10
|
+
import { Stop } from './Stop'
|
|
11
|
+
import { Persona } from './Persona'
|
|
12
|
+
import { Constraints } from './Constraints'
|
|
13
|
+
import { Task } from './Task'
|
|
14
|
+
import { Human } from './Human'
|
|
15
|
+
import { Subagent } from './Subagent'
|
|
16
|
+
import { ClaudeApi } from './ClaudeApi'
|
|
17
|
+
|
|
18
|
+
describe('Phase component', () => {
|
|
19
|
+
test('creates phase element with name prop', () => {
|
|
20
|
+
const node = jsx(Phase, { name: 'research' })
|
|
21
|
+
expect(node.type).toBe('phase')
|
|
22
|
+
expect(node.props.name).toBe('research')
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
test('spreads additional props', () => {
|
|
26
|
+
const node = jsx(Phase, { name: 'test', count: 42, enabled: true })
|
|
27
|
+
expect(node.props.name).toBe('test')
|
|
28
|
+
expect(node.props.count).toBe(42)
|
|
29
|
+
expect(node.props.enabled).toBe(true)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
test('renders children', () => {
|
|
33
|
+
const child = jsx('step', { children: 'Step content' })
|
|
34
|
+
const node = jsx(Phase, { name: 'test', children: child })
|
|
35
|
+
expect(node.children).toHaveLength(1)
|
|
36
|
+
})
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
describe('Step component', () => {
|
|
40
|
+
test('creates step element', () => {
|
|
41
|
+
const node = jsx(Step, { children: 'Do something' })
|
|
42
|
+
expect(node.type).toBe('step')
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
test('renders text children', () => {
|
|
46
|
+
const node = jsx(Step, { children: 'Read the docs' })
|
|
47
|
+
expect(node.children).toHaveLength(1)
|
|
48
|
+
expect(node.children[0].type).toBe('TEXT')
|
|
49
|
+
})
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
describe('Stop component', () => {
|
|
53
|
+
test('creates smithers-stop element', () => {
|
|
54
|
+
const node = jsx(Stop, { reason: 'All done' })
|
|
55
|
+
expect(node.type).toBe('smithers-stop')
|
|
56
|
+
expect(node.props.reason).toBe('All done')
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
test('works without reason', () => {
|
|
60
|
+
const node = jsx(Stop, {})
|
|
61
|
+
expect(node.type).toBe('smithers-stop')
|
|
62
|
+
})
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
describe('Persona component', () => {
|
|
66
|
+
test('creates persona element with role', () => {
|
|
67
|
+
const node = jsx(Persona, { role: 'security expert' })
|
|
68
|
+
expect(node.type).toBe('persona')
|
|
69
|
+
expect(node.props.role).toBe('security expert')
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
test('renders description children', () => {
|
|
73
|
+
const node = jsx(Persona, { role: 'expert', children: 'You specialize in security.' })
|
|
74
|
+
expect(node.children).toHaveLength(1)
|
|
75
|
+
})
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
describe('Constraints component', () => {
|
|
79
|
+
test('creates constraints element', () => {
|
|
80
|
+
const node = jsx(Constraints, { children: '- Be concise' })
|
|
81
|
+
expect(node.type).toBe('constraints')
|
|
82
|
+
})
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
describe('Task component', () => {
|
|
86
|
+
test('creates task element with done prop', () => {
|
|
87
|
+
const node = jsx(Task, { done: false, children: 'Pending task' })
|
|
88
|
+
expect(node.type).toBe('task')
|
|
89
|
+
expect(node.props.done).toBe(false)
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
test('done prop can be true', () => {
|
|
93
|
+
const node = jsx(Task, { done: true, children: 'Completed task' })
|
|
94
|
+
expect(node.props.done).toBe(true)
|
|
95
|
+
})
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
describe('Human component', () => {
|
|
99
|
+
test('creates human element with message', () => {
|
|
100
|
+
const node = jsx(Human, { message: 'Approve?' })
|
|
101
|
+
expect(node.type).toBe('human')
|
|
102
|
+
expect(node.props.message).toBe('Approve?')
|
|
103
|
+
})
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
describe('Subagent component', () => {
|
|
107
|
+
test('creates subagent element with name', () => {
|
|
108
|
+
const node = jsx(Subagent, { name: 'researcher' })
|
|
109
|
+
expect(node.type).toBe('subagent')
|
|
110
|
+
expect(node.props.name).toBe('researcher')
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
test('parallel prop is set', () => {
|
|
114
|
+
const node = jsx(Subagent, { name: 'parallel-agent', parallel: true })
|
|
115
|
+
expect(node.props.parallel).toBe(true)
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
test('renders child components', () => {
|
|
119
|
+
const child = jsx(Phase, { name: 'inner' })
|
|
120
|
+
const node = jsx(Subagent, { name: 'outer', children: child })
|
|
121
|
+
expect(node.children).toHaveLength(1)
|
|
122
|
+
})
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
describe('ClaudeApi component', () => {
|
|
126
|
+
test('creates claude-api element', () => {
|
|
127
|
+
const node = jsx(ClaudeApi, { children: 'Prompt text' })
|
|
128
|
+
expect(node.type).toBe('claude-api')
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
test('accepts model prop', () => {
|
|
132
|
+
const node = jsx(ClaudeApi, { model: 'claude-opus-4' })
|
|
133
|
+
expect(node.props.model).toBe('claude-opus-4')
|
|
134
|
+
})
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
describe('Component composition', () => {
|
|
138
|
+
test('nested components create proper tree structure', () => {
|
|
139
|
+
const stepNode = jsx(Step, { children: 'Step 1' })
|
|
140
|
+
const phaseNode = jsx(Phase, { name: 'test', children: stepNode })
|
|
141
|
+
|
|
142
|
+
expect(phaseNode.type).toBe('phase')
|
|
143
|
+
expect(phaseNode.children).toHaveLength(1)
|
|
144
|
+
expect(phaseNode.children[0].type).toBe('step')
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
test('serializes nested structure correctly', () => {
|
|
148
|
+
const stepNode = jsx(Step, { children: 'Do work' })
|
|
149
|
+
const phaseNode = jsx(Phase, { name: 'main', children: stepNode })
|
|
150
|
+
|
|
151
|
+
const xml = serialize(phaseNode)
|
|
152
|
+
|
|
153
|
+
expect(xml).toContain('<phase name="main">')
|
|
154
|
+
expect(xml).toContain('<step>')
|
|
155
|
+
expect(xml).toContain('Do work')
|
|
156
|
+
expect(xml).toContain('</phase>')
|
|
157
|
+
})
|
|
158
|
+
})
|