@yasserkhanorg/e2e-agents 1.8.4 → 1.9.5

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 (259) 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/plan_crew.d.ts.map +1 -1
  63. package/dist/cli/commands/plan_crew.js +33 -21
  64. package/dist/cli/commands/train.d.ts.map +1 -1
  65. package/dist/cli/commands/train.js +16 -21
  66. package/dist/cli/defaults.d.ts +35 -0
  67. package/dist/cli/defaults.d.ts.map +1 -0
  68. package/dist/cli/defaults.js +125 -0
  69. package/dist/cli/errors.d.ts +27 -0
  70. package/dist/cli/errors.d.ts.map +1 -0
  71. package/dist/cli/errors.js +57 -0
  72. package/dist/cli/parse_args.d.ts.map +1 -1
  73. package/dist/cli/parse_args.js +24 -2
  74. package/dist/cli/types.d.ts +7 -1
  75. package/dist/cli/types.d.ts.map +1 -1
  76. package/dist/cli.js +47 -2
  77. package/dist/crew/context.d.ts +15 -0
  78. package/dist/crew/context.d.ts.map +1 -1
  79. package/dist/crew/orchestrator.d.ts +14 -0
  80. package/dist/crew/orchestrator.d.ts.map +1 -1
  81. package/dist/crew/orchestrator.js +162 -4
  82. package/dist/crew/protocol.d.ts +13 -0
  83. package/dist/crew/protocol.d.ts.map +1 -1
  84. package/dist/crew/provider.d.ts +15 -1
  85. package/dist/crew/provider.d.ts.map +1 -1
  86. package/dist/crew/provider.js +24 -4
  87. package/dist/custom_provider.d.ts.map +1 -1
  88. package/dist/custom_provider.js +1 -0
  89. package/dist/engine/diff_loader.d.ts.map +1 -1
  90. package/dist/engine/diff_loader.js +3 -14
  91. package/dist/engine/impact_engine.d.ts.map +1 -1
  92. package/dist/engine/impact_engine.js +9 -23
  93. package/dist/esm/adapters/cypress.js +49 -0
  94. package/dist/esm/adapters/framework_adapter.js +114 -0
  95. package/dist/esm/adapters/playwright.js +49 -0
  96. package/dist/esm/adapters/pytest.js +59 -0
  97. package/dist/esm/adapters/supertest.js +48 -0
  98. package/dist/esm/agent/git.js +3 -1
  99. package/dist/esm/agentic/fix_loop.js +5 -4
  100. package/dist/esm/agentic/runner.js +15 -12
  101. package/dist/esm/agents/cross-impact.js +6 -1
  102. package/dist/esm/agents/executor.js +6 -1
  103. package/dist/esm/agents/strategist.js +6 -1
  104. package/dist/esm/agents/test-designer.js +6 -1
  105. package/dist/esm/anthropic_provider.js +1 -0
  106. package/dist/esm/base_provider.js +121 -0
  107. package/dist/esm/budget_ledger.js +58 -0
  108. package/dist/esm/cache/cached_provider.js +82 -0
  109. package/dist/esm/cache/response_cache.js +140 -0
  110. package/dist/esm/cli/commands/bootstrap.js +106 -0
  111. package/dist/esm/cli/commands/cost_report.js +112 -0
  112. package/dist/esm/cli/commands/crew.js +118 -1
  113. package/dist/esm/cli/commands/gate.js +83 -0
  114. package/dist/esm/cli/commands/init.js +3 -58
  115. package/dist/esm/cli/commands/plan_crew.js +33 -21
  116. package/dist/esm/cli/commands/train.js +16 -21
  117. package/dist/esm/cli/defaults.js +118 -0
  118. package/dist/esm/cli/errors.js +52 -0
  119. package/dist/esm/cli/parse_args.js +24 -2
  120. package/dist/esm/cli.js +47 -2
  121. package/dist/esm/crew/orchestrator.js +162 -4
  122. package/dist/esm/crew/provider.js +24 -4
  123. package/dist/esm/custom_provider.js +1 -0
  124. package/dist/esm/engine/diff_loader.js +1 -12
  125. package/dist/esm/engine/impact_engine.js +9 -23
  126. package/dist/esm/index.js +21 -0
  127. package/dist/esm/knowledge/cluster_utils.js +60 -0
  128. package/dist/esm/knowledge/kg_bridge.js +381 -0
  129. package/dist/esm/knowledge/kg_types.js +3 -0
  130. package/dist/esm/knowledge/route_families.js +89 -0
  131. package/dist/esm/mcp-server.js +2 -4
  132. package/dist/esm/metrics/prometheus.js +149 -0
  133. package/dist/esm/model_router.js +59 -0
  134. package/dist/esm/ollama_provider.js +1 -0
  135. package/dist/esm/openai_provider.js +1 -0
  136. package/dist/esm/pipeline/orchestrator.js +6 -12
  137. package/dist/esm/pipeline/stage0_preprocess.js +12 -19
  138. package/dist/esm/pipeline/stage2_coverage.js +1 -0
  139. package/dist/esm/pipeline/stage3_generation.js +1 -0
  140. package/dist/esm/progress.js +112 -0
  141. package/dist/esm/prompts/coverage.js +7 -24
  142. package/dist/esm/prompts/cross-impact.js +3 -21
  143. package/dist/esm/prompts/generation.js +158 -36
  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/version.js +33 -0
  160. package/dist/index.d.ts +21 -1
  161. package/dist/index.d.ts.map +1 -1
  162. package/dist/index.js +45 -1
  163. package/dist/knowledge/cluster_utils.d.ts +28 -0
  164. package/dist/knowledge/cluster_utils.d.ts.map +1 -0
  165. package/dist/knowledge/cluster_utils.js +67 -0
  166. package/dist/knowledge/kg_bridge.d.ts +31 -0
  167. package/dist/knowledge/kg_bridge.d.ts.map +1 -0
  168. package/dist/knowledge/kg_bridge.js +388 -0
  169. package/dist/knowledge/kg_types.d.ts +75 -0
  170. package/dist/knowledge/kg_types.d.ts.map +1 -0
  171. package/dist/knowledge/kg_types.js +4 -0
  172. package/dist/knowledge/route_families.d.ts +18 -0
  173. package/dist/knowledge/route_families.d.ts.map +1 -1
  174. package/dist/knowledge/route_families.js +91 -0
  175. package/dist/mcp-server.d.ts.map +1 -1
  176. package/dist/mcp-server.js +2 -4
  177. package/dist/metrics/prometheus.d.ts +37 -0
  178. package/dist/metrics/prometheus.d.ts.map +1 -0
  179. package/dist/metrics/prometheus.js +153 -0
  180. package/dist/model_router.d.ts +28 -0
  181. package/dist/model_router.d.ts.map +1 -0
  182. package/dist/model_router.js +63 -0
  183. package/dist/ollama_provider.d.ts.map +1 -1
  184. package/dist/ollama_provider.js +1 -0
  185. package/dist/openai_provider.d.ts.map +1 -1
  186. package/dist/openai_provider.js +1 -0
  187. package/dist/pipeline/orchestrator.d.ts +2 -0
  188. package/dist/pipeline/orchestrator.d.ts.map +1 -1
  189. package/dist/pipeline/orchestrator.js +6 -12
  190. package/dist/pipeline/stage0_preprocess.d.ts.map +1 -1
  191. package/dist/pipeline/stage0_preprocess.js +11 -18
  192. package/dist/pipeline/stage2_coverage.d.ts +2 -0
  193. package/dist/pipeline/stage2_coverage.d.ts.map +1 -1
  194. package/dist/pipeline/stage2_coverage.js +1 -0
  195. package/dist/pipeline/stage3_generation.d.ts +2 -0
  196. package/dist/pipeline/stage3_generation.d.ts.map +1 -1
  197. package/dist/pipeline/stage3_generation.js +1 -0
  198. package/dist/pipeline/stage4_heal.d.ts +2 -0
  199. package/dist/pipeline/stage4_heal.d.ts.map +1 -1
  200. package/dist/progress.d.ts +22 -0
  201. package/dist/progress.d.ts.map +1 -0
  202. package/dist/progress.js +116 -0
  203. package/dist/prompts/coverage.d.ts +2 -0
  204. package/dist/prompts/coverage.d.ts.map +1 -1
  205. package/dist/prompts/coverage.js +7 -24
  206. package/dist/prompts/cross-impact.d.ts +1 -0
  207. package/dist/prompts/cross-impact.d.ts.map +1 -1
  208. package/dist/prompts/cross-impact.js +3 -21
  209. package/dist/prompts/generation.d.ts +3 -1
  210. package/dist/prompts/generation.d.ts.map +1 -1
  211. package/dist/prompts/generation.js +158 -36
  212. package/dist/prompts/generation_profile.d.ts +29 -0
  213. package/dist/prompts/generation_profile.d.ts.map +1 -0
  214. package/dist/prompts/generation_profile.js +151 -0
  215. package/dist/prompts/heal.d.ts +3 -1
  216. package/dist/prompts/heal.d.ts.map +1 -1
  217. package/dist/prompts/heal.js +33 -15
  218. package/dist/prompts/impact.d.ts +1 -0
  219. package/dist/prompts/impact.d.ts.map +1 -1
  220. package/dist/prompts/impact.js +3 -22
  221. package/dist/prompts/json_extract.d.ts +14 -0
  222. package/dist/prompts/json_extract.d.ts.map +1 -0
  223. package/dist/prompts/json_extract.js +39 -0
  224. package/dist/prompts/strategist.d.ts.map +1 -1
  225. package/dist/prompts/strategist.js +2 -20
  226. package/dist/prompts/test-designer.d.ts +2 -0
  227. package/dist/prompts/test-designer.d.ts.map +1 -1
  228. package/dist/prompts/test-designer.js +6 -21
  229. package/dist/provider_factory.d.ts.map +1 -1
  230. package/dist/provider_factory.js +6 -4
  231. package/dist/reporters/junit.d.ts +6 -0
  232. package/dist/reporters/junit.d.ts.map +1 -0
  233. package/dist/reporters/junit.js +89 -0
  234. package/dist/reporters/reporter.d.ts +42 -0
  235. package/dist/reporters/reporter.d.ts.map +1 -0
  236. package/dist/reporters/reporter.js +4 -0
  237. package/dist/reporters/sarif.d.ts +7 -0
  238. package/dist/reporters/sarif.d.ts.map +1 -0
  239. package/dist/reporters/sarif.js +134 -0
  240. package/dist/resilience/circuit_breaker.d.ts +36 -0
  241. package/dist/resilience/circuit_breaker.d.ts.map +1 -0
  242. package/dist/resilience/circuit_breaker.js +82 -0
  243. package/dist/resilience/retry.d.ts +11 -0
  244. package/dist/resilience/retry.d.ts.map +1 -0
  245. package/dist/resilience/retry.js +59 -0
  246. package/dist/sanitize.d.ts +15 -0
  247. package/dist/sanitize.d.ts.map +1 -0
  248. package/dist/sanitize.js +71 -0
  249. package/dist/training/kg_scanner.d.ts +13 -0
  250. package/dist/training/kg_scanner.d.ts.map +1 -0
  251. package/dist/training/kg_scanner.js +118 -0
  252. package/dist/training/scanner.d.ts +7 -2
  253. package/dist/training/scanner.d.ts.map +1 -1
  254. package/dist/training/scanner.js +27 -34
  255. package/dist/version.d.ts +6 -0
  256. package/dist/version.d.ts.map +1 -0
  257. package/dist/version.js +36 -0
  258. package/package.json +7 -2
  259. package/schemas/route-families.schema.json +31 -1
@@ -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 = [
@@ -21,6 +23,8 @@ function resolveRelevantPageObjects(apiSurface, decision) {
21
23
  return [...new Set(relevant)].slice(0, 10);
22
24
  }
23
25
  export function buildGenerationPrompt(ctx) {
26
+ const profile = ctx.profile;
27
+ const isMM = profile ? isMattermostProfile(profile) : true;
24
28
  const relevantClasses = resolveRelevantPageObjects(ctx.apiSurface, ctx.decision);
25
29
  const apiBlock = relevantClasses.length > 0
26
30
  ? formatApiSurfaceForPrompt(ctx.apiSurface, relevantClasses)
@@ -35,73 +39,187 @@ export function buildGenerationPrompt(ctx) {
35
39
  ? `Create a NEW spec file at: ${ctx.specPath}`
36
40
  : `ADD scenarios to the EXISTING spec at: ${ctx.specPath}`;
37
41
  const routeFamilyTag = ctx.decision.routeFamily;
42
+ // Build prompt based on profile
43
+ const projectName = profile?.projectName || 'Mattermost';
44
+ const testFramework = profile?.testFramework || 'Playwright';
45
+ const importStatement = profile?.importStatement || '@mattermost/playwright-lib';
46
+ // API test mode prompt
47
+ if (profile?.testMode === 'api') {
48
+ return buildApiTestPrompt(ctx, profile, scenariosBlock, routeFamilyTag);
49
+ }
50
+ // Build rules from profile conventions or use Mattermost defaults
51
+ const rules = isMM
52
+ ? [
53
+ `1. Import ONLY from "${importStatement}" — no other test framework imports.`,
54
+ '2. Every test must call `await pw.initSetup()` first.',
55
+ '3. Use `await pw.testBrowser.login(user)` to log in — never hardcode credentials.',
56
+ '4. Use ONLY page object methods listed above. Do NOT invent methods that are not listed.',
57
+ '5. If a method is not available, use `page.getByRole()` or `page.getByTestId()`.',
58
+ `6. Tag every test: {tag: '@${routeFamilyTag}'}`,
59
+ '7. Write one test per scenario with a descriptive name of what the user does and what is verified.',
60
+ `8. Use \`expect\` from "${importStatement}" — do NOT import from "@playwright/test".`,
61
+ '9. Include the copyright header for new files.',
62
+ '10. NEVER fabricate test IDs (MM-TXXXX). Use descriptive names only.',
63
+ ]
64
+ : [
65
+ ...(profile?.conventions || []).map((c, i) => `${i + 1}. ${c}`),
66
+ `${(profile?.conventions?.length || 0) + 1}. Use ONLY page object methods listed above. Do NOT invent methods that are not listed.`,
67
+ `${(profile?.conventions?.length || 0) + 2}. If a method is not available, use \`page.getByRole()\` or \`page.getByTestId()\`.`,
68
+ `${(profile?.conventions?.length || 0) + 3}. Tag every test: {tag: '@${routeFamilyTag}'}`,
69
+ ];
70
+ // Build example block
71
+ const exampleBlock = isMM
72
+ ? [
73
+ 'EXAMPLE SPEC STRUCTURE:',
74
+ '```typescript',
75
+ '// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.',
76
+ '// See LICENSE.txt for license information.',
77
+ '',
78
+ `import {expect, test} from '${importStatement}';`,
79
+ '',
80
+ 'test(',
81
+ " 'descriptive name of what is tested',",
82
+ ` {tag: '@${routeFamilyTag}'},`,
83
+ ' async ({pw}) => {',
84
+ ' const {user} = await pw.initSetup();',
85
+ ' const {channelsPage} = await pw.testBrowser.login(user);',
86
+ ' await channelsPage.goto();',
87
+ ' await channelsPage.toBeVisible();',
88
+ ' // test steps...',
89
+ ' },',
90
+ ');',
91
+ '```',
92
+ ]
93
+ : [
94
+ 'EXAMPLE SPEC STRUCTURE:',
95
+ '```typescript',
96
+ ...(profile?.copyrightHeader ? [profile.copyrightHeader, ''] : []),
97
+ `import {test, expect} from '${importStatement}';`,
98
+ '',
99
+ 'test(',
100
+ " 'descriptive name of what is tested',",
101
+ ` {tag: '@${routeFamilyTag}'},`,
102
+ ' async ({page}) => {',
103
+ ' // test steps...',
104
+ ' },',
105
+ ');',
106
+ '```',
107
+ ];
38
108
  return [
39
- 'You are generating Mattermost Playwright E2E test code.',
109
+ `You are generating ${projectName} ${testFramework} E2E test code.`,
40
110
  '',
41
111
  `TASK: ${modeInstruction}`,
42
112
  '',
43
- `FLOW: ${ctx.decision.flowName}`,
113
+ `FLOW: ${sanitizeForPrompt(ctx.decision.flowName)}`,
44
114
  `Route Family: ${ctx.decision.routeFamily}${ctx.decision.featureId ? ` / ${ctx.decision.featureId}` : ''}`,
45
115
  `Route: ${ctx.decision.specificRoute || '(not specified)'}`,
46
116
  `Priority: ${ctx.decision.priority}`,
47
- `Evidence: ${ctx.decision.evidence}`,
117
+ `Evidence: ${sanitizeForPrompt(ctx.decision.evidence)}`,
48
118
  '',
49
119
  'SCENARIOS TO IMPLEMENT:',
50
120
  scenariosBlock || ' (implement core user actions for this flow)',
51
121
  '',
52
122
  'USER ACTIONS:',
53
- ctx.decision.userActions.map((a) => ` - ${a}`).join('\n') || ' (none specified)',
123
+ ctx.decision.userActions.map((a) => ` - ${sanitizeForPrompt(a)}`).join('\n') || ' (none specified)',
54
124
  '',
55
125
  'AVAILABLE PAGE OBJECTS AND METHODS:',
56
126
  apiBlock,
57
127
  existingBlock,
58
128
  '',
59
129
  '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.',
130
+ ...rules,
70
131
  '',
71
- 'EXAMPLE SPEC STRUCTURE:',
72
- '```typescript',
73
- '// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.',
74
- '// See LICENSE.txt for license information.',
132
+ ...exampleBlock,
75
133
  '',
76
- "import {expect, test} from '@mattermost/playwright-lib';",
134
+ 'Return ONLY the TypeScript code. No explanations, no markdown fences.',
135
+ ].join('\n');
136
+ }
137
+ function buildApiTestPrompt(ctx, profile, scenariosBlock, routeFamilyTag) {
138
+ const modeInstruction = ctx.mode === 'create_spec'
139
+ ? `Create a NEW test file at: ${ctx.specPath}`
140
+ : `ADD test cases to the EXISTING file at: ${ctx.specPath}`;
141
+ const existingBlock = ctx.existingSpecContent
142
+ ? `\nEXISTING FILE (extend this):\n\`\`\`typescript\n${ctx.existingSpecContent}\n\`\`\``
143
+ : '';
144
+ return [
145
+ `You are generating ${profile.projectName} API test code using ${profile.testFramework}.`,
77
146
  '',
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
- '```',
147
+ `TASK: ${modeInstruction}`,
90
148
  '',
91
- 'Return ONLY the TypeScript code. No explanations, no markdown fences.',
149
+ `FLOW: ${sanitizeForPrompt(ctx.decision.flowName)}`,
150
+ `Route Family: ${ctx.decision.routeFamily}${ctx.decision.featureId ? ` / ${ctx.decision.featureId}` : ''}`,
151
+ `Endpoint: ${ctx.decision.specificRoute || '(not specified)'}`,
152
+ `Priority: ${ctx.decision.priority}`,
153
+ `Evidence: ${sanitizeForPrompt(ctx.decision.evidence)}`,
154
+ '',
155
+ 'SCENARIOS TO IMPLEMENT:',
156
+ scenariosBlock || ' (implement core API endpoint tests)',
157
+ '',
158
+ 'USER ACTIONS:',
159
+ ctx.decision.userActions.map((a) => ` - ${sanitizeForPrompt(a)}`).join('\n') || ' (none specified)',
160
+ existingBlock,
161
+ '',
162
+ 'MANDATORY RULES:',
163
+ ...profile.conventions.map((c, i) => `${i + 1}. ${c}`),
164
+ `${profile.conventions.length + 1}. Tag every test: {tag: '@${routeFamilyTag}'}`,
165
+ '',
166
+ 'EXAMPLE TEST STRUCTURE:',
167
+ ...(profile.testFramework.toLowerCase().includes('pytest')
168
+ ? [
169
+ '```python',
170
+ ...(profile.copyrightHeader ? [profile.copyrightHeader, ''] : []),
171
+ 'import pytest',
172
+ 'import requests',
173
+ '',
174
+ `BASE_URL = 'http://localhost:3000'`,
175
+ '',
176
+ '',
177
+ `class Test${ctx.decision.routeFamily.replace(/[^a-zA-Z0-9]/g, '')}:`,
178
+ " def test_should_return_200_for_valid_request(self):",
179
+ ` res = requests.get(f'{BASE_URL}${ctx.decision.specificRoute || '/api/endpoint'}')`,
180
+ ' assert res.status_code == 200',
181
+ '```',
182
+ ]
183
+ : [
184
+ '```typescript',
185
+ ...(profile.copyrightHeader ? [profile.copyrightHeader, ''] : []),
186
+ `import {describe, it, expect} from '${profile.importStatement}';`,
187
+ "import supertest from 'supertest';",
188
+ '',
189
+ "const request = supertest('http://localhost:3000');",
190
+ '',
191
+ `describe('${ctx.decision.routeFamily}', () => {`,
192
+ " it('should return 200 for valid request', async () => {",
193
+ ` const res = await request.get('${ctx.decision.specificRoute || '/api/endpoint'}');`,
194
+ ' expect(res.status).toBe(200);',
195
+ ' });',
196
+ '});',
197
+ '```',
198
+ ]),
199
+ '',
200
+ ...(profile.testFramework.toLowerCase().includes('pytest')
201
+ ? ['Return ONLY the Python code. No explanations, no markdown fences.']
202
+ : ['Return ONLY the TypeScript code. No explanations, no markdown fences.']),
92
203
  ].join('\n');
93
204
  }
94
- export function parseGenerationResponse(text, expectedPath, mode, flowId) {
205
+ export function parseGenerationResponse(text, expectedPath, mode, flowId, profile) {
95
206
  let code = text.trim();
96
207
  const fenced = code.match(/^```(?:typescript|ts)?\s*([\s\S]*?)```\s*$/i);
97
208
  if (fenced) {
98
209
  code = fenced[1].trim();
99
210
  }
100
- if (!code.includes('test(')) {
211
+ if (!code.includes('test(') && !code.includes('it(') && !code.includes('describe(')) {
101
212
  return null;
102
213
  }
103
- if (!code.includes('@mattermost/playwright-lib')) {
104
- code = `import {expect, test} from '@mattermost/playwright-lib';\n\n${code}`;
214
+ const importStatement = profile?.importStatement || '@mattermost/playwright-lib';
215
+ // Auto-add import if missing
216
+ if (!code.includes(importStatement)) {
217
+ if (profile?.testMode === 'api') {
218
+ code = `import {describe, it, expect} from '${importStatement}';\n\n${code}`;
219
+ }
220
+ else {
221
+ code = `import {expect, test} from '${importStatement}';\n\n${code}`;
222
+ }
105
223
  }
106
224
  return { specPath: expectedPath, code, mode, flowId };
107
225
  }
@@ -121,6 +239,10 @@ const BUILT_IN_METHODS = new Set([
121
239
  'initSetup', 'login', 'waitUntil', 'skipIfNoLicense', 'ensureLicense',
122
240
  'random', 'duration', 'isOutsideRemoteUserHour', 'setTimeout',
123
241
  'skip', 'fixme', 'slow', 'fail',
242
+ // API testing built-ins
243
+ 'get', 'post', 'put', 'patch', 'delete', 'send', 'set', 'query',
244
+ 'toBe', 'toEqual', 'toBeDefined', 'toContain', 'toHaveProperty',
245
+ 'toMatchObject', 'toHaveLength', 'toBeTruthy', 'toBeFalsy',
124
246
  ]);
125
247
  /**
126
248
  * Returns method names that appear in generated code but do not exist in the API surface.
@@ -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.',
@@ -1,5 +1,6 @@
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';
3
4
  import { formatSpecsForPrompt } from '../knowledge/spec_index.js';
4
5
  import { formatApiSurfaceForPrompt } from '../knowledge/api_surface.js';
5
6
  export function buildImpactPrompt(ctx) {
@@ -23,7 +24,7 @@ export function buildImpactPrompt(ctx) {
23
24
  })
24
25
  .join('\n\n');
25
26
  return [
26
- 'You are analyzing code changes in Mattermost to identify impacted user-facing flows.',
27
+ `You are analyzing code changes in ${ctx.projectName || 'Mattermost'} to identify impacted user-facing flows.`,
27
28
  '',
28
29
  `ROUTE FAMILY: ${ctx.family.id}`,
29
30
  `ROUTES: ${familyRoutes}`,
@@ -58,25 +59,5 @@ export function buildImpactPrompt(ctx) {
58
59
  ].filter(Boolean).join('\n');
59
60
  }
60
61
  export function parseImpactResponse(text) {
61
- // Try to extract JSON from the response
62
- const fenced = text.match(/```(?:json)?\s*([\s\S]*?)```/i);
63
- const candidates = fenced ? [fenced[1], text] : [text];
64
- for (const candidate of candidates) {
65
- const start = candidate.indexOf('{');
66
- const end = candidate.lastIndexOf('}');
67
- if (start < 0 || end <= start) {
68
- continue;
69
- }
70
- const raw = candidate.slice(start, end + 1);
71
- try {
72
- const parsed = JSON.parse(raw);
73
- if (parsed && Array.isArray(parsed.flows)) {
74
- return parsed;
75
- }
76
- }
77
- catch {
78
- continue;
79
- }
80
- }
81
- return null;
62
+ return extractJsonFromResponse(text, (obj) => obj != null && typeof obj === 'object' && Array.isArray(obj.flows));
82
63
  }
@@ -0,0 +1,36 @@
1
+ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
2
+ // See LICENSE.txt for license information.
3
+ /**
4
+ * Shared JSON extraction from LLM text responses.
5
+ * Handles fenced code blocks, bare JSON, and partial text.
6
+ */
7
+ /**
8
+ * Extract and parse JSON from LLM response text.
9
+ * Tries fenced code blocks first, then raw text.
10
+ * Returns null if no valid JSON found.
11
+ *
12
+ * @param text - Raw LLM response text
13
+ * @param validate - Predicate to check if parsed object has the expected shape
14
+ */
15
+ export function extractJsonFromResponse(text, validate) {
16
+ const fenced = text.match(/```(?:json)?\s*([\s\S]*?)```/i);
17
+ const candidates = fenced ? [fenced[1], text] : [text];
18
+ for (const candidate of candidates) {
19
+ const start = candidate.indexOf('{');
20
+ const end = candidate.lastIndexOf('}');
21
+ if (start < 0 || end <= start) {
22
+ continue;
23
+ }
24
+ const raw = candidate.slice(start, end + 1);
25
+ try {
26
+ const parsed = JSON.parse(raw);
27
+ if (validate(parsed)) {
28
+ return parsed;
29
+ }
30
+ }
31
+ catch {
32
+ continue;
33
+ }
34
+ }
35
+ return null;
36
+ }
@@ -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 buildStrategistPrompt(ctx) {
5
6
  const flowsBlock = ctx.impactedFlows
6
7
  .map((f) => {
@@ -56,24 +57,5 @@ export function buildStrategistPrompt(ctx) {
56
57
  ].join('\n');
57
58
  }
58
59
  export function parseStrategistResponse(text) {
59
- const fenced = text.match(/```(?:json)?\s*([\s\S]*?)```/i);
60
- const candidates = fenced ? [fenced[1], text] : [text];
61
- for (const candidate of candidates) {
62
- const start = candidate.indexOf('{');
63
- const end = candidate.lastIndexOf('}');
64
- if (start < 0 || end <= start) {
65
- continue;
66
- }
67
- const raw = candidate.slice(start, end + 1);
68
- try {
69
- const parsed = JSON.parse(raw);
70
- if (parsed && Array.isArray(parsed.strategy)) {
71
- return parsed;
72
- }
73
- }
74
- catch {
75
- continue;
76
- }
77
- }
78
- return null;
60
+ return extractJsonFromResponse(text, (obj) => obj != null && typeof obj === 'object' && Array.isArray(obj.strategy));
79
61
  }
@@ -1,5 +1,6 @@
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';
3
4
  import { formatApiSurfaceForPrompt } from '../knowledge/api_surface.js';
4
5
  import { sanitizeForPrompt } from '../crew/sanitize.js';
5
6
  export function buildTestDesignerPrompt(ctx) {
@@ -24,7 +25,7 @@ export function buildTestDesignerPrompt(ctx) {
24
25
  : 'None detected.';
25
26
  const categories = ctx.strategy.testCategories.join(', ');
26
27
  return [
27
- 'You are a senior QA engineer designing comprehensive test cases for a Mattermost user flow.',
28
+ `You are a senior QA engineer designing comprehensive test cases for a ${ctx.profile?.projectName || 'Mattermost'} user flow.`,
28
29
  '',
29
30
  `FLOW: ${ctx.flow.flowName}`,
30
31
  `Flow ID: ${ctx.flow.flowId}`,
@@ -84,24 +85,8 @@ export function buildTestDesignerPrompt(ctx) {
84
85
  ].join('\n');
85
86
  }
86
87
  export function parseTestDesignerResponse(text) {
87
- const fenced = text.match(/```(?:json)?\s*([\s\S]*?)```/i);
88
- const candidates = fenced ? [fenced[1], text] : [text];
89
- for (const candidate of candidates) {
90
- const start = candidate.indexOf('{');
91
- const end = candidate.lastIndexOf('}');
92
- if (start < 0 || end <= start) {
93
- continue;
94
- }
95
- const raw = candidate.slice(start, end + 1);
96
- try {
97
- const parsed = JSON.parse(raw);
98
- if (parsed?.testDesign?.testCases && Array.isArray(parsed.testDesign.testCases)) {
99
- return parsed;
100
- }
101
- }
102
- catch {
103
- continue;
104
- }
105
- }
106
- return null;
88
+ return extractJsonFromResponse(text, (obj) => {
89
+ const r = obj;
90
+ return r?.testDesign?.testCases != null && Array.isArray(r.testDesign.testCases);
91
+ });
107
92
  }