popeye-cli 1.0.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 (209) hide show
  1. package/.env.example +25 -0
  2. package/.prettierrc +8 -0
  3. package/README.md +320 -0
  4. package/dist/adapters/claude.d.ts +82 -0
  5. package/dist/adapters/claude.d.ts.map +1 -0
  6. package/dist/adapters/claude.js +230 -0
  7. package/dist/adapters/claude.js.map +1 -0
  8. package/dist/adapters/openai.d.ts +48 -0
  9. package/dist/adapters/openai.d.ts.map +1 -0
  10. package/dist/adapters/openai.js +257 -0
  11. package/dist/adapters/openai.js.map +1 -0
  12. package/dist/auth/claude.d.ts +44 -0
  13. package/dist/auth/claude.d.ts.map +1 -0
  14. package/dist/auth/claude.js +139 -0
  15. package/dist/auth/claude.js.map +1 -0
  16. package/dist/auth/index.d.ts +61 -0
  17. package/dist/auth/index.d.ts.map +1 -0
  18. package/dist/auth/index.js +141 -0
  19. package/dist/auth/index.js.map +1 -0
  20. package/dist/auth/keychain.d.ts +66 -0
  21. package/dist/auth/keychain.d.ts.map +1 -0
  22. package/dist/auth/keychain.js +125 -0
  23. package/dist/auth/keychain.js.map +1 -0
  24. package/dist/auth/openai-entry.d.ts +9 -0
  25. package/dist/auth/openai-entry.d.ts.map +1 -0
  26. package/dist/auth/openai-entry.js +410 -0
  27. package/dist/auth/openai-entry.js.map +1 -0
  28. package/dist/auth/openai.d.ts +71 -0
  29. package/dist/auth/openai.d.ts.map +1 -0
  30. package/dist/auth/openai.js +212 -0
  31. package/dist/auth/openai.js.map +1 -0
  32. package/dist/auth/server.d.ts +32 -0
  33. package/dist/auth/server.d.ts.map +1 -0
  34. package/dist/auth/server.js +213 -0
  35. package/dist/auth/server.js.map +1 -0
  36. package/dist/cli/commands/auth.d.ts +10 -0
  37. package/dist/cli/commands/auth.d.ts.map +1 -0
  38. package/dist/cli/commands/auth.js +162 -0
  39. package/dist/cli/commands/auth.js.map +1 -0
  40. package/dist/cli/commands/config.d.ts +10 -0
  41. package/dist/cli/commands/config.d.ts.map +1 -0
  42. package/dist/cli/commands/config.js +215 -0
  43. package/dist/cli/commands/config.js.map +1 -0
  44. package/dist/cli/commands/create.d.ts +10 -0
  45. package/dist/cli/commands/create.d.ts.map +1 -0
  46. package/dist/cli/commands/create.js +240 -0
  47. package/dist/cli/commands/create.js.map +1 -0
  48. package/dist/cli/commands/index.d.ts +10 -0
  49. package/dist/cli/commands/index.d.ts.map +1 -0
  50. package/dist/cli/commands/index.js +10 -0
  51. package/dist/cli/commands/index.js.map +1 -0
  52. package/dist/cli/commands/resume.d.ts +18 -0
  53. package/dist/cli/commands/resume.d.ts.map +1 -0
  54. package/dist/cli/commands/resume.js +241 -0
  55. package/dist/cli/commands/resume.js.map +1 -0
  56. package/dist/cli/commands/status.d.ts +18 -0
  57. package/dist/cli/commands/status.d.ts.map +1 -0
  58. package/dist/cli/commands/status.js +154 -0
  59. package/dist/cli/commands/status.js.map +1 -0
  60. package/dist/cli/index.d.ts +17 -0
  61. package/dist/cli/index.d.ts.map +1 -0
  62. package/dist/cli/index.js +71 -0
  63. package/dist/cli/index.js.map +1 -0
  64. package/dist/cli/interactive.d.ts +9 -0
  65. package/dist/cli/interactive.d.ts.map +1 -0
  66. package/dist/cli/interactive.js +330 -0
  67. package/dist/cli/interactive.js.map +1 -0
  68. package/dist/cli/output.d.ts +182 -0
  69. package/dist/cli/output.d.ts.map +1 -0
  70. package/dist/cli/output.js +355 -0
  71. package/dist/cli/output.js.map +1 -0
  72. package/dist/config/defaults.d.ts +57 -0
  73. package/dist/config/defaults.d.ts.map +1 -0
  74. package/dist/config/defaults.js +103 -0
  75. package/dist/config/defaults.js.map +1 -0
  76. package/dist/config/index.d.ts +138 -0
  77. package/dist/config/index.d.ts.map +1 -0
  78. package/dist/config/index.js +244 -0
  79. package/dist/config/index.js.map +1 -0
  80. package/dist/config/schema.d.ts +220 -0
  81. package/dist/config/schema.d.ts.map +1 -0
  82. package/dist/config/schema.js +141 -0
  83. package/dist/config/schema.js.map +1 -0
  84. package/dist/generators/index.d.ts +101 -0
  85. package/dist/generators/index.d.ts.map +1 -0
  86. package/dist/generators/index.js +200 -0
  87. package/dist/generators/index.js.map +1 -0
  88. package/dist/generators/python.d.ts +48 -0
  89. package/dist/generators/python.d.ts.map +1 -0
  90. package/dist/generators/python.js +262 -0
  91. package/dist/generators/python.js.map +1 -0
  92. package/dist/generators/templates/index.d.ts +6 -0
  93. package/dist/generators/templates/index.d.ts.map +1 -0
  94. package/dist/generators/templates/index.js +6 -0
  95. package/dist/generators/templates/index.js.map +1 -0
  96. package/dist/generators/templates/python.d.ts +53 -0
  97. package/dist/generators/templates/python.d.ts.map +1 -0
  98. package/dist/generators/templates/python.js +454 -0
  99. package/dist/generators/templates/python.js.map +1 -0
  100. package/dist/generators/templates/typescript.d.ts +53 -0
  101. package/dist/generators/templates/typescript.d.ts.map +1 -0
  102. package/dist/generators/templates/typescript.js +394 -0
  103. package/dist/generators/templates/typescript.js.map +1 -0
  104. package/dist/generators/typescript.d.ts +64 -0
  105. package/dist/generators/typescript.d.ts.map +1 -0
  106. package/dist/generators/typescript.js +271 -0
  107. package/dist/generators/typescript.js.map +1 -0
  108. package/dist/index.d.ts +7 -0
  109. package/dist/index.d.ts.map +1 -0
  110. package/dist/index.js +12 -0
  111. package/dist/index.js.map +1 -0
  112. package/dist/state/index.d.ts +168 -0
  113. package/dist/state/index.d.ts.map +1 -0
  114. package/dist/state/index.js +338 -0
  115. package/dist/state/index.js.map +1 -0
  116. package/dist/state/persistence.d.ts +91 -0
  117. package/dist/state/persistence.d.ts.map +1 -0
  118. package/dist/state/persistence.js +201 -0
  119. package/dist/state/persistence.js.map +1 -0
  120. package/dist/types/cli.d.ts +132 -0
  121. package/dist/types/cli.d.ts.map +1 -0
  122. package/dist/types/cli.js +17 -0
  123. package/dist/types/cli.js.map +1 -0
  124. package/dist/types/consensus.d.ts +111 -0
  125. package/dist/types/consensus.d.ts.map +1 -0
  126. package/dist/types/consensus.js +29 -0
  127. package/dist/types/consensus.js.map +1 -0
  128. package/dist/types/index.d.ts +9 -0
  129. package/dist/types/index.d.ts.map +1 -0
  130. package/dist/types/index.js +13 -0
  131. package/dist/types/index.js.map +1 -0
  132. package/dist/types/project.d.ts +73 -0
  133. package/dist/types/project.d.ts.map +1 -0
  134. package/dist/types/project.js +55 -0
  135. package/dist/types/project.js.map +1 -0
  136. package/dist/types/workflow.d.ts +236 -0
  137. package/dist/types/workflow.d.ts.map +1 -0
  138. package/dist/types/workflow.js +74 -0
  139. package/dist/types/workflow.js.map +1 -0
  140. package/dist/workflow/consensus.d.ts +89 -0
  141. package/dist/workflow/consensus.d.ts.map +1 -0
  142. package/dist/workflow/consensus.js +220 -0
  143. package/dist/workflow/consensus.js.map +1 -0
  144. package/dist/workflow/execution-mode.d.ts +82 -0
  145. package/dist/workflow/execution-mode.d.ts.map +1 -0
  146. package/dist/workflow/execution-mode.js +346 -0
  147. package/dist/workflow/execution-mode.js.map +1 -0
  148. package/dist/workflow/index.d.ts +110 -0
  149. package/dist/workflow/index.d.ts.map +1 -0
  150. package/dist/workflow/index.js +283 -0
  151. package/dist/workflow/index.js.map +1 -0
  152. package/dist/workflow/plan-mode.d.ts +83 -0
  153. package/dist/workflow/plan-mode.d.ts.map +1 -0
  154. package/dist/workflow/plan-mode.js +241 -0
  155. package/dist/workflow/plan-mode.js.map +1 -0
  156. package/dist/workflow/test-runner.d.ts +87 -0
  157. package/dist/workflow/test-runner.d.ts.map +1 -0
  158. package/dist/workflow/test-runner.js +273 -0
  159. package/dist/workflow/test-runner.js.map +1 -0
  160. package/eslint.config.js +25 -0
  161. package/package.json +66 -0
  162. package/src/adapters/claude.ts +298 -0
  163. package/src/adapters/openai.ts +300 -0
  164. package/src/auth/claude.ts +166 -0
  165. package/src/auth/index.ts +171 -0
  166. package/src/auth/keychain.ts +138 -0
  167. package/src/auth/openai-entry.ts +410 -0
  168. package/src/auth/openai.ts +260 -0
  169. package/src/auth/server.ts +252 -0
  170. package/src/cli/commands/auth.ts +194 -0
  171. package/src/cli/commands/config.ts +241 -0
  172. package/src/cli/commands/create.ts +308 -0
  173. package/src/cli/commands/index.ts +10 -0
  174. package/src/cli/commands/resume.ts +304 -0
  175. package/src/cli/commands/status.ts +189 -0
  176. package/src/cli/index.ts +90 -0
  177. package/src/cli/interactive.ts +418 -0
  178. package/src/cli/output.ts +410 -0
  179. package/src/config/defaults.ts +114 -0
  180. package/src/config/index.ts +315 -0
  181. package/src/config/schema.ts +164 -0
  182. package/src/generators/index.ts +251 -0
  183. package/src/generators/python.ts +318 -0
  184. package/src/generators/templates/index.ts +6 -0
  185. package/src/generators/templates/python.ts +465 -0
  186. package/src/generators/templates/typescript.ts +417 -0
  187. package/src/generators/typescript.ts +340 -0
  188. package/src/index.ts +13 -0
  189. package/src/state/index.ts +454 -0
  190. package/src/state/persistence.ts +230 -0
  191. package/src/types/cli.ts +146 -0
  192. package/src/types/consensus.ts +116 -0
  193. package/src/types/index.ts +64 -0
  194. package/src/types/project.ts +85 -0
  195. package/src/types/workflow.ts +149 -0
  196. package/src/workflow/consensus.ts +299 -0
  197. package/src/workflow/execution-mode.ts +517 -0
  198. package/src/workflow/index.ts +396 -0
  199. package/src/workflow/plan-mode.ts +356 -0
  200. package/src/workflow/test-runner.ts +345 -0
  201. package/tests/adapters/openai.test.ts +145 -0
  202. package/tests/config/config.test.ts +208 -0
  203. package/tests/generators/generators.test.ts +185 -0
  204. package/tests/types/consensus.test.ts +152 -0
  205. package/tests/types/project.test.ts +134 -0
  206. package/tests/workflow/consensus.test.ts +221 -0
  207. package/tests/workflow/test-runner.test.ts +214 -0
  208. package/tsconfig.json +25 -0
  209. package/vitest.config.ts +22 -0
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Tests for project types and schemas
3
+ */
4
+
5
+ import { describe, it, expect } from 'vitest';
6
+ import {
7
+ ProjectSpecSchema,
8
+ OutputLanguageSchema,
9
+ OpenAIModelSchema,
10
+ } from '../../src/types/project.js';
11
+
12
+ describe('ProjectSpecSchema', () => {
13
+ describe('valid inputs', () => {
14
+ it('should accept a valid project spec with all fields', () => {
15
+ const spec = {
16
+ idea: 'Build a REST API for managing tasks',
17
+ name: 'task-api',
18
+ language: 'python',
19
+ openaiModel: 'gpt-4o',
20
+ outputDir: '/tmp/projects',
21
+ };
22
+
23
+ const result = ProjectSpecSchema.safeParse(spec);
24
+ expect(result.success).toBe(true);
25
+ if (result.success) {
26
+ expect(result.data.idea).toBe(spec.idea);
27
+ expect(result.data.language).toBe('python');
28
+ }
29
+ });
30
+
31
+ it('should accept spec with only required fields', () => {
32
+ const spec = {
33
+ idea: 'Simple CLI tool',
34
+ language: 'typescript',
35
+ openaiModel: 'gpt-4o-mini',
36
+ };
37
+
38
+ const result = ProjectSpecSchema.safeParse(spec);
39
+ expect(result.success).toBe(true);
40
+ });
41
+
42
+ it('should accept all valid OpenAI models', () => {
43
+ const models = ['gpt-4o', 'gpt-4o-mini', 'gpt-4-turbo', 'o1-preview', 'o1-mini'];
44
+
45
+ for (const model of models) {
46
+ const spec = {
47
+ idea: 'Test project idea here',
48
+ language: 'python',
49
+ openaiModel: model,
50
+ };
51
+
52
+ const result = ProjectSpecSchema.safeParse(spec);
53
+ expect(result.success).toBe(true);
54
+ }
55
+ });
56
+ });
57
+
58
+ describe('invalid inputs', () => {
59
+ it('should reject idea shorter than 10 characters', () => {
60
+ const spec = {
61
+ idea: 'Too short',
62
+ language: 'python',
63
+ openaiModel: 'gpt-4o',
64
+ };
65
+
66
+ const result = ProjectSpecSchema.safeParse(spec);
67
+ expect(result.success).toBe(false);
68
+ });
69
+
70
+ it('should reject invalid language', () => {
71
+ const spec = {
72
+ idea: 'Build something great',
73
+ language: 'java',
74
+ openaiModel: 'gpt-4o',
75
+ };
76
+
77
+ const result = ProjectSpecSchema.safeParse(spec);
78
+ expect(result.success).toBe(false);
79
+ });
80
+
81
+ it('should reject invalid OpenAI model', () => {
82
+ const spec = {
83
+ idea: 'Build something great',
84
+ language: 'python',
85
+ openaiModel: 'invalid-model',
86
+ };
87
+
88
+ const result = ProjectSpecSchema.safeParse(spec);
89
+ expect(result.success).toBe(false);
90
+ });
91
+
92
+ it('should reject missing required fields', () => {
93
+ const spec = {
94
+ name: 'my-project',
95
+ };
96
+
97
+ const result = ProjectSpecSchema.safeParse(spec);
98
+ expect(result.success).toBe(false);
99
+ });
100
+ });
101
+ });
102
+
103
+ describe('OutputLanguageSchema', () => {
104
+ it('should accept python', () => {
105
+ const result = OutputLanguageSchema.safeParse('python');
106
+ expect(result.success).toBe(true);
107
+ });
108
+
109
+ it('should accept typescript', () => {
110
+ const result = OutputLanguageSchema.safeParse('typescript');
111
+ expect(result.success).toBe(true);
112
+ });
113
+
114
+ it('should reject invalid language', () => {
115
+ const result = OutputLanguageSchema.safeParse('ruby');
116
+ expect(result.success).toBe(false);
117
+ });
118
+ });
119
+
120
+ describe('OpenAIModelSchema', () => {
121
+ it('should accept all valid models', () => {
122
+ const validModels = ['gpt-4o', 'gpt-4o-mini', 'gpt-4-turbo', 'o1-preview', 'o1-mini'];
123
+
124
+ for (const model of validModels) {
125
+ const result = OpenAIModelSchema.safeParse(model);
126
+ expect(result.success).toBe(true);
127
+ }
128
+ });
129
+
130
+ it('should reject invalid model', () => {
131
+ const result = OpenAIModelSchema.safeParse('gpt-5');
132
+ expect(result.success).toBe(false);
133
+ });
134
+ });
@@ -0,0 +1,221 @@
1
+ /**
2
+ * Tests for consensus workflow
3
+ */
4
+
5
+ import { describe, it, expect } from 'vitest';
6
+ import {
7
+ formatPlanForReview,
8
+ extractConcerns,
9
+ meetsThreshold,
10
+ validatePlanStructure,
11
+ calculateAverageScore,
12
+ getScoreTrend,
13
+ } from '../../src/workflow/consensus.js';
14
+ import type { ConsensusResult, ConsensusIteration } from '../../src/types/consensus.js';
15
+
16
+ describe('formatPlanForReview', () => {
17
+ it('should format plan with context', () => {
18
+ const plan = '# My Plan\n\nThis is the plan content.';
19
+ const context = 'Python project for data processing';
20
+
21
+ const formatted = formatPlanForReview(plan, context);
22
+
23
+ expect(formatted).toContain('## Development Plan');
24
+ expect(formatted).toContain(plan);
25
+ expect(formatted).toContain('## Project Context');
26
+ expect(formatted).toContain(context);
27
+ });
28
+
29
+ it('should handle empty context', () => {
30
+ const plan = 'Simple plan';
31
+ const context = '';
32
+
33
+ const formatted = formatPlanForReview(plan, context);
34
+
35
+ expect(formatted).toContain(plan);
36
+ expect(formatted).toContain('## Project Context');
37
+ });
38
+ });
39
+
40
+ describe('extractConcerns', () => {
41
+ it('should extract concerns from result', () => {
42
+ const result: ConsensusResult = {
43
+ score: 80,
44
+ analysis: 'Good plan overall',
45
+ concerns: ['Missing error handling', 'No tests defined'],
46
+ recommendations: ['Add logging', 'Consider caching'],
47
+ approved: false,
48
+ rawResponse: '',
49
+ };
50
+
51
+ const concerns = extractConcerns(result);
52
+
53
+ expect(concerns).toContain('Missing error handling');
54
+ expect(concerns).toContain('No tests defined');
55
+ expect(concerns).toContain('Consider: Add logging');
56
+ expect(concerns).toContain('Consider: Consider caching');
57
+ });
58
+
59
+ it('should handle empty concerns and recommendations', () => {
60
+ const result: ConsensusResult = {
61
+ score: 95,
62
+ analysis: 'Perfect',
63
+ approved: true,
64
+ rawResponse: '',
65
+ };
66
+
67
+ const concerns = extractConcerns(result);
68
+
69
+ expect(concerns).toEqual([]);
70
+ });
71
+ });
72
+
73
+ describe('meetsThreshold', () => {
74
+ it('should return true when score meets threshold', () => {
75
+ expect(meetsThreshold(95, 95)).toBe(true);
76
+ expect(meetsThreshold(100, 95)).toBe(true);
77
+ });
78
+
79
+ it('should return false when score is below threshold', () => {
80
+ expect(meetsThreshold(94, 95)).toBe(false);
81
+ expect(meetsThreshold(0, 95)).toBe(false);
82
+ });
83
+
84
+ it('should use default threshold of 95', () => {
85
+ expect(meetsThreshold(95)).toBe(true);
86
+ expect(meetsThreshold(94)).toBe(false);
87
+ });
88
+ });
89
+
90
+ describe('validatePlanStructure', () => {
91
+ it('should validate plan with all required sections', () => {
92
+ const plan = `
93
+ # Background
94
+ Some background info
95
+
96
+ ## Goals
97
+ 1. Goal one
98
+ 2. Goal two
99
+
100
+ ### Milestones
101
+ - Milestone 1
102
+ - Milestone 2
103
+
104
+ #### Tasks
105
+ - Task 1
106
+ - Task 2
107
+
108
+ ##### Test Plan
109
+ - Test case 1
110
+ `;
111
+
112
+ const result = validatePlanStructure(plan);
113
+
114
+ expect(result.valid).toBe(true);
115
+ expect(result.missingSections).toEqual([]);
116
+ });
117
+
118
+ it('should detect missing sections', () => {
119
+ const plan = `
120
+ # Background
121
+ Some background
122
+
123
+ ## Goals
124
+ Some goals
125
+ `;
126
+
127
+ const result = validatePlanStructure(plan);
128
+
129
+ expect(result.valid).toBe(false);
130
+ expect(result.missingSections).toContain('Milestones');
131
+ expect(result.missingSections).toContain('Tasks');
132
+ expect(result.missingSections).toContain('Test');
133
+ });
134
+
135
+ it('should handle case-insensitive matching', () => {
136
+ const plan = `
137
+ # BACKGROUND
138
+ ## GOALS
139
+ ### MILESTONES
140
+ #### TASKS
141
+ ##### TEST
142
+ `;
143
+
144
+ const result = validatePlanStructure(plan);
145
+
146
+ expect(result.valid).toBe(true);
147
+ });
148
+ });
149
+
150
+ describe('calculateAverageScore', () => {
151
+ it('should calculate average of iteration scores', () => {
152
+ const iterations: ConsensusIteration[] = [
153
+ { iteration: 1, plan: '', result: { score: 80, analysis: '', approved: false, rawResponse: '' }, timestamp: '' },
154
+ { iteration: 2, plan: '', result: { score: 90, analysis: '', approved: false, rawResponse: '' }, timestamp: '' },
155
+ { iteration: 3, plan: '', result: { score: 100, analysis: '', approved: true, rawResponse: '' }, timestamp: '' },
156
+ ];
157
+
158
+ const average = calculateAverageScore(iterations);
159
+
160
+ expect(average).toBe(90);
161
+ });
162
+
163
+ it('should return 0 for empty iterations', () => {
164
+ expect(calculateAverageScore([])).toBe(0);
165
+ });
166
+
167
+ it('should handle single iteration', () => {
168
+ const iterations: ConsensusIteration[] = [
169
+ { iteration: 1, plan: '', result: { score: 75, analysis: '', approved: false, rawResponse: '' }, timestamp: '' },
170
+ ];
171
+
172
+ expect(calculateAverageScore(iterations)).toBe(75);
173
+ });
174
+ });
175
+
176
+ describe('getScoreTrend', () => {
177
+ it('should detect improving trend', () => {
178
+ const iterations: ConsensusIteration[] = [
179
+ { iteration: 1, plan: '', result: { score: 70, analysis: '', approved: false, rawResponse: '' }, timestamp: '' },
180
+ { iteration: 2, plan: '', result: { score: 75, analysis: '', approved: false, rawResponse: '' }, timestamp: '' },
181
+ { iteration: 3, plan: '', result: { score: 85, analysis: '', approved: false, rawResponse: '' }, timestamp: '' },
182
+ { iteration: 4, plan: '', result: { score: 90, analysis: '', approved: false, rawResponse: '' }, timestamp: '' },
183
+ ];
184
+
185
+ expect(getScoreTrend(iterations)).toBe('improving');
186
+ });
187
+
188
+ it('should detect declining trend', () => {
189
+ const iterations: ConsensusIteration[] = [
190
+ { iteration: 1, plan: '', result: { score: 90, analysis: '', approved: false, rawResponse: '' }, timestamp: '' },
191
+ { iteration: 2, plan: '', result: { score: 85, analysis: '', approved: false, rawResponse: '' }, timestamp: '' },
192
+ { iteration: 3, plan: '', result: { score: 75, analysis: '', approved: false, rawResponse: '' }, timestamp: '' },
193
+ { iteration: 4, plan: '', result: { score: 70, analysis: '', approved: false, rawResponse: '' }, timestamp: '' },
194
+ ];
195
+
196
+ expect(getScoreTrend(iterations)).toBe('declining');
197
+ });
198
+
199
+ it('should detect stable trend', () => {
200
+ const iterations: ConsensusIteration[] = [
201
+ { iteration: 1, plan: '', result: { score: 80, analysis: '', approved: false, rawResponse: '' }, timestamp: '' },
202
+ { iteration: 2, plan: '', result: { score: 82, analysis: '', approved: false, rawResponse: '' }, timestamp: '' },
203
+ { iteration: 3, plan: '', result: { score: 81, analysis: '', approved: false, rawResponse: '' }, timestamp: '' },
204
+ { iteration: 4, plan: '', result: { score: 83, analysis: '', approved: false, rawResponse: '' }, timestamp: '' },
205
+ ];
206
+
207
+ expect(getScoreTrend(iterations)).toBe('stable');
208
+ });
209
+
210
+ it('should return stable for single iteration', () => {
211
+ const iterations: ConsensusIteration[] = [
212
+ { iteration: 1, plan: '', result: { score: 80, analysis: '', approved: false, rawResponse: '' }, timestamp: '' },
213
+ ];
214
+
215
+ expect(getScoreTrend(iterations)).toBe('stable');
216
+ });
217
+
218
+ it('should return stable for empty iterations', () => {
219
+ expect(getScoreTrend([])).toBe('stable');
220
+ });
221
+ });
@@ -0,0 +1,214 @@
1
+ /**
2
+ * Tests for test runner module
3
+ */
4
+
5
+ import { describe, it, expect } from 'vitest';
6
+ import {
7
+ buildTestCommand,
8
+ parseTestOutput,
9
+ getTestSummary,
10
+ DEFAULT_TEST_COMMANDS,
11
+ } from '../../src/workflow/test-runner.js';
12
+
13
+ describe('buildTestCommand', () => {
14
+ describe('Python', () => {
15
+ it('should build basic pytest command', () => {
16
+ const command = buildTestCommand({ language: 'python' });
17
+ expect(command).toBe('python -m pytest tests/');
18
+ });
19
+
20
+ it('should include verbose flag', () => {
21
+ const command = buildTestCommand({ language: 'python', verbose: true });
22
+ expect(command).toContain('-v');
23
+ });
24
+
25
+ it('should include coverage flag', () => {
26
+ const command = buildTestCommand({ language: 'python', coverage: true });
27
+ expect(command).toContain('--cov=src');
28
+ expect(command).toContain('--cov-report=term-missing');
29
+ });
30
+
31
+ it('should use custom test directory', () => {
32
+ const command = buildTestCommand({ language: 'python', testDir: 'my_tests/' });
33
+ expect(command).toContain('my_tests/');
34
+ });
35
+ });
36
+
37
+ describe('TypeScript', () => {
38
+ it('should build basic npm test command', () => {
39
+ const command = buildTestCommand({ language: 'typescript' });
40
+ expect(command).toBe('npm test');
41
+ });
42
+
43
+ it('should include coverage flag', () => {
44
+ const command = buildTestCommand({ language: 'typescript', coverage: true });
45
+ expect(command).toContain('--coverage');
46
+ });
47
+ });
48
+ });
49
+
50
+ describe('parseTestOutput', () => {
51
+ describe('Python/pytest', () => {
52
+ it('should parse successful pytest output', () => {
53
+ const output = `
54
+ ============================= test session starts ==============================
55
+ collected 10 items
56
+
57
+ tests/test_main.py .......... [100%]
58
+
59
+ ============================== 10 passed in 2.34s ==============================
60
+ `;
61
+
62
+ const result = parseTestOutput(output, 'python');
63
+
64
+ expect(result.success).toBe(true);
65
+ expect(result.passed).toBe(10);
66
+ expect(result.failed).toBe(0);
67
+ expect(result.total).toBe(10);
68
+ });
69
+
70
+ it('should parse pytest output with failures', () => {
71
+ const output = `
72
+ ============================= test session starts ==============================
73
+ collected 5 items
74
+
75
+ tests/test_main.py ...F. [100%]
76
+
77
+ =================================== FAILURES ===================================
78
+ FAILED tests/test_main.py::test_something
79
+ ================================ 1 failed, 4 passed in 1.23s ==============================
80
+ `;
81
+
82
+ const result = parseTestOutput(output, 'python');
83
+
84
+ expect(result.success).toBe(false);
85
+ expect(result.passed).toBe(4);
86
+ expect(result.failed).toBe(1);
87
+ expect(result.total).toBe(5);
88
+ expect(result.failedTests).toContain('tests/test_main.py::test_something');
89
+ });
90
+
91
+ it('should handle no tests collected', () => {
92
+ const output = 'collected 0 items';
93
+
94
+ const result = parseTestOutput(output, 'python');
95
+
96
+ expect(result.success).toBe(false);
97
+ expect(result.total).toBe(0);
98
+ });
99
+ });
100
+
101
+ describe('TypeScript/Jest', () => {
102
+ it('should parse successful Jest output', () => {
103
+ const output = `
104
+ PASS tests/index.test.ts
105
+ MyComponent
106
+ v should render correctly (25 ms)
107
+ v should handle click events (12 ms)
108
+
109
+ Tests: 2 passed, 2 total
110
+ `;
111
+
112
+ const result = parseTestOutput(output, 'typescript');
113
+
114
+ expect(result.success).toBe(true);
115
+ expect(result.passed).toBe(2);
116
+ expect(result.total).toBe(2);
117
+ });
118
+
119
+ it('should parse Jest output with failures', () => {
120
+ const output = `
121
+ FAIL tests/index.test.ts
122
+ MyComponent
123
+ v should render correctly (25 ms)
124
+ x should handle click events (12 ms)
125
+
126
+ Tests: 1 failed, 1 passed, 2 total
127
+ `;
128
+
129
+ const result = parseTestOutput(output, 'typescript');
130
+
131
+ expect(result.success).toBe(false);
132
+ expect(result.passed).toBe(1);
133
+ expect(result.failed).toBe(1);
134
+ expect(result.total).toBe(2);
135
+ });
136
+ });
137
+ });
138
+
139
+ describe('getTestSummary', () => {
140
+ it('should summarize successful tests', () => {
141
+ const result = {
142
+ success: true,
143
+ passed: 10,
144
+ failed: 0,
145
+ total: 10,
146
+ output: '',
147
+ };
148
+
149
+ const summary = getTestSummary(result);
150
+
151
+ expect(summary).toContain('10/10 tests passed');
152
+ expect(summary).toContain('✓');
153
+ });
154
+
155
+ it('should summarize failed tests', () => {
156
+ const result = {
157
+ success: false,
158
+ passed: 8,
159
+ failed: 2,
160
+ total: 10,
161
+ output: '',
162
+ };
163
+
164
+ const summary = getTestSummary(result);
165
+
166
+ expect(summary).toContain('8/10 tests passed');
167
+ expect(summary).toContain('2 failed');
168
+ expect(summary).toContain('✗');
169
+ });
170
+
171
+ it('should handle error case', () => {
172
+ const result = {
173
+ success: false,
174
+ passed: 0,
175
+ failed: 0,
176
+ total: 0,
177
+ output: '',
178
+ error: 'Tests crashed',
179
+ };
180
+
181
+ const summary = getTestSummary(result);
182
+
183
+ expect(summary).toContain('Tests crashed');
184
+ });
185
+
186
+ it('should handle no tests found', () => {
187
+ const result = {
188
+ success: false,
189
+ passed: 0,
190
+ failed: 0,
191
+ total: 0,
192
+ output: '',
193
+ };
194
+
195
+ const summary = getTestSummary(result);
196
+
197
+ expect(summary).toContain('No tests found');
198
+ });
199
+ });
200
+
201
+ describe('DEFAULT_TEST_COMMANDS', () => {
202
+ it('should have commands for both languages', () => {
203
+ expect(DEFAULT_TEST_COMMANDS.python).toBeDefined();
204
+ expect(DEFAULT_TEST_COMMANDS.typescript).toBeDefined();
205
+ });
206
+
207
+ it('should have valid pytest command for Python', () => {
208
+ expect(DEFAULT_TEST_COMMANDS.python).toContain('pytest');
209
+ });
210
+
211
+ it('should have valid npm command for TypeScript', () => {
212
+ expect(DEFAULT_TEST_COMMANDS.typescript).toContain('npm');
213
+ });
214
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "lib": ["ES2022"],
7
+ "outDir": "./dist",
8
+ "rootDir": "./src",
9
+ "strict": true,
10
+ "esModuleInterop": true,
11
+ "skipLibCheck": true,
12
+ "forceConsistentCasingInFileNames": true,
13
+ "resolveJsonModule": true,
14
+ "declaration": true,
15
+ "declarationMap": true,
16
+ "sourceMap": true,
17
+ "noUnusedLocals": true,
18
+ "noUnusedParameters": true,
19
+ "noImplicitReturns": true,
20
+ "noFallthroughCasesInSwitch": true,
21
+ "allowSyntheticDefaultImports": true
22
+ },
23
+ "include": ["src/**/*"],
24
+ "exclude": ["node_modules", "dist", "tests"]
25
+ }
@@ -0,0 +1,22 @@
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ globals: true,
6
+ environment: 'node',
7
+ include: ['tests/**/*.test.ts'],
8
+ coverage: {
9
+ provider: 'v8',
10
+ reporter: ['text', 'json', 'html'],
11
+ include: ['src/**/*.ts'],
12
+ exclude: ['src/index.ts', 'src/**/*.d.ts'],
13
+ thresholds: {
14
+ statements: 80,
15
+ branches: 80,
16
+ functions: 80,
17
+ lines: 80,
18
+ },
19
+ },
20
+ testTimeout: 10000,
21
+ },
22
+ });