lacuna-cli 0.1.4 → 0.1.6

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 (61) hide show
  1. package/README.md +51 -21
  2. package/dist/agent/fix-loop.d.ts +5 -0
  3. package/dist/agent/fix-loop.d.ts.map +1 -1
  4. package/dist/agent/fix-loop.js +342 -35
  5. package/dist/agent/fix-loop.js.map +1 -1
  6. package/dist/agent/generator.d.ts +4 -1
  7. package/dist/agent/generator.d.ts.map +1 -1
  8. package/dist/agent/generator.js +42 -3
  9. package/dist/agent/generator.js.map +1 -1
  10. package/dist/agent/loop.d.ts +8 -0
  11. package/dist/agent/loop.d.ts.map +1 -1
  12. package/dist/agent/loop.js +26 -9
  13. package/dist/agent/loop.js.map +1 -1
  14. package/dist/agent/prompts.d.ts +8 -0
  15. package/dist/agent/prompts.d.ts.map +1 -1
  16. package/dist/agent/prompts.js +395 -67
  17. package/dist/agent/prompts.js.map +1 -1
  18. package/dist/commands/fix.d.ts +2 -0
  19. package/dist/commands/fix.d.ts.map +1 -1
  20. package/dist/commands/fix.js +31 -2
  21. package/dist/commands/fix.js.map +1 -1
  22. package/dist/commands/init.d.ts.map +1 -1
  23. package/dist/commands/init.js +245 -34
  24. package/dist/commands/init.js.map +1 -1
  25. package/dist/lib/config.d.ts +8 -8
  26. package/dist/lib/config.d.ts.map +1 -1
  27. package/dist/lib/config.js +8 -6
  28. package/dist/lib/config.js.map +1 -1
  29. package/dist/lib/coverage/gaps.d.ts +2 -2
  30. package/dist/lib/coverage/gaps.d.ts.map +1 -1
  31. package/dist/lib/coverage/gaps.js +4 -4
  32. package/dist/lib/coverage/gaps.js.map +1 -1
  33. package/dist/lib/detector.d.ts +3 -2
  34. package/dist/lib/detector.d.ts.map +1 -1
  35. package/dist/lib/detector.js +138 -0
  36. package/dist/lib/detector.js.map +1 -1
  37. package/dist/lib/providers/anthropic.d.ts.map +1 -1
  38. package/dist/lib/providers/anthropic.js +26 -3
  39. package/dist/lib/providers/anthropic.js.map +1 -1
  40. package/dist/lib/providers/openai-compatible.d.ts +1 -1
  41. package/dist/lib/providers/openai-compatible.d.ts.map +1 -1
  42. package/dist/lib/providers/openai-compatible.js +17 -1
  43. package/dist/lib/providers/openai-compatible.js.map +1 -1
  44. package/dist/lib/skeleton.d.ts +4 -0
  45. package/dist/lib/skeleton.d.ts.map +1 -1
  46. package/dist/lib/skeleton.js +220 -0
  47. package/dist/lib/skeleton.js.map +1 -1
  48. package/dist/lib/validate.d.ts +1 -0
  49. package/dist/lib/validate.d.ts.map +1 -1
  50. package/dist/lib/validate.js +132 -0
  51. package/dist/lib/validate.js.map +1 -1
  52. package/dist/lib/worker-display.d.ts +3 -0
  53. package/dist/lib/worker-display.d.ts.map +1 -1
  54. package/dist/lib/worker-display.js +19 -4
  55. package/dist/lib/worker-display.js.map +1 -1
  56. package/package.json +1 -1
  57. package/dist/lib/report-upload.d.ts +0 -3
  58. package/dist/lib/report-upload.d.ts.map +0 -1
  59. package/dist/lib/report-upload.js +0 -15
  60. package/dist/lib/report-upload.js.map +0 -1
  61. package/oclif.manifest.json +0 -295
@@ -1,4 +1,4 @@
1
- import { buildSourceSkeleton, shouldUseSkeleton } from '../lib/skeleton.js';
1
+ import { buildSourceSkeleton, shouldUseSkeleton, compressSource, filterMockFileForTest, filterMockFileForSource } from '../lib/skeleton.js';
2
2
  // Extracts module names that are already globally mocked in the setup file.
3
3
  // Used to tell the agent "don't mock these again" to avoid double-mock conflicts.
4
4
  function extractGlobalNextMocks(setupCode) {
@@ -53,6 +53,38 @@ function buildNextJsGuidance(a) {
53
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
54
  return lines.join('\n');
55
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
+ }
56
88
  // Matches both directory-level patterns (/api/, /services/) and file-level names
57
89
  // (../../lib/api, ../apiClient, ./httpService) so axios instance files aren't missed.
58
90
  const API_IMPORT_RE = /\/(?:api|services?|requests?|http|client|network)\/|\/(?:api|axios|http|request)(?:Client|Config|Instance|Service|Helper)?(?:\/|$)|[/.]api(?:[./]|$)/i;
@@ -70,6 +102,70 @@ function analyzeNetworkDeps(sourceCode) {
70
102
  }
71
103
  return { usesAxios, usesFetch, usesCustomInstance, apiModuleImports };
72
104
  }
105
+ // Surfaces TypeScript compiler errors in a form the model can act on directly.
106
+ // Philosophy: tsc output is already the fix instruction — our job is to make sure
107
+ // the model reads it literally rather than overriding it with framework conventions.
108
+ // Two layers:
109
+ // 1. Extract structured info from the error text for the highest-value patterns
110
+ // (member lists, suggestions, mismatched types) so the model doesn't have to hunt.
111
+ // 2. Generic pass-through for everything else — the compiler already said what's wrong.
112
+ function detectTypeScriptErrors(errorOutput) {
113
+ if (!/error TS\d+:/.test(errorOutput))
114
+ return null;
115
+ const parts = [
116
+ 'TYPESCRIPT ERRORS — treat each compiler message as an exact instruction, not a hint:',
117
+ 'The TypeScript compiler tells you precisely what is wrong and usually what the fix is.',
118
+ 'Do NOT override it with framework conventions or assumptions.',
119
+ ];
120
+ // TS1378: top-level await — only specific branch because the fix is structural
121
+ if (/TS1378/.test(errorOutput)) {
122
+ parts.push('• Top-level await (TS1378): move ALL await calls inside it()/test()/beforeEach()/etc.', ' WRONG: const result = await fn();', ' RIGHT: it("desc", async () => { const result = await fn(); });');
123
+ }
124
+ // Wrong member name — extract the actual member list TypeScript printed inline.
125
+ // Covers TS2339, TS2551, TS2561 and any variant with "does not exist on/in type '{...}'"
126
+ const propErrors = [...errorOutput.matchAll(/'(\w+)' does not exist (?:on|in) type '\{([^']+)\}'/g)];
127
+ if (propErrors.length > 0) {
128
+ parts.push('• Wrong member name — the actual available members are:');
129
+ const seen = new Set();
130
+ for (const m of propErrors) {
131
+ const wrongProp = m[1];
132
+ const available = [...m[2].matchAll(/(\w+)\s*[?]?\s*:/g)].map(p => p[1]).filter(p => p !== 'type');
133
+ const key = wrongProp + available.join();
134
+ if (seen.has(key))
135
+ continue;
136
+ seen.add(key);
137
+ parts.push(` '${wrongProp}' → not valid. Use one of: ${available.slice(0, 12).join(', ')}${available.length > 12 ? ' …' : ''}`);
138
+ }
139
+ }
140
+ // Compiler suggestions — TypeScript already provides the answer
141
+ const suggestions = [...new Set([...errorOutput.matchAll(/Did you mean(?: to write)? '(\w+)'\?/g)].map(m => m[1]))];
142
+ if (suggestions.length > 0) {
143
+ parts.push(`• Compiler suggestion: use ${suggestions.map(s => `'${s}'`).join(', ')}`);
144
+ }
145
+ // Type mismatch — extract what was passed vs what was required
146
+ const typeMismatches = [...errorOutput.matchAll(/Argument of type '([^']+)' is not assignable to parameter of type '([^']+)'/g)];
147
+ if (typeMismatches.length > 0) {
148
+ for (const m of typeMismatches) {
149
+ parts.push(`• Type mismatch: passed '${m[1].slice(0, 80)}', required '${m[2].slice(0, 80)}'`);
150
+ }
151
+ parts.push(' (use null not undefined for nullable values; check TYPE DEFINITIONS for the required shape)');
152
+ }
153
+ // Generic pass-through for all other TS errors — list them so the model reads each one
154
+ // rather than guessing. No special handling needed: the message IS the instruction.
155
+ const otherErrors = [...errorOutput.matchAll(/error (TS(?!1378|2339|2551|2561|2345)\d+): ([^\n]+)/g)];
156
+ if (otherErrors.length > 0) {
157
+ parts.push('• Additional compiler errors — read each one and apply the exact fix it describes:');
158
+ const seen = new Set();
159
+ for (const m of otherErrors) {
160
+ const msg = `${m[1]}: ${m[2].slice(0, 120)}`;
161
+ if (seen.has(msg))
162
+ continue;
163
+ seen.add(msg);
164
+ parts.push(` ${msg}`);
165
+ }
166
+ }
167
+ return parts.join('\n');
168
+ }
73
169
  // Detects thinking-bleed parse errors: model wrote reasoning inside <code_output>
74
170
  // causing the file to start with prose instead of TypeScript.
75
171
  function detectThinkingBleed(errorOutput) {
@@ -99,21 +195,25 @@ function detectNextJsImportError(errorOutput) {
99
195
  if (!failedImport)
100
196
  return null;
101
197
  const importPath = failedImport[1];
198
+ const isServerOnly = importPath === 'server-only';
102
199
  const isAlias = importPath.startsWith('@/');
103
200
  const isClient = importPath.endsWith('.client');
104
201
  const isServer = importPath.endsWith('.server');
105
202
  const isNextInternal = importPath.startsWith('next/');
106
203
  const isProviderOrSession = /session|auth|provider/i.test(importPath);
107
- if (!isAlias && !isClient && !isServer && !isNextInternal && !isProviderOrSession)
204
+ if (!isServerOnly && !isAlias && !isClient && !isServer && !isNextInternal && !isProviderOrSession)
108
205
  return null;
109
206
  const lines = [
110
207
  `IMPORT RESOLUTION ERROR — Vitest cannot resolve "${importPath}".`,
111
208
  ];
112
- if (isAlias) {
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) {
113
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.`);
114
214
  }
115
215
  else if (isClient || isServer) {
116
- lines.push(`This is a Next.js ${isClient ? 'client' : 'server'} boundary file. Vitest never resolves these mock it with the exact import path from the source file:`, ` vi.mock('${importPath}', () => ({`, ` // each function the source imports: myFn: vi.fn()`, ` }))`);
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.`);
117
217
  }
118
218
  else if (isNextInternal) {
119
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() */ }))`);
@@ -178,6 +278,56 @@ function buildNetworkMockingGuidance(analysis, sourceFile) {
178
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.`);
179
279
  return lines.join('\n');
180
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
+ }
181
331
  // Extract exported names from a mocks file so the AI sees a concrete inventory.
182
332
  function parseMockExports(code) {
183
333
  const names = [];
@@ -200,80 +350,145 @@ function parseMockExports(code) {
200
350
  return [...new Set(names)];
201
351
  }
202
352
  export function buildSystemPrompt(env) {
353
+ const isJS = env.language === 'typescript' || env.language === 'javascript' || env.language === 'unknown';
354
+ const isTS = env.language === 'typescript';
355
+ const isVitest = env.testRunner === 'vitest';
356
+ const isJSRunner = env.testRunner === 'jest' || env.testRunner === 'vitest' || env.testRunner === 'mocha';
357
+ // ── Thinking template ────────────────────────────────────────────────────────
358
+ const mockAuditStep = isJS ? `
359
+ 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.
368
+ 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
+ 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
+ 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.
371
+ 3. COMPONENT RENDER MAP (React/Vue components only): Before writing any assertion, list what is in the DOM in each relevant state (idle / loading / error / success). Read the template/JSX — check every ternary, &&, and conditional — to determine whether a button is disabled vs unmounted, what text changes, what elements appear.
372
+ GUARD CLAUSE AUDIT: Identify every conditional render guard in the component (e.g. payments.length > 0, isLoading, hasPermission). A test that provides data violating a guard will never find the element — the guard hides it. Match mock data to the guard condition required by each test.
373
+ STALE TEST AUDIT: Check whether any existing test asserts UI or behavior that the current source no longer has. DELETE those tests — do not try to make the component pass a test for features it no longer has.
374
+ BUG-ASSERTING TEST RULE: NEVER write a test that asserts the component/function throws or crashes due to a missing null check, missing guard, or undefined field — unless you have read the source and confirmed the crash path exists. A test named "throws when X is undefined due to missing null check" is testing for a bug, not behavior. If the source handles the case gracefully, test the graceful output instead. If it truly crashes, fix the source — do not write a test to document the bug.` : `
375
+ 2. DEPENDENCY AUDIT: List every external dependency the source calls. For each one, determine what needs to be mocked and what return value the code expects. Read every call site — don't infer the expected shape from the type name.
376
+ 3. DATA FIXTURE AUDIT: Read the source's selector logic — every filter, find, and field access. Fixture data field names must match what the source reads exactly.`;
377
+ const thinkingTemplate = `
378
+ 1. WHAT IS NEEDED: What functions/behaviors are untested or broken?${mockAuditStep}
379
+ 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
+ 5. PLAN: List the exact steps you will take before writing a single line of code.`;
381
+ // ── JS/TS-specific rules ─────────────────────────────────────────────────────
382
+ const jsRules = isJS ? `
383
+ 3. Use path aliases from the PROJECT TYPESCRIPT CONFIG section in IMPORT statements (e.g. "@/components/Button" not "../../components/Button").
384
+ EXCEPTION — mock call paths: use the exact same path string that appears in the SOURCE FILE'S import statement.
385
+ 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
+ Never second-guess the pre-computed paths. Never convert them back to @/ aliases in mock calls.
387
+ 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.
389
+ CRITICAL — never rename or change the casing of existing mock exports.
390
+ 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---
391
+ 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.
393
+ 8. If a TEST SETUP FILE is shown, assume its globals and matchers are already available. Do NOT import or re-declare them.` : `
394
+ 3. Use the project's import conventions as shown in the source file and existing tests.
395
+ 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.` : '';
404
+ const ruleCount = isTS ? 10 : (isJS ? 9 : 6);
405
+ // ── JS/Vitest-specific output format rules ───────────────────────────────────
406
+ const jsOutputRules = isJS ? `
407
+ ${ruleCount + 2}. Inside <code_output>: output ONLY the test file content (or test file // ---MOCKS_FILE--- mocks file).
408
+ 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.
410
+ NEVER put describe(), it(), test(), or expect() calls after the separator.
411
+ ${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 ────────────────────────────────────────
414
+ const universalCauses = `- Wrong import paths (use the project's conventions — aliases where configured, relative paths otherwise)
415
+ - Importing from test utilities that are not in the dependency list
416
+ - Mocking modules that are already mocked in the setup file
417
+ - Forgetting to await async functions
418
+ - 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
+ - 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
+ - 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 ────────────────────────────────────────────────
455
+ const hookSuiteNote = isJSRunner
456
+ ? `\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
+ : '';
203
458
  return `You are a senior QA engineer with 10+ years of experience writing production test suites for ${env.language} projects. You use ${env.testRunner} and you take testing seriously.
204
459
 
205
460
  Your tests catch real bugs. You think about what could go wrong — null inputs, empty arrays, async race conditions, error boundaries, permission checks, off-by-one errors — and you write assertions that would actually fail if the code broke. You never write a test just to hit a coverage number.
206
461
 
207
462
  RULES — follow every one:
208
463
  1. Write tests that verify real behavior: correctness, edge cases, boundary values, and error handling. Never write empty or trivial assertions (e.g. expect(true).toBe(true)).
209
- 2. Match the EXACT import style shown in the existing test file or PROJECT TEST EXAMPLES. If none exists, use the style from the source file.
210
- 3. Use path aliases from the PROJECT TYPESCRIPT CONFIG section in IMPORT statements (e.g. "@/components/Button" not "../../components/Button").
211
- EXCEPTION vi.mock() call paths: use the exact same path string that appears in the SOURCE FILE'S import statement.
212
- If a LOCAL IMPORT PATHS section is provided, use those pre-computed relative paths in vi.mock() calls — they are the fallback when aliases cannot be resolved by Vitest.
213
- Never second-guess the pre-computed paths. Never convert them back to @/ aliases in vi.mock() calls.
214
- 4. Only import from packages listed in PROJECT DEPENDENCIES. Do not invent packages that are not listed.
215
- 5. When a SHARED MOCK FILE is provided, its exported names are listed under "Available exports". Before writing the test, go through that list and identify every mock that relates to what the source file does (by name, type, or domain). Import and use ALL of those mocks. Never re-create inline vi.fn() / jest.fn() for anything already exported from the mocks file.
216
- CRITICAL — never rename or change the casing of existing mock exports. If the mocks file has mockValidationError, use exactly mockValidationError — do NOT change it to MockValidationError or any other variant. Renaming an existing mock breaks every test that already imports it by the original name.
217
- 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---
218
- 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.
219
- CRITICAL — the mocks file must contain ONLY: vi.fn()/jest.fn() mock definitions, vi.mock() module stubs, shared mock objects/constants, and beforeEach reset hooks. NEVER write describe(), it(), test(), or expect() calls in the mocks file. Those belong exclusively in the test file. A mocks file that contains test blocks will break the entire test suite.
220
- 8. If a TEST SETUP FILE is shown, assume its globals and matchers are already available (e.g. expect(...).toBeInTheDocument()). Do NOT import or re-declare them.
221
- 9. TypeScript type safety: all test code must compile without type errors.
222
- - Check PROJECT TYPESCRIPT CONFIG for strict flags — if strict or noImplicitAny is set, every value must be properly typed.
223
- - Never use "as any", "@ts-ignore", or "@ts-expect-error" to suppress type errors. Use proper types or generics instead.
224
- - 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>>().
225
- - Use ReturnType<>, Parameters<>, and other utility types to derive mock types from the real function signatures rather than guessing.
226
- - When accessing optional properties, handle null/undefined correctly — do not assume they exist if the type says otherwise.
227
- 10. Every test file MUST contain at least one it() or test() call with real assertions. A file with only imports, describe() blocks, types, or helper functions is invalid and will be rejected. If you cannot write meaningful tests, write a minimal test that exercises the simplest exported function.
228
- 11. Structure ALL output using exactly these two XML blocks — nothing before, nothing after:
229
- <thinking>
230
- 1. WHAT IS NEEDED: What functions/behaviors are untested or broken?
231
- 2. COMPONENT RENDER MAP (React components only): Before writing any assertion, list what is in the DOM in each relevant state (idle / loading / error / success). Read the JSX — check every ternary, &&, and switch — to determine whether a button is disabled vs unmounted, what text changes, what elements appear. Never assume a button is disabled during loading without verifying this in the JSX.
232
- 3. WHY IT FAILED (retries only): What is the structural root cause — wrong mock level, missing await, bad import path, type mismatch?
233
- 4. PLAN: List the exact steps you will take before writing a single line of code.
464
+ 2. Match the EXACT import style shown in the existing test file or PROJECT TEST EXAMPLES. If none exists, use the style from the source file.${jsRules}${tsRule}
465
+ ${ruleCount}. Every test file MUST contain at least one it() or test() call with real assertions. A file with only imports, describe() blocks, types, or helper functions is invalid and will be rejected.
466
+ ${ruleCount + 1}. Structure ALL output using exactly these two XML blocks nothing before, nothing after:
467
+ <thinking>${thinkingTemplate}
234
468
  </thinking>
235
469
  <code_output>
236
470
  // complete test file here
237
471
  </code_output>
238
- CRITICAL: Once you open <code_output>, ALL remaining output must be code. Never continue reasoning, writing bullet points,
239
- or restating the problem inside <code_output>. If you are still uncertain, finish your reasoning inside <thinking> first,
240
- then commit to a solution and write only code inside <code_output>. Thinking inside <code_output> corrupts the output file.
241
- 12. Inside <code_output>: do NOT wrap in markdown code fences.
242
- 13. Inside <code_output>: output ONLY the test file content (or test file // ---MOCKS_FILE--- mocks file).
243
- If you use // ---MOCKS_FILE---, everything AFTER the separator is the mocks file. The mocks file must contain ONLY:
244
- vi.fn()/jest.fn() mock definitions, vi.mock() module stubs, shared constants, and beforeEach resets.
245
- NEVER put describe(), it(), test(), or expect() calls after the separator — those belong BEFORE it, in the test file.
246
- Writing test blocks in the mocks section corrupts the shared mock file for every other test in the project.
247
- 14. NEVER output vitest.config.ts, jest.config.js, or any framework configuration. If an import cannot be resolved,
248
- fix it by mocking it with vi.mock() — NOT by modifying the test runner configuration. You cannot modify config
249
- files from here, and outputting them will cause the entire mocks file to be discarded.
472
+ CRITICAL: Once you open <code_output>, ALL remaining output must be code. Finish ALL reasoning inside <thinking> first.
473
+ ${ruleCount + 2 <= ruleCount + 1 ? '' : `${ruleCount + 2}. Inside <code_output>: do NOT wrap in markdown code fences.`}${jsOutputRules}
250
474
 
251
475
  A good test suite you write will have:
252
476
  - A happy-path test that confirms the main behavior works
253
477
  - At least one edge-case test per function (empty input, zero, null, boundary values)
254
- - Error-path tests for any function that throws, rejects, or returns an error state
255
- - Async tests properly awaited — never fire-and-forget
478
+ - Error-path tests for any function that throws, rejects, or returns an error state — but ONLY assert the observable effect. Read the catch block first: does it set state, call a notification, or just log? Test only what's observable.
479
+ - Async tests properly awaited — never fire-and-forget${hookSuiteNote}
256
480
  - Clear, descriptive test names that read like a spec ("returns null when user is not authenticated")
257
481
 
258
482
  Common failure causes to avoid:
259
- - Wrong import paths (use aliases, not relative ../../ paths if the project uses aliases)
260
- - Importing from test utils that are not in the dependency list
261
- - Mocking modules that are already mocked in the setup file
262
- - Using browser globals without jsdom (only use them if the setup file configures jsdom)
263
- - Forgetting to await async functions
264
- - React 18 act() async rule: ALWAYS await act() when it wraps async code — \`await act(async () => { ... })\`. Never store an unawaited act() call in a variable like \`const promise = act(async () => ...)\` without immediately awaiting it. Unawaited act() calls cause React state updates to leak into subsequent tests, producing cascading timeout failures and "Cannot read properties of null" errors in unrelated tests.
265
- - vi.mock() paths are relative to the TEST FILE, not the source file. If the test is at src/features/auth/__tests__/Login.test.tsx and you need to mock src/components/Button, the mock path is ../../../components/Button — count directories from the TEST file's location, not the source file's.
266
- - Loading state architecture: before asserting that a button is disabled during loading, check whether the component hides the button entirely and replaces it with a spinner. If the button is unmounted during loading rather than disabled, \`getByText("Submit")\` will throw — test for the spinner instead, or use \`queryByText("Submit")\` with a null assertion.
267
- - Unhandled promise rejections: when testing error paths with mockRejectedValueOnce, the rejection must be fully resolved inside the test. After triggering the action, always use await waitFor(() => expect(errorElement).toBeInTheDocument()) to tie the rejection to the test scope. Never let a rejected mock promise go unawaited — Vitest will flag it as an unhandled error even if the component catches it internally.
268
- - Real HTTP requests: NEVER let a real network call reach the internet. If you see a real URL (https://...), a 401/403 error, or a network timeout in test output, your mock is missing or at the wrong level. Every function that calls an API must be mocked before the test runs.
269
- - Barrel file vi.mock() resolution: if a module is exported from a barrel/index file (e.g. src/components/index.ts re-exports Foo from ./Foo), mock the DIRECT file path, not the barrel. vi.mock('../components') mocks the barrel but the component may import directly from '../components/Foo' — making the mock miss. Always mock the specific module the source file actually imports. If unsure, mock both the direct file AND the barrel.
483
+ ${universalCauses}${jsCauses}${vitestCauses}${reactCauses}
270
484
 
271
485
  Test file pattern for this project: ${env.testFilePattern}
272
486
 
273
487
  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.`;
274
488
  }
275
489
  export function buildGeneratePrompt(args) {
276
- const { sourceFile, sourceCode, existingTestCode, uncoveredFunctions, uncoveredLines, sourceImportPath, mocksCode, mocksImportPath, setupFileCode, packageDeps, tsconfigPaths, typeDefinitions, localImportPaths, reactMajorVersion, projectMemory, } = args;
490
+ const { sourceFile, existingTestCode, uncoveredFunctions, uncoveredLines, sourceImportPath, mocksCode, mocksImportPath, setupFileCode, packageDeps, tsconfigPaths, typeDefinitions, localImportPaths, reactMajorVersion, projectMemory, } = args;
491
+ const sourceCode = compressSource(args.sourceCode);
277
492
  const parts = [];
278
493
  if (projectMemory) {
279
494
  parts.push(projectMemory);
@@ -319,11 +534,36 @@ export function buildGeneratePrompt(args) {
319
534
  parts.push(`\nSHARED MOCK FILE (import from: '${mocksImportPath}')`);
320
535
  if (exports.length > 0) {
321
536
  parts.push(`Available exports: ${exports.join(', ')}`);
322
- parts.push(`↑ Before writing the test, identify which of these match the source file's domain and import every relevant one. Do NOT create inline mocks for anything already in this list.\n↑ NAMES ARE FROZEN — use each export exactly as spelled above. Never rename, recase, or restructure an existing mock (e.g. do not change mockFoo → MockFoo or const → class). Renaming breaks every other test that imports the original name.`);
537
+ parts.push(`↑ Every name above ALREADY EXISTS in the mock file — do NOT re-declare any of them in a ---MOCKS_FILE--- block. Only declare names that do NOT appear in this list.\n↑ Before writing the test, identify which of these match the source file's domain and import every relevant one. Do NOT create inline mocks for anything already in this list.\n↑ NAMES ARE FROZEN — use each export exactly as spelled above. Never rename, recase, or restructure an existing mock (e.g. do not change mockFoo → MockFoo or const → class). Renaming breaks every other test that imports the original name.`);
323
538
  }
324
- parts.push('```');
325
- parts.push(mocksCode);
326
- parts.push('```');
539
+ const inventory = parseMockInventory(mocksCode);
540
+ if (inventory.length > 0) {
541
+ const maxLen = Math.max(...inventory.map(e => e.modulePath.length));
542
+ parts.push('\nMOCK MODULE INVENTORY — modules already vi.mocked:');
543
+ for (const entry of inventory) {
544
+ const path = `'${entry.modulePath}'`.padEnd(maxLen + 2);
545
+ const exp = entry.exports.length > 0
546
+ ? entry.exports.join(', ')
547
+ : '(no simple key exports)';
548
+ parts.push(` ${path} → ${exp}`);
549
+ }
550
+ 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.');
554
+ }
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
+ const relevantMocks = filterMockFileForSource(mocksCode, sourceCode);
559
+ if (relevantMocks !== mocksCode || relevantMocks.split('\n').length < 80) {
560
+ // Filtered down or already small — worth sending
561
+ parts.push('```');
562
+ parts.push(relevantMocks);
563
+ parts.push('```');
564
+ }
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.
327
567
  }
328
568
  else {
329
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.`);
@@ -335,6 +575,10 @@ export function buildGeneratePrompt(args) {
335
575
  const nextGuidance = buildNextJsGuidance(analyzeNextJs(sourceCode));
336
576
  if (nextGuidance)
337
577
  parts.push(`\n${nextGuidance}`);
578
+ if (detectReactNative(packageDeps ?? null))
579
+ parts.push(`\n${buildReactNativeGuidance()}`);
580
+ else if (detectVue(packageDeps ?? null))
581
+ parts.push(`\n${buildVueGuidance()}`);
338
582
  const displaySource = buildSourceSkeleton(sourceCode, uncoveredFunctions);
339
583
  const skeletonized = shouldUseSkeleton(sourceCode);
340
584
  parts.push(`\nSOURCE FILE: ${sourceFile}${skeletonized ? ' (large file — bodies of already-covered functions collapsed; uncovered functions shown in full)' : ''}`);
@@ -363,7 +607,8 @@ export function buildGeneratePrompt(args) {
363
607
  return parts.join('\n');
364
608
  }
365
609
  export function buildFixPrompt(args) {
366
- const { testFile, testCode, sourceFile, sourceCode, sourceImportPath, errorOutput, mocksCode, mocksImportPath, setupFileCode, packageDeps, tsconfigPaths, typeDefinitions, localImportPaths, reactMajorVersion, projectMemory } = args;
610
+ const { testFile, testCode, sourceFile, sourceImportPath, errorOutput, mocksCode, mocksImportPath, setupFileCode, packageDeps, tsconfigPaths, typeDefinitions, localImportPaths, reactMajorVersion, projectMemory } = args;
611
+ const sourceCode = args.sourceCode ? compressSource(args.sourceCode) : null;
367
612
  const parts = [];
368
613
  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.');
369
614
  parts.push('');
@@ -404,19 +649,45 @@ export function buildFixPrompt(args) {
404
649
  if (mocksImportPath) {
405
650
  if (mocksCode) {
406
651
  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
+ const compressed = filterMockFileForTest(mocksCode, args.testCode);
407
657
  parts.push(`\nSHARED MOCK FILE (import from: '${mocksImportPath}')`);
408
658
  if (exports.length > 0) {
409
659
  parts.push(`Available exports: ${exports.join(', ')}`);
410
- parts.push(`↑ Check this list against the source file — import every relevant mock. Do NOT create inline mocks for anything already exported here.`);
660
+ 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
+ }
662
+ // Parse inventory from the compressed file so line numbers match what the AI sees below
663
+ const inventory = parseMockInventory(compressed);
664
+ if (inventory.length > 0) {
665
+ 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):');
667
+ for (const entry of inventory) {
668
+ const path = `'${entry.modulePath}'`.padEnd(maxLen + 2);
669
+ const exp = entry.exports.length > 0
670
+ ? entry.exports.join(', ')
671
+ : '(no simple key exports)';
672
+ parts.push(` Line ${String(entry.lineNumber).padStart(4)}: ${path} → ${exp}`);
673
+ }
674
+ 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.');
411
678
  }
412
679
  parts.push('```');
413
- parts.push(mocksCode);
680
+ parts.push(compressed);
414
681
  parts.push('```');
415
682
  }
416
683
  else {
417
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.`);
418
685
  }
419
686
  }
687
+ if (detectReactNative(packageDeps ?? null))
688
+ parts.push(`\n${buildReactNativeGuidance()}`);
689
+ else if (detectVue(packageDeps ?? null))
690
+ parts.push(`\n${buildVueGuidance()}`);
420
691
  if (sourceFile && sourceCode) {
421
692
  const networkGuidance = buildNetworkMockingGuidance(analyzeNetworkDeps(sourceCode), sourceFile);
422
693
  if (networkGuidance)
@@ -424,9 +695,9 @@ export function buildFixPrompt(args) {
424
695
  const nextGuidance = buildNextJsGuidance(analyzeNextJs(sourceCode));
425
696
  if (nextGuidance)
426
697
  parts.push(`\n${nextGuidance}`);
427
- // For fix: skeleton with no function expansion the test already tells the AI
428
- // which function matters; showing signatures is enough to understand the API.
429
- const FIX_SKELETON_THRESHOLD = 150;
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
+ const FIX_SKELETON_THRESHOLD = 600;
430
701
  const displaySource = sourceCode.split('\n').length > FIX_SKELETON_THRESHOLD
431
702
  ? buildSourceSkeleton(sourceCode, [])
432
703
  : sourceCode;
@@ -459,6 +730,9 @@ export function buildFixPrompt(args) {
459
730
  const bleedWarning = detectThinkingBleed(errorOutput);
460
731
  if (bleedWarning)
461
732
  parts.push(`\n⚠️ ${bleedWarning}`);
733
+ const tsErrorWarning = detectTypeScriptErrors(errorOutput);
734
+ if (tsErrorWarning)
735
+ parts.push(`\n⚠️ ${tsErrorWarning}`);
462
736
  // Detect wrong mock pattern: test uses vi.mock('axios') but source uses axios.create()
463
737
  const testHasAxiosMock = /vi\.mock\(['"]axios['"]\)/.test(testCode);
464
738
  const sourceHasCustomInstance = sourceCode != null && /axios\.create\s*\(/.test(sourceCode);
@@ -475,13 +749,64 @@ export function buildFixPrompt(args) {
475
749
  parts.push('\nReturn your response in the required <thinking> + <code_output> format.');
476
750
  return parts.join('\n');
477
751
  }
752
+ export function buildPollutionFixPrompt(args) {
753
+ const { pollutorFile, pollutorCode, victimFile, victimCode, victimError } = args;
754
+ const parts = [];
755
+ parts.push('This test file corrupts shared state and causes another test file to fail when run afterwards.');
756
+ parts.push('Your job: add afterEach() or afterAll() cleanup to reset whatever global state this file mutates.');
757
+ parts.push('');
758
+ parts.push('Rules:');
759
+ parts.push('- DO NOT remove, rewrite, or alter any existing test logic or assertions');
760
+ parts.push('- ONLY add cleanup hooks — nothing else');
761
+ parts.push('- The fix must be minimal: add the smallest afterEach/afterAll that resets the leaked state');
762
+ parts.push('');
763
+ parts.push(`POLLUTING FILE (add cleanup here): ${pollutorFile}`);
764
+ parts.push('```');
765
+ parts.push(pollutorCode);
766
+ parts.push('```');
767
+ parts.push('');
768
+ parts.push(`VICTIM FILE (fails when run after the polluting file): ${victimFile}`);
769
+ parts.push('```');
770
+ parts.push(victimCode);
771
+ parts.push('```');
772
+ parts.push('');
773
+ parts.push('ERROR the victim gets when run after this file:');
774
+ parts.push('```');
775
+ parts.push(victimError.slice(0, 2000));
776
+ parts.push('```');
777
+ parts.push('');
778
+ parts.push('HOW TO DIAGNOSE:');
779
+ parts.push("1. Read the victim's error — what value is null/undefined/wrong, or what element is missing?");
780
+ parts.push('2. Search the polluting file for where that thing is set or modified (localStorage, window properties, module singletons, mock state, React context, timers, environment variables)');
781
+ parts.push('3. Add afterEach (or afterAll) in the polluting file to reset exactly that thing');
782
+ parts.push('');
783
+ parts.push('Common cleanup patterns:');
784
+ parts.push(' afterEach(() => { vi.restoreAllMocks(); vi.clearAllMocks() })');
785
+ parts.push(' afterEach(() => { localStorage.clear(); sessionStorage.clear() })');
786
+ parts.push(' afterEach(() => { delete (window as any).myProperty })');
787
+ parts.push(' afterEach(() => { myModuleSingleton.reset() })');
788
+ parts.push(' afterEach(() => { vi.useRealTimers() })');
789
+ parts.push('');
790
+ parts.push('Return the complete modified polluting file in the required <thinking> + <code_output> format.');
791
+ return parts.join('\n');
792
+ }
478
793
  export function buildRetryPrompt(failureOutput, failedAttempts = []) {
479
794
  const parts = [];
480
795
  if (failedAttempts.length > 0) {
481
796
  parts.push(`You have already attempted to fix this ${failedAttempts.length} time(s). Do NOT repeat these failed approaches:`);
482
797
  for (const a of failedAttempts) {
483
- const hyp = a.hypothesis ? `Planned: [${a.hypothesis.slice(0, 300)}]` : '(no plan recorded)';
484
- parts.push(`- Attempt ${a.attemptNumber}: ${hyp} → failed with: ${a.failureReason.slice(0, 300)}`);
798
+ let hypContext = a.hypothesis;
799
+ if (hypContext) {
800
+ const planMatch = hypContext.match(/(?:4\.\s*WHY IT FAILED|5\.\s*PLAN)[\s\S]*/i);
801
+ if (planMatch) {
802
+ hypContext = planMatch[0];
803
+ }
804
+ else if (hypContext.length > 800) {
805
+ hypContext = '...' + hypContext.slice(-800);
806
+ }
807
+ }
808
+ const hyp = hypContext ? `[${hypContext.slice(0, 1000)}]` : '(no plan recorded)';
809
+ parts.push(`- Attempt ${a.attemptNumber} Reasoning: ${hyp}\n Failed with: ${a.failureReason.slice(0, 800)}`);
485
810
  }
486
811
  parts.push('');
487
812
  }
@@ -501,6 +826,9 @@ export function buildRetryPrompt(failureOutput, failedAttempts = []) {
501
826
  const bleedWarning = detectThinkingBleed(failureOutput);
502
827
  if (bleedWarning)
503
828
  parts.push(`\n⚠️ ${bleedWarning}`);
829
+ const tsErrorWarning = detectTypeScriptErrors(failureOutput);
830
+ if (tsErrorWarning)
831
+ parts.push(`\n⚠️ ${tsErrorWarning}`);
504
832
  parts.push('');
505
833
  parts.push('Common causes:');
506
834
  parts.push('- Wrong import path — check the path aliases and dependency list from the original prompt');