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
|
@@ -0,0 +1,668 @@
|
|
|
1
|
+
import { buildSourceSkeleton, shouldUseSkeleton, compressSource, filterMockFileForTest, filterMockFileForSource } from '../../lib/skeleton.js';
|
|
2
|
+
import { detectReactNative, buildReactNativeGuidance, detectRntlErrors } from './react-native.js';
|
|
3
|
+
import { analyzeNextJs, buildNextJsGuidance, detectNextJsImportError } from './nextjs.js';
|
|
4
|
+
import { buildReactCauses } from './react.js';
|
|
5
|
+
import { detectVue, buildVueGuidance } from './vue.js';
|
|
6
|
+
import { buildJsCauses } from './runners/js-common.js';
|
|
7
|
+
import { buildVitestCauses } from './runners/vitest.js';
|
|
8
|
+
import { buildTsRule } from './runners/typescript.js';
|
|
9
|
+
// ─── Setup file mock extractor ────────────────────────────────────────────────
|
|
10
|
+
function extractGlobalNextMocks(setupCode) {
|
|
11
|
+
const mocked = [];
|
|
12
|
+
for (const m of setupCode.matchAll(/(?:vi|jest)\.mock\(['"]([^'"]+)['"]/g)) {
|
|
13
|
+
mocked.push(m[1]);
|
|
14
|
+
}
|
|
15
|
+
return [...new Set(mocked)];
|
|
16
|
+
}
|
|
17
|
+
const API_IMPORT_RE = /\/(?:api|services?|requests?|http|client|network)\/|\/(?:api|axios|http|request)(?:Client|Config|Instance|Service|Helper)?(?:\/|$)|[/.]api(?:[./]|$)/i;
|
|
18
|
+
function analyzeNetworkDeps(sourceCode) {
|
|
19
|
+
const usesAxios = /\baxios\b/.test(sourceCode);
|
|
20
|
+
const usesFetch = /\bfetch\s*\(/.test(sourceCode);
|
|
21
|
+
const usesCustomInstance = /axios\.create\s*\(/.test(sourceCode);
|
|
22
|
+
const apiModuleImports = [];
|
|
23
|
+
for (const m of sourceCode.matchAll(/from\s+['"](\.[^'"]+)['"]/g)) {
|
|
24
|
+
const path = m[1];
|
|
25
|
+
if (API_IMPORT_RE.test(path))
|
|
26
|
+
apiModuleImports.push(path);
|
|
27
|
+
}
|
|
28
|
+
return { usesAxios, usesFetch, usesCustomInstance, apiModuleImports };
|
|
29
|
+
}
|
|
30
|
+
function buildNetworkMockingGuidance(analysis, sourceFile, mockApi) {
|
|
31
|
+
if (!analysis.usesAxios && !analysis.usesFetch && analysis.apiModuleImports.length === 0)
|
|
32
|
+
return null;
|
|
33
|
+
const lines = ['NETWORK MOCKING (critical — a real HTTP request reaching the network is a test bug):'];
|
|
34
|
+
if (analysis.apiModuleImports.length > 0) {
|
|
35
|
+
lines.push(`The source file imports from API/service modules: ${analysis.apiModuleImports.join(', ')}`, `Mock THOSE modules, not the underlying HTTP client:`, ` ${mockApi}.mock('${analysis.apiModuleImports[0]}', () => ({ myFn: ${mockApi}.fn() }))`, `This is the most reliable approach — it intercepts at the contract boundary regardless of which HTTP client is used underneath.`);
|
|
36
|
+
}
|
|
37
|
+
if (analysis.usesCustomInstance) {
|
|
38
|
+
lines.push(`The source creates a custom axios instance (axios.create()). ${mockApi}.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.`);
|
|
39
|
+
}
|
|
40
|
+
else if (analysis.usesAxios && analysis.apiModuleImports.length === 0) {
|
|
41
|
+
lines.push(`The source imports axios directly. Mock it with: ${mockApi}.mock('axios') and set return values with axios.get.mockResolvedValue({ data: ... })`);
|
|
42
|
+
}
|
|
43
|
+
if (analysis.usesFetch) {
|
|
44
|
+
lines.push(`The source uses fetch. Mock it with: ${mockApi}.spyOn(global, 'fetch').mockResolvedValue(new Response(JSON.stringify(data)))`);
|
|
45
|
+
}
|
|
46
|
+
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.`);
|
|
47
|
+
return lines.join('\n');
|
|
48
|
+
}
|
|
49
|
+
function parseMockInventory(code) {
|
|
50
|
+
const entries = [];
|
|
51
|
+
const lines = code.split('\n');
|
|
52
|
+
for (let i = 0; i < lines.length; i++) {
|
|
53
|
+
const mockMatch = lines[i].match(/\b(?:vi|jest)\.mock\(\s*(['"])([^'"]+)\1/);
|
|
54
|
+
if (!mockMatch)
|
|
55
|
+
continue;
|
|
56
|
+
const modulePath = mockMatch[2];
|
|
57
|
+
const lineNumber = i + 1;
|
|
58
|
+
const exports = [];
|
|
59
|
+
let braceDepth = 0;
|
|
60
|
+
let inFactory = false;
|
|
61
|
+
for (let j = i; j < Math.min(i + 80, lines.length); j++) {
|
|
62
|
+
const l = lines[j];
|
|
63
|
+
if (!inFactory && /\(\)\s*=>\s*\(?\s*\{/.test(l))
|
|
64
|
+
inFactory = true;
|
|
65
|
+
if (!inFactory)
|
|
66
|
+
continue;
|
|
67
|
+
for (const ch of l) {
|
|
68
|
+
if (ch === '{')
|
|
69
|
+
braceDepth++;
|
|
70
|
+
if (ch === '}')
|
|
71
|
+
braceDepth--;
|
|
72
|
+
}
|
|
73
|
+
const multiLine = l.match(/^\s{2,}(\w+)\s*:\s*(\w+)/);
|
|
74
|
+
if (multiLine && multiLine[1] !== 'type') {
|
|
75
|
+
const key = multiLine[1], val = multiLine[2];
|
|
76
|
+
exports.push(val && val !== key ? `${key}(${val})` : key);
|
|
77
|
+
}
|
|
78
|
+
if (j === i || l.includes('() =>') || l.includes('() => {')) {
|
|
79
|
+
for (const m of l.matchAll(/\b(\w+)\s*:\s*(mock\w+)/gi)) {
|
|
80
|
+
const key = m[1], val = m[2];
|
|
81
|
+
const entry = val.toLowerCase() !== `mock${key.toLowerCase()}` ? `${key}(${val})` : key;
|
|
82
|
+
if (!exports.some(e => e === key || e.startsWith(`${key}(`)))
|
|
83
|
+
exports.push(entry);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (braceDepth <= 0 && inFactory)
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
entries.push({ modulePath, lineNumber, exports });
|
|
90
|
+
}
|
|
91
|
+
return entries;
|
|
92
|
+
}
|
|
93
|
+
function parseMockExports(code) {
|
|
94
|
+
const names = [];
|
|
95
|
+
for (const m of code.matchAll(/^export\s+(?:const|let|var|function|class|async\s+function)\s+(\w+)/gm)) {
|
|
96
|
+
names.push(m[1]);
|
|
97
|
+
}
|
|
98
|
+
for (const m of code.matchAll(/^export\s*\{([^}]+)\}/gm)) {
|
|
99
|
+
for (const part of m[1].split(',')) {
|
|
100
|
+
const alias = part.trim().split(/\s+as\s+/).pop()?.trim();
|
|
101
|
+
if (alias && /^\w+$/.test(alias))
|
|
102
|
+
names.push(alias);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const defM = code.match(/^export\s+default\s+(\w+)/m);
|
|
106
|
+
if (defM)
|
|
107
|
+
names.push(`default (${defM[1]})`);
|
|
108
|
+
return [...new Set(names)];
|
|
109
|
+
}
|
|
110
|
+
// ─── Error detectors ──────────────────────────────────────────────────────────
|
|
111
|
+
function detectTypeScriptErrors(errorOutput) {
|
|
112
|
+
if (!/error TS\d+:/.test(errorOutput))
|
|
113
|
+
return null;
|
|
114
|
+
const parts = [
|
|
115
|
+
'TYPESCRIPT ERRORS — treat each compiler message as an exact instruction, not a hint:',
|
|
116
|
+
'The TypeScript compiler tells you precisely what is wrong and usually what the fix is.',
|
|
117
|
+
'Do NOT override it with framework conventions or assumptions.',
|
|
118
|
+
];
|
|
119
|
+
if (/TS1378/.test(errorOutput)) {
|
|
120
|
+
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(); });');
|
|
121
|
+
}
|
|
122
|
+
const propErrors = [...errorOutput.matchAll(/'(\w+)' does not exist (?:on|in) type '\{([^']+)\}'/g)];
|
|
123
|
+
if (propErrors.length > 0) {
|
|
124
|
+
parts.push('• Wrong member name — the actual available members are:');
|
|
125
|
+
const seen = new Set();
|
|
126
|
+
for (const m of propErrors) {
|
|
127
|
+
const wrongProp = m[1];
|
|
128
|
+
const available = [...m[2].matchAll(/(\w+)\s*[?]?\s*:/g)].map(p => p[1]).filter(p => p !== 'type');
|
|
129
|
+
const key = wrongProp + available.join();
|
|
130
|
+
if (seen.has(key))
|
|
131
|
+
continue;
|
|
132
|
+
seen.add(key);
|
|
133
|
+
parts.push(` '${wrongProp}' → not valid. Use one of: ${available.slice(0, 12).join(', ')}${available.length > 12 ? ' …' : ''}`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
const suggestions = [...new Set([...errorOutput.matchAll(/Did you mean(?: to write)? '(\w+)'\?/g)].map(m => m[1]))];
|
|
137
|
+
if (suggestions.length > 0) {
|
|
138
|
+
parts.push(`• Compiler suggestion: use ${suggestions.map(s => `'${s}'`).join(', ')}`);
|
|
139
|
+
}
|
|
140
|
+
const typeMismatches = [...errorOutput.matchAll(/Argument of type '([^']+)' is not assignable to parameter of type '([^']+)'/g)];
|
|
141
|
+
if (typeMismatches.length > 0) {
|
|
142
|
+
for (const m of typeMismatches) {
|
|
143
|
+
parts.push(`• Type mismatch: passed '${m[1].slice(0, 80)}', required '${m[2].slice(0, 80)}'`);
|
|
144
|
+
}
|
|
145
|
+
parts.push(' (use null not undefined for nullable values; check TYPE DEFINITIONS for the required shape)');
|
|
146
|
+
}
|
|
147
|
+
const otherErrors = [...errorOutput.matchAll(/error (TS(?!1378|2339|2551|2561|2345)\d+): ([^\n]+)/g)];
|
|
148
|
+
if (otherErrors.length > 0) {
|
|
149
|
+
parts.push('• Additional compiler errors — read each one and apply the exact fix it describes:');
|
|
150
|
+
const seen = new Set();
|
|
151
|
+
for (const m of otherErrors) {
|
|
152
|
+
const msg = `${m[1]}: ${m[2].slice(0, 120)}`;
|
|
153
|
+
if (seen.has(msg))
|
|
154
|
+
continue;
|
|
155
|
+
seen.add(msg);
|
|
156
|
+
parts.push(` ${msg}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return parts.join('\n');
|
|
160
|
+
}
|
|
161
|
+
function detectThinkingBleed(errorOutput) {
|
|
162
|
+
const parseErr = errorOutput.match(/PARSE_ERROR|Unexpected token|SyntaxError.*\b1:\d+\b/);
|
|
163
|
+
if (!parseErr)
|
|
164
|
+
return null;
|
|
165
|
+
const contextLine = errorOutput.match(/^\s*1\s*[│|]\s*(.+)/m);
|
|
166
|
+
if (!contextLine)
|
|
167
|
+
return null;
|
|
168
|
+
const firstLine = contextLine[1].trim();
|
|
169
|
+
if (/^(import|export|const|let|var|\/\/|\/\*|describe|it\s*\(|test\s*\(|vi\.|jest\.)/.test(firstLine))
|
|
170
|
+
return null;
|
|
171
|
+
return [
|
|
172
|
+
`THINKING BLEED DETECTED — your previous response had reasoning text inside <code_output>.`,
|
|
173
|
+
`The file started with: "${firstLine.slice(0, 80)}"`,
|
|
174
|
+
`This is not valid TypeScript and caused a parse error at line 1.`,
|
|
175
|
+
`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.`,
|
|
176
|
+
`Do NOT continue thinking inside <code_output> under any circumstances.`,
|
|
177
|
+
].join('\n');
|
|
178
|
+
}
|
|
179
|
+
function detectUnhandledRejection(errorOutput) {
|
|
180
|
+
const hasUnhandled = /unhandled\s+(promise\s+)?rejection|vitest caught \d+ unhandled/i.test(errorOutput);
|
|
181
|
+
const hasRejectedMock = /mockRejectedValue(Once)?/.test(errorOutput);
|
|
182
|
+
if (!hasUnhandled && !hasRejectedMock)
|
|
183
|
+
return null;
|
|
184
|
+
return [
|
|
185
|
+
'UNHANDLED REJECTION DETECTED — a mockRejectedValueOnce (or mockRejectedValue) promise is escaping the test scope.',
|
|
186
|
+
'The component may catch the error internally, but Vitest still requires the rejection to be resolved inside the test.',
|
|
187
|
+
'Required fix: after the action that triggers the rejection, await the resulting error state:',
|
|
188
|
+
" await waitFor(() => expect(screen.getByText(/error text/i)).toBeInTheDocument())",
|
|
189
|
+
'This chains the rejection inside the test scope. Without it, Vitest flags it as unhandled even if the UI handles it correctly.',
|
|
190
|
+
].join('\n');
|
|
191
|
+
}
|
|
192
|
+
function detectRealRequestInError(errorOutput, mockApi = 'vi') {
|
|
193
|
+
const hasRealUrl = /https?:\/\/[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/.test(errorOutput);
|
|
194
|
+
const hasHttpStatus = /\bstatus:\s*[45]\d\d\b/.test(errorOutput);
|
|
195
|
+
const hasNetworkError = /ECONNREFUSED|ENOTFOUND|ETIMEDOUT|network\s+error/i.test(errorOutput);
|
|
196
|
+
if (!hasRealUrl && !hasHttpStatus && !hasNetworkError)
|
|
197
|
+
return null;
|
|
198
|
+
const lines = [
|
|
199
|
+
'REAL HTTP REQUEST DETECTED — the test is hitting the actual network. This is the root cause of the failure.',
|
|
200
|
+
'A mock is either missing entirely or applied at the wrong level. Fix this before anything else.',
|
|
201
|
+
];
|
|
202
|
+
const urlMatch = errorOutput.match(/https?:\/\/[^\s,'")\]}]+/);
|
|
203
|
+
if (urlMatch)
|
|
204
|
+
lines.push(`Intercepted URL: ${urlMatch[0]}`);
|
|
205
|
+
lines.push('Required fix: find which module the source file imports for its API calls and mock THAT module.', `${mockApi}.mock('axios') does NOT intercept axios.create() instances — you must mock the module that exports the instance or the service layer above it.`);
|
|
206
|
+
return lines.join('\n');
|
|
207
|
+
}
|
|
208
|
+
// ─── System prompt ────────────────────────────────────────────────────────────
|
|
209
|
+
export function buildSystemPrompt(env) {
|
|
210
|
+
const isJS = env.language === 'typescript' || env.language === 'javascript' || env.language === 'unknown';
|
|
211
|
+
const isTS = env.language === 'typescript';
|
|
212
|
+
const isVitest = env.testRunner === 'vitest';
|
|
213
|
+
const isJSRunner = env.testRunner === 'jest' || env.testRunner === 'vitest' || env.testRunner === 'mocha';
|
|
214
|
+
const mockApi = isVitest ? 'vi' : 'jest';
|
|
215
|
+
const mockAuditStep = isJS ? `
|
|
216
|
+
2. MOCK AUDIT — do this before writing a single line of test code:
|
|
217
|
+
a) SOURCE ANCHOR — AI models pattern-match function names to common implementations. A route named "inviteTeamMember" triggers priors about Firestore, teamId parameters, and standard error messages that may be completely wrong for this specific codebase. Override every prior by quoting verbatim from the actual source file before you write anything:
|
|
218
|
+
• Backend routes/functions: (1) list every req.body/req.params/req.query field the source ACTUALLY reads — not what the function name implies; (2) quote the exact database or service call pattern used (e.g. adminDb.ref(path) vs adminDb.collection().doc()); (3) copy every error message string verbatim from each res.json({error:...}) or throw statement in the source; (4) note every HTTP status code and the exact guard condition (if block) that produces it.
|
|
219
|
+
• Components/screens: (1) quote every string literal rendered in JSX/template — not what you assume is shown; (2) list the exact method name on each service/hook call as it appears in the source (e.g. UserService.inviteMember not UserService.invite); (3) note every conditional render guard (ternary, &&) and the exact state variable driving it.
|
|
220
|
+
If ANYTHING you quote differs from what you expected — the source wins. Write tests for what the code ACTUALLY does, not what a similarly-named function typically does.
|
|
221
|
+
b) IMPORT INVENTORY: List every import in the source file. For each client/service/hook, find every method it calls (grep for Client.method() patterns). Mock exactly those methods — nothing more, nothing less. Mocking a method the source never calls is useless; missing a method the source DOES call is a silent failure.
|
|
222
|
+
c) RESPONSE ENVELOPE: At each Client.method() call site, check how the return value is consumed. If the source guards with \`if (res.success)\` or destructures \`{ success, data }\`, the mock MUST return that envelope — NOT a raw array. \`mockResolvedValue([...])\` when the hook expects \`{ success: true, data: [...] }\` produces silently empty state with no error. Pattern: \`const ok = (data: unknown) => ({ success: true, data })\`.
|
|
223
|
+
d) RETURN FIELD ENUMERATION: Check the DEPENDENCY FILE CONTENTS section for each hook's implementation and read its \`return { ... }\` statement. List every key the hook actually returns — not just what the component destructures. A hook may return more keys than the component currently uses, and missing keys in mocks are silently undefined and can break conditional renders, validation, or dynamic text. If the section is absent, fall back to the component's destructure as a minimum baseline.
|
|
224
|
+
e) LOADING TRIGGER MAP: Not all data loads on mount. For each piece of state, find what populates it. If a function like loadResults(classId) must be called explicitly (user selects something), the mount test will never see that data. Map: state → function that populates it → when that function is triggered.
|
|
225
|
+
f) FIXTURE FIELD NAMES: Read the source's selector logic — every .find(), .filter(), and property access. Field names in fixture data must match what the source reads, not what sounds reasonable. \`is_active\` and \`is_current\` are both plausible; only one will pass the filter. Read the source.
|
|
226
|
+
g) MOCK STRUCTURE — object vs factory: when the source imports a client/service as a module export and calls it as \`SomeClient.method()\`, the mock must be a plain object \`{ SomeClient: { method: ${mockApi}.fn() } }\`. If you mock it as \`${mockApi}.fn().mockReturnValue({ method: ${mockApi}.fn() })\`, SomeClient.method is undefined at runtime — the mock replaced a singleton with a callable that the source never calls. The mock structure must match how the source uses the import, not how you'd design an API.
|
|
227
|
+
h) DATA TRANSFORMATIONS: Before writing any assertion about the shape of loaded data, read every .map(), .filter(), and mutation the hook applies to the raw API response. If the hook does \`.map(s => ({ ...s, selected: true, status: 'promoted' }))\`, the fixture assertion must expect the TRANSFORMED shape, not the raw API fixture. Keep two separate fixtures: the raw API response (for mockResolvedValue) and the expected hook output (for assertions).
|
|
228
|
+
i) USEEFFECT COMPOUND SIDE EFFECTS: For each useEffect, read its dependency array AND every state setter it calls. Some effects reset sibling state as a side effect (e.g. fetchSourceClasses always calls setSelectedSourceClassId('')). Setting state that triggers such an effect will silently undo other state you set in the same act(). Map the full chain: which state changes trigger which effects, and what those effects do to other state — before writing any test that sets multiple state values.
|
|
229
|
+
HOOK STATE SYNC: If the test mocks a hook or function that returns an object (e.g. useClasses(), useUsers()), compare its CURRENT return signature in the source against the mocked return object in the test. If any properties are missing, renamed, or stale, realign the mock FIRST — before touching any assertions.
|
|
230
|
+
UNCONDITIONAL CRASH CHECK: Look at the very top of the component body — what fields are destructured and used BEFORE any conditional render (e.g. totalRevenue.toLocaleString(), sessions.length)? Every one of those fields MUST be present in the mock return value or ALL tests will crash immediately.
|
|
231
|
+
MOCK PROP INTERFACE: When mocking a child component (e.g. EmptyState, Modal), check how the PARENT calls it — what prop names does the JSX pass? Use those exact names in the mock, not the names from the child's own prop type definition.
|
|
232
|
+
3. COMPONENT RENDER MAP (React/Vue components only): Before writing any assertion, list what is in the DOM in each relevant state (idle / loading / error / success). Read the template/JSX — check every ternary, &&, and conditional — to determine whether a button is disabled vs unmounted, what text changes, what elements appear.
|
|
233
|
+
GUARD CLAUSE AUDIT: Identify every conditional render guard in the component (e.g. payments.length > 0, isLoading, hasPermission). A test that provides data violating a guard will never find the element — the guard hides it. Match mock data to the guard condition required by each test.
|
|
234
|
+
STALE TEST AUDIT: Check whether any existing test asserts UI or behavior that the current source no longer has. DELETE those tests — do not try to make the component pass a test for features it no longer has.
|
|
235
|
+
BUG-ASSERTING TEST RULE: NEVER write a test that asserts the component/function throws or crashes due to a missing null check, missing guard, or undefined field — unless you have read the source and confirmed the crash path exists. A test named "throws when X is undefined due to missing null check" is testing for a bug, not behavior. If the source handles the case gracefully, test the graceful output instead. If it truly crashes, fix the source — do not write a test to document the bug.` : `
|
|
236
|
+
2. DEPENDENCY AUDIT: List every external dependency the source calls. For each one, determine what needs to be mocked and what return value the code expects. Read every call site — don't infer the expected shape from the type name.
|
|
237
|
+
3. DATA FIXTURE AUDIT: Read the source's selector logic — every filter, find, and field access. Fixture data field names must match what the source reads exactly.`;
|
|
238
|
+
const thinkingTemplate = `
|
|
239
|
+
1. WHAT IS NEEDED: What functions/behaviors are untested or broken?${mockAuditStep}
|
|
240
|
+
4. WHY IT FAILED (retries only): Errors cascade — a compile error hides a resolution error which hides a wiring error which hides a logic error. Fix the first layer and expect a new error to surface. What layer are we on now?
|
|
241
|
+
5. PLAN: List the exact steps you will take before writing a single line of code.`;
|
|
242
|
+
const jsRules = isJS ? `
|
|
243
|
+
3. Use path aliases from the PROJECT TYPESCRIPT CONFIG section in IMPORT statements (e.g. "@/components/Button" not "../../components/Button").
|
|
244
|
+
EXCEPTION — mock call paths: use the exact same path string that appears in the SOURCE FILE'S import statement.
|
|
245
|
+
If a LOCAL IMPORT PATHS section is provided, use those pre-computed relative paths in mock calls — they are the fallback when aliases cannot be resolved by the test runner.
|
|
246
|
+
Never second-guess the pre-computed paths. Never convert them back to @/ aliases in mock calls.
|
|
247
|
+
4. Only import from packages listed in PROJECT DEPENDENCIES. Do not invent packages that are not listed.
|
|
248
|
+
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. Import and use ALL of those mocks. Never re-create inline ${mockApi}.fn() for anything already exported from the mocks file.
|
|
249
|
+
CRITICAL — never rename or change the casing of existing mock exports.
|
|
250
|
+
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---
|
|
251
|
+
CRITICAL — if you have NO new mocks to add, OMIT the // ---MOCKS_FILE--- separator entirely. Do NOT write a comment-only or placeholder mock file (e.g. "// No mocks needed"). Either add real code or omit the separator.
|
|
252
|
+
CRITICAL — when writing the mocks file, you receive the FULL EXISTING content. You MUST write the complete merged result. Never add a second import statement for a module that is already imported — merge all named imports from the same module into one statement. WRONG: line 2 has \`import { View } from 'react-native'\` and you add line 28 \`import { View, Text } from 'react-native'\`. RIGHT: update line 2 to \`import { View, Text } from 'react-native'\` and omit the duplicate.
|
|
253
|
+
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.
|
|
254
|
+
CRITICAL — the mocks file must contain ONLY: ${mockApi}.fn() mock definitions, ${mockApi}.mock() module stubs, shared mock objects/constants, and beforeEach reset hooks. NEVER write describe(), it(), test(), or expect() calls in the mocks file.
|
|
255
|
+
8. If a TEST SETUP FILE is shown, assume its globals and matchers are already available. Do NOT import or re-declare them.` : `
|
|
256
|
+
3. Use the project's import conventions as shown in the source file and existing tests.
|
|
257
|
+
4. Only import from packages listed in PROJECT DEPENDENCIES. Do not invent packages that are not listed.`;
|
|
258
|
+
const tsRule = isTS ? buildTsRule(mockApi) : '';
|
|
259
|
+
const ruleCount = isTS ? 10 : (isJS ? 9 : 6);
|
|
260
|
+
const jsOutputRules = isJS ? `
|
|
261
|
+
${ruleCount + 2}. Inside <code_output>: output ONLY the test file content (or test file // ---MOCKS_FILE--- mocks file).
|
|
262
|
+
If you use // ---MOCKS_FILE---, everything AFTER the separator is the mocks file. The mocks file must contain ONLY:
|
|
263
|
+
${mockApi}.fn() mock definitions, ${mockApi}.mock() module stubs, shared constants, and beforeEach resets.
|
|
264
|
+
NEVER put describe(), it(), test(), or expect() calls after the separator.
|
|
265
|
+
${ruleCount + 3}. NEVER output vitest.config.ts, jest.config.js, or any framework configuration. If an import cannot be resolved,
|
|
266
|
+
fix it by mocking it with ${mockApi}.mock() — NOT by modifying the test runner configuration.` : '';
|
|
267
|
+
const universalCauses = `- Wrong import paths (use the project's conventions — aliases where configured, relative paths otherwise)
|
|
268
|
+
- Importing from test utilities that are not in the dependency list
|
|
269
|
+
- Mocking modules that are already mocked in the setup file
|
|
270
|
+
- Forgetting to await async functions
|
|
271
|
+
- Real HTTP requests: NEVER let a real network call reach the internet. Every function that calls an API must be mocked before the test runs.
|
|
272
|
+
- Error surface mismatch: before writing any error-path test, find the catch block. Does it set state, call a notification, or just log silently? Test only what is actually observable from outside.
|
|
273
|
+
- Code drift — assert what the code ACTUALLY does: before writing any assertion, re-read the relevant section of the source. If it catches an error and returns null, assert null — not a rejection.`;
|
|
274
|
+
const jsCauses = isJSRunner ? buildJsCauses(mockApi) : '';
|
|
275
|
+
const vitestCauses = isVitest ? buildVitestCauses() : '';
|
|
276
|
+
const reactCauses = buildReactCauses(isJSRunner, mockApi);
|
|
277
|
+
const hookSuiteNote = isJSRunner
|
|
278
|
+
? `\n- For hooks: cover mutations (save, update, delete) and derived/computed state — not just the initial-load lifecycle. Mutations and derived state are where real bugs hide.`
|
|
279
|
+
: '';
|
|
280
|
+
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.
|
|
281
|
+
|
|
282
|
+
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.
|
|
283
|
+
|
|
284
|
+
RULES — follow every one:
|
|
285
|
+
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)).
|
|
286
|
+
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.${jsRules}${tsRule}
|
|
287
|
+
${ruleCount}. 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.
|
|
288
|
+
${ruleCount + 1}. Structure ALL output using exactly these two XML blocks — nothing before, nothing after:
|
|
289
|
+
<thinking>${thinkingTemplate}
|
|
290
|
+
</thinking>
|
|
291
|
+
<code_output>
|
|
292
|
+
// complete test file here
|
|
293
|
+
</code_output>
|
|
294
|
+
CRITICAL: Once you open <code_output>, ALL remaining output must be code. Finish ALL reasoning inside <thinking> first.
|
|
295
|
+
${ruleCount + 2 <= ruleCount + 1 ? '' : `${ruleCount + 2}. Inside <code_output>: do NOT wrap in markdown code fences.`}${jsOutputRules}
|
|
296
|
+
|
|
297
|
+
A good test suite you write will have:
|
|
298
|
+
- A happy-path test that confirms the main behavior works
|
|
299
|
+
- At least one edge-case test per function (empty input, zero, null, boundary values)
|
|
300
|
+
- Error-path tests for any function that throws, rejects, or returns an error state — but ONLY assert the observable effect. Read the catch block first: does it set state, call a notification, or just log? Test only what's observable.
|
|
301
|
+
- Async tests properly awaited — never fire-and-forget${hookSuiteNote}
|
|
302
|
+
- Clear, descriptive test names that read like a spec ("returns null when user is not authenticated")
|
|
303
|
+
|
|
304
|
+
Common failure causes to avoid:
|
|
305
|
+
${universalCauses}${jsCauses}${vitestCauses}${reactCauses}
|
|
306
|
+
|
|
307
|
+
Test file pattern for this project: ${env.testFilePattern}
|
|
308
|
+
|
|
309
|
+
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.`;
|
|
310
|
+
}
|
|
311
|
+
// ─── Generate prompt ──────────────────────────────────────────────────────────
|
|
312
|
+
export function buildGeneratePrompt(args) {
|
|
313
|
+
const { sourceFile, env, existingTestCode, uncoveredFunctions, uncoveredLines, sourceImportPath, mocksCode, mocksImportPath, setupFileCode, packageDeps, tsconfigPaths, typeDefinitions, localImportPaths, localImportContents, reactMajorVersion, projectMemory, } = args;
|
|
314
|
+
const sourceCode = compressSource(args.sourceCode);
|
|
315
|
+
const mockApi = env.testRunner === 'vitest' ? 'vi' : 'jest';
|
|
316
|
+
const parts = [];
|
|
317
|
+
if (projectMemory) {
|
|
318
|
+
parts.push(projectMemory);
|
|
319
|
+
parts.push('');
|
|
320
|
+
}
|
|
321
|
+
if (packageDeps) {
|
|
322
|
+
parts.push('PROJECT DEPENDENCIES (only import from these):');
|
|
323
|
+
parts.push('```');
|
|
324
|
+
parts.push(packageDeps);
|
|
325
|
+
parts.push('```');
|
|
326
|
+
}
|
|
327
|
+
if (reactMajorVersion !== null && reactMajorVersion !== undefined && reactMajorVersion >= 18) {
|
|
328
|
+
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.`);
|
|
329
|
+
}
|
|
330
|
+
if (tsconfigPaths) {
|
|
331
|
+
parts.push('\nPROJECT TYPESCRIPT CONFIG (strict flags, target, and path aliases — follow these exactly):');
|
|
332
|
+
parts.push(tsconfigPaths);
|
|
333
|
+
}
|
|
334
|
+
if (localImportPaths && localImportPaths.length > 0) {
|
|
335
|
+
parts.push(`\nLOCAL IMPORT PATHS (pre-computed relative to the test file — use EXACTLY these strings in ${mockApi}.mock() calls, even if the source file uses @/ aliases. The test runner resolves ${mockApi}.mock() paths relative to the test file, not via tsconfig aliases. Do NOT convert these back to @/ paths in ${mockApi}.mock(). Do NOT recount directory levels yourself.):`);
|
|
336
|
+
for (const p of localImportPaths)
|
|
337
|
+
parts.push(` ${p}`);
|
|
338
|
+
}
|
|
339
|
+
if (typeDefinitions) {
|
|
340
|
+
parts.push('\nTYPE DEFINITIONS (exported from files the source imports — use these exact shapes, do NOT invent properties or guess types):');
|
|
341
|
+
parts.push('```typescript');
|
|
342
|
+
parts.push(typeDefinitions);
|
|
343
|
+
parts.push('```');
|
|
344
|
+
}
|
|
345
|
+
if (localImportContents) {
|
|
346
|
+
parts.push('\nUSED SYMBOL DEFINITIONS (extracted from files the component imports — only the specific symbols used, with function bodies collapsed to signature + return and class bodies collapsed to method signatures. Use this to find exact hook return shapes, service method names, and type definitions. Cross-check every hook mock against the hook\'s actual return statement here):');
|
|
347
|
+
parts.push('```typescript');
|
|
348
|
+
parts.push(localImportContents);
|
|
349
|
+
parts.push('```');
|
|
350
|
+
}
|
|
351
|
+
if (setupFileCode) {
|
|
352
|
+
const nextMocked = extractGlobalNextMocks(setupFileCode);
|
|
353
|
+
const setupNote = nextMocked.length > 0
|
|
354
|
+
? `\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 ${mockApi}.mock() for them in the test: ${nextMocked.join(', ')}`
|
|
355
|
+
: `\nTEST SETUP FILE (already loaded before every test — do NOT import it again):`;
|
|
356
|
+
parts.push(setupNote);
|
|
357
|
+
parts.push('```');
|
|
358
|
+
parts.push(setupFileCode);
|
|
359
|
+
parts.push('```');
|
|
360
|
+
}
|
|
361
|
+
if (mocksImportPath) {
|
|
362
|
+
if (mocksCode) {
|
|
363
|
+
const exports = parseMockExports(mocksCode);
|
|
364
|
+
parts.push(`\nSHARED MOCK FILE (import from: '${mocksImportPath}')`);
|
|
365
|
+
if (exports.length > 0) {
|
|
366
|
+
parts.push(`Available exports: ${exports.join(', ')}`);
|
|
367
|
+
parts.push(`↑ Every name above ALREADY EXISTS in the mock file — do NOT re-declare any of them in a ---MOCKS_FILE--- block. Only declare names that do NOT appear in this list.\n↑ 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.`);
|
|
368
|
+
}
|
|
369
|
+
const inventory = parseMockInventory(mocksCode);
|
|
370
|
+
if (inventory.length > 0) {
|
|
371
|
+
const maxLen = Math.max(...inventory.map(e => e.modulePath.length));
|
|
372
|
+
parts.push(`\nMOCK MODULE INVENTORY — modules already ${mockApi}.mocked:`);
|
|
373
|
+
for (const entry of inventory) {
|
|
374
|
+
const path = `'${entry.modulePath}'`.padEnd(maxLen + 2);
|
|
375
|
+
const exp = entry.exports.length > 0 ? entry.exports.join(', ') : '(no simple key exports)';
|
|
376
|
+
parts.push(` ${path} → ${exp}`);
|
|
377
|
+
}
|
|
378
|
+
parts.push('MOCK EDITING RULES — follow exactly when returning a ---MOCKS_FILE--- block:');
|
|
379
|
+
parts.push(`• A module in the inventory is ALREADY mocked. To add a new export: write ONE updated ${mockApi}.mock() block with ALL existing exports PLUS the new one. NEVER write a second ${mockApi}.mock() for the same path — the second block silently wipes every export from the first.`);
|
|
380
|
+
parts.push(`• New export const mockFoo = ${mockApi}.fn(): declare it near other exports of the same domain. Add its .mockReset() or .mockClear() to the EXISTING beforeEach — do NOT create an extra beforeEach for one variable.`);
|
|
381
|
+
parts.push(`• New module (not in inventory): append a new ${mockApi}.mock() at the END of the file, before the final beforeEach.`);
|
|
382
|
+
}
|
|
383
|
+
const relevantMocks = filterMockFileForSource(mocksCode, sourceCode);
|
|
384
|
+
if (relevantMocks !== mocksCode || relevantMocks.split('\n').length < 80) {
|
|
385
|
+
parts.push('```');
|
|
386
|
+
parts.push(relevantMocks);
|
|
387
|
+
parts.push('```');
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
else {
|
|
391
|
+
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 ${mockApi}.fn()/${mockApi}.mock() definitions and beforeEach resets — NEVER describe/it/test/expect blocks.`);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
const networkGuidance = buildNetworkMockingGuidance(analyzeNetworkDeps(sourceCode), sourceFile, mockApi);
|
|
395
|
+
if (networkGuidance)
|
|
396
|
+
parts.push(`\n${networkGuidance}`);
|
|
397
|
+
const nextGuidance = buildNextJsGuidance(analyzeNextJs(sourceCode), mockApi);
|
|
398
|
+
if (nextGuidance)
|
|
399
|
+
parts.push(`\n${nextGuidance}`);
|
|
400
|
+
if (detectReactNative(packageDeps ?? null))
|
|
401
|
+
parts.push(`\n${buildReactNativeGuidance()}`);
|
|
402
|
+
else if (detectVue(packageDeps ?? null))
|
|
403
|
+
parts.push(`\n${buildVueGuidance()}`);
|
|
404
|
+
const displaySource = buildSourceSkeleton(sourceCode, uncoveredFunctions);
|
|
405
|
+
const skeletonized = shouldUseSkeleton(sourceCode);
|
|
406
|
+
parts.push(`\nSOURCE FILE: ${sourceFile}${skeletonized ? ' (large file — bodies of already-covered functions collapsed; uncovered functions shown in full)' : ''}`);
|
|
407
|
+
if (sourceImportPath) {
|
|
408
|
+
parts.push(`SOURCE FILE IMPORT PATH: when importing the source in your test file, use exactly: '${sourceImportPath}'`);
|
|
409
|
+
}
|
|
410
|
+
parts.push('```');
|
|
411
|
+
parts.push(displaySource);
|
|
412
|
+
parts.push('```');
|
|
413
|
+
if (existingTestCode) {
|
|
414
|
+
parts.push('\nEXISTING TEST FILE (preserve all existing tests, only add new ones):');
|
|
415
|
+
parts.push('```');
|
|
416
|
+
parts.push(existingTestCode);
|
|
417
|
+
parts.push('```');
|
|
418
|
+
}
|
|
419
|
+
else {
|
|
420
|
+
parts.push('\nNo existing test file — create one from scratch.');
|
|
421
|
+
}
|
|
422
|
+
if (uncoveredFunctions.length > 0) {
|
|
423
|
+
parts.push(`\nUNCOVERED FUNCTIONS (must write tests for these): ${uncoveredFunctions.join(', ')}`);
|
|
424
|
+
}
|
|
425
|
+
if (uncoveredLines.length > 0) {
|
|
426
|
+
parts.push(`\nUNCOVERED LINES: ${uncoveredLines.slice(0, 30).join(', ')}${uncoveredLines.length > 30 ? '…' : ''}`);
|
|
427
|
+
}
|
|
428
|
+
parts.push('\nWrite the complete test file now.');
|
|
429
|
+
return parts.join('\n');
|
|
430
|
+
}
|
|
431
|
+
// ─── Fix prompt ───────────────────────────────────────────────────────────────
|
|
432
|
+
export function buildFixPrompt(args) {
|
|
433
|
+
const { testFile, testCode, sourceFile, sourceImportPath, errorOutput, env, mocksCode, mocksImportPath, setupFileCode, packageDeps, tsconfigPaths, typeDefinitions, localImportPaths, reactMajorVersion, projectMemory } = args;
|
|
434
|
+
const sourceCode = args.sourceCode ? compressSource(args.sourceCode) : null;
|
|
435
|
+
const mockApi = env.testRunner === 'vitest' ? 'vi' : 'jest';
|
|
436
|
+
const parts = [];
|
|
437
|
+
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.');
|
|
438
|
+
parts.push('');
|
|
439
|
+
if (projectMemory) {
|
|
440
|
+
parts.push(projectMemory);
|
|
441
|
+
parts.push('');
|
|
442
|
+
}
|
|
443
|
+
if (packageDeps) {
|
|
444
|
+
parts.push('PROJECT DEPENDENCIES (only import from these):');
|
|
445
|
+
parts.push('```');
|
|
446
|
+
parts.push(packageDeps);
|
|
447
|
+
parts.push('```');
|
|
448
|
+
}
|
|
449
|
+
if (reactMajorVersion !== null && reactMajorVersion !== undefined && reactMajorVersion >= 18) {
|
|
450
|
+
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.`);
|
|
451
|
+
}
|
|
452
|
+
if (tsconfigPaths) {
|
|
453
|
+
parts.push('\nPROJECT TYPESCRIPT CONFIG:');
|
|
454
|
+
parts.push(tsconfigPaths);
|
|
455
|
+
}
|
|
456
|
+
if (localImportPaths && localImportPaths.length > 0) {
|
|
457
|
+
parts.push(`\nLOCAL IMPORT PATHS (pre-computed relative to the test file — use EXACTLY these strings in ${mockApi}.mock() calls, even if the source file uses @/ aliases. The test runner resolves ${mockApi}.mock() paths relative to the test file, not via tsconfig aliases. Do NOT convert these back to @/ paths in ${mockApi}.mock(). Do NOT recount directory levels yourself.):`);
|
|
458
|
+
for (const p of localImportPaths)
|
|
459
|
+
parts.push(` ${p}`);
|
|
460
|
+
}
|
|
461
|
+
if (typeDefinitions) {
|
|
462
|
+
parts.push('\nTYPE DEFINITIONS (exported from files the source imports — use these exact shapes, do NOT invent properties or guess types):');
|
|
463
|
+
parts.push('```typescript');
|
|
464
|
+
parts.push(typeDefinitions);
|
|
465
|
+
parts.push('```');
|
|
466
|
+
}
|
|
467
|
+
if (setupFileCode) {
|
|
468
|
+
parts.push('\nTEST SETUP FILE (already loaded — do NOT import it again):');
|
|
469
|
+
parts.push('```');
|
|
470
|
+
parts.push(setupFileCode);
|
|
471
|
+
parts.push('```');
|
|
472
|
+
}
|
|
473
|
+
if (mocksImportPath) {
|
|
474
|
+
if (mocksCode) {
|
|
475
|
+
const exports = parseMockExports(mocksCode);
|
|
476
|
+
const compressed = filterMockFileForTest(mocksCode, args.testCode);
|
|
477
|
+
parts.push(`\nSHARED MOCK FILE (import from: '${mocksImportPath}')`);
|
|
478
|
+
if (exports.length > 0) {
|
|
479
|
+
parts.push(`Available exports: ${exports.join(', ')}`);
|
|
480
|
+
parts.push(`↑ Every name above ALREADY EXISTS in the mock file — do NOT re-declare any of them in a ---MOCKS_FILE--- block. Only declare names that do NOT appear in this list.\n↑ Import every mock that matches the source file's domain. Do NOT create inline mocks for anything already in this list.`);
|
|
481
|
+
}
|
|
482
|
+
const inventory = parseMockInventory(compressed);
|
|
483
|
+
if (inventory.length > 0) {
|
|
484
|
+
const maxLen = Math.max(...inventory.map(e => e.modulePath.length));
|
|
485
|
+
parts.push(`\nMOCK MODULE INVENTORY — modules already ${mockApi}.mocked (line numbers refer to the file below):`);
|
|
486
|
+
for (const entry of inventory) {
|
|
487
|
+
const path = `'${entry.modulePath}'`.padEnd(maxLen + 2);
|
|
488
|
+
const exp = entry.exports.length > 0 ? entry.exports.join(', ') : '(no simple key exports)';
|
|
489
|
+
parts.push(` Line ${String(entry.lineNumber).padStart(4)}: ${path} → ${exp}`);
|
|
490
|
+
}
|
|
491
|
+
parts.push('MOCK EDITING RULES — follow exactly when returning a ---MOCKS_FILE--- block:');
|
|
492
|
+
parts.push(`• A module in the inventory is ALREADY mocked. To add a new export: write ONE updated ${mockApi}.mock() block with ALL existing exports PLUS the new one. NEVER write a second ${mockApi}.mock() for the same path — the second block silently wipes every export from the first.`);
|
|
493
|
+
parts.push(`• New export const mockFoo = ${mockApi}.fn(): declare it near other exports of the same domain. Add its .mockReset() or .mockClear() to the EXISTING beforeEach — do NOT create an extra beforeEach for one variable.`);
|
|
494
|
+
parts.push(`• New module (not in inventory): append a new ${mockApi}.mock() at the END of the file, before the final beforeEach.`);
|
|
495
|
+
}
|
|
496
|
+
parts.push('```');
|
|
497
|
+
parts.push(compressed);
|
|
498
|
+
parts.push('```');
|
|
499
|
+
}
|
|
500
|
+
else {
|
|
501
|
+
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 ${mockApi}.fn()/${mockApi}.mock() definitions and beforeEach resets — NEVER describe/it/test/expect blocks.`);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
if (detectReactNative(packageDeps ?? null))
|
|
505
|
+
parts.push(`\n${buildReactNativeGuidance()}`);
|
|
506
|
+
else if (detectVue(packageDeps ?? null))
|
|
507
|
+
parts.push(`\n${buildVueGuidance()}`);
|
|
508
|
+
if (sourceFile && sourceCode) {
|
|
509
|
+
const networkGuidance = buildNetworkMockingGuidance(analyzeNetworkDeps(sourceCode), sourceFile, mockApi);
|
|
510
|
+
if (networkGuidance)
|
|
511
|
+
parts.push(`\n${networkGuidance}`);
|
|
512
|
+
const nextGuidance = buildNextJsGuidance(analyzeNextJs(sourceCode), mockApi);
|
|
513
|
+
if (nextGuidance)
|
|
514
|
+
parts.push(`\n${nextGuidance}`);
|
|
515
|
+
const FIX_SKELETON_THRESHOLD = 600;
|
|
516
|
+
const displaySource = sourceCode.split('\n').length > FIX_SKELETON_THRESHOLD
|
|
517
|
+
? buildSourceSkeleton(sourceCode, [])
|
|
518
|
+
: sourceCode;
|
|
519
|
+
const fixSkeletonized = displaySource !== sourceCode;
|
|
520
|
+
parts.push(`\nSOURCE FILE (what is being tested): ${sourceFile}${fixSkeletonized ? ' (large file — function bodies collapsed to signatures)' : ''}`);
|
|
521
|
+
if (sourceImportPath) {
|
|
522
|
+
parts.push(`SOURCE FILE IMPORT PATH: when importing the source in the test file, use exactly: '${sourceImportPath}'`);
|
|
523
|
+
}
|
|
524
|
+
parts.push('```');
|
|
525
|
+
parts.push(displaySource);
|
|
526
|
+
parts.push('```');
|
|
527
|
+
}
|
|
528
|
+
parts.push(`\nFAILING TEST FILE: ${testFile}`);
|
|
529
|
+
parts.push('```');
|
|
530
|
+
parts.push(testCode);
|
|
531
|
+
parts.push('```');
|
|
532
|
+
parts.push('\nFAILURE OUTPUT:');
|
|
533
|
+
parts.push('```');
|
|
534
|
+
parts.push(errorOutput.slice(0, 3000));
|
|
535
|
+
parts.push('```');
|
|
536
|
+
const realRequestWarning = detectRealRequestInError(errorOutput, mockApi);
|
|
537
|
+
if (realRequestWarning)
|
|
538
|
+
parts.push(`\n⚠️ ${realRequestWarning}`);
|
|
539
|
+
const rejectionWarning = detectUnhandledRejection(errorOutput);
|
|
540
|
+
if (rejectionWarning)
|
|
541
|
+
parts.push(`\n⚠️ ${rejectionWarning}`);
|
|
542
|
+
const rntlWarning = detectRntlErrors(errorOutput);
|
|
543
|
+
if (rntlWarning)
|
|
544
|
+
parts.push(`\n⚠️ ${rntlWarning}`);
|
|
545
|
+
const nextImportWarning = detectNextJsImportError(errorOutput, mockApi);
|
|
546
|
+
if (nextImportWarning)
|
|
547
|
+
parts.push(`\n⚠️ ${nextImportWarning}`);
|
|
548
|
+
const bleedWarning = detectThinkingBleed(errorOutput);
|
|
549
|
+
if (bleedWarning)
|
|
550
|
+
parts.push(`\n⚠️ ${bleedWarning}`);
|
|
551
|
+
const tsErrorWarning = detectTypeScriptErrors(errorOutput);
|
|
552
|
+
if (tsErrorWarning)
|
|
553
|
+
parts.push(`\n⚠️ ${tsErrorWarning}`);
|
|
554
|
+
const testHasAxiosMock = /vi\.mock\(['"]axios['"]\)/.test(testCode);
|
|
555
|
+
const sourceHasCustomInstance = sourceCode != null && /axios\.create\s*\(/.test(sourceCode);
|
|
556
|
+
if (testHasAxiosMock && sourceHasCustomInstance) {
|
|
557
|
+
parts.push("\n⚠️ WRONG MOCK PATTERN: The test mocks 'axios' directly but the source file uses axios.create().", `${mockApi}.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.');
|
|
558
|
+
}
|
|
559
|
+
parts.push('\nCommon causes to check:');
|
|
560
|
+
parts.push('- Wrong import path (use path aliases, not deep relative paths)');
|
|
561
|
+
parts.push('- Mock not set up correctly (check the shared mock file)');
|
|
562
|
+
parts.push('- Asserting on the wrong value or using the wrong matcher');
|
|
563
|
+
parts.push('- Async code not awaited');
|
|
564
|
+
parts.push('- Component/function API changed — check the source file');
|
|
565
|
+
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.');
|
|
566
|
+
parts.push('\nReturn your response in the required <thinking> + <code_output> format.');
|
|
567
|
+
return parts.join('\n');
|
|
568
|
+
}
|
|
569
|
+
// ─── Pollution fix prompt ─────────────────────────────────────────────────────
|
|
570
|
+
export function buildPollutionFixPrompt(args) {
|
|
571
|
+
const { pollutorFile, pollutorCode, victimFile, victimCode, victimError } = args;
|
|
572
|
+
const parts = [];
|
|
573
|
+
parts.push('This test file corrupts shared state and causes another test file to fail when run afterwards.');
|
|
574
|
+
parts.push('Your job: add afterEach() or afterAll() cleanup to reset whatever global state this file mutates.');
|
|
575
|
+
parts.push('');
|
|
576
|
+
parts.push('Rules:');
|
|
577
|
+
parts.push('- DO NOT remove, rewrite, or alter any existing test logic or assertions');
|
|
578
|
+
parts.push('- ONLY add cleanup hooks — nothing else');
|
|
579
|
+
parts.push('- The fix must be minimal: add the smallest afterEach/afterAll that resets the leaked state');
|
|
580
|
+
parts.push('');
|
|
581
|
+
parts.push(`POLLUTING FILE (add cleanup here): ${pollutorFile}`);
|
|
582
|
+
parts.push('```');
|
|
583
|
+
parts.push(pollutorCode);
|
|
584
|
+
parts.push('```');
|
|
585
|
+
parts.push('');
|
|
586
|
+
parts.push(`VICTIM FILE (fails when run after the polluting file): ${victimFile}`);
|
|
587
|
+
parts.push('```');
|
|
588
|
+
parts.push(victimCode);
|
|
589
|
+
parts.push('```');
|
|
590
|
+
parts.push('');
|
|
591
|
+
parts.push('ERROR the victim gets when run after this file:');
|
|
592
|
+
parts.push('```');
|
|
593
|
+
parts.push(victimError.slice(0, 2000));
|
|
594
|
+
parts.push('```');
|
|
595
|
+
parts.push('');
|
|
596
|
+
parts.push('HOW TO DIAGNOSE:');
|
|
597
|
+
parts.push("1. Read the victim's error — what value is null/undefined/wrong, or what element is missing?");
|
|
598
|
+
parts.push('2. Search the polluting file for where that thing is set or modified (localStorage, window properties, module singletons, mock state, React context, timers, environment variables)');
|
|
599
|
+
parts.push('3. Add afterEach (or afterAll) in the polluting file to reset exactly that thing');
|
|
600
|
+
parts.push('');
|
|
601
|
+
parts.push('Common cleanup patterns:');
|
|
602
|
+
parts.push(' afterEach(() => { vi.restoreAllMocks(); vi.clearAllMocks() })');
|
|
603
|
+
parts.push(' afterEach(() => { localStorage.clear(); sessionStorage.clear() })');
|
|
604
|
+
parts.push(' afterEach(() => { delete (window as any).myProperty })');
|
|
605
|
+
parts.push(' afterEach(() => { myModuleSingleton.reset() })');
|
|
606
|
+
parts.push(' afterEach(() => { vi.useRealTimers() })');
|
|
607
|
+
parts.push('');
|
|
608
|
+
parts.push('Return the complete modified polluting file in the required <thinking> + <code_output> format.');
|
|
609
|
+
return parts.join('\n');
|
|
610
|
+
}
|
|
611
|
+
export function buildRetryPrompt(failureOutput, failedAttempts = []) {
|
|
612
|
+
const parts = [];
|
|
613
|
+
if (failedAttempts.length > 0) {
|
|
614
|
+
parts.push(`You have already attempted to fix this ${failedAttempts.length} time(s). Do NOT repeat these failed approaches:`);
|
|
615
|
+
for (const a of failedAttempts) {
|
|
616
|
+
let hypContext = a.hypothesis;
|
|
617
|
+
if (hypContext) {
|
|
618
|
+
const planMatch = hypContext.match(/(?:4\.\s*WHY IT FAILED|5\.\s*PLAN)[\s\S]*/i);
|
|
619
|
+
if (planMatch) {
|
|
620
|
+
hypContext = planMatch[0];
|
|
621
|
+
}
|
|
622
|
+
else if (hypContext.length > 800) {
|
|
623
|
+
hypContext = '...' + hypContext.slice(-800);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
const hyp = hypContext ? `[${hypContext.slice(0, 1000)}]` : '(no plan recorded)';
|
|
627
|
+
parts.push(`- Attempt ${a.attemptNumber} Reasoning: ${hyp}\n Failed with: ${a.failureReason.slice(0, 800)}`);
|
|
628
|
+
}
|
|
629
|
+
parts.push('');
|
|
630
|
+
}
|
|
631
|
+
parts.push(`The tests failed. Error output:`);
|
|
632
|
+
parts.push('```');
|
|
633
|
+
parts.push(failureOutput.slice(0, 3000));
|
|
634
|
+
parts.push('```');
|
|
635
|
+
const realRequestWarning = detectRealRequestInError(failureOutput);
|
|
636
|
+
if (realRequestWarning)
|
|
637
|
+
parts.push(`\n⚠️ ${realRequestWarning}`);
|
|
638
|
+
const rejectionWarning = detectUnhandledRejection(failureOutput);
|
|
639
|
+
if (rejectionWarning)
|
|
640
|
+
parts.push(`\n⚠️ ${rejectionWarning}`);
|
|
641
|
+
const rntlRetryWarning = detectRntlErrors(failureOutput);
|
|
642
|
+
if (rntlRetryWarning)
|
|
643
|
+
parts.push(`\n⚠️ ${rntlRetryWarning}`);
|
|
644
|
+
const nextImportWarning = detectNextJsImportError(failureOutput);
|
|
645
|
+
if (nextImportWarning)
|
|
646
|
+
parts.push(`\n⚠️ ${nextImportWarning}`);
|
|
647
|
+
const bleedWarning = detectThinkingBleed(failureOutput);
|
|
648
|
+
if (bleedWarning)
|
|
649
|
+
parts.push(`\n⚠️ ${bleedWarning}`);
|
|
650
|
+
const tsErrorWarning = detectTypeScriptErrors(failureOutput);
|
|
651
|
+
if (tsErrorWarning)
|
|
652
|
+
parts.push(`\n⚠️ ${tsErrorWarning}`);
|
|
653
|
+
parts.push('');
|
|
654
|
+
parts.push('Common causes:');
|
|
655
|
+
parts.push('- Wrong import path — check the path aliases and dependency list from the original prompt');
|
|
656
|
+
parts.push('- Missing mock — if a module needs mocking, add it to the shared mock file');
|
|
657
|
+
parts.push('- Wrong 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/.');
|
|
658
|
+
parts.push('- Barrel file mock miss: if a module is re-exported from a barrel/index file, mocking the barrel will NOT intercept imports of the direct file. Mock the specific file the source actually imports. If unsure, mock both.');
|
|
659
|
+
parts.push('- Wrong API — use only methods that exist in the installed version of the library');
|
|
660
|
+
parts.push('- Type error — make sure the types match what the source file exports');
|
|
661
|
+
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.');
|
|
662
|
+
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.');
|
|
663
|
+
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.');
|
|
664
|
+
parts.push('');
|
|
665
|
+
parts.push('Fix the issue and return your response in the required <thinking> + <code_output> format.');
|
|
666
|
+
return parts.join('\n');
|
|
667
|
+
}
|
|
668
|
+
//# sourceMappingURL=index.js.map
|