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.
Files changed (94) hide show
  1. package/README.md +49 -11
  2. package/dist/agent/context.d.ts +2 -0
  3. package/dist/agent/context.d.ts.map +1 -1
  4. package/dist/agent/context.js +314 -1
  5. package/dist/agent/context.js.map +1 -1
  6. package/dist/agent/fix-loop.d.ts.map +1 -1
  7. package/dist/agent/fix-loop.js +36 -8
  8. package/dist/agent/fix-loop.js.map +1 -1
  9. package/dist/agent/generator.d.ts +5 -1
  10. package/dist/agent/generator.d.ts.map +1 -1
  11. package/dist/agent/generator.js +51 -12
  12. package/dist/agent/generator.js.map +1 -1
  13. package/dist/agent/loop.d.ts.map +1 -1
  14. package/dist/agent/loop.js +83 -38
  15. package/dist/agent/loop.js.map +1 -1
  16. package/dist/agent/{prompts.d.ts → prompts/index.d.ts} +4 -2
  17. package/dist/agent/prompts/index.d.ts.map +1 -0
  18. package/dist/agent/{prompts.js → prompts/index.js} +162 -341
  19. package/dist/agent/prompts/index.js.map +1 -0
  20. package/dist/agent/prompts/nextjs.d.ts +12 -0
  21. package/dist/agent/prompts/nextjs.d.ts.map +1 -0
  22. package/dist/agent/prompts/nextjs.js +138 -0
  23. package/dist/agent/prompts/nextjs.js.map +1 -0
  24. package/dist/agent/prompts/react-native.d.ts +4 -0
  25. package/dist/agent/prompts/react-native.d.ts.map +1 -0
  26. package/dist/agent/prompts/react-native.js +82 -0
  27. package/dist/agent/prompts/react-native.js.map +1 -0
  28. package/dist/agent/prompts/react.d.ts +2 -0
  29. package/dist/agent/prompts/react.d.ts.map +1 -0
  30. package/dist/agent/prompts/react.js +48 -0
  31. package/dist/agent/prompts/react.js.map +1 -0
  32. package/dist/agent/prompts/runners/js-common.d.ts +2 -0
  33. package/dist/agent/prompts/runners/js-common.d.ts.map +1 -0
  34. package/dist/agent/prompts/runners/js-common.js +13 -0
  35. package/dist/agent/prompts/runners/js-common.js.map +1 -0
  36. package/dist/agent/prompts/runners/typescript.d.ts +2 -0
  37. package/dist/agent/prompts/runners/typescript.d.ts.map +1 -0
  38. package/dist/agent/prompts/runners/typescript.js +12 -0
  39. package/dist/agent/prompts/runners/typescript.js.map +1 -0
  40. package/dist/agent/prompts/runners/vitest.d.ts +2 -0
  41. package/dist/agent/prompts/runners/vitest.d.ts.map +1 -0
  42. package/dist/agent/prompts/runners/vitest.js +23 -0
  43. package/dist/agent/prompts/runners/vitest.js.map +1 -0
  44. package/dist/agent/prompts/vue.d.ts +3 -0
  45. package/dist/agent/prompts/vue.d.ts.map +1 -0
  46. package/dist/agent/prompts/vue.js +29 -0
  47. package/dist/agent/prompts/vue.js.map +1 -0
  48. package/dist/commands/analyze.d.ts.map +1 -1
  49. package/dist/commands/analyze.js +43 -32
  50. package/dist/commands/analyze.js.map +1 -1
  51. package/dist/commands/fix.d.ts.map +1 -1
  52. package/dist/commands/fix.js +6 -1
  53. package/dist/commands/fix.js.map +1 -1
  54. package/dist/commands/generate.d.ts.map +1 -1
  55. package/dist/commands/generate.js +5 -0
  56. package/dist/commands/generate.js.map +1 -1
  57. package/dist/commands/init.d.ts.map +1 -1
  58. package/dist/commands/init.js +104 -14
  59. package/dist/commands/init.js.map +1 -1
  60. package/dist/lib/coverage/gaps.d.ts.map +1 -1
  61. package/dist/lib/coverage/gaps.js +33 -3
  62. package/dist/lib/coverage/gaps.js.map +1 -1
  63. package/dist/lib/coverage/index.d.ts +1 -1
  64. package/dist/lib/coverage/index.d.ts.map +1 -1
  65. package/dist/lib/coverage/index.js +1 -1
  66. package/dist/lib/coverage/index.js.map +1 -1
  67. package/dist/lib/detector.d.ts.map +1 -1
  68. package/dist/lib/detector.js +26 -12
  69. package/dist/lib/detector.js.map +1 -1
  70. package/dist/lib/feedback.d.ts +3 -0
  71. package/dist/lib/feedback.d.ts.map +1 -0
  72. package/dist/lib/feedback.js +27 -0
  73. package/dist/lib/feedback.js.map +1 -0
  74. package/dist/lib/providers/anthropic.d.ts.map +1 -1
  75. package/dist/lib/providers/anthropic.js +30 -1
  76. package/dist/lib/providers/anthropic.js.map +1 -1
  77. package/dist/lib/providers/openai-compatible.d.ts.map +1 -1
  78. package/dist/lib/providers/openai-compatible.js +35 -2
  79. package/dist/lib/providers/openai-compatible.js.map +1 -1
  80. package/dist/lib/providers/types.d.ts +4 -0
  81. package/dist/lib/providers/types.d.ts.map +1 -1
  82. package/dist/lib/providers/types.js +10 -0
  83. package/dist/lib/providers/types.js.map +1 -1
  84. package/dist/lib/validate.d.ts.map +1 -1
  85. package/dist/lib/validate.js +52 -20
  86. package/dist/lib/validate.js.map +1 -1
  87. package/dist/lib/worker-display.d.ts +7 -0
  88. package/dist/lib/worker-display.d.ts.map +1 -1
  89. package/dist/lib/worker-display.js +35 -2
  90. package/dist/lib/worker-display.js.map +1 -1
  91. package/oclif.manifest.json +309 -0
  92. package/package.json +1 -1
  93. package/dist/agent/prompts.d.ts.map +0 -1
  94. package/dist/agent/prompts.js.map +0 -1
@@ -1,114 +1,113 @@
1
- import { buildSourceSkeleton, shouldUseSkeleton, compressSource, filterMockFileForTest, filterMockFileForSource } from '../lib/skeleton.js';
2
- // Extracts module names that are already globally mocked in the setup file.
3
- // Used to tell the agent "don't mock these again" to avoid double-mock conflicts.
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
- // Surfaces TypeScript compiler errors in a form the model can act on directly.
106
- // Philosophy: tsc output is already the fix instruction — our job is to make sure
107
- // the model reads it literally rather than overriding it with framework conventions.
108
- // Two layers:
109
- // 1. Extract structured info from the error text for the highest-value patterns
110
- // (member lists, suggestions, mismatched types) so the model doesn't have to hunt.
111
- // 2. Generic pass-through for everything else — the compiler already said what's wrong.
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
- // Scans error output for signs that a real HTTP request leaked through.
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.', "vi.mock('axios') does NOT intercept axios.create() instances — you must mock the module that exports the instance or the service layer above it.");
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
- function buildNetworkMockingGuidance(analysis, sourceFile) {
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
- // ── Thinking template ────────────────────────────────────────────────────────
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) 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.
361
- b) 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 })\`.
362
- c) RETURN FIELD ENUMERATION: Find the hook's \`return { ... }\` statement. List every key. Only write assertions for fields that actually appear there. A field not in the return statement is always undefined asserting it produces a vacuous test that passes and fails for the wrong reasons.
363
- d) 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.
364
- e) 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.
365
- f) 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: vi.fn() } }\`. If you mock it as \`vi.fn().mockReturnValue({ method: vi.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.
366
- g) 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).
367
- h) 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.
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 vi.fn() / jest.fn() for anything already exported from the mocks file.
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: vi.fn()/jest.fn() mock definitions, vi.mock() module stubs, shared mock objects/constants, and beforeEach reset hooks. NEVER write describe(), it(), test(), or expect() calls in the mocks file.
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
- vi.fn()/jest.fn() mock definitions, vi.mock() module stubs, shared constants, and beforeEach resets.
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 vi.mock() — NOT by modifying the test runner configuration.` : '';
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
- // ── Common failure causes JS/Jest/Vitest only ───────────────────────────────
422
- const jsCauses = isJSRunner ? `
423
- - vi.mock() / jest.mock() paths are relative to the TEST FILE, not the source file. Count directories from the test file's location, not the source file's.
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('\nLOCAL IMPORT PATHS (pre-computed relative to the test file — use EXACTLY these strings in vi.mock() calls, even if the source file uses @/ aliases. Vitest resolves vi.mock() paths relative to the test file, not via tsconfig aliases. Do NOT convert these back to @/ paths in vi.mock(). Do NOT recount directory levels yourself.):');
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 vi.mock() for them in the test: ${nextMocked.join(', ')}`
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('\nMOCK MODULE INVENTORY — modules already vi.mocked:');
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('• A module in the inventory is ALREADY mocked. To add a new export: write ONE updated vi.mock() block with ALL existing exports PLUS the new one. NEVER write a second vi.mock() for the same path — Vitest only honours the last one, so the second block silently wipes every export from the first.');
552
- parts.push('• New export const mockFoo = vi.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.');
553
- parts.push('• New module (not in inventory): append a new vi.mock() at the END of the file, before the final beforeEach.');
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 vi.fn()/vi.mock() definitions and beforeEach resets — NEVER describe/it/test/expect blocks.`);
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('\nLOCAL IMPORT PATHS (pre-computed relative to the test file — use EXACTLY these strings in vi.mock() calls, even if the source file uses @/ aliases. Vitest resolves vi.mock() paths relative to the test file, not via tsconfig aliases. Do NOT convert these back to @/ paths in vi.mock(). Do NOT recount directory levels yourself.):');
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('\nMOCK MODULE INVENTORY — modules already vi.mocked (line numbers refer to the file below):');
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('• A module in the inventory is ALREADY mocked. To add a new export: write ONE updated vi.mock() block with ALL existing exports PLUS the new one. NEVER write a second vi.mock() for the same path — Vitest only honours the last one, so the second block silently wipes every export from the first.');
676
- parts.push('• New export const mockFoo = vi.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.');
677
- parts.push('• New module (not in inventory): append a new vi.mock() at the END of the file, before the final beforeEach.');
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 vi.fn()/vi.mock() definitions and beforeEach resets — NEVER describe/it/test/expect blocks.`);
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 nextImportWarning = detectNextJsImportError(errorOutput);
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().", "vi.mock('axios') cannot intercept a custom axios instance.", 'You must mock the module that exports the axios instance, or mock the service/API module the source imports.');
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 vi.mock() path: mock paths are relative to the TEST FILE, not the source file. Count up from the test file\'s directory to reach the mocked module — if the test is in src/features/x/__tests__/ and mocks src/components/, that is ../../../components/, not ../components/.');
837
- parts.push('- Barrel file mock miss: if a module is re-exported from a barrel/index file, mocking the barrel (vi.mock(\'../components\')) will NOT intercept imports of the direct file (\'../components/Foo\'). Mock the specific file the source actually imports. If unsure, mock both.');
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=prompts.js.map
668
+ //# sourceMappingURL=index.js.map