smithers-orchestrator 0.1.12 → 0.1.13

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 CHANGED
@@ -1,9 +1,8 @@
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
4
  jsx = "react-jsx"
6
- jsxImportSource = "solid-js/h"
5
+ jsxImportSource = "smithers-orchestrator"
7
6
 
8
7
  [install]
9
8
  auto = "fallback"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "smithers-orchestrator",
3
- "version": "0.1.12",
3
+ "version": "0.1.13",
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",
@@ -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 name={props.name}>
16
- {props.children}
16
+ <phase {...rest}>
17
+ {children}
17
18
  </phase>
18
19
  )
19
20
  }
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Unit tests for execute.ts - Ralph Wiggum loop execution engine.
3
+ */
4
+ import { describe, test, expect } from 'bun:test'
5
+ import { executePlan } from './execute'
6
+ import type { SmithersNode } from './types'
7
+
8
+ describe('executePlan', () => {
9
+ test('returns immediately when tree has no pending nodes', async () => {
10
+ const tree: SmithersNode = {
11
+ type: 'ROOT',
12
+ props: {},
13
+ children: [],
14
+ parent: null,
15
+ }
16
+
17
+ const result = await executePlan(tree)
18
+
19
+ expect(result.frames).toBe(1)
20
+ expect(result.output).toBeNull()
21
+ expect(result.totalDuration).toBeGreaterThanOrEqual(0)
22
+ })
23
+
24
+ test('respects maxFrames option', async () => {
25
+ const tree: SmithersNode = {
26
+ type: 'ROOT',
27
+ props: {},
28
+ children: [],
29
+ parent: null,
30
+ }
31
+
32
+ const result = await executePlan(tree, { maxFrames: 5 })
33
+
34
+ expect(result.frames).toBeLessThanOrEqual(5)
35
+ })
36
+
37
+ test('times out when timeout exceeded', async () => {
38
+ // Create a tree that would run forever (if findPendingExecutables returned nodes)
39
+ const tree: SmithersNode = {
40
+ type: 'ROOT',
41
+ props: {},
42
+ children: [],
43
+ parent: null,
44
+ }
45
+
46
+ // With current implementation (no pending nodes), this completes immediately
47
+ const result = await executePlan(tree, { timeout: 100 })
48
+ expect(result.frames).toBe(1)
49
+ })
50
+
51
+ test('handles nested tree structure', async () => {
52
+ const child: SmithersNode = {
53
+ type: 'phase',
54
+ props: { name: 'test' },
55
+ children: [],
56
+ parent: null,
57
+ }
58
+ const tree: SmithersNode = {
59
+ type: 'ROOT',
60
+ props: {},
61
+ children: [child],
62
+ parent: null,
63
+ }
64
+ child.parent = tree
65
+
66
+ const result = await executePlan(tree)
67
+
68
+ expect(result).toBeDefined()
69
+ expect(result.frames).toBe(1)
70
+ })
71
+
72
+ test('verbose option does not throw', async () => {
73
+ const tree: SmithersNode = {
74
+ type: 'ROOT',
75
+ props: {},
76
+ children: [],
77
+ parent: null,
78
+ }
79
+
80
+ // Should not throw with verbose mode
81
+ const result = await executePlan(tree, { verbose: true })
82
+ expect(result).toBeDefined()
83
+ })
84
+ })
@@ -0,0 +1,163 @@
1
+ /**
2
+ * Unit tests for h.ts - Hyperscript function for JSX compilation.
3
+ */
4
+ import { describe, test, expect } from 'bun:test'
5
+ import { h, Fragment } from './h'
6
+ import type { SmithersNode } from '../core/types'
7
+
8
+ describe('h() hyperscript function', () => {
9
+ test('creates basic element node', () => {
10
+ const node = h('div', null)
11
+
12
+ expect(node.type).toBe('div')
13
+ expect(node.props).toEqual({})
14
+ expect(node.children).toEqual([])
15
+ expect(node.parent).toBeNull()
16
+ })
17
+
18
+ test('creates element with props', () => {
19
+ const node = h('phase', { name: 'test', count: 42 })
20
+
21
+ expect(node.type).toBe('phase')
22
+ expect(node.props.name).toBe('test')
23
+ expect(node.props.count).toBe(42)
24
+ })
25
+
26
+ test('handles null props', () => {
27
+ const node = h('step', null)
28
+
29
+ expect(node.props).toEqual({})
30
+ })
31
+
32
+ test('creates text node from string child', () => {
33
+ const node = h('phase', null, 'Hello world')
34
+
35
+ expect(node.children).toHaveLength(1)
36
+ expect(node.children[0].type).toBe('TEXT')
37
+ expect(node.children[0].props.value).toBe('Hello world')
38
+ expect(node.children[0].parent).toBe(node)
39
+ })
40
+
41
+ test('creates text node from number child', () => {
42
+ const node = h('phase', null, 42)
43
+
44
+ expect(node.children).toHaveLength(1)
45
+ expect(node.children[0].type).toBe('TEXT')
46
+ expect(node.children[0].props.value).toBe('42')
47
+ })
48
+
49
+ test('sets parent reference for child nodes', () => {
50
+ const child = h('step', null)
51
+ const parent = h('phase', null, child)
52
+
53
+ expect(parent.children).toHaveLength(1)
54
+ expect(parent.children[0]).toBe(child)
55
+ expect(child.parent).toBe(parent)
56
+ })
57
+
58
+ test('handles multiple children', () => {
59
+ const child1 = h('step', null)
60
+ const child2 = h('step', null)
61
+ const parent = h('phase', null, child1, child2)
62
+
63
+ expect(parent.children).toHaveLength(2)
64
+ expect(child1.parent).toBe(parent)
65
+ expect(child2.parent).toBe(parent)
66
+ })
67
+
68
+ test('flattens nested arrays', () => {
69
+ const child1 = h('step', null)
70
+ const child2 = h('step', null)
71
+ const parent = h('phase', null, [child1, [child2]])
72
+
73
+ expect(parent.children).toHaveLength(2)
74
+ })
75
+
76
+ test('filters out null/undefined/boolean children', () => {
77
+ const child = h('step', null)
78
+ const parent = h('phase', null, null, undefined, false, true, child)
79
+
80
+ expect(parent.children).toHaveLength(1)
81
+ expect(parent.children[0]).toBe(child)
82
+ })
83
+
84
+ test('handles key prop specially', () => {
85
+ const node = h('step', { key: 'unique', name: 'test' })
86
+
87
+ expect(node.key).toBe('unique')
88
+ expect(node.props.key).toBeUndefined()
89
+ expect(node.props.name).toBe('test')
90
+ })
91
+
92
+ test('handles numeric key', () => {
93
+ const node = h('step', { key: 123 })
94
+
95
+ expect(node.key).toBe(123)
96
+ expect(node.props.key).toBeUndefined()
97
+ })
98
+
99
+ test('calls function components with props', () => {
100
+ const MyComponent = (props: { name: string }) => {
101
+ return h('custom', { customName: props.name })
102
+ }
103
+
104
+ const result = h(MyComponent, { name: 'test' })
105
+
106
+ expect(result.type).toBe('custom')
107
+ expect(result.props.customName).toBe('test')
108
+ })
109
+
110
+ test('passes children to function components', () => {
111
+ const MyComponent = (props: { children: any }) => {
112
+ return h('wrapper', null, props.children)
113
+ }
114
+
115
+ const child = h('step', null)
116
+ const result = h(MyComponent, null, child)
117
+
118
+ expect(result.type).toBe('wrapper')
119
+ expect(result.children).toHaveLength(1)
120
+ })
121
+
122
+ test('handles mixed children (nodes, strings, numbers)', () => {
123
+ const child = h('step', null)
124
+ const parent = h('phase', null, 'Text before', child, 42, 'Text after')
125
+
126
+ expect(parent.children).toHaveLength(4)
127
+ expect(parent.children[0].type).toBe('TEXT')
128
+ expect(parent.children[0].props.value).toBe('Text before')
129
+ expect(parent.children[1]).toBe(child)
130
+ expect(parent.children[2].type).toBe('TEXT')
131
+ expect(parent.children[2].props.value).toBe('42')
132
+ expect(parent.children[3].type).toBe('TEXT')
133
+ expect(parent.children[3].props.value).toBe('Text after')
134
+ })
135
+ })
136
+
137
+ describe('Fragment', () => {
138
+ test('returns children as-is', () => {
139
+ const children = [h('step', null), h('step', null)]
140
+ const result = Fragment({ children })
141
+
142
+ expect(result).toBe(children)
143
+ })
144
+
145
+ test('handles single child', () => {
146
+ const child = h('step', null)
147
+ const result = Fragment({ children: child })
148
+
149
+ expect(result).toBe(child)
150
+ })
151
+
152
+ test('handles undefined children', () => {
153
+ const result = Fragment({})
154
+
155
+ expect(result).toBeUndefined()
156
+ })
157
+
158
+ test('handles string children', () => {
159
+ const result = Fragment({ children: 'text content' })
160
+
161
+ expect(result).toBe('text content')
162
+ })
163
+ })
@@ -0,0 +1,203 @@
1
+ /**
2
+ * Unit tests for vcs.ts - VCS utilities for git and jj operations.
3
+ */
4
+ import { describe, test, expect } from 'bun:test'
5
+ import { parseGitStatus, parseJJStatus, parseDiffStats, isGitRepo, getCurrentBranch } from './vcs'
6
+
7
+ describe('parseGitStatus', () => {
8
+ test('parses modified files', () => {
9
+ const output = `M src/file1.ts
10
+ M src/file2.ts`
11
+
12
+ const result = parseGitStatus(output)
13
+
14
+ expect(result.modified).toContain('src/file1.ts')
15
+ expect(result.modified).toContain('src/file2.ts')
16
+ expect(result.added).toHaveLength(0)
17
+ expect(result.deleted).toHaveLength(0)
18
+ })
19
+
20
+ test('parses added files', () => {
21
+ const output = `A src/new-file.ts
22
+ AM src/another.ts`
23
+
24
+ const result = parseGitStatus(output)
25
+
26
+ expect(result.added).toContain('src/new-file.ts')
27
+ expect(result.added).toContain('src/another.ts')
28
+ })
29
+
30
+ test('parses deleted files', () => {
31
+ const output = `D src/removed.ts
32
+ D src/deleted.ts`
33
+
34
+ const result = parseGitStatus(output)
35
+
36
+ expect(result.deleted).toContain('src/removed.ts')
37
+ expect(result.deleted).toContain('src/deleted.ts')
38
+ })
39
+
40
+ test('parses mixed status', () => {
41
+ const output = `M src/modified.ts
42
+ A src/added.ts
43
+ D src/deleted.ts
44
+ ?? src/untracked.ts`
45
+
46
+ const result = parseGitStatus(output)
47
+
48
+ expect(result.modified).toContain('src/modified.ts')
49
+ expect(result.added).toContain('src/added.ts')
50
+ expect(result.deleted).toContain('src/deleted.ts')
51
+ // Untracked files are not included (no M, A, or D status)
52
+ })
53
+
54
+ test('handles empty output', () => {
55
+ const result = parseGitStatus('')
56
+
57
+ expect(result.modified).toHaveLength(0)
58
+ expect(result.added).toHaveLength(0)
59
+ expect(result.deleted).toHaveLength(0)
60
+ })
61
+
62
+ test('handles whitespace-only lines', () => {
63
+ const output = `M src/file.ts
64
+
65
+
66
+ A src/other.ts`
67
+
68
+ const result = parseGitStatus(output)
69
+
70
+ expect(result.modified).toHaveLength(1)
71
+ expect(result.added).toHaveLength(1)
72
+ })
73
+ })
74
+
75
+ describe('parseJJStatus', () => {
76
+ test('parses modified files', () => {
77
+ const output = `M src/file1.ts
78
+ M src/file2.ts`
79
+
80
+ const result = parseJJStatus(output)
81
+
82
+ expect(result.modified).toContain('src/file1.ts')
83
+ expect(result.modified).toContain('src/file2.ts')
84
+ })
85
+
86
+ test('parses added files', () => {
87
+ const output = `A src/new-file.ts
88
+ A src/another.ts`
89
+
90
+ const result = parseJJStatus(output)
91
+
92
+ expect(result.added).toContain('src/new-file.ts')
93
+ expect(result.added).toContain('src/another.ts')
94
+ })
95
+
96
+ test('parses deleted files', () => {
97
+ const output = `D src/removed.ts
98
+ D src/deleted.ts`
99
+
100
+ const result = parseJJStatus(output)
101
+
102
+ expect(result.deleted).toContain('src/removed.ts')
103
+ expect(result.deleted).toContain('src/deleted.ts')
104
+ })
105
+
106
+ test('parses mixed status', () => {
107
+ const output = `M src/modified.ts
108
+ A src/added.ts
109
+ D src/deleted.ts`
110
+
111
+ const result = parseJJStatus(output)
112
+
113
+ expect(result.modified).toContain('src/modified.ts')
114
+ expect(result.added).toContain('src/added.ts')
115
+ expect(result.deleted).toContain('src/deleted.ts')
116
+ })
117
+
118
+ test('handles empty output', () => {
119
+ const result = parseJJStatus('')
120
+
121
+ expect(result.modified).toHaveLength(0)
122
+ expect(result.added).toHaveLength(0)
123
+ expect(result.deleted).toHaveLength(0)
124
+ })
125
+ })
126
+
127
+ describe('parseDiffStats', () => {
128
+ test('parses file change lines', () => {
129
+ const output = ` src/file1.ts | 10 ++++++----
130
+ src/file2.ts | 5 +++++
131
+ src/file3.ts | 3 ---`
132
+
133
+ const result = parseDiffStats(output)
134
+
135
+ expect(result.files).toHaveLength(3)
136
+ expect(result.files).toContain('src/file1.ts')
137
+ expect(result.files).toContain('src/file2.ts')
138
+ expect(result.files).toContain('src/file3.ts')
139
+ })
140
+
141
+ test('counts insertions correctly', () => {
142
+ const output = ` src/file1.ts | 10 ++++++----`
143
+
144
+ const result = parseDiffStats(output)
145
+
146
+ // 6 + symbols
147
+ expect(result.insertions).toBe(6)
148
+ })
149
+
150
+ test('counts deletions correctly', () => {
151
+ const output = ` src/file1.ts | 10 ++++++----`
152
+
153
+ const result = parseDiffStats(output)
154
+
155
+ // 4 - symbols
156
+ expect(result.deletions).toBe(4)
157
+ })
158
+
159
+ test('sums insertions and deletions across files', () => {
160
+ const output = ` src/file1.ts | 10 ++++++----
161
+ src/file2.ts | 5 +++++
162
+ src/file3.ts | 3 ---`
163
+
164
+ const result = parseDiffStats(output)
165
+
166
+ // file1: 6+, 4-; file2: 5+, 0-; file3: 0+, 3-
167
+ expect(result.insertions).toBe(11)
168
+ expect(result.deletions).toBe(7)
169
+ })
170
+
171
+ test('handles empty output', () => {
172
+ const result = parseDiffStats('')
173
+
174
+ expect(result.files).toHaveLength(0)
175
+ expect(result.insertions).toBe(0)
176
+ expect(result.deletions).toBe(0)
177
+ })
178
+
179
+ test('ignores summary lines', () => {
180
+ const output = ` src/file.ts | 10 +++++++---
181
+ 2 files changed, 10 insertions(+), 5 deletions(-)`
182
+
183
+ const result = parseDiffStats(output)
184
+
185
+ // Only the first line should match
186
+ expect(result.files).toHaveLength(1)
187
+ })
188
+ })
189
+
190
+ describe('isGitRepo', () => {
191
+ test('returns true in current directory (which is a git repo)', async () => {
192
+ const result = await isGitRepo()
193
+ expect(result).toBe(true)
194
+ })
195
+ })
196
+
197
+ describe('getCurrentBranch', () => {
198
+ test('returns branch name in a git repo', async () => {
199
+ const branch = await getCurrentBranch()
200
+ // We're in a git repo, so should return something
201
+ expect(typeof branch).toBe('string')
202
+ })
203
+ })