@auto-engineer/component-implementor-react 1.110.2 → 1.110.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/.turbo/turbo-test.log +6 -6
- package/.turbo/turbo-type-check.log +1 -1
- package/CHANGELOG.md +32 -0
- package/dist/src/commands/implement-component.d.ts +5 -6
- package/dist/src/commands/implement-component.d.ts.map +1 -1
- package/dist/src/commands/implement-component.js +37 -9
- package/dist/src/commands/implement-component.js.map +1 -1
- package/dist/src/commands/implement-component.test.js +41 -54
- package/dist/src/commands/implement-component.test.js.map +1 -1
- package/dist/src/pipeline/run-pipeline.d.ts +25 -5
- package/dist/src/pipeline/run-pipeline.d.ts.map +1 -1
- package/dist/src/pipeline/run-pipeline.js +47 -17
- package/dist/src/pipeline/run-pipeline.js.map +1 -1
- package/dist/src/pipeline/run-pipeline.test.js +129 -29
- package/dist/src/pipeline/run-pipeline.test.js.map +1 -1
- package/dist/src/pipeline/steps/generate-component.test.js +5 -1
- package/dist/src/pipeline/steps/generate-component.test.js.map +1 -1
- package/dist/src/pipeline/steps/generate-story.test.js +5 -1
- package/dist/src/pipeline/steps/generate-story.test.js.map +1 -1
- package/dist/src/pipeline/steps/generate-test.test.js +5 -1
- package/dist/src/pipeline/steps/generate-test.test.js.map +1 -1
- package/dist/src/pipeline/steps/lint-fix-loop.d.ts +4 -0
- package/dist/src/pipeline/steps/lint-fix-loop.d.ts.map +1 -0
- package/dist/src/pipeline/steps/lint-fix-loop.js +46 -0
- package/dist/src/pipeline/steps/lint-fix-loop.js.map +1 -0
- package/dist/src/pipeline/steps/lint-fix-loop.test.d.ts +2 -0
- package/dist/src/pipeline/steps/lint-fix-loop.test.d.ts.map +1 -0
- package/dist/src/pipeline/steps/lint-fix-loop.test.js +123 -0
- package/dist/src/pipeline/steps/lint-fix-loop.test.js.map +1 -0
- package/dist/src/pipeline/steps/story-fix-loop.d.ts +4 -0
- package/dist/src/pipeline/steps/story-fix-loop.d.ts.map +1 -0
- package/dist/src/pipeline/steps/story-fix-loop.js +35 -0
- package/dist/src/pipeline/steps/story-fix-loop.js.map +1 -0
- package/dist/src/pipeline/steps/story-fix-loop.test.d.ts +2 -0
- package/dist/src/pipeline/steps/story-fix-loop.test.d.ts.map +1 -0
- package/dist/src/pipeline/steps/story-fix-loop.test.js +98 -0
- package/dist/src/pipeline/steps/story-fix-loop.test.js.map +1 -0
- package/dist/src/pipeline/steps/storybook-test.d.ts +3 -0
- package/dist/src/pipeline/steps/storybook-test.d.ts.map +1 -0
- package/dist/src/pipeline/steps/storybook-test.js +22 -0
- package/dist/src/pipeline/steps/storybook-test.js.map +1 -0
- package/dist/src/pipeline/steps/storybook-test.test.d.ts +2 -0
- package/dist/src/pipeline/steps/storybook-test.test.d.ts.map +1 -0
- package/dist/src/pipeline/steps/storybook-test.test.js +70 -0
- package/dist/src/pipeline/steps/storybook-test.test.js.map +1 -0
- package/dist/src/pipeline/steps/test-fix-loop.d.ts +4 -0
- package/dist/src/pipeline/steps/test-fix-loop.d.ts.map +1 -0
- package/dist/src/pipeline/steps/test-fix-loop.js +45 -0
- package/dist/src/pipeline/steps/test-fix-loop.js.map +1 -0
- package/dist/src/pipeline/steps/test-fix-loop.test.d.ts +2 -0
- package/dist/src/pipeline/steps/test-fix-loop.test.d.ts.map +1 -0
- package/dist/src/pipeline/steps/test-fix-loop.test.js +172 -0
- package/dist/src/pipeline/steps/test-fix-loop.test.js.map +1 -0
- package/dist/src/pipeline/steps/type-fix-loop.d.ts +4 -0
- package/dist/src/pipeline/steps/type-fix-loop.d.ts.map +1 -0
- package/dist/src/pipeline/steps/type-fix-loop.js +44 -0
- package/dist/src/pipeline/steps/type-fix-loop.js.map +1 -0
- package/dist/src/pipeline/steps/type-fix-loop.test.d.ts +2 -0
- package/dist/src/pipeline/steps/type-fix-loop.test.d.ts.map +1 -0
- package/dist/src/pipeline/steps/type-fix-loop.test.js +116 -0
- package/dist/src/pipeline/steps/type-fix-loop.test.js.map +1 -0
- package/dist/src/pipeline/steps/visual-test.d.ts +3 -0
- package/dist/src/pipeline/steps/visual-test.d.ts.map +1 -0
- package/dist/src/pipeline/steps/visual-test.js +4 -0
- package/dist/src/pipeline/steps/visual-test.js.map +1 -0
- package/dist/src/pipeline/steps/visual-test.test.d.ts +2 -0
- package/dist/src/pipeline/steps/visual-test.test.d.ts.map +1 -0
- package/dist/src/pipeline/steps/visual-test.test.js +9 -0
- package/dist/src/pipeline/steps/visual-test.test.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
- package/src/commands/implement-component.test.ts +47 -57
- package/src/commands/implement-component.ts +51 -14
- package/src/pipeline/run-pipeline.test.ts +137 -32
- package/src/pipeline/run-pipeline.ts +74 -22
- package/src/pipeline/steps/generate-component.test.ts +5 -1
- package/src/pipeline/steps/generate-story.test.ts +5 -1
- package/src/pipeline/steps/generate-test.test.ts +5 -1
- package/src/pipeline/steps/lint-fix-loop.test.ts +159 -0
- package/src/pipeline/steps/lint-fix-loop.ts +60 -0
- package/src/pipeline/steps/story-fix-loop.test.ts +127 -0
- package/src/pipeline/steps/story-fix-loop.ts +48 -0
- package/src/pipeline/steps/storybook-test.test.ts +86 -0
- package/src/pipeline/steps/storybook-test.ts +27 -0
- package/src/pipeline/steps/test-fix-loop.test.ts +205 -0
- package/src/pipeline/steps/test-fix-loop.ts +57 -0
- package/src/pipeline/steps/type-fix-loop.test.ts +149 -0
- package/src/pipeline/steps/type-fix-loop.ts +56 -0
- package/src/pipeline/steps/visual-test.test.ts +10 -0
- package/src/pipeline/steps/visual-test.ts +5 -0
- package/dist/src/pipeline/steps/fix-from-feedback.d.ts +0 -4
- package/dist/src/pipeline/steps/fix-from-feedback.d.ts.map +0 -1
- package/dist/src/pipeline/steps/fix-from-feedback.js +0 -94
- package/dist/src/pipeline/steps/fix-from-feedback.js.map +0 -1
- package/src/pipeline/steps/fix-from-feedback.ts +0 -105
|
@@ -2,10 +2,15 @@ import type { LanguageModel } from 'ai';
|
|
|
2
2
|
import createDebug from 'debug';
|
|
3
3
|
import type { ScaffoldProp } from '../scaffold';
|
|
4
4
|
import type { SpecDeltas } from '../spec-contract';
|
|
5
|
-
import { fixFromFeedbackStep } from './steps/fix-from-feedback';
|
|
6
5
|
import { generateComponentStep } from './steps/generate-component';
|
|
7
6
|
import { generateStoryStep } from './steps/generate-story';
|
|
8
7
|
import { generateTestStep } from './steps/generate-test';
|
|
8
|
+
import { lintFixLoop } from './steps/lint-fix-loop';
|
|
9
|
+
import { storyFixLoop } from './steps/story-fix-loop';
|
|
10
|
+
import { storybookTestStep } from './steps/storybook-test';
|
|
11
|
+
import { testFixLoop } from './steps/test-fix-loop';
|
|
12
|
+
import { typeFixLoop } from './steps/type-fix-loop';
|
|
13
|
+
import { visualTestStep } from './steps/visual-test';
|
|
9
14
|
|
|
10
15
|
const debug = createDebug('auto:component-implementor-react:pipeline');
|
|
11
16
|
|
|
@@ -37,55 +42,60 @@ export type PipelineContext = {
|
|
|
37
42
|
componentCode?: string;
|
|
38
43
|
storyCode?: string;
|
|
39
44
|
llmCalls: number;
|
|
40
|
-
|
|
41
|
-
|
|
45
|
+
fixIterations: number;
|
|
46
|
+
typeFixIterations: number;
|
|
47
|
+
testFixIterations: number;
|
|
48
|
+
lintFixIterations: number;
|
|
49
|
+
storyFixIterations: number;
|
|
42
50
|
};
|
|
43
51
|
|
|
44
52
|
export type ModelConfig = {
|
|
45
53
|
generateTest: string;
|
|
46
54
|
generateComponent: string;
|
|
47
55
|
generateStory: string;
|
|
48
|
-
|
|
56
|
+
typeFixer: string;
|
|
57
|
+
testFixer: string;
|
|
58
|
+
lintFixer: string;
|
|
59
|
+
storyFixer: string;
|
|
49
60
|
};
|
|
50
61
|
|
|
51
62
|
export type PipelineConfig = {
|
|
52
63
|
models: ModelConfig;
|
|
64
|
+
maxTypeFixIterations: number;
|
|
65
|
+
maxTestFixIterations: number;
|
|
66
|
+
maxLintFixIterations: number;
|
|
67
|
+
maxStoryFixIterations: number;
|
|
68
|
+
enableStorybookTest: boolean;
|
|
69
|
+
enableVisualTest: boolean;
|
|
53
70
|
};
|
|
54
71
|
|
|
55
72
|
export type PipelineModels = {
|
|
56
73
|
generateTest: LanguageModel;
|
|
57
74
|
generateComponent: LanguageModel;
|
|
58
75
|
generateStory: LanguageModel;
|
|
59
|
-
|
|
76
|
+
typeFixer: LanguageModel;
|
|
77
|
+
testFixer: LanguageModel;
|
|
78
|
+
lintFixer: LanguageModel;
|
|
79
|
+
storyFixer: LanguageModel;
|
|
60
80
|
};
|
|
61
81
|
|
|
62
82
|
export type PipelineResult = {
|
|
63
83
|
success: boolean;
|
|
64
84
|
error?: string;
|
|
65
85
|
llmCalls: number;
|
|
86
|
+
fixIterations: number;
|
|
87
|
+
typeFixIterations: number;
|
|
88
|
+
testFixIterations: number;
|
|
89
|
+
lintFixIterations: number;
|
|
90
|
+
storyFixIterations: number;
|
|
66
91
|
};
|
|
67
92
|
|
|
68
93
|
export function buildPipelineSteps(
|
|
69
94
|
models: PipelineModels,
|
|
70
|
-
|
|
95
|
+
config: PipelineConfig,
|
|
71
96
|
ctx: PipelineContext,
|
|
72
97
|
): PipelineStep[] {
|
|
73
|
-
|
|
74
|
-
if (ctx.attemptNumber > 0 && ctx.errorFeedback) {
|
|
75
|
-
return [
|
|
76
|
-
{
|
|
77
|
-
name: 'Fix From Feedback',
|
|
78
|
-
run: () => fixFromFeedbackStep(models.fixer, ctx),
|
|
79
|
-
},
|
|
80
|
-
{
|
|
81
|
-
name: 'Generate Story',
|
|
82
|
-
run: () => generateStoryStep(models.generateStory, ctx),
|
|
83
|
-
},
|
|
84
|
-
];
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// First attempt: generate everything
|
|
88
|
-
return [
|
|
98
|
+
const steps: PipelineStep[] = [
|
|
89
99
|
{
|
|
90
100
|
name: 'Generate Test',
|
|
91
101
|
run: () => generateTestStep(models.generateTest, ctx),
|
|
@@ -94,11 +104,43 @@ export function buildPipelineSteps(
|
|
|
94
104
|
name: 'Generate Component',
|
|
95
105
|
run: () => generateComponentStep(models.generateComponent, ctx),
|
|
96
106
|
},
|
|
107
|
+
{
|
|
108
|
+
name: 'Type Fix Loop',
|
|
109
|
+
run: () => typeFixLoop(models.typeFixer, ctx, config.maxTypeFixIterations),
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
name: 'Test Fix Loop',
|
|
113
|
+
run: () => testFixLoop(models.testFixer, ctx, config.maxTestFixIterations),
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
name: 'Lint Fix Loop',
|
|
117
|
+
run: () => lintFixLoop(models.lintFixer, ctx, config.maxLintFixIterations),
|
|
118
|
+
},
|
|
97
119
|
{
|
|
98
120
|
name: 'Generate Story',
|
|
99
121
|
run: () => generateStoryStep(models.generateStory, ctx),
|
|
100
122
|
},
|
|
123
|
+
{
|
|
124
|
+
name: 'Story Fix Loop',
|
|
125
|
+
run: () => storyFixLoop(models.storyFixer, ctx, config.maxStoryFixIterations),
|
|
126
|
+
},
|
|
101
127
|
];
|
|
128
|
+
|
|
129
|
+
if (config.enableStorybookTest) {
|
|
130
|
+
steps.push({
|
|
131
|
+
name: 'Storybook Test',
|
|
132
|
+
run: () => storybookTestStep(ctx, true),
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (config.enableVisualTest) {
|
|
137
|
+
steps.push({
|
|
138
|
+
name: 'Visual Test',
|
|
139
|
+
run: () => visualTestStep(),
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return steps;
|
|
102
144
|
}
|
|
103
145
|
|
|
104
146
|
export async function runPipeline(steps: PipelineStep[], ctx: PipelineContext): Promise<PipelineResult> {
|
|
@@ -112,6 +154,11 @@ export async function runPipeline(steps: PipelineStep[], ctx: PipelineContext):
|
|
|
112
154
|
success: false,
|
|
113
155
|
error: `Step "${step.name}" failed: ${result.error}`,
|
|
114
156
|
llmCalls: ctx.llmCalls,
|
|
157
|
+
fixIterations: ctx.fixIterations,
|
|
158
|
+
typeFixIterations: ctx.typeFixIterations,
|
|
159
|
+
testFixIterations: ctx.testFixIterations,
|
|
160
|
+
lintFixIterations: ctx.lintFixIterations,
|
|
161
|
+
storyFixIterations: ctx.storyFixIterations,
|
|
115
162
|
};
|
|
116
163
|
}
|
|
117
164
|
|
|
@@ -121,5 +168,10 @@ export async function runPipeline(steps: PipelineStep[], ctx: PipelineContext):
|
|
|
121
168
|
return {
|
|
122
169
|
success: true,
|
|
123
170
|
llmCalls: ctx.llmCalls,
|
|
171
|
+
fixIterations: ctx.fixIterations,
|
|
172
|
+
typeFixIterations: ctx.typeFixIterations,
|
|
173
|
+
testFixIterations: ctx.testFixIterations,
|
|
174
|
+
lintFixIterations: ctx.lintFixIterations,
|
|
175
|
+
storyFixIterations: ctx.storyFixIterations,
|
|
124
176
|
};
|
|
125
177
|
}
|
|
@@ -36,7 +36,11 @@ function makeCtx(overrides: Partial<PipelineContext> = {}): PipelineContext {
|
|
|
36
36
|
composes: [],
|
|
37
37
|
isModify: false,
|
|
38
38
|
llmCalls: 0,
|
|
39
|
-
|
|
39
|
+
fixIterations: 0,
|
|
40
|
+
typeFixIterations: 0,
|
|
41
|
+
testFixIterations: 0,
|
|
42
|
+
lintFixIterations: 0,
|
|
43
|
+
storyFixIterations: 0,
|
|
40
44
|
testCode: 'import { render } from "@testing-library/react";',
|
|
41
45
|
...overrides,
|
|
42
46
|
};
|
|
@@ -28,7 +28,11 @@ function makeCtx(overrides: Partial<PipelineContext> = {}): PipelineContext {
|
|
|
28
28
|
composes: [],
|
|
29
29
|
isModify: false,
|
|
30
30
|
llmCalls: 0,
|
|
31
|
-
|
|
31
|
+
fixIterations: 0,
|
|
32
|
+
typeFixIterations: 0,
|
|
33
|
+
testFixIterations: 0,
|
|
34
|
+
lintFixIterations: 0,
|
|
35
|
+
storyFixIterations: 0,
|
|
32
36
|
...overrides,
|
|
33
37
|
};
|
|
34
38
|
}
|
|
@@ -35,7 +35,11 @@ function makeCtx(overrides: Partial<PipelineContext> = {}): PipelineContext {
|
|
|
35
35
|
composes: [],
|
|
36
36
|
isModify: false,
|
|
37
37
|
llmCalls: 0,
|
|
38
|
-
|
|
38
|
+
fixIterations: 0,
|
|
39
|
+
typeFixIterations: 0,
|
|
40
|
+
testFixIterations: 0,
|
|
41
|
+
lintFixIterations: 0,
|
|
42
|
+
storyFixIterations: 0,
|
|
39
43
|
...overrides,
|
|
40
44
|
};
|
|
41
45
|
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
vi.mock('ai', () => ({
|
|
4
|
+
generateText: vi.fn(),
|
|
5
|
+
}));
|
|
6
|
+
|
|
7
|
+
vi.mock('node:fs/promises', () => ({
|
|
8
|
+
readFile: vi.fn(),
|
|
9
|
+
writeFile: vi.fn(),
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
vi.mock('../../tools/lint-runner', () => ({
|
|
13
|
+
runLint: vi.fn(),
|
|
14
|
+
runLintFix: vi.fn(),
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
18
|
+
import { generateText } from 'ai';
|
|
19
|
+
import { runLint, runLintFix } from '../../tools/lint-runner';
|
|
20
|
+
import type { PipelineContext } from '../run-pipeline';
|
|
21
|
+
import { lintFixLoop } from './lint-fix-loop';
|
|
22
|
+
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
vi.clearAllMocks();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
function makeCtx(overrides: Partial<PipelineContext> = {}): PipelineContext {
|
|
28
|
+
return {
|
|
29
|
+
componentName: 'MyButton',
|
|
30
|
+
componentPath: '/project/src/MyButton.tsx',
|
|
31
|
+
testPath: '/project/src/MyButton.test.tsx',
|
|
32
|
+
storyPath: '/project/src/MyButton.stories.tsx',
|
|
33
|
+
componentImportPath: '@/components/ui/MyButton',
|
|
34
|
+
targetDir: '/project',
|
|
35
|
+
specDeltas: { structure: [], rendering: [], interaction: [], styling: [] },
|
|
36
|
+
projectSection: '',
|
|
37
|
+
composes: [],
|
|
38
|
+
isModify: false,
|
|
39
|
+
llmCalls: 0,
|
|
40
|
+
fixIterations: 0,
|
|
41
|
+
typeFixIterations: 0,
|
|
42
|
+
testFixIterations: 0,
|
|
43
|
+
lintFixIterations: 0,
|
|
44
|
+
storyFixIterations: 0,
|
|
45
|
+
componentCode: 'original component',
|
|
46
|
+
testCode: 'original test',
|
|
47
|
+
...overrides,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
describe('lintFixLoop', () => {
|
|
52
|
+
it('runs auto-fix first, then returns success if lint passes', async () => {
|
|
53
|
+
vi.mocked(runLintFix).mockReturnValue({ passed: true, errors: [] });
|
|
54
|
+
vi.mocked(readFile).mockResolvedValue('auto-fixed code' as never);
|
|
55
|
+
vi.mocked(runLint).mockReturnValue({ passed: true, errors: [] });
|
|
56
|
+
|
|
57
|
+
const result = await lintFixLoop('mock-model' as never, makeCtx(), 2);
|
|
58
|
+
|
|
59
|
+
expect(result).toEqual({ success: true });
|
|
60
|
+
expect(runLintFix).toHaveBeenCalledWith(
|
|
61
|
+
['/project/src/MyButton.tsx', '/project/src/MyButton.test.tsx'],
|
|
62
|
+
'/project',
|
|
63
|
+
);
|
|
64
|
+
expect(generateText).not.toHaveBeenCalled();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('calls LLM for remaining lint errors after auto-fix', async () => {
|
|
68
|
+
vi.mocked(runLintFix).mockReturnValue({ passed: false, errors: ['unfixable'] });
|
|
69
|
+
vi.mocked(readFile).mockResolvedValue('auto-fixed partial' as never);
|
|
70
|
+
vi.mocked(runLint)
|
|
71
|
+
.mockReturnValueOnce({ passed: false, errors: ['remaining error'] })
|
|
72
|
+
.mockReturnValueOnce({ passed: true, errors: [] });
|
|
73
|
+
|
|
74
|
+
vi.mocked(generateText).mockResolvedValue({
|
|
75
|
+
text: '```tsx\nlint-fixed component\n```\n```tsx\nlint-fixed test\n```',
|
|
76
|
+
} as Awaited<ReturnType<typeof generateText>>);
|
|
77
|
+
vi.mocked(writeFile).mockResolvedValue(undefined);
|
|
78
|
+
|
|
79
|
+
const result = await lintFixLoop('mock-model' as never, makeCtx(), 2);
|
|
80
|
+
|
|
81
|
+
expect(result).toEqual({ success: true });
|
|
82
|
+
expect(generateText).toHaveBeenCalledTimes(1);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('returns failure after exhausting max iterations', async () => {
|
|
86
|
+
vi.mocked(runLintFix).mockReturnValue({ passed: true, errors: [] });
|
|
87
|
+
vi.mocked(readFile).mockResolvedValue('code' as never);
|
|
88
|
+
vi.mocked(runLint).mockReturnValue({ passed: false, errors: ['persistent lint error'] });
|
|
89
|
+
|
|
90
|
+
vi.mocked(generateText).mockResolvedValue({
|
|
91
|
+
text: '```tsx\nstill bad\n```\n```tsx\nstill bad test\n```',
|
|
92
|
+
} as Awaited<ReturnType<typeof generateText>>);
|
|
93
|
+
vi.mocked(writeFile).mockResolvedValue(undefined);
|
|
94
|
+
|
|
95
|
+
const result = await lintFixLoop('mock-model' as never, makeCtx(), 1);
|
|
96
|
+
|
|
97
|
+
expect(result).toEqual({
|
|
98
|
+
success: false,
|
|
99
|
+
error: expect.stringContaining('Lint errors remain after 1 iterations'),
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('handles single code block response by updating only component', async () => {
|
|
104
|
+
vi.mocked(runLintFix).mockReturnValue({ passed: true, errors: [] });
|
|
105
|
+
vi.mocked(readFile).mockResolvedValue('auto-fixed' as never);
|
|
106
|
+
vi.mocked(runLint)
|
|
107
|
+
.mockReturnValueOnce({ passed: false, errors: ['lint error'] })
|
|
108
|
+
.mockReturnValueOnce({ passed: true, errors: [] });
|
|
109
|
+
|
|
110
|
+
vi.mocked(generateText).mockResolvedValue({
|
|
111
|
+
text: '```tsx\nfixed component only\n```',
|
|
112
|
+
} as Awaited<ReturnType<typeof generateText>>);
|
|
113
|
+
vi.mocked(writeFile).mockResolvedValue(undefined);
|
|
114
|
+
|
|
115
|
+
const result = await lintFixLoop('mock-model' as never, makeCtx(), 2);
|
|
116
|
+
|
|
117
|
+
expect(result).toEqual({ success: true });
|
|
118
|
+
expect(writeFile).toHaveBeenCalledWith('/project/src/MyButton.tsx', 'fixed component only', 'utf-8');
|
|
119
|
+
expect(writeFile).toHaveBeenCalledTimes(1);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('returns success when final check passes after last iteration', async () => {
|
|
123
|
+
vi.mocked(runLintFix).mockReturnValue({ passed: true, errors: [] });
|
|
124
|
+
vi.mocked(readFile).mockResolvedValue('auto-fixed' as never);
|
|
125
|
+
vi.mocked(runLint)
|
|
126
|
+
.mockReturnValueOnce({ passed: false, errors: ['error'] })
|
|
127
|
+
.mockReturnValueOnce({ passed: true, errors: [] });
|
|
128
|
+
|
|
129
|
+
vi.mocked(generateText).mockResolvedValue({
|
|
130
|
+
text: '```tsx\nfixed\n```\n```tsx\nfixed test\n```',
|
|
131
|
+
} as Awaited<ReturnType<typeof generateText>>);
|
|
132
|
+
vi.mocked(writeFile).mockResolvedValue(undefined);
|
|
133
|
+
|
|
134
|
+
const result = await lintFixLoop('mock-model' as never, makeCtx(), 1);
|
|
135
|
+
|
|
136
|
+
expect(result).toEqual({ success: true });
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('uses empty string fallback when componentCode and testCode are undefined after readFile', async () => {
|
|
140
|
+
vi.mocked(runLintFix).mockReturnValue({ passed: true, errors: [] });
|
|
141
|
+
vi.mocked(readFile).mockResolvedValue(undefined as never);
|
|
142
|
+
vi.mocked(runLint)
|
|
143
|
+
.mockReturnValueOnce({ passed: false, errors: ['error'] })
|
|
144
|
+
.mockReturnValueOnce({ passed: true, errors: [] });
|
|
145
|
+
|
|
146
|
+
vi.mocked(generateText).mockResolvedValue({
|
|
147
|
+
text: '```tsx\nfixed\n```\n```tsx\nfixed test\n```',
|
|
148
|
+
} as Awaited<ReturnType<typeof generateText>>);
|
|
149
|
+
vi.mocked(writeFile).mockResolvedValue(undefined);
|
|
150
|
+
|
|
151
|
+
const result = await lintFixLoop(
|
|
152
|
+
'mock-model' as never,
|
|
153
|
+
makeCtx({ componentCode: undefined, testCode: undefined }),
|
|
154
|
+
1,
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
expect(result).toEqual({ success: true });
|
|
158
|
+
});
|
|
159
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import type { LanguageModel } from 'ai';
|
|
3
|
+
import { generateText } from 'ai';
|
|
4
|
+
import { extractCodeBlocks } from '../../extract-code-block';
|
|
5
|
+
import { buildLintFixPrompt } from '../../prompt';
|
|
6
|
+
import { runLint, runLintFix } from '../../tools/lint-runner';
|
|
7
|
+
import type { PipelineContext, StepResult } from '../run-pipeline';
|
|
8
|
+
|
|
9
|
+
export async function lintFixLoop(
|
|
10
|
+
model: LanguageModel,
|
|
11
|
+
ctx: PipelineContext,
|
|
12
|
+
maxIterations: number,
|
|
13
|
+
): Promise<StepResult> {
|
|
14
|
+
const filePaths = [ctx.componentPath, ctx.testPath];
|
|
15
|
+
|
|
16
|
+
runLintFix(filePaths, ctx.targetDir);
|
|
17
|
+
|
|
18
|
+
ctx.componentCode = await readFile(ctx.componentPath, 'utf-8');
|
|
19
|
+
ctx.testCode = await readFile(ctx.testPath, 'utf-8');
|
|
20
|
+
|
|
21
|
+
for (let i = 0; i < maxIterations; i++) {
|
|
22
|
+
const result = runLint(filePaths, ctx.targetDir);
|
|
23
|
+
|
|
24
|
+
if (result.passed) {
|
|
25
|
+
return { success: true };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const { system, prompt } = buildLintFixPrompt({
|
|
29
|
+
componentCode: ctx.componentCode ?? '',
|
|
30
|
+
testCode: ctx.testCode ?? '',
|
|
31
|
+
errors: result.errors,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const { text } = await generateText({ model, system, prompt });
|
|
35
|
+
ctx.llmCalls++;
|
|
36
|
+
ctx.fixIterations++;
|
|
37
|
+
ctx.lintFixIterations++;
|
|
38
|
+
|
|
39
|
+
const blocks = extractCodeBlocks(text);
|
|
40
|
+
if (blocks.length >= 2) {
|
|
41
|
+
ctx.componentCode = blocks[0];
|
|
42
|
+
ctx.testCode = blocks[1];
|
|
43
|
+
await writeFile(ctx.componentPath, blocks[0], 'utf-8');
|
|
44
|
+
await writeFile(ctx.testPath, blocks[1], 'utf-8');
|
|
45
|
+
} else if (blocks.length === 1) {
|
|
46
|
+
ctx.componentCode = blocks[0];
|
|
47
|
+
await writeFile(ctx.componentPath, blocks[0], 'utf-8');
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const finalResult = runLint(filePaths, ctx.targetDir);
|
|
52
|
+
if (finalResult.passed) {
|
|
53
|
+
return { success: true };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
success: false,
|
|
58
|
+
error: `Lint errors remain after ${maxIterations} iterations: ${finalResult.errors.slice(0, 3).join('; ')}`,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
vi.mock('ai', () => ({
|
|
4
|
+
generateText: vi.fn(),
|
|
5
|
+
}));
|
|
6
|
+
|
|
7
|
+
vi.mock('node:fs/promises', () => ({
|
|
8
|
+
readFile: vi.fn(),
|
|
9
|
+
writeFile: vi.fn(),
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
vi.mock('../../tools/type-checker', () => ({
|
|
13
|
+
runTypeCheck: vi.fn(),
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
17
|
+
import { generateText } from 'ai';
|
|
18
|
+
import { runTypeCheck } from '../../tools/type-checker';
|
|
19
|
+
import type { PipelineContext } from '../run-pipeline';
|
|
20
|
+
import { storyFixLoop } from './story-fix-loop';
|
|
21
|
+
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
vi.clearAllMocks();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
function makeCtx(overrides: Partial<PipelineContext> = {}): PipelineContext {
|
|
27
|
+
return {
|
|
28
|
+
componentName: 'MyButton',
|
|
29
|
+
componentPath: '/project/src/MyButton.tsx',
|
|
30
|
+
testPath: '/project/src/MyButton.test.tsx',
|
|
31
|
+
storyPath: '/project/src/MyButton.stories.tsx',
|
|
32
|
+
componentImportPath: '@/components/ui/MyButton',
|
|
33
|
+
targetDir: '/project',
|
|
34
|
+
specDeltas: { structure: [], rendering: [], interaction: [], styling: [] },
|
|
35
|
+
projectSection: '',
|
|
36
|
+
composes: [],
|
|
37
|
+
isModify: false,
|
|
38
|
+
llmCalls: 0,
|
|
39
|
+
fixIterations: 0,
|
|
40
|
+
typeFixIterations: 0,
|
|
41
|
+
testFixIterations: 0,
|
|
42
|
+
lintFixIterations: 0,
|
|
43
|
+
storyFixIterations: 0,
|
|
44
|
+
storyCode: 'original story',
|
|
45
|
+
componentCode: 'original component',
|
|
46
|
+
...overrides,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
describe('storyFixLoop', () => {
|
|
51
|
+
it('returns success immediately when story type-checks', async () => {
|
|
52
|
+
vi.mocked(runTypeCheck).mockReturnValue({ passed: true, errors: [] });
|
|
53
|
+
|
|
54
|
+
const result = await storyFixLoop('mock-model' as never, makeCtx(), 2);
|
|
55
|
+
|
|
56
|
+
expect(result).toEqual({ success: true });
|
|
57
|
+
expect(generateText).not.toHaveBeenCalled();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('calls LLM to fix story type errors and writes fixed story', async () => {
|
|
61
|
+
vi.mocked(runTypeCheck)
|
|
62
|
+
.mockReturnValueOnce({ passed: false, errors: ['error in story'] })
|
|
63
|
+
.mockReturnValueOnce({ passed: true, errors: [] });
|
|
64
|
+
|
|
65
|
+
vi.mocked(generateText).mockResolvedValue({
|
|
66
|
+
text: '```tsx\nfixed story code\n```',
|
|
67
|
+
} as Awaited<ReturnType<typeof generateText>>);
|
|
68
|
+
vi.mocked(writeFile).mockResolvedValue(undefined);
|
|
69
|
+
|
|
70
|
+
const result = await storyFixLoop('mock-model' as never, makeCtx(), 2);
|
|
71
|
+
|
|
72
|
+
expect(result).toEqual({ success: true });
|
|
73
|
+
expect(generateText).toHaveBeenCalledTimes(1);
|
|
74
|
+
expect(writeFile).toHaveBeenCalledWith('/project/src/MyButton.stories.tsx', 'fixed story code', 'utf-8');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('returns failure after exhausting max iterations', async () => {
|
|
78
|
+
vi.mocked(runTypeCheck).mockReturnValue({ passed: false, errors: ['persistent story error'] });
|
|
79
|
+
|
|
80
|
+
vi.mocked(generateText).mockResolvedValue({
|
|
81
|
+
text: '```tsx\nstill broken story\n```',
|
|
82
|
+
} as Awaited<ReturnType<typeof generateText>>);
|
|
83
|
+
vi.mocked(writeFile).mockResolvedValue(undefined);
|
|
84
|
+
vi.mocked(readFile).mockResolvedValue('still broken story' as never);
|
|
85
|
+
|
|
86
|
+
const result = await storyFixLoop('mock-model' as never, makeCtx(), 1);
|
|
87
|
+
|
|
88
|
+
expect(result).toEqual({
|
|
89
|
+
success: false,
|
|
90
|
+
error: expect.stringContaining('Story type errors remain after 1 iterations'),
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('returns success when final check passes after last iteration fix', async () => {
|
|
95
|
+
vi.mocked(runTypeCheck)
|
|
96
|
+
.mockReturnValueOnce({ passed: false, errors: ['error'] })
|
|
97
|
+
.mockReturnValueOnce({ passed: true, errors: [] });
|
|
98
|
+
|
|
99
|
+
vi.mocked(generateText).mockResolvedValue({
|
|
100
|
+
text: '```tsx\nfixed story\n```',
|
|
101
|
+
} as Awaited<ReturnType<typeof generateText>>);
|
|
102
|
+
vi.mocked(writeFile).mockResolvedValue(undefined);
|
|
103
|
+
|
|
104
|
+
const result = await storyFixLoop('mock-model' as never, makeCtx(), 1);
|
|
105
|
+
|
|
106
|
+
expect(result).toEqual({ success: true });
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('uses empty string fallback when storyCode and componentCode are undefined', async () => {
|
|
110
|
+
vi.mocked(runTypeCheck)
|
|
111
|
+
.mockReturnValueOnce({ passed: false, errors: ['error'] })
|
|
112
|
+
.mockReturnValueOnce({ passed: true, errors: [] });
|
|
113
|
+
|
|
114
|
+
vi.mocked(generateText).mockResolvedValue({
|
|
115
|
+
text: '```tsx\nfixed story\n```',
|
|
116
|
+
} as Awaited<ReturnType<typeof generateText>>);
|
|
117
|
+
vi.mocked(writeFile).mockResolvedValue(undefined);
|
|
118
|
+
|
|
119
|
+
const result = await storyFixLoop(
|
|
120
|
+
'mock-model' as never,
|
|
121
|
+
makeCtx({ storyCode: undefined, componentCode: undefined }),
|
|
122
|
+
1,
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
expect(result).toEqual({ success: true });
|
|
126
|
+
});
|
|
127
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import type { LanguageModel } from 'ai';
|
|
3
|
+
import { generateText } from 'ai';
|
|
4
|
+
import { extractCodeBlock } from '../../extract-code-block';
|
|
5
|
+
import { buildStoryFixPrompt } from '../../prompt';
|
|
6
|
+
import { runTypeCheck } from '../../tools/type-checker';
|
|
7
|
+
import type { PipelineContext, StepResult } from '../run-pipeline';
|
|
8
|
+
|
|
9
|
+
export async function storyFixLoop(
|
|
10
|
+
model: LanguageModel,
|
|
11
|
+
ctx: PipelineContext,
|
|
12
|
+
maxIterations: number,
|
|
13
|
+
): Promise<StepResult> {
|
|
14
|
+
for (let i = 0; i < maxIterations; i++) {
|
|
15
|
+
const result = runTypeCheck(ctx.targetDir, [ctx.storyPath]);
|
|
16
|
+
|
|
17
|
+
if (result.passed) {
|
|
18
|
+
return { success: true };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const { system, prompt } = buildStoryFixPrompt({
|
|
22
|
+
storyCode: ctx.storyCode ?? '',
|
|
23
|
+
componentCode: ctx.componentCode ?? '',
|
|
24
|
+
errors: result.errors,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const { text } = await generateText({ model, system, prompt });
|
|
28
|
+
ctx.llmCalls++;
|
|
29
|
+
ctx.fixIterations++;
|
|
30
|
+
ctx.storyFixIterations++;
|
|
31
|
+
|
|
32
|
+
const fixedStory = extractCodeBlock(text);
|
|
33
|
+
ctx.storyCode = fixedStory;
|
|
34
|
+
await writeFile(ctx.storyPath, fixedStory, 'utf-8');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const finalResult = runTypeCheck(ctx.targetDir, [ctx.storyPath]);
|
|
38
|
+
if (finalResult.passed) {
|
|
39
|
+
return { success: true };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
ctx.storyCode = await readFile(ctx.storyPath, 'utf-8');
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
success: false,
|
|
46
|
+
error: `Story type errors remain after ${maxIterations} iterations: ${finalResult.errors.join('; ')}`,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
vi.mock('../../tools/type-checker', () => ({
|
|
4
|
+
runTypeCheck: vi.fn(),
|
|
5
|
+
}));
|
|
6
|
+
|
|
7
|
+
vi.mock('../../tools/storybook-runner', () => ({
|
|
8
|
+
runStorybookTest: vi.fn(),
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
import { runStorybookTest } from '../../tools/storybook-runner';
|
|
12
|
+
import { runTypeCheck } from '../../tools/type-checker';
|
|
13
|
+
import type { PipelineContext } from '../run-pipeline';
|
|
14
|
+
import { storybookTestStep } from './storybook-test';
|
|
15
|
+
|
|
16
|
+
function makeCtx(overrides: Partial<PipelineContext> = {}): PipelineContext {
|
|
17
|
+
return {
|
|
18
|
+
componentName: 'MyButton',
|
|
19
|
+
componentPath: '/project/src/MyButton.tsx',
|
|
20
|
+
testPath: '/project/src/MyButton.test.tsx',
|
|
21
|
+
storyPath: '/project/src/MyButton.stories.tsx',
|
|
22
|
+
componentImportPath: '@/components/ui/MyButton',
|
|
23
|
+
targetDir: '/project',
|
|
24
|
+
specDeltas: { structure: [], rendering: [], interaction: [], styling: [] },
|
|
25
|
+
projectSection: '',
|
|
26
|
+
composes: [],
|
|
27
|
+
isModify: false,
|
|
28
|
+
llmCalls: 0,
|
|
29
|
+
fixIterations: 0,
|
|
30
|
+
typeFixIterations: 0,
|
|
31
|
+
testFixIterations: 0,
|
|
32
|
+
lintFixIterations: 0,
|
|
33
|
+
storyFixIterations: 0,
|
|
34
|
+
...overrides,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
describe('storybookTestStep', () => {
|
|
39
|
+
it('returns success when type check passes and storybook cli disabled', async () => {
|
|
40
|
+
vi.mocked(runTypeCheck).mockReturnValue({ passed: true, errors: [] });
|
|
41
|
+
|
|
42
|
+
const result = await storybookTestStep(makeCtx(), false);
|
|
43
|
+
|
|
44
|
+
expect(result).toEqual({ success: true });
|
|
45
|
+
expect(runStorybookTest).not.toHaveBeenCalled();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('returns failure when story type check fails', async () => {
|
|
49
|
+
vi.mocked(runTypeCheck).mockReturnValue({
|
|
50
|
+
passed: false,
|
|
51
|
+
errors: ['error in story'],
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const result = await storybookTestStep(makeCtx(), false);
|
|
55
|
+
|
|
56
|
+
expect(result).toEqual({
|
|
57
|
+
success: false,
|
|
58
|
+
error: expect.stringContaining('Story type errors'),
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('runs storybook CLI test when enabled and type check passes', async () => {
|
|
63
|
+
vi.mocked(runTypeCheck).mockReturnValue({ passed: true, errors: [] });
|
|
64
|
+
vi.mocked(runStorybookTest).mockReturnValue({ passed: true, errors: [] });
|
|
65
|
+
|
|
66
|
+
const result = await storybookTestStep(makeCtx(), true);
|
|
67
|
+
|
|
68
|
+
expect(result).toEqual({ success: true });
|
|
69
|
+
expect(runStorybookTest).toHaveBeenCalledWith('/project/src/MyButton.stories.tsx', '/project');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('returns failure when storybook CLI test fails', async () => {
|
|
73
|
+
vi.mocked(runTypeCheck).mockReturnValue({ passed: true, errors: [] });
|
|
74
|
+
vi.mocked(runStorybookTest).mockReturnValue({
|
|
75
|
+
passed: false,
|
|
76
|
+
errors: ['Story failed'],
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const result = await storybookTestStep(makeCtx(), true);
|
|
80
|
+
|
|
81
|
+
expect(result).toEqual({
|
|
82
|
+
success: false,
|
|
83
|
+
error: expect.stringContaining('Storybook test failures'),
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
});
|