mindheal 1.0.0

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 (255) hide show
  1. package/.env.example +48 -0
  2. package/CHANGELOG.md +27 -0
  3. package/LICENSE +21 -0
  4. package/README.md +481 -0
  5. package/dist/cjs/ai/ai-provider.js +46 -0
  6. package/dist/cjs/ai/ai-provider.js.map +1 -0
  7. package/dist/cjs/ai/anthropic-provider.js +106 -0
  8. package/dist/cjs/ai/anthropic-provider.js.map +1 -0
  9. package/dist/cjs/ai/azure-openai-provider.js +130 -0
  10. package/dist/cjs/ai/azure-openai-provider.js.map +1 -0
  11. package/dist/cjs/ai/bedrock-provider.js +183 -0
  12. package/dist/cjs/ai/bedrock-provider.js.map +1 -0
  13. package/dist/cjs/ai/deepseek-provider.js +118 -0
  14. package/dist/cjs/ai/deepseek-provider.js.map +1 -0
  15. package/dist/cjs/ai/gemini-provider.js +129 -0
  16. package/dist/cjs/ai/gemini-provider.js.map +1 -0
  17. package/dist/cjs/ai/groq-provider.js +118 -0
  18. package/dist/cjs/ai/groq-provider.js.map +1 -0
  19. package/dist/cjs/ai/meta-provider.js +118 -0
  20. package/dist/cjs/ai/meta-provider.js.map +1 -0
  21. package/dist/cjs/ai/ollama-provider.js +127 -0
  22. package/dist/cjs/ai/ollama-provider.js.map +1 -0
  23. package/dist/cjs/ai/openai-provider.js +117 -0
  24. package/dist/cjs/ai/openai-provider.js.map +1 -0
  25. package/dist/cjs/ai/perplexity-provider.js +118 -0
  26. package/dist/cjs/ai/perplexity-provider.js.map +1 -0
  27. package/dist/cjs/ai/prompt-templates.js +174 -0
  28. package/dist/cjs/ai/prompt-templates.js.map +1 -0
  29. package/dist/cjs/ai/qwen-provider.js +118 -0
  30. package/dist/cjs/ai/qwen-provider.js.map +1 -0
  31. package/dist/cjs/analytics/healing-analytics.js +263 -0
  32. package/dist/cjs/analytics/healing-analytics.js.map +1 -0
  33. package/dist/cjs/cli/init.js +517 -0
  34. package/dist/cjs/cli/init.js.map +1 -0
  35. package/dist/cjs/config/config-loader.js +135 -0
  36. package/dist/cjs/config/config-loader.js.map +1 -0
  37. package/dist/cjs/config/defaults.js +109 -0
  38. package/dist/cjs/config/defaults.js.map +1 -0
  39. package/dist/cjs/core/dom-snapshot.js +280 -0
  40. package/dist/cjs/core/dom-snapshot.js.map +1 -0
  41. package/dist/cjs/core/enterprise-strategy.js +702 -0
  42. package/dist/cjs/core/enterprise-strategy.js.map +1 -0
  43. package/dist/cjs/core/healer.js +283 -0
  44. package/dist/cjs/core/healer.js.map +1 -0
  45. package/dist/cjs/core/interceptor.js +945 -0
  46. package/dist/cjs/core/interceptor.js.map +1 -0
  47. package/dist/cjs/core/locator-analyzer.js +172 -0
  48. package/dist/cjs/core/locator-analyzer.js.map +1 -0
  49. package/dist/cjs/core/locator-strategies.js +891 -0
  50. package/dist/cjs/core/locator-strategies.js.map +1 -0
  51. package/dist/cjs/core/self-heal-cache.js +178 -0
  52. package/dist/cjs/core/self-heal-cache.js.map +1 -0
  53. package/dist/cjs/core/smart-retry.js +248 -0
  54. package/dist/cjs/core/smart-retry.js.map +1 -0
  55. package/dist/cjs/core/visual-verification.js +262 -0
  56. package/dist/cjs/core/visual-verification.js.map +1 -0
  57. package/dist/cjs/git/code-modifier.js +184 -0
  58. package/dist/cjs/git/code-modifier.js.map +1 -0
  59. package/dist/cjs/git/git-operations.js +145 -0
  60. package/dist/cjs/git/git-operations.js.map +1 -0
  61. package/dist/cjs/git/pr-creator.js +190 -0
  62. package/dist/cjs/git/pr-creator.js.map +1 -0
  63. package/dist/cjs/index.js +97 -0
  64. package/dist/cjs/index.js.map +1 -0
  65. package/dist/cjs/rag/context-retriever.js +289 -0
  66. package/dist/cjs/rag/context-retriever.js.map +1 -0
  67. package/dist/cjs/rag/embeddings.js +82 -0
  68. package/dist/cjs/rag/embeddings.js.map +1 -0
  69. package/dist/cjs/rag/knowledge-store.js +159 -0
  70. package/dist/cjs/rag/knowledge-store.js.map +1 -0
  71. package/dist/cjs/reporters/heal-report.js +279 -0
  72. package/dist/cjs/reporters/heal-report.js.map +1 -0
  73. package/dist/cjs/reporters/heal-reporter.js +294 -0
  74. package/dist/cjs/reporters/heal-reporter.js.map +1 -0
  75. package/dist/cjs/server/review-server.js +166 -0
  76. package/dist/cjs/server/review-server.js.map +1 -0
  77. package/dist/cjs/server/routes.js +92 -0
  78. package/dist/cjs/server/routes.js.map +1 -0
  79. package/dist/cjs/utils/environment.js +57 -0
  80. package/dist/cjs/utils/environment.js.map +1 -0
  81. package/dist/cjs/utils/file-lock.js +136 -0
  82. package/dist/cjs/utils/file-lock.js.map +1 -0
  83. package/dist/cjs/utils/file-utils.js +49 -0
  84. package/dist/cjs/utils/file-utils.js.map +1 -0
  85. package/dist/cjs/utils/logger.js +78 -0
  86. package/dist/cjs/utils/logger.js.map +1 -0
  87. package/dist/esm/ai/ai-provider.js +44 -0
  88. package/dist/esm/ai/ai-provider.js.map +1 -0
  89. package/dist/esm/ai/anthropic-provider.js +104 -0
  90. package/dist/esm/ai/anthropic-provider.js.map +1 -0
  91. package/dist/esm/ai/azure-openai-provider.js +128 -0
  92. package/dist/esm/ai/azure-openai-provider.js.map +1 -0
  93. package/dist/esm/ai/bedrock-provider.js +181 -0
  94. package/dist/esm/ai/bedrock-provider.js.map +1 -0
  95. package/dist/esm/ai/deepseek-provider.js +116 -0
  96. package/dist/esm/ai/deepseek-provider.js.map +1 -0
  97. package/dist/esm/ai/gemini-provider.js +127 -0
  98. package/dist/esm/ai/gemini-provider.js.map +1 -0
  99. package/dist/esm/ai/groq-provider.js +116 -0
  100. package/dist/esm/ai/groq-provider.js.map +1 -0
  101. package/dist/esm/ai/meta-provider.js +116 -0
  102. package/dist/esm/ai/meta-provider.js.map +1 -0
  103. package/dist/esm/ai/ollama-provider.js +125 -0
  104. package/dist/esm/ai/ollama-provider.js.map +1 -0
  105. package/dist/esm/ai/openai-provider.js +115 -0
  106. package/dist/esm/ai/openai-provider.js.map +1 -0
  107. package/dist/esm/ai/perplexity-provider.js +116 -0
  108. package/dist/esm/ai/perplexity-provider.js.map +1 -0
  109. package/dist/esm/ai/prompt-templates.js +171 -0
  110. package/dist/esm/ai/prompt-templates.js.map +1 -0
  111. package/dist/esm/ai/qwen-provider.js +116 -0
  112. package/dist/esm/ai/qwen-provider.js.map +1 -0
  113. package/dist/esm/analytics/healing-analytics.js +261 -0
  114. package/dist/esm/analytics/healing-analytics.js.map +1 -0
  115. package/dist/esm/cli/init.js +495 -0
  116. package/dist/esm/cli/init.js.map +1 -0
  117. package/dist/esm/config/config-loader.js +132 -0
  118. package/dist/esm/config/config-loader.js.map +1 -0
  119. package/dist/esm/config/defaults.js +107 -0
  120. package/dist/esm/config/defaults.js.map +1 -0
  121. package/dist/esm/core/dom-snapshot.js +278 -0
  122. package/dist/esm/core/dom-snapshot.js.map +1 -0
  123. package/dist/esm/core/enterprise-strategy.js +695 -0
  124. package/dist/esm/core/enterprise-strategy.js.map +1 -0
  125. package/dist/esm/core/healer.js +281 -0
  126. package/dist/esm/core/healer.js.map +1 -0
  127. package/dist/esm/core/interceptor.js +940 -0
  128. package/dist/esm/core/interceptor.js.map +1 -0
  129. package/dist/esm/core/locator-analyzer.js +169 -0
  130. package/dist/esm/core/locator-analyzer.js.map +1 -0
  131. package/dist/esm/core/locator-strategies.js +882 -0
  132. package/dist/esm/core/locator-strategies.js.map +1 -0
  133. package/dist/esm/core/self-heal-cache.js +176 -0
  134. package/dist/esm/core/self-heal-cache.js.map +1 -0
  135. package/dist/esm/core/smart-retry.js +246 -0
  136. package/dist/esm/core/smart-retry.js.map +1 -0
  137. package/dist/esm/core/visual-verification.js +260 -0
  138. package/dist/esm/core/visual-verification.js.map +1 -0
  139. package/dist/esm/git/code-modifier.js +182 -0
  140. package/dist/esm/git/code-modifier.js.map +1 -0
  141. package/dist/esm/git/git-operations.js +143 -0
  142. package/dist/esm/git/git-operations.js.map +1 -0
  143. package/dist/esm/git/pr-creator.js +188 -0
  144. package/dist/esm/git/pr-creator.js.map +1 -0
  145. package/dist/esm/index.js +37 -0
  146. package/dist/esm/index.js.map +1 -0
  147. package/dist/esm/rag/context-retriever.js +287 -0
  148. package/dist/esm/rag/context-retriever.js.map +1 -0
  149. package/dist/esm/rag/embeddings.js +77 -0
  150. package/dist/esm/rag/embeddings.js.map +1 -0
  151. package/dist/esm/rag/knowledge-store.js +157 -0
  152. package/dist/esm/rag/knowledge-store.js.map +1 -0
  153. package/dist/esm/reporters/heal-report.js +277 -0
  154. package/dist/esm/reporters/heal-report.js.map +1 -0
  155. package/dist/esm/reporters/heal-reporter.js +290 -0
  156. package/dist/esm/reporters/heal-reporter.js.map +1 -0
  157. package/dist/esm/server/review-server.js +164 -0
  158. package/dist/esm/server/review-server.js.map +1 -0
  159. package/dist/esm/server/routes.js +90 -0
  160. package/dist/esm/server/routes.js.map +1 -0
  161. package/dist/esm/utils/environment.js +53 -0
  162. package/dist/esm/utils/environment.js.map +1 -0
  163. package/dist/esm/utils/file-lock.js +134 -0
  164. package/dist/esm/utils/file-lock.js.map +1 -0
  165. package/dist/esm/utils/file-utils.js +43 -0
  166. package/dist/esm/utils/file-utils.js.map +1 -0
  167. package/dist/esm/utils/logger.js +75 -0
  168. package/dist/esm/utils/logger.js.map +1 -0
  169. package/dist/types/ai/ai-provider.d.ts +4 -0
  170. package/dist/types/ai/ai-provider.d.ts.map +1 -0
  171. package/dist/types/ai/anthropic-provider.d.ts +11 -0
  172. package/dist/types/ai/anthropic-provider.d.ts.map +1 -0
  173. package/dist/types/ai/azure-openai-provider.d.ts +13 -0
  174. package/dist/types/ai/azure-openai-provider.d.ts.map +1 -0
  175. package/dist/types/ai/bedrock-provider.d.ts +14 -0
  176. package/dist/types/ai/bedrock-provider.d.ts.map +1 -0
  177. package/dist/types/ai/deepseek-provider.d.ts +12 -0
  178. package/dist/types/ai/deepseek-provider.d.ts.map +1 -0
  179. package/dist/types/ai/gemini-provider.d.ts +12 -0
  180. package/dist/types/ai/gemini-provider.d.ts.map +1 -0
  181. package/dist/types/ai/groq-provider.d.ts +12 -0
  182. package/dist/types/ai/groq-provider.d.ts.map +1 -0
  183. package/dist/types/ai/meta-provider.d.ts +12 -0
  184. package/dist/types/ai/meta-provider.d.ts.map +1 -0
  185. package/dist/types/ai/ollama-provider.d.ts +10 -0
  186. package/dist/types/ai/ollama-provider.d.ts.map +1 -0
  187. package/dist/types/ai/openai-provider.d.ts +11 -0
  188. package/dist/types/ai/openai-provider.d.ts.map +1 -0
  189. package/dist/types/ai/perplexity-provider.d.ts +12 -0
  190. package/dist/types/ai/perplexity-provider.d.ts.map +1 -0
  191. package/dist/types/ai/prompt-templates.d.ts +11 -0
  192. package/dist/types/ai/prompt-templates.d.ts.map +1 -0
  193. package/dist/types/ai/qwen-provider.d.ts +12 -0
  194. package/dist/types/ai/qwen-provider.d.ts.map +1 -0
  195. package/dist/types/analytics/healing-analytics.d.ts +36 -0
  196. package/dist/types/analytics/healing-analytics.d.ts.map +1 -0
  197. package/dist/types/cli/init.d.ts +15 -0
  198. package/dist/types/cli/init.d.ts.map +1 -0
  199. package/dist/types/config/config-loader.d.ts +4 -0
  200. package/dist/types/config/config-loader.d.ts.map +1 -0
  201. package/dist/types/config/defaults.d.ts +3 -0
  202. package/dist/types/config/defaults.d.ts.map +1 -0
  203. package/dist/types/core/dom-snapshot.d.ts +12 -0
  204. package/dist/types/core/dom-snapshot.d.ts.map +1 -0
  205. package/dist/types/core/enterprise-strategy.d.ts +56 -0
  206. package/dist/types/core/enterprise-strategy.d.ts.map +1 -0
  207. package/dist/types/core/healer.d.ts +52 -0
  208. package/dist/types/core/healer.d.ts.map +1 -0
  209. package/dist/types/core/interceptor.d.ts +64 -0
  210. package/dist/types/core/interceptor.d.ts.map +1 -0
  211. package/dist/types/core/locator-analyzer.d.ts +31 -0
  212. package/dist/types/core/locator-analyzer.d.ts.map +1 -0
  213. package/dist/types/core/locator-strategies.d.ts +45 -0
  214. package/dist/types/core/locator-strategies.d.ts.map +1 -0
  215. package/dist/types/core/self-heal-cache.d.ts +51 -0
  216. package/dist/types/core/self-heal-cache.d.ts.map +1 -0
  217. package/dist/types/core/smart-retry.d.ts +64 -0
  218. package/dist/types/core/smart-retry.d.ts.map +1 -0
  219. package/dist/types/core/visual-verification.d.ts +46 -0
  220. package/dist/types/core/visual-verification.d.ts.map +1 -0
  221. package/dist/types/git/code-modifier.d.ts +51 -0
  222. package/dist/types/git/code-modifier.d.ts.map +1 -0
  223. package/dist/types/git/git-operations.d.ts +40 -0
  224. package/dist/types/git/git-operations.d.ts.map +1 -0
  225. package/dist/types/git/pr-creator.d.ts +27 -0
  226. package/dist/types/git/pr-creator.d.ts.map +1 -0
  227. package/dist/types/index.d.ts +40 -0
  228. package/dist/types/index.d.ts.map +1 -0
  229. package/dist/types/rag/context-retriever.d.ts +69 -0
  230. package/dist/types/rag/context-retriever.d.ts.map +1 -0
  231. package/dist/types/rag/embeddings.d.ts +32 -0
  232. package/dist/types/rag/embeddings.d.ts.map +1 -0
  233. package/dist/types/rag/index.d.ts +12 -0
  234. package/dist/types/rag/index.d.ts.map +1 -0
  235. package/dist/types/rag/knowledge-store.d.ts +38 -0
  236. package/dist/types/rag/knowledge-store.d.ts.map +1 -0
  237. package/dist/types/reporters/heal-report.d.ts +29 -0
  238. package/dist/types/reporters/heal-report.d.ts.map +1 -0
  239. package/dist/types/reporters/heal-reporter.d.ts +49 -0
  240. package/dist/types/reporters/heal-reporter.d.ts.map +1 -0
  241. package/dist/types/server/review-server.d.ts +20 -0
  242. package/dist/types/server/review-server.d.ts.map +1 -0
  243. package/dist/types/server/routes.d.ts +4 -0
  244. package/dist/types/server/routes.d.ts.map +1 -0
  245. package/dist/types/types/index.d.ts +433 -0
  246. package/dist/types/types/index.d.ts.map +1 -0
  247. package/dist/types/utils/environment.d.ts +10 -0
  248. package/dist/types/utils/environment.d.ts.map +1 -0
  249. package/dist/types/utils/file-lock.d.ts +37 -0
  250. package/dist/types/utils/file-lock.d.ts.map +1 -0
  251. package/dist/types/utils/file-utils.d.ts +7 -0
  252. package/dist/types/utils/file-utils.d.ts.map +1 -0
  253. package/dist/types/utils/logger.d.ts +9 -0
  254. package/dist/types/utils/logger.d.ts.map +1 -0
  255. package/package.json +106 -0
@@ -0,0 +1,940 @@
1
+ import { test } from '@playwright/test';
2
+ import { Healer } from './healer.js';
3
+ import { SelfHealCache } from './self-heal-cache.js';
4
+ import { createAIProvider } from '../ai/ai-provider.js';
5
+ import { loadConfig } from '../config/config-loader.js';
6
+ import { logger } from '../utils/logger.js';
7
+ import { KnowledgeStore } from '../rag/knowledge-store.js';
8
+ import { ContextRetriever } from '../rag/context-retriever.js';
9
+ import { SmartRetry } from './smart-retry.js';
10
+ import { VisualVerifier } from './visual-verification.js';
11
+ import { HealingAnalytics } from '../analytics/healing-analytics.js';
12
+ import { FileLock } from '../utils/file-lock.js';
13
+
14
+ // ─── Global session manager ──────────────────────────────────────────────────
15
+ /**
16
+ * A global map that allows the reporter (which runs in a separate context)
17
+ * to access healing sessions keyed by a deterministic test-worker id.
18
+ */
19
+ const _sessions = new Map();
20
+ function getAllHealingSessions() {
21
+ return Array.from(_sessions.values());
22
+ }
23
+ // ─── Source location extraction ──────────────────────────────────────────────
24
+ /**
25
+ * Parses an Error stack trace to find the originating test file and line.
26
+ * Skips frames that originate from node_modules or from this module itself.
27
+ */
28
+ function extractSourceLocation(error) {
29
+ const stack = error.stack;
30
+ if (!stack)
31
+ return null;
32
+ const lines = stack.split('\n');
33
+ for (const line of lines) {
34
+ // Skip frames from mindheal internals and node_modules
35
+ if (line.includes('node_modules'))
36
+ continue;
37
+ if (line.includes('interceptor.ts') || line.includes('interceptor.js'))
38
+ continue;
39
+ if (line.includes('healer.ts') || line.includes('healer.js'))
40
+ continue;
41
+ // Match common stack frame patterns:
42
+ // at Function (file:line:col)
43
+ // at file:line:col
44
+ const match = line.match(/\(?([^()]+?):(\d+):(\d+)\)?$/);
45
+ if (match) {
46
+ const filePath = match[1].replace(/^file:\/\/\//, '');
47
+ const lineNum = parseInt(match[2], 10);
48
+ const column = parseInt(match[3], 10);
49
+ if (!isNaN(lineNum) && !isNaN(column)) {
50
+ return { filePath, line: lineNum, column };
51
+ }
52
+ }
53
+ }
54
+ return null;
55
+ }
56
+ // ─── Event ID generation ─────────────────────────────────────────────────────
57
+ function generateEventId() {
58
+ const timestamp = Date.now().toString(36);
59
+ const random = Math.random().toString(36).substring(2, 10);
60
+ return `evt_${timestamp}_${random}`;
61
+ }
62
+ // ─── Method-to-type mapping ──────────────────────────────────────────────────
63
+ const PAGE_LOCATOR_METHODS = [
64
+ 'locator',
65
+ 'getByRole',
66
+ 'getByText',
67
+ 'getByTestId',
68
+ 'getByLabel',
69
+ 'getByPlaceholder',
70
+ 'getByAltText',
71
+ 'getByTitle',
72
+ ];
73
+ const METHOD_TO_TYPE = {
74
+ locator: 'css',
75
+ getByRole: 'role',
76
+ getByText: 'text',
77
+ getByTestId: 'testid',
78
+ getByLabel: 'label',
79
+ getByPlaceholder: 'placeholder',
80
+ getByAltText: 'alttext',
81
+ getByTitle: 'title',
82
+ };
83
+ /**
84
+ * Actions that perform mutations or interactions and should trigger healing on failure.
85
+ */
86
+ const INTERCEPTED_ACTIONS = new Set([
87
+ 'click',
88
+ 'fill',
89
+ 'type',
90
+ 'check',
91
+ 'uncheck',
92
+ 'selectOption',
93
+ 'hover',
94
+ 'press',
95
+ 'dblclick',
96
+ 'textContent',
97
+ 'innerText',
98
+ 'innerHTML',
99
+ 'inputValue',
100
+ 'isVisible',
101
+ 'isEnabled',
102
+ 'isChecked',
103
+ 'waitFor',
104
+ ]);
105
+ // ─── Locator info helpers ────────────────────────────────────────────────────
106
+ function buildLocatorInfo(method, args) {
107
+ const selector = typeof args[0] === 'string' ? args[0] : String(args[0]);
108
+ const options = args.length > 1 && typeof args[1] === 'object' && args[1] !== null
109
+ ? args[1]
110
+ : undefined;
111
+ let expression;
112
+ if (method === 'locator') {
113
+ expression = options
114
+ ? `page.locator('${selector}', ${JSON.stringify(options)})`
115
+ : `page.locator('${selector}')`;
116
+ }
117
+ else if (options) {
118
+ expression = `page.${method}('${selector}', ${JSON.stringify(options)})`;
119
+ }
120
+ else {
121
+ expression = `page.${method}('${selector}')`;
122
+ }
123
+ return {
124
+ type: METHOD_TO_TYPE[method],
125
+ selector,
126
+ options,
127
+ playwrightExpression: expression,
128
+ };
129
+ }
130
+ // ─── Error classification ────────────────────────────────────────────────────
131
+ /**
132
+ * Determines whether an error is a locator-resolution failure that healing
133
+ * can potentially fix (timeout waiting for element, strict mode violation, etc.).
134
+ */
135
+ function isHealableError(error) {
136
+ if (!(error instanceof Error))
137
+ return false;
138
+ const msg = error.message.toLowerCase();
139
+ // Playwright TimeoutError when waiting for a selector
140
+ if (error.name === 'TimeoutError')
141
+ return true;
142
+ // Strict mode violation (multiple elements matched)
143
+ if (msg.includes('strict mode violation'))
144
+ return true;
145
+ // Element not found patterns
146
+ if (msg.includes('waiting for selector') || msg.includes('waiting for locator'))
147
+ return true;
148
+ if (msg.includes('no element matches selector'))
149
+ return true;
150
+ if (msg.includes('resolved to') && msg.includes('element'))
151
+ return true;
152
+ return false;
153
+ }
154
+ // ─── Locator Proxy ───────────────────────────────────────────────────────────
155
+ /**
156
+ * Wraps a Playwright Locator so that any intercepted action automatically
157
+ * triggers the healing pipeline when the original locator fails.
158
+ */
159
+ function createHealingLocatorProxy(originalLocator, locatorInfo, page, healer, session, config) {
160
+ const handler = {
161
+ get(target, prop, receiver) {
162
+ const value = Reflect.get(target, prop, receiver);
163
+ // Only intercept known action methods.
164
+ if (typeof prop !== 'string' || !INTERCEPTED_ACTIONS.has(prop)) {
165
+ // Special-case: the .locator() method on a Locator returns a chained
166
+ // locator. We need to wrap it so healing propagates.
167
+ if (prop === 'locator' && typeof value === 'function') {
168
+ return function chainedLocator(...args) {
169
+ const childLocator = value.apply(target, args);
170
+ const childSelector = typeof args[0] === 'string' ? args[0] : String(args[0]);
171
+ const childInfo = {
172
+ type: 'css',
173
+ selector: childSelector,
174
+ playwrightExpression: `${locatorInfo.playwrightExpression}.locator('${childSelector}')`,
175
+ };
176
+ return createHealingLocatorProxy(childLocator, childInfo, page, healer, session, config);
177
+ };
178
+ }
179
+ // Bind functions so they keep the correct `this`.
180
+ if (typeof value === 'function') {
181
+ return value.bind(target);
182
+ }
183
+ return value;
184
+ }
185
+ // Return a wrapper function for the intercepted action.
186
+ return async function interceptedAction(...args) {
187
+ const actionName = prop;
188
+ try {
189
+ // Happy path: try the original locator action.
190
+ return await value.apply(target, args);
191
+ }
192
+ catch (originalError) {
193
+ // Only attempt healing for locator-resolution failures.
194
+ if (!isHealableError(originalError) || !config.healing.enabled) {
195
+ throw originalError;
196
+ }
197
+ logger.info(`Action "${actionName}" failed on "${locatorInfo.playwrightExpression}". Attempting healing...`);
198
+ const sourceLocation = extractSourceLocation(originalError);
199
+ let healingResult;
200
+ try {
201
+ healingResult = await healer.heal(page, locatorInfo, actionName, originalError);
202
+ }
203
+ catch (healError) {
204
+ const msg = healError instanceof Error ? healError.message : String(healError);
205
+ logger.error(`Healing itself threw an error: ${msg}`);
206
+ // Record the failure and re-throw the ORIGINAL error.
207
+ recordHealingEvent(session, page, locatorInfo, null, actionName, sourceLocation, false);
208
+ throw originalError;
209
+ }
210
+ if (!healingResult.success || !healingResult.healedLocator) {
211
+ logger.warn(`Healing failed for "${locatorInfo.playwrightExpression}". Re-throwing original error.`);
212
+ recordHealingEvent(session, page, locatorInfo, healingResult, actionName, sourceLocation, false);
213
+ throw originalError;
214
+ }
215
+ // Healing succeeded -- record the event and retry the action with the
216
+ // healed locator.
217
+ logger.info(`Retrying "${actionName}" with healed locator: ${healingResult.healedLocator.playwrightExpression}`);
218
+ recordHealingEvent(session, page, locatorInfo, healingResult, actionName, sourceLocation, true);
219
+ try {
220
+ const healedPwLocator = resolveLocator(page, healingResult.healedLocator);
221
+ return await healedPwLocator[actionName](...args);
222
+ }
223
+ catch (retryError) {
224
+ logger.error(`Retry with healed locator also failed: ${retryError.message}`);
225
+ // Throw the ORIGINAL error so the test's error message stays sensible.
226
+ throw originalError;
227
+ }
228
+ }
229
+ };
230
+ },
231
+ };
232
+ return new Proxy(originalLocator, handler);
233
+ }
234
+ // ─── Locator resolution ──────────────────────────────────────────────────────
235
+ /**
236
+ * Converts a LocatorInfo back into a live Playwright Locator on the given page.
237
+ */
238
+ function resolveLocator(page, info) {
239
+ switch (info.type) {
240
+ case 'role': {
241
+ const name = info.options?.['name'];
242
+ const opts = name ? { name } : undefined;
243
+ return page.getByRole(info.selector, opts);
244
+ }
245
+ case 'text':
246
+ return page.getByText(info.selector);
247
+ case 'testid':
248
+ return page.getByTestId(info.selector);
249
+ case 'label':
250
+ return page.getByLabel(info.selector);
251
+ case 'placeholder':
252
+ return page.getByPlaceholder(info.selector);
253
+ case 'alttext':
254
+ return page.getByAltText(info.selector);
255
+ case 'title':
256
+ return page.getByTitle(info.selector);
257
+ case 'css':
258
+ case 'xpath':
259
+ default:
260
+ return page.locator(info.selector);
261
+ }
262
+ }
263
+ // ─── Healing event recording ─────────────────────────────────────────────────
264
+ function recordHealingEvent(session, page, originalLocator, healingResult, action, sourceLocation, success) {
265
+ let pageUrl = 'unknown';
266
+ try {
267
+ pageUrl = page.url();
268
+ }
269
+ catch {
270
+ // Page may have been closed.
271
+ }
272
+ const event = {
273
+ id: generateEventId(),
274
+ timestamp: Date.now(),
275
+ testTitle: '',
276
+ testFile: sourceLocation?.filePath ?? '',
277
+ pageUrl,
278
+ action,
279
+ originalLocator,
280
+ healedLocator: healingResult?.healedLocator ?? null,
281
+ strategy: healingResult?.strategy ?? null,
282
+ confidence: healingResult?.confidence ?? 0,
283
+ reasoning: healingResult?.reasoning ?? 'Healing was not attempted or failed.',
284
+ duration: healingResult?.duration ?? 0,
285
+ sourceLocation,
286
+ status: success ? 'healed' : 'failed',
287
+ reviewStatus: 'pending',
288
+ };
289
+ session.events.push(event);
290
+ }
291
+ // ─── Frame Proxy ────────────────────────────────────────────────────────────
292
+ /**
293
+ * Wraps a Playwright Frame so that locator-creation methods on the frame
294
+ * also go through the healing pipeline.
295
+ */
296
+ function createHealingFrameProxy(frame, page, healer, session, config) {
297
+ const handler = {
298
+ get(target, prop, receiver) {
299
+ const value = Reflect.get(target, prop, receiver);
300
+ if (typeof prop !== 'string') {
301
+ if (typeof value === 'function') {
302
+ return value.bind(target);
303
+ }
304
+ return value;
305
+ }
306
+ // Intercept locator-creation methods on the Frame.
307
+ if (PAGE_LOCATOR_METHODS.includes(prop)) {
308
+ return function interceptedFrameLocatorCreation(...args) {
309
+ const method = prop;
310
+ const originalLocator = value.apply(target, args);
311
+ if (!config.healing.enabled) {
312
+ return originalLocator;
313
+ }
314
+ const locatorInfo = buildLocatorInfo(method, args);
315
+ // Prefix expression with frame context
316
+ locatorInfo.playwrightExpression = `frame.${locatorInfo.playwrightExpression.replace('page.', '')}`;
317
+ return createHealingLocatorProxy(originalLocator, locatorInfo, page, healer, session, config);
318
+ };
319
+ }
320
+ // Intercept frameLocator on Frame (nested frames)
321
+ if (prop === 'frameLocator' && typeof value === 'function') {
322
+ return function interceptedNestedFrameLocator(...args) {
323
+ const originalFrameLocator = value.apply(target, args);
324
+ return createHealingFrameLocatorProxy(originalFrameLocator, page, healer, session, config);
325
+ };
326
+ }
327
+ if (typeof value === 'function') {
328
+ return value.bind(target);
329
+ }
330
+ return value;
331
+ },
332
+ };
333
+ return new Proxy(frame, handler);
334
+ }
335
+ // ─── FrameLocator Proxy ─────────────────────────────────────────────────────
336
+ /**
337
+ * Wraps a Playwright FrameLocator so that locator-creation methods on it
338
+ * produce healing-enabled locators. FrameLocator is a builder — it returns
339
+ * Locators that resolve inside the iframe.
340
+ */
341
+ function createHealingFrameLocatorProxy(frameLocator, page, healer, session, config) {
342
+ const handler = {
343
+ get(target, prop, receiver) {
344
+ const value = Reflect.get(target, prop, receiver);
345
+ if (typeof prop !== 'string') {
346
+ if (typeof value === 'function') {
347
+ return value.bind(target);
348
+ }
349
+ return value;
350
+ }
351
+ // Intercept locator-creation methods on the FrameLocator.
352
+ if (PAGE_LOCATOR_METHODS.includes(prop)) {
353
+ return function interceptedFrameLocatorCreation(...args) {
354
+ const method = prop;
355
+ const originalLocator = value.apply(target, args);
356
+ if (!config.healing.enabled) {
357
+ return originalLocator;
358
+ }
359
+ const locatorInfo = buildLocatorInfo(method, args);
360
+ locatorInfo.playwrightExpression = `frameLocator.${locatorInfo.playwrightExpression.replace('page.', '')}`;
361
+ return createHealingLocatorProxy(originalLocator, locatorInfo, page, healer, session, config);
362
+ };
363
+ }
364
+ // Nested frameLocator (e.g., page.frameLocator('#outer').frameLocator('#inner'))
365
+ if (prop === 'frameLocator' && typeof value === 'function') {
366
+ return function nestedFrameLocator(...args) {
367
+ const nested = value.apply(target, args);
368
+ return createHealingFrameLocatorProxy(nested, page, healer, session, config);
369
+ };
370
+ }
371
+ if (typeof value === 'function') {
372
+ return value.bind(target);
373
+ }
374
+ return value;
375
+ },
376
+ };
377
+ return new Proxy(frameLocator, handler);
378
+ }
379
+ // ─── Page Proxy ──────────────────────────────────────────────────────────────
380
+ /**
381
+ * Wraps a Playwright Page in a Proxy that intercepts locator-creation methods
382
+ * so every returned Locator is automatically wrapped with healing logic.
383
+ * Also intercepts frame() and frameLocator() to propagate healing into iframes.
384
+ */
385
+ function createHealingPageProxy(page, healer, session, config) {
386
+ // Attach the session to the page for reporter access.
387
+ page['_mindHealSession'] = session;
388
+ const handler = {
389
+ get(target, prop, receiver) {
390
+ const value = Reflect.get(target, prop, receiver);
391
+ if (typeof prop !== 'string') {
392
+ if (typeof value === 'function') {
393
+ return value.bind(target);
394
+ }
395
+ return value;
396
+ }
397
+ // Intercept locator-creation methods.
398
+ if (PAGE_LOCATOR_METHODS.includes(prop)) {
399
+ return function interceptedLocatorCreation(...args) {
400
+ const method = prop;
401
+ const originalLocator = value.apply(target, args);
402
+ if (!config.healing.enabled) {
403
+ return originalLocator;
404
+ }
405
+ const locatorInfo = buildLocatorInfo(method, args);
406
+ return createHealingLocatorProxy(originalLocator, locatorInfo, target, healer, session, config);
407
+ };
408
+ }
409
+ // Intercept page.frame() — returns a Frame or null.
410
+ if (prop === 'frame' && typeof value === 'function') {
411
+ return function interceptedFrame(...args) {
412
+ const frame = value.apply(target, args);
413
+ if (!frame || !config.healing.enabled)
414
+ return frame;
415
+ return createHealingFrameProxy(frame, target, healer, session, config);
416
+ };
417
+ }
418
+ // Intercept page.frameLocator() — returns a FrameLocator.
419
+ if (prop === 'frameLocator' && typeof value === 'function') {
420
+ return function interceptedFrameLocator(...args) {
421
+ const frameLocator = value.apply(target, args);
422
+ if (!config.healing.enabled)
423
+ return frameLocator;
424
+ return createHealingFrameLocatorProxy(frameLocator, target, healer, session, config);
425
+ };
426
+ }
427
+ // Intercept page.frames() — returns Frame[].
428
+ if (prop === 'frames' && typeof value === 'function') {
429
+ return function interceptedFrames(...args) {
430
+ const frames = value.apply(target, args);
431
+ if (!config.healing.enabled)
432
+ return frames;
433
+ return frames.map((f) => createHealingFrameProxy(f, target, healer, session, config));
434
+ };
435
+ }
436
+ // Intercept page.mainFrame() — returns Frame.
437
+ if (prop === 'mainFrame' && typeof value === 'function') {
438
+ return function interceptedMainFrame(...args) {
439
+ const frame = value.apply(target, args);
440
+ if (!config.healing.enabled)
441
+ return frame;
442
+ return createHealingFrameProxy(frame, target, healer, session, config);
443
+ };
444
+ }
445
+ // Bind all other functions so they keep correct `this`.
446
+ if (typeof value === 'function') {
447
+ return value.bind(target);
448
+ }
449
+ return value;
450
+ },
451
+ };
452
+ return new Proxy(page, handler);
453
+ }
454
+ // ─── Dialog handling ────────────────────────────────────────────────────────
455
+ /**
456
+ * Default dialog handling configuration.
457
+ */
458
+ const DEFAULT_DIALOG_CONFIG = {
459
+ dismissAlerts: true,
460
+ acceptConfirms: true,
461
+ promptResponse: '',
462
+ logDialogs: true,
463
+ };
464
+ /**
465
+ * Resolves the dialog handling config from the healing config.
466
+ */
467
+ function resolveDialogConfig(config) {
468
+ const handleDialogs = config.healing.handleDialogs;
469
+ if (handleDialogs === false)
470
+ return null;
471
+ if (handleDialogs === true)
472
+ return DEFAULT_DIALOG_CONFIG;
473
+ if (typeof handleDialogs === 'object') {
474
+ return { ...DEFAULT_DIALOG_CONFIG, ...handleDialogs };
475
+ }
476
+ return DEFAULT_DIALOG_CONFIG;
477
+ }
478
+ /**
479
+ * Installs an automatic dialog handler on the page that handles
480
+ * alert(), confirm(), and prompt() browser dialogs so they don't
481
+ * block test execution.
482
+ *
483
+ * Returns a cleanup function to remove the listener.
484
+ */
485
+ function installDialogHandler(page, dialogConfig, session) {
486
+ const handler = async (dialog) => {
487
+ const dialogType = dialog.type(); // 'alert' | 'confirm' | 'prompt' | 'beforeunload'
488
+ const message = dialog.message();
489
+ if (dialogConfig.logDialogs) {
490
+ logger.info(`[MindHeal] Browser ${dialogType} dialog intercepted: "${message.substring(0, 200)}"`);
491
+ }
492
+ // Record as a session event for reporting
493
+ const event = {
494
+ id: generateEventId(),
495
+ timestamp: Date.now(),
496
+ testTitle: '',
497
+ testFile: '',
498
+ pageUrl: safeGetUrl(page),
499
+ action: `dialog:${dialogType}`,
500
+ originalLocator: {
501
+ type: 'css',
502
+ selector: `[dialog:${dialogType}]`,
503
+ playwrightExpression: `page.on('dialog') // ${dialogType}: "${message.substring(0, 100)}"`,
504
+ },
505
+ healedLocator: {
506
+ type: 'css',
507
+ selector: `[dialog:${dialogType}:auto-handled]`,
508
+ playwrightExpression: dialogType === 'alert'
509
+ ? 'dialog.dismiss()'
510
+ : dialogType === 'confirm'
511
+ ? 'dialog.accept()'
512
+ : `dialog.accept('${dialogConfig.promptResponse}')`,
513
+ },
514
+ strategy: null,
515
+ confidence: 1.0,
516
+ reasoning: `Auto-handled browser ${dialogType} dialog: "${message.substring(0, 100)}"`,
517
+ duration: 0,
518
+ sourceLocation: null,
519
+ status: 'healed',
520
+ reviewStatus: 'approved',
521
+ };
522
+ session.events.push(event);
523
+ try {
524
+ switch (dialogType) {
525
+ case 'alert':
526
+ if (dialogConfig.dismissAlerts) {
527
+ await dialog.dismiss();
528
+ }
529
+ else {
530
+ await dialog.accept();
531
+ }
532
+ break;
533
+ case 'confirm':
534
+ if (dialogConfig.acceptConfirms) {
535
+ await dialog.accept();
536
+ }
537
+ else {
538
+ await dialog.dismiss();
539
+ }
540
+ break;
541
+ case 'prompt':
542
+ await dialog.accept(dialogConfig.promptResponse);
543
+ break;
544
+ case 'beforeunload':
545
+ await dialog.accept();
546
+ break;
547
+ default:
548
+ await dialog.dismiss();
549
+ break;
550
+ }
551
+ }
552
+ catch (error) {
553
+ const msg = error instanceof Error ? error.message : String(error);
554
+ logger.warn(`[MindHeal] Failed to handle ${dialogType} dialog: ${msg}`);
555
+ }
556
+ };
557
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
558
+ page.on('dialog', handler);
559
+ return () => {
560
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
561
+ page.off('dialog', handler);
562
+ };
563
+ }
564
+ function safeGetUrl(page) {
565
+ try {
566
+ return page.url();
567
+ }
568
+ catch {
569
+ return 'unknown';
570
+ }
571
+ }
572
+ // ─── Fixture factory ─────────────────────────────────────────────────────────
573
+ /**
574
+ * Creates a Playwright fixture that wraps the built-in `page` fixture with the
575
+ * MindHeal healing proxy. Tests require zero changes; failures are healed
576
+ * transparently.
577
+ *
578
+ * Usage:
579
+ * ```ts
580
+ * import { createMindHealFixture } from 'mindheal';
581
+ * const test = createMindHealFixture(myConfig);
582
+ * ```
583
+ */
584
+ function createMindHealFixture(config) {
585
+ // Shared, long-lived objects -- one per worker.
586
+ let cache = null;
587
+ let healer = null;
588
+ return test.extend({
589
+ // Worker-scoped fixture: one healing session per worker.
590
+ _mindHealSession: [
591
+ async ({}, use) => {
592
+ const sessionId = `session_${Date.now().toString(36)}_${Math.random().toString(36).substring(2, 8)}`;
593
+ const session = {
594
+ id: sessionId,
595
+ startTime: Date.now(),
596
+ events: [],
597
+ config,
598
+ };
599
+ _sessions.set(sessionId, session);
600
+ // Initialise cache and healer once per worker.
601
+ cache = new SelfHealCache(config.healing.cachePath);
602
+ cache.load();
603
+ let aiProvider = null;
604
+ if (config.ai.apiKey && config.healing.strategies.includes('ai')) {
605
+ try {
606
+ aiProvider = createAIProvider(config.ai);
607
+ }
608
+ catch (err) {
609
+ const msg = err instanceof Error ? err.message : String(err);
610
+ logger.warn(`Failed to create AI provider: ${msg}. AI strategy will be skipped.`);
611
+ }
612
+ }
613
+ // Initialize RAG context retriever if enabled.
614
+ let contextRetriever = null;
615
+ if (config.rag?.enabled) {
616
+ try {
617
+ const knowledgeStore = new KnowledgeStore(config.rag.storePath);
618
+ knowledgeStore.load();
619
+ contextRetriever = new ContextRetriever(config.rag, knowledgeStore);
620
+ logger.debug(`RAG context retriever initialized (${knowledgeStore.size} entries)`);
621
+ }
622
+ catch (err) {
623
+ const msg = err instanceof Error ? err.message : String(err);
624
+ logger.warn(`Failed to initialize RAG: ${msg}. Continuing without RAG.`);
625
+ }
626
+ }
627
+ // Initialize smart retry, visual verifier, and analytics.
628
+ let smartRetry = null;
629
+ if (config.smartRetry?.enabled) {
630
+ smartRetry = new SmartRetry(config.smartRetry);
631
+ smartRetry.load();
632
+ }
633
+ let visualVerifier = null;
634
+ if (config.visualVerification?.enabled) {
635
+ visualVerifier = new VisualVerifier(config.visualVerification);
636
+ }
637
+ let analytics = null;
638
+ if (config.analytics?.enabled) {
639
+ analytics = new HealingAnalytics(config.analytics);
640
+ analytics.load();
641
+ }
642
+ healer = new Healer(config, aiProvider, cache, contextRetriever, smartRetry, visualVerifier, analytics);
643
+ // Stash references for post-healing operations.
644
+ session['_contextRetriever'] = contextRetriever;
645
+ session['_analytics'] = analytics;
646
+ session['_smartRetry'] = smartRetry;
647
+ await use(session);
648
+ // Teardown: ingest healing events into RAG store and analytics, then save.
649
+ for (const event of session.events) {
650
+ if (contextRetriever)
651
+ contextRetriever.ingestHealingEvent(event);
652
+ if (analytics)
653
+ analytics.recordEvent(event);
654
+ if (smartRetry) {
655
+ smartRetry.recordAttempt(event.testFile, event.testTitle, event.originalLocator, event.status === 'healed');
656
+ }
657
+ }
658
+ // Parallel-safe saves with file locking
659
+ const fileLock = config.parallel?.enabled ? new FileLock(config.parallel) : null;
660
+ if (analytics) {
661
+ if (fileLock) {
662
+ await fileLock.withLock(config.analytics.storePath, () => analytics.save());
663
+ }
664
+ else {
665
+ analytics.save();
666
+ }
667
+ }
668
+ if (smartRetry) {
669
+ if (fileLock) {
670
+ await fileLock.withLock(config.smartRetry.flakyStorePath, () => smartRetry.save());
671
+ }
672
+ else {
673
+ smartRetry.save();
674
+ }
675
+ }
676
+ if (visualVerifier) {
677
+ visualVerifier.cleanup();
678
+ }
679
+ const healed = session.events.filter((e) => e.status === 'healed').length;
680
+ const failed = session.events.filter((e) => e.status === 'failed').length;
681
+ if (session.events.length > 0) {
682
+ logger.info(`Session ${sessionId} complete: ${healed} healed, ${failed} failed out of ${session.events.length} total healing attempts.`);
683
+ }
684
+ },
685
+ { scope: 'worker' },
686
+ ],
687
+ // Test-scoped page fixture override.
688
+ page: async ({ page, _mindHealSession }, use, testInfo) => {
689
+ if (!config.healing.enabled || !healer) {
690
+ await use(page);
691
+ return;
692
+ }
693
+ // Check exclude patterns against the test file path.
694
+ const excludePatterns = config.healing.excludePatterns ?? [];
695
+ const testFilePath = testInfo.file ?? '';
696
+ const excluded = excludePatterns.some((pattern) => {
697
+ try {
698
+ return new RegExp(pattern).test(testFilePath);
699
+ }
700
+ catch {
701
+ return testFilePath.includes(pattern);
702
+ }
703
+ });
704
+ if (excluded) {
705
+ logger.debug(`Test file excluded from healing: ${testFilePath}`);
706
+ await use(page);
707
+ return;
708
+ }
709
+ // Install dialog auto-handler for alert/confirm/prompt.
710
+ let removeDialogHandler = null;
711
+ const dialogConfig = resolveDialogConfig(config);
712
+ if (dialogConfig) {
713
+ removeDialogHandler = installDialogHandler(page, dialogConfig, _mindHealSession);
714
+ }
715
+ const proxiedPage = createHealingPageProxy(page, healer, _mindHealSession, config);
716
+ await use(proxiedPage);
717
+ // Cleanup dialog handler.
718
+ if (removeDialogHandler) {
719
+ removeDialogHandler();
720
+ }
721
+ },
722
+ });
723
+ }
724
+ // ─── High-level config helper ────────────────────────────────────────────────
725
+ /**
726
+ * Convenience function that returns a partial Playwright config object
727
+ * pre-configured with the MindHeal fixture and reporter.
728
+ *
729
+ * Usage in `playwright.config.ts`:
730
+ * ```ts
731
+ * import { mindHealConfig } from 'mindheal';
732
+ * import { defineConfig } from '@playwright/test';
733
+ *
734
+ * export default defineConfig({
735
+ * ...mindHealConfig({ ai: { provider: 'anthropic', apiKey: '...' } }),
736
+ * });
737
+ * ```
738
+ */
739
+ function mindHealConfig(userConfig = {}) {
740
+ const resolvedConfig = loadConfig(userConfig);
741
+ const mindHealTest = createMindHealFixture(resolvedConfig);
742
+ // Store the resolved config globally so the auto-fixture can access it.
743
+ _globalConfig = resolvedConfig;
744
+ return {
745
+ // The test instance with the fixture applied. Consumers can import `test`
746
+ // from their config or re-export it.
747
+ test: mindHealTest,
748
+ // Provide the projects entry that uses our fixture.
749
+ use: {},
750
+ // Reporter setup -- users can merge this with their own reporters.
751
+ reporter: [
752
+ ['list'],
753
+ ...(resolvedConfig.reporting.generateJSON
754
+ ? [
755
+ [
756
+ 'json',
757
+ {
758
+ outputFile: `${resolvedConfig.reporting.outputDir ?? '.mindheal/reports'}/results.json`,
759
+ },
760
+ ],
761
+ ]
762
+ : []),
763
+ ],
764
+ // Pass config through metadata so custom reporters can access it.
765
+ metadata: {
766
+ mindHealConfig: resolvedConfig,
767
+ },
768
+ };
769
+ }
770
+ // ─── Global config for auto-fixture ──────────────────────────────────────────
771
+ let _globalConfig = null;
772
+ /**
773
+ * Pre-built `test` fixture that reads config from `mindHealConfig()` call in
774
+ * `playwright.config.ts`. This enables zero-change imports:
775
+ *
776
+ * ```ts
777
+ * // playwright.config.ts
778
+ * import { mindHealConfig } from 'mindheal';
779
+ * export default defineConfig({ ...mindHealConfig({ ai: { ... } }) });
780
+ *
781
+ * // Any test file — no changes needed if you use this import:
782
+ * import { test, expect } from 'mindheal';
783
+ * ```
784
+ *
785
+ * If `mindHealConfig()` has not been called yet (e.g. the config file hasn't
786
+ * loaded), this falls back to the standard `@playwright/test` `test` with no
787
+ * healing — tests still run, they just won't self-heal.
788
+ */
789
+ const autoTest = (() => {
790
+ // Lazy: we can't call createMindHealFixture at module-load time because
791
+ // the config may not exist yet. Instead, extend baseTest with a page
792
+ // fixture that delegates to the global config at runtime.
793
+ return test.extend({
794
+ _mindHealSession: [
795
+ async ({}, use) => {
796
+ const config = _globalConfig;
797
+ if (!config) {
798
+ // No config — run without healing.
799
+ const emptySession = {
800
+ id: 'noop',
801
+ startTime: Date.now(),
802
+ events: [],
803
+ config: null,
804
+ };
805
+ await use(emptySession);
806
+ return;
807
+ }
808
+ const sessionId = `session_${Date.now().toString(36)}_${Math.random().toString(36).substring(2, 8)}`;
809
+ const session = {
810
+ id: sessionId,
811
+ startTime: Date.now(),
812
+ events: [],
813
+ config,
814
+ };
815
+ _sessions.set(sessionId, session);
816
+ // Initialise cache and healer once per worker.
817
+ const cache = new SelfHealCache(config.healing.cachePath);
818
+ cache.load();
819
+ let aiProvider = null;
820
+ if (config.ai.apiKey && config.healing.strategies.includes('ai')) {
821
+ try {
822
+ aiProvider = createAIProvider(config.ai);
823
+ }
824
+ catch (err) {
825
+ const msg = err instanceof Error ? err.message : String(err);
826
+ logger.warn(`Failed to create AI provider: ${msg}. AI strategy will be skipped.`);
827
+ }
828
+ }
829
+ // Initialize RAG context retriever if enabled.
830
+ let contextRetriever = null;
831
+ if (config.rag?.enabled) {
832
+ try {
833
+ const knowledgeStore = new KnowledgeStore(config.rag.storePath);
834
+ knowledgeStore.load();
835
+ contextRetriever = new ContextRetriever(config.rag, knowledgeStore);
836
+ logger.debug(`RAG context retriever initialized (${knowledgeStore.size} entries)`);
837
+ }
838
+ catch (err) {
839
+ const msg = err instanceof Error ? err.message : String(err);
840
+ logger.warn(`Failed to initialize RAG: ${msg}. Continuing without RAG.`);
841
+ }
842
+ }
843
+ // Initialize smart retry, visual verifier, and analytics.
844
+ let smartRetry = null;
845
+ if (config.smartRetry?.enabled) {
846
+ smartRetry = new SmartRetry(config.smartRetry);
847
+ smartRetry.load();
848
+ }
849
+ let visualVerifier = null;
850
+ if (config.visualVerification?.enabled) {
851
+ visualVerifier = new VisualVerifier(config.visualVerification);
852
+ }
853
+ let analytics = null;
854
+ if (config.analytics?.enabled) {
855
+ analytics = new HealingAnalytics(config.analytics);
856
+ analytics.load();
857
+ }
858
+ // Stash healer on the session so the page fixture can use it.
859
+ session['_healer'] = new Healer(config, aiProvider, cache, contextRetriever, smartRetry, visualVerifier, analytics);
860
+ session['_contextRetriever'] = contextRetriever;
861
+ session['_analytics'] = analytics;
862
+ session['_smartRetry'] = smartRetry;
863
+ await use(session);
864
+ // Teardown: ingest events and save all stores with file locking.
865
+ for (const event of session.events) {
866
+ if (contextRetriever)
867
+ contextRetriever.ingestHealingEvent(event);
868
+ if (analytics)
869
+ analytics.recordEvent(event);
870
+ if (smartRetry) {
871
+ smartRetry.recordAttempt(event.testFile, event.testTitle, event.originalLocator, event.status === 'healed');
872
+ }
873
+ }
874
+ const fileLock = config.parallel?.enabled ? new FileLock(config.parallel) : null;
875
+ if (analytics) {
876
+ if (fileLock) {
877
+ await fileLock.withLock(config.analytics.storePath, () => analytics.save());
878
+ }
879
+ else {
880
+ analytics.save();
881
+ }
882
+ }
883
+ if (smartRetry) {
884
+ if (fileLock) {
885
+ await fileLock.withLock(config.smartRetry.flakyStorePath, () => smartRetry.save());
886
+ }
887
+ else {
888
+ smartRetry.save();
889
+ }
890
+ }
891
+ if (visualVerifier)
892
+ visualVerifier.cleanup();
893
+ const healed = session.events.filter((e) => e.status === 'healed').length;
894
+ const failed = session.events.filter((e) => e.status === 'failed').length;
895
+ if (session.events.length > 0) {
896
+ logger.info(`Session ${sessionId} complete: ${healed} healed, ${failed} failed out of ${session.events.length} total healing attempts.`);
897
+ }
898
+ },
899
+ { scope: 'worker' },
900
+ ],
901
+ page: async ({ page, _mindHealSession }, use, testInfo) => {
902
+ const config = _globalConfig;
903
+ const healer = _mindHealSession['_healer'];
904
+ if (!config || !config.healing.enabled || !healer) {
905
+ await use(page);
906
+ return;
907
+ }
908
+ // Check exclude patterns.
909
+ const excludePatterns = config.healing.excludePatterns ?? [];
910
+ const testFilePath = testInfo.file ?? '';
911
+ const excluded = excludePatterns.some((pattern) => {
912
+ try {
913
+ return new RegExp(pattern).test(testFilePath);
914
+ }
915
+ catch {
916
+ return testFilePath.includes(pattern);
917
+ }
918
+ });
919
+ if (excluded) {
920
+ logger.debug(`Test file excluded from healing: ${testFilePath}`);
921
+ await use(page);
922
+ return;
923
+ }
924
+ // Install dialog auto-handler.
925
+ let removeDialogHandler = null;
926
+ const dialogConfig = resolveDialogConfig(config);
927
+ if (dialogConfig) {
928
+ removeDialogHandler = installDialogHandler(page, dialogConfig, _mindHealSession);
929
+ }
930
+ const proxiedPage = createHealingPageProxy(page, healer, _mindHealSession, config);
931
+ await use(proxiedPage);
932
+ if (removeDialogHandler) {
933
+ removeDialogHandler();
934
+ }
935
+ },
936
+ });
937
+ })();
938
+
939
+ export { autoTest, createMindHealFixture, getAllHealingSessions, mindHealConfig };
940
+ //# sourceMappingURL=interceptor.js.map