lacuna-cli 0.1.5 → 0.1.7
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/README.md +83 -21
- package/dist/agent/context.d.ts +2 -0
- package/dist/agent/context.d.ts.map +1 -1
- package/dist/agent/context.js +314 -1
- package/dist/agent/context.js.map +1 -1
- package/dist/agent/fix-loop.d.ts +5 -0
- package/dist/agent/fix-loop.d.ts.map +1 -1
- package/dist/agent/fix-loop.js +376 -41
- package/dist/agent/fix-loop.js.map +1 -1
- package/dist/agent/generator.d.ts +8 -1
- package/dist/agent/generator.d.ts.map +1 -1
- package/dist/agent/generator.js +89 -11
- package/dist/agent/generator.js.map +1 -1
- package/dist/agent/loop.d.ts +8 -0
- package/dist/agent/loop.d.ts.map +1 -1
- package/dist/agent/loop.js +108 -46
- package/dist/agent/loop.js.map +1 -1
- package/dist/agent/{prompts.d.ts → prompts/index.d.ts} +12 -2
- package/dist/agent/prompts/index.d.ts.map +1 -0
- package/dist/agent/prompts/index.js +668 -0
- package/dist/agent/prompts/index.js.map +1 -0
- package/dist/agent/prompts/nextjs.d.ts +12 -0
- package/dist/agent/prompts/nextjs.d.ts.map +1 -0
- package/dist/agent/prompts/nextjs.js +138 -0
- package/dist/agent/prompts/nextjs.js.map +1 -0
- package/dist/agent/prompts/react-native.d.ts +4 -0
- package/dist/agent/prompts/react-native.d.ts.map +1 -0
- package/dist/agent/prompts/react-native.js +82 -0
- package/dist/agent/prompts/react-native.js.map +1 -0
- package/dist/agent/prompts/react.d.ts +2 -0
- package/dist/agent/prompts/react.d.ts.map +1 -0
- package/dist/agent/prompts/react.js +48 -0
- package/dist/agent/prompts/react.js.map +1 -0
- package/dist/agent/prompts/runners/js-common.d.ts +2 -0
- package/dist/agent/prompts/runners/js-common.d.ts.map +1 -0
- package/dist/agent/prompts/runners/js-common.js +13 -0
- package/dist/agent/prompts/runners/js-common.js.map +1 -0
- package/dist/agent/prompts/runners/typescript.d.ts +2 -0
- package/dist/agent/prompts/runners/typescript.d.ts.map +1 -0
- package/dist/agent/prompts/runners/typescript.js +12 -0
- package/dist/agent/prompts/runners/typescript.js.map +1 -0
- package/dist/agent/prompts/runners/vitest.d.ts +2 -0
- package/dist/agent/prompts/runners/vitest.d.ts.map +1 -0
- package/dist/agent/prompts/runners/vitest.js +23 -0
- package/dist/agent/prompts/runners/vitest.js.map +1 -0
- package/dist/agent/prompts/vue.d.ts +3 -0
- package/dist/agent/prompts/vue.d.ts.map +1 -0
- package/dist/agent/prompts/vue.js +29 -0
- package/dist/agent/prompts/vue.js.map +1 -0
- package/dist/commands/analyze.d.ts.map +1 -1
- package/dist/commands/analyze.js +43 -32
- package/dist/commands/analyze.js.map +1 -1
- package/dist/commands/fix.d.ts +2 -0
- package/dist/commands/fix.d.ts.map +1 -1
- package/dist/commands/fix.js +32 -3
- package/dist/commands/fix.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +208 -32
- package/dist/commands/init.js.map +1 -1
- package/dist/lib/config.d.ts +3 -3
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +3 -1
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/coverage/gaps.d.ts +2 -2
- package/dist/lib/coverage/gaps.d.ts.map +1 -1
- package/dist/lib/coverage/gaps.js +35 -5
- package/dist/lib/coverage/gaps.js.map +1 -1
- package/dist/lib/coverage/index.d.ts +1 -1
- package/dist/lib/coverage/index.d.ts.map +1 -1
- package/dist/lib/coverage/index.js +1 -1
- package/dist/lib/coverage/index.js.map +1 -1
- package/dist/lib/detector.d.ts +1 -0
- package/dist/lib/detector.d.ts.map +1 -1
- package/dist/lib/detector.js +41 -8
- package/dist/lib/detector.js.map +1 -1
- package/dist/lib/providers/anthropic.d.ts.map +1 -1
- package/dist/lib/providers/anthropic.js +46 -3
- package/dist/lib/providers/anthropic.js.map +1 -1
- package/dist/lib/providers/openai-compatible.d.ts +1 -1
- package/dist/lib/providers/openai-compatible.d.ts.map +1 -1
- package/dist/lib/providers/openai-compatible.js +43 -2
- package/dist/lib/providers/openai-compatible.js.map +1 -1
- package/dist/lib/providers/types.d.ts +4 -0
- package/dist/lib/providers/types.d.ts.map +1 -1
- package/dist/lib/providers/types.js +10 -0
- package/dist/lib/providers/types.js.map +1 -1
- package/dist/lib/skeleton.d.ts +4 -0
- package/dist/lib/skeleton.d.ts.map +1 -1
- package/dist/lib/skeleton.js +220 -0
- package/dist/lib/skeleton.js.map +1 -1
- package/dist/lib/validate.d.ts +1 -0
- package/dist/lib/validate.d.ts.map +1 -1
- package/dist/lib/validate.js +179 -15
- package/dist/lib/validate.js.map +1 -1
- package/dist/lib/worker-display.d.ts +10 -0
- package/dist/lib/worker-display.d.ts.map +1 -1
- package/dist/lib/worker-display.js +53 -5
- package/dist/lib/worker-display.js.map +1 -1
- package/oclif.manifest.json +16 -2
- package/package.json +1 -1
- package/dist/agent/prompts.d.ts.map +0 -1
- package/dist/agent/prompts.js +0 -632
- package/dist/agent/prompts.js.map +0 -1
- package/dist/lib/report-upload.d.ts +0 -3
- package/dist/lib/report-upload.d.ts.map +0 -1
- package/dist/lib/report-upload.js +0 -15
- package/dist/lib/report-upload.js.map +0 -1
package/dist/agent/prompts.js
DELETED
|
@@ -1,632 +0,0 @@
|
|
|
1
|
-
import { buildSourceSkeleton, shouldUseSkeleton } from '../lib/skeleton.js';
|
|
2
|
-
// Extracts module names that are already globally mocked in the setup file.
|
|
3
|
-
// Used to tell the agent "don't mock these again" to avoid double-mock conflicts.
|
|
4
|
-
function extractGlobalNextMocks(setupCode) {
|
|
5
|
-
const mocked = [];
|
|
6
|
-
for (const m of setupCode.matchAll(/vi\.mock\(['"]([^'"]+)['"]/g)) {
|
|
7
|
-
mocked.push(m[1]);
|
|
8
|
-
}
|
|
9
|
-
return [...new Set(mocked)];
|
|
10
|
-
}
|
|
11
|
-
function analyzeNextJs(sourceCode) {
|
|
12
|
-
const hasNavigation = /from\s+['"]next\/navigation['"]/.test(sourceCode);
|
|
13
|
-
const hasHeaders = /from\s+['"]next\/headers['"]/.test(sourceCode);
|
|
14
|
-
const hasCache = /from\s+['"]next\/cache['"]/.test(sourceCode);
|
|
15
|
-
const clientModules = [];
|
|
16
|
-
for (const m of sourceCode.matchAll(/from\s+['"]([^'"]+\.client)['"]/g))
|
|
17
|
-
clientModules.push(m[1]);
|
|
18
|
-
const serverModules = [];
|
|
19
|
-
for (const m of sourceCode.matchAll(/from\s+['"]([^'"]+\.server)['"]/g))
|
|
20
|
-
serverModules.push(m[1]);
|
|
21
|
-
const sessionProviders = [];
|
|
22
|
-
for (const m of sourceCode.matchAll(/from\s+['"]([^'"]*(?:session|auth)[^'"]*)['"]/gi)) {
|
|
23
|
-
const p = m[1];
|
|
24
|
-
if (!p.startsWith('next-auth') && !p.startsWith('@auth'))
|
|
25
|
-
sessionProviders.push(p);
|
|
26
|
-
}
|
|
27
|
-
return { hasNavigation, hasHeaders, hasCache, clientModules, serverModules, sessionProviders };
|
|
28
|
-
}
|
|
29
|
-
function buildNextJsGuidance(a) {
|
|
30
|
-
if (!a.hasNavigation && !a.hasHeaders && !a.hasCache && !a.clientModules.length && !a.serverModules.length && !a.sessionProviders.length)
|
|
31
|
-
return null;
|
|
32
|
-
const lines = [
|
|
33
|
-
'NEXT.JS MOCKING (critical — Vitest cannot resolve these modules without explicit mocks):',
|
|
34
|
-
];
|
|
35
|
-
if (a.hasNavigation) {
|
|
36
|
-
lines.push("Mock next/navigation — required for any component that uses useRouter, usePathname, useSearchParams, or useParams (skip if already listed as globally mocked above):", " vi.mock('next/navigation', () => ({", " useRouter: vi.fn(() => ({ push: vi.fn(), replace: vi.fn(), back: vi.fn(), forward: vi.fn() })),", " usePathname: vi.fn(() => '/'),", " useSearchParams: vi.fn(() => new URLSearchParams()),", " useParams: vi.fn(() => ({})),", " redirect: vi.fn(),", " }))");
|
|
37
|
-
}
|
|
38
|
-
if (a.hasHeaders) {
|
|
39
|
-
lines.push("Mock next/headers — it is server-only and will throw in Vitest:", " vi.mock('next/headers', () => ({", " cookies: vi.fn(() => ({ get: vi.fn(), set: vi.fn(), delete: vi.fn(), has: vi.fn() })),", " headers: vi.fn(() => new Headers()),", " }))");
|
|
40
|
-
}
|
|
41
|
-
if (a.hasCache) {
|
|
42
|
-
lines.push("Mock next/cache — revalidatePath, revalidateTag, unstable_cache are no-ops in tests:", " vi.mock('next/cache', () => ({", " revalidatePath: vi.fn(),", " revalidateTag: vi.fn(),", " unstable_cache: vi.fn((fn: () => unknown) => fn),", " }))");
|
|
43
|
-
}
|
|
44
|
-
if (a.clientModules.length > 0) {
|
|
45
|
-
lines.push(`The source imports .client boundary files that Vitest cannot resolve: ${a.clientModules.join(', ')}`, "Mock each one by its EXACT import path as it appears in the source file. Export every function the source uses as a vi.fn():", ...a.clientModules.map(p => ` vi.mock('${p}', () => ({ /* each exported function: myFn: vi.fn() */ }))`), "Do not try to import the real .client file — it will always fail in Vitest.");
|
|
46
|
-
}
|
|
47
|
-
if (a.serverModules.length > 0) {
|
|
48
|
-
lines.push(`The source imports .server boundary files that Vitest cannot resolve: ${a.serverModules.join(', ')}`, "Mock each one the same way as .client files:", ...a.serverModules.map(p => ` vi.mock('${p}', () => ({ /* each exported function: myFn: vi.fn() */ }))`));
|
|
49
|
-
}
|
|
50
|
-
if (a.sessionProviders.length > 0) {
|
|
51
|
-
lines.push(`The source imports session/auth providers: ${a.sessionProviders.join(', ')}`, "Mock them and return a controlled session object:", ` vi.mock('${a.sessionProviders[0]}', () => ({`, " useSession: vi.fn(() => ({ user: { id: 'user-1', email: 'test@example.com' }, status: 'authenticated' })),", " getSession: vi.fn(() => Promise.resolve({ user: { id: 'user-1' } })),", " }))");
|
|
52
|
-
}
|
|
53
|
-
lines.push("If Vitest still reports 'Failed to resolve import', the mock path does not exactly match the import in the source. Copy it character-for-character.");
|
|
54
|
-
return lines.join('\n');
|
|
55
|
-
}
|
|
56
|
-
// ─── React Native detector & guidance ───────────────────────────────────────
|
|
57
|
-
function detectReactNative(packageDeps) {
|
|
58
|
-
if (!packageDeps)
|
|
59
|
-
return false;
|
|
60
|
-
return /\breact-native\b/.test(packageDeps) || /\bexpo\b/.test(packageDeps);
|
|
61
|
-
}
|
|
62
|
-
function buildReactNativeGuidance() {
|
|
63
|
-
return [
|
|
64
|
-
'REACT NATIVE PROJECT — use @testing-library/react-native, not @testing-library/react.',
|
|
65
|
-
"- Import render, screen, fireEvent, waitFor from '@testing-library/react-native'",
|
|
66
|
-
'- No document, window, or localStorage globals — they do not exist in React Native',
|
|
67
|
-
"- Mock react-native modules with vi.mock('react-native', ...) or jest.mock('react-native', ...)",
|
|
68
|
-
'- Mock navigation: if @react-navigation/native is used, mock useNavigation, useRoute',
|
|
69
|
-
'- Mock expo-router if present: mock useRouter, useLocalSearchParams, Link',
|
|
70
|
-
'- Use fireEvent.press() not fireEvent.click() for Pressable/TouchableOpacity',
|
|
71
|
-
'- Async state: use waitFor() from @testing-library/react-native, not from @testing-library/react',
|
|
72
|
-
].join('\n');
|
|
73
|
-
}
|
|
74
|
-
// ─── Vue detector & guidance ─────────────────────────────────────────────────
|
|
75
|
-
function detectVue(packageDeps) {
|
|
76
|
-
if (!packageDeps)
|
|
77
|
-
return false;
|
|
78
|
-
return /\bvue\b/.test(packageDeps);
|
|
79
|
-
}
|
|
80
|
-
function buildVueGuidance() {
|
|
81
|
-
return [
|
|
82
|
-
'VUE PROJECT — use @testing-library/vue for component tests.',
|
|
83
|
-
"- Import render, screen, fireEvent, waitFor from '@testing-library/vue'",
|
|
84
|
-
"- Import userEvent from '@testing-library/user-event'",
|
|
85
|
-
'- Wrap async user interactions with await userEvent.setup() and await act()',
|
|
86
|
-
].join('\n');
|
|
87
|
-
}
|
|
88
|
-
// Matches both directory-level patterns (/api/, /services/) and file-level names
|
|
89
|
-
// (../../lib/api, ../apiClient, ./httpService) so axios instance files aren't missed.
|
|
90
|
-
const API_IMPORT_RE = /\/(?:api|services?|requests?|http|client|network)\/|\/(?:api|axios|http|request)(?:Client|Config|Instance|Service|Helper)?(?:\/|$)|[/.]api(?:[./]|$)/i;
|
|
91
|
-
function analyzeNetworkDeps(sourceCode) {
|
|
92
|
-
const usesAxios = /\baxios\b/.test(sourceCode);
|
|
93
|
-
const usesFetch = /\bfetch\s*\(/.test(sourceCode);
|
|
94
|
-
const usesCustomInstance = /axios\.create\s*\(/.test(sourceCode);
|
|
95
|
-
// Collect local imports from paths that look like API/service modules
|
|
96
|
-
const apiModuleImports = [];
|
|
97
|
-
for (const m of sourceCode.matchAll(/from\s+['"](\.[^'"]+)['"]/g)) {
|
|
98
|
-
const path = m[1];
|
|
99
|
-
if (API_IMPORT_RE.test(path)) {
|
|
100
|
-
apiModuleImports.push(path);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
return { usesAxios, usesFetch, usesCustomInstance, apiModuleImports };
|
|
104
|
-
}
|
|
105
|
-
// Surfaces TypeScript compiler errors in a form the model can act on directly.
|
|
106
|
-
// Philosophy: tsc output is already the fix instruction — our job is to make sure
|
|
107
|
-
// the model reads it literally rather than overriding it with framework conventions.
|
|
108
|
-
// Two layers:
|
|
109
|
-
// 1. Extract structured info from the error text for the highest-value patterns
|
|
110
|
-
// (member lists, suggestions, mismatched types) so the model doesn't have to hunt.
|
|
111
|
-
// 2. Generic pass-through for everything else — the compiler already said what's wrong.
|
|
112
|
-
function detectTypeScriptErrors(errorOutput) {
|
|
113
|
-
if (!/error TS\d+:/.test(errorOutput))
|
|
114
|
-
return null;
|
|
115
|
-
const parts = [
|
|
116
|
-
'TYPESCRIPT ERRORS — treat each compiler message as an exact instruction, not a hint:',
|
|
117
|
-
'The TypeScript compiler tells you precisely what is wrong and usually what the fix is.',
|
|
118
|
-
'Do NOT override it with framework conventions or assumptions.',
|
|
119
|
-
];
|
|
120
|
-
// TS1378: top-level await — only specific branch because the fix is structural
|
|
121
|
-
if (/TS1378/.test(errorOutput)) {
|
|
122
|
-
parts.push('• Top-level await (TS1378): move ALL await calls inside it()/test()/beforeEach()/etc.', ' WRONG: const result = await fn();', ' RIGHT: it("desc", async () => { const result = await fn(); });');
|
|
123
|
-
}
|
|
124
|
-
// Wrong member name — extract the actual member list TypeScript printed inline.
|
|
125
|
-
// Covers TS2339, TS2551, TS2561 and any variant with "does not exist on/in type '{...}'"
|
|
126
|
-
const propErrors = [...errorOutput.matchAll(/'(\w+)' does not exist (?:on|in) type '\{([^']+)\}'/g)];
|
|
127
|
-
if (propErrors.length > 0) {
|
|
128
|
-
parts.push('• Wrong member name — the actual available members are:');
|
|
129
|
-
const seen = new Set();
|
|
130
|
-
for (const m of propErrors) {
|
|
131
|
-
const wrongProp = m[1];
|
|
132
|
-
const available = [...m[2].matchAll(/(\w+)\s*[?]?\s*:/g)].map(p => p[1]).filter(p => p !== 'type');
|
|
133
|
-
const key = wrongProp + available.join();
|
|
134
|
-
if (seen.has(key))
|
|
135
|
-
continue;
|
|
136
|
-
seen.add(key);
|
|
137
|
-
parts.push(` '${wrongProp}' → not valid. Use one of: ${available.slice(0, 12).join(', ')}${available.length > 12 ? ' …' : ''}`);
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
// Compiler suggestions — TypeScript already provides the answer
|
|
141
|
-
const suggestions = [...new Set([...errorOutput.matchAll(/Did you mean(?: to write)? '(\w+)'\?/g)].map(m => m[1]))];
|
|
142
|
-
if (suggestions.length > 0) {
|
|
143
|
-
parts.push(`• Compiler suggestion: use ${suggestions.map(s => `'${s}'`).join(', ')}`);
|
|
144
|
-
}
|
|
145
|
-
// Type mismatch — extract what was passed vs what was required
|
|
146
|
-
const typeMismatches = [...errorOutput.matchAll(/Argument of type '([^']+)' is not assignable to parameter of type '([^']+)'/g)];
|
|
147
|
-
if (typeMismatches.length > 0) {
|
|
148
|
-
for (const m of typeMismatches) {
|
|
149
|
-
parts.push(`• Type mismatch: passed '${m[1].slice(0, 80)}', required '${m[2].slice(0, 80)}'`);
|
|
150
|
-
}
|
|
151
|
-
parts.push(' (use null not undefined for nullable values; check TYPE DEFINITIONS for the required shape)');
|
|
152
|
-
}
|
|
153
|
-
// Generic pass-through for all other TS errors — list them so the model reads each one
|
|
154
|
-
// rather than guessing. No special handling needed: the message IS the instruction.
|
|
155
|
-
const otherErrors = [...errorOutput.matchAll(/error (TS(?!1378|2339|2551|2561|2345)\d+): ([^\n]+)/g)];
|
|
156
|
-
if (otherErrors.length > 0) {
|
|
157
|
-
parts.push('• Additional compiler errors — read each one and apply the exact fix it describes:');
|
|
158
|
-
const seen = new Set();
|
|
159
|
-
for (const m of otherErrors) {
|
|
160
|
-
const msg = `${m[1]}: ${m[2].slice(0, 120)}`;
|
|
161
|
-
if (seen.has(msg))
|
|
162
|
-
continue;
|
|
163
|
-
seen.add(msg);
|
|
164
|
-
parts.push(` ${msg}`);
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
return parts.join('\n');
|
|
168
|
-
}
|
|
169
|
-
// Detects thinking-bleed parse errors: model wrote reasoning inside <code_output>
|
|
170
|
-
// causing the file to start with prose instead of TypeScript.
|
|
171
|
-
function detectThinkingBleed(errorOutput) {
|
|
172
|
-
// Vitest/esbuild parse errors at line 1 with non-code content are a strong signal
|
|
173
|
-
const parseErr = errorOutput.match(/PARSE_ERROR|Unexpected token|SyntaxError.*\b1:\d+\b/);
|
|
174
|
-
if (!parseErr)
|
|
175
|
-
return null;
|
|
176
|
-
// Check if the error context shows non-TypeScript at the file start
|
|
177
|
-
const contextLine = errorOutput.match(/^\s*1\s*[│|]\s*(.+)/m);
|
|
178
|
-
if (!contextLine)
|
|
179
|
-
return null;
|
|
180
|
-
const firstLine = contextLine[1].trim();
|
|
181
|
-
// If first line looks like prose (no import/export/const/vi./etc.)
|
|
182
|
-
if (/^(import|export|const|let|var|\/\/|\/\*|describe|it\s*\(|test\s*\(|vi\.|jest\.)/.test(firstLine))
|
|
183
|
-
return null;
|
|
184
|
-
return [
|
|
185
|
-
`THINKING BLEED DETECTED — your previous response had reasoning text inside <code_output>.`,
|
|
186
|
-
`The file started with: "${firstLine.slice(0, 80)}"`,
|
|
187
|
-
`This is not valid TypeScript and caused a parse error at line 1.`,
|
|
188
|
-
`RULE: finish ALL reasoning inside <thinking> first. Once <code_output> opens, the very first character must be valid code — an import, function definition, comment (//, #), or similar construct for the project's language.`,
|
|
189
|
-
`Do NOT continue thinking inside <code_output> under any circumstances.`,
|
|
190
|
-
].join('\n');
|
|
191
|
-
}
|
|
192
|
-
// Scans error output for Next.js import resolution failures (Failed to resolve import).
|
|
193
|
-
function detectNextJsImportError(errorOutput) {
|
|
194
|
-
const failedImport = errorOutput.match(/Failed to resolve import "([^"]+)"/);
|
|
195
|
-
if (!failedImport)
|
|
196
|
-
return null;
|
|
197
|
-
const importPath = failedImport[1];
|
|
198
|
-
const isAlias = importPath.startsWith('@/');
|
|
199
|
-
const isClient = importPath.endsWith('.client');
|
|
200
|
-
const isServer = importPath.endsWith('.server');
|
|
201
|
-
const isNextInternal = importPath.startsWith('next/');
|
|
202
|
-
const isProviderOrSession = /session|auth|provider/i.test(importPath);
|
|
203
|
-
if (!isAlias && !isClient && !isServer && !isNextInternal && !isProviderOrSession)
|
|
204
|
-
return null;
|
|
205
|
-
const lines = [
|
|
206
|
-
`IMPORT RESOLUTION ERROR — Vitest cannot resolve "${importPath}".`,
|
|
207
|
-
];
|
|
208
|
-
if (isAlias) {
|
|
209
|
-
lines.push(`The "@/" alias is not configured in vitest.config.ts — you cannot fix this by changing the test file.`, `WORKAROUND: use the pre-computed relative paths from LOCAL IMPORT PATHS in your vi.mock() calls instead of "@/" paths.`, ` WRONG: vi.mock('@/components/ui/button', ...)`, ` CORRECT: vi.mock('../../../components/ui/button', ...) ← use the relative path from LOCAL IMPORT PATHS`, `Do NOT attempt to add resolve aliases inside the test file — that does not work.`, `Do NOT switch import statements to relative paths — only vi.mock() calls need to change.`);
|
|
210
|
-
}
|
|
211
|
-
else if (isClient || isServer) {
|
|
212
|
-
lines.push(`This is a Next.js ${isClient ? 'client' : 'server'} boundary file. Vitest never resolves these — mock it with the exact import path from the source file:`, ` vi.mock('${importPath}', () => ({`, ` // each function the source imports: myFn: vi.fn()`, ` }))`);
|
|
213
|
-
}
|
|
214
|
-
else if (isNextInternal) {
|
|
215
|
-
lines.push(`${importPath} is a Next.js internal that does not work in Vitest. Mock it:`, ` vi.mock('${importPath}', () => ({ /* relevant exports as vi.fn() */ }))`);
|
|
216
|
-
}
|
|
217
|
-
else if (isProviderOrSession) {
|
|
218
|
-
lines.push(`This looks like a session or auth provider that Vitest cannot resolve. Mock it:`, ` vi.mock('${importPath}', () => ({`, ` useSession: vi.fn(() => ({ user: { id: 'user-1', email: 'test@example.com' }, status: 'authenticated' })),`, ` }))`);
|
|
219
|
-
}
|
|
220
|
-
return lines.join('\n');
|
|
221
|
-
}
|
|
222
|
-
// Scans error output for an unhandled rejection caused by mockRejectedValueOnce.
|
|
223
|
-
// Vitest surfaces this as a top-level "Unhandled Rejection" or "Vitest caught N unhandled error(s)"
|
|
224
|
-
// even when the component catches the error internally — the test never awaited the error state.
|
|
225
|
-
function detectUnhandledRejection(errorOutput) {
|
|
226
|
-
const hasUnhandled = /unhandled\s+(promise\s+)?rejection|vitest caught \d+ unhandled/i.test(errorOutput);
|
|
227
|
-
const hasRejectedMock = /mockRejectedValue(Once)?/.test(errorOutput);
|
|
228
|
-
if (!hasUnhandled && !hasRejectedMock)
|
|
229
|
-
return null;
|
|
230
|
-
return [
|
|
231
|
-
'UNHANDLED REJECTION DETECTED — a mockRejectedValueOnce (or mockRejectedValue) promise is escaping the test scope.',
|
|
232
|
-
'The component may catch the error internally, but Vitest still requires the rejection to be resolved inside the test.',
|
|
233
|
-
'Required fix: after the action that triggers the rejection, await the resulting error state:',
|
|
234
|
-
" await waitFor(() => expect(screen.getByText(/error text/i)).toBeInTheDocument())",
|
|
235
|
-
'This chains the rejection inside the test scope. Without it, Vitest flags it as unhandled even if the UI handles it correctly.',
|
|
236
|
-
].join('\n');
|
|
237
|
-
}
|
|
238
|
-
// Scans error output for signs that a real HTTP request leaked through.
|
|
239
|
-
function detectRealRequestInError(errorOutput) {
|
|
240
|
-
const hasRealUrl = /https?:\/\/[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/.test(errorOutput);
|
|
241
|
-
const hasHttpStatus = /\bstatus:\s*[45]\d\d\b/.test(errorOutput);
|
|
242
|
-
const hasNetworkError = /ECONNREFUSED|ENOTFOUND|ETIMEDOUT|network\s+error/i.test(errorOutput);
|
|
243
|
-
if (!hasRealUrl && !hasHttpStatus && !hasNetworkError)
|
|
244
|
-
return null;
|
|
245
|
-
const lines = [
|
|
246
|
-
'REAL HTTP REQUEST DETECTED — the test is hitting the actual network. This is the root cause of the failure.',
|
|
247
|
-
'A mock is either missing entirely or applied at the wrong level. Fix this before anything else.',
|
|
248
|
-
];
|
|
249
|
-
const urlMatch = errorOutput.match(/https?:\/\/[^\s,'")\]}]+/);
|
|
250
|
-
if (urlMatch)
|
|
251
|
-
lines.push(`Intercepted URL: ${urlMatch[0]}`);
|
|
252
|
-
lines.push('Required fix: find which module the source file imports for its API calls and mock THAT module.', "vi.mock('axios') does NOT intercept axios.create() instances — you must mock the module that exports the instance or the service layer above it.");
|
|
253
|
-
return lines.join('\n');
|
|
254
|
-
}
|
|
255
|
-
function buildNetworkMockingGuidance(analysis, sourceFile) {
|
|
256
|
-
if (!analysis.usesAxios && !analysis.usesFetch && analysis.apiModuleImports.length === 0) {
|
|
257
|
-
return null;
|
|
258
|
-
}
|
|
259
|
-
const lines = [
|
|
260
|
-
'NETWORK MOCKING (critical — a real HTTP request reaching the network is a test bug):',
|
|
261
|
-
];
|
|
262
|
-
if (analysis.apiModuleImports.length > 0) {
|
|
263
|
-
lines.push(`The source file imports from API/service modules: ${analysis.apiModuleImports.join(', ')}`, `Mock THOSE modules, not the underlying HTTP client:`, ` vi.mock('${analysis.apiModuleImports[0]}', () => ({ myFn: vi.fn() }))`, `This is the most reliable approach — it intercepts at the contract boundary regardless of which HTTP client is used underneath.`);
|
|
264
|
-
}
|
|
265
|
-
if (analysis.usesCustomInstance) {
|
|
266
|
-
lines.push(`The source creates a custom axios instance (axios.create()). vi.mock('axios') alone WILL NOT intercept calls made through a custom instance.`, `Instead: mock the module that exports the axios instance, or mock the API service module that wraps it.`);
|
|
267
|
-
}
|
|
268
|
-
else if (analysis.usesAxios && analysis.apiModuleImports.length === 0) {
|
|
269
|
-
lines.push(`The source imports axios directly. Mock it with: vi.mock('axios') and set return values with axios.get.mockResolvedValue({ data: ... })`);
|
|
270
|
-
}
|
|
271
|
-
if (analysis.usesFetch) {
|
|
272
|
-
lines.push(`The source uses fetch. Mock it with: vi.spyOn(global, 'fetch').mockResolvedValue(new Response(JSON.stringify(data)))`);
|
|
273
|
-
}
|
|
274
|
-
lines.push(`If you see a real URL (e.g. https://...) or a 401/403/network error in the test output, your mock is missing or at the wrong module level. Fix it before the test can pass.`);
|
|
275
|
-
return lines.join('\n');
|
|
276
|
-
}
|
|
277
|
-
// Extract exported names from a mocks file so the AI sees a concrete inventory.
|
|
278
|
-
function parseMockExports(code) {
|
|
279
|
-
const names = [];
|
|
280
|
-
// export const/let/var/function/class/async function name
|
|
281
|
-
for (const m of code.matchAll(/^export\s+(?:const|let|var|function|class|async\s+function)\s+(\w+)/gm)) {
|
|
282
|
-
names.push(m[1]);
|
|
283
|
-
}
|
|
284
|
-
// export { name1, name2 as alias2, ... }
|
|
285
|
-
for (const m of code.matchAll(/^export\s*\{([^}]+)\}/gm)) {
|
|
286
|
-
for (const part of m[1].split(',')) {
|
|
287
|
-
const alias = part.trim().split(/\s+as\s+/).pop()?.trim();
|
|
288
|
-
if (alias && /^\w+$/.test(alias))
|
|
289
|
-
names.push(alias);
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
// export default identifier
|
|
293
|
-
const defM = code.match(/^export\s+default\s+(\w+)/m);
|
|
294
|
-
if (defM)
|
|
295
|
-
names.push(`default (${defM[1]})`);
|
|
296
|
-
return [...new Set(names)];
|
|
297
|
-
}
|
|
298
|
-
export function buildSystemPrompt(env) {
|
|
299
|
-
return `You are a senior QA engineer with 10+ years of experience writing production test suites for ${env.language} projects. You use ${env.testRunner} and you take testing seriously.
|
|
300
|
-
|
|
301
|
-
Your tests catch real bugs. You think about what could go wrong — null inputs, empty arrays, async race conditions, error boundaries, permission checks, off-by-one errors — and you write assertions that would actually fail if the code broke. You never write a test just to hit a coverage number.
|
|
302
|
-
|
|
303
|
-
RULES — follow every one:
|
|
304
|
-
1. Write tests that verify real behavior: correctness, edge cases, boundary values, and error handling. Never write empty or trivial assertions (e.g. expect(true).toBe(true)).
|
|
305
|
-
2. Match the EXACT import style shown in the existing test file or PROJECT TEST EXAMPLES. If none exists, use the style from the source file.
|
|
306
|
-
3. Use path aliases from the PROJECT TYPESCRIPT CONFIG section in IMPORT statements (e.g. "@/components/Button" not "../../components/Button").
|
|
307
|
-
EXCEPTION — vi.mock() call paths: use the exact same path string that appears in the SOURCE FILE'S import statement.
|
|
308
|
-
If a LOCAL IMPORT PATHS section is provided, use those pre-computed relative paths in vi.mock() calls — they are the fallback when aliases cannot be resolved by Vitest.
|
|
309
|
-
Never second-guess the pre-computed paths. Never convert them back to @/ aliases in vi.mock() calls.
|
|
310
|
-
4. Only import from packages listed in PROJECT DEPENDENCIES. Do not invent packages that are not listed.
|
|
311
|
-
5. When a SHARED MOCK FILE is provided, its exported names are listed under "Available exports". Before writing the test, go through that list and identify every mock that relates to what the source file does (by name, type, or domain). Import and use ALL of those mocks. Never re-create inline vi.fn() / jest.fn() for anything already exported from the mocks file.
|
|
312
|
-
CRITICAL — never rename or change the casing of existing mock exports. If the mocks file has mockValidationError, use exactly mockValidationError — do NOT change it to MockValidationError or any other variant. Renaming an existing mock breaks every test that already imports it by the original name.
|
|
313
|
-
6. If you need a mock that is missing from the shared mock file, add it to that file AND import it in the test. Return BOTH files separated by exactly one line containing only: // ---MOCKS_FILE---
|
|
314
|
-
7. If a SHARED MOCK FILE (does not exist yet) section is shown — create it for any mocks you need and return it using the // ---MOCKS_FILE--- separator.
|
|
315
|
-
CRITICAL — the mocks file must contain ONLY: vi.fn()/jest.fn() mock definitions, vi.mock() module stubs, shared mock objects/constants, and beforeEach reset hooks. NEVER write describe(), it(), test(), or expect() calls in the mocks file. Those belong exclusively in the test file. A mocks file that contains test blocks will break the entire test suite.
|
|
316
|
-
8. If a TEST SETUP FILE is shown, assume its globals and matchers are already available (e.g. expect(...).toBeInTheDocument()). Do NOT import or re-declare them.
|
|
317
|
-
9. TypeScript type safety: all test code must compile without type errors. The TypeScript compiler is the authority — its error messages are exact instructions, not hints.
|
|
318
|
-
- When tsc reports an error, read it literally and apply the precise fix it describes. Never override the compiler with framework conventions or assumptions.
|
|
319
|
-
- Use EXACT property/member names from the TYPE DEFINITIONS section and source code. Do not apply naming conventions: if a hook returns { loading, users }, do NOT write isLoading or isUsers. Read what is actually declared.
|
|
320
|
-
- Import enums, constants, interfaces, and types from the project's existing files. Do NOT redeclare them inline or invent lookalike values. Check TYPE DEFINITIONS and the source file's imports for what already exists.
|
|
321
|
-
- Never use "as any", "@ts-ignore", or "@ts-expect-error" to suppress type errors. Use proper types or generics instead.
|
|
322
|
-
- For vi.fn() / jest.fn(), either let TypeScript infer the type from context or type it explicitly: vi.fn<Parameters<typeof fn>, ReturnType<typeof fn>>().
|
|
323
|
-
- Use ReturnType<>, Parameters<>, and other utility types to derive mock types from the real function signatures rather than guessing.
|
|
324
|
-
- When accessing optional properties, handle null/undefined correctly — do not assume they exist if the type says otherwise.
|
|
325
|
-
10. Every test file MUST contain at least one it() or test() call with real assertions. A file with only imports, describe() blocks, types, or helper functions is invalid and will be rejected. If you cannot write meaningful tests, write a minimal test that exercises the simplest exported function.
|
|
326
|
-
11. Structure ALL output using exactly these two XML blocks — nothing before, nothing after:
|
|
327
|
-
<thinking>
|
|
328
|
-
1. WHAT IS NEEDED: What functions/behaviors are untested or broken?
|
|
329
|
-
2. COMPONENT RENDER MAP (React components only): Before writing any assertion, list what is in the DOM in each relevant state (idle / loading / error / success). Read the JSX — check every ternary, &&, and switch — to determine whether a button is disabled vs unmounted, what text changes, what elements appear. Never assume a button is disabled during loading without verifying this in the JSX.
|
|
330
|
-
3. WHY IT FAILED (retries only): What is the structural root cause — wrong mock level, missing await, bad import path, type mismatch?
|
|
331
|
-
4. PLAN: List the exact steps you will take before writing a single line of code.
|
|
332
|
-
</thinking>
|
|
333
|
-
<code_output>
|
|
334
|
-
// complete test file here
|
|
335
|
-
</code_output>
|
|
336
|
-
CRITICAL: Once you open <code_output>, ALL remaining output must be code. Never continue reasoning, writing bullet points,
|
|
337
|
-
or restating the problem inside <code_output>. If you are still uncertain, finish your reasoning inside <thinking> first,
|
|
338
|
-
then commit to a solution and write only code inside <code_output>. Thinking inside <code_output> corrupts the output file.
|
|
339
|
-
12. Inside <code_output>: do NOT wrap in markdown code fences.
|
|
340
|
-
13. Inside <code_output>: output ONLY the test file content (or test file // ---MOCKS_FILE--- mocks file).
|
|
341
|
-
If you use // ---MOCKS_FILE---, everything AFTER the separator is the mocks file. The mocks file must contain ONLY:
|
|
342
|
-
vi.fn()/jest.fn() mock definitions, vi.mock() module stubs, shared constants, and beforeEach resets.
|
|
343
|
-
NEVER put describe(), it(), test(), or expect() calls after the separator — those belong BEFORE it, in the test file.
|
|
344
|
-
Writing test blocks in the mocks section corrupts the shared mock file for every other test in the project.
|
|
345
|
-
14. NEVER output vitest.config.ts, jest.config.js, or any framework configuration. If an import cannot be resolved,
|
|
346
|
-
fix it by mocking it with vi.mock() — NOT by modifying the test runner configuration. You cannot modify config
|
|
347
|
-
files from here, and outputting them will cause the entire mocks file to be discarded.
|
|
348
|
-
|
|
349
|
-
A good test suite you write will have:
|
|
350
|
-
- A happy-path test that confirms the main behavior works
|
|
351
|
-
- At least one edge-case test per function (empty input, zero, null, boundary values)
|
|
352
|
-
- Error-path tests for any function that throws, rejects, or returns an error state
|
|
353
|
-
- Async tests properly awaited — never fire-and-forget
|
|
354
|
-
- Clear, descriptive test names that read like a spec ("returns null when user is not authenticated")
|
|
355
|
-
|
|
356
|
-
Common failure causes to avoid:
|
|
357
|
-
- Wrong import paths (use aliases, not relative ../../ paths if the project uses aliases)
|
|
358
|
-
- Importing from test utils that are not in the dependency list
|
|
359
|
-
- Mocking modules that are already mocked in the setup file
|
|
360
|
-
- Using browser globals without jsdom (only use them if the setup file configures jsdom)
|
|
361
|
-
- Forgetting to await async functions
|
|
362
|
-
- Top-level await (TS1378): NEVER use \`await\` at the top level of a test file. Every \`await\` must be inside an async callback: \`it("...", async () => { ... })\`, \`beforeEach(async () => { ... })\`, etc. Top-level \`await\` is rejected by most TypeScript configurations and will cause a type error even if the tests appear to run.
|
|
363
|
-
- React 18 act() async rule: ALWAYS await act() when it wraps async code — \`await act(async () => { ... })\`. Never store an unawaited act() call in a variable like \`const promise = act(async () => ...)\` without immediately awaiting it. Unawaited act() calls cause React state updates to leak into subsequent tests, producing cascading timeout failures and "Cannot read properties of null" errors in unrelated tests.
|
|
364
|
-
- vi.mock() paths are relative to the TEST FILE, not the source file. If the test is at src/features/auth/__tests__/Login.test.tsx and you need to mock src/components/Button, the mock path is ../../../components/Button — count directories from the TEST file's location, not the source file's.
|
|
365
|
-
- Loading state architecture: before asserting that a button is disabled during loading, check whether the component hides the button entirely and replaces it with a spinner. If the button is unmounted during loading rather than disabled, \`getByText("Submit")\` will throw — test for the spinner instead, or use \`queryByText("Submit")\` with a null assertion.
|
|
366
|
-
- Unhandled promise rejections: when testing error paths with mockRejectedValueOnce, the rejection must be fully resolved inside the test. After triggering the action, always use await waitFor(() => expect(errorElement).toBeInTheDocument()) to tie the rejection to the test scope. Never let a rejected mock promise go unawaited — Vitest will flag it as an unhandled error even if the component catches it internally.
|
|
367
|
-
- Real HTTP requests: NEVER let a real network call reach the internet. If you see a real URL (https://...), a 401/403 error, or a network timeout in test output, your mock is missing or at the wrong level. Every function that calls an API must be mocked before the test runs.
|
|
368
|
-
- Barrel file vi.mock() resolution: if a module is exported from a barrel/index file (e.g. src/components/index.ts re-exports Foo from ./Foo), mock the DIRECT file path, not the barrel. vi.mock('../components') mocks the barrel but the component may import directly from '../components/Foo' — making the mock miss. Always mock the specific module the source file actually imports. If unsure, mock both the direct file AND the barrel.
|
|
369
|
-
|
|
370
|
-
Test file pattern for this project: ${env.testFilePattern}
|
|
371
|
-
|
|
372
|
-
You MUST wrap your reasoning inside <thinking> tags and your complete file output inside <code_output> tags. Do not output anything outside of these two tags.`;
|
|
373
|
-
}
|
|
374
|
-
export function buildGeneratePrompt(args) {
|
|
375
|
-
const { sourceFile, sourceCode, existingTestCode, uncoveredFunctions, uncoveredLines, sourceImportPath, mocksCode, mocksImportPath, setupFileCode, packageDeps, tsconfigPaths, typeDefinitions, localImportPaths, reactMajorVersion, projectMemory, } = args;
|
|
376
|
-
const parts = [];
|
|
377
|
-
if (projectMemory) {
|
|
378
|
-
parts.push(projectMemory);
|
|
379
|
-
parts.push('');
|
|
380
|
-
}
|
|
381
|
-
if (packageDeps) {
|
|
382
|
-
parts.push('PROJECT DEPENDENCIES (only import from these):');
|
|
383
|
-
parts.push('```');
|
|
384
|
-
parts.push(packageDeps);
|
|
385
|
-
parts.push('```');
|
|
386
|
-
}
|
|
387
|
-
if (reactMajorVersion !== null && reactMajorVersion !== undefined && reactMajorVersion >= 18) {
|
|
388
|
-
parts.push(`\nREACT ${reactMajorVersion} DETECTED — act() async rule: every act(async () => { ... }) call MUST be awaited. Never assign an unawaited act() to a variable. Unawaited act() leaks state updates into subsequent tests, causing cascading failures and null-read errors in unrelated tests.`);
|
|
389
|
-
}
|
|
390
|
-
if (tsconfigPaths) {
|
|
391
|
-
parts.push('\nPROJECT TYPESCRIPT CONFIG (strict flags, target, and path aliases — follow these exactly):');
|
|
392
|
-
parts.push(tsconfigPaths);
|
|
393
|
-
}
|
|
394
|
-
if (localImportPaths && localImportPaths.length > 0) {
|
|
395
|
-
parts.push('\nLOCAL IMPORT PATHS (pre-computed relative to the test file — use EXACTLY these strings in vi.mock() calls, even if the source file uses @/ aliases. Vitest resolves vi.mock() paths relative to the test file, not via tsconfig aliases. Do NOT convert these back to @/ paths in vi.mock(). Do NOT recount directory levels yourself.):');
|
|
396
|
-
for (const p of localImportPaths)
|
|
397
|
-
parts.push(` ${p}`);
|
|
398
|
-
}
|
|
399
|
-
if (typeDefinitions) {
|
|
400
|
-
parts.push('\nTYPE DEFINITIONS (exported from files the source imports — use these exact shapes, do NOT invent properties or guess types):');
|
|
401
|
-
parts.push('```typescript');
|
|
402
|
-
parts.push(typeDefinitions);
|
|
403
|
-
parts.push('```');
|
|
404
|
-
}
|
|
405
|
-
if (setupFileCode) {
|
|
406
|
-
const nextMocked = extractGlobalNextMocks(setupFileCode);
|
|
407
|
-
const setupNote = nextMocked.length > 0
|
|
408
|
-
? `\nTEST SETUP FILE (already loaded before every test — do NOT import it again):\nThe following modules are ALREADY mocked globally in this setup file — do NOT add vi.mock() for them in the test: ${nextMocked.join(', ')}`
|
|
409
|
-
: `\nTEST SETUP FILE (already loaded before every test — do NOT import it again):`;
|
|
410
|
-
parts.push(setupNote);
|
|
411
|
-
parts.push('```');
|
|
412
|
-
parts.push(setupFileCode);
|
|
413
|
-
parts.push('```');
|
|
414
|
-
}
|
|
415
|
-
if (mocksImportPath) {
|
|
416
|
-
if (mocksCode) {
|
|
417
|
-
const exports = parseMockExports(mocksCode);
|
|
418
|
-
parts.push(`\nSHARED MOCK FILE (import from: '${mocksImportPath}')`);
|
|
419
|
-
if (exports.length > 0) {
|
|
420
|
-
parts.push(`Available exports: ${exports.join(', ')}`);
|
|
421
|
-
parts.push(`↑ Before writing the test, identify which of these match the source file's domain and import every relevant one. Do NOT create inline mocks for anything already in this list.\n↑ NAMES ARE FROZEN — use each export exactly as spelled above. Never rename, recase, or restructure an existing mock (e.g. do not change mockFoo → MockFoo or const → class). Renaming breaks every other test that imports the original name.`);
|
|
422
|
-
}
|
|
423
|
-
parts.push('```');
|
|
424
|
-
parts.push(mocksCode);
|
|
425
|
-
parts.push('```');
|
|
426
|
-
}
|
|
427
|
-
else {
|
|
428
|
-
parts.push(`\nSHARED MOCK FILE (does not exist yet) — create it if you need mocks, return it via the // ---MOCKS_FILE--- separator. Path: '${mocksImportPath}'\n⚠ Mocks file must contain ONLY vi.fn()/vi.mock() definitions and beforeEach resets — NEVER describe/it/test/expect blocks.`);
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
const networkGuidance = buildNetworkMockingGuidance(analyzeNetworkDeps(sourceCode), sourceFile);
|
|
432
|
-
if (networkGuidance)
|
|
433
|
-
parts.push(`\n${networkGuidance}`);
|
|
434
|
-
const nextGuidance = buildNextJsGuidance(analyzeNextJs(sourceCode));
|
|
435
|
-
if (nextGuidance)
|
|
436
|
-
parts.push(`\n${nextGuidance}`);
|
|
437
|
-
if (detectReactNative(packageDeps ?? null))
|
|
438
|
-
parts.push(`\n${buildReactNativeGuidance()}`);
|
|
439
|
-
else if (detectVue(packageDeps ?? null))
|
|
440
|
-
parts.push(`\n${buildVueGuidance()}`);
|
|
441
|
-
const displaySource = buildSourceSkeleton(sourceCode, uncoveredFunctions);
|
|
442
|
-
const skeletonized = shouldUseSkeleton(sourceCode);
|
|
443
|
-
parts.push(`\nSOURCE FILE: ${sourceFile}${skeletonized ? ' (large file — bodies of already-covered functions collapsed; uncovered functions shown in full)' : ''}`);
|
|
444
|
-
if (sourceImportPath) {
|
|
445
|
-
parts.push(`SOURCE FILE IMPORT PATH: when importing the source in your test file, use exactly: '${sourceImportPath}'`);
|
|
446
|
-
}
|
|
447
|
-
parts.push('```');
|
|
448
|
-
parts.push(displaySource);
|
|
449
|
-
parts.push('```');
|
|
450
|
-
if (existingTestCode) {
|
|
451
|
-
parts.push('\nEXISTING TEST FILE (preserve all existing tests, only add new ones):');
|
|
452
|
-
parts.push('```');
|
|
453
|
-
parts.push(existingTestCode);
|
|
454
|
-
parts.push('```');
|
|
455
|
-
}
|
|
456
|
-
else {
|
|
457
|
-
parts.push('\nNo existing test file — create one from scratch.');
|
|
458
|
-
}
|
|
459
|
-
if (uncoveredFunctions.length > 0) {
|
|
460
|
-
parts.push(`\nUNCOVERED FUNCTIONS (must write tests for these): ${uncoveredFunctions.join(', ')}`);
|
|
461
|
-
}
|
|
462
|
-
if (uncoveredLines.length > 0) {
|
|
463
|
-
parts.push(`\nUNCOVERED LINES: ${uncoveredLines.slice(0, 30).join(', ')}${uncoveredLines.length > 30 ? '…' : ''}`);
|
|
464
|
-
}
|
|
465
|
-
parts.push('\nWrite the complete test file now.');
|
|
466
|
-
return parts.join('\n');
|
|
467
|
-
}
|
|
468
|
-
export function buildFixPrompt(args) {
|
|
469
|
-
const { testFile, testCode, sourceFile, sourceCode, sourceImportPath, errorOutput, mocksCode, mocksImportPath, setupFileCode, packageDeps, tsconfigPaths, typeDefinitions, localImportPaths, reactMajorVersion, projectMemory } = args;
|
|
470
|
-
const parts = [];
|
|
471
|
-
parts.push('Your job is to fix a failing test file. Do NOT rewrite it from scratch — preserve every existing test and only change what is necessary to make them pass.');
|
|
472
|
-
parts.push('');
|
|
473
|
-
if (projectMemory) {
|
|
474
|
-
parts.push(projectMemory);
|
|
475
|
-
parts.push('');
|
|
476
|
-
}
|
|
477
|
-
if (packageDeps) {
|
|
478
|
-
parts.push('PROJECT DEPENDENCIES (only import from these):');
|
|
479
|
-
parts.push('```');
|
|
480
|
-
parts.push(packageDeps);
|
|
481
|
-
parts.push('```');
|
|
482
|
-
}
|
|
483
|
-
if (reactMajorVersion !== null && reactMajorVersion !== undefined && reactMajorVersion >= 18) {
|
|
484
|
-
parts.push(`\nREACT ${reactMajorVersion} DETECTED — act() async rule: every act(async () => { ... }) call MUST be awaited. Never assign an unawaited act() to a variable. Unawaited act() leaks state updates into subsequent tests, causing cascading failures and null-read errors in unrelated tests.`);
|
|
485
|
-
}
|
|
486
|
-
if (tsconfigPaths) {
|
|
487
|
-
parts.push('\nPROJECT TYPESCRIPT CONFIG:');
|
|
488
|
-
parts.push(tsconfigPaths);
|
|
489
|
-
}
|
|
490
|
-
if (localImportPaths && localImportPaths.length > 0) {
|
|
491
|
-
parts.push('\nLOCAL IMPORT PATHS (pre-computed relative to the test file — use EXACTLY these strings in vi.mock() calls, even if the source file uses @/ aliases. Vitest resolves vi.mock() paths relative to the test file, not via tsconfig aliases. Do NOT convert these back to @/ paths in vi.mock(). Do NOT recount directory levels yourself.):');
|
|
492
|
-
for (const p of localImportPaths)
|
|
493
|
-
parts.push(` ${p}`);
|
|
494
|
-
}
|
|
495
|
-
if (typeDefinitions) {
|
|
496
|
-
parts.push('\nTYPE DEFINITIONS (exported from files the source imports — use these exact shapes, do NOT invent properties or guess types):');
|
|
497
|
-
parts.push('```typescript');
|
|
498
|
-
parts.push(typeDefinitions);
|
|
499
|
-
parts.push('```');
|
|
500
|
-
}
|
|
501
|
-
if (setupFileCode) {
|
|
502
|
-
parts.push('\nTEST SETUP FILE (already loaded — do NOT import it again):');
|
|
503
|
-
parts.push('```');
|
|
504
|
-
parts.push(setupFileCode);
|
|
505
|
-
parts.push('```');
|
|
506
|
-
}
|
|
507
|
-
if (mocksImportPath) {
|
|
508
|
-
if (mocksCode) {
|
|
509
|
-
const exports = parseMockExports(mocksCode);
|
|
510
|
-
parts.push(`\nSHARED MOCK FILE (import from: '${mocksImportPath}')`);
|
|
511
|
-
if (exports.length > 0) {
|
|
512
|
-
parts.push(`Available exports: ${exports.join(', ')}`);
|
|
513
|
-
parts.push(`↑ Check this list against the source file — import every relevant mock. Do NOT create inline mocks for anything already exported here.`);
|
|
514
|
-
}
|
|
515
|
-
parts.push('```');
|
|
516
|
-
parts.push(mocksCode);
|
|
517
|
-
parts.push('```');
|
|
518
|
-
}
|
|
519
|
-
else {
|
|
520
|
-
parts.push(`\nSHARED MOCK FILE (does not exist yet) — create it if you need mocks, return it via the // ---MOCKS_FILE--- separator. Path: '${mocksImportPath}'\n⚠ Mocks file must contain ONLY vi.fn()/vi.mock() definitions and beforeEach resets — NEVER describe/it/test/expect blocks.`);
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
if (detectReactNative(packageDeps ?? null))
|
|
524
|
-
parts.push(`\n${buildReactNativeGuidance()}`);
|
|
525
|
-
else if (detectVue(packageDeps ?? null))
|
|
526
|
-
parts.push(`\n${buildVueGuidance()}`);
|
|
527
|
-
if (sourceFile && sourceCode) {
|
|
528
|
-
const networkGuidance = buildNetworkMockingGuidance(analyzeNetworkDeps(sourceCode), sourceFile);
|
|
529
|
-
if (networkGuidance)
|
|
530
|
-
parts.push(`\n${networkGuidance}`);
|
|
531
|
-
const nextGuidance = buildNextJsGuidance(analyzeNextJs(sourceCode));
|
|
532
|
-
if (nextGuidance)
|
|
533
|
-
parts.push(`\n${nextGuidance}`);
|
|
534
|
-
// For fix: skeleton with no function expansion — the test already tells the AI
|
|
535
|
-
// which function matters; showing signatures is enough to understand the API.
|
|
536
|
-
const FIX_SKELETON_THRESHOLD = 150;
|
|
537
|
-
const displaySource = sourceCode.split('\n').length > FIX_SKELETON_THRESHOLD
|
|
538
|
-
? buildSourceSkeleton(sourceCode, [])
|
|
539
|
-
: sourceCode;
|
|
540
|
-
const fixSkeletonized = displaySource !== sourceCode;
|
|
541
|
-
parts.push(`\nSOURCE FILE (what is being tested): ${sourceFile}${fixSkeletonized ? ' (large file — function bodies collapsed to signatures)' : ''}`);
|
|
542
|
-
if (sourceImportPath) {
|
|
543
|
-
parts.push(`SOURCE FILE IMPORT PATH: when importing the source in the test file, use exactly: '${sourceImportPath}'`);
|
|
544
|
-
}
|
|
545
|
-
parts.push('```');
|
|
546
|
-
parts.push(displaySource);
|
|
547
|
-
parts.push('```');
|
|
548
|
-
}
|
|
549
|
-
parts.push(`\nFAILING TEST FILE: ${testFile}`);
|
|
550
|
-
parts.push('```');
|
|
551
|
-
parts.push(testCode);
|
|
552
|
-
parts.push('```');
|
|
553
|
-
parts.push('\nFAILURE OUTPUT:');
|
|
554
|
-
parts.push('```');
|
|
555
|
-
parts.push(errorOutput.slice(0, 3000));
|
|
556
|
-
parts.push('```');
|
|
557
|
-
const realRequestWarning = detectRealRequestInError(errorOutput);
|
|
558
|
-
if (realRequestWarning)
|
|
559
|
-
parts.push(`\n⚠️ ${realRequestWarning}`);
|
|
560
|
-
const rejectionWarning = detectUnhandledRejection(errorOutput);
|
|
561
|
-
if (rejectionWarning)
|
|
562
|
-
parts.push(`\n⚠️ ${rejectionWarning}`);
|
|
563
|
-
const nextImportWarning = detectNextJsImportError(errorOutput);
|
|
564
|
-
if (nextImportWarning)
|
|
565
|
-
parts.push(`\n⚠️ ${nextImportWarning}`);
|
|
566
|
-
const bleedWarning = detectThinkingBleed(errorOutput);
|
|
567
|
-
if (bleedWarning)
|
|
568
|
-
parts.push(`\n⚠️ ${bleedWarning}`);
|
|
569
|
-
const tsErrorWarning = detectTypeScriptErrors(errorOutput);
|
|
570
|
-
if (tsErrorWarning)
|
|
571
|
-
parts.push(`\n⚠️ ${tsErrorWarning}`);
|
|
572
|
-
// Detect wrong mock pattern: test uses vi.mock('axios') but source uses axios.create()
|
|
573
|
-
const testHasAxiosMock = /vi\.mock\(['"]axios['"]\)/.test(testCode);
|
|
574
|
-
const sourceHasCustomInstance = sourceCode != null && /axios\.create\s*\(/.test(sourceCode);
|
|
575
|
-
if (testHasAxiosMock && sourceHasCustomInstance) {
|
|
576
|
-
parts.push("\n⚠️ WRONG MOCK PATTERN: The test mocks 'axios' directly but the source file uses axios.create().", "vi.mock('axios') cannot intercept a custom axios instance.", 'You must mock the module that exports the axios instance, or mock the service/API module the source imports.');
|
|
577
|
-
}
|
|
578
|
-
parts.push('\nCommon causes to check:');
|
|
579
|
-
parts.push('- Wrong import path (use path aliases, not deep relative paths)');
|
|
580
|
-
parts.push('- Mock not set up correctly (check the shared mock file)');
|
|
581
|
-
parts.push('- Asserting on the wrong value or using the wrong matcher');
|
|
582
|
-
parts.push('- Async code not awaited');
|
|
583
|
-
parts.push('- Component/function API changed — check the source file');
|
|
584
|
-
parts.push('- Unhandled rejection: if the error output says "Unhandled Rejection" or "Vitest caught 1 unhandled error", a mockRejectedValueOnce promise is escaping the test scope. Fix by adding await waitFor(() => expect(errorElement).toBeInTheDocument()) after the triggering action, so the rejection is fully resolved inside the test.');
|
|
585
|
-
parts.push('\nReturn your response in the required <thinking> + <code_output> format.');
|
|
586
|
-
return parts.join('\n');
|
|
587
|
-
}
|
|
588
|
-
export function buildRetryPrompt(failureOutput, failedAttempts = []) {
|
|
589
|
-
const parts = [];
|
|
590
|
-
if (failedAttempts.length > 0) {
|
|
591
|
-
parts.push(`You have already attempted to fix this ${failedAttempts.length} time(s). Do NOT repeat these failed approaches:`);
|
|
592
|
-
for (const a of failedAttempts) {
|
|
593
|
-
const hyp = a.hypothesis ? `Planned: [${a.hypothesis.slice(0, 300)}]` : '(no plan recorded)';
|
|
594
|
-
parts.push(`- Attempt ${a.attemptNumber}: ${hyp} → failed with: ${a.failureReason.slice(0, 300)}`);
|
|
595
|
-
}
|
|
596
|
-
parts.push('');
|
|
597
|
-
}
|
|
598
|
-
parts.push(`The tests failed. Error output:`);
|
|
599
|
-
parts.push('```');
|
|
600
|
-
parts.push(failureOutput.slice(0, 3000));
|
|
601
|
-
parts.push('```');
|
|
602
|
-
const realRequestWarning = detectRealRequestInError(failureOutput);
|
|
603
|
-
if (realRequestWarning)
|
|
604
|
-
parts.push(`\n⚠️ ${realRequestWarning}`);
|
|
605
|
-
const rejectionWarning = detectUnhandledRejection(failureOutput);
|
|
606
|
-
if (rejectionWarning)
|
|
607
|
-
parts.push(`\n⚠️ ${rejectionWarning}`);
|
|
608
|
-
const nextImportWarning = detectNextJsImportError(failureOutput);
|
|
609
|
-
if (nextImportWarning)
|
|
610
|
-
parts.push(`\n⚠️ ${nextImportWarning}`);
|
|
611
|
-
const bleedWarning = detectThinkingBleed(failureOutput);
|
|
612
|
-
if (bleedWarning)
|
|
613
|
-
parts.push(`\n⚠️ ${bleedWarning}`);
|
|
614
|
-
const tsErrorWarning = detectTypeScriptErrors(failureOutput);
|
|
615
|
-
if (tsErrorWarning)
|
|
616
|
-
parts.push(`\n⚠️ ${tsErrorWarning}`);
|
|
617
|
-
parts.push('');
|
|
618
|
-
parts.push('Common causes:');
|
|
619
|
-
parts.push('- Wrong import path — check the path aliases and dependency list from the original prompt');
|
|
620
|
-
parts.push('- Missing mock — if a module needs mocking, add it to the shared mock file');
|
|
621
|
-
parts.push('- Wrong vi.mock() path: mock paths are relative to the TEST FILE, not the source file. Count up from the test file\'s directory to reach the mocked module — if the test is in src/features/x/__tests__/ and mocks src/components/, that is ../../../components/, not ../components/.');
|
|
622
|
-
parts.push('- Barrel file mock miss: if a module is re-exported from a barrel/index file, mocking the barrel (vi.mock(\'../components\')) will NOT intercept imports of the direct file (\'../components/Foo\'). Mock the specific file the source actually imports. If unsure, mock both.');
|
|
623
|
-
parts.push('- Wrong API — use only methods that exist in the installed version of the library');
|
|
624
|
-
parts.push('- Type error — make sure the types match what the source file exports');
|
|
625
|
-
parts.push('- React 18 act() async: every act(async () => ...) MUST be awaited. Unawaited act() calls cause state to leak across tests, producing "Cannot read properties of null" or timeout failures in unrelated tests. Fix: add await before every act() call that wraps async code.');
|
|
626
|
-
parts.push('- Loading state — if the error is "Unable to find element" on a Submit/Save button, the component likely unmounts the button during loading rather than disabling it. Assert on the spinner or loading indicator instead.');
|
|
627
|
-
parts.push('- Unhandled rejection ("Vitest caught 1 unhandled error" / "Unhandled Rejection"): a mockRejectedValueOnce promise is escaping the test scope. After the action that triggers the rejection, add: await waitFor(() => expect(screen.getByText(/error/i)).toBeInTheDocument()) — this keeps the rejection chained inside the test so Vitest doesn\'t treat it as unhandled. The component may already catch the error internally, but the test still needs to await the resulting state change.');
|
|
628
|
-
parts.push('');
|
|
629
|
-
parts.push('Fix the issue and return your response in the required <thinking> + <code_output> format.');
|
|
630
|
-
return parts.join('\n');
|
|
631
|
-
}
|
|
632
|
-
//# sourceMappingURL=prompts.js.map
|