retestkit 1.4.1

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 (327) hide show
  1. package/.claude/commands/openspec/apply.md +23 -0
  2. package/.claude/commands/openspec/archive.md +27 -0
  3. package/.claude/commands/openspec/proposal.md +28 -0
  4. package/.gemini/commands/openspec/apply.toml +21 -0
  5. package/.gemini/commands/openspec/archive.toml +25 -0
  6. package/.gemini/commands/openspec/proposal.toml +26 -0
  7. package/.github/prompts/openspec-apply.prompt.md +22 -0
  8. package/.github/prompts/openspec-archive.prompt.md +26 -0
  9. package/.github/prompts/openspec-proposal.prompt.md +27 -0
  10. package/.github/workflows/release.yml +33 -0
  11. package/.kilocode/workflows/openspec-apply.md +17 -0
  12. package/.kilocode/workflows/openspec-archive.md +21 -0
  13. package/.kilocode/workflows/openspec-proposal.md +22 -0
  14. package/.mcp.json +23 -0
  15. package/.opencode/command/openspec-apply.md +25 -0
  16. package/.opencode/command/openspec-archive.md +28 -0
  17. package/.opencode/command/openspec-proposal.md +30 -0
  18. package/.roo/commands/openspec-apply.md +20 -0
  19. package/.roo/commands/openspec-archive.md +24 -0
  20. package/.roo/commands/openspec-proposal.md +25 -0
  21. package/.vscode/mcp.json +23 -0
  22. package/AGENTS.md +18 -0
  23. package/CLAUDE.md +18 -0
  24. package/LICENSE +65 -0
  25. package/README.md +303 -0
  26. package/dist/config.d.ts +4 -0
  27. package/dist/config.d.ts.map +1 -0
  28. package/dist/config.js +27 -0
  29. package/dist/config.js.map +1 -0
  30. package/dist/elicitation/index.d.ts +17 -0
  31. package/dist/elicitation/index.d.ts.map +1 -0
  32. package/dist/elicitation/index.js +118 -0
  33. package/dist/elicitation/index.js.map +1 -0
  34. package/dist/elicitation/types.d.ts +35 -0
  35. package/dist/elicitation/types.d.ts.map +1 -0
  36. package/dist/elicitation/types.js +39 -0
  37. package/dist/elicitation/types.js.map +1 -0
  38. package/dist/index.d.ts +3 -0
  39. package/dist/index.d.ts.map +1 -0
  40. package/dist/index.js +76 -0
  41. package/dist/index.js.map +1 -0
  42. package/dist/lifecycle/index.d.ts +31 -0
  43. package/dist/lifecycle/index.d.ts.map +1 -0
  44. package/dist/lifecycle/index.js +61 -0
  45. package/dist/lifecycle/index.js.map +1 -0
  46. package/dist/logger.d.ts +21 -0
  47. package/dist/logger.d.ts.map +1 -0
  48. package/dist/logger.js +182 -0
  49. package/dist/logger.js.map +1 -0
  50. package/dist/playwright-client/index.d.ts +29 -0
  51. package/dist/playwright-client/index.d.ts.map +1 -0
  52. package/dist/playwright-client/index.js +288 -0
  53. package/dist/playwright-client/index.js.map +1 -0
  54. package/dist/playwright-client/types.d.ts +44 -0
  55. package/dist/playwright-client/types.d.ts.map +1 -0
  56. package/dist/playwright-client/types.js +49 -0
  57. package/dist/playwright-client/types.js.map +1 -0
  58. package/dist/progress/index.d.ts +39 -0
  59. package/dist/progress/index.d.ts.map +1 -0
  60. package/dist/progress/index.js +106 -0
  61. package/dist/progress/index.js.map +1 -0
  62. package/dist/progress/types.d.ts +24 -0
  63. package/dist/progress/types.d.ts.map +1 -0
  64. package/dist/progress/types.js +2 -0
  65. package/dist/progress/types.js.map +1 -0
  66. package/dist/prompts/index.d.ts +19 -0
  67. package/dist/prompts/index.d.ts.map +1 -0
  68. package/dist/prompts/index.js +207 -0
  69. package/dist/prompts/index.js.map +1 -0
  70. package/dist/prompts/loader.d.ts +20 -0
  71. package/dist/prompts/loader.d.ts.map +1 -0
  72. package/dist/prompts/loader.js +47 -0
  73. package/dist/prompts/loader.js.map +1 -0
  74. package/dist/resources/index.d.ts +27 -0
  75. package/dist/resources/index.d.ts.map +1 -0
  76. package/dist/resources/index.js +186 -0
  77. package/dist/resources/index.js.map +1 -0
  78. package/dist/resources/subscriptions.d.ts +10 -0
  79. package/dist/resources/subscriptions.d.ts.map +1 -0
  80. package/dist/resources/subscriptions.js +23 -0
  81. package/dist/resources/subscriptions.js.map +1 -0
  82. package/dist/sampling/index.d.ts +11 -0
  83. package/dist/sampling/index.d.ts.map +1 -0
  84. package/dist/sampling/index.js +201 -0
  85. package/dist/sampling/index.js.map +1 -0
  86. package/dist/sampling/prompts.d.ts +56 -0
  87. package/dist/sampling/prompts.d.ts.map +1 -0
  88. package/dist/sampling/prompts.js +124 -0
  89. package/dist/sampling/prompts.js.map +1 -0
  90. package/dist/sampling/types.d.ts +57 -0
  91. package/dist/sampling/types.d.ts.map +1 -0
  92. package/dist/sampling/types.js +2 -0
  93. package/dist/sampling/types.js.map +1 -0
  94. package/dist/schemas/config.d.ts +40 -0
  95. package/dist/schemas/config.d.ts.map +1 -0
  96. package/dist/schemas/config.js +30 -0
  97. package/dist/schemas/config.js.map +1 -0
  98. package/dist/security/index.d.ts +38 -0
  99. package/dist/security/index.d.ts.map +1 -0
  100. package/dist/security/index.js +281 -0
  101. package/dist/security/index.js.map +1 -0
  102. package/dist/server.d.ts +9 -0
  103. package/dist/server.d.ts.map +1 -0
  104. package/dist/server.js +142 -0
  105. package/dist/server.js.map +1 -0
  106. package/dist/test-utils/index.d.ts +6 -0
  107. package/dist/test-utils/index.d.ts.map +1 -0
  108. package/dist/test-utils/index.js +6 -0
  109. package/dist/test-utils/index.js.map +1 -0
  110. package/dist/test-utils/mock-context.d.ts +64 -0
  111. package/dist/test-utils/mock-context.d.ts.map +1 -0
  112. package/dist/test-utils/mock-context.js +347 -0
  113. package/dist/test-utils/mock-context.js.map +1 -0
  114. package/dist/test-utils/mock-playwright-client.d.ts +62 -0
  115. package/dist/test-utils/mock-playwright-client.d.ts.map +1 -0
  116. package/dist/test-utils/mock-playwright-client.js +315 -0
  117. package/dist/test-utils/mock-playwright-client.js.map +1 -0
  118. package/dist/tools/index.d.ts +4 -0
  119. package/dist/tools/index.d.ts.map +1 -0
  120. package/dist/tools/index.js +8 -0
  121. package/dist/tools/index.js.map +1 -0
  122. package/dist/tools/webtest/crawl.d.ts +46 -0
  123. package/dist/tools/webtest/crawl.d.ts.map +1 -0
  124. package/dist/tools/webtest/crawl.js +678 -0
  125. package/dist/tools/webtest/crawl.js.map +1 -0
  126. package/dist/tools/webtest/discover-features.d.ts +30 -0
  127. package/dist/tools/webtest/discover-features.d.ts.map +1 -0
  128. package/dist/tools/webtest/discover-features.js +343 -0
  129. package/dist/tools/webtest/discover-features.js.map +1 -0
  130. package/dist/tools/webtest/discover-flows.d.ts +29 -0
  131. package/dist/tools/webtest/discover-flows.d.ts.map +1 -0
  132. package/dist/tools/webtest/discover-flows.js +341 -0
  133. package/dist/tools/webtest/discover-flows.js.map +1 -0
  134. package/dist/tools/webtest/generate-tests.d.ts +54 -0
  135. package/dist/tools/webtest/generate-tests.d.ts.map +1 -0
  136. package/dist/tools/webtest/generate-tests.js +364 -0
  137. package/dist/tools/webtest/generate-tests.js.map +1 -0
  138. package/dist/tools/webtest/index.d.ts +8 -0
  139. package/dist/tools/webtest/index.d.ts.map +1 -0
  140. package/dist/tools/webtest/index.js +8 -0
  141. package/dist/tools/webtest/index.js.map +1 -0
  142. package/dist/tools/webtest/run-test-case.d.ts +28 -0
  143. package/dist/tools/webtest/run-test-case.d.ts.map +1 -0
  144. package/dist/tools/webtest/run-test-case.js +420 -0
  145. package/dist/tools/webtest/run-test-case.js.map +1 -0
  146. package/dist/tools/webtest/schemas.d.ts +175 -0
  147. package/dist/tools/webtest/schemas.d.ts.map +1 -0
  148. package/dist/tools/webtest/schemas.js +156 -0
  149. package/dist/tools/webtest/schemas.js.map +1 -0
  150. package/dist/tools/webtest/start-analysis.d.ts +16 -0
  151. package/dist/tools/webtest/start-analysis.d.ts.map +1 -0
  152. package/dist/tools/webtest/start-analysis.js +137 -0
  153. package/dist/tools/webtest/start-analysis.js.map +1 -0
  154. package/dist/transports/http.d.ts +8 -0
  155. package/dist/transports/http.d.ts.map +1 -0
  156. package/dist/transports/http.js +9 -0
  157. package/dist/transports/http.js.map +1 -0
  158. package/dist/transports/index.d.ts +14 -0
  159. package/dist/transports/index.d.ts.map +1 -0
  160. package/dist/transports/index.js +20 -0
  161. package/dist/transports/index.js.map +1 -0
  162. package/dist/transports/stdio.d.ts +4 -0
  163. package/dist/transports/stdio.d.ts.map +1 -0
  164. package/dist/transports/stdio.js +6 -0
  165. package/dist/transports/stdio.js.map +1 -0
  166. package/dist/types/capabilities.d.ts +18 -0
  167. package/dist/types/capabilities.d.ts.map +1 -0
  168. package/dist/types/capabilities.js +35 -0
  169. package/dist/types/capabilities.js.map +1 -0
  170. package/dist/types/context.d.ts +20 -0
  171. package/dist/types/context.d.ts.map +1 -0
  172. package/dist/types/context.js +2 -0
  173. package/dist/types/context.js.map +1 -0
  174. package/dist/types/tool.d.ts +10 -0
  175. package/dist/types/tool.d.ts.map +1 -0
  176. package/dist/types/tool.js +2 -0
  177. package/dist/types/tool.js.map +1 -0
  178. package/dist/workspace/index.d.ts +99 -0
  179. package/dist/workspace/index.d.ts.map +1 -0
  180. package/dist/workspace/index.js +648 -0
  181. package/dist/workspace/index.js.map +1 -0
  182. package/dist/workspace/markdown.d.ts +50 -0
  183. package/dist/workspace/markdown.d.ts.map +1 -0
  184. package/dist/workspace/markdown.js +210 -0
  185. package/dist/workspace/markdown.js.map +1 -0
  186. package/dist/workspace/types.d.ts +173 -0
  187. package/dist/workspace/types.d.ts.map +1 -0
  188. package/dist/workspace/types.js +2 -0
  189. package/dist/workspace/types.js.map +1 -0
  190. package/openspec/AGENTS.md +456 -0
  191. package/openspec/changes/archive/2025-12-18-add-hybrid-artifact-paths/proposal.md +33 -0
  192. package/openspec/changes/archive/2025-12-18-add-hybrid-artifact-paths/specs/webtest-resources/spec.md +27 -0
  193. package/openspec/changes/archive/2025-12-18-add-hybrid-artifact-paths/specs/webtest-tools/spec.md +304 -0
  194. package/openspec/changes/archive/2025-12-18-add-hybrid-artifact-paths/tasks.md +43 -0
  195. package/openspec/changes/archive/2025-12-18-add-mcp-server-foundation/design.md +209 -0
  196. package/openspec/changes/archive/2025-12-18-add-mcp-server-foundation/proposal.md +41 -0
  197. package/openspec/changes/archive/2025-12-18-add-mcp-server-foundation/specs/mcp-server-core/spec.md +183 -0
  198. package/openspec/changes/archive/2025-12-18-add-mcp-server-foundation/tasks.md +112 -0
  199. package/openspec/changes/archive/2025-12-18-add-webtest-orchestrator/design.md +333 -0
  200. package/openspec/changes/archive/2025-12-18-add-webtest-orchestrator/proposal.md +66 -0
  201. package/openspec/changes/archive/2025-12-18-add-webtest-orchestrator/specs/mcp-server-core/spec.md +129 -0
  202. package/openspec/changes/archive/2025-12-18-add-webtest-orchestrator/specs/webtest-lifecycle/spec.md +138 -0
  203. package/openspec/changes/archive/2025-12-18-add-webtest-orchestrator/specs/webtest-logging/spec.md +211 -0
  204. package/openspec/changes/archive/2025-12-18-add-webtest-orchestrator/specs/webtest-prompts/spec.md +157 -0
  205. package/openspec/changes/archive/2025-12-18-add-webtest-orchestrator/specs/webtest-resources/spec.md +213 -0
  206. package/openspec/changes/archive/2025-12-18-add-webtest-orchestrator/specs/webtest-sampling/spec.md +257 -0
  207. package/openspec/changes/archive/2025-12-18-add-webtest-orchestrator/specs/webtest-tools/spec.md +501 -0
  208. package/openspec/changes/archive/2025-12-18-add-webtest-orchestrator/tasks.md +264 -0
  209. package/openspec/changes/archive/2025-12-18-allow-analysis-of-incomplete-crawls/proposal.md +24 -0
  210. package/openspec/changes/archive/2025-12-18-allow-analysis-of-incomplete-crawls/specs/webtest-tools/spec.md +80 -0
  211. package/openspec/changes/archive/2025-12-18-allow-analysis-of-incomplete-crawls/tasks.md +8 -0
  212. package/openspec/changes/archive/2025-12-18-fix-crawl-loop-stability/design.md +90 -0
  213. package/openspec/changes/archive/2025-12-18-fix-crawl-loop-stability/proposal.md +28 -0
  214. package/openspec/changes/archive/2025-12-18-fix-crawl-loop-stability/specs/webtest-sampling/spec.md +90 -0
  215. package/openspec/changes/archive/2025-12-18-fix-crawl-loop-stability/tasks.md +33 -0
  216. package/openspec/changes/archive/2025-12-18-use-markdown-artifacts/design.md +558 -0
  217. package/openspec/changes/archive/2025-12-18-use-markdown-artifacts/proposal.md +119 -0
  218. package/openspec/changes/archive/2025-12-18-use-markdown-artifacts/specs/webtest-resources/spec.md +109 -0
  219. package/openspec/changes/archive/2025-12-18-use-markdown-artifacts/specs/webtest-tools/spec.md +121 -0
  220. package/openspec/changes/archive/2025-12-18-use-markdown-artifacts/tasks.md +133 -0
  221. package/openspec/changes/extract-prompts-to-markdown/design.md +86 -0
  222. package/openspec/changes/extract-prompts-to-markdown/proposal.md +50 -0
  223. package/openspec/changes/extract-prompts-to-markdown/specs/webtest-prompts/spec.md +74 -0
  224. package/openspec/changes/extract-prompts-to-markdown/tasks.md +40 -0
  225. package/openspec/changes/refactor-webtest-naming/design.md +95 -0
  226. package/openspec/changes/refactor-webtest-naming/proposal.md +66 -0
  227. package/openspec/changes/refactor-webtest-naming/specs/webtest-prompts/spec.md +79 -0
  228. package/openspec/changes/refactor-webtest-naming/specs/webtest-resources/spec.md +80 -0
  229. package/openspec/changes/refactor-webtest-naming/specs/webtest-sampling/spec.md +122 -0
  230. package/openspec/changes/refactor-webtest-naming/specs/webtest-tools/spec.md +113 -0
  231. package/openspec/changes/refactor-webtest-naming/tasks.md +119 -0
  232. package/openspec/changes/rename-package-to-retest/proposal.md +52 -0
  233. package/openspec/changes/rename-package-to-retest/specs/mcp-server-core/spec.md +53 -0
  234. package/openspec/changes/rename-package-to-retest/specs/retest-lifecycle/spec.md +68 -0
  235. package/openspec/changes/rename-package-to-retest/specs/retest-logging/spec.md +35 -0
  236. package/openspec/changes/rename-package-to-retest/specs/retest-prompts/spec.md +159 -0
  237. package/openspec/changes/rename-package-to-retest/specs/retest-resources/spec.md +251 -0
  238. package/openspec/changes/rename-package-to-retest/specs/retest-sampling/spec.md +99 -0
  239. package/openspec/changes/rename-package-to-retest/specs/retest-tools/spec.md +295 -0
  240. package/openspec/changes/rename-package-to-retest/tasks.md +71 -0
  241. package/openspec/project.md +31 -0
  242. package/openspec/specs/mcp-server-core/spec.md +178 -0
  243. package/openspec/specs/webtest-lifecycle/spec.md +136 -0
  244. package/openspec/specs/webtest-logging/spec.md +209 -0
  245. package/openspec/specs/webtest-prompts/spec.md +155 -0
  246. package/openspec/specs/webtest-resources/spec.md +248 -0
  247. package/openspec/specs/webtest-sampling/spec.md +344 -0
  248. package/openspec/specs/webtest-tools/spec.md +282 -0
  249. package/package.json +54 -0
  250. package/release.config.js +9 -0
  251. package/src/config.test.ts +96 -0
  252. package/src/config.ts +32 -0
  253. package/src/elicitation/index.test.ts +399 -0
  254. package/src/elicitation/index.ts +171 -0
  255. package/src/elicitation/types.ts +68 -0
  256. package/src/index.ts +83 -0
  257. package/src/lifecycle/index.test.ts +260 -0
  258. package/src/lifecycle/index.ts +101 -0
  259. package/src/logger.redaction.test.ts +322 -0
  260. package/src/logger.test.ts +123 -0
  261. package/src/logger.ts +229 -0
  262. package/src/playwright-client/index.ts +392 -0
  263. package/src/playwright-client/types.ts +99 -0
  264. package/src/progress/index.test.ts +327 -0
  265. package/src/progress/index.ts +170 -0
  266. package/src/progress/types.ts +25 -0
  267. package/src/prompts/index.test.ts +451 -0
  268. package/src/prompts/index.ts +246 -0
  269. package/src/prompts/loader.test.ts +100 -0
  270. package/src/prompts/loader.ts +59 -0
  271. package/src/prompts/templates/mcp/webtest-crawl.md +7 -0
  272. package/src/prompts/templates/mcp/webtest-discover-flows.md +11 -0
  273. package/src/prompts/templates/mcp/webtest-discover.md +12 -0
  274. package/src/prompts/templates/mcp/webtest-full-workflow.md +12 -0
  275. package/src/prompts/templates/mcp/webtest-generate-tests.md +11 -0
  276. package/src/prompts/templates/mcp/webtest-run-test.md +11 -0
  277. package/src/prompts/templates/mcp/webtest-start.md +8 -0
  278. package/src/prompts/templates/sampling/crawl-action.md +35 -0
  279. package/src/prompts/templates/sampling/feature-discovery.md +27 -0
  280. package/src/prompts/templates/sampling/flow-discovery.md +29 -0
  281. package/src/prompts/templates/sampling/page-content-wrapper.md +5 -0
  282. package/src/prompts/templates/sampling/system-prefix.md +12 -0
  283. package/src/prompts/templates/sampling/test-evaluation.md +17 -0
  284. package/src/prompts/templates/sampling/test-generation.md +31 -0
  285. package/src/resources/index.ts +250 -0
  286. package/src/resources/subscriptions.ts +37 -0
  287. package/src/sampling/index.test.ts +414 -0
  288. package/src/sampling/index.ts +286 -0
  289. package/src/sampling/prompts.ts +194 -0
  290. package/src/sampling/types.ts +60 -0
  291. package/src/schemas/config.ts +39 -0
  292. package/src/security/index.test.ts +441 -0
  293. package/src/security/index.ts +361 -0
  294. package/src/security/security-scenarios.test.ts +468 -0
  295. package/src/server.ts +211 -0
  296. package/src/test-utils/index.ts +6 -0
  297. package/src/test-utils/mock-context.ts +426 -0
  298. package/src/test-utils/mock-playwright-client.ts +422 -0
  299. package/src/tools/index.ts +11 -0
  300. package/src/tools/webtest/crawl.test.ts +834 -0
  301. package/src/tools/webtest/crawl.ts +901 -0
  302. package/src/tools/webtest/discover-features.ts +412 -0
  303. package/src/tools/webtest/discover-flows.ts +408 -0
  304. package/src/tools/webtest/generate-tests.test.ts +532 -0
  305. package/src/tools/webtest/generate-tests.ts +425 -0
  306. package/src/tools/webtest/index.ts +7 -0
  307. package/src/tools/webtest/integration.test.ts +536 -0
  308. package/src/tools/webtest/run-test-case.test.ts +659 -0
  309. package/src/tools/webtest/run-test-case.ts +508 -0
  310. package/src/tools/webtest/schemas.ts +201 -0
  311. package/src/tools/webtest/start-analysis.test.ts +151 -0
  312. package/src/tools/webtest/start-analysis.ts +158 -0
  313. package/src/transports/http.ts +19 -0
  314. package/src/transports/index.ts +30 -0
  315. package/src/transports/stdio.ts +7 -0
  316. package/src/types/capabilities.test.ts +193 -0
  317. package/src/types/capabilities.ts +50 -0
  318. package/src/types/context.ts +21 -0
  319. package/src/types/tool.ts +11 -0
  320. package/src/workspace/index.ts +945 -0
  321. package/src/workspace/markdown.ts +272 -0
  322. package/src/workspace/types.ts +186 -0
  323. package/tests/integration/server.test.ts +89 -0
  324. package/tests/integration/tools.test.ts +99 -0
  325. package/tsconfig.json +20 -0
  326. package/vitest.config.ts +9 -0
  327. package/vitest.integration.config.ts +10 -0
@@ -0,0 +1,412 @@
1
+ import { z } from "zod";
2
+ import type { McpTool, ToolResult } from "../../types/tool.js";
3
+ import type { ServerContext } from "../../types/context.js";
4
+ import type { WorkspaceManager, Feature } from "../../workspace/index.js";
5
+ import type { SamplingClient } from "../../sampling/index.js";
6
+ import type { ResourceManager } from "../../resources/index.js";
7
+ import { buildFeatureDiscoveryPrompt } from "../../sampling/prompts.js";
8
+ import { FeaturesDiscoverySchema, AnalysisIdSchema, CrawlIdSchema, type FeaturesDiscovery } from "./schemas.js";
9
+
10
+ export const discoverFeaturesInputSchema = z.object({
11
+ analysisId: AnalysisIdSchema,
12
+ crawlId: CrawlIdSchema
13
+ .optional()
14
+ .describe("Specific crawl to analyze. If not provided, uses the most recent crawl with captured pages."),
15
+ manualDiscovery: z
16
+ .object({
17
+ appPurpose: z.string(),
18
+ appType: z.string(),
19
+ features: z.array(z.object({
20
+ slug: z.string(),
21
+ name: z.string(),
22
+ description: z.string(),
23
+ entities: z.array(z.string()),
24
+ entryPoints: z.array(z.string()),
25
+ })),
26
+ securityObservations: z.array(z.string()).optional(),
27
+ accessibilityObservations: z.array(z.string()).optional(),
28
+ })
29
+ .optional()
30
+ .describe("Manual discovery result when sampling is unavailable. Provide this after executing the returned prompt."),
31
+ });
32
+
33
+ export type DiscoverFeaturesInput = z.infer<typeof discoverFeaturesInputSchema>;
34
+
35
+ export function createDiscoverFeaturesTool(
36
+ getContext: () => ServerContext & {
37
+ workspaceManager: WorkspaceManager;
38
+ samplingClient: SamplingClient;
39
+ resourceManager: ResourceManager;
40
+ }
41
+ ): McpTool<DiscoverFeaturesInput> {
42
+ return {
43
+ name: "webtest_discover_features",
44
+ description: `Discover application features/modules from crawl data.
45
+
46
+ This tool uses AI to identify distinct features of the web application:
47
+ - Identifies the application purpose and type
48
+ - Discovers distinct features/modules (e.g., Authentication, Shopping Cart, Search)
49
+ - Lists entities and entry points for each feature
50
+ - Notes security and accessibility observations
51
+
52
+ Outputs features.md at workspace root. After discovering features, use webtest_discover_flows to identify user flows within each feature.
53
+
54
+ Requires a crawl with captured pages. If sampling is unavailable, returns a prompt for manual execution.`,
55
+
56
+ inputSchema: discoverFeaturesInputSchema,
57
+
58
+ async handler(input: DiscoverFeaturesInput): Promise<ToolResult> {
59
+ const ctx = getContext();
60
+ const { logger, workspaceManager, samplingClient, resourceManager } = ctx;
61
+
62
+ const discoveryLogger = logger.withCorrelation({
63
+ analysisId: input.analysisId,
64
+ });
65
+
66
+ discoveryLogger.info("Starting feature discovery", { crawlId: input.crawlId });
67
+
68
+ try {
69
+ // Validate workspace exists
70
+ if (!(await workspaceManager.workspaceExists(input.analysisId))) {
71
+ return {
72
+ content: [
73
+ {
74
+ type: "text",
75
+ text: `Error: Analysis workspace "${input.analysisId}" not found.`,
76
+ },
77
+ ],
78
+ isError: true,
79
+ };
80
+ }
81
+
82
+ const workspace = await workspaceManager.readWorkspaceIndex(input.analysisId);
83
+
84
+ // Find the crawl to analyze
85
+ let targetCrawlId = input.crawlId;
86
+ let selectedCrawlRef: (typeof workspace.crawls)[0] | undefined;
87
+
88
+ if (!targetCrawlId) {
89
+ // Sort crawls by most recent first
90
+ const sortedCrawls = [...workspace.crawls].sort(
91
+ (a, b) =>
92
+ new Date(b.completedAt || b.startedAt).getTime() -
93
+ new Date(a.completedAt || a.startedAt).getTime()
94
+ );
95
+
96
+ // First try crawls with pagesVisited > 0 in workspace index
97
+ const crawlsWithData = sortedCrawls.filter((c) => c.pagesVisited > 0);
98
+
99
+ if (crawlsWithData.length > 0) {
100
+ selectedCrawlRef = crawlsWithData[0];
101
+ targetCrawlId = selectedCrawlRef.crawlId;
102
+ } else if (sortedCrawls.length > 0) {
103
+ // Fallback: check actual crawl indexes for pages (workspace index may be stale)
104
+ for (const crawlRef of sortedCrawls) {
105
+ try {
106
+ const crawlIndex = await workspaceManager.readCrawlIndex(
107
+ input.analysisId,
108
+ crawlRef.crawlId
109
+ );
110
+ if (crawlIndex.pages.length > 0) {
111
+ selectedCrawlRef = crawlRef;
112
+ targetCrawlId = crawlRef.crawlId;
113
+ discoveryLogger.info("Found crawl with data via index check", {
114
+ crawlId: crawlRef.crawlId,
115
+ pagesInIndex: crawlIndex.pages.length,
116
+ pagesInWorkspace: crawlRef.pagesVisited,
117
+ });
118
+ break;
119
+ }
120
+ } catch {
121
+ // Crawl index not readable, skip
122
+ }
123
+ }
124
+ }
125
+
126
+ if (!targetCrawlId) {
127
+ return {
128
+ content: [
129
+ {
130
+ type: "text",
131
+ text: "Error: No crawls with captured data found. Run webtest_crawl_app first.",
132
+ },
133
+ ],
134
+ isError: true,
135
+ };
136
+ }
137
+ } else {
138
+ selectedCrawlRef = workspace.crawls.find((c) => c.crawlId === targetCrawlId);
139
+ }
140
+
141
+ // Load crawl data
142
+ const crawlIndex = await workspaceManager.readCrawlIndex(
143
+ input.analysisId,
144
+ targetCrawlId
145
+ );
146
+
147
+ if (crawlIndex.pages.length === 0) {
148
+ return {
149
+ content: [
150
+ {
151
+ type: "text",
152
+ text: "Error: Crawl has no captured pages. The crawl may have failed.",
153
+ },
154
+ ],
155
+ isError: true,
156
+ };
157
+ }
158
+
159
+ discoveryLogger.info("Analyzing crawl data for features", {
160
+ crawlId: targetCrawlId,
161
+ pageCount: crawlIndex.pages.length,
162
+ });
163
+
164
+ // Build crawl summary
165
+ const crawlSummary = `
166
+ Target URL: ${workspace.url}
167
+ Domain: ${workspace.domain}
168
+ Focus: ${workspace.focus || "General exploration"}
169
+ Goal: ${crawlIndex.goal}
170
+ Strategy: ${crawlIndex.strategy}
171
+ Pages visited: ${crawlIndex.pages.length}
172
+ Actions taken: ${crawlIndex.actionHistory.length}
173
+ Status: ${crawlIndex.status}
174
+ `;
175
+
176
+ // Load page snapshots (limited to avoid token limits)
177
+ const pageSnapshots: Array<{ url: string; content: string }> = [];
178
+
179
+ for (const page of crawlIndex.pages.slice(0, 10)) {
180
+ try {
181
+ const snapshotContent = await resourceManager.readResource(
182
+ page.snapshotUri
183
+ );
184
+ if (snapshotContent.text) {
185
+ const snapshot = JSON.parse(snapshotContent.text);
186
+ pageSnapshots.push({
187
+ url: page.url,
188
+ content: snapshot.content || "",
189
+ });
190
+ }
191
+ } catch (error) {
192
+ discoveryLogger.warn("Failed to load page snapshot", {
193
+ pageId: page.pageId,
194
+ error: error instanceof Error ? error.message : "Unknown error",
195
+ });
196
+ }
197
+ }
198
+
199
+ // Determine discovery source: manual input, sampling, or fallback prompt
200
+ let discovery: FeaturesDiscovery;
201
+
202
+ if (input.manualDiscovery) {
203
+ // Use manually provided discovery (from fallback mode)
204
+ discoveryLogger.info("Using manual discovery input");
205
+ discovery = input.manualDiscovery as FeaturesDiscovery;
206
+ } else if (!samplingClient.hasSampling()) {
207
+ // No sampling available and no manual input - return prompt for manual execution
208
+ const prompt = buildFeatureDiscoveryPrompt({
209
+ crawlSummary,
210
+ pageSnapshots,
211
+ });
212
+
213
+ return {
214
+ content: [
215
+ {
216
+ type: "text",
217
+ text: JSON.stringify(
218
+ {
219
+ needsManualInput: true,
220
+ analysisId: input.analysisId,
221
+ crawlId: targetCrawlId,
222
+ prompt,
223
+ expectedResponseSchema: FeaturesDiscoverySchema._def,
224
+ instructions:
225
+ "Execute this prompt with your LLM, then call webtest_discover_features again with the result in the 'manualDiscovery' parameter",
226
+ },
227
+ null,
228
+ 2
229
+ ),
230
+ },
231
+ ],
232
+ };
233
+ } else {
234
+ // Request discovery via sampling
235
+ const samplingResult = await samplingClient.createMessage({
236
+ systemPrompt: `You are analyzing a web application to identify its distinct features and modules.
237
+ Each feature should be a cohesive capability that users interact with (e.g., Authentication, Shopping Cart, Search).
238
+ Provide thorough feature identification that will help in discovering user flows within each feature.`,
239
+ userPrompt: buildFeatureDiscoveryPrompt({
240
+ crawlSummary,
241
+ pageSnapshots,
242
+ }),
243
+ schema: FeaturesDiscoverySchema,
244
+ maxTokens: 4096,
245
+ });
246
+
247
+ if (!samplingResult.success || !samplingResult.data) {
248
+ discoveryLogger.error("Feature discovery sampling failed", {
249
+ error: samplingResult.error,
250
+ });
251
+
252
+ return {
253
+ content: [
254
+ {
255
+ type: "text",
256
+ text: `Error during feature discovery: ${samplingResult.error}`,
257
+ },
258
+ ],
259
+ isError: true,
260
+ };
261
+ }
262
+
263
+ discovery = samplingResult.data;
264
+ }
265
+
266
+ // Generate markdown report
267
+ const markdownReport = generateFeaturesMarkdown(discovery, workspace, crawlIndex);
268
+
269
+ // Build frontmatter with features
270
+ const frontmatter = {
271
+ appPurpose: discovery.appPurpose,
272
+ appType: discovery.appType,
273
+ features: discovery.features,
274
+ securityObservations: discovery.securityObservations,
275
+ accessibilityObservations: discovery.accessibilityObservations,
276
+ };
277
+
278
+ // Save features
279
+ const { featuresFilePath, featuresUri, featureCount } =
280
+ await workspaceManager.saveFeatures(input.analysisId, {
281
+ markdown: markdownReport,
282
+ frontmatter: frontmatter as { features: Feature[] },
283
+ });
284
+
285
+ await resourceManager.notifyListChanged();
286
+
287
+ // Build warning if crawl was incomplete
288
+ const crawlStatus = selectedCrawlRef?.status;
289
+ const isIncompleteCrawl = crawlStatus && crawlStatus !== "completed";
290
+ const warning = isIncompleteCrawl
291
+ ? `Note: Discovery based on incomplete crawl (status: ${crawlStatus}, pages: ${selectedCrawlRef?.pagesVisited || crawlIndex.pages.length}). Results may be partial.`
292
+ : undefined;
293
+
294
+ // Build next steps suggesting to discover flows for each feature
295
+ const nextSteps = discovery.features.map(
296
+ (f) => `Use webtest_discover_flows with analysisId="${input.analysisId}" featureSlug="${f.slug}" to discover flows for "${f.name}"`
297
+ );
298
+ nextSteps.push(`Review discovered features at ${featuresUri}`);
299
+
300
+ const result = {
301
+ analysisId: input.analysisId,
302
+ crawlId: targetCrawlId,
303
+ ...(warning && { warning }),
304
+ featuresFilePath,
305
+ featuresUri,
306
+ summary: {
307
+ appPurpose: discovery.appPurpose,
308
+ appType: discovery.appType,
309
+ featureCount,
310
+ features: discovery.features.map((f) => ({
311
+ slug: f.slug,
312
+ name: f.name,
313
+ })),
314
+ crawlStatus: crawlStatus || "unknown",
315
+ },
316
+ nextSteps,
317
+ };
318
+
319
+ discoveryLogger.info("Feature discovery completed", {
320
+ featureCount,
321
+ features: discovery.features.map((f) => f.slug),
322
+ });
323
+
324
+ return {
325
+ content: [
326
+ {
327
+ type: "text",
328
+ text: JSON.stringify(result, null, 2),
329
+ },
330
+ ],
331
+ };
332
+ } catch (error) {
333
+ const message = error instanceof Error ? error.message : "Unknown error";
334
+ discoveryLogger.error("Feature discovery failed", { error: message });
335
+
336
+ return {
337
+ content: [
338
+ {
339
+ type: "text",
340
+ text: `Error during feature discovery: ${message}`,
341
+ },
342
+ ],
343
+ isError: true,
344
+ };
345
+ }
346
+ },
347
+ };
348
+ }
349
+
350
+ function generateFeaturesMarkdown(
351
+ discovery: FeaturesDiscovery,
352
+ workspace: { url: string; domain: string; focus?: string },
353
+ crawl: { goal: string; pages: { url: string }[] }
354
+ ): string {
355
+ let md = `# Application Features: ${workspace.domain}
356
+
357
+ ## Overview
358
+
359
+ - **URL**: ${workspace.url}
360
+ - **Purpose**: ${discovery.appPurpose}
361
+ - **Type**: ${discovery.appType}
362
+ - **Focus**: ${workspace.focus || "General"}
363
+ - **Crawl Goal**: ${crawl.goal}
364
+ - **Pages Analyzed**: ${crawl.pages.length}
365
+
366
+ ## Discovered Features
367
+
368
+ `;
369
+
370
+ for (const feature of discovery.features) {
371
+ md += `### ${feature.name}
372
+
373
+ **Slug**: \`${feature.slug}\`
374
+
375
+ ${feature.description}
376
+
377
+ **Entities**: ${feature.entities.length > 0 ? feature.entities.join(", ") : "None identified"}
378
+
379
+ **Entry Points**:
380
+ `;
381
+ for (const ep of feature.entryPoints) {
382
+ md += `- ${ep}\n`;
383
+ }
384
+ md += "\n";
385
+ }
386
+
387
+ if (discovery.securityObservations && discovery.securityObservations.length > 0) {
388
+ md += `## Security Observations
389
+
390
+ `;
391
+ for (const obs of discovery.securityObservations) {
392
+ md += `- ${obs}\n`;
393
+ }
394
+ md += "\n";
395
+ }
396
+
397
+ if (discovery.accessibilityObservations && discovery.accessibilityObservations.length > 0) {
398
+ md += `## Accessibility Observations
399
+
400
+ `;
401
+ for (const obs of discovery.accessibilityObservations) {
402
+ md += `- ${obs}\n`;
403
+ }
404
+ md += "\n";
405
+ }
406
+
407
+ md += `---
408
+ *Generated by testing-mcp*
409
+ `;
410
+
411
+ return md;
412
+ }