@auto-engineer/component-implementor-react 1.110.2 → 1.110.3

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 (96) 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 +19 -0
  5. package/dist/src/commands/implement-component.d.ts +5 -6
  6. package/dist/src/commands/implement-component.d.ts.map +1 -1
  7. package/dist/src/commands/implement-component.js +37 -9
  8. package/dist/src/commands/implement-component.js.map +1 -1
  9. package/dist/src/commands/implement-component.test.js +41 -54
  10. package/dist/src/commands/implement-component.test.js.map +1 -1
  11. package/dist/src/pipeline/run-pipeline.d.ts +25 -5
  12. package/dist/src/pipeline/run-pipeline.d.ts.map +1 -1
  13. package/dist/src/pipeline/run-pipeline.js +47 -17
  14. package/dist/src/pipeline/run-pipeline.js.map +1 -1
  15. package/dist/src/pipeline/run-pipeline.test.js +129 -29
  16. package/dist/src/pipeline/run-pipeline.test.js.map +1 -1
  17. package/dist/src/pipeline/steps/generate-component.test.js +5 -1
  18. package/dist/src/pipeline/steps/generate-component.test.js.map +1 -1
  19. package/dist/src/pipeline/steps/generate-story.test.js +5 -1
  20. package/dist/src/pipeline/steps/generate-story.test.js.map +1 -1
  21. package/dist/src/pipeline/steps/generate-test.test.js +5 -1
  22. package/dist/src/pipeline/steps/generate-test.test.js.map +1 -1
  23. package/dist/src/pipeline/steps/lint-fix-loop.d.ts +4 -0
  24. package/dist/src/pipeline/steps/lint-fix-loop.d.ts.map +1 -0
  25. package/dist/src/pipeline/steps/lint-fix-loop.js +46 -0
  26. package/dist/src/pipeline/steps/lint-fix-loop.js.map +1 -0
  27. package/dist/src/pipeline/steps/lint-fix-loop.test.d.ts +2 -0
  28. package/dist/src/pipeline/steps/lint-fix-loop.test.d.ts.map +1 -0
  29. package/dist/src/pipeline/steps/lint-fix-loop.test.js +123 -0
  30. package/dist/src/pipeline/steps/lint-fix-loop.test.js.map +1 -0
  31. package/dist/src/pipeline/steps/story-fix-loop.d.ts +4 -0
  32. package/dist/src/pipeline/steps/story-fix-loop.d.ts.map +1 -0
  33. package/dist/src/pipeline/steps/story-fix-loop.js +35 -0
  34. package/dist/src/pipeline/steps/story-fix-loop.js.map +1 -0
  35. package/dist/src/pipeline/steps/story-fix-loop.test.d.ts +2 -0
  36. package/dist/src/pipeline/steps/story-fix-loop.test.d.ts.map +1 -0
  37. package/dist/src/pipeline/steps/story-fix-loop.test.js +98 -0
  38. package/dist/src/pipeline/steps/story-fix-loop.test.js.map +1 -0
  39. package/dist/src/pipeline/steps/storybook-test.d.ts +3 -0
  40. package/dist/src/pipeline/steps/storybook-test.d.ts.map +1 -0
  41. package/dist/src/pipeline/steps/storybook-test.js +22 -0
  42. package/dist/src/pipeline/steps/storybook-test.js.map +1 -0
  43. package/dist/src/pipeline/steps/storybook-test.test.d.ts +2 -0
  44. package/dist/src/pipeline/steps/storybook-test.test.d.ts.map +1 -0
  45. package/dist/src/pipeline/steps/storybook-test.test.js +70 -0
  46. package/dist/src/pipeline/steps/storybook-test.test.js.map +1 -0
  47. package/dist/src/pipeline/steps/test-fix-loop.d.ts +4 -0
  48. package/dist/src/pipeline/steps/test-fix-loop.d.ts.map +1 -0
  49. package/dist/src/pipeline/steps/test-fix-loop.js +45 -0
  50. package/dist/src/pipeline/steps/test-fix-loop.js.map +1 -0
  51. package/dist/src/pipeline/steps/test-fix-loop.test.d.ts +2 -0
  52. package/dist/src/pipeline/steps/test-fix-loop.test.d.ts.map +1 -0
  53. package/dist/src/pipeline/steps/test-fix-loop.test.js +172 -0
  54. package/dist/src/pipeline/steps/test-fix-loop.test.js.map +1 -0
  55. package/dist/src/pipeline/steps/type-fix-loop.d.ts +4 -0
  56. package/dist/src/pipeline/steps/type-fix-loop.d.ts.map +1 -0
  57. package/dist/src/pipeline/steps/type-fix-loop.js +44 -0
  58. package/dist/src/pipeline/steps/type-fix-loop.js.map +1 -0
  59. package/dist/src/pipeline/steps/type-fix-loop.test.d.ts +2 -0
  60. package/dist/src/pipeline/steps/type-fix-loop.test.d.ts.map +1 -0
  61. package/dist/src/pipeline/steps/type-fix-loop.test.js +116 -0
  62. package/dist/src/pipeline/steps/type-fix-loop.test.js.map +1 -0
  63. package/dist/src/pipeline/steps/visual-test.d.ts +3 -0
  64. package/dist/src/pipeline/steps/visual-test.d.ts.map +1 -0
  65. package/dist/src/pipeline/steps/visual-test.js +4 -0
  66. package/dist/src/pipeline/steps/visual-test.js.map +1 -0
  67. package/dist/src/pipeline/steps/visual-test.test.d.ts +2 -0
  68. package/dist/src/pipeline/steps/visual-test.test.d.ts.map +1 -0
  69. package/dist/src/pipeline/steps/visual-test.test.js +9 -0
  70. package/dist/src/pipeline/steps/visual-test.test.js.map +1 -0
  71. package/dist/tsconfig.tsbuildinfo +1 -1
  72. package/package.json +3 -3
  73. package/src/commands/implement-component.test.ts +47 -57
  74. package/src/commands/implement-component.ts +51 -14
  75. package/src/pipeline/run-pipeline.test.ts +137 -32
  76. package/src/pipeline/run-pipeline.ts +74 -22
  77. package/src/pipeline/steps/generate-component.test.ts +5 -1
  78. package/src/pipeline/steps/generate-story.test.ts +5 -1
  79. package/src/pipeline/steps/generate-test.test.ts +5 -1
  80. package/src/pipeline/steps/lint-fix-loop.test.ts +159 -0
  81. package/src/pipeline/steps/lint-fix-loop.ts +60 -0
  82. package/src/pipeline/steps/story-fix-loop.test.ts +127 -0
  83. package/src/pipeline/steps/story-fix-loop.ts +48 -0
  84. package/src/pipeline/steps/storybook-test.test.ts +86 -0
  85. package/src/pipeline/steps/storybook-test.ts +27 -0
  86. package/src/pipeline/steps/test-fix-loop.test.ts +205 -0
  87. package/src/pipeline/steps/test-fix-loop.ts +57 -0
  88. package/src/pipeline/steps/type-fix-loop.test.ts +149 -0
  89. package/src/pipeline/steps/type-fix-loop.ts +56 -0
  90. package/src/pipeline/steps/visual-test.test.ts +10 -0
  91. package/src/pipeline/steps/visual-test.ts +5 -0
  92. package/dist/src/pipeline/steps/fix-from-feedback.d.ts +0 -4
  93. package/dist/src/pipeline/steps/fix-from-feedback.d.ts.map +0 -1
  94. package/dist/src/pipeline/steps/fix-from-feedback.js +0 -94
  95. package/dist/src/pipeline/steps/fix-from-feedback.js.map +0 -1
  96. package/src/pipeline/steps/fix-from-feedback.ts +0 -105
package/package.json CHANGED
@@ -6,13 +6,13 @@
6
6
  "dependencies": {
7
7
  "ai": "^6.0.0",
8
8
  "debug": "^4.4.1",
9
- "@auto-engineer/message-bus": "1.110.2",
10
- "@auto-engineer/model-factory": "1.110.2"
9
+ "@auto-engineer/message-bus": "1.110.3",
10
+ "@auto-engineer/model-factory": "1.110.3"
11
11
  },
12
12
  "devDependencies": {
13
13
  "vitest": "^3.2.1"
14
14
  },
15
- "version": "1.110.2",
15
+ "version": "1.110.3",
16
16
  "publishConfig": {
17
17
  "access": "public"
18
18
  },
@@ -58,6 +58,19 @@ function makeCommand(overrides: Record<string, unknown> = {}) {
58
58
  };
59
59
  }
60
60
 
61
+ function mockPipelineResult(overrides: Record<string, unknown> = {}) {
62
+ return {
63
+ success: true,
64
+ llmCalls: 4,
65
+ fixIterations: 0,
66
+ typeFixIterations: 0,
67
+ testFixIterations: 0,
68
+ lintFixIterations: 0,
69
+ storyFixIterations: 0,
70
+ ...overrides,
71
+ };
72
+ }
73
+
61
74
  describe('implement-component', () => {
62
75
  afterEach(() => {
63
76
  vi.clearAllMocks();
@@ -66,10 +79,7 @@ describe('implement-component', () => {
66
79
  describe('handleImplementComponent', () => {
67
80
  it('runs pipeline and returns ComponentImplemented on success', async () => {
68
81
  vi.mocked(existsSync).mockReturnValue(false);
69
- vi.mocked(runPipeline).mockResolvedValue({
70
- success: true,
71
- llmCalls: 4,
72
- });
82
+ vi.mocked(runPipeline).mockResolvedValue(mockPipelineResult());
73
83
 
74
84
  const result = await handleImplementComponent(makeCommand());
75
85
 
@@ -88,8 +98,11 @@ describe('implement-component', () => {
88
98
  targetDir: '/project/client',
89
99
  job: makeCommand().data.job,
90
100
  llmCalls: expect.any(Number),
91
- attemptNumber: 0,
92
- intermediate: true,
101
+ fixIterations: 0,
102
+ typeFixIterations: 0,
103
+ testFixIterations: 0,
104
+ lintFixIterations: 0,
105
+ storyFixIterations: 0,
93
106
  },
94
107
  timestamp: expect.any(Date),
95
108
  requestId: 'req-1',
@@ -101,11 +114,13 @@ describe('implement-component', () => {
101
114
 
102
115
  it('returns ComponentImplementationFailed when pipeline fails', async () => {
103
116
  vi.mocked(existsSync).mockReturnValue(false);
104
- vi.mocked(runPipeline).mockResolvedValue({
105
- success: false,
106
- error: 'Step "Type Fix Loop" failed: errors remain',
107
- llmCalls: 5,
108
- });
117
+ vi.mocked(runPipeline).mockResolvedValue(
118
+ mockPipelineResult({
119
+ success: false,
120
+ error: 'Step "Type Fix Loop" failed: errors remain',
121
+ llmCalls: 5,
122
+ }),
123
+ );
109
124
 
110
125
  const result = await handleImplementComponent(makeCommand());
111
126
 
@@ -124,10 +139,7 @@ describe('implement-component', () => {
124
139
  it('reads existing component when modifying', async () => {
125
140
  vi.mocked(existsSync).mockReturnValue(true);
126
141
  vi.mocked(readFile).mockResolvedValue('existing component code' as never);
127
- vi.mocked(runPipeline).mockResolvedValue({
128
- success: true,
129
- llmCalls: 3,
130
- });
142
+ vi.mocked(runPipeline).mockResolvedValue(mockPipelineResult({ llmCalls: 3 }));
131
143
 
132
144
  const command = makeCommand({
133
145
  job: {
@@ -180,10 +192,7 @@ describe('implement-component', () => {
180
192
 
181
193
  it('handles payload with undefined spec delta fields without crashing', async () => {
182
194
  vi.mocked(existsSync).mockReturnValue(false);
183
- vi.mocked(runPipeline).mockResolvedValue({
184
- success: true,
185
- llmCalls: 4,
186
- });
195
+ vi.mocked(runPipeline).mockResolvedValue(mockPipelineResult());
187
196
 
188
197
  const command = {
189
198
  type: 'ImplementComponent' as const,
@@ -217,10 +226,7 @@ describe('implement-component', () => {
217
226
 
218
227
  it('defaults targetDir to ./client when not provided', async () => {
219
228
  vi.mocked(existsSync).mockReturnValue(false);
220
- vi.mocked(runPipeline).mockResolvedValue({
221
- success: true,
222
- llmCalls: 2,
223
- });
229
+ vi.mocked(runPipeline).mockResolvedValue(mockPipelineResult({ llmCalls: 2 }));
224
230
 
225
231
  const command = {
226
232
  type: 'ImplementComponent' as const,
@@ -258,10 +264,7 @@ describe('implement-component', () => {
258
264
 
259
265
  it('derives component path from componentId when files.create is empty string', async () => {
260
266
  vi.mocked(existsSync).mockReturnValue(false);
261
- vi.mocked(runPipeline).mockResolvedValue({
262
- success: true,
263
- llmCalls: 4,
264
- });
267
+ vi.mocked(runPipeline).mockResolvedValue(mockPipelineResult());
265
268
 
266
269
  const command = makeCommand({
267
270
  job: {
@@ -297,8 +300,11 @@ describe('implement-component', () => {
297
300
  targetDir: '/project/client',
298
301
  job: command.data.job,
299
302
  llmCalls: expect.any(Number),
300
- attemptNumber: 0,
301
- intermediate: true,
303
+ fixIterations: 0,
304
+ typeFixIterations: 0,
305
+ testFixIterations: 0,
306
+ lintFixIterations: 0,
307
+ storyFixIterations: 0,
302
308
  },
303
309
  timestamp: expect.any(Date),
304
310
  requestId: 'req-1',
@@ -308,10 +314,7 @@ describe('implement-component', () => {
308
314
 
309
315
  it('passes correct context to pipeline', async () => {
310
316
  vi.mocked(existsSync).mockReturnValue(false);
311
- vi.mocked(runPipeline).mockResolvedValue({
312
- success: true,
313
- llmCalls: 2,
314
- });
317
+ vi.mocked(runPipeline).mockResolvedValue(mockPipelineResult({ llmCalls: 2 }));
315
318
 
316
319
  await handleImplementComponent(makeCommand());
317
320
 
@@ -331,6 +334,11 @@ describe('implement-component', () => {
331
334
  },
332
335
  isModify: false,
333
336
  llmCalls: 0,
337
+ fixIterations: 0,
338
+ typeFixIterations: 0,
339
+ testFixIterations: 0,
340
+ lintFixIterations: 0,
341
+ storyFixIterations: 0,
334
342
  }),
335
343
  );
336
344
  });
@@ -344,10 +352,7 @@ describe('implement-component', () => {
344
352
 
345
353
  it('handle delegates to handleImplementComponent', async () => {
346
354
  vi.mocked(existsSync).mockReturnValue(false);
347
- vi.mocked(runPipeline).mockResolvedValue({
348
- success: true,
349
- llmCalls: 0,
350
- });
355
+ vi.mocked(runPipeline).mockResolvedValue(mockPipelineResult({ llmCalls: 0 }));
351
356
 
352
357
  const result = await commandHandler.handle(makeCommand());
353
358
 
@@ -358,10 +363,7 @@ describe('implement-component', () => {
358
363
  describe('filePathToImportAlias edge cases', () => {
359
364
  it('handles paths starting with ./', async () => {
360
365
  vi.mocked(existsSync).mockReturnValue(false);
361
- vi.mocked(runPipeline).mockResolvedValue({
362
- success: true,
363
- llmCalls: 0,
364
- });
366
+ vi.mocked(runPipeline).mockResolvedValue(mockPipelineResult({ llmCalls: 0 }));
365
367
 
366
368
  const command = makeCommand({
367
369
  job: {
@@ -396,10 +398,7 @@ describe('implement-component', () => {
396
398
  it('reads existing test when modifying and test file exists', async () => {
397
399
  vi.mocked(existsSync).mockReturnValue(true);
398
400
  vi.mocked(readFile).mockResolvedValue('existing code' as never);
399
- vi.mocked(runPipeline).mockResolvedValue({
400
- success: true,
401
- llmCalls: 1,
402
- });
401
+ vi.mocked(runPipeline).mockResolvedValue(mockPipelineResult({ llmCalls: 1 }));
403
402
 
404
403
  const command = makeCommand({
405
404
  job: {
@@ -433,10 +432,7 @@ describe('implement-component', () => {
433
432
  it('sets existingTest to undefined when test file does not exist in modify mode', async () => {
434
433
  vi.mocked(existsSync).mockReturnValueOnce(true).mockReturnValueOnce(false);
435
434
  vi.mocked(readFile).mockResolvedValue('existing component' as never);
436
- vi.mocked(runPipeline).mockResolvedValue({
437
- success: true,
438
- llmCalls: 1,
439
- });
435
+ vi.mocked(runPipeline).mockResolvedValue(mockPipelineResult({ llmCalls: 1 }));
440
436
 
441
437
  const command = makeCommand({
442
438
  job: {
@@ -469,10 +465,7 @@ describe('implement-component', () => {
469
465
 
470
466
  it('uses fallback error message when pipeline result has no error string', async () => {
471
467
  vi.mocked(existsSync).mockReturnValue(false);
472
- vi.mocked(runPipeline).mockResolvedValue({
473
- success: false,
474
- llmCalls: 0,
475
- });
468
+ vi.mocked(runPipeline).mockResolvedValue(mockPipelineResult({ success: false, llmCalls: 0 }));
476
469
 
477
470
  const result = await handleImplementComponent(makeCommand());
478
471
 
@@ -508,10 +501,7 @@ describe('implement-component', () => {
508
501
 
509
502
  it('handles empty files payload gracefully', async () => {
510
503
  vi.mocked(existsSync).mockReturnValue(false);
511
- vi.mocked(runPipeline).mockResolvedValue({
512
- success: true,
513
- llmCalls: 0,
514
- });
504
+ vi.mocked(runPipeline).mockResolvedValue(mockPipelineResult({ llmCalls: 0 }));
515
505
 
516
506
  const command = makeCommand({
517
507
  job: {
@@ -53,7 +53,6 @@ export type ImplementComponentCommand = Command<
53
53
  {
54
54
  targetDir: string;
55
55
  job: ComponentJob;
56
- context?: { previousOutputs?: string; attemptNumber?: number };
57
56
  }
58
57
  >;
59
58
 
@@ -68,8 +67,11 @@ export type ComponentImplementedEvent = Event<
68
67
  targetDir: string;
69
68
  job: ComponentJob;
70
69
  llmCalls: number;
71
- attemptNumber: number;
72
- intermediate?: boolean;
70
+ fixIterations: number;
71
+ typeFixIterations: number;
72
+ testFixIterations: number;
73
+ lintFixIterations: number;
74
+ storyFixIterations: number;
73
75
  }
74
76
  >;
75
77
 
@@ -138,13 +140,37 @@ function buildPipelineConfig(): PipelineConfig {
138
140
  process.env.IMPL_MODEL ??
139
141
  process.env.CUSTOM_PROVIDER_DEFAULT_MODEL ??
140
142
  '',
141
- fixer:
142
- process.env.STEP_FIXER_MODEL ??
143
+ typeFixer:
144
+ process.env.STEP_TYPE_FIXER_MODEL ??
145
+ process.env.IMPL_FIXER_MODEL ??
146
+ process.env.IMPL_MODEL ??
147
+ process.env.CUSTOM_PROVIDER_DEFAULT_MODEL ??
148
+ '',
149
+ testFixer:
150
+ process.env.STEP_TEST_FIXER_MODEL ??
151
+ process.env.IMPL_FIXER_MODEL ??
152
+ process.env.IMPL_MODEL ??
153
+ process.env.CUSTOM_PROVIDER_DEFAULT_MODEL ??
154
+ '',
155
+ lintFixer:
156
+ process.env.STEP_LINT_FIXER_MODEL ??
157
+ process.env.IMPL_FIXER_MODEL ??
158
+ process.env.IMPL_MODEL ??
159
+ process.env.CUSTOM_PROVIDER_DEFAULT_MODEL ??
160
+ '',
161
+ storyFixer:
162
+ process.env.STEP_STORY_FIXER_MODEL ??
143
163
  process.env.IMPL_FIXER_MODEL ??
144
164
  process.env.IMPL_MODEL ??
145
165
  process.env.CUSTOM_PROVIDER_DEFAULT_MODEL ??
146
166
  '',
147
167
  },
168
+ maxTypeFixIterations: Number(process.env.MAX_TYPE_FIX_ITERATIONS ?? 3),
169
+ maxTestFixIterations: Number(process.env.MAX_TEST_FIX_ITERATIONS ?? 3),
170
+ maxLintFixIterations: Number(process.env.MAX_LINT_FIX_ITERATIONS ?? 2),
171
+ maxStoryFixIterations: Number(process.env.MAX_STORY_FIX_ITERATIONS ?? 2),
172
+ enableStorybookTest: process.env.ENABLE_STORYBOOK_TEST === 'true',
173
+ enableVisualTest: process.env.ENABLE_VISUAL_TEST === 'true',
148
174
  };
149
175
  }
150
176
 
@@ -154,7 +180,10 @@ function createPipelineModels(): PipelineModels {
154
180
  generateTest: model,
155
181
  generateComponent: model,
156
182
  generateStory: model,
157
- fixer: model,
183
+ typeFixer: model,
184
+ testFixer: model,
185
+ lintFixer: model,
186
+ storyFixer: model,
158
187
  };
159
188
  }
160
189
 
@@ -191,9 +220,6 @@ export async function handleImplementComponent(
191
220
 
192
221
  await mkdir(path.dirname(testPath), { recursive: true });
193
222
 
194
- const attemptNumber = command.data.context?.attemptNumber ?? 0;
195
- const errorFeedback = command.data.context?.previousOutputs;
196
-
197
223
  const ctx: PipelineContext = {
198
224
  componentName,
199
225
  componentPath,
@@ -209,8 +235,11 @@ export async function handleImplementComponent(
209
235
  existingTest,
210
236
  isModify,
211
237
  llmCalls: 0,
212
- errorFeedback,
213
- attemptNumber,
238
+ fixIterations: 0,
239
+ typeFixIterations: 0,
240
+ testFixIterations: 0,
241
+ lintFixIterations: 0,
242
+ storyFixIterations: 0,
214
243
  };
215
244
 
216
245
  const config = buildPipelineConfig();
@@ -231,7 +260,12 @@ export async function handleImplementComponent(
231
260
  };
232
261
  }
233
262
 
234
- debug('Pipeline succeeded for %s (LLM calls: %d)', componentName, result.llmCalls);
263
+ debug(
264
+ 'Pipeline succeeded for %s (LLM calls: %d, fix iterations: %d)',
265
+ componentName,
266
+ result.llmCalls,
267
+ result.fixIterations,
268
+ );
235
269
 
236
270
  return {
237
271
  type: 'ComponentImplemented',
@@ -244,8 +278,11 @@ export async function handleImplementComponent(
244
278
  targetDir,
245
279
  job,
246
280
  llmCalls: result.llmCalls,
247
- attemptNumber,
248
- intermediate: true,
281
+ fixIterations: result.fixIterations,
282
+ typeFixIterations: result.typeFixIterations,
283
+ testFixIterations: result.testFixIterations,
284
+ lintFixIterations: result.lintFixIterations,
285
+ storyFixIterations: result.storyFixIterations,
249
286
  },
250
287
  timestamp: new Date(),
251
288
  requestId: command.requestId,
@@ -7,11 +7,26 @@ vi.mock('./steps/generate-test', () => ({
7
7
  vi.mock('./steps/generate-component', () => ({
8
8
  generateComponentStep: vi.fn(async () => ({ success: true })),
9
9
  }));
10
+ vi.mock('./steps/type-fix-loop', () => ({
11
+ typeFixLoop: vi.fn(async () => ({ success: true })),
12
+ }));
13
+ vi.mock('./steps/test-fix-loop', () => ({
14
+ testFixLoop: vi.fn(async () => ({ success: true })),
15
+ }));
16
+ vi.mock('./steps/lint-fix-loop', () => ({
17
+ lintFixLoop: vi.fn(async () => ({ success: true })),
18
+ }));
10
19
  vi.mock('./steps/generate-story', () => ({
11
20
  generateStoryStep: vi.fn(async () => ({ success: true })),
12
21
  }));
13
- vi.mock('./steps/fix-from-feedback', () => ({
14
- fixFromFeedbackStep: vi.fn(async () => ({ success: true })),
22
+ vi.mock('./steps/story-fix-loop', () => ({
23
+ storyFixLoop: vi.fn(async () => ({ success: true })),
24
+ }));
25
+ vi.mock('./steps/storybook-test', () => ({
26
+ storybookTestStep: vi.fn(async () => ({ success: true })),
27
+ }));
28
+ vi.mock('./steps/visual-test', () => ({
29
+ visualTestStep: vi.fn(async () => ({ success: true })),
15
30
  }));
16
31
 
17
32
  import {
@@ -22,10 +37,15 @@ import {
22
37
  type PipelineStep,
23
38
  runPipeline,
24
39
  } from './run-pipeline';
25
- import { fixFromFeedbackStep } from './steps/fix-from-feedback';
26
40
  import { generateComponentStep } from './steps/generate-component';
27
41
  import { generateStoryStep } from './steps/generate-story';
28
42
  import { generateTestStep } from './steps/generate-test';
43
+ import { lintFixLoop } from './steps/lint-fix-loop';
44
+ import { storyFixLoop } from './steps/story-fix-loop';
45
+ import { storybookTestStep } from './steps/storybook-test';
46
+ import { testFixLoop } from './steps/test-fix-loop';
47
+ import { typeFixLoop } from './steps/type-fix-loop';
48
+ import { visualTestStep } from './steps/visual-test';
29
49
 
30
50
  afterEach(() => {
31
51
  vi.clearAllMocks();
@@ -44,7 +64,11 @@ function makeCtx(overrides: Partial<PipelineContext> = {}): PipelineContext {
44
64
  composes: [],
45
65
  isModify: false,
46
66
  llmCalls: 0,
47
- attemptNumber: 0,
67
+ fixIterations: 0,
68
+ typeFixIterations: 0,
69
+ testFixIterations: 0,
70
+ lintFixIterations: 0,
71
+ storyFixIterations: 0,
48
72
  ...overrides,
49
73
  };
50
74
  }
@@ -85,6 +109,11 @@ describe('runPipeline', () => {
85
109
  expect(result).toEqual({
86
110
  success: true,
87
111
  llmCalls: 0,
112
+ fixIterations: 0,
113
+ typeFixIterations: 0,
114
+ testFixIterations: 0,
115
+ lintFixIterations: 0,
116
+ storyFixIterations: 0,
88
117
  });
89
118
  expect(executionOrder).toEqual(['A', 'B', 'C']);
90
119
  });
@@ -115,6 +144,11 @@ describe('runPipeline', () => {
115
144
  success: false,
116
145
  error: 'Step "Step B" failed: something broke',
117
146
  llmCalls: 0,
147
+ fixIterations: 0,
148
+ typeFixIterations: 0,
149
+ testFixIterations: 0,
150
+ lintFixIterations: 0,
151
+ storyFixIterations: 0,
118
152
  });
119
153
  expect(executionOrder).toEqual(['A']);
120
154
  });
@@ -125,24 +159,34 @@ describe('runPipeline', () => {
125
159
  expect(result).toEqual({
126
160
  success: true,
127
161
  llmCalls: 0,
162
+ fixIterations: 0,
163
+ typeFixIterations: 0,
164
+ testFixIterations: 0,
165
+ lintFixIterations: 0,
166
+ storyFixIterations: 0,
128
167
  });
129
168
  });
130
169
 
131
- it('reports accumulated llmCalls from context', async () => {
170
+ it('reports accumulated llmCalls and fixIterations from context', async () => {
132
171
  const steps: PipelineStep[] = [{ name: 'Step A', run: async () => ({ success: true }) }];
133
- const ctx = makeCtx({ llmCalls: 5 });
172
+ const ctx = makeCtx({ llmCalls: 5, fixIterations: 3 });
134
173
 
135
174
  const result = await runPipeline(steps, ctx);
136
175
 
137
176
  expect(result).toEqual({
138
177
  success: true,
139
178
  llmCalls: 5,
179
+ fixIterations: 3,
180
+ typeFixIterations: 0,
181
+ testFixIterations: 0,
182
+ lintFixIterations: 0,
183
+ storyFixIterations: 0,
140
184
  });
141
185
  });
142
186
 
143
187
  it('reports context metrics on failure', async () => {
144
188
  const steps: PipelineStep[] = [failStep('Broken', 'oops')];
145
- const ctx = makeCtx({ llmCalls: 2 });
189
+ const ctx = makeCtx({ llmCalls: 2, fixIterations: 1 });
146
190
 
147
191
  const result = await runPipeline(steps, ctx);
148
192
 
@@ -150,6 +194,11 @@ describe('runPipeline', () => {
150
194
  success: false,
151
195
  error: 'Step "Broken" failed: oops',
152
196
  llmCalls: 2,
197
+ fixIterations: 1,
198
+ typeFixIterations: 0,
199
+ testFixIterations: 0,
200
+ lintFixIterations: 0,
201
+ storyFixIterations: 0,
153
202
  });
154
203
  });
155
204
  });
@@ -161,38 +210,102 @@ function makeModels(): PipelineModels {
161
210
  generateTest: fakeModel,
162
211
  generateComponent: fakeModel,
163
212
  generateStory: fakeModel,
164
- fixer: fakeModel,
213
+ typeFixer: fakeModel,
214
+ testFixer: fakeModel,
215
+ lintFixer: fakeModel,
216
+ storyFixer: fakeModel,
165
217
  };
166
218
  }
167
219
 
168
- function makeConfig(): PipelineConfig {
220
+ function makeConfig(overrides: Partial<PipelineConfig> = {}): PipelineConfig {
169
221
  return {
170
222
  models: {
171
223
  generateTest: '',
172
224
  generateComponent: '',
173
225
  generateStory: '',
174
- fixer: '',
226
+ typeFixer: '',
227
+ testFixer: '',
228
+ lintFixer: '',
229
+ storyFixer: '',
175
230
  },
231
+ maxTypeFixIterations: 3,
232
+ maxTestFixIterations: 3,
233
+ maxLintFixIterations: 2,
234
+ maxStoryFixIterations: 2,
235
+ enableStorybookTest: false,
236
+ enableVisualTest: false,
237
+ ...overrides,
176
238
  };
177
239
  }
178
240
 
179
241
  describe('buildPipelineSteps', () => {
180
- it('returns 3 core steps for first attempt', () => {
242
+ it('returns 7 core steps by default', () => {
181
243
  const steps = buildPipelineSteps(makeModels(), makeConfig(), makeCtx());
182
244
 
183
- expect(steps.map((s) => s.name)).toEqual(['Generate Test', 'Generate Component', 'Generate Story']);
245
+ expect(steps.map((s) => s.name)).toEqual([
246
+ 'Generate Test',
247
+ 'Generate Component',
248
+ 'Type Fix Loop',
249
+ 'Test Fix Loop',
250
+ 'Lint Fix Loop',
251
+ 'Generate Story',
252
+ 'Story Fix Loop',
253
+ ]);
184
254
  });
185
255
 
186
- it('returns fix + story steps for retry attempt', () => {
187
- const ctx = makeCtx({ attemptNumber: 1, errorFeedback: 'Test failed: expected X to equal Y' });
188
- const steps = buildPipelineSteps(makeModels(), makeConfig(), ctx);
256
+ it('adds Storybook Test step when enabled', () => {
257
+ const steps = buildPipelineSteps(makeModels(), makeConfig({ enableStorybookTest: true }), makeCtx());
258
+
259
+ expect(steps.map((s) => s.name)).toEqual([
260
+ 'Generate Test',
261
+ 'Generate Component',
262
+ 'Type Fix Loop',
263
+ 'Test Fix Loop',
264
+ 'Lint Fix Loop',
265
+ 'Generate Story',
266
+ 'Story Fix Loop',
267
+ 'Storybook Test',
268
+ ]);
269
+ });
189
270
 
190
- expect(steps.map((s) => s.name)).toEqual(['Fix From Feedback', 'Generate Story']);
271
+ it('adds Visual Test step when enabled', () => {
272
+ const steps = buildPipelineSteps(makeModels(), makeConfig({ enableVisualTest: true }), makeCtx());
273
+
274
+ expect(steps.map((s) => s.name)).toEqual([
275
+ 'Generate Test',
276
+ 'Generate Component',
277
+ 'Type Fix Loop',
278
+ 'Test Fix Loop',
279
+ 'Lint Fix Loop',
280
+ 'Generate Story',
281
+ 'Story Fix Loop',
282
+ 'Visual Test',
283
+ ]);
191
284
  });
192
285
 
193
- it('wires each step to its corresponding function on first attempt', async () => {
286
+ it('adds both optional steps when both enabled', () => {
287
+ const steps = buildPipelineSteps(
288
+ makeModels(),
289
+ makeConfig({ enableStorybookTest: true, enableVisualTest: true }),
290
+ makeCtx(),
291
+ );
292
+
293
+ expect(steps.map((s) => s.name)).toEqual([
294
+ 'Generate Test',
295
+ 'Generate Component',
296
+ 'Type Fix Loop',
297
+ 'Test Fix Loop',
298
+ 'Lint Fix Loop',
299
+ 'Generate Story',
300
+ 'Story Fix Loop',
301
+ 'Storybook Test',
302
+ 'Visual Test',
303
+ ]);
304
+ });
305
+
306
+ it('wires each step to its corresponding function', async () => {
194
307
  const models = makeModels();
195
- const config = makeConfig();
308
+ const config = makeConfig({ enableStorybookTest: true, enableVisualTest: true });
196
309
  const ctx = makeCtx();
197
310
  const steps = buildPipelineSteps(models, config, ctx);
198
311
 
@@ -202,20 +315,12 @@ describe('buildPipelineSteps', () => {
202
315
 
203
316
  expect(generateTestStep).toHaveBeenCalledWith(models.generateTest, ctx);
204
317
  expect(generateComponentStep).toHaveBeenCalledWith(models.generateComponent, ctx);
318
+ expect(typeFixLoop).toHaveBeenCalledWith(models.typeFixer, ctx, 3);
319
+ expect(testFixLoop).toHaveBeenCalledWith(models.testFixer, ctx, 3);
320
+ expect(lintFixLoop).toHaveBeenCalledWith(models.lintFixer, ctx, 2);
205
321
  expect(generateStoryStep).toHaveBeenCalledWith(models.generateStory, ctx);
206
- });
207
-
208
- it('wires fix step on retry attempt', async () => {
209
- const models = makeModels();
210
- const config = makeConfig();
211
- const ctx = makeCtx({ attemptNumber: 1, errorFeedback: 'Some error' });
212
- const steps = buildPipelineSteps(models, config, ctx);
213
-
214
- for (const step of steps) {
215
- await step.run();
216
- }
217
-
218
- expect(fixFromFeedbackStep).toHaveBeenCalledWith(models.fixer, ctx);
219
- expect(generateStoryStep).toHaveBeenCalledWith(models.generateStory, ctx);
322
+ expect(storyFixLoop).toHaveBeenCalledWith(models.storyFixer, ctx, 2);
323
+ expect(storybookTestStep).toHaveBeenCalledWith(ctx, true);
324
+ expect(visualTestStep).toHaveBeenCalled();
220
325
  });
221
326
  });