lacuna-cli 0.1.5 → 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 +38 -10
  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 +285 -70
  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 +107 -21
  24. package/dist/commands/init.js.map +1 -1
  25. package/dist/lib/config.d.ts +3 -3
  26. package/dist/lib/config.d.ts.map +1 -1
  27. package/dist/lib/config.js +3 -1
  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 +1 -0
  34. package/dist/lib/detector.d.ts.map +1 -1
  35. package/dist/lib/detector.js +19 -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 +16 -2
  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 +9 -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) {
@@ -195,21 +195,25 @@ function detectNextJsImportError(errorOutput) {
195
195
  if (!failedImport)
196
196
  return null;
197
197
  const importPath = failedImport[1];
198
+ const isServerOnly = importPath === 'server-only';
198
199
  const isAlias = importPath.startsWith('@/');
199
200
  const isClient = importPath.endsWith('.client');
200
201
  const isServer = importPath.endsWith('.server');
201
202
  const isNextInternal = importPath.startsWith('next/');
202
203
  const isProviderOrSession = /session|auth|provider/i.test(importPath);
203
- if (!isAlias && !isClient && !isServer && !isNextInternal && !isProviderOrSession)
204
+ if (!isServerOnly && !isAlias && !isClient && !isServer && !isNextInternal && !isProviderOrSession)
204
205
  return null;
205
206
  const lines = [
206
207
  `IMPORT RESOLUTION ERROR — Vitest cannot resolve "${importPath}".`,
207
208
  ];
208
- 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) {
209
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.`);
210
214
  }
211
215
  else if (isClient || isServer) {
212
- lines.push(`This is a Next.js ${isClient ? 'client' : 'server'} boundary file. Vitest never resolves these mock it with the exact import path from the source file:`, ` vi.mock('${importPath}', () => ({`, ` // each function the source imports: myFn: vi.fn()`, ` }))`);
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.`);
213
217
  }
214
218
  else if (isNextInternal) {
215
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() */ }))`);
@@ -274,6 +278,56 @@ function buildNetworkMockingGuidance(analysis, sourceFile) {
274
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.`);
275
279
  return lines.join('\n');
276
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
+ }
277
331
  // Extract exported names from a mocks file so the AI sees a concrete inventory.
278
332
  function parseMockExports(code) {
279
333
  const names = [];
@@ -296,83 +350,145 @@ function parseMockExports(code) {
296
350
  return [...new Set(names)];
297
351
  }
298
352
  export function buildSystemPrompt(env) {
299
- return `You are a senior QA engineer with 10+ years of experience writing production test suites for ${env.language} projects. You use ${env.testRunner} and you take testing seriously.
300
-
301
- Your tests catch real bugs. You think about what could go wrong — null inputs, empty arrays, async race conditions, error boundaries, permission checks, off-by-one errors — and you write assertions that would actually fail if the code broke. You never write a test just to hit a coverage number.
302
-
303
- RULES follow every one:
304
- 1. Write tests that verify real behavior: correctness, edge cases, boundary values, and error handling. Never write empty or trivial assertions (e.g. expect(true).toBe(true)).
305
- 2. Match the EXACT import style shown in the existing test file or PROJECT TEST EXAMPLES. If none exists, use the style from the source file.
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 ? `
306
383
  3. Use path aliases from the PROJECT TYPESCRIPT CONFIG section in IMPORT statements (e.g. "@/components/Button" not "../../components/Button").
307
- EXCEPTION — vi.mock() call paths: use the exact same path string that appears in the SOURCE FILE'S import statement.
308
- If a LOCAL IMPORT PATHS section is provided, use those pre-computed relative paths in vi.mock() calls — they are the fallback when aliases cannot be resolved by Vitest.
309
- Never second-guess the pre-computed paths. Never convert them back to @/ aliases in vi.mock() calls.
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.
310
387
  4. Only import from packages listed in PROJECT DEPENDENCIES. Do not invent packages that are not listed.
311
- 5. When a SHARED MOCK FILE is provided, its exported names are listed under "Available exports". Before writing the test, go through that list and identify every mock that relates to what the source file does (by name, type, or domain). Import and use ALL of those mocks. Never re-create inline vi.fn() / jest.fn() for anything already exported from the mocks file.
312
- CRITICAL — never rename or change the casing of existing mock exports. If the mocks file has mockValidationError, use exactly mockValidationError — do NOT change it to MockValidationError or any other variant. Renaming an existing mock breaks every test that already imports it by the original name.
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.
313
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---
314
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.
315
- CRITICAL — the mocks file must contain ONLY: vi.fn()/jest.fn() mock definitions, vi.mock() module stubs, shared mock objects/constants, and beforeEach reset hooks. NEVER write describe(), it(), test(), or expect() calls in the mocks file. Those belong exclusively in the test file. A mocks file that contains test blocks will break the entire test suite.
316
- 8. If a TEST SETUP FILE is shown, assume its globals and matchers are already available (e.g. expect(...).toBeInTheDocument()). Do NOT import or re-declare them.
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 ? `
317
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.
318
- - When tsc reports an error, read it literally and apply the precise fix it describes. Never override the compiler with framework conventions or assumptions.
319
- - Use EXACT property/member names from the TYPE DEFINITIONS section and source code. Do not apply naming conventions: if a hook returns { loading, users }, do NOT write isLoading or isUsers. Read what is actually declared.
320
- - Import enums, constants, interfaces, and types from the project's existing files. Do NOT redeclare them inline or invent lookalike values. Check TYPE DEFINITIONS and the source file's imports for what already exists.
321
- - Never use "as any", "@ts-ignore", or "@ts-expect-error" to suppress type errors. Use proper types or generics instead.
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.
322
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>>().
323
- - Use ReturnType<>, Parameters<>, and other utility types to derive mock types from the real function signatures rather than guessing.
324
- - When accessing optional properties, handle null/undefined correctly do not assume they exist if the type says otherwise.
325
- 10. Every test file MUST contain at least one it() or test() call with real assertions. A file with only imports, describe() blocks, types, or helper functions is invalid and will be rejected. If you cannot write meaningful tests, write a minimal test that exercises the simplest exported function.
326
- 11. Structure ALL output using exactly these two XML blocks — nothing before, nothing after:
327
- <thinking>
328
- 1. WHAT IS NEEDED: What functions/behaviors are untested or broken?
329
- 2. COMPONENT RENDER MAP (React components only): Before writing any assertion, list what is in the DOM in each relevant state (idle / loading / error / success). Read the JSX check every ternary, &&, and switch — to determine whether a button is disabled vs unmounted, what text changes, what elements appear. Never assume a button is disabled during loading without verifying this in the JSX.
330
- 3. WHY IT FAILED (retries only): What is the structural root cause — wrong mock level, missing await, bad import path, type mismatch?
331
- 4. PLAN: List the exact steps you will take before writing a single line of code.
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
+ : '';
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.
459
+
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.
461
+
462
+ RULES — follow every one:
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)).
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}
332
468
  </thinking>
333
469
  <code_output>
334
470
  // complete test file here
335
471
  </code_output>
336
- CRITICAL: Once you open <code_output>, ALL remaining output must be code. Never continue reasoning, writing bullet points,
337
- or restating the problem inside <code_output>. If you are still uncertain, finish your reasoning inside <thinking> first,
338
- then commit to a solution and write only code inside <code_output>. Thinking inside <code_output> corrupts the output file.
339
- 12. Inside <code_output>: do NOT wrap in markdown code fences.
340
- 13. Inside <code_output>: output ONLY the test file content (or test file // ---MOCKS_FILE--- mocks file).
341
- If you use // ---MOCKS_FILE---, everything AFTER the separator is the mocks file. The mocks file must contain ONLY:
342
- vi.fn()/jest.fn() mock definitions, vi.mock() module stubs, shared constants, and beforeEach resets.
343
- NEVER put describe(), it(), test(), or expect() calls after the separator — those belong BEFORE it, in the test file.
344
- Writing test blocks in the mocks section corrupts the shared mock file for every other test in the project.
345
- 14. NEVER output vitest.config.ts, jest.config.js, or any framework configuration. If an import cannot be resolved,
346
- fix it by mocking it with vi.mock() — NOT by modifying the test runner configuration. You cannot modify config
347
- files from here, and outputting them will cause the entire mocks file to be discarded.
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}
348
474
 
349
475
  A good test suite you write will have:
350
476
  - A happy-path test that confirms the main behavior works
351
477
  - At least one edge-case test per function (empty input, zero, null, boundary values)
352
- - Error-path tests for any function that throws, rejects, or returns an error state
353
- - Async tests properly awaited — never fire-and-forget
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}
354
480
  - Clear, descriptive test names that read like a spec ("returns null when user is not authenticated")
355
481
 
356
482
  Common failure causes to avoid:
357
- - Wrong import paths (use aliases, not relative ../../ paths if the project uses aliases)
358
- - Importing from test utils that are not in the dependency list
359
- - Mocking modules that are already mocked in the setup file
360
- - Using browser globals without jsdom (only use them if the setup file configures jsdom)
361
- - Forgetting to await async functions
362
- - Top-level await (TS1378): NEVER use \`await\` at the top level of a test file. Every \`await\` must be inside an async callback: \`it("...", async () => { ... })\`, \`beforeEach(async () => { ... })\`, etc. Top-level \`await\` is rejected by most TypeScript configurations and will cause a type error even if the tests appear to run.
363
- - React 18 act() async rule: ALWAYS await act() when it wraps async code — \`await act(async () => { ... })\`. Never store an unawaited act() call in a variable like \`const promise = act(async () => ...)\` without immediately awaiting it. Unawaited act() calls cause React state updates to leak into subsequent tests, producing cascading timeout failures and "Cannot read properties of null" errors in unrelated tests.
364
- - vi.mock() paths are relative to the TEST FILE, not the source file. If the test is at src/features/auth/__tests__/Login.test.tsx and you need to mock src/components/Button, the mock path is ../../../components/Button — count directories from the TEST file's location, not the source file's.
365
- - Loading state architecture: before asserting that a button is disabled during loading, check whether the component hides the button entirely and replaces it with a spinner. If the button is unmounted during loading rather than disabled, \`getByText("Submit")\` will throw — test for the spinner instead, or use \`queryByText("Submit")\` with a null assertion.
366
- - Unhandled promise rejections: when testing error paths with mockRejectedValueOnce, the rejection must be fully resolved inside the test. After triggering the action, always use await waitFor(() => expect(errorElement).toBeInTheDocument()) to tie the rejection to the test scope. Never let a rejected mock promise go unawaited — Vitest will flag it as an unhandled error even if the component catches it internally.
367
- - Real HTTP requests: NEVER let a real network call reach the internet. If you see a real URL (https://...), a 401/403 error, or a network timeout in test output, your mock is missing or at the wrong level. Every function that calls an API must be mocked before the test runs.
368
- - Barrel file vi.mock() resolution: if a module is exported from a barrel/index file (e.g. src/components/index.ts re-exports Foo from ./Foo), mock the DIRECT file path, not the barrel. vi.mock('../components') mocks the barrel but the component may import directly from '../components/Foo' — making the mock miss. Always mock the specific module the source file actually imports. If unsure, mock both the direct file AND the barrel.
483
+ ${universalCauses}${jsCauses}${vitestCauses}${reactCauses}
369
484
 
370
485
  Test file pattern for this project: ${env.testFilePattern}
371
486
 
372
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.`;
373
488
  }
374
489
  export function buildGeneratePrompt(args) {
375
- 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);
376
492
  const parts = [];
377
493
  if (projectMemory) {
378
494
  parts.push(projectMemory);
@@ -418,11 +534,36 @@ export function buildGeneratePrompt(args) {
418
534
  parts.push(`\nSHARED MOCK FILE (import from: '${mocksImportPath}')`);
419
535
  if (exports.length > 0) {
420
536
  parts.push(`Available exports: ${exports.join(', ')}`);
421
- parts.push(`↑ Before writing the test, identify which of these match the source file's domain and import every relevant one. Do NOT create inline mocks for anything already in this list.\n↑ NAMES ARE FROZEN — use each export exactly as spelled above. Never rename, recase, or restructure an existing mock (e.g. do not change mockFoo → MockFoo or const → class). Renaming breaks every other test that imports the original name.`);
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.`);
422
538
  }
423
- parts.push('```');
424
- parts.push(mocksCode);
425
- 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.
426
567
  }
427
568
  else {
428
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.`);
@@ -466,7 +607,8 @@ export function buildGeneratePrompt(args) {
466
607
  return parts.join('\n');
467
608
  }
468
609
  export function buildFixPrompt(args) {
469
- 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;
470
612
  const parts = [];
471
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.');
472
614
  parts.push('');
@@ -507,13 +649,35 @@ export function buildFixPrompt(args) {
507
649
  if (mocksImportPath) {
508
650
  if (mocksCode) {
509
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);
510
657
  parts.push(`\nSHARED MOCK FILE (import from: '${mocksImportPath}')`);
511
658
  if (exports.length > 0) {
512
659
  parts.push(`Available exports: ${exports.join(', ')}`);
513
- parts.push(`↑ Check this list against the source file — import every relevant mock. Do NOT create inline mocks for anything already exported here.`);
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.');
514
678
  }
515
679
  parts.push('```');
516
- parts.push(mocksCode);
680
+ parts.push(compressed);
517
681
  parts.push('```');
518
682
  }
519
683
  else {
@@ -531,9 +695,9 @@ export function buildFixPrompt(args) {
531
695
  const nextGuidance = buildNextJsGuidance(analyzeNextJs(sourceCode));
532
696
  if (nextGuidance)
533
697
  parts.push(`\n${nextGuidance}`);
534
- // For fix: skeleton with no function expansion the test already tells the AI
535
- // which function matters; showing signatures is enough to understand the API.
536
- const FIX_SKELETON_THRESHOLD = 150;
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;
537
701
  const displaySource = sourceCode.split('\n').length > FIX_SKELETON_THRESHOLD
538
702
  ? buildSourceSkeleton(sourceCode, [])
539
703
  : sourceCode;
@@ -585,13 +749,64 @@ export function buildFixPrompt(args) {
585
749
  parts.push('\nReturn your response in the required <thinking> + <code_output> format.');
586
750
  return parts.join('\n');
587
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
+ }
588
793
  export function buildRetryPrompt(failureOutput, failedAttempts = []) {
589
794
  const parts = [];
590
795
  if (failedAttempts.length > 0) {
591
796
  parts.push(`You have already attempted to fix this ${failedAttempts.length} time(s). Do NOT repeat these failed approaches:`);
592
797
  for (const a of failedAttempts) {
593
- const hyp = a.hypothesis ? `Planned: [${a.hypothesis.slice(0, 300)}]` : '(no plan recorded)';
594
- parts.push(`- Attempt ${a.attemptNumber}: ${hyp} → failed with: ${a.failureReason.slice(0, 300)}`);
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)}`);
595
810
  }
596
811
  parts.push('');
597
812
  }