@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
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { runStorybookTest } from '../../tools/storybook-runner';
|
|
2
|
+
import { runTypeCheck } from '../../tools/type-checker';
|
|
3
|
+
import type { PipelineContext, StepResult } from '../run-pipeline';
|
|
4
|
+
|
|
5
|
+
export async function storybookTestStep(ctx: PipelineContext, enableStorybookCli: boolean): Promise<StepResult> {
|
|
6
|
+
const typeResult = runTypeCheck(ctx.targetDir, [ctx.storyPath]);
|
|
7
|
+
|
|
8
|
+
if (!typeResult.passed) {
|
|
9
|
+
return {
|
|
10
|
+
success: false,
|
|
11
|
+
error: `Story type errors: ${typeResult.errors.join('; ')}`,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (enableStorybookCli) {
|
|
16
|
+
const testResult = runStorybookTest(ctx.storyPath, ctx.targetDir);
|
|
17
|
+
|
|
18
|
+
if (!testResult.passed) {
|
|
19
|
+
return {
|
|
20
|
+
success: false,
|
|
21
|
+
error: `Storybook test failures: ${testResult.errors.join('; ')}`,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return { success: true };
|
|
27
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
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/test-runner', () => ({
|
|
13
|
+
runTests: vi.fn(),
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
17
|
+
import { generateText } from 'ai';
|
|
18
|
+
import { runTests } from '../../tools/test-runner';
|
|
19
|
+
import type { PipelineContext } from '../run-pipeline';
|
|
20
|
+
import { testFixLoop } from './test-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
|
+
componentCode: 'original component',
|
|
45
|
+
testCode: 'original test',
|
|
46
|
+
...overrides,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
describe('testFixLoop', () => {
|
|
51
|
+
it('returns success immediately when all tests pass', async () => {
|
|
52
|
+
vi.mocked(runTests).mockReturnValue({
|
|
53
|
+
passed: true,
|
|
54
|
+
numPassed: 3,
|
|
55
|
+
numFailed: 0,
|
|
56
|
+
failures: [],
|
|
57
|
+
output: '',
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const result = await testFixLoop('mock-model' as never, makeCtx(), 3);
|
|
61
|
+
|
|
62
|
+
expect(result).toEqual({ success: true });
|
|
63
|
+
expect(generateText).not.toHaveBeenCalled();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('calls LLM to fix failures and writes fixed files', async () => {
|
|
67
|
+
vi.mocked(runTests)
|
|
68
|
+
.mockReturnValueOnce({
|
|
69
|
+
passed: false,
|
|
70
|
+
numPassed: 1,
|
|
71
|
+
numFailed: 1,
|
|
72
|
+
failures: ['renders button: element not found'],
|
|
73
|
+
output: 'FAIL',
|
|
74
|
+
})
|
|
75
|
+
.mockReturnValueOnce({
|
|
76
|
+
passed: true,
|
|
77
|
+
numPassed: 2,
|
|
78
|
+
numFailed: 0,
|
|
79
|
+
failures: [],
|
|
80
|
+
output: 'PASS',
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
vi.mocked(generateText).mockResolvedValue({
|
|
84
|
+
text: '```tsx\nfixed component\n```\n```tsx\nfixed test\n```',
|
|
85
|
+
} as Awaited<ReturnType<typeof generateText>>);
|
|
86
|
+
vi.mocked(writeFile).mockResolvedValue(undefined);
|
|
87
|
+
|
|
88
|
+
const result = await testFixLoop('mock-model' as never, makeCtx(), 3);
|
|
89
|
+
|
|
90
|
+
expect(result).toEqual({ success: true });
|
|
91
|
+
expect(generateText).toHaveBeenCalledTimes(1);
|
|
92
|
+
expect(writeFile).toHaveBeenCalledWith('/project/src/MyButton.tsx', 'fixed component', 'utf-8');
|
|
93
|
+
expect(writeFile).toHaveBeenCalledWith('/project/src/MyButton.test.tsx', 'fixed test', 'utf-8');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('returns failure after exhausting max iterations', async () => {
|
|
97
|
+
vi.mocked(runTests).mockReturnValue({
|
|
98
|
+
passed: false,
|
|
99
|
+
numPassed: 0,
|
|
100
|
+
numFailed: 1,
|
|
101
|
+
failures: ['persistent failure'],
|
|
102
|
+
output: 'FAIL',
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
vi.mocked(generateText).mockResolvedValue({
|
|
106
|
+
text: '```tsx\nstill broken\n```\n```tsx\nstill broken test\n```',
|
|
107
|
+
} as Awaited<ReturnType<typeof generateText>>);
|
|
108
|
+
vi.mocked(writeFile).mockResolvedValue(undefined);
|
|
109
|
+
vi.mocked(readFile).mockResolvedValue('still broken' as never);
|
|
110
|
+
|
|
111
|
+
const result = await testFixLoop('mock-model' as never, makeCtx(), 1);
|
|
112
|
+
|
|
113
|
+
expect(result).toEqual({
|
|
114
|
+
success: false,
|
|
115
|
+
error: expect.stringContaining('Test failures remain after 1 iterations'),
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('handles single code block response by updating only component', async () => {
|
|
120
|
+
vi.mocked(runTests)
|
|
121
|
+
.mockReturnValueOnce({
|
|
122
|
+
passed: false,
|
|
123
|
+
numPassed: 0,
|
|
124
|
+
numFailed: 1,
|
|
125
|
+
failures: ['failure'],
|
|
126
|
+
output: 'FAIL',
|
|
127
|
+
})
|
|
128
|
+
.mockReturnValueOnce({
|
|
129
|
+
passed: true,
|
|
130
|
+
numPassed: 1,
|
|
131
|
+
numFailed: 0,
|
|
132
|
+
failures: [],
|
|
133
|
+
output: 'PASS',
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
vi.mocked(generateText).mockResolvedValue({
|
|
137
|
+
text: '```tsx\nfixed component only\n```',
|
|
138
|
+
} as Awaited<ReturnType<typeof generateText>>);
|
|
139
|
+
vi.mocked(writeFile).mockResolvedValue(undefined);
|
|
140
|
+
|
|
141
|
+
const result = await testFixLoop('mock-model' as never, makeCtx(), 3);
|
|
142
|
+
|
|
143
|
+
expect(result).toEqual({ success: true });
|
|
144
|
+
expect(writeFile).toHaveBeenCalledWith('/project/src/MyButton.tsx', 'fixed component only', 'utf-8');
|
|
145
|
+
expect(writeFile).toHaveBeenCalledTimes(1);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('returns success when final check passes after last iteration fix', async () => {
|
|
149
|
+
vi.mocked(runTests)
|
|
150
|
+
.mockReturnValueOnce({
|
|
151
|
+
passed: false,
|
|
152
|
+
numPassed: 0,
|
|
153
|
+
numFailed: 1,
|
|
154
|
+
failures: ['failure'],
|
|
155
|
+
output: 'FAIL',
|
|
156
|
+
})
|
|
157
|
+
.mockReturnValueOnce({
|
|
158
|
+
passed: true,
|
|
159
|
+
numPassed: 1,
|
|
160
|
+
numFailed: 0,
|
|
161
|
+
failures: [],
|
|
162
|
+
output: 'PASS',
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
vi.mocked(generateText).mockResolvedValue({
|
|
166
|
+
text: '```tsx\nfixed\n```\n```tsx\nfixed test\n```',
|
|
167
|
+
} as Awaited<ReturnType<typeof generateText>>);
|
|
168
|
+
vi.mocked(writeFile).mockResolvedValue(undefined);
|
|
169
|
+
|
|
170
|
+
const result = await testFixLoop('mock-model' as never, makeCtx(), 1);
|
|
171
|
+
|
|
172
|
+
expect(result).toEqual({ success: true });
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('uses empty string fallback when componentCode and testCode are undefined', async () => {
|
|
176
|
+
vi.mocked(runTests)
|
|
177
|
+
.mockReturnValueOnce({
|
|
178
|
+
passed: false,
|
|
179
|
+
numPassed: 0,
|
|
180
|
+
numFailed: 1,
|
|
181
|
+
failures: ['failure'],
|
|
182
|
+
output: 'FAIL',
|
|
183
|
+
})
|
|
184
|
+
.mockReturnValueOnce({
|
|
185
|
+
passed: true,
|
|
186
|
+
numPassed: 1,
|
|
187
|
+
numFailed: 0,
|
|
188
|
+
failures: [],
|
|
189
|
+
output: 'PASS',
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
vi.mocked(generateText).mockResolvedValue({
|
|
193
|
+
text: '```tsx\nfixed\n```\n```tsx\nfixed test\n```',
|
|
194
|
+
} as Awaited<ReturnType<typeof generateText>>);
|
|
195
|
+
vi.mocked(writeFile).mockResolvedValue(undefined);
|
|
196
|
+
|
|
197
|
+
const result = await testFixLoop(
|
|
198
|
+
'mock-model' as never,
|
|
199
|
+
makeCtx({ componentCode: undefined, testCode: undefined }),
|
|
200
|
+
1,
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
expect(result).toEqual({ success: true });
|
|
204
|
+
});
|
|
205
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
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 { buildTestFixPrompt } from '../../prompt';
|
|
6
|
+
import { runTests } from '../../tools/test-runner';
|
|
7
|
+
import type { PipelineContext, StepResult } from '../run-pipeline';
|
|
8
|
+
|
|
9
|
+
export async function testFixLoop(
|
|
10
|
+
model: LanguageModel,
|
|
11
|
+
ctx: PipelineContext,
|
|
12
|
+
maxIterations: number,
|
|
13
|
+
): Promise<StepResult> {
|
|
14
|
+
for (let i = 0; i < maxIterations; i++) {
|
|
15
|
+
const result = runTests(ctx.testPath, ctx.targetDir);
|
|
16
|
+
|
|
17
|
+
if (result.passed) {
|
|
18
|
+
return { success: true };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const { system, prompt } = buildTestFixPrompt({
|
|
22
|
+
componentCode: ctx.componentCode ?? '',
|
|
23
|
+
testCode: ctx.testCode ?? '',
|
|
24
|
+
failures: result.failures,
|
|
25
|
+
testOutput: result.output,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const { text } = await generateText({ model, system, prompt });
|
|
29
|
+
ctx.llmCalls++;
|
|
30
|
+
ctx.fixIterations++;
|
|
31
|
+
ctx.testFixIterations++;
|
|
32
|
+
|
|
33
|
+
const blocks = extractCodeBlocks(text);
|
|
34
|
+
if (blocks.length >= 2) {
|
|
35
|
+
ctx.componentCode = blocks[0];
|
|
36
|
+
ctx.testCode = blocks[1];
|
|
37
|
+
await writeFile(ctx.componentPath, blocks[0], 'utf-8');
|
|
38
|
+
await writeFile(ctx.testPath, blocks[1], 'utf-8');
|
|
39
|
+
} else if (blocks.length === 1) {
|
|
40
|
+
ctx.componentCode = blocks[0];
|
|
41
|
+
await writeFile(ctx.componentPath, blocks[0], 'utf-8');
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const finalResult = runTests(ctx.testPath, ctx.targetDir);
|
|
46
|
+
if (finalResult.passed) {
|
|
47
|
+
return { success: true };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
ctx.componentCode = await readFile(ctx.componentPath, 'utf-8');
|
|
51
|
+
ctx.testCode = await readFile(ctx.testPath, 'utf-8');
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
success: false,
|
|
55
|
+
error: `Test failures remain after ${maxIterations} iterations: ${finalResult.failures.join('; ')}`,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
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 { typeFixLoop } from './type-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
|
+
componentCode: 'original component',
|
|
45
|
+
testCode: 'original test',
|
|
46
|
+
...overrides,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
describe('typeFixLoop', () => {
|
|
51
|
+
it('returns success immediately when type check passes', async () => {
|
|
52
|
+
vi.mocked(runTypeCheck).mockReturnValue({ passed: true, errors: [] });
|
|
53
|
+
|
|
54
|
+
const result = await typeFixLoop('mock-model' as never, makeCtx(), 3);
|
|
55
|
+
|
|
56
|
+
expect(result).toEqual({ success: true });
|
|
57
|
+
expect(generateText).not.toHaveBeenCalled();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('calls LLM to fix type errors and writes fixed files', async () => {
|
|
61
|
+
vi.mocked(runTypeCheck)
|
|
62
|
+
.mockReturnValueOnce({ passed: false, errors: ['error TS2307: Module not found'] })
|
|
63
|
+
.mockReturnValueOnce({ passed: true, errors: [] });
|
|
64
|
+
|
|
65
|
+
vi.mocked(generateText).mockResolvedValue({
|
|
66
|
+
text: '```tsx\nfixed component\n```\n```tsx\nfixed test\n```',
|
|
67
|
+
} as Awaited<ReturnType<typeof generateText>>);
|
|
68
|
+
vi.mocked(writeFile).mockResolvedValue(undefined);
|
|
69
|
+
|
|
70
|
+
const result = await typeFixLoop('mock-model' as never, makeCtx(), 3);
|
|
71
|
+
|
|
72
|
+
expect(result).toEqual({ success: true });
|
|
73
|
+
expect(generateText).toHaveBeenCalledTimes(1);
|
|
74
|
+
expect(writeFile).toHaveBeenCalledWith('/project/src/MyButton.tsx', 'fixed component', 'utf-8');
|
|
75
|
+
expect(writeFile).toHaveBeenCalledWith('/project/src/MyButton.test.tsx', 'fixed test', 'utf-8');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('returns failure after exhausting max iterations', async () => {
|
|
79
|
+
vi.mocked(runTypeCheck).mockReturnValue({
|
|
80
|
+
passed: false,
|
|
81
|
+
errors: ['persistent error'],
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
vi.mocked(generateText).mockResolvedValue({
|
|
85
|
+
text: '```tsx\nstill broken\n```\n```tsx\nstill broken test\n```',
|
|
86
|
+
} as Awaited<ReturnType<typeof generateText>>);
|
|
87
|
+
vi.mocked(writeFile).mockResolvedValue(undefined);
|
|
88
|
+
vi.mocked(readFile).mockResolvedValue('still broken' as never);
|
|
89
|
+
|
|
90
|
+
const result = await typeFixLoop('mock-model' as never, makeCtx(), 2);
|
|
91
|
+
|
|
92
|
+
expect(result).toEqual({
|
|
93
|
+
success: false,
|
|
94
|
+
error: expect.stringContaining('Type errors remain after 2 iterations'),
|
|
95
|
+
});
|
|
96
|
+
expect(generateText).toHaveBeenCalledTimes(2);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('handles single code block response by updating only component', async () => {
|
|
100
|
+
vi.mocked(runTypeCheck)
|
|
101
|
+
.mockReturnValueOnce({ passed: false, errors: ['error in component'] })
|
|
102
|
+
.mockReturnValueOnce({ passed: true, errors: [] });
|
|
103
|
+
|
|
104
|
+
vi.mocked(generateText).mockResolvedValue({
|
|
105
|
+
text: '```tsx\nfixed component only\n```',
|
|
106
|
+
} as Awaited<ReturnType<typeof generateText>>);
|
|
107
|
+
vi.mocked(writeFile).mockResolvedValue(undefined);
|
|
108
|
+
|
|
109
|
+
const result = await typeFixLoop('mock-model' as never, makeCtx(), 3);
|
|
110
|
+
|
|
111
|
+
expect(result).toEqual({ success: true });
|
|
112
|
+
expect(writeFile).toHaveBeenCalledWith('/project/src/MyButton.tsx', 'fixed component only', 'utf-8');
|
|
113
|
+
expect(writeFile).toHaveBeenCalledTimes(1);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('returns success when final check passes after last iteration fix', async () => {
|
|
117
|
+
vi.mocked(runTypeCheck)
|
|
118
|
+
.mockReturnValueOnce({ passed: false, errors: ['error'] })
|
|
119
|
+
.mockReturnValueOnce({ passed: true, errors: [] });
|
|
120
|
+
|
|
121
|
+
vi.mocked(generateText).mockResolvedValue({
|
|
122
|
+
text: '```tsx\nfixed\n```\n```tsx\nfixed test\n```',
|
|
123
|
+
} as Awaited<ReturnType<typeof generateText>>);
|
|
124
|
+
vi.mocked(writeFile).mockResolvedValue(undefined);
|
|
125
|
+
|
|
126
|
+
const result = await typeFixLoop('mock-model' as never, makeCtx(), 1);
|
|
127
|
+
|
|
128
|
+
expect(result).toEqual({ success: true });
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('uses empty string fallback when componentCode and testCode are undefined', async () => {
|
|
132
|
+
vi.mocked(runTypeCheck)
|
|
133
|
+
.mockReturnValueOnce({ passed: false, errors: ['error'] })
|
|
134
|
+
.mockReturnValueOnce({ passed: true, errors: [] });
|
|
135
|
+
|
|
136
|
+
vi.mocked(generateText).mockResolvedValue({
|
|
137
|
+
text: '```tsx\nfixed\n```\n```tsx\nfixed test\n```',
|
|
138
|
+
} as Awaited<ReturnType<typeof generateText>>);
|
|
139
|
+
vi.mocked(writeFile).mockResolvedValue(undefined);
|
|
140
|
+
|
|
141
|
+
const result = await typeFixLoop(
|
|
142
|
+
'mock-model' as never,
|
|
143
|
+
makeCtx({ componentCode: undefined, testCode: undefined }),
|
|
144
|
+
1,
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
expect(result).toEqual({ success: true });
|
|
148
|
+
});
|
|
149
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
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 { buildTypeFixPrompt } from '../../prompt';
|
|
6
|
+
import { runTypeCheck } from '../../tools/type-checker';
|
|
7
|
+
import type { PipelineContext, StepResult } from '../run-pipeline';
|
|
8
|
+
|
|
9
|
+
export async function typeFixLoop(
|
|
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.componentPath, ctx.testPath]);
|
|
16
|
+
|
|
17
|
+
if (result.passed) {
|
|
18
|
+
return { success: true };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const { system, prompt } = buildTypeFixPrompt({
|
|
22
|
+
componentCode: ctx.componentCode ?? '',
|
|
23
|
+
testCode: ctx.testCode ?? '',
|
|
24
|
+
errors: result.errors,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const { text } = await generateText({ model, system, prompt });
|
|
28
|
+
ctx.llmCalls++;
|
|
29
|
+
ctx.fixIterations++;
|
|
30
|
+
ctx.typeFixIterations++;
|
|
31
|
+
|
|
32
|
+
const blocks = extractCodeBlocks(text);
|
|
33
|
+
if (blocks.length >= 2) {
|
|
34
|
+
ctx.componentCode = blocks[0];
|
|
35
|
+
ctx.testCode = blocks[1];
|
|
36
|
+
await writeFile(ctx.componentPath, blocks[0], 'utf-8');
|
|
37
|
+
await writeFile(ctx.testPath, blocks[1], 'utf-8');
|
|
38
|
+
} else if (blocks.length === 1) {
|
|
39
|
+
ctx.componentCode = blocks[0];
|
|
40
|
+
await writeFile(ctx.componentPath, blocks[0], 'utf-8');
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const finalResult = runTypeCheck(ctx.targetDir, [ctx.componentPath, ctx.testPath]);
|
|
45
|
+
if (finalResult.passed) {
|
|
46
|
+
return { success: true };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
ctx.componentCode = await readFile(ctx.componentPath, 'utf-8');
|
|
50
|
+
ctx.testCode = await readFile(ctx.testPath, 'utf-8');
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
success: false,
|
|
54
|
+
error: `Type errors remain after ${maxIterations} iterations: ${finalResult.errors.join('; ')}`,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { visualTestStep } from './visual-test';
|
|
3
|
+
|
|
4
|
+
describe('visualTestStep', () => {
|
|
5
|
+
it('returns success (placeholder)', async () => {
|
|
6
|
+
const result = await visualTestStep();
|
|
7
|
+
|
|
8
|
+
expect(result).toEqual({ success: true });
|
|
9
|
+
});
|
|
10
|
+
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"fix-from-feedback.d.ts","sourceRoot":"","sources":["../../../../src/pipeline/steps/fix-from-feedback.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,IAAI,CAAC;AAIxC,OAAO,KAAK,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAoBnE,wBAAsB,mBAAmB,CAAC,KAAK,EAAE,aAAa,EAAE,GAAG,EAAE,eAAe,GAAG,OAAO,CAAC,UAAU,CAAC,CA+EzG"}
|
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
import { readFile, writeFile } from 'node:fs/promises';
|
|
2
|
-
import { generateText } from 'ai';
|
|
3
|
-
import { extractCodeBlocks } from '../../extract-code-block.js';
|
|
4
|
-
import { buildLintFixPrompt, buildTestFixPrompt, buildTypeFixPrompt } from '../../prompt.js';
|
|
5
|
-
function categorizeErrors(errorFeedback) {
|
|
6
|
-
const hasTypeError = /error TS\d+|type.*error|typescript/i.test(errorFeedback);
|
|
7
|
-
const hasTestFailure = /test.*fail|vitest|expect.*to|assertion/i.test(errorFeedback);
|
|
8
|
-
const hasLintError = /lint|biome|eslint/i.test(errorFeedback);
|
|
9
|
-
const categories = [hasTypeError, hasTestFailure, hasLintError].filter(Boolean).length;
|
|
10
|
-
if (categories > 1)
|
|
11
|
-
return 'mixed';
|
|
12
|
-
if (hasTypeError)
|
|
13
|
-
return 'type';
|
|
14
|
-
if (hasTestFailure)
|
|
15
|
-
return 'test';
|
|
16
|
-
if (hasLintError)
|
|
17
|
-
return 'lint';
|
|
18
|
-
// Default to test errors if we can't categorize
|
|
19
|
-
return 'test';
|
|
20
|
-
}
|
|
21
|
-
export async function fixFromFeedbackStep(model, ctx) {
|
|
22
|
-
if (!ctx.errorFeedback) {
|
|
23
|
-
return { success: true };
|
|
24
|
-
}
|
|
25
|
-
// Read existing files
|
|
26
|
-
const componentCode = await readFile(ctx.componentPath, 'utf-8');
|
|
27
|
-
const testCode = await readFile(ctx.testPath, 'utf-8');
|
|
28
|
-
ctx.componentCode = componentCode;
|
|
29
|
-
ctx.testCode = testCode;
|
|
30
|
-
const errorCategory = categorizeErrors(ctx.errorFeedback);
|
|
31
|
-
let system;
|
|
32
|
-
let prompt;
|
|
33
|
-
switch (errorCategory) {
|
|
34
|
-
case 'type': {
|
|
35
|
-
const typePrompt = buildTypeFixPrompt({
|
|
36
|
-
componentCode,
|
|
37
|
-
testCode,
|
|
38
|
-
errors: [ctx.errorFeedback],
|
|
39
|
-
});
|
|
40
|
-
system = typePrompt.system;
|
|
41
|
-
prompt = typePrompt.prompt;
|
|
42
|
-
break;
|
|
43
|
-
}
|
|
44
|
-
case 'test': {
|
|
45
|
-
const testPrompt = buildTestFixPrompt({
|
|
46
|
-
componentCode,
|
|
47
|
-
testCode,
|
|
48
|
-
failures: [ctx.errorFeedback],
|
|
49
|
-
testOutput: ctx.errorFeedback,
|
|
50
|
-
});
|
|
51
|
-
system = testPrompt.system;
|
|
52
|
-
prompt = testPrompt.prompt;
|
|
53
|
-
break;
|
|
54
|
-
}
|
|
55
|
-
case 'lint': {
|
|
56
|
-
const lintPrompt = buildLintFixPrompt({
|
|
57
|
-
componentCode,
|
|
58
|
-
testCode,
|
|
59
|
-
errors: [ctx.errorFeedback],
|
|
60
|
-
});
|
|
61
|
-
system = lintPrompt.system;
|
|
62
|
-
prompt = lintPrompt.prompt;
|
|
63
|
-
break;
|
|
64
|
-
}
|
|
65
|
-
case 'mixed':
|
|
66
|
-
default: {
|
|
67
|
-
// For mixed errors, use test fix prompt (most comprehensive)
|
|
68
|
-
const mixedPrompt = buildTestFixPrompt({
|
|
69
|
-
componentCode,
|
|
70
|
-
testCode,
|
|
71
|
-
failures: [ctx.errorFeedback],
|
|
72
|
-
testOutput: ctx.errorFeedback,
|
|
73
|
-
});
|
|
74
|
-
system = mixedPrompt.system;
|
|
75
|
-
prompt = mixedPrompt.prompt;
|
|
76
|
-
break;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
const { text } = await generateText({ model, system, prompt });
|
|
80
|
-
ctx.llmCalls++;
|
|
81
|
-
const blocks = extractCodeBlocks(text);
|
|
82
|
-
if (blocks.length >= 2) {
|
|
83
|
-
ctx.componentCode = blocks[0];
|
|
84
|
-
ctx.testCode = blocks[1];
|
|
85
|
-
await writeFile(ctx.componentPath, blocks[0], 'utf-8');
|
|
86
|
-
await writeFile(ctx.testPath, blocks[1], 'utf-8');
|
|
87
|
-
}
|
|
88
|
-
else if (blocks.length === 1) {
|
|
89
|
-
ctx.componentCode = blocks[0];
|
|
90
|
-
await writeFile(ctx.componentPath, blocks[0], 'utf-8');
|
|
91
|
-
}
|
|
92
|
-
return { success: true };
|
|
93
|
-
}
|
|
94
|
-
//# sourceMappingURL=fix-from-feedback.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"fix-from-feedback.js","sourceRoot":"","sources":["../../../../src/pipeline/steps/fix-from-feedback.ts"],"names":[],"mappings":"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"}
|