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