@yasserkhanorg/e2e-agents 1.8.5 → 1.10.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 (274) hide show
  1. package/README.md +95 -8
  2. package/dist/adapters/cypress.d.ts +10 -0
  3. package/dist/adapters/cypress.d.ts.map +1 -0
  4. package/dist/adapters/cypress.js +86 -0
  5. package/dist/adapters/framework_adapter.d.ts +41 -0
  6. package/dist/adapters/framework_adapter.d.ts.map +1 -0
  7. package/dist/adapters/framework_adapter.js +152 -0
  8. package/dist/adapters/playwright.d.ts +10 -0
  9. package/dist/adapters/playwright.d.ts.map +1 -0
  10. package/dist/adapters/playwright.js +86 -0
  11. package/dist/adapters/pytest.d.ts +10 -0
  12. package/dist/adapters/pytest.d.ts.map +1 -0
  13. package/dist/adapters/pytest.js +96 -0
  14. package/dist/adapters/supertest.d.ts +12 -0
  15. package/dist/adapters/supertest.d.ts.map +1 -0
  16. package/dist/adapters/supertest.js +85 -0
  17. package/dist/agent/config.d.ts +1 -1
  18. package/dist/agent/config.d.ts.map +1 -1
  19. package/dist/agent/git.d.ts +1 -0
  20. package/dist/agent/git.d.ts.map +1 -1
  21. package/dist/agent/git.js +3 -0
  22. package/dist/agentic/fix_loop.d.ts.map +1 -1
  23. package/dist/agentic/fix_loop.js +5 -4
  24. package/dist/agentic/runner.d.ts +2 -0
  25. package/dist/agentic/runner.d.ts.map +1 -1
  26. package/dist/agentic/runner.js +15 -12
  27. package/dist/agents/cross-impact.d.ts.map +1 -1
  28. package/dist/agents/cross-impact.js +6 -1
  29. package/dist/agents/executor.d.ts.map +1 -1
  30. package/dist/agents/executor.js +6 -1
  31. package/dist/agents/strategist.d.ts.map +1 -1
  32. package/dist/agents/strategist.js +6 -1
  33. package/dist/agents/test-designer.d.ts.map +1 -1
  34. package/dist/agents/test-designer.js +6 -1
  35. package/dist/anthropic_provider.d.ts.map +1 -1
  36. package/dist/anthropic_provider.js +1 -0
  37. package/dist/base_provider.d.ts +56 -0
  38. package/dist/base_provider.d.ts.map +1 -1
  39. package/dist/base_provider.js +123 -1
  40. package/dist/budget_ledger.d.ts +28 -0
  41. package/dist/budget_ledger.d.ts.map +1 -0
  42. package/dist/budget_ledger.js +62 -0
  43. package/dist/cache/cached_provider.d.ts +45 -0
  44. package/dist/cache/cached_provider.d.ts.map +1 -0
  45. package/dist/cache/cached_provider.js +88 -0
  46. package/dist/cache/response_cache.d.ts +79 -0
  47. package/dist/cache/response_cache.d.ts.map +1 -0
  48. package/dist/cache/response_cache.js +177 -0
  49. package/dist/cli/commands/bootstrap.d.ts +3 -0
  50. package/dist/cli/commands/bootstrap.d.ts.map +1 -0
  51. package/dist/cli/commands/bootstrap.js +109 -0
  52. package/dist/cli/commands/cost_report.d.ts +3 -0
  53. package/dist/cli/commands/cost_report.d.ts.map +1 -0
  54. package/dist/cli/commands/cost_report.js +115 -0
  55. package/dist/cli/commands/crew.d.ts.map +1 -1
  56. package/dist/cli/commands/crew.js +118 -1
  57. package/dist/cli/commands/gate.d.ts +3 -0
  58. package/dist/cli/commands/gate.d.ts.map +1 -0
  59. package/dist/cli/commands/gate.js +86 -0
  60. package/dist/cli/commands/init.d.ts.map +1 -1
  61. package/dist/cli/commands/init.js +7 -62
  62. package/dist/cli/commands/train.d.ts.map +1 -1
  63. package/dist/cli/commands/train.js +16 -21
  64. package/dist/cli/defaults.d.ts +35 -0
  65. package/dist/cli/defaults.d.ts.map +1 -0
  66. package/dist/cli/defaults.js +125 -0
  67. package/dist/cli/errors.d.ts +27 -0
  68. package/dist/cli/errors.d.ts.map +1 -0
  69. package/dist/cli/errors.js +57 -0
  70. package/dist/cli/parse_args.d.ts.map +1 -1
  71. package/dist/cli/parse_args.js +24 -2
  72. package/dist/cli/types.d.ts +7 -1
  73. package/dist/cli/types.d.ts.map +1 -1
  74. package/dist/cli.js +47 -2
  75. package/dist/crew/context.d.ts +15 -0
  76. package/dist/crew/context.d.ts.map +1 -1
  77. package/dist/crew/orchestrator.d.ts +14 -0
  78. package/dist/crew/orchestrator.d.ts.map +1 -1
  79. package/dist/crew/orchestrator.js +162 -4
  80. package/dist/crew/protocol.d.ts +13 -0
  81. package/dist/crew/protocol.d.ts.map +1 -1
  82. package/dist/crew/provider.d.ts +15 -1
  83. package/dist/crew/provider.d.ts.map +1 -1
  84. package/dist/crew/provider.js +24 -4
  85. package/dist/custom_provider.d.ts.map +1 -1
  86. package/dist/custom_provider.js +1 -0
  87. package/dist/engine/diff_loader.d.ts.map +1 -1
  88. package/dist/engine/diff_loader.js +3 -14
  89. package/dist/engine/impact_engine.d.ts.map +1 -1
  90. package/dist/engine/impact_engine.js +9 -23
  91. package/dist/esm/adapters/cypress.js +49 -0
  92. package/dist/esm/adapters/framework_adapter.js +114 -0
  93. package/dist/esm/adapters/playwright.js +49 -0
  94. package/dist/esm/adapters/pytest.js +59 -0
  95. package/dist/esm/adapters/supertest.js +48 -0
  96. package/dist/esm/agent/git.js +3 -1
  97. package/dist/esm/agentic/fix_loop.js +5 -4
  98. package/dist/esm/agentic/runner.js +15 -12
  99. package/dist/esm/agents/cross-impact.js +6 -1
  100. package/dist/esm/agents/executor.js +6 -1
  101. package/dist/esm/agents/strategist.js +6 -1
  102. package/dist/esm/agents/test-designer.js +6 -1
  103. package/dist/esm/anthropic_provider.js +1 -0
  104. package/dist/esm/base_provider.js +121 -0
  105. package/dist/esm/budget_ledger.js +58 -0
  106. package/dist/esm/cache/cached_provider.js +82 -0
  107. package/dist/esm/cache/response_cache.js +140 -0
  108. package/dist/esm/cli/commands/bootstrap.js +106 -0
  109. package/dist/esm/cli/commands/cost_report.js +112 -0
  110. package/dist/esm/cli/commands/crew.js +118 -1
  111. package/dist/esm/cli/commands/gate.js +83 -0
  112. package/dist/esm/cli/commands/init.js +3 -58
  113. package/dist/esm/cli/commands/train.js +16 -21
  114. package/dist/esm/cli/defaults.js +118 -0
  115. package/dist/esm/cli/errors.js +52 -0
  116. package/dist/esm/cli/parse_args.js +24 -2
  117. package/dist/esm/cli.js +47 -2
  118. package/dist/esm/crew/orchestrator.js +162 -4
  119. package/dist/esm/crew/provider.js +24 -4
  120. package/dist/esm/custom_provider.js +1 -0
  121. package/dist/esm/engine/diff_loader.js +1 -12
  122. package/dist/esm/engine/impact_engine.js +9 -23
  123. package/dist/esm/index.js +21 -0
  124. package/dist/esm/knowledge/api_surface.js +265 -34
  125. package/dist/esm/knowledge/cluster_utils.js +60 -0
  126. package/dist/esm/knowledge/failure_history.js +121 -0
  127. package/dist/esm/knowledge/kg_bridge.js +381 -0
  128. package/dist/esm/knowledge/kg_types.js +3 -0
  129. package/dist/esm/knowledge/route_families.js +119 -0
  130. package/dist/esm/mcp-server.js +2 -4
  131. package/dist/esm/metrics/prometheus.js +149 -0
  132. package/dist/esm/model_router.js +59 -0
  133. package/dist/esm/ollama_provider.js +1 -0
  134. package/dist/esm/openai_provider.js +1 -0
  135. package/dist/esm/pipeline/orchestrator.js +6 -12
  136. package/dist/esm/pipeline/stage0_preprocess.js +12 -19
  137. package/dist/esm/pipeline/stage1_impact.js +19 -3
  138. package/dist/esm/pipeline/stage2_coverage.js +29 -7
  139. package/dist/esm/pipeline/stage3_generation.js +21 -1
  140. package/dist/esm/progress.js +112 -0
  141. package/dist/esm/prompts/coverage.js +17 -24
  142. package/dist/esm/prompts/cross-impact.js +3 -21
  143. package/dist/esm/prompts/generation.js +201 -45
  144. package/dist/esm/prompts/generation_profile.js +147 -0
  145. package/dist/esm/prompts/heal.js +33 -15
  146. package/dist/esm/prompts/impact.js +3 -22
  147. package/dist/esm/prompts/json_extract.js +36 -0
  148. package/dist/esm/prompts/strategist.js +2 -20
  149. package/dist/esm/prompts/test-designer.js +6 -21
  150. package/dist/esm/provider_factory.js +6 -4
  151. package/dist/esm/reporters/junit.js +86 -0
  152. package/dist/esm/reporters/reporter.js +3 -0
  153. package/dist/esm/reporters/sarif.js +131 -0
  154. package/dist/esm/resilience/circuit_breaker.js +78 -0
  155. package/dist/esm/resilience/retry.js +56 -0
  156. package/dist/esm/sanitize.js +66 -0
  157. package/dist/esm/training/kg_scanner.js +115 -0
  158. package/dist/esm/training/scanner.js +27 -34
  159. package/dist/esm/validation/guardrails.js +5 -0
  160. package/dist/esm/version.js +33 -0
  161. package/dist/index.d.ts +21 -1
  162. package/dist/index.d.ts.map +1 -1
  163. package/dist/index.js +45 -1
  164. package/dist/knowledge/api_surface.d.ts +12 -0
  165. package/dist/knowledge/api_surface.d.ts.map +1 -1
  166. package/dist/knowledge/api_surface.js +268 -34
  167. package/dist/knowledge/cluster_utils.d.ts +28 -0
  168. package/dist/knowledge/cluster_utils.d.ts.map +1 -0
  169. package/dist/knowledge/cluster_utils.js +67 -0
  170. package/dist/knowledge/failure_history.d.ts +39 -0
  171. package/dist/knowledge/failure_history.d.ts.map +1 -0
  172. package/dist/knowledge/failure_history.js +128 -0
  173. package/dist/knowledge/kg_bridge.d.ts +31 -0
  174. package/dist/knowledge/kg_bridge.d.ts.map +1 -0
  175. package/dist/knowledge/kg_bridge.js +388 -0
  176. package/dist/knowledge/kg_types.d.ts +75 -0
  177. package/dist/knowledge/kg_types.d.ts.map +1 -0
  178. package/dist/knowledge/kg_types.js +4 -0
  179. package/dist/knowledge/route_families.d.ts +29 -0
  180. package/dist/knowledge/route_families.d.ts.map +1 -1
  181. package/dist/knowledge/route_families.js +122 -0
  182. package/dist/mcp-server.d.ts.map +1 -1
  183. package/dist/mcp-server.js +2 -4
  184. package/dist/metrics/prometheus.d.ts +37 -0
  185. package/dist/metrics/prometheus.d.ts.map +1 -0
  186. package/dist/metrics/prometheus.js +153 -0
  187. package/dist/model_router.d.ts +28 -0
  188. package/dist/model_router.d.ts.map +1 -0
  189. package/dist/model_router.js +63 -0
  190. package/dist/ollama_provider.d.ts.map +1 -1
  191. package/dist/ollama_provider.js +1 -0
  192. package/dist/openai_provider.d.ts.map +1 -1
  193. package/dist/openai_provider.js +1 -0
  194. package/dist/pipeline/orchestrator.d.ts +2 -0
  195. package/dist/pipeline/orchestrator.d.ts.map +1 -1
  196. package/dist/pipeline/orchestrator.js +6 -12
  197. package/dist/pipeline/stage0_preprocess.d.ts.map +1 -1
  198. package/dist/pipeline/stage0_preprocess.js +11 -18
  199. package/dist/pipeline/stage1_impact.d.ts +1 -1
  200. package/dist/pipeline/stage1_impact.d.ts.map +1 -1
  201. package/dist/pipeline/stage1_impact.js +18 -2
  202. package/dist/pipeline/stage2_coverage.d.ts +2 -0
  203. package/dist/pipeline/stage2_coverage.d.ts.map +1 -1
  204. package/dist/pipeline/stage2_coverage.js +29 -7
  205. package/dist/pipeline/stage3_generation.d.ts +2 -0
  206. package/dist/pipeline/stage3_generation.d.ts.map +1 -1
  207. package/dist/pipeline/stage3_generation.js +21 -1
  208. package/dist/pipeline/stage4_heal.d.ts +2 -0
  209. package/dist/pipeline/stage4_heal.d.ts.map +1 -1
  210. package/dist/progress.d.ts +22 -0
  211. package/dist/progress.d.ts.map +1 -0
  212. package/dist/progress.js +116 -0
  213. package/dist/prompts/coverage.d.ts +2 -0
  214. package/dist/prompts/coverage.d.ts.map +1 -1
  215. package/dist/prompts/coverage.js +17 -24
  216. package/dist/prompts/cross-impact.d.ts +1 -0
  217. package/dist/prompts/cross-impact.d.ts.map +1 -1
  218. package/dist/prompts/cross-impact.js +3 -21
  219. package/dist/prompts/generation.d.ts +4 -2
  220. package/dist/prompts/generation.d.ts.map +1 -1
  221. package/dist/prompts/generation.js +201 -45
  222. package/dist/prompts/generation_profile.d.ts +29 -0
  223. package/dist/prompts/generation_profile.d.ts.map +1 -0
  224. package/dist/prompts/generation_profile.js +151 -0
  225. package/dist/prompts/heal.d.ts +3 -1
  226. package/dist/prompts/heal.d.ts.map +1 -1
  227. package/dist/prompts/heal.js +33 -15
  228. package/dist/prompts/impact.d.ts +1 -0
  229. package/dist/prompts/impact.d.ts.map +1 -1
  230. package/dist/prompts/impact.js +3 -22
  231. package/dist/prompts/json_extract.d.ts +14 -0
  232. package/dist/prompts/json_extract.d.ts.map +1 -0
  233. package/dist/prompts/json_extract.js +39 -0
  234. package/dist/prompts/strategist.d.ts.map +1 -1
  235. package/dist/prompts/strategist.js +2 -20
  236. package/dist/prompts/test-designer.d.ts +2 -0
  237. package/dist/prompts/test-designer.d.ts.map +1 -1
  238. package/dist/prompts/test-designer.js +6 -21
  239. package/dist/provider_factory.d.ts.map +1 -1
  240. package/dist/provider_factory.js +6 -4
  241. package/dist/reporters/junit.d.ts +6 -0
  242. package/dist/reporters/junit.d.ts.map +1 -0
  243. package/dist/reporters/junit.js +89 -0
  244. package/dist/reporters/reporter.d.ts +42 -0
  245. package/dist/reporters/reporter.d.ts.map +1 -0
  246. package/dist/reporters/reporter.js +4 -0
  247. package/dist/reporters/sarif.d.ts +7 -0
  248. package/dist/reporters/sarif.d.ts.map +1 -0
  249. package/dist/reporters/sarif.js +134 -0
  250. package/dist/resilience/circuit_breaker.d.ts +36 -0
  251. package/dist/resilience/circuit_breaker.d.ts.map +1 -0
  252. package/dist/resilience/circuit_breaker.js +82 -0
  253. package/dist/resilience/retry.d.ts +11 -0
  254. package/dist/resilience/retry.d.ts.map +1 -0
  255. package/dist/resilience/retry.js +59 -0
  256. package/dist/sanitize.d.ts +15 -0
  257. package/dist/sanitize.d.ts.map +1 -0
  258. package/dist/sanitize.js +71 -0
  259. package/dist/training/kg_scanner.d.ts +13 -0
  260. package/dist/training/kg_scanner.d.ts.map +1 -0
  261. package/dist/training/kg_scanner.js +118 -0
  262. package/dist/training/scanner.d.ts +7 -2
  263. package/dist/training/scanner.d.ts.map +1 -1
  264. package/dist/training/scanner.js +27 -34
  265. package/dist/validation/guardrails.d.ts +2 -0
  266. package/dist/validation/guardrails.d.ts.map +1 -1
  267. package/dist/validation/guardrails.js +5 -0
  268. package/dist/validation/output_schema.d.ts +3 -0
  269. package/dist/validation/output_schema.d.ts.map +1 -1
  270. package/dist/version.d.ts +6 -0
  271. package/dist/version.d.ts.map +1 -0
  272. package/dist/version.js +36 -0
  273. package/package.json +7 -2
  274. package/schemas/route-families.schema.json +31 -1
@@ -1,10 +1,12 @@
1
1
  // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
2
2
  // See LICENSE.txt for license information.
3
+ import { extractJsonFromResponse } from './json_extract.js';
4
+ import { sanitizeForPrompt } from '../crew/sanitize.js';
3
5
  export function buildCoveragePrompt(ctx) {
4
6
  const flowsBlock = ctx.flows
5
7
  .map((f) => {
6
- const actions = f.userActions.length > 0 ? f.userActions.join('; ') : 'unknown';
7
- return `- ${f.flowId} (${f.priority}): ${f.flowName}\n Route: ${f.route}\n User actions: ${actions}\n Evidence: ${f.evidence}`;
8
+ const actions = f.userActions.length > 0 ? f.userActions.map((a) => sanitizeForPrompt(a)).join('; ') : 'unknown';
9
+ return `- ${f.flowId} (${f.priority}): ${f.flowName}\n Route: ${f.route}\n User actions: ${actions}\n Evidence: ${sanitizeForPrompt(f.evidence)}`;
8
10
  })
9
11
  .join('\n\n');
10
12
  const specsBlock = ctx.specs
@@ -13,7 +15,7 @@ export function buildCoveragePrompt(ctx) {
13
15
  })
14
16
  .join('\n\n');
15
17
  return [
16
- 'You are evaluating whether existing Mattermost Playwright E2E tests cover the impacted flows.',
18
+ `You are evaluating whether existing ${ctx.profile?.projectName || 'Mattermost'} ${ctx.profile?.testFramework || 'Playwright'} E2E tests cover the impacted flows.`,
17
19
  '',
18
20
  `IMPACTED FLOWS (${ctx.flows.length}):`,
19
21
  flowsBlock,
@@ -36,29 +38,20 @@ export function buildCoveragePrompt(ctx) {
36
38
  ' Wrong: "test the new isEditing state"',
37
39
  ' Right: "test editing a scheduled message while it is in pending state"',
38
40
  '- For add_scenarios, specify which existing spec file to extend in targetSpec.',
39
- '- For create_spec, suggest a path following Mattermost conventions.',
41
+ `- For create_spec, suggest a path following ${ctx.profile?.projectName || 'Mattermost'} conventions.`,
40
42
  '- Prefer adding scenarios to existing specs over creating new spec files.',
43
+ '',
44
+ 'SEMANTIC MATCHING RULES (critical for accuracy):',
45
+ '- A happy-path test does NOT cover the negative/error path of the same feature.',
46
+ ' "user can edit post" does NOT cover "user without permission cannot edit post".',
47
+ '- A test for one user role does NOT cover a different role.',
48
+ ' "admin can delete channel" does NOT cover "member cannot delete channel".',
49
+ '- A test for creation does NOT cover editing or deletion of the same entity.',
50
+ '- "partial" means: same feature area but different specific scenario.',
51
+ '- "full" means: the exact user action sequence and outcome is tested.',
52
+ '- When in doubt between "full" and "partial", choose "partial".',
41
53
  ].join('\n');
42
54
  }
43
55
  export function parseCoverageResponse(text) {
44
- const fenced = text.match(/```(?:json)?\s*([\s\S]*?)```/i);
45
- const candidates = fenced ? [fenced[1], text] : [text];
46
- for (const candidate of candidates) {
47
- const start = candidate.indexOf('{');
48
- const end = candidate.lastIndexOf('}');
49
- if (start < 0 || end <= start) {
50
- continue;
51
- }
52
- const raw = candidate.slice(start, end + 1);
53
- try {
54
- const parsed = JSON.parse(raw);
55
- if (parsed && Array.isArray(parsed.coverage)) {
56
- return parsed;
57
- }
58
- }
59
- catch {
60
- continue;
61
- }
62
- }
63
- return null;
56
+ return extractJsonFromResponse(text, (obj) => obj != null && typeof obj === 'object' && Array.isArray(obj.coverage));
64
57
  }
@@ -1,6 +1,7 @@
1
1
  // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
2
2
  // See LICENSE.txt for license information.
3
3
  import { sanitizeForPrompt } from '../crew/sanitize.js';
4
+ import { extractJsonFromResponse } from './json_extract.js';
4
5
  export function buildCrossImpactPrompt(ctx) {
5
6
  const familiesBlock = ctx.families
6
7
  .map((f) => {
@@ -14,7 +15,7 @@ export function buildCrossImpactPrompt(ctx) {
14
15
  .join('\n');
15
16
  const changedBlock = ctx.changedFiles.map((f) => sanitizeForPrompt(f)).join('\n');
16
17
  return [
17
- 'You are analyzing code changes in Mattermost to identify cross-family ripple effects.',
18
+ `You are analyzing code changes in ${ctx.projectName || 'Mattermost'} to identify cross-family ripple effects.`,
18
19
  'When a change in one route family could affect another family through shared dependencies,',
19
20
  'that is a cross-impact.',
20
21
  '',
@@ -48,24 +49,5 @@ export function buildCrossImpactPrompt(ctx) {
48
49
  ].join('\n');
49
50
  }
50
51
  export function parseCrossImpactResponse(text) {
51
- const fenced = text.match(/```(?:json)?\s*([\s\S]*?)```/i);
52
- const candidates = fenced ? [fenced[1], text] : [text];
53
- for (const candidate of candidates) {
54
- const start = candidate.indexOf('{');
55
- const end = candidate.lastIndexOf('}');
56
- if (start < 0 || end <= start) {
57
- continue;
58
- }
59
- const raw = candidate.slice(start, end + 1);
60
- try {
61
- const parsed = JSON.parse(raw);
62
- if (parsed && Array.isArray(parsed.crossImpacts)) {
63
- return parsed;
64
- }
65
- }
66
- catch {
67
- continue;
68
- }
69
- }
70
- return null;
52
+ return extractJsonFromResponse(text, (obj) => obj != null && typeof obj === 'object' && Array.isArray(obj.crossImpacts));
71
53
  }
@@ -1,6 +1,8 @@
1
1
  // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
2
2
  // See LICENSE.txt for license information.
3
3
  import { formatApiSurfaceForPrompt } from '../knowledge/api_surface.js';
4
+ import { sanitizeForPrompt } from '../crew/sanitize.js';
5
+ import { isMattermostProfile } from './generation_profile.js';
4
6
  function resolveRelevantPageObjects(apiSurface, decision) {
5
7
  const relevant = [];
6
8
  const familyHints = [
@@ -20,7 +22,21 @@ function resolveRelevantPageObjects(apiSurface, decision) {
20
22
  }
21
23
  return [...new Set(relevant)].slice(0, 10);
22
24
  }
25
+ function buildAssertionPatternsBlock(patterns) {
26
+ if (!patterns || patterns.length === 0) {
27
+ return [];
28
+ }
29
+ return [
30
+ 'REQUIRED ASSERTION PATTERNS:',
31
+ 'Your tests MUST include assertions that verify each of these behaviors.',
32
+ 'Do NOT just check element visibility — verify the actual business outcome.',
33
+ ...patterns.map((p) => ` - [${p.type}] ${p.pattern}`),
34
+ '',
35
+ ];
36
+ }
23
37
  export function buildGenerationPrompt(ctx) {
38
+ const profile = ctx.profile;
39
+ const isMM = profile ? isMattermostProfile(profile) : true;
24
40
  const relevantClasses = resolveRelevantPageObjects(ctx.apiSurface, ctx.decision);
25
41
  const apiBlock = relevantClasses.length > 0
26
42
  ? formatApiSurfaceForPrompt(ctx.apiSurface, relevantClasses)
@@ -35,73 +51,190 @@ export function buildGenerationPrompt(ctx) {
35
51
  ? `Create a NEW spec file at: ${ctx.specPath}`
36
52
  : `ADD scenarios to the EXISTING spec at: ${ctx.specPath}`;
37
53
  const routeFamilyTag = ctx.decision.routeFamily;
54
+ // Build prompt based on profile
55
+ const projectName = profile?.projectName || 'Mattermost';
56
+ const testFramework = profile?.testFramework || 'Playwright';
57
+ const importStatement = profile?.importStatement || '@mattermost/playwright-lib';
58
+ // API test mode prompt
59
+ if (profile?.testMode === 'api') {
60
+ return buildApiTestPrompt(ctx, profile, scenariosBlock, routeFamilyTag);
61
+ }
62
+ // Build rules from profile conventions or use Mattermost defaults
63
+ const rules = isMM
64
+ ? [
65
+ `1. Import ONLY from "${importStatement}" — no other test framework imports.`,
66
+ '2. Every test must call `await pw.initSetup()` first.',
67
+ '3. Use `await pw.testBrowser.login(user)` to log in — never hardcode credentials.',
68
+ '4. Use ONLY page object methods listed above. Do NOT invent methods that are not listed.',
69
+ '5. If a method is not available, use `page.getByRole()` or `page.getByTestId()`.',
70
+ `6. Tag every test: {tag: '@${routeFamilyTag}'}`,
71
+ '7. Write one test per scenario with a descriptive name of what the user does and what is verified.',
72
+ `8. Use \`expect\` from "${importStatement}" — do NOT import from "@playwright/test".`,
73
+ '9. Include the copyright header for new files.',
74
+ '10. NEVER fabricate test IDs (MM-TXXXX). Use descriptive names only.',
75
+ ]
76
+ : [
77
+ ...(profile?.conventions || []).map((c, i) => `${i + 1}. ${c}`),
78
+ `${(profile?.conventions?.length || 0) + 1}. Use ONLY page object methods listed above. Do NOT invent methods that are not listed.`,
79
+ `${(profile?.conventions?.length || 0) + 2}. If a method is not available, use \`page.getByRole()\` or \`page.getByTestId()\`.`,
80
+ `${(profile?.conventions?.length || 0) + 3}. Tag every test: {tag: '@${routeFamilyTag}'}`,
81
+ ];
82
+ // Build example block
83
+ const exampleBlock = isMM
84
+ ? [
85
+ 'EXAMPLE SPEC STRUCTURE:',
86
+ '```typescript',
87
+ '// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.',
88
+ '// See LICENSE.txt for license information.',
89
+ '',
90
+ `import {expect, test} from '${importStatement}';`,
91
+ '',
92
+ 'test(',
93
+ " 'descriptive name of what is tested',",
94
+ ` {tag: '@${routeFamilyTag}'},`,
95
+ ' async ({pw}) => {',
96
+ ' const {user} = await pw.initSetup();',
97
+ ' const {channelsPage} = await pw.testBrowser.login(user);',
98
+ ' await channelsPage.goto();',
99
+ ' await channelsPage.toBeVisible();',
100
+ ' // test steps...',
101
+ ' },',
102
+ ');',
103
+ '```',
104
+ ]
105
+ : [
106
+ 'EXAMPLE SPEC STRUCTURE:',
107
+ '```typescript',
108
+ ...(profile?.copyrightHeader ? [profile.copyrightHeader, ''] : []),
109
+ `import {test, expect} from '${importStatement}';`,
110
+ '',
111
+ 'test(',
112
+ " 'descriptive name of what is tested',",
113
+ ` {tag: '@${routeFamilyTag}'},`,
114
+ ' async ({page}) => {',
115
+ ' // test steps...',
116
+ ' },',
117
+ ');',
118
+ '```',
119
+ ];
38
120
  return [
39
- 'You are generating Mattermost Playwright E2E test code.',
121
+ `You are generating ${projectName} ${testFramework} E2E test code.`,
40
122
  '',
41
123
  `TASK: ${modeInstruction}`,
42
124
  '',
43
- `FLOW: ${ctx.decision.flowName}`,
125
+ `FLOW: ${sanitizeForPrompt(ctx.decision.flowName)}`,
44
126
  `Route Family: ${ctx.decision.routeFamily}${ctx.decision.featureId ? ` / ${ctx.decision.featureId}` : ''}`,
45
127
  `Route: ${ctx.decision.specificRoute || '(not specified)'}`,
46
128
  `Priority: ${ctx.decision.priority}`,
47
- `Evidence: ${ctx.decision.evidence}`,
129
+ `Evidence: ${sanitizeForPrompt(ctx.decision.evidence)}`,
48
130
  '',
49
131
  'SCENARIOS TO IMPLEMENT:',
50
132
  scenariosBlock || ' (implement core user actions for this flow)',
51
133
  '',
52
134
  'USER ACTIONS:',
53
- ctx.decision.userActions.map((a) => ` - ${a}`).join('\n') || ' (none specified)',
135
+ ctx.decision.userActions.map((a) => ` - ${sanitizeForPrompt(a)}`).join('\n') || ' (none specified)',
54
136
  '',
137
+ ...buildAssertionPatternsBlock(ctx.decision.assertionPatterns),
55
138
  'AVAILABLE PAGE OBJECTS AND METHODS:',
56
139
  apiBlock,
57
140
  existingBlock,
58
141
  '',
59
142
  'MANDATORY RULES:',
60
- '1. Import ONLY from "@mattermost/playwright-lib" — no other test framework imports.',
61
- '2. Every test must call `await pw.initSetup()` first.',
62
- '3. Use `await pw.testBrowser.login(user)` to log in — never hardcode credentials.',
63
- '4. Use ONLY page object methods listed above. Do NOT invent methods that are not listed.',
64
- '5. If a method is not available, use `page.getByRole()` or `page.getByTestId()`.',
65
- `6. Tag every test: {tag: '@${routeFamilyTag}'}`,
66
- '7. Write one test per scenario with a descriptive name of what the user does and what is verified.',
67
- '8. Use `expect` from "@mattermost/playwright-lib" — do NOT import from "@playwright/test".',
68
- '9. Include the copyright header for new files.',
69
- '10. NEVER fabricate test IDs (MM-TXXXX). Use descriptive names only.',
70
- '',
71
- 'EXAMPLE SPEC STRUCTURE:',
72
- '```typescript',
73
- '// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.',
74
- '// See LICENSE.txt for license information.',
75
- '',
76
- "import {expect, test} from '@mattermost/playwright-lib';",
77
- '',
78
- 'test(',
79
- " 'descriptive name of what is tested',",
80
- ` {tag: '@${routeFamilyTag}'},`,
81
- ' async ({pw}) => {',
82
- ' const {user} = await pw.initSetup();',
83
- ' const {channelsPage} = await pw.testBrowser.login(user);',
84
- ' await channelsPage.goto();',
85
- ' await channelsPage.toBeVisible();',
86
- ' // test steps...',
87
- ' },',
88
- ');',
89
- '```',
143
+ ...rules,
144
+ '',
145
+ ...exampleBlock,
90
146
  '',
91
147
  'Return ONLY the TypeScript code. No explanations, no markdown fences.',
92
148
  ].join('\n');
93
149
  }
94
- export function parseGenerationResponse(text, expectedPath, mode, flowId) {
150
+ function buildApiTestPrompt(ctx, profile, scenariosBlock, routeFamilyTag) {
151
+ const modeInstruction = ctx.mode === 'create_spec'
152
+ ? `Create a NEW test file at: ${ctx.specPath}`
153
+ : `ADD test cases to the EXISTING file at: ${ctx.specPath}`;
154
+ const existingBlock = ctx.existingSpecContent
155
+ ? `\nEXISTING FILE (extend this):\n\`\`\`typescript\n${ctx.existingSpecContent}\n\`\`\``
156
+ : '';
157
+ return [
158
+ `You are generating ${profile.projectName} API test code using ${profile.testFramework}.`,
159
+ '',
160
+ `TASK: ${modeInstruction}`,
161
+ '',
162
+ `FLOW: ${sanitizeForPrompt(ctx.decision.flowName)}`,
163
+ `Route Family: ${ctx.decision.routeFamily}${ctx.decision.featureId ? ` / ${ctx.decision.featureId}` : ''}`,
164
+ `Endpoint: ${ctx.decision.specificRoute || '(not specified)'}`,
165
+ `Priority: ${ctx.decision.priority}`,
166
+ `Evidence: ${sanitizeForPrompt(ctx.decision.evidence)}`,
167
+ '',
168
+ 'SCENARIOS TO IMPLEMENT:',
169
+ scenariosBlock || ' (implement core API endpoint tests)',
170
+ '',
171
+ 'USER ACTIONS:',
172
+ ctx.decision.userActions.map((a) => ` - ${sanitizeForPrompt(a)}`).join('\n') || ' (none specified)',
173
+ '',
174
+ ...buildAssertionPatternsBlock(ctx.decision.assertionPatterns),
175
+ existingBlock,
176
+ '',
177
+ 'MANDATORY RULES:',
178
+ ...profile.conventions.map((c, i) => `${i + 1}. ${c}`),
179
+ `${profile.conventions.length + 1}. Tag every test: {tag: '@${routeFamilyTag}'}`,
180
+ '',
181
+ 'EXAMPLE TEST STRUCTURE:',
182
+ ...(profile.testFramework.toLowerCase().includes('pytest')
183
+ ? [
184
+ '```python',
185
+ ...(profile.copyrightHeader ? [profile.copyrightHeader, ''] : []),
186
+ 'import pytest',
187
+ 'import requests',
188
+ '',
189
+ `BASE_URL = 'http://localhost:3000'`,
190
+ '',
191
+ '',
192
+ `class Test${ctx.decision.routeFamily.replace(/[^a-zA-Z0-9]/g, '')}:`,
193
+ " def test_should_return_200_for_valid_request(self):",
194
+ ` res = requests.get(f'{BASE_URL}${ctx.decision.specificRoute || '/api/endpoint'}')`,
195
+ ' assert res.status_code == 200',
196
+ '```',
197
+ ]
198
+ : [
199
+ '```typescript',
200
+ ...(profile.copyrightHeader ? [profile.copyrightHeader, ''] : []),
201
+ `import {describe, it, expect} from '${profile.importStatement}';`,
202
+ "import supertest from 'supertest';",
203
+ '',
204
+ "const request = supertest('http://localhost:3000');",
205
+ '',
206
+ `describe('${ctx.decision.routeFamily}', () => {`,
207
+ " it('should return 200 for valid request', async () => {",
208
+ ` const res = await request.get('${ctx.decision.specificRoute || '/api/endpoint'}');`,
209
+ ' expect(res.status).toBe(200);',
210
+ ' });',
211
+ '});',
212
+ '```',
213
+ ]),
214
+ '',
215
+ ...(profile.testFramework.toLowerCase().includes('pytest')
216
+ ? ['Return ONLY the Python code. No explanations, no markdown fences.']
217
+ : ['Return ONLY the TypeScript code. No explanations, no markdown fences.']),
218
+ ].join('\n');
219
+ }
220
+ export function parseGenerationResponse(text, expectedPath, mode, flowId, profile) {
95
221
  let code = text.trim();
96
222
  const fenced = code.match(/^```(?:typescript|ts)?\s*([\s\S]*?)```\s*$/i);
97
223
  if (fenced) {
98
224
  code = fenced[1].trim();
99
225
  }
100
- if (!code.includes('test(')) {
226
+ if (!code.includes('test(') && !code.includes('it(') && !code.includes('describe(')) {
101
227
  return null;
102
228
  }
103
- if (!code.includes('@mattermost/playwright-lib')) {
104
- code = `import {expect, test} from '@mattermost/playwright-lib';\n\n${code}`;
229
+ const importStatement = profile?.importStatement || '@mattermost/playwright-lib';
230
+ // Auto-add import if missing
231
+ if (!code.includes(importStatement)) {
232
+ if (profile?.testMode === 'api') {
233
+ code = `import {describe, it, expect} from '${importStatement}';\n\n${code}`;
234
+ }
235
+ else {
236
+ code = `import {expect, test} from '${importStatement}';\n\n${code}`;
237
+ }
105
238
  }
106
239
  return { specPath: expectedPath, code, mode, flowId };
107
240
  }
@@ -121,21 +254,44 @@ const BUILT_IN_METHODS = new Set([
121
254
  'initSetup', 'login', 'waitUntil', 'skipIfNoLicense', 'ensureLicense',
122
255
  'random', 'duration', 'isOutsideRemoteUserHour', 'setTimeout',
123
256
  'skip', 'fixme', 'slow', 'fail',
257
+ // API testing built-ins
258
+ 'get', 'post', 'put', 'patch', 'delete', 'send', 'set', 'query',
259
+ 'toBe', 'toEqual', 'toBeDefined', 'toContain', 'toHaveProperty',
260
+ 'toMatchObject', 'toHaveLength', 'toBeTruthy', 'toBeFalsy',
124
261
  ]);
125
262
  /**
126
263
  * Returns method names that appear in generated code but do not exist in the API surface.
127
- * Used for logging; does not block generation.
264
+ * Detects all call patterns: await X.Y(), X.Y(), const z = X.Y(), chained calls.
128
265
  */
129
266
  export function detectHallucinatedMethods(code, apiSurface) {
130
267
  const allMethods = new Set(apiSurface.pageObjects.flatMap((po) => po.methods.map((m) => m.name)));
131
- const suspected = [];
132
- const callRe = /\bawait\s+\w+\.([a-zA-Z_]\w*)\s*\(/g;
268
+ const suspected = new Set();
269
+ const callPatterns = [
270
+ /\bawait\s+\w+\.([a-zA-Z_]\w*)\s*\(/g,
271
+ /\b(?:const|let|var)\s+\w+\s*=\s*\w+\.([a-zA-Z_]\w*)\s*\(/g,
272
+ /\b\w+Page\.([a-zA-Z_]\w*)\s*\(/g, // any *Page object (channelsPage, settingsPage, etc.)
273
+ /\b(?:pw|page|this)\.\w*\.?([a-zA-Z_]\w*)\s*\(/g,
274
+ ];
275
+ const generalCallRe = /\b[a-zA-Z_]\w*\.([a-zA-Z_]\w*)\s*\(/g;
276
+ for (const re of callPatterns) {
277
+ let match;
278
+ while ((match = re.exec(code)) !== null) {
279
+ const methodName = match[1];
280
+ if (!BUILT_IN_METHODS.has(methodName) && !allMethods.has(methodName) && methodName.length > 3) {
281
+ suspected.add(methodName);
282
+ }
283
+ }
284
+ }
133
285
  let match;
134
- while ((match = callRe.exec(code)) !== null) {
286
+ while ((match = generalCallRe.exec(code)) !== null) {
135
287
  const methodName = match[1];
136
- if (!BUILT_IN_METHODS.has(methodName) && !allMethods.has(methodName) && methodName.length > 3) {
137
- suspected.push(methodName);
288
+ if (!BUILT_IN_METHODS.has(methodName) &&
289
+ !allMethods.has(methodName) &&
290
+ methodName.length > 3 &&
291
+ !methodName.startsWith('to') &&
292
+ !methodName.startsWith('get')) {
293
+ suspected.add(methodName);
138
294
  }
139
295
  }
140
- return [...new Set(suspected)];
296
+ return [...suspected];
141
297
  }
@@ -0,0 +1,147 @@
1
+ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
2
+ // See LICENSE.txt for license information.
3
+ import { UI_FRAMEWORKS, API_FRAMEWORKS } from '../adapters/framework_adapter.js';
4
+ const MATTERMOST_PROFILE = {
5
+ projectName: 'Mattermost',
6
+ testFramework: 'Playwright',
7
+ importStatement: '@mattermost/playwright-lib',
8
+ conventions: [
9
+ 'Import ONLY from "@mattermost/playwright-lib" — no other test framework imports.',
10
+ 'Every test must call `await pw.initSetup()` first.',
11
+ 'Use `await pw.testBrowser.login(user)` to log in — never hardcode credentials.',
12
+ 'Use `expect` from "@mattermost/playwright-lib" — do NOT import from "@playwright/test".',
13
+ 'Include the copyright header for new files.',
14
+ ],
15
+ copyrightHeader: [
16
+ '// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.',
17
+ '// See LICENSE.txt for license information.',
18
+ ].join('\n'),
19
+ testMode: 'ui',
20
+ };
21
+ const DEFAULT_PLAYWRIGHT_PROFILE = {
22
+ projectName: 'Project',
23
+ testFramework: 'Playwright',
24
+ importStatement: '@playwright/test',
25
+ conventions: [
26
+ 'Import from "@playwright/test" for test and expect.',
27
+ 'Use page fixtures provided by Playwright.',
28
+ 'Prefer ARIA roles and data-testid attributes for selectors.',
29
+ 'Write one test per scenario with a descriptive name.',
30
+ ],
31
+ testMode: 'ui',
32
+ };
33
+ const DEFAULT_API_PROFILE = {
34
+ projectName: 'Project',
35
+ testFramework: 'vitest + supertest',
36
+ importStatement: 'vitest',
37
+ conventions: [
38
+ 'Import from "vitest" for test and expect.',
39
+ 'Use supertest for HTTP request assertions.',
40
+ 'Validate response status codes, headers, and body structure.',
41
+ 'Test both success and error paths for each endpoint.',
42
+ ],
43
+ testMode: 'api',
44
+ };
45
+ /**
46
+ * Resolves the generation profile from config and optional KG metadata.
47
+ * - If profile='mattermost' or Mattermost is detected, returns Mattermost profile.
48
+ * - If KG is present, derives project-specific profile from it.
49
+ * - Otherwise, returns generic Playwright profile.
50
+ */
51
+ export function resolveGenerationProfile(config, kg) {
52
+ // Explicit Mattermost profile
53
+ if (config?.profile === 'mattermost') {
54
+ return { ...MATTERMOST_PROFILE };
55
+ }
56
+ // KG-based profile derivation
57
+ if (kg) {
58
+ const frameworks = kg.project.frameworks.map((f) => f.toLowerCase());
59
+ const isMattermost = kg.project.name.toLowerCase().includes('mattermost') ||
60
+ frameworks.includes('@mattermost/playwright-lib');
61
+ if (isMattermost) {
62
+ return { ...MATTERMOST_PROFILE };
63
+ }
64
+ const testMode = config?.testMode || deriveTestMode(frameworks);
65
+ const testFramework = deriveTestFramework(frameworks, testMode);
66
+ const importStatement = deriveImportStatement(frameworks, testMode);
67
+ return {
68
+ projectName: kg.project.name || 'Project',
69
+ testFramework,
70
+ importStatement,
71
+ conventions: buildConventions(testFramework, importStatement, testMode),
72
+ testMode,
73
+ };
74
+ }
75
+ // Default profiles based on test mode
76
+ if (config?.testMode === 'api') {
77
+ return { ...DEFAULT_API_PROFILE };
78
+ }
79
+ return { ...DEFAULT_PLAYWRIGHT_PROFILE };
80
+ }
81
+ /**
82
+ * Checks if a profile is the Mattermost profile (for backward compatibility checks).
83
+ */
84
+ export function isMattermostProfile(profile) {
85
+ return profile.importStatement === '@mattermost/playwright-lib';
86
+ }
87
+ // ---------------------------------------------------------------------------
88
+ // Internal helpers
89
+ // ---------------------------------------------------------------------------
90
+ function deriveTestMode(frameworks) {
91
+ const uiSet = new Set(UI_FRAMEWORKS);
92
+ const apiSet = new Set(API_FRAMEWORKS);
93
+ const hasUiFramework = frameworks.some((f) => uiSet.has(f));
94
+ const hasApiFramework = frameworks.some((f) => apiSet.has(f));
95
+ if (hasUiFramework && hasApiFramework)
96
+ return 'both';
97
+ if (hasApiFramework && !hasUiFramework)
98
+ return 'api';
99
+ return 'ui';
100
+ }
101
+ function deriveTestFramework(frameworks, testMode) {
102
+ if (testMode === 'api') {
103
+ if (frameworks.includes('pytest'))
104
+ return 'pytest';
105
+ if (frameworks.includes('jest'))
106
+ return 'jest + supertest';
107
+ return 'vitest + supertest';
108
+ }
109
+ if (frameworks.includes('cypress'))
110
+ return 'Cypress';
111
+ if (frameworks.includes('selenium'))
112
+ return 'Selenium';
113
+ return 'Playwright';
114
+ }
115
+ function deriveImportStatement(frameworks, testMode) {
116
+ if (testMode === 'api') {
117
+ if (frameworks.includes('pytest'))
118
+ return 'pytest';
119
+ if (frameworks.includes('jest'))
120
+ return 'jest';
121
+ return 'vitest';
122
+ }
123
+ if (frameworks.includes('cypress'))
124
+ return 'cypress';
125
+ return '@playwright/test';
126
+ }
127
+ function buildConventions(testFramework, importStatement, testMode) {
128
+ const conventions = [];
129
+ if (testMode === 'api' || testMode === 'both') {
130
+ conventions.push(`Import from "${importStatement}" for test and expect.`);
131
+ conventions.push('Validate response status codes, headers, and body structure.');
132
+ conventions.push('Test both success and error paths for each endpoint.');
133
+ }
134
+ if (testMode === 'ui' || testMode === 'both') {
135
+ if (testFramework.includes('Playwright')) {
136
+ conventions.push('Import from "@playwright/test" for test and expect.');
137
+ conventions.push('Use page fixtures provided by Playwright.');
138
+ }
139
+ else if (testFramework.includes('Cypress')) {
140
+ conventions.push('Use cy.* commands for browser interaction.');
141
+ }
142
+ conventions.push('Prefer ARIA roles and data-testid attributes for selectors.');
143
+ }
144
+ conventions.push('Write one test per scenario with a descriptive name of what the user does and what is verified.');
145
+ conventions.push('NEVER fabricate test IDs. Use descriptive names only.');
146
+ return conventions;
147
+ }
@@ -1,5 +1,7 @@
1
1
  // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
2
2
  // See LICENSE.txt for license information.
3
+ import { sanitizeForPrompt } from '../crew/sanitize.js';
4
+ import { isMattermostProfile } from './generation_profile.js';
3
5
  /**
4
6
  * Builds a route-family-aware heal prompt for the playwright-test-healer agent.
5
7
  * Enriches the base healer constraints with flow context so the agent understands
@@ -10,24 +12,43 @@ export function buildHealPrompt(ctx) {
10
12
  ? [
11
13
  '',
12
14
  'FLOW CONTEXT (use to understand test intent — do not change test objectives):',
13
- ` Flow: ${ctx.decision.flowName}`,
15
+ ` Flow: ${sanitizeForPrompt(ctx.decision.flowName)}`,
14
16
  ` Route Family: ${ctx.decision.routeFamily}${ctx.decision.featureId ? ` / ${ctx.decision.featureId}` : ''}`,
15
17
  ` Route: ${ctx.decision.specificRoute || '(family-level)'}`,
16
- ` User Actions: ${ctx.decision.userActions.join('; ') || 'not specified'}`,
17
- ` Evidence: ${ctx.decision.evidence}`,
18
+ ` User Actions: ${sanitizeForPrompt(ctx.decision.userActions.join('; ')) || 'not specified'}`,
19
+ ` Evidence: ${sanitizeForPrompt(ctx.decision.evidence)}`,
18
20
  ].join('\n')
19
21
  : '';
20
22
  const statusNote = ctx.status === 'flaky'
21
23
  ? 'This test is FLAKY (passes sometimes, fails other times). Look for race conditions, missing waits, or order-dependent state.'
22
24
  : 'This test is FAILING consistently. The selector, URL, or API call is likely broken.';
23
25
  const failureBlock = ctx.failureDetail
24
- ? `\nFailure detail:\n${ctx.failureDetail}`
26
+ ? `\nFailure detail:\n${sanitizeForPrompt(ctx.failureDetail)}`
25
27
  : '';
26
28
  const consoleBlock = ctx.consoleErrors && ctx.consoleErrors.length > 0
27
- ? `\nRecent console errors from test run:\n${ctx.consoleErrors.slice(-3).map((e) => ` - ${e}`).join('\n')}`
29
+ ? `\nRecent console errors from test run:\n${ctx.consoleErrors.slice(-3).map((e) => ` - ${sanitizeForPrompt(e)}`).join('\n')}`
28
30
  : '';
31
+ const importLib = ctx.profile?.importStatement || '@mattermost/playwright-lib';
32
+ const isMM = ctx.profile ? isMattermostProfile(ctx.profile) : true;
33
+ const projectLabel = ctx.profile?.projectName || 'Mattermost';
34
+ const frameworkLabel = ctx.profile?.testFramework || 'Playwright';
35
+ const constraints = isMM
36
+ ? [
37
+ `- Import ONLY from "${importLib}". Do not use "@playwright/test" directly.`,
38
+ '- Do not use test.describe or test.only.',
39
+ '- Keep a single tag string matching the route family (e.g. "@channels", "@scheduled_posts").',
40
+ `- Use only existing ${projectLabel} ${frameworkLabel} fixture and page-object APIs.`,
41
+ '- Do NOT invent new pw.* clients or page object methods that do not exist.',
42
+ '- Avoid brittle class selectors (.backstage-navbar, .admin-console__wrapper, .left-panel, .panel-card).',
43
+ ]
44
+ : [
45
+ `- Import from "${importLib}".`,
46
+ '- Keep a single tag string matching the route family.',
47
+ `- Use only existing ${projectLabel} ${frameworkLabel} page-object APIs.`,
48
+ '- Do NOT invent page object methods that do not exist.',
49
+ ];
29
50
  return [
30
- 'Heal this specific Playwright test file and keep edits minimal.',
51
+ `Heal this specific ${frameworkLabel} test file and keep edits minimal.`,
31
52
  '',
32
53
  `Target test file: ${ctx.specPath}`,
33
54
  `Status: ${ctx.status.toUpperCase()} — ${statusNote}`,
@@ -36,12 +57,7 @@ export function buildHealPrompt(ctx) {
36
57
  flowBlock,
37
58
  '',
38
59
  'Healing constraints (must follow):',
39
- '- Import ONLY from "@mattermost/playwright-lib". Do not use "@playwright/test" directly.',
40
- '- Do not use test.describe or test.only.',
41
- '- Keep a single tag string matching the route family (e.g. "@channels", "@scheduled_posts").',
42
- '- Use only existing Mattermost Playwright fixture and page-object APIs.',
43
- '- Do NOT invent new pw.* clients or page object methods that do not exist.',
44
- '- Avoid brittle class selectors (.backstage-navbar, .admin-console__wrapper, .left-panel, .panel-card).',
60
+ ...constraints,
45
61
  '- Prefer stable assertions using URL patterns, data-testid attributes, ARIA roles, and page-object methods.',
46
62
  '- For flaky tests: add explicit waits (waitFor, expect().toBeVisible()) before interactions.',
47
63
  '- Keep the test intent and scenario unchanged — only fix what is broken.',
@@ -54,9 +70,11 @@ export function buildHealPrompt(ctx) {
54
70
  * Builds a minimal quality-fix prompt for spec files that fail content validation
55
71
  * (e.g. contain test.describe, test.only, wrong imports).
56
72
  */
57
- export function buildQualityFixPrompt(specPath, qualityIssues) {
73
+ export function buildQualityFixPrompt(specPath, qualityIssues, profile) {
74
+ const importLib = profile?.importStatement || '@mattermost/playwright-lib';
75
+ const frameworkLabel = profile?.testFramework || 'Playwright';
58
76
  return [
59
- 'Fix quality issues in this Playwright spec file. Make minimal edits only.',
77
+ `Fix quality issues in this ${frameworkLabel} spec file. Make minimal edits only.`,
60
78
  '',
61
79
  `Target file: ${specPath}`,
62
80
  '',
@@ -64,7 +82,7 @@ export function buildQualityFixPrompt(specPath, qualityIssues) {
64
82
  ...qualityIssues.map((issue) => ` - ${issue}`),
65
83
  '',
66
84
  'Rules:',
67
- '- Import only from "@mattermost/playwright-lib".',
85
+ `- Import only from "${importLib}".`,
68
86
  '- Remove test.describe wrappers (flatten to top-level test() calls).',
69
87
  '- Remove test.only calls.',
70
88
  '- Ensure each test has exactly one tag string.',