@auto-engineer/component-implementor-react 1.97.2 → 1.99.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 (241) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/.turbo/turbo-test.log +6 -6
  3. package/.turbo/turbo-type-check.log +1 -1
  4. package/CHANGELOG.md +99 -0
  5. package/dist/src/commands/implement-component.d.ts +19 -0
  6. package/dist/src/commands/implement-component.d.ts.map +1 -1
  7. package/dist/src/commands/implement-component.js +116 -34
  8. package/dist/src/commands/implement-component.js.map +1 -1
  9. package/dist/src/commands/implement-component.test.js +352 -33
  10. package/dist/src/commands/implement-component.test.js.map +1 -1
  11. package/dist/src/extract-exports.d.ts +6 -0
  12. package/dist/src/extract-exports.d.ts.map +1 -0
  13. package/dist/src/extract-exports.js +46 -0
  14. package/dist/src/extract-exports.js.map +1 -0
  15. package/dist/src/generate-story-deterministic.d.ts +30 -0
  16. package/dist/src/generate-story-deterministic.d.ts.map +1 -0
  17. package/dist/src/generate-story-deterministic.js +229 -0
  18. package/dist/src/generate-story-deterministic.js.map +1 -0
  19. package/dist/src/index.d.ts +4 -0
  20. package/dist/src/index.d.ts.map +1 -1
  21. package/dist/src/index.js +3 -0
  22. package/dist/src/index.js.map +1 -1
  23. package/dist/src/pipeline/run-pipeline.d.ts +69 -0
  24. package/dist/src/pipeline/run-pipeline.d.ts.map +1 -0
  25. package/dist/src/pipeline/run-pipeline.js +78 -0
  26. package/dist/src/pipeline/run-pipeline.js.map +1 -0
  27. package/dist/src/pipeline/run-pipeline.test.d.ts +2 -0
  28. package/dist/src/pipeline/run-pipeline.test.d.ts.map +1 -0
  29. package/dist/src/pipeline/run-pipeline.test.js +247 -0
  30. package/dist/src/pipeline/run-pipeline.test.js.map +1 -0
  31. package/dist/src/pipeline/steps/generate-component.d.ts +4 -0
  32. package/dist/src/pipeline/steps/generate-component.d.ts.map +1 -0
  33. package/dist/src/pipeline/steps/generate-component.js +50 -0
  34. package/dist/src/pipeline/steps/generate-component.js.map +1 -0
  35. package/dist/src/pipeline/steps/generate-component.test.d.ts.map +1 -0
  36. package/dist/src/pipeline/steps/generate-component.test.js +106 -0
  37. package/dist/src/pipeline/steps/generate-component.test.js.map +1 -0
  38. package/dist/src/pipeline/steps/generate-story.d.ts +3 -0
  39. package/dist/src/pipeline/steps/generate-story.d.ts.map +1 -0
  40. package/dist/src/pipeline/steps/generate-story.js +14 -0
  41. package/dist/src/pipeline/steps/generate-story.js.map +1 -0
  42. package/dist/src/pipeline/steps/generate-story.test.d.ts.map +1 -0
  43. package/dist/src/pipeline/steps/generate-story.test.js +41 -0
  44. package/dist/src/pipeline/steps/generate-story.test.js.map +1 -0
  45. package/dist/src/pipeline/steps/generate-test.d.ts +4 -0
  46. package/dist/src/pipeline/steps/generate-test.d.ts.map +1 -0
  47. package/dist/src/pipeline/steps/generate-test.js +19 -0
  48. package/dist/src/pipeline/steps/generate-test.js.map +1 -0
  49. package/dist/src/pipeline/steps/generate-test.test.d.ts.map +1 -0
  50. package/dist/src/pipeline/steps/generate-test.test.js +60 -0
  51. package/dist/src/pipeline/steps/generate-test.test.js.map +1 -0
  52. package/dist/src/pipeline/steps/lint-fix-loop.d.ts +4 -0
  53. package/dist/src/pipeline/steps/lint-fix-loop.d.ts.map +1 -0
  54. package/dist/src/pipeline/steps/lint-fix-loop.js +45 -0
  55. package/dist/src/pipeline/steps/lint-fix-loop.js.map +1 -0
  56. package/dist/src/pipeline/steps/lint-fix-loop.test.d.ts +2 -0
  57. package/dist/src/pipeline/steps/lint-fix-loop.test.d.ts.map +1 -0
  58. package/dist/src/pipeline/steps/lint-fix-loop.test.js +119 -0
  59. package/dist/src/pipeline/steps/lint-fix-loop.test.js.map +1 -0
  60. package/dist/src/pipeline/steps/story-fix-loop.d.ts +4 -0
  61. package/dist/src/pipeline/steps/story-fix-loop.d.ts.map +1 -0
  62. package/dist/src/pipeline/steps/story-fix-loop.js +34 -0
  63. package/dist/src/pipeline/steps/story-fix-loop.js.map +1 -0
  64. package/dist/src/pipeline/steps/story-fix-loop.test.d.ts +2 -0
  65. package/dist/src/pipeline/steps/story-fix-loop.test.d.ts.map +1 -0
  66. package/dist/src/pipeline/steps/story-fix-loop.test.js +94 -0
  67. package/dist/src/pipeline/steps/story-fix-loop.test.js.map +1 -0
  68. package/dist/src/pipeline/steps/storybook-test.d.ts +3 -0
  69. package/dist/src/pipeline/steps/storybook-test.d.ts.map +1 -0
  70. package/dist/src/pipeline/steps/storybook-test.js +22 -0
  71. package/dist/src/pipeline/steps/storybook-test.js.map +1 -0
  72. package/dist/src/pipeline/steps/storybook-test.test.d.ts +2 -0
  73. package/dist/src/pipeline/steps/storybook-test.test.d.ts.map +1 -0
  74. package/dist/src/pipeline/steps/storybook-test.test.js +66 -0
  75. package/dist/src/pipeline/steps/storybook-test.test.js.map +1 -0
  76. package/dist/src/pipeline/steps/test-fix-loop.d.ts +4 -0
  77. package/dist/src/pipeline/steps/test-fix-loop.d.ts.map +1 -0
  78. package/dist/src/pipeline/steps/test-fix-loop.js +44 -0
  79. package/dist/src/pipeline/steps/test-fix-loop.js.map +1 -0
  80. package/dist/src/pipeline/steps/test-fix-loop.test.d.ts +2 -0
  81. package/dist/src/pipeline/steps/test-fix-loop.test.d.ts.map +1 -0
  82. package/dist/src/pipeline/steps/test-fix-loop.test.js +168 -0
  83. package/dist/src/pipeline/steps/test-fix-loop.test.js.map +1 -0
  84. package/dist/src/pipeline/steps/type-fix-loop.d.ts +4 -0
  85. package/dist/src/pipeline/steps/type-fix-loop.d.ts.map +1 -0
  86. package/dist/src/pipeline/steps/type-fix-loop.js +43 -0
  87. package/dist/src/pipeline/steps/type-fix-loop.js.map +1 -0
  88. package/dist/src/pipeline/steps/type-fix-loop.test.d.ts +2 -0
  89. package/dist/src/pipeline/steps/type-fix-loop.test.d.ts.map +1 -0
  90. package/dist/src/pipeline/steps/type-fix-loop.test.js +112 -0
  91. package/dist/src/pipeline/steps/type-fix-loop.test.js.map +1 -0
  92. package/dist/src/pipeline/steps/visual-test.d.ts +3 -0
  93. package/dist/src/pipeline/steps/visual-test.d.ts.map +1 -0
  94. package/dist/src/pipeline/steps/visual-test.js +4 -0
  95. package/dist/src/pipeline/steps/visual-test.js.map +1 -0
  96. package/dist/src/pipeline/steps/visual-test.test.d.ts +2 -0
  97. package/dist/src/pipeline/steps/visual-test.test.d.ts.map +1 -0
  98. package/dist/src/pipeline/steps/visual-test.test.js +9 -0
  99. package/dist/src/pipeline/steps/visual-test.test.js.map +1 -0
  100. package/dist/src/project-context.d.ts +10 -0
  101. package/dist/src/project-context.d.ts.map +1 -0
  102. package/dist/src/project-context.js +178 -0
  103. package/dist/src/project-context.js.map +1 -0
  104. package/dist/src/prompt.d.ts +39 -7
  105. package/dist/src/prompt.d.ts.map +1 -1
  106. package/dist/src/prompt.js +233 -23
  107. package/dist/src/prompt.js.map +1 -1
  108. package/dist/src/prompt.test.js +225 -9
  109. package/dist/src/prompt.test.js.map +1 -1
  110. package/dist/src/scaffold.d.ts +49 -0
  111. package/dist/src/scaffold.d.ts.map +1 -0
  112. package/dist/src/scaffold.js +208 -0
  113. package/dist/src/scaffold.js.map +1 -0
  114. package/dist/src/spec-contract.js +1 -1
  115. package/dist/src/spec-contract.js.map +1 -1
  116. package/dist/src/spec-contract.test.d.ts +2 -0
  117. package/dist/src/spec-contract.test.d.ts.map +1 -0
  118. package/dist/src/spec-contract.test.js +21 -0
  119. package/dist/src/spec-contract.test.js.map +1 -0
  120. package/dist/src/tools/lint-runner.d.ts +7 -0
  121. package/dist/src/tools/lint-runner.d.ts.map +1 -0
  122. package/dist/src/tools/lint-runner.js +48 -0
  123. package/dist/src/tools/lint-runner.js.map +1 -0
  124. package/dist/src/tools/lint-runner.test.d.ts +2 -0
  125. package/dist/src/tools/lint-runner.test.d.ts.map +1 -0
  126. package/dist/src/tools/lint-runner.test.js +90 -0
  127. package/dist/src/tools/lint-runner.test.js.map +1 -0
  128. package/dist/src/tools/storybook-runner.d.ts +6 -0
  129. package/dist/src/tools/storybook-runner.d.ts.map +1 -0
  130. package/dist/src/tools/storybook-runner.js +25 -0
  131. package/dist/src/tools/storybook-runner.js.map +1 -0
  132. package/dist/src/tools/storybook-runner.test.d.ts +2 -0
  133. package/dist/src/tools/storybook-runner.test.d.ts.map +1 -0
  134. package/dist/src/tools/storybook-runner.test.js +43 -0
  135. package/dist/src/tools/storybook-runner.test.js.map +1 -0
  136. package/dist/src/tools/test-runner.d.ts +9 -0
  137. package/dist/src/tools/test-runner.d.ts.map +1 -0
  138. package/dist/src/tools/test-runner.js +74 -0
  139. package/dist/src/tools/test-runner.js.map +1 -0
  140. package/dist/src/tools/test-runner.test.d.ts +2 -0
  141. package/dist/src/tools/test-runner.test.d.ts.map +1 -0
  142. package/dist/src/tools/test-runner.test.js +177 -0
  143. package/dist/src/tools/test-runner.test.js.map +1 -0
  144. package/dist/src/tools/type-checker.d.ts +6 -0
  145. package/dist/src/tools/type-checker.d.ts.map +1 -0
  146. package/dist/src/tools/type-checker.js +36 -0
  147. package/dist/src/tools/type-checker.js.map +1 -0
  148. package/dist/src/tools/type-checker.test.d.ts +2 -0
  149. package/dist/src/tools/type-checker.test.d.ts.map +1 -0
  150. package/dist/src/tools/type-checker.test.js +96 -0
  151. package/dist/src/tools/type-checker.test.js.map +1 -0
  152. package/dist/tsconfig.tsbuildinfo +1 -1
  153. package/inputs/model-a/spec-deltas.json +1460 -0
  154. package/inputs/model-b/spec-deltas.json +1424 -0
  155. package/inputs/model-c/spec-deltas.json +1432 -0
  156. package/inputs/model-d/spec-deltas.json +967 -0
  157. package/inputs/model-e/spec-deltas.json +2292 -0
  158. package/ketchup-plan.md +43 -8
  159. package/package.json +3 -3
  160. package/scoring-heuristic.md +138 -0
  161. package/scripts/improve.ts +23 -18
  162. package/src/commands/implement-component.test.ts +420 -37
  163. package/src/commands/implement-component.ts +163 -35
  164. package/src/extract-exports.ts +53 -0
  165. package/src/generate-story-deterministic.ts +267 -0
  166. package/src/index.ts +12 -0
  167. package/src/pipeline/run-pipeline.test.ts +292 -0
  168. package/src/pipeline/run-pipeline.ts +160 -0
  169. package/src/pipeline/steps/generate-component.test.ts +130 -0
  170. package/src/pipeline/steps/generate-component.ts +60 -0
  171. package/src/pipeline/steps/generate-story.test.ts +54 -0
  172. package/src/pipeline/steps/generate-story.ts +17 -0
  173. package/src/pipeline/steps/generate-test.test.ts +75 -0
  174. package/src/pipeline/steps/generate-test.ts +25 -0
  175. package/src/pipeline/steps/lint-fix-loop.test.ts +155 -0
  176. package/src/pipeline/steps/lint-fix-loop.ts +59 -0
  177. package/src/pipeline/steps/story-fix-loop.test.ts +123 -0
  178. package/src/pipeline/steps/story-fix-loop.ts +47 -0
  179. package/src/pipeline/steps/storybook-test.test.ts +82 -0
  180. package/src/pipeline/steps/storybook-test.ts +27 -0
  181. package/src/pipeline/steps/test-fix-loop.test.ts +201 -0
  182. package/src/pipeline/steps/test-fix-loop.ts +56 -0
  183. package/src/pipeline/steps/type-fix-loop.test.ts +145 -0
  184. package/src/pipeline/steps/type-fix-loop.ts +55 -0
  185. package/src/pipeline/steps/visual-test.test.ts +10 -0
  186. package/src/pipeline/steps/visual-test.ts +5 -0
  187. package/src/project-context.ts +205 -0
  188. package/src/prompt.test.ts +253 -8
  189. package/src/prompt.ts +301 -23
  190. package/src/scaffold.ts +281 -0
  191. package/src/spec-contract.test.ts +23 -0
  192. package/src/spec-contract.ts +1 -1
  193. package/src/tools/lint-runner.test.ts +112 -0
  194. package/src/tools/lint-runner.ts +52 -0
  195. package/src/tools/storybook-runner.test.ts +53 -0
  196. package/src/tools/storybook-runner.ts +29 -0
  197. package/src/tools/test-runner.test.ts +213 -0
  198. package/src/tools/test-runner.ts +84 -0
  199. package/src/tools/type-checker.test.ts +120 -0
  200. package/src/tools/type-checker.ts +42 -0
  201. package/vitest.config.ts +9 -1
  202. package/dist/src/generate-component.d.ts +0 -4
  203. package/dist/src/generate-component.d.ts.map +0 -1
  204. package/dist/src/generate-component.js +0 -14
  205. package/dist/src/generate-component.js.map +0 -1
  206. package/dist/src/generate-component.test.d.ts.map +0 -1
  207. package/dist/src/generate-component.test.js +0 -73
  208. package/dist/src/generate-component.test.js.map +0 -1
  209. package/dist/src/generate-story.d.ts +0 -4
  210. package/dist/src/generate-story.d.ts.map +0 -1
  211. package/dist/src/generate-story.js +0 -14
  212. package/dist/src/generate-story.js.map +0 -1
  213. package/dist/src/generate-story.test.d.ts.map +0 -1
  214. package/dist/src/generate-story.test.js +0 -58
  215. package/dist/src/generate-story.test.js.map +0 -1
  216. package/dist/src/generate-test.d.ts +0 -4
  217. package/dist/src/generate-test.d.ts.map +0 -1
  218. package/dist/src/generate-test.js +0 -14
  219. package/dist/src/generate-test.js.map +0 -1
  220. package/dist/src/generate-test.test.d.ts.map +0 -1
  221. package/dist/src/generate-test.test.js +0 -77
  222. package/dist/src/generate-test.test.js.map +0 -1
  223. package/dist/src/reconcile.d.ts +0 -8
  224. package/dist/src/reconcile.d.ts.map +0 -1
  225. package/dist/src/reconcile.js +0 -18
  226. package/dist/src/reconcile.js.map +0 -1
  227. package/dist/src/reconcile.test.d.ts +0 -2
  228. package/dist/src/reconcile.test.d.ts.map +0 -1
  229. package/dist/src/reconcile.test.js +0 -108
  230. package/dist/src/reconcile.test.js.map +0 -1
  231. package/src/generate-component.test.ts +0 -89
  232. package/src/generate-component.ts +0 -16
  233. package/src/generate-story.test.ts +0 -71
  234. package/src/generate-story.ts +0 -16
  235. package/src/generate-test.test.ts +0 -93
  236. package/src/generate-test.ts +0 -16
  237. package/src/reconcile.test.ts +0 -127
  238. package/src/reconcile.ts +0 -27
  239. /package/dist/src/{generate-component.test.d.ts → pipeline/steps/generate-component.test.d.ts} +0 -0
  240. /package/dist/src/{generate-story.test.d.ts → pipeline/steps/generate-story.test.d.ts} +0 -0
  241. /package/dist/src/{generate-test.test.d.ts → pipeline/steps/generate-test.test.d.ts} +0 -0
@@ -1,16 +1,11 @@
1
1
  import { afterEach, describe, expect, it, vi } from 'vitest';
2
2
 
3
- vi.mock('ai', () => ({
4
- generateText: vi.fn(),
5
- }));
6
-
7
3
  vi.mock('@auto-engineer/model-factory', () => ({
8
4
  createModelFromEnv: vi.fn(() => 'mock-model'),
9
5
  }));
10
6
 
11
7
  vi.mock('node:fs/promises', () => ({
12
8
  readFile: vi.fn(),
13
- writeFile: vi.fn(),
14
9
  mkdir: vi.fn(),
15
10
  }));
16
11
 
@@ -18,11 +13,24 @@ vi.mock('node:fs', () => ({
18
13
  existsSync: vi.fn(),
19
14
  }));
20
15
 
16
+ vi.mock('../project-context', () => ({
17
+ buildFullProjectSection: vi.fn().mockResolvedValue(''),
18
+ }));
19
+
20
+ vi.mock('../pipeline/run-pipeline', () => ({
21
+ buildPipelineSteps: vi.fn(() => []),
22
+ runPipeline: vi.fn(),
23
+ }));
24
+
21
25
  import { existsSync } from 'node:fs';
22
- import { readFile, writeFile } from 'node:fs/promises';
23
- import { generateText } from 'ai';
26
+ import { readFile } from 'node:fs/promises';
27
+ import { runPipeline } from '../pipeline/run-pipeline';
24
28
  import { commandHandler, handleImplementComponent } from './implement-component';
25
29
 
30
+ afterEach(() => {
31
+ vi.clearAllMocks();
32
+ });
33
+
26
34
  function makeCommand(overrides: Record<string, unknown> = {}) {
27
35
  return {
28
36
  type: 'ImplementComponent' as const,
@@ -40,6 +48,7 @@ function makeCommand(overrides: Record<string, unknown> = {}) {
40
48
  styling: ['uses primary class'],
41
49
  storybookPath: 'src/components/ui/MyButton.stories.tsx',
42
50
  files: { create: ['src/components/ui/MyButton.tsx'] },
51
+ composes: [] as { id: string; path: string }[],
43
52
  },
44
53
  },
45
54
  ...overrides,
@@ -55,20 +64,13 @@ describe('implement-component', () => {
55
64
  });
56
65
 
57
66
  describe('handleImplementComponent', () => {
58
- it('generates 3 files via parallel agents + reconciliation and returns ComponentImplemented', async () => {
59
- const mockGenerateText = vi.mocked(generateText);
60
- // Phase 1: 3 parallel calls (component, test, story)
61
- mockGenerateText
62
- .mockResolvedValueOnce({ text: 'component file code' } as Awaited<ReturnType<typeof generateText>>)
63
- .mockResolvedValueOnce({ text: 'test file code' } as Awaited<ReturnType<typeof generateText>>)
64
- .mockResolvedValueOnce({ text: 'story file code' } as Awaited<ReturnType<typeof generateText>>)
65
- // Phase 2: reconciliation
66
- .mockResolvedValueOnce({
67
- text: '```tsx\nreconciled component\n```\n\n```tsx\nreconciled story\n```',
68
- } as Awaited<ReturnType<typeof generateText>>);
69
-
67
+ it('runs pipeline and returns ComponentImplemented on success', async () => {
70
68
  vi.mocked(existsSync).mockReturnValue(false);
71
- vi.mocked(writeFile).mockResolvedValue(undefined);
69
+ vi.mocked(runPipeline).mockResolvedValue({
70
+ success: true,
71
+ llmCalls: 4,
72
+ fixIterations: 1,
73
+ });
72
74
 
73
75
  const result = await handleImplementComponent(makeCommand());
74
76
 
@@ -90,23 +92,40 @@ describe('implement-component', () => {
90
92
  correlationId: 'cor-1',
91
93
  });
92
94
 
93
- expect(mockGenerateText).toHaveBeenCalledTimes(4);
94
- expect(writeFile).toHaveBeenCalledTimes(3);
95
+ expect(runPipeline).toHaveBeenCalledTimes(1);
95
96
  });
96
97
 
97
- it('reads existing component when modifying', async () => {
98
- const mockGenerateText = vi.mocked(generateText);
99
- mockGenerateText
100
- .mockResolvedValueOnce({ text: 'component code' } as Awaited<ReturnType<typeof generateText>>)
101
- .mockResolvedValueOnce({ text: 'test code' } as Awaited<ReturnType<typeof generateText>>)
102
- .mockResolvedValueOnce({ text: 'story code' } as Awaited<ReturnType<typeof generateText>>)
103
- .mockResolvedValueOnce({
104
- text: '```tsx\nrc\n```\n```tsx\nrs\n```',
105
- } as Awaited<ReturnType<typeof generateText>>);
98
+ it('returns ComponentImplementationFailed when pipeline fails', async () => {
99
+ vi.mocked(existsSync).mockReturnValue(false);
100
+ vi.mocked(runPipeline).mockResolvedValue({
101
+ success: false,
102
+ error: 'Step "Type Fix Loop" failed: errors remain',
103
+ llmCalls: 5,
104
+ fixIterations: 3,
105
+ });
106
106
 
107
+ const result = await handleImplementComponent(makeCommand());
108
+
109
+ expect(result).toEqual({
110
+ type: 'ComponentImplementationFailed',
111
+ data: {
112
+ error: 'Step "Type Fix Loop" failed: errors remain',
113
+ name: 'MyButton',
114
+ },
115
+ timestamp: expect.any(Date),
116
+ requestId: 'req-1',
117
+ correlationId: 'cor-1',
118
+ });
119
+ });
120
+
121
+ it('reads existing component when modifying', async () => {
107
122
  vi.mocked(existsSync).mockReturnValue(true);
108
123
  vi.mocked(readFile).mockResolvedValue('existing component code' as never);
109
- vi.mocked(writeFile).mockResolvedValue(undefined);
124
+ vi.mocked(runPipeline).mockResolvedValue({
125
+ success: true,
126
+ llmCalls: 3,
127
+ fixIterations: 0,
128
+ });
110
129
 
111
130
  const command = makeCommand({
112
131
  job: {
@@ -121,6 +140,7 @@ describe('implement-component', () => {
121
140
  styling: [],
122
141
  storybookPath: 'src/components/ui/MyButton.stories.tsx',
123
142
  files: { modify: ['src/components/ui/MyButton.tsx'] },
143
+ composes: [] as { id: string; path: string }[],
124
144
  },
125
145
  },
126
146
  });
@@ -128,20 +148,26 @@ describe('implement-component', () => {
128
148
  await handleImplementComponent(command);
129
149
 
130
150
  expect(readFile).toHaveBeenCalledWith('/project/client/src/components/ui/MyButton.tsx', 'utf-8');
131
- });
132
151
 
133
- it('returns ComponentImplementationFailed on error', async () => {
134
- const mockGenerateText = vi.mocked(generateText);
135
- mockGenerateText.mockRejectedValue(new Error('AI service down'));
152
+ expect(runPipeline).toHaveBeenCalledWith(
153
+ expect.anything(),
154
+ expect.objectContaining({
155
+ existingComponent: 'existing component code',
156
+ isModify: true,
157
+ }),
158
+ );
159
+ });
136
160
 
161
+ it('returns ComponentImplementationFailed on unexpected error', async () => {
137
162
  vi.mocked(existsSync).mockReturnValue(false);
163
+ vi.mocked(runPipeline).mockRejectedValue(new Error('Unexpected crash'));
138
164
 
139
165
  const result = await handleImplementComponent(makeCommand());
140
166
 
141
167
  expect(result).toEqual({
142
168
  type: 'ComponentImplementationFailed',
143
169
  data: {
144
- error: 'AI service down',
170
+ error: 'Unexpected crash',
145
171
  name: 'MyButton',
146
172
  },
147
173
  timestamp: expect.any(Date),
@@ -149,6 +175,163 @@ describe('implement-component', () => {
149
175
  correlationId: 'cor-1',
150
176
  });
151
177
  });
178
+
179
+ it('handles payload with undefined spec delta fields without crashing', async () => {
180
+ vi.mocked(existsSync).mockReturnValue(false);
181
+ vi.mocked(runPipeline).mockResolvedValue({
182
+ success: true,
183
+ llmCalls: 4,
184
+ fixIterations: 1,
185
+ });
186
+
187
+ const command = {
188
+ type: 'ImplementComponent' as const,
189
+ data: {
190
+ targetDir: '/project/client',
191
+ job: {
192
+ id: 'job_3',
193
+ dependsOn: [],
194
+ target: 'ImplementComponent' as const,
195
+ payload: {
196
+ componentId: 'resource-preview',
197
+ structure: ['renders a preview card'],
198
+ rendering: undefined,
199
+ interaction: undefined,
200
+ styling: undefined,
201
+ storybookPath: 'src/components/ui/ResourcePreview.stories.tsx',
202
+ files: { create: ['src/components/ui/ResourcePreview.tsx'] },
203
+ },
204
+ },
205
+ },
206
+ requestId: 'req-1',
207
+ correlationId: 'cor-1',
208
+ };
209
+
210
+ const result = await handleImplementComponent(
211
+ command as unknown as Parameters<typeof handleImplementComponent>[0],
212
+ );
213
+
214
+ expect(result.type).toBe('ComponentImplemented');
215
+ });
216
+
217
+ it('defaults targetDir to ./client when not provided', async () => {
218
+ vi.mocked(existsSync).mockReturnValue(false);
219
+ vi.mocked(runPipeline).mockResolvedValue({
220
+ success: true,
221
+ llmCalls: 2,
222
+ fixIterations: 0,
223
+ });
224
+
225
+ const command = {
226
+ type: 'ImplementComponent' as const,
227
+ data: {
228
+ job: {
229
+ id: 'job_1',
230
+ dependsOn: [],
231
+ target: 'ImplementComponent' as const,
232
+ payload: {
233
+ componentId: 'my-widget',
234
+ structure: ['renders a widget'],
235
+ rendering: ['shows content'],
236
+ interaction: ['handles click'],
237
+ styling: ['uses primary'],
238
+ storybookPath: 'src/MyWidget.stories.tsx',
239
+ files: { create: ['src/MyWidget.tsx'] },
240
+ },
241
+ },
242
+ },
243
+ requestId: 'req-1',
244
+ correlationId: 'cor-1',
245
+ };
246
+
247
+ const result = await handleImplementComponent(
248
+ command as unknown as Parameters<typeof handleImplementComponent>[0],
249
+ );
250
+
251
+ expect(result).toEqual(
252
+ expect.objectContaining({
253
+ type: 'ComponentImplemented',
254
+ data: expect.objectContaining({ name: 'MyWidget' }),
255
+ }),
256
+ );
257
+ });
258
+
259
+ it('derives component path from componentId when files.create is empty string', async () => {
260
+ vi.mocked(existsSync).mockReturnValue(false);
261
+ vi.mocked(runPipeline).mockResolvedValue({
262
+ success: true,
263
+ llmCalls: 4,
264
+ fixIterations: 0,
265
+ });
266
+
267
+ const command = makeCommand({
268
+ job: {
269
+ id: 'job_1',
270
+ dependsOn: [],
271
+ target: 'ImplementComponent',
272
+ payload: {
273
+ componentId: 'task-form-input',
274
+ structure: ['renders input'],
275
+ rendering: [],
276
+ interaction: [],
277
+ styling: [],
278
+ storybookPath: '.stories',
279
+ files: { create: [''] },
280
+ },
281
+ },
282
+ });
283
+
284
+ const result = await handleImplementComponent(command);
285
+
286
+ expect(result).toEqual({
287
+ type: 'ComponentImplemented',
288
+ data: {
289
+ name: 'TaskFormInput',
290
+ componentPath: expect.stringContaining('src/components/TaskFormInput.tsx'),
291
+ testPath: expect.stringContaining('src/components/TaskFormInput.test.tsx'),
292
+ storyPath: expect.stringContaining('src/components/TaskFormInput.stories.tsx'),
293
+ filesCreated: expect.arrayContaining([
294
+ expect.stringContaining('TaskFormInput.tsx'),
295
+ expect.stringContaining('TaskFormInput.test.tsx'),
296
+ expect.stringContaining('TaskFormInput.stories.tsx'),
297
+ ]),
298
+ },
299
+ timestamp: expect.any(Date),
300
+ requestId: 'req-1',
301
+ correlationId: 'cor-1',
302
+ });
303
+ });
304
+
305
+ it('passes correct context to pipeline', async () => {
306
+ vi.mocked(existsSync).mockReturnValue(false);
307
+ vi.mocked(runPipeline).mockResolvedValue({
308
+ success: true,
309
+ llmCalls: 2,
310
+ fixIterations: 0,
311
+ });
312
+
313
+ await handleImplementComponent(makeCommand());
314
+
315
+ expect(runPipeline).toHaveBeenCalledWith(
316
+ expect.anything(),
317
+ expect.objectContaining({
318
+ componentName: 'MyButton',
319
+ componentPath: '/project/client/src/components/ui/MyButton.tsx',
320
+ testPath: '/project/client/src/components/ui/MyButton.test.tsx',
321
+ storyPath: '/project/client/src/components/ui/MyButton.stories.tsx',
322
+ componentImportPath: '@/components/ui/MyButton',
323
+ specDeltas: {
324
+ structure: ['renders a button element'],
325
+ rendering: ['shows spinner when loading'],
326
+ interaction: ['calls onClick handler'],
327
+ styling: ['uses primary class'],
328
+ },
329
+ isModify: false,
330
+ llmCalls: 0,
331
+ fixIterations: 0,
332
+ }),
333
+ );
334
+ });
152
335
  });
153
336
 
154
337
  describe('commandHandler', () => {
@@ -156,5 +339,205 @@ describe('implement-component', () => {
156
339
  expect(commandHandler.name).toBe('ImplementComponent');
157
340
  expect(commandHandler.alias).toBe('implement:component');
158
341
  });
342
+
343
+ it('handle delegates to handleImplementComponent', async () => {
344
+ vi.mocked(existsSync).mockReturnValue(false);
345
+ vi.mocked(runPipeline).mockResolvedValue({
346
+ success: true,
347
+ llmCalls: 0,
348
+ fixIterations: 0,
349
+ });
350
+
351
+ const result = await commandHandler.handle(makeCommand());
352
+
353
+ expect(result).toEqual(expect.objectContaining({ type: 'ComponentImplemented' }));
354
+ });
355
+ });
356
+
357
+ describe('filePathToImportAlias edge cases', () => {
358
+ it('handles paths starting with ./', async () => {
359
+ vi.mocked(existsSync).mockReturnValue(false);
360
+ vi.mocked(runPipeline).mockResolvedValue({
361
+ success: true,
362
+ llmCalls: 0,
363
+ fixIterations: 0,
364
+ });
365
+
366
+ const command = makeCommand({
367
+ job: {
368
+ id: 'job_1',
369
+ dependsOn: [],
370
+ target: 'ImplementComponent',
371
+ payload: {
372
+ componentId: 'my-button',
373
+ structure: [],
374
+ rendering: [],
375
+ interaction: [],
376
+ styling: [],
377
+ storybookPath: 'src/components/ui/MyButton.stories.tsx',
378
+ files: { create: ['./src/components/ui/MyButton.tsx'] },
379
+ composes: [] as { id: string; path: string }[],
380
+ },
381
+ },
382
+ });
383
+
384
+ await handleImplementComponent(command);
385
+
386
+ expect(runPipeline).toHaveBeenCalledWith(
387
+ expect.anything(),
388
+ expect.objectContaining({
389
+ componentImportPath: '@/components/ui/MyButton',
390
+ }),
391
+ );
392
+ });
393
+ });
394
+
395
+ describe('edge cases', () => {
396
+ it('reads existing test when modifying and test file exists', async () => {
397
+ vi.mocked(existsSync).mockReturnValue(true);
398
+ vi.mocked(readFile).mockResolvedValue('existing code' as never);
399
+ vi.mocked(runPipeline).mockResolvedValue({
400
+ success: true,
401
+ llmCalls: 1,
402
+ fixIterations: 0,
403
+ });
404
+
405
+ const command = makeCommand({
406
+ job: {
407
+ id: 'job_2',
408
+ dependsOn: [],
409
+ target: 'ImplementComponent',
410
+ payload: {
411
+ componentId: 'my-button',
412
+ structure: [],
413
+ rendering: [],
414
+ interaction: [],
415
+ styling: [],
416
+ storybookPath: 'src/components/ui/MyButton.stories.tsx',
417
+ files: { modify: ['src/components/ui/MyButton.tsx'] },
418
+ composes: [] as { id: string; path: string }[],
419
+ },
420
+ },
421
+ });
422
+
423
+ await handleImplementComponent(command);
424
+
425
+ expect(runPipeline).toHaveBeenCalledWith(
426
+ expect.anything(),
427
+ expect.objectContaining({
428
+ existingComponent: 'existing code',
429
+ existingTest: 'existing code',
430
+ }),
431
+ );
432
+ });
433
+
434
+ it('sets existingTest to undefined when test file does not exist in modify mode', async () => {
435
+ vi.mocked(existsSync).mockReturnValueOnce(true).mockReturnValueOnce(false);
436
+ vi.mocked(readFile).mockResolvedValue('existing component' as never);
437
+ vi.mocked(runPipeline).mockResolvedValue({
438
+ success: true,
439
+ llmCalls: 1,
440
+ fixIterations: 0,
441
+ });
442
+
443
+ const command = makeCommand({
444
+ job: {
445
+ id: 'job_2',
446
+ dependsOn: [],
447
+ target: 'ImplementComponent',
448
+ payload: {
449
+ componentId: 'my-button',
450
+ structure: [],
451
+ rendering: [],
452
+ interaction: [],
453
+ styling: [],
454
+ storybookPath: 'src/components/ui/MyButton.stories.tsx',
455
+ files: { modify: ['src/components/ui/MyButton.tsx'] },
456
+ composes: [] as { id: string; path: string }[],
457
+ },
458
+ },
459
+ });
460
+
461
+ await handleImplementComponent(command);
462
+
463
+ expect(runPipeline).toHaveBeenCalledWith(
464
+ expect.anything(),
465
+ expect.objectContaining({
466
+ existingComponent: 'existing component',
467
+ existingTest: undefined,
468
+ }),
469
+ );
470
+ });
471
+
472
+ it('uses fallback error message when pipeline result has no error string', async () => {
473
+ vi.mocked(existsSync).mockReturnValue(false);
474
+ vi.mocked(runPipeline).mockResolvedValue({
475
+ success: false,
476
+ llmCalls: 0,
477
+ fixIterations: 0,
478
+ });
479
+
480
+ const result = await handleImplementComponent(makeCommand());
481
+
482
+ expect(result).toEqual({
483
+ type: 'ComponentImplementationFailed',
484
+ data: {
485
+ error: 'Pipeline failed',
486
+ name: 'MyButton',
487
+ },
488
+ timestamp: expect.any(Date),
489
+ requestId: 'req-1',
490
+ correlationId: 'cor-1',
491
+ });
492
+ });
493
+
494
+ it('handles non-Error thrown from pipeline', async () => {
495
+ vi.mocked(existsSync).mockReturnValue(false);
496
+ vi.mocked(runPipeline).mockRejectedValue('string error');
497
+
498
+ const result = await handleImplementComponent(makeCommand());
499
+
500
+ expect(result).toEqual({
501
+ type: 'ComponentImplementationFailed',
502
+ data: {
503
+ error: 'string error',
504
+ name: 'MyButton',
505
+ },
506
+ timestamp: expect.any(Date),
507
+ requestId: 'req-1',
508
+ correlationId: 'cor-1',
509
+ });
510
+ });
511
+
512
+ it('handles empty files payload gracefully', async () => {
513
+ vi.mocked(existsSync).mockReturnValue(false);
514
+ vi.mocked(runPipeline).mockResolvedValue({
515
+ success: true,
516
+ llmCalls: 0,
517
+ fixIterations: 0,
518
+ });
519
+
520
+ const command = makeCommand({
521
+ job: {
522
+ id: 'job_1',
523
+ dependsOn: [],
524
+ target: 'ImplementComponent',
525
+ payload: {
526
+ componentId: 'my-button',
527
+ structure: [],
528
+ rendering: [],
529
+ interaction: [],
530
+ styling: [],
531
+ storybookPath: 'src/components/ui/MyButton.stories.tsx',
532
+ files: {},
533
+ composes: [] as { id: string; path: string }[],
534
+ },
535
+ },
536
+ });
537
+
538
+ const result = await handleImplementComponent(command);
539
+
540
+ expect(result).toEqual(expect.objectContaining({ type: 'ComponentImplemented' }));
541
+ });
159
542
  });
160
543
  });