lacuna-cli 0.1.6 → 0.1.8
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 +49 -11
- 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.map +1 -1
- package/dist/agent/fix-loop.js +36 -8
- package/dist/agent/fix-loop.js.map +1 -1
- package/dist/agent/generator.d.ts +5 -1
- package/dist/agent/generator.d.ts.map +1 -1
- package/dist/agent/generator.js +51 -12
- package/dist/agent/generator.js.map +1 -1
- package/dist/agent/loop.d.ts.map +1 -1
- package/dist/agent/loop.js +83 -38
- package/dist/agent/loop.js.map +1 -1
- package/dist/agent/{prompts.d.ts → prompts/index.d.ts} +4 -2
- package/dist/agent/prompts/index.d.ts.map +1 -0
- package/dist/agent/{prompts.js → prompts/index.js} +162 -341
- 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.map +1 -1
- package/dist/commands/fix.js +6 -1
- package/dist/commands/fix.js.map +1 -1
- package/dist/commands/generate.d.ts.map +1 -1
- package/dist/commands/generate.js +5 -0
- package/dist/commands/generate.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +104 -14
- package/dist/commands/init.js.map +1 -1
- package/dist/lib/coverage/gaps.d.ts.map +1 -1
- package/dist/lib/coverage/gaps.js +33 -3
- 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.map +1 -1
- package/dist/lib/detector.js +26 -12
- package/dist/lib/detector.js.map +1 -1
- package/dist/lib/feedback.d.ts +3 -0
- package/dist/lib/feedback.d.ts.map +1 -0
- package/dist/lib/feedback.js +27 -0
- package/dist/lib/feedback.js.map +1 -0
- package/dist/lib/providers/anthropic.d.ts.map +1 -1
- package/dist/lib/providers/anthropic.js +30 -1
- package/dist/lib/providers/anthropic.js.map +1 -1
- package/dist/lib/providers/openai-compatible.d.ts.map +1 -1
- package/dist/lib/providers/openai-compatible.js +35 -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/validate.d.ts.map +1 -1
- package/dist/lib/validate.js +52 -20
- package/dist/lib/validate.js.map +1 -1
- package/dist/lib/worker-display.d.ts +7 -0
- package/dist/lib/worker-display.d.ts.map +1 -1
- package/dist/lib/worker-display.js +35 -2
- package/dist/lib/worker-display.js.map +1 -1
- package/oclif.manifest.json +309 -0
- package/package.json +1 -1
- package/dist/agent/prompts.d.ts.map +0 -1
- package/dist/agent/prompts.js.map +0 -1
|
@@ -1,114 +1,113 @@
|
|
|
1
|
-
import { buildSourceSkeleton, shouldUseSkeleton, compressSource, filterMockFileForTest, filterMockFileForSource } from '
|
|
2
|
-
|
|
3
|
-
|
|
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 ────────────────────────────────────────────────
|
|
4
10
|
function extractGlobalNextMocks(setupCode) {
|
|
5
11
|
const mocked = [];
|
|
6
|
-
for (const m of setupCode.matchAll(/vi\.mock\(['"]([^'"]+)['"]/g)) {
|
|
12
|
+
for (const m of setupCode.matchAll(/(?:vi|jest)\.mock\(['"]([^'"]+)['"]/g)) {
|
|
7
13
|
mocked.push(m[1]);
|
|
8
14
|
}
|
|
9
15
|
return [...new Set(mocked)];
|
|
10
16
|
}
|
|
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
17
|
const API_IMPORT_RE = /\/(?:api|services?|requests?|http|client|network)\/|\/(?:api|axios|http|request)(?:Client|Config|Instance|Service|Helper)?(?:\/|$)|[/.]api(?:[./]|$)/i;
|
|
91
18
|
function analyzeNetworkDeps(sourceCode) {
|
|
92
19
|
const usesAxios = /\baxios\b/.test(sourceCode);
|
|
93
20
|
const usesFetch = /\bfetch\s*\(/.test(sourceCode);
|
|
94
21
|
const usesCustomInstance = /axios\.create\s*\(/.test(sourceCode);
|
|
95
|
-
// Collect local imports from paths that look like API/service modules
|
|
96
22
|
const apiModuleImports = [];
|
|
97
23
|
for (const m of sourceCode.matchAll(/from\s+['"](\.[^'"]+)['"]/g)) {
|
|
98
24
|
const path = m[1];
|
|
99
|
-
if (API_IMPORT_RE.test(path))
|
|
25
|
+
if (API_IMPORT_RE.test(path))
|
|
100
26
|
apiModuleImports.push(path);
|
|
101
|
-
}
|
|
102
27
|
}
|
|
103
28
|
return { usesAxios, usesFetch, usesCustomInstance, apiModuleImports };
|
|
104
29
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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 ──────────────────────────────────────────────────────────
|
|
112
111
|
function detectTypeScriptErrors(errorOutput) {
|
|
113
112
|
if (!/error TS\d+:/.test(errorOutput))
|
|
114
113
|
return null;
|
|
@@ -117,12 +116,9 @@ function detectTypeScriptErrors(errorOutput) {
|
|
|
117
116
|
'The TypeScript compiler tells you precisely what is wrong and usually what the fix is.',
|
|
118
117
|
'Do NOT override it with framework conventions or assumptions.',
|
|
119
118
|
];
|
|
120
|
-
// TS1378: top-level await — only specific branch because the fix is structural
|
|
121
119
|
if (/TS1378/.test(errorOutput)) {
|
|
122
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(); });');
|
|
123
121
|
}
|
|
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
122
|
const propErrors = [...errorOutput.matchAll(/'(\w+)' does not exist (?:on|in) type '\{([^']+)\}'/g)];
|
|
127
123
|
if (propErrors.length > 0) {
|
|
128
124
|
parts.push('• Wrong member name — the actual available members are:');
|
|
@@ -137,12 +133,10 @@ function detectTypeScriptErrors(errorOutput) {
|
|
|
137
133
|
parts.push(` '${wrongProp}' → not valid. Use one of: ${available.slice(0, 12).join(', ')}${available.length > 12 ? ' …' : ''}`);
|
|
138
134
|
}
|
|
139
135
|
}
|
|
140
|
-
// Compiler suggestions — TypeScript already provides the answer
|
|
141
136
|
const suggestions = [...new Set([...errorOutput.matchAll(/Did you mean(?: to write)? '(\w+)'\?/g)].map(m => m[1]))];
|
|
142
137
|
if (suggestions.length > 0) {
|
|
143
138
|
parts.push(`• Compiler suggestion: use ${suggestions.map(s => `'${s}'`).join(', ')}`);
|
|
144
139
|
}
|
|
145
|
-
// Type mismatch — extract what was passed vs what was required
|
|
146
140
|
const typeMismatches = [...errorOutput.matchAll(/Argument of type '([^']+)' is not assignable to parameter of type '([^']+)'/g)];
|
|
147
141
|
if (typeMismatches.length > 0) {
|
|
148
142
|
for (const m of typeMismatches) {
|
|
@@ -150,8 +144,6 @@ function detectTypeScriptErrors(errorOutput) {
|
|
|
150
144
|
}
|
|
151
145
|
parts.push(' (use null not undefined for nullable values; check TYPE DEFINITIONS for the required shape)');
|
|
152
146
|
}
|
|
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
147
|
const otherErrors = [...errorOutput.matchAll(/error (TS(?!1378|2339|2551|2561|2345)\d+): ([^\n]+)/g)];
|
|
156
148
|
if (otherErrors.length > 0) {
|
|
157
149
|
parts.push('• Additional compiler errors — read each one and apply the exact fix it describes:');
|
|
@@ -166,19 +158,14 @@ function detectTypeScriptErrors(errorOutput) {
|
|
|
166
158
|
}
|
|
167
159
|
return parts.join('\n');
|
|
168
160
|
}
|
|
169
|
-
// Detects thinking-bleed parse errors: model wrote reasoning inside <code_output>
|
|
170
|
-
// causing the file to start with prose instead of TypeScript.
|
|
171
161
|
function detectThinkingBleed(errorOutput) {
|
|
172
|
-
// Vitest/esbuild parse errors at line 1 with non-code content are a strong signal
|
|
173
162
|
const parseErr = errorOutput.match(/PARSE_ERROR|Unexpected token|SyntaxError.*\b1:\d+\b/);
|
|
174
163
|
if (!parseErr)
|
|
175
164
|
return null;
|
|
176
|
-
// Check if the error context shows non-TypeScript at the file start
|
|
177
165
|
const contextLine = errorOutput.match(/^\s*1\s*[│|]\s*(.+)/m);
|
|
178
166
|
if (!contextLine)
|
|
179
167
|
return null;
|
|
180
168
|
const firstLine = contextLine[1].trim();
|
|
181
|
-
// If first line looks like prose (no import/export/const/vi./etc.)
|
|
182
169
|
if (/^(import|export|const|let|var|\/\/|\/\*|describe|it\s*\(|test\s*\(|vi\.|jest\.)/.test(firstLine))
|
|
183
170
|
return null;
|
|
184
171
|
return [
|
|
@@ -189,43 +176,6 @@ function detectThinkingBleed(errorOutput) {
|
|
|
189
176
|
`Do NOT continue thinking inside <code_output> under any circumstances.`,
|
|
190
177
|
].join('\n');
|
|
191
178
|
}
|
|
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 isServerOnly = importPath === 'server-only';
|
|
199
|
-
const isAlias = importPath.startsWith('@/');
|
|
200
|
-
const isClient = importPath.endsWith('.client');
|
|
201
|
-
const isServer = importPath.endsWith('.server');
|
|
202
|
-
const isNextInternal = importPath.startsWith('next/');
|
|
203
|
-
const isProviderOrSession = /session|auth|provider/i.test(importPath);
|
|
204
|
-
if (!isServerOnly && !isAlias && !isClient && !isServer && !isNextInternal && !isProviderOrSession)
|
|
205
|
-
return null;
|
|
206
|
-
const lines = [
|
|
207
|
-
`IMPORT RESOLUTION ERROR — Vitest cannot resolve "${importPath}".`,
|
|
208
|
-
];
|
|
209
|
-
if (isServerOnly) {
|
|
210
|
-
lines.push(`"server-only" is a Next.js build guard — it throws intentionally when server code is loaded in a non-server context.`, `This is a CONFIGURATION issue, not something fixable in the test file. Two options:`, ` OPTION A (preferred): Add an alias in vitest.config.ts that maps "server-only" to an empty module:`, ` alias: { 'server-only': path.resolve(__dirname, './test/empty-module.ts') }`, ` and create test/empty-module.ts containing: export default {}`, ` OPTION B: Mock the entire module that imports server-only (the hook or service) so Vitest never resolves its dependency tree.`, `Do NOT try to mock "server-only" directly in the test file — vi.mock('server-only') is too late; the import is resolved before mocks run.`);
|
|
211
|
-
}
|
|
212
|
-
else if (isAlias) {
|
|
213
|
-
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.`);
|
|
214
|
-
}
|
|
215
|
-
else if (isClient || isServer) {
|
|
216
|
-
lines.push(`This is a Next.js ${isClient ? 'client' : 'server'} boundary file. Vitest never resolves these.`, `If the source file IMPORTS this module directly, mock it with the exact import path:`, ` vi.mock('${importPath}', () => ({ myFn: vi.fn() }))`, ``, `If the failing source is a HOOK that internally imports *.client services (which chain into many other server-only modules),`, `use the complete self-replacement strategy — mock the entire hook, not its sub-dependencies:`, ` const mockState = vi.hoisted(() => ({ data: [], loading: false, error: null }))`, ` vi.mock('../useMyHook', () => ({`, ` useMyHook: () => mockState,`, ` }))`, `This bypasses the entire dependency tree and tests the component contract, not the hook internals.`);
|
|
217
|
-
}
|
|
218
|
-
else if (isNextInternal) {
|
|
219
|
-
lines.push(`${importPath} is a Next.js internal that does not work in Vitest. Mock it:`, ` vi.mock('${importPath}', () => ({ /* relevant exports as vi.fn() */ }))`);
|
|
220
|
-
}
|
|
221
|
-
else if (isProviderOrSession) {
|
|
222
|
-
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' })),`, ` }))`);
|
|
223
|
-
}
|
|
224
|
-
return lines.join('\n');
|
|
225
|
-
}
|
|
226
|
-
// Scans error output for an unhandled rejection caused by mockRejectedValueOnce.
|
|
227
|
-
// Vitest surfaces this as a top-level "Unhandled Rejection" or "Vitest caught N unhandled error(s)"
|
|
228
|
-
// even when the component catches the error internally — the test never awaited the error state.
|
|
229
179
|
function detectUnhandledRejection(errorOutput) {
|
|
230
180
|
const hasUnhandled = /unhandled\s+(promise\s+)?rejection|vitest caught \d+ unhandled/i.test(errorOutput);
|
|
231
181
|
const hasRejectedMock = /mockRejectedValue(Once)?/.test(errorOutput);
|
|
@@ -239,8 +189,7 @@ function detectUnhandledRejection(errorOutput) {
|
|
|
239
189
|
'This chains the rejection inside the test scope. Without it, Vitest flags it as unhandled even if the UI handles it correctly.',
|
|
240
190
|
].join('\n');
|
|
241
191
|
}
|
|
242
|
-
|
|
243
|
-
function detectRealRequestInError(errorOutput) {
|
|
192
|
+
function detectRealRequestInError(errorOutput, mockApi = 'vi') {
|
|
244
193
|
const hasRealUrl = /https?:\/\/[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/.test(errorOutput);
|
|
245
194
|
const hasHttpStatus = /\bstatus:\s*[45]\d\d\b/.test(errorOutput);
|
|
246
195
|
const hasNetworkError = /ECONNREFUSED|ENOTFOUND|ETIMEDOUT|network\s+error/i.test(errorOutput);
|
|
@@ -253,118 +202,30 @@ function detectRealRequestInError(errorOutput) {
|
|
|
253
202
|
const urlMatch = errorOutput.match(/https?:\/\/[^\s,'")\]}]+/);
|
|
254
203
|
if (urlMatch)
|
|
255
204
|
lines.push(`Intercepted URL: ${urlMatch[0]}`);
|
|
256
|
-
lines.push('Required fix: find which module the source file imports for its API calls and mock THAT module.',
|
|
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.`);
|
|
257
206
|
return lines.join('\n');
|
|
258
207
|
}
|
|
259
|
-
|
|
260
|
-
if (!analysis.usesAxios && !analysis.usesFetch && analysis.apiModuleImports.length === 0) {
|
|
261
|
-
return null;
|
|
262
|
-
}
|
|
263
|
-
const lines = [
|
|
264
|
-
'NETWORK MOCKING (critical — a real HTTP request reaching the network is a test bug):',
|
|
265
|
-
];
|
|
266
|
-
if (analysis.apiModuleImports.length > 0) {
|
|
267
|
-
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.`);
|
|
268
|
-
}
|
|
269
|
-
if (analysis.usesCustomInstance) {
|
|
270
|
-
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.`);
|
|
271
|
-
}
|
|
272
|
-
else if (analysis.usesAxios && analysis.apiModuleImports.length === 0) {
|
|
273
|
-
lines.push(`The source imports axios directly. Mock it with: vi.mock('axios') and set return values with axios.get.mockResolvedValue({ data: ... })`);
|
|
274
|
-
}
|
|
275
|
-
if (analysis.usesFetch) {
|
|
276
|
-
lines.push(`The source uses fetch. Mock it with: vi.spyOn(global, 'fetch').mockResolvedValue(new Response(JSON.stringify(data)))`);
|
|
277
|
-
}
|
|
278
|
-
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.`);
|
|
279
|
-
return lines.join('\n');
|
|
280
|
-
}
|
|
281
|
-
function parseMockInventory(code) {
|
|
282
|
-
const entries = [];
|
|
283
|
-
const lines = code.split('\n');
|
|
284
|
-
for (let i = 0; i < lines.length; i++) {
|
|
285
|
-
const mockMatch = lines[i].match(/\bvi\.mock\(\s*(['"])([^'"]+)\1/);
|
|
286
|
-
if (!mockMatch)
|
|
287
|
-
continue;
|
|
288
|
-
const modulePath = mockMatch[2];
|
|
289
|
-
const lineNumber = i + 1;
|
|
290
|
-
const exports = [];
|
|
291
|
-
// Scan forward up to 80 lines to find the factory () => ({ ... })
|
|
292
|
-
// and extract the top-level key names from the object literal.
|
|
293
|
-
let braceDepth = 0;
|
|
294
|
-
let inFactory = false;
|
|
295
|
-
for (let j = i; j < Math.min(i + 80, lines.length); j++) {
|
|
296
|
-
const l = lines[j];
|
|
297
|
-
if (!inFactory && /\(\)\s*=>\s*\(?\s*\{/.test(l))
|
|
298
|
-
inFactory = true;
|
|
299
|
-
if (!inFactory)
|
|
300
|
-
continue;
|
|
301
|
-
for (const ch of l) {
|
|
302
|
-
if (ch === '{')
|
|
303
|
-
braceDepth++;
|
|
304
|
-
if (ch === '}')
|
|
305
|
-
braceDepth--;
|
|
306
|
-
}
|
|
307
|
-
// Top-level keys: indented 2+ spaces (multi-line). Capture both key and variable value
|
|
308
|
-
// so the inventory shows key(Variable) — e.g. ValidationError(MockValidationError).
|
|
309
|
-
// This lets the AI look up exactly which export to import for a given mocked symbol.
|
|
310
|
-
const multiLine = l.match(/^\s{2,}(\w+)\s*:\s*(\w+)/);
|
|
311
|
-
if (multiLine && multiLine[1] !== 'type') {
|
|
312
|
-
const key = multiLine[1], val = multiLine[2];
|
|
313
|
-
exports.push(val && val !== key ? `${key}(${val})` : key);
|
|
314
|
-
}
|
|
315
|
-
// Inline single-line factory: vi.mock('x', () => ({ key: mockVar, ... }))
|
|
316
|
-
if (j === i || l.includes('() =>')) {
|
|
317
|
-
for (const m of l.matchAll(/\b(\w+)\s*:\s*(mock\w+)/gi)) {
|
|
318
|
-
const key = m[1], val = m[2];
|
|
319
|
-
const entry = val.toLowerCase() !== `mock${key.toLowerCase()}` ? `${key}(${val})` : key;
|
|
320
|
-
if (!exports.some(e => e === key || e.startsWith(`${key}(`)))
|
|
321
|
-
exports.push(entry);
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
if (braceDepth <= 0 && inFactory)
|
|
325
|
-
break;
|
|
326
|
-
}
|
|
327
|
-
entries.push({ modulePath, lineNumber, exports });
|
|
328
|
-
}
|
|
329
|
-
return entries;
|
|
330
|
-
}
|
|
331
|
-
// Extract exported names from a mocks file so the AI sees a concrete inventory.
|
|
332
|
-
function parseMockExports(code) {
|
|
333
|
-
const names = [];
|
|
334
|
-
// export const/let/var/function/class/async function name
|
|
335
|
-
for (const m of code.matchAll(/^export\s+(?:const|let|var|function|class|async\s+function)\s+(\w+)/gm)) {
|
|
336
|
-
names.push(m[1]);
|
|
337
|
-
}
|
|
338
|
-
// export { name1, name2 as alias2, ... }
|
|
339
|
-
for (const m of code.matchAll(/^export\s*\{([^}]+)\}/gm)) {
|
|
340
|
-
for (const part of m[1].split(',')) {
|
|
341
|
-
const alias = part.trim().split(/\s+as\s+/).pop()?.trim();
|
|
342
|
-
if (alias && /^\w+$/.test(alias))
|
|
343
|
-
names.push(alias);
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
// export default identifier
|
|
347
|
-
const defM = code.match(/^export\s+default\s+(\w+)/m);
|
|
348
|
-
if (defM)
|
|
349
|
-
names.push(`default (${defM[1]})`);
|
|
350
|
-
return [...new Set(names)];
|
|
351
|
-
}
|
|
208
|
+
// ─── System prompt ────────────────────────────────────────────────────────────
|
|
352
209
|
export function buildSystemPrompt(env) {
|
|
353
210
|
const isJS = env.language === 'typescript' || env.language === 'javascript' || env.language === 'unknown';
|
|
354
211
|
const isTS = env.language === 'typescript';
|
|
355
212
|
const isVitest = env.testRunner === 'vitest';
|
|
356
213
|
const isJSRunner = env.testRunner === 'jest' || env.testRunner === 'vitest' || env.testRunner === 'mocha';
|
|
357
|
-
|
|
214
|
+
const mockApi = isVitest ? 'vi' : 'jest';
|
|
358
215
|
const mockAuditStep = isJS ? `
|
|
359
216
|
2. MOCK AUDIT — do this before writing a single line of test code:
|
|
360
|
-
a)
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
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.
|
|
368
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.
|
|
369
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.
|
|
370
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.
|
|
@@ -378,39 +239,31 @@ export function buildSystemPrompt(env) {
|
|
|
378
239
|
1. WHAT IS NEEDED: What functions/behaviors are untested or broken?${mockAuditStep}
|
|
379
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?
|
|
380
241
|
5. PLAN: List the exact steps you will take before writing a single line of code.`;
|
|
381
|
-
// ── JS/TS-specific rules ─────────────────────────────────────────────────────
|
|
382
242
|
const jsRules = isJS ? `
|
|
383
243
|
3. Use path aliases from the PROJECT TYPESCRIPT CONFIG section in IMPORT statements (e.g. "@/components/Button" not "../../components/Button").
|
|
384
244
|
EXCEPTION — mock call paths: use the exact same path string that appears in the SOURCE FILE'S import statement.
|
|
385
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.
|
|
386
246
|
Never second-guess the pre-computed paths. Never convert them back to @/ aliases in mock calls.
|
|
387
247
|
4. Only import from packages listed in PROJECT DEPENDENCIES. Do not invent packages that are not listed.
|
|
388
|
-
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
|
|
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.
|
|
389
249
|
CRITICAL — never rename or change the casing of existing mock exports.
|
|
390
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.
|
|
391
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.
|
|
392
|
-
CRITICAL — the mocks file must contain ONLY:
|
|
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.
|
|
393
255
|
8. If a TEST SETUP FILE is shown, assume its globals and matchers are already available. Do NOT import or re-declare them.` : `
|
|
394
256
|
3. Use the project's import conventions as shown in the source file and existing tests.
|
|
395
257
|
4. Only import from packages listed in PROJECT DEPENDENCIES. Do not invent packages that are not listed.`;
|
|
396
|
-
const tsRule = isTS ?
|
|
397
|
-
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.
|
|
398
|
-
- 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.
|
|
399
|
-
- Import enums, constants, interfaces, and types from the project's existing files. Do NOT redeclare them inline or invent lookalike values.
|
|
400
|
-
- Never use "as any", "@ts-ignore", or "@ts-expect-error" to suppress type errors.
|
|
401
|
-
- 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>>().
|
|
402
|
-
- Use ReturnType<>, Parameters<>, and other utility types to derive mock types from real function signatures.
|
|
403
|
-
- When accessing optional properties, handle null/undefined correctly.` : '';
|
|
258
|
+
const tsRule = isTS ? buildTsRule(mockApi) : '';
|
|
404
259
|
const ruleCount = isTS ? 10 : (isJS ? 9 : 6);
|
|
405
|
-
// ── JS/Vitest-specific output format rules ───────────────────────────────────
|
|
406
260
|
const jsOutputRules = isJS ? `
|
|
407
261
|
${ruleCount + 2}. Inside <code_output>: output ONLY the test file content (or test file // ---MOCKS_FILE--- mocks file).
|
|
408
262
|
If you use // ---MOCKS_FILE---, everything AFTER the separator is the mocks file. The mocks file must contain ONLY:
|
|
409
|
-
|
|
263
|
+
${mockApi}.fn() mock definitions, ${mockApi}.mock() module stubs, shared constants, and beforeEach resets.
|
|
410
264
|
NEVER put describe(), it(), test(), or expect() calls after the separator.
|
|
411
265
|
${ruleCount + 3}. NEVER output vitest.config.ts, jest.config.js, or any framework configuration. If an import cannot be resolved,
|
|
412
|
-
fix it by mocking it with
|
|
413
|
-
// ── Common failure causes — universal ────────────────────────────────────────
|
|
266
|
+
fix it by mocking it with ${mockApi}.mock() — NOT by modifying the test runner configuration.` : '';
|
|
414
267
|
const universalCauses = `- Wrong import paths (use the project's conventions — aliases where configured, relative paths otherwise)
|
|
415
268
|
- Importing from test utilities that are not in the dependency list
|
|
416
269
|
- Mocking modules that are already mocked in the setup file
|
|
@@ -418,40 +271,9 @@ ${ruleCount + 3}. NEVER output vitest.config.ts, jest.config.js, or any framewor
|
|
|
418
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.
|
|
419
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.
|
|
420
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.`;
|
|
421
|
-
|
|
422
|
-
const
|
|
423
|
-
|
|
424
|
-
- Missing mock is the silent killer: if expect(mockFn).toHaveBeenCalled() fails but the code path is clearly reached, the mock declaration is missing or uses the wrong path. The real implementation ran instead.
|
|
425
|
-
- Response envelope mismatch (silent empty state): if a hook guards \`if (res.success)\` or destructures \`{ success, data }\`, mocking with a raw array means .success is undefined and state is never populated — silently. Trace each call site individually — different clients in the same hook often have different shapes (one returns \`{ data: [] }\`, another \`{ success, data: { items: [] } }\` with nested access). Never assume a uniform envelope across the whole hook.
|
|
426
|
-
- Barrel file mock miss: mocking a barrel/index file (vi.mock('../components')) will NOT intercept imports of the direct file ('../components/Foo'). Mock the specific module the source actually imports.` : '';
|
|
427
|
-
const vitestCauses = isVitest ? `
|
|
428
|
-
- Never use require() in Vitest: Vitest runs files as ESM — dynamic require() fails at transform time. Always use static ES import + vi.mocked().
|
|
429
|
-
- Shared mock file factory syntax: \`vi.mock('../service', async () => await import('../../test/mocks'))\` — synchronous factory cannot import external files in Vitest.
|
|
430
|
-
- vi.hoisted() for shared mock references: when a mock object is created inside vi.mock() AND configured in beforeEach, use vi.hoisted() so both closures reference the same object instance.
|
|
431
|
-
- Complete hook self-replacement for *.client-importing hooks: when a hook imports *.client services that chain into browser/server-only modules, mock the entire hook — \`vi.mock('../useMyHook', () => ({ useMyHook: vi.fn() }))\` — rather than each sub-dependency.
|
|
432
|
-
- server-only resolution in Next.js: \`Failed to resolve import "server-only"\` is a config issue, not a test issue. Add an alias in vitest.config.ts: \`'server-only': path.resolve(__dirname, './test/empty-module.ts')\` and create that file with \`export default {}\`.
|
|
433
|
-
- 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 () => { ... })\`.
|
|
434
|
-
- NEVER call vi.spyOn(global, ...) or vi.spyOn(globalThis, ...) at module level (outside beforeEach/beforeAll). Each Vitest file has its own vi registry. A module-level spy is installed on the shared worker globalThis but setup-file afterEach cleanup (vi.restoreAllMocks) uses a different vi instance and cannot remove it — the spy persists after the file ends and poisons the next file that runs in the same worker. Always create global spies inside beforeEach so they are fresh per test and properly cleaned up:
|
|
435
|
-
WRONG: const mockFetch = vi.spyOn(global, 'fetch'); // module level — breaks other files
|
|
436
|
-
RIGHT: let mockFetch: ReturnType<typeof vi.spyOn>;
|
|
437
|
-
beforeEach(() => { mockFetch = vi.spyOn(global, 'fetch'); });
|
|
438
|
-
- Never write two vi.mock() calls for the same module path in the same file. The second call silently overrides the first — exports from the first mock are lost. If a module exports many things (e.g. lucide-react icons), list them all in a single vi.mock() factory:
|
|
439
|
-
WRONG: vi.mock('lucide-react', () => ({ Search: () => null })) ...later... vi.mock('lucide-react', () => ({ Plus: () => null }))
|
|
440
|
-
RIGHT: vi.mock('lucide-react', () => ({ Search: () => null, Plus: () => null }))
|
|
441
|
-
- vi.resetAllMocks() vs vi.clearAllMocks() — use the right one or tests break under shuffle:
|
|
442
|
-
clearAllMocks() only wipes call history (mock.calls, mock.results). It does NOT reset mockReturnValue / mockResolvedValue / mockImplementation.
|
|
443
|
-
resetAllMocks() wipes call history AND resets all implementations back to undefined.
|
|
444
|
-
RULE: if ANY test in the file calls mockFn.mockImplementation(...) directly (e.g. a loading-state test that uses \`() => new Promise(() => {})\` to freeze async), the beforeEach MUST use vi.resetAllMocks() — otherwise that implementation bleeds into the next test when run in shuffle order, and mockResolvedValue set in beforeEach is silently overridden by the stale mockImplementation.
|
|
445
|
-
PATTERN: loading-state test sets mockImplementation → test passes → shuffle puts it before the success-state test → success test's beforeEach calls clearAllMocks (history wiped, implementation NOT wiped) → mockResolvedValue set in beforeEach loses to the stale never-resolving implementation → success test gets 0 instead of expected data.` : '';
|
|
446
|
-
// ── Common failure causes — React/Vue (JS component tests) only ───────────────
|
|
447
|
-
const reactCauses = isJSRunner ? `
|
|
448
|
-
- React 18 act() async rule: ALWAYS await act() when it wraps async code. Unawaited act() calls cause state to leak across tests, producing "Cannot read properties of null" failures in unrelated tests.
|
|
449
|
-
- Loading state architecture: before asserting a button is disabled during loading, check whether the component unmounts it entirely. If unmounted, getByText("Submit") throws — test for the spinner instead.
|
|
450
|
-
- Unhandled promise rejections: after triggering an action with mockRejectedValueOnce, always await the resulting state change with waitFor() so the rejection is resolved inside the test scope.
|
|
451
|
-
- getByText / getByTestId ambiguity: generic strings and reused icon components often appear multiple times on a complex page. Use getAllByText(...)[0], getByRole, or within(container).getByText(...) to scope queries.
|
|
452
|
-
- Functional state updater assertions: when a component calls setState with an updater function (e.g. setPage(p => p + 1)), toHaveBeenCalledWith(3) always fails. Capture the updater and call it: \`const fn = mockSetPage.mock.calls[0][0]; expect(fn(2)).toBe(3)\`.
|
|
453
|
-
- React 18 act() warning in hook tests: wrap async mock resolutions in \`await act(async () => {})\` at the end of the test to flush pending state updates before the test exits.` : '';
|
|
454
|
-
// ── Good test suite checklist ────────────────────────────────────────────────
|
|
274
|
+
const jsCauses = isJSRunner ? buildJsCauses(mockApi) : '';
|
|
275
|
+
const vitestCauses = isVitest ? buildVitestCauses() : '';
|
|
276
|
+
const reactCauses = buildReactCauses(isJSRunner, mockApi);
|
|
455
277
|
const hookSuiteNote = isJSRunner
|
|
456
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.`
|
|
457
279
|
: '';
|
|
@@ -486,9 +308,11 @@ Test file pattern for this project: ${env.testFilePattern}
|
|
|
486
308
|
|
|
487
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.`;
|
|
488
310
|
}
|
|
311
|
+
// ─── Generate prompt ──────────────────────────────────────────────────────────
|
|
489
312
|
export function buildGeneratePrompt(args) {
|
|
490
|
-
const { sourceFile, existingTestCode, uncoveredFunctions, uncoveredLines, sourceImportPath, mocksCode, mocksImportPath, setupFileCode, packageDeps, tsconfigPaths, typeDefinitions, localImportPaths, reactMajorVersion, projectMemory, } = args;
|
|
313
|
+
const { sourceFile, env, existingTestCode, uncoveredFunctions, uncoveredLines, sourceImportPath, mocksCode, mocksImportPath, setupFileCode, packageDeps, tsconfigPaths, typeDefinitions, localImportPaths, localImportContents, reactMajorVersion, projectMemory, } = args;
|
|
491
314
|
const sourceCode = compressSource(args.sourceCode);
|
|
315
|
+
const mockApi = env.testRunner === 'vitest' ? 'vi' : 'jest';
|
|
492
316
|
const parts = [];
|
|
493
317
|
if (projectMemory) {
|
|
494
318
|
parts.push(projectMemory);
|
|
@@ -508,7 +332,7 @@ export function buildGeneratePrompt(args) {
|
|
|
508
332
|
parts.push(tsconfigPaths);
|
|
509
333
|
}
|
|
510
334
|
if (localImportPaths && localImportPaths.length > 0) {
|
|
511
|
-
parts.push(
|
|
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.):`);
|
|
512
336
|
for (const p of localImportPaths)
|
|
513
337
|
parts.push(` ${p}`);
|
|
514
338
|
}
|
|
@@ -518,10 +342,16 @@ export function buildGeneratePrompt(args) {
|
|
|
518
342
|
parts.push(typeDefinitions);
|
|
519
343
|
parts.push('```');
|
|
520
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
|
+
}
|
|
521
351
|
if (setupFileCode) {
|
|
522
352
|
const nextMocked = extractGlobalNextMocks(setupFileCode);
|
|
523
353
|
const setupNote = nextMocked.length > 0
|
|
524
|
-
? `\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
|
|
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(', ')}`
|
|
525
355
|
: `\nTEST SETUP FILE (already loaded before every test — do NOT import it again):`;
|
|
526
356
|
parts.push(setupNote);
|
|
527
357
|
parts.push('```');
|
|
@@ -539,40 +369,32 @@ export function buildGeneratePrompt(args) {
|
|
|
539
369
|
const inventory = parseMockInventory(mocksCode);
|
|
540
370
|
if (inventory.length > 0) {
|
|
541
371
|
const maxLen = Math.max(...inventory.map(e => e.modulePath.length));
|
|
542
|
-
parts.push(
|
|
372
|
+
parts.push(`\nMOCK MODULE INVENTORY — modules already ${mockApi}.mocked:`);
|
|
543
373
|
for (const entry of inventory) {
|
|
544
374
|
const path = `'${entry.modulePath}'`.padEnd(maxLen + 2);
|
|
545
|
-
const exp = entry.exports.length > 0
|
|
546
|
-
? entry.exports.join(', ')
|
|
547
|
-
: '(no simple key exports)';
|
|
375
|
+
const exp = entry.exports.length > 0 ? entry.exports.join(', ') : '(no simple key exports)';
|
|
548
376
|
parts.push(` ${path} → ${exp}`);
|
|
549
377
|
}
|
|
550
378
|
parts.push('MOCK EDITING RULES — follow exactly when returning a ---MOCKS_FILE--- block:');
|
|
551
|
-
parts.push(
|
|
552
|
-
parts.push(
|
|
553
|
-
parts.push(
|
|
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.`);
|
|
554
382
|
}
|
|
555
|
-
// Generate prompt: send only the sections of the mock file relevant to this source file
|
|
556
|
-
// (vi.mock() blocks for modules it imports + export declarations for inferred mock names).
|
|
557
|
-
// Skips unrelated mocks entirely. Falls back to the full file if nothing matches.
|
|
558
383
|
const relevantMocks = filterMockFileForSource(mocksCode, sourceCode);
|
|
559
384
|
if (relevantMocks !== mocksCode || relevantMocks.split('\n').length < 80) {
|
|
560
|
-
// Filtered down or already small — worth sending
|
|
561
385
|
parts.push('```');
|
|
562
386
|
parts.push(relevantMocks);
|
|
563
387
|
parts.push('```');
|
|
564
388
|
}
|
|
565
|
-
// If filter returned the full file unchanged AND it's large, skip it —
|
|
566
|
-
// the exports list + inventory above already cover what the AI needs.
|
|
567
389
|
}
|
|
568
390
|
else {
|
|
569
|
-
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
|
|
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.`);
|
|
570
392
|
}
|
|
571
393
|
}
|
|
572
|
-
const networkGuidance = buildNetworkMockingGuidance(analyzeNetworkDeps(sourceCode), sourceFile);
|
|
394
|
+
const networkGuidance = buildNetworkMockingGuidance(analyzeNetworkDeps(sourceCode), sourceFile, mockApi);
|
|
573
395
|
if (networkGuidance)
|
|
574
396
|
parts.push(`\n${networkGuidance}`);
|
|
575
|
-
const nextGuidance = buildNextJsGuidance(analyzeNextJs(sourceCode));
|
|
397
|
+
const nextGuidance = buildNextJsGuidance(analyzeNextJs(sourceCode), mockApi);
|
|
576
398
|
if (nextGuidance)
|
|
577
399
|
parts.push(`\n${nextGuidance}`);
|
|
578
400
|
if (detectReactNative(packageDeps ?? null))
|
|
@@ -606,9 +428,11 @@ export function buildGeneratePrompt(args) {
|
|
|
606
428
|
parts.push('\nWrite the complete test file now.');
|
|
607
429
|
return parts.join('\n');
|
|
608
430
|
}
|
|
431
|
+
// ─── Fix prompt ───────────────────────────────────────────────────────────────
|
|
609
432
|
export function buildFixPrompt(args) {
|
|
610
|
-
const { testFile, testCode, sourceFile, sourceImportPath, errorOutput, mocksCode, mocksImportPath, setupFileCode, packageDeps, tsconfigPaths, typeDefinitions, localImportPaths, reactMajorVersion, projectMemory } = args;
|
|
433
|
+
const { testFile, testCode, sourceFile, sourceImportPath, errorOutput, env, mocksCode, mocksImportPath, setupFileCode, packageDeps, tsconfigPaths, typeDefinitions, localImportPaths, reactMajorVersion, projectMemory } = args;
|
|
611
434
|
const sourceCode = args.sourceCode ? compressSource(args.sourceCode) : null;
|
|
435
|
+
const mockApi = env.testRunner === 'vitest' ? 'vi' : 'jest';
|
|
612
436
|
const parts = [];
|
|
613
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.');
|
|
614
438
|
parts.push('');
|
|
@@ -630,7 +454,7 @@ export function buildFixPrompt(args) {
|
|
|
630
454
|
parts.push(tsconfigPaths);
|
|
631
455
|
}
|
|
632
456
|
if (localImportPaths && localImportPaths.length > 0) {
|
|
633
|
-
parts.push(
|
|
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.):`);
|
|
634
458
|
for (const p of localImportPaths)
|
|
635
459
|
parts.push(` ${p}`);
|
|
636
460
|
}
|
|
@@ -649,39 +473,32 @@ export function buildFixPrompt(args) {
|
|
|
649
473
|
if (mocksImportPath) {
|
|
650
474
|
if (mocksCode) {
|
|
651
475
|
const exports = parseMockExports(mocksCode);
|
|
652
|
-
// Filter to only the sections this failing test actually imports.
|
|
653
|
-
// A billing test importing 6 mocks gets ~40 lines instead of 820.
|
|
654
|
-
// The filter already does the heavy lifting — no compression needed on top.
|
|
655
|
-
// Line numbers in the inventory refer to lines in THIS filtered slice.
|
|
656
476
|
const compressed = filterMockFileForTest(mocksCode, args.testCode);
|
|
657
477
|
parts.push(`\nSHARED MOCK FILE (import from: '${mocksImportPath}')`);
|
|
658
478
|
if (exports.length > 0) {
|
|
659
479
|
parts.push(`Available exports: ${exports.join(', ')}`);
|
|
660
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.`);
|
|
661
481
|
}
|
|
662
|
-
// Parse inventory from the compressed file so line numbers match what the AI sees below
|
|
663
482
|
const inventory = parseMockInventory(compressed);
|
|
664
483
|
if (inventory.length > 0) {
|
|
665
484
|
const maxLen = Math.max(...inventory.map(e => e.modulePath.length));
|
|
666
|
-
parts.push(
|
|
485
|
+
parts.push(`\nMOCK MODULE INVENTORY — modules already ${mockApi}.mocked (line numbers refer to the file below):`);
|
|
667
486
|
for (const entry of inventory) {
|
|
668
487
|
const path = `'${entry.modulePath}'`.padEnd(maxLen + 2);
|
|
669
|
-
const exp = entry.exports.length > 0
|
|
670
|
-
? entry.exports.join(', ')
|
|
671
|
-
: '(no simple key exports)';
|
|
488
|
+
const exp = entry.exports.length > 0 ? entry.exports.join(', ') : '(no simple key exports)';
|
|
672
489
|
parts.push(` Line ${String(entry.lineNumber).padStart(4)}: ${path} → ${exp}`);
|
|
673
490
|
}
|
|
674
491
|
parts.push('MOCK EDITING RULES — follow exactly when returning a ---MOCKS_FILE--- block:');
|
|
675
|
-
parts.push(
|
|
676
|
-
parts.push(
|
|
677
|
-
parts.push(
|
|
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.`);
|
|
678
495
|
}
|
|
679
496
|
parts.push('```');
|
|
680
497
|
parts.push(compressed);
|
|
681
498
|
parts.push('```');
|
|
682
499
|
}
|
|
683
500
|
else {
|
|
684
|
-
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
|
|
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.`);
|
|
685
502
|
}
|
|
686
503
|
}
|
|
687
504
|
if (detectReactNative(packageDeps ?? null))
|
|
@@ -689,14 +506,12 @@ export function buildFixPrompt(args) {
|
|
|
689
506
|
else if (detectVue(packageDeps ?? null))
|
|
690
507
|
parts.push(`\n${buildVueGuidance()}`);
|
|
691
508
|
if (sourceFile && sourceCode) {
|
|
692
|
-
const networkGuidance = buildNetworkMockingGuidance(analyzeNetworkDeps(sourceCode), sourceFile);
|
|
509
|
+
const networkGuidance = buildNetworkMockingGuidance(analyzeNetworkDeps(sourceCode), sourceFile, mockApi);
|
|
693
510
|
if (networkGuidance)
|
|
694
511
|
parts.push(`\n${networkGuidance}`);
|
|
695
|
-
const nextGuidance = buildNextJsGuidance(analyzeNextJs(sourceCode));
|
|
512
|
+
const nextGuidance = buildNextJsGuidance(analyzeNextJs(sourceCode), mockApi);
|
|
696
513
|
if (nextGuidance)
|
|
697
514
|
parts.push(`\n${nextGuidance}`);
|
|
698
|
-
// For fix: show the full source so the AI can see exact return shapes, field names,
|
|
699
|
-
// and mock structure. Only skeleton truly enormous files to stay within token limits.
|
|
700
515
|
const FIX_SKELETON_THRESHOLD = 600;
|
|
701
516
|
const displaySource = sourceCode.split('\n').length > FIX_SKELETON_THRESHOLD
|
|
702
517
|
? buildSourceSkeleton(sourceCode, [])
|
|
@@ -718,13 +533,16 @@ export function buildFixPrompt(args) {
|
|
|
718
533
|
parts.push('```');
|
|
719
534
|
parts.push(errorOutput.slice(0, 3000));
|
|
720
535
|
parts.push('```');
|
|
721
|
-
const realRequestWarning = detectRealRequestInError(errorOutput);
|
|
536
|
+
const realRequestWarning = detectRealRequestInError(errorOutput, mockApi);
|
|
722
537
|
if (realRequestWarning)
|
|
723
538
|
parts.push(`\n⚠️ ${realRequestWarning}`);
|
|
724
539
|
const rejectionWarning = detectUnhandledRejection(errorOutput);
|
|
725
540
|
if (rejectionWarning)
|
|
726
541
|
parts.push(`\n⚠️ ${rejectionWarning}`);
|
|
727
|
-
const
|
|
542
|
+
const rntlWarning = detectRntlErrors(errorOutput);
|
|
543
|
+
if (rntlWarning)
|
|
544
|
+
parts.push(`\n⚠️ ${rntlWarning}`);
|
|
545
|
+
const nextImportWarning = detectNextJsImportError(errorOutput, mockApi);
|
|
728
546
|
if (nextImportWarning)
|
|
729
547
|
parts.push(`\n⚠️ ${nextImportWarning}`);
|
|
730
548
|
const bleedWarning = detectThinkingBleed(errorOutput);
|
|
@@ -733,11 +551,10 @@ export function buildFixPrompt(args) {
|
|
|
733
551
|
const tsErrorWarning = detectTypeScriptErrors(errorOutput);
|
|
734
552
|
if (tsErrorWarning)
|
|
735
553
|
parts.push(`\n⚠️ ${tsErrorWarning}`);
|
|
736
|
-
// Detect wrong mock pattern: test uses vi.mock('axios') but source uses axios.create()
|
|
737
554
|
const testHasAxiosMock = /vi\.mock\(['"]axios['"]\)/.test(testCode);
|
|
738
555
|
const sourceHasCustomInstance = sourceCode != null && /axios\.create\s*\(/.test(sourceCode);
|
|
739
556
|
if (testHasAxiosMock && sourceHasCustomInstance) {
|
|
740
|
-
parts.push("\n⚠️ WRONG MOCK PATTERN: The test mocks 'axios' directly but the source file uses axios.create().",
|
|
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.');
|
|
741
558
|
}
|
|
742
559
|
parts.push('\nCommon causes to check:');
|
|
743
560
|
parts.push('- Wrong import path (use path aliases, not deep relative paths)');
|
|
@@ -749,6 +566,7 @@ export function buildFixPrompt(args) {
|
|
|
749
566
|
parts.push('\nReturn your response in the required <thinking> + <code_output> format.');
|
|
750
567
|
return parts.join('\n');
|
|
751
568
|
}
|
|
569
|
+
// ─── Pollution fix prompt ─────────────────────────────────────────────────────
|
|
752
570
|
export function buildPollutionFixPrompt(args) {
|
|
753
571
|
const { pollutorFile, pollutorCode, victimFile, victimCode, victimError } = args;
|
|
754
572
|
const parts = [];
|
|
@@ -820,6 +638,9 @@ export function buildRetryPrompt(failureOutput, failedAttempts = []) {
|
|
|
820
638
|
const rejectionWarning = detectUnhandledRejection(failureOutput);
|
|
821
639
|
if (rejectionWarning)
|
|
822
640
|
parts.push(`\n⚠️ ${rejectionWarning}`);
|
|
641
|
+
const rntlRetryWarning = detectRntlErrors(failureOutput);
|
|
642
|
+
if (rntlRetryWarning)
|
|
643
|
+
parts.push(`\n⚠️ ${rntlRetryWarning}`);
|
|
823
644
|
const nextImportWarning = detectNextJsImportError(failureOutput);
|
|
824
645
|
if (nextImportWarning)
|
|
825
646
|
parts.push(`\n⚠️ ${nextImportWarning}`);
|
|
@@ -833,8 +654,8 @@ export function buildRetryPrompt(failureOutput, failedAttempts = []) {
|
|
|
833
654
|
parts.push('Common causes:');
|
|
834
655
|
parts.push('- Wrong import path — check the path aliases and dependency list from the original prompt');
|
|
835
656
|
parts.push('- Missing mock — if a module needs mocking, add it to the shared mock file');
|
|
836
|
-
parts.push('- Wrong
|
|
837
|
-
parts.push('- Barrel file mock miss: if a module is re-exported from a barrel/index file, mocking the barrel
|
|
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.');
|
|
838
659
|
parts.push('- Wrong API — use only methods that exist in the installed version of the library');
|
|
839
660
|
parts.push('- Type error — make sure the types match what the source file exports');
|
|
840
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.');
|
|
@@ -844,4 +665,4 @@ export function buildRetryPrompt(failureOutput, failedAttempts = []) {
|
|
|
844
665
|
parts.push('Fix the issue and return your response in the required <thinking> + <code_output> format.');
|
|
845
666
|
return parts.join('\n');
|
|
846
667
|
}
|
|
847
|
-
//# sourceMappingURL=
|
|
668
|
+
//# sourceMappingURL=index.js.map
|