@auto-engineer/component-implementor-react 1.98.0 → 1.100.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 (233) 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 +92 -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 +109 -30
  8. package/dist/src/commands/implement-component.js.map +1 -1
  9. package/dist/src/commands/implement-component.test.js +259 -69
  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 +154 -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/tools/lint-runner.d.ts +7 -0
  115. package/dist/src/tools/lint-runner.d.ts.map +1 -0
  116. package/dist/src/tools/lint-runner.js +48 -0
  117. package/dist/src/tools/lint-runner.js.map +1 -0
  118. package/dist/src/tools/lint-runner.test.d.ts +2 -0
  119. package/dist/src/tools/lint-runner.test.d.ts.map +1 -0
  120. package/dist/src/tools/lint-runner.test.js +90 -0
  121. package/dist/src/tools/lint-runner.test.js.map +1 -0
  122. package/dist/src/tools/storybook-runner.d.ts +6 -0
  123. package/dist/src/tools/storybook-runner.d.ts.map +1 -0
  124. package/dist/src/tools/storybook-runner.js +25 -0
  125. package/dist/src/tools/storybook-runner.js.map +1 -0
  126. package/dist/src/tools/storybook-runner.test.d.ts +2 -0
  127. package/dist/src/tools/storybook-runner.test.d.ts.map +1 -0
  128. package/dist/src/tools/storybook-runner.test.js +43 -0
  129. package/dist/src/tools/storybook-runner.test.js.map +1 -0
  130. package/dist/src/tools/test-runner.d.ts +9 -0
  131. package/dist/src/tools/test-runner.d.ts.map +1 -0
  132. package/dist/src/tools/test-runner.js +74 -0
  133. package/dist/src/tools/test-runner.js.map +1 -0
  134. package/dist/src/tools/test-runner.test.d.ts +2 -0
  135. package/dist/src/tools/test-runner.test.d.ts.map +1 -0
  136. package/dist/src/tools/test-runner.test.js +177 -0
  137. package/dist/src/tools/test-runner.test.js.map +1 -0
  138. package/dist/src/tools/type-checker.d.ts +6 -0
  139. package/dist/src/tools/type-checker.d.ts.map +1 -0
  140. package/dist/src/tools/type-checker.js +36 -0
  141. package/dist/src/tools/type-checker.js.map +1 -0
  142. package/dist/src/tools/type-checker.test.d.ts +2 -0
  143. package/dist/src/tools/type-checker.test.d.ts.map +1 -0
  144. package/dist/src/tools/type-checker.test.js +96 -0
  145. package/dist/src/tools/type-checker.test.js.map +1 -0
  146. package/dist/tsconfig.tsbuildinfo +1 -1
  147. package/inputs/model-a/spec-deltas.json +1460 -0
  148. package/inputs/model-b/spec-deltas.json +1424 -0
  149. package/inputs/model-c/spec-deltas.json +1432 -0
  150. package/inputs/model-d/spec-deltas.json +967 -0
  151. package/inputs/model-e/spec-deltas.json +2292 -0
  152. package/ketchup-plan.md +43 -8
  153. package/package.json +3 -3
  154. package/scoring-heuristic.md +138 -0
  155. package/scripts/improve.ts +23 -18
  156. package/src/commands/implement-component.test.ts +309 -76
  157. package/src/commands/implement-component.ts +155 -31
  158. package/src/extract-exports.ts +53 -0
  159. package/src/generate-story-deterministic.ts +267 -0
  160. package/src/index.ts +12 -0
  161. package/src/pipeline/run-pipeline.test.ts +292 -0
  162. package/src/pipeline/run-pipeline.ts +160 -0
  163. package/src/pipeline/steps/generate-component.test.ts +130 -0
  164. package/src/pipeline/steps/generate-component.ts +60 -0
  165. package/src/pipeline/steps/generate-story.test.ts +54 -0
  166. package/src/pipeline/steps/generate-story.ts +17 -0
  167. package/src/pipeline/steps/generate-test.test.ts +75 -0
  168. package/src/pipeline/steps/generate-test.ts +25 -0
  169. package/src/pipeline/steps/lint-fix-loop.test.ts +155 -0
  170. package/src/pipeline/steps/lint-fix-loop.ts +59 -0
  171. package/src/pipeline/steps/story-fix-loop.test.ts +123 -0
  172. package/src/pipeline/steps/story-fix-loop.ts +47 -0
  173. package/src/pipeline/steps/storybook-test.test.ts +82 -0
  174. package/src/pipeline/steps/storybook-test.ts +27 -0
  175. package/src/pipeline/steps/test-fix-loop.test.ts +201 -0
  176. package/src/pipeline/steps/test-fix-loop.ts +56 -0
  177. package/src/pipeline/steps/type-fix-loop.test.ts +145 -0
  178. package/src/pipeline/steps/type-fix-loop.ts +55 -0
  179. package/src/pipeline/steps/visual-test.test.ts +10 -0
  180. package/src/pipeline/steps/visual-test.ts +5 -0
  181. package/src/project-context.ts +205 -0
  182. package/src/prompt.test.ts +174 -8
  183. package/src/prompt.ts +301 -23
  184. package/src/scaffold.ts +281 -0
  185. package/src/tools/lint-runner.test.ts +112 -0
  186. package/src/tools/lint-runner.ts +52 -0
  187. package/src/tools/storybook-runner.test.ts +53 -0
  188. package/src/tools/storybook-runner.ts +29 -0
  189. package/src/tools/test-runner.test.ts +213 -0
  190. package/src/tools/test-runner.ts +84 -0
  191. package/src/tools/type-checker.test.ts +120 -0
  192. package/src/tools/type-checker.ts +42 -0
  193. package/vitest.config.ts +9 -1
  194. package/dist/src/generate-component.d.ts +0 -4
  195. package/dist/src/generate-component.d.ts.map +0 -1
  196. package/dist/src/generate-component.js +0 -14
  197. package/dist/src/generate-component.js.map +0 -1
  198. package/dist/src/generate-component.test.d.ts.map +0 -1
  199. package/dist/src/generate-component.test.js +0 -73
  200. package/dist/src/generate-component.test.js.map +0 -1
  201. package/dist/src/generate-story.d.ts +0 -4
  202. package/dist/src/generate-story.d.ts.map +0 -1
  203. package/dist/src/generate-story.js +0 -14
  204. package/dist/src/generate-story.js.map +0 -1
  205. package/dist/src/generate-story.test.d.ts.map +0 -1
  206. package/dist/src/generate-story.test.js +0 -58
  207. package/dist/src/generate-story.test.js.map +0 -1
  208. package/dist/src/generate-test.d.ts +0 -4
  209. package/dist/src/generate-test.d.ts.map +0 -1
  210. package/dist/src/generate-test.js +0 -14
  211. package/dist/src/generate-test.js.map +0 -1
  212. package/dist/src/generate-test.test.d.ts.map +0 -1
  213. package/dist/src/generate-test.test.js +0 -77
  214. package/dist/src/generate-test.test.js.map +0 -1
  215. package/dist/src/reconcile.d.ts +0 -8
  216. package/dist/src/reconcile.d.ts.map +0 -1
  217. package/dist/src/reconcile.js +0 -18
  218. package/dist/src/reconcile.js.map +0 -1
  219. package/dist/src/reconcile.test.d.ts +0 -2
  220. package/dist/src/reconcile.test.d.ts.map +0 -1
  221. package/dist/src/reconcile.test.js +0 -108
  222. package/dist/src/reconcile.test.js.map +0 -1
  223. package/src/generate-component.test.ts +0 -89
  224. package/src/generate-component.ts +0 -16
  225. package/src/generate-story.test.ts +0 -71
  226. package/src/generate-story.ts +0 -16
  227. package/src/generate-test.test.ts +0 -93
  228. package/src/generate-test.ts +0 -16
  229. package/src/reconcile.test.ts +0 -127
  230. package/src/reconcile.ts +0 -27
  231. /package/dist/src/{generate-component.test.d.ts → pipeline/steps/generate-component.test.d.ts} +0 -0
  232. /package/dist/src/{generate-story.test.d.ts → pipeline/steps/generate-story.test.d.ts} +0 -0
  233. /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
+
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
+ });
106
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),
@@ -151,22 +177,13 @@ describe('implement-component', () => {
151
177
  });
152
178
 
153
179
  it('handles payload with undefined spec delta fields without crashing', async () => {
154
- const mockGenerateText = vi.mocked(generateText);
155
- mockGenerateText
156
- .mockResolvedValueOnce({ text: 'component file code' } as Awaited<ReturnType<typeof generateText>>)
157
- .mockResolvedValueOnce({ text: 'test file code' } as Awaited<ReturnType<typeof generateText>>)
158
- .mockResolvedValueOnce({ text: 'story file code' } as Awaited<ReturnType<typeof generateText>>)
159
- .mockResolvedValueOnce({
160
- text: '```tsx\nreconciled component\n```\n\n```tsx\nreconciled story\n```',
161
- } as Awaited<ReturnType<typeof generateText>>);
162
-
163
180
  vi.mocked(existsSync).mockReturnValue(false);
164
- vi.mocked(writeFile).mockResolvedValue(undefined);
181
+ vi.mocked(runPipeline).mockResolvedValue({
182
+ success: true,
183
+ llmCalls: 4,
184
+ fixIterations: 1,
185
+ });
165
186
 
166
- // Simulate production payload where the pipeline omits spec delta fields
167
- // (e.g. "Resource-preview", "Plant-fields-page" components).
168
- // At runtime JSON deserialization can produce undefined for these fields,
169
- // bypassing TypeScript's compile-time guarantees.
170
187
  const command = {
171
188
  type: 'ImplementComponent' as const,
172
189
  data: {
@@ -190,30 +207,20 @@ describe('implement-component', () => {
190
207
  correlationId: 'cor-1',
191
208
  };
192
209
 
193
- // Cast through unknown to bypass TypeScript — this is what happens at runtime
194
- // when the pipeline sends a payload with missing fields
195
210
  const result = await handleImplementComponent(
196
211
  command as unknown as Parameters<typeof handleImplementComponent>[0],
197
212
  );
198
213
 
199
- // Should succeed, not crash. Undefined spec delta fields should be treated as empty arrays.
200
- // Currently fails: buildSpecSection() throws "Cannot read properties of undefined (reading 'map')"
201
- // which gets caught by try/catch and returns ComponentImplementationFailed instead.
202
214
  expect(result.type).toBe('ComponentImplemented');
203
215
  });
204
216
 
205
217
  it('defaults targetDir to ./client when not provided', async () => {
206
- const mockGenerateText = vi.mocked(generateText);
207
- mockGenerateText
208
- .mockResolvedValueOnce({ text: 'component code' } as Awaited<ReturnType<typeof generateText>>)
209
- .mockResolvedValueOnce({ text: 'test code' } as Awaited<ReturnType<typeof generateText>>)
210
- .mockResolvedValueOnce({ text: 'story code' } as Awaited<ReturnType<typeof generateText>>)
211
- .mockResolvedValueOnce({
212
- text: '```tsx\nrc\n```\n```tsx\nrs\n```',
213
- } as Awaited<ReturnType<typeof generateText>>);
214
-
215
218
  vi.mocked(existsSync).mockReturnValue(false);
216
- vi.mocked(writeFile).mockResolvedValue(undefined);
219
+ vi.mocked(runPipeline).mockResolvedValue({
220
+ success: true,
221
+ llmCalls: 2,
222
+ fixIterations: 0,
223
+ });
217
224
 
218
225
  const command = {
219
226
  type: 'ImplementComponent' as const,
@@ -250,17 +257,12 @@ describe('implement-component', () => {
250
257
  });
251
258
 
252
259
  it('derives component path from componentId when files.create is empty string', async () => {
253
- const mockGenerateText = vi.mocked(generateText);
254
- mockGenerateText
255
- .mockResolvedValueOnce({ text: 'component code' } as Awaited<ReturnType<typeof generateText>>)
256
- .mockResolvedValueOnce({ text: 'test code' } as Awaited<ReturnType<typeof generateText>>)
257
- .mockResolvedValueOnce({ text: 'story code' } as Awaited<ReturnType<typeof generateText>>)
258
- .mockResolvedValueOnce({
259
- text: '```tsx\nrc\n```\n```tsx\nrs\n```',
260
- } as Awaited<ReturnType<typeof generateText>>);
261
-
262
260
  vi.mocked(existsSync).mockReturnValue(false);
263
- vi.mocked(writeFile).mockResolvedValue(undefined);
261
+ vi.mocked(runPipeline).mockResolvedValue({
262
+ success: true,
263
+ llmCalls: 4,
264
+ fixIterations: 0,
265
+ });
264
266
 
265
267
  const command = makeCommand({
266
268
  job: {
@@ -299,6 +301,37 @@ describe('implement-component', () => {
299
301
  correlationId: 'cor-1',
300
302
  });
301
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
+ });
302
335
  });
303
336
 
304
337
  describe('commandHandler', () => {
@@ -306,5 +339,205 @@ describe('implement-component', () => {
306
339
  expect(commandHandler.name).toBe('ImplementComponent');
307
340
  expect(commandHandler.alias).toBe('implement:component');
308
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
+ });
309
542
  });
310
543
  });