retestkit 1.4.1 → 1.5.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 (238) hide show
  1. package/README.md +59 -40
  2. package/dist/config.js +8 -8
  3. package/dist/config.js.map +1 -1
  4. package/dist/logger.js +1 -1
  5. package/dist/logger.js.map +1 -1
  6. package/dist/prompts/index.d.ts +1 -1
  7. package/dist/prompts/index.d.ts.map +1 -1
  8. package/dist/prompts/index.js +21 -21
  9. package/dist/prompts/index.js.map +1 -1
  10. package/dist/prompts/templates/mcp/retest-crawl.md +7 -0
  11. package/{src/prompts/templates/mcp/webtest-discover-flows.md → dist/prompts/templates/mcp/retest-discover-flows.md} +1 -1
  12. package/{src/prompts/templates/mcp/webtest-discover.md → dist/prompts/templates/mcp/retest-discover.md} +2 -2
  13. package/dist/prompts/templates/mcp/retest-full-workflow.md +12 -0
  14. package/{src/prompts/templates/mcp/webtest-generate-tests.md → dist/prompts/templates/mcp/retest-generate-tests.md} +1 -1
  15. package/{src/prompts/templates/mcp/webtest-run-test.md → dist/prompts/templates/mcp/retest-run-test.md} +1 -1
  16. package/{src/prompts/templates/mcp/webtest-start.md → dist/prompts/templates/mcp/retest-start.md} +1 -1
  17. package/{src → dist}/prompts/templates/sampling/system-prefix.md +1 -1
  18. package/dist/resources/index.js +7 -7
  19. package/dist/resources/index.js.map +1 -1
  20. package/dist/schemas/config.js +2 -2
  21. package/dist/schemas/config.js.map +1 -1
  22. package/dist/security/index.js +1 -1
  23. package/dist/security/index.js.map +1 -1
  24. package/dist/server.js +3 -3
  25. package/dist/server.js.map +1 -1
  26. package/dist/test-utils/mock-context.js +22 -22
  27. package/dist/test-utils/mock-context.js.map +1 -1
  28. package/dist/tools/index.d.ts +1 -1
  29. package/dist/tools/index.d.ts.map +1 -1
  30. package/dist/tools/index.js +5 -5
  31. package/dist/tools/index.js.map +1 -1
  32. package/dist/tools/retest/crawl.d.ts.map +1 -0
  33. package/dist/tools/{webtest → retest}/crawl.js +7 -7
  34. package/dist/tools/retest/crawl.js.map +1 -0
  35. package/dist/tools/retest/discover-features.d.ts.map +1 -0
  36. package/dist/tools/{webtest → retest}/discover-features.js +6 -6
  37. package/dist/tools/retest/discover-features.js.map +1 -0
  38. package/dist/tools/retest/discover-flows.d.ts.map +1 -0
  39. package/dist/tools/{webtest → retest}/discover-flows.js +6 -6
  40. package/dist/tools/retest/discover-flows.js.map +1 -0
  41. package/dist/tools/retest/generate-tests.d.ts.map +1 -0
  42. package/dist/tools/{webtest → retest}/generate-tests.js +5 -5
  43. package/dist/tools/retest/generate-tests.js.map +1 -0
  44. package/dist/tools/retest/index.d.ts.map +1 -0
  45. package/dist/tools/retest/index.js.map +1 -0
  46. package/dist/tools/retest/run-test-case.d.ts.map +1 -0
  47. package/dist/tools/{webtest → retest}/run-test-case.js +3 -3
  48. package/dist/tools/retest/run-test-case.js.map +1 -0
  49. package/dist/tools/retest/schemas.d.ts.map +1 -0
  50. package/dist/tools/retest/schemas.js.map +1 -0
  51. package/dist/tools/retest/start-analysis.d.ts.map +1 -0
  52. package/dist/tools/{webtest → retest}/start-analysis.js +5 -5
  53. package/dist/tools/retest/start-analysis.js.map +1 -0
  54. package/dist/workspace/index.js +8 -8
  55. package/dist/workspace/index.js.map +1 -1
  56. package/dist/workspace/types.d.ts +2 -2
  57. package/dist/workspace/types.d.ts.map +1 -1
  58. package/package.json +6 -2
  59. package/.claude/commands/openspec/apply.md +0 -23
  60. package/.claude/commands/openspec/archive.md +0 -27
  61. package/.claude/commands/openspec/proposal.md +0 -28
  62. package/.gemini/commands/openspec/apply.toml +0 -21
  63. package/.gemini/commands/openspec/archive.toml +0 -25
  64. package/.gemini/commands/openspec/proposal.toml +0 -26
  65. package/.github/prompts/openspec-apply.prompt.md +0 -22
  66. package/.github/prompts/openspec-archive.prompt.md +0 -26
  67. package/.github/prompts/openspec-proposal.prompt.md +0 -27
  68. package/.github/workflows/release.yml +0 -33
  69. package/.kilocode/workflows/openspec-apply.md +0 -17
  70. package/.kilocode/workflows/openspec-archive.md +0 -21
  71. package/.kilocode/workflows/openspec-proposal.md +0 -22
  72. package/.mcp.json +0 -23
  73. package/.opencode/command/openspec-apply.md +0 -25
  74. package/.opencode/command/openspec-archive.md +0 -28
  75. package/.opencode/command/openspec-proposal.md +0 -30
  76. package/.roo/commands/openspec-apply.md +0 -20
  77. package/.roo/commands/openspec-archive.md +0 -24
  78. package/.roo/commands/openspec-proposal.md +0 -25
  79. package/.vscode/mcp.json +0 -23
  80. package/AGENTS.md +0 -18
  81. package/CLAUDE.md +0 -18
  82. package/dist/tools/webtest/crawl.d.ts.map +0 -1
  83. package/dist/tools/webtest/crawl.js.map +0 -1
  84. package/dist/tools/webtest/discover-features.d.ts.map +0 -1
  85. package/dist/tools/webtest/discover-features.js.map +0 -1
  86. package/dist/tools/webtest/discover-flows.d.ts.map +0 -1
  87. package/dist/tools/webtest/discover-flows.js.map +0 -1
  88. package/dist/tools/webtest/generate-tests.d.ts.map +0 -1
  89. package/dist/tools/webtest/generate-tests.js.map +0 -1
  90. package/dist/tools/webtest/index.d.ts.map +0 -1
  91. package/dist/tools/webtest/index.js.map +0 -1
  92. package/dist/tools/webtest/run-test-case.d.ts.map +0 -1
  93. package/dist/tools/webtest/run-test-case.js.map +0 -1
  94. package/dist/tools/webtest/schemas.d.ts.map +0 -1
  95. package/dist/tools/webtest/schemas.js.map +0 -1
  96. package/dist/tools/webtest/start-analysis.d.ts.map +0 -1
  97. package/dist/tools/webtest/start-analysis.js.map +0 -1
  98. package/openspec/AGENTS.md +0 -456
  99. package/openspec/changes/archive/2025-12-18-add-hybrid-artifact-paths/proposal.md +0 -33
  100. package/openspec/changes/archive/2025-12-18-add-hybrid-artifact-paths/specs/webtest-resources/spec.md +0 -27
  101. package/openspec/changes/archive/2025-12-18-add-hybrid-artifact-paths/specs/webtest-tools/spec.md +0 -304
  102. package/openspec/changes/archive/2025-12-18-add-hybrid-artifact-paths/tasks.md +0 -43
  103. package/openspec/changes/archive/2025-12-18-add-mcp-server-foundation/design.md +0 -209
  104. package/openspec/changes/archive/2025-12-18-add-mcp-server-foundation/proposal.md +0 -41
  105. package/openspec/changes/archive/2025-12-18-add-mcp-server-foundation/specs/mcp-server-core/spec.md +0 -183
  106. package/openspec/changes/archive/2025-12-18-add-mcp-server-foundation/tasks.md +0 -112
  107. package/openspec/changes/archive/2025-12-18-add-webtest-orchestrator/design.md +0 -333
  108. package/openspec/changes/archive/2025-12-18-add-webtest-orchestrator/proposal.md +0 -66
  109. package/openspec/changes/archive/2025-12-18-add-webtest-orchestrator/specs/mcp-server-core/spec.md +0 -129
  110. package/openspec/changes/archive/2025-12-18-add-webtest-orchestrator/specs/webtest-lifecycle/spec.md +0 -138
  111. package/openspec/changes/archive/2025-12-18-add-webtest-orchestrator/specs/webtest-logging/spec.md +0 -211
  112. package/openspec/changes/archive/2025-12-18-add-webtest-orchestrator/specs/webtest-prompts/spec.md +0 -157
  113. package/openspec/changes/archive/2025-12-18-add-webtest-orchestrator/specs/webtest-resources/spec.md +0 -213
  114. package/openspec/changes/archive/2025-12-18-add-webtest-orchestrator/specs/webtest-sampling/spec.md +0 -257
  115. package/openspec/changes/archive/2025-12-18-add-webtest-orchestrator/specs/webtest-tools/spec.md +0 -501
  116. package/openspec/changes/archive/2025-12-18-add-webtest-orchestrator/tasks.md +0 -264
  117. package/openspec/changes/archive/2025-12-18-allow-analysis-of-incomplete-crawls/proposal.md +0 -24
  118. package/openspec/changes/archive/2025-12-18-allow-analysis-of-incomplete-crawls/specs/webtest-tools/spec.md +0 -80
  119. package/openspec/changes/archive/2025-12-18-allow-analysis-of-incomplete-crawls/tasks.md +0 -8
  120. package/openspec/changes/archive/2025-12-18-fix-crawl-loop-stability/design.md +0 -90
  121. package/openspec/changes/archive/2025-12-18-fix-crawl-loop-stability/proposal.md +0 -28
  122. package/openspec/changes/archive/2025-12-18-fix-crawl-loop-stability/specs/webtest-sampling/spec.md +0 -90
  123. package/openspec/changes/archive/2025-12-18-fix-crawl-loop-stability/tasks.md +0 -33
  124. package/openspec/changes/archive/2025-12-18-use-markdown-artifacts/design.md +0 -558
  125. package/openspec/changes/archive/2025-12-18-use-markdown-artifacts/proposal.md +0 -119
  126. package/openspec/changes/archive/2025-12-18-use-markdown-artifacts/specs/webtest-resources/spec.md +0 -109
  127. package/openspec/changes/archive/2025-12-18-use-markdown-artifacts/specs/webtest-tools/spec.md +0 -121
  128. package/openspec/changes/archive/2025-12-18-use-markdown-artifacts/tasks.md +0 -133
  129. package/openspec/changes/extract-prompts-to-markdown/design.md +0 -86
  130. package/openspec/changes/extract-prompts-to-markdown/proposal.md +0 -50
  131. package/openspec/changes/extract-prompts-to-markdown/specs/webtest-prompts/spec.md +0 -74
  132. package/openspec/changes/extract-prompts-to-markdown/tasks.md +0 -40
  133. package/openspec/changes/refactor-webtest-naming/design.md +0 -95
  134. package/openspec/changes/refactor-webtest-naming/proposal.md +0 -66
  135. package/openspec/changes/refactor-webtest-naming/specs/webtest-prompts/spec.md +0 -79
  136. package/openspec/changes/refactor-webtest-naming/specs/webtest-resources/spec.md +0 -80
  137. package/openspec/changes/refactor-webtest-naming/specs/webtest-sampling/spec.md +0 -122
  138. package/openspec/changes/refactor-webtest-naming/specs/webtest-tools/spec.md +0 -113
  139. package/openspec/changes/refactor-webtest-naming/tasks.md +0 -119
  140. package/openspec/changes/rename-package-to-retest/proposal.md +0 -52
  141. package/openspec/changes/rename-package-to-retest/specs/mcp-server-core/spec.md +0 -53
  142. package/openspec/changes/rename-package-to-retest/specs/retest-lifecycle/spec.md +0 -68
  143. package/openspec/changes/rename-package-to-retest/specs/retest-logging/spec.md +0 -35
  144. package/openspec/changes/rename-package-to-retest/specs/retest-prompts/spec.md +0 -159
  145. package/openspec/changes/rename-package-to-retest/specs/retest-resources/spec.md +0 -251
  146. package/openspec/changes/rename-package-to-retest/specs/retest-sampling/spec.md +0 -99
  147. package/openspec/changes/rename-package-to-retest/specs/retest-tools/spec.md +0 -295
  148. package/openspec/changes/rename-package-to-retest/tasks.md +0 -71
  149. package/openspec/project.md +0 -31
  150. package/openspec/specs/mcp-server-core/spec.md +0 -178
  151. package/openspec/specs/webtest-lifecycle/spec.md +0 -136
  152. package/openspec/specs/webtest-logging/spec.md +0 -209
  153. package/openspec/specs/webtest-prompts/spec.md +0 -155
  154. package/openspec/specs/webtest-resources/spec.md +0 -248
  155. package/openspec/specs/webtest-sampling/spec.md +0 -344
  156. package/openspec/specs/webtest-tools/spec.md +0 -282
  157. package/release.config.js +0 -9
  158. package/src/config.test.ts +0 -96
  159. package/src/config.ts +0 -32
  160. package/src/elicitation/index.test.ts +0 -399
  161. package/src/elicitation/index.ts +0 -171
  162. package/src/elicitation/types.ts +0 -68
  163. package/src/index.ts +0 -83
  164. package/src/lifecycle/index.test.ts +0 -260
  165. package/src/lifecycle/index.ts +0 -101
  166. package/src/logger.redaction.test.ts +0 -322
  167. package/src/logger.test.ts +0 -123
  168. package/src/logger.ts +0 -229
  169. package/src/playwright-client/index.ts +0 -392
  170. package/src/playwright-client/types.ts +0 -99
  171. package/src/progress/index.test.ts +0 -327
  172. package/src/progress/index.ts +0 -170
  173. package/src/progress/types.ts +0 -25
  174. package/src/prompts/index.test.ts +0 -451
  175. package/src/prompts/index.ts +0 -246
  176. package/src/prompts/loader.test.ts +0 -100
  177. package/src/prompts/loader.ts +0 -59
  178. package/src/prompts/templates/mcp/webtest-crawl.md +0 -7
  179. package/src/prompts/templates/mcp/webtest-full-workflow.md +0 -12
  180. package/src/resources/index.ts +0 -250
  181. package/src/resources/subscriptions.ts +0 -37
  182. package/src/sampling/index.test.ts +0 -414
  183. package/src/sampling/index.ts +0 -286
  184. package/src/sampling/prompts.ts +0 -194
  185. package/src/sampling/types.ts +0 -60
  186. package/src/schemas/config.ts +0 -39
  187. package/src/security/index.test.ts +0 -441
  188. package/src/security/index.ts +0 -361
  189. package/src/security/security-scenarios.test.ts +0 -468
  190. package/src/server.ts +0 -211
  191. package/src/test-utils/index.ts +0 -6
  192. package/src/test-utils/mock-context.ts +0 -426
  193. package/src/test-utils/mock-playwright-client.ts +0 -422
  194. package/src/tools/index.ts +0 -11
  195. package/src/tools/webtest/crawl.test.ts +0 -834
  196. package/src/tools/webtest/crawl.ts +0 -901
  197. package/src/tools/webtest/discover-features.ts +0 -412
  198. package/src/tools/webtest/discover-flows.ts +0 -408
  199. package/src/tools/webtest/generate-tests.test.ts +0 -532
  200. package/src/tools/webtest/generate-tests.ts +0 -425
  201. package/src/tools/webtest/index.ts +0 -7
  202. package/src/tools/webtest/integration.test.ts +0 -536
  203. package/src/tools/webtest/run-test-case.test.ts +0 -659
  204. package/src/tools/webtest/run-test-case.ts +0 -508
  205. package/src/tools/webtest/schemas.ts +0 -201
  206. package/src/tools/webtest/start-analysis.test.ts +0 -151
  207. package/src/tools/webtest/start-analysis.ts +0 -158
  208. package/src/transports/http.ts +0 -19
  209. package/src/transports/index.ts +0 -30
  210. package/src/transports/stdio.ts +0 -7
  211. package/src/types/capabilities.test.ts +0 -193
  212. package/src/types/capabilities.ts +0 -50
  213. package/src/types/context.ts +0 -21
  214. package/src/types/tool.ts +0 -11
  215. package/src/workspace/index.ts +0 -945
  216. package/src/workspace/markdown.ts +0 -272
  217. package/src/workspace/types.ts +0 -186
  218. package/tests/integration/server.test.ts +0 -89
  219. package/tests/integration/tools.test.ts +0 -99
  220. package/tsconfig.json +0 -20
  221. package/vitest.config.ts +0 -9
  222. package/vitest.integration.config.ts +0 -10
  223. /package/{src → dist}/prompts/templates/sampling/crawl-action.md +0 -0
  224. /package/{src → dist}/prompts/templates/sampling/feature-discovery.md +0 -0
  225. /package/{src → dist}/prompts/templates/sampling/flow-discovery.md +0 -0
  226. /package/{src → dist}/prompts/templates/sampling/page-content-wrapper.md +0 -0
  227. /package/{src → dist}/prompts/templates/sampling/test-evaluation.md +0 -0
  228. /package/{src → dist}/prompts/templates/sampling/test-generation.md +0 -0
  229. /package/dist/tools/{webtest → retest}/crawl.d.ts +0 -0
  230. /package/dist/tools/{webtest → retest}/discover-features.d.ts +0 -0
  231. /package/dist/tools/{webtest → retest}/discover-flows.d.ts +0 -0
  232. /package/dist/tools/{webtest → retest}/generate-tests.d.ts +0 -0
  233. /package/dist/tools/{webtest → retest}/index.d.ts +0 -0
  234. /package/dist/tools/{webtest → retest}/index.js +0 -0
  235. /package/dist/tools/{webtest → retest}/run-test-case.d.ts +0 -0
  236. /package/dist/tools/{webtest → retest}/schemas.d.ts +0 -0
  237. /package/dist/tools/{webtest → retest}/schemas.js +0 -0
  238. /package/dist/tools/{webtest → retest}/start-analysis.d.ts +0 -0
@@ -1,901 +0,0 @@
1
- import { z } from "zod";
2
- import { join } from "node:path";
3
- import type { McpTool, ToolResult } from "../../types/tool.js";
4
- import type { ServerContext } from "../../types/context.js";
5
- import type { WorkspaceManager, CrawlCheckpoint, CrawlIndex } from "../../workspace/index.js";
6
- import type { PlaywrightClient } from "../../playwright-client/index.js";
7
- import type { SamplingClient } from "../../sampling/index.js";
8
- import type { ElicitationClient, ElicitationType } from "../../elicitation/index.js";
9
- import type { CancellationRegistry, ProgressEmitter } from "../../progress/index.js";
10
- import type { SecurityValidator } from "../../security/index.js";
11
- import type { ResourceManager } from "../../resources/index.js";
12
- import { CancellationError, calculateBudgetStatus } from "../../progress/index.js";
13
- import { buildCrawlActionPrompt } from "../../sampling/prompts.js";
14
- import { CrawlActionSchema, AnalysisIdSchema, type CrawlAction } from "./schemas.js";
15
- import { createDomSignature } from "../../security/index.js";
16
-
17
- export const crawlInputSchema = z.object({
18
- analysisId: AnalysisIdSchema,
19
- goal: z.string().min(1, "Goal is required").describe(
20
- "What you want to discover or test (e.g., 'Find all navigation paths', 'Explore the checkout flow')"
21
- ),
22
- strategy: z
23
- .enum(["breadth_first", "depth_first", "goal_directed"])
24
- .default("goal_directed")
25
- .describe("Crawling strategy"),
26
- limits: z
27
- .object({
28
- maxSteps: z.number().int().min(1).max(1000).optional(),
29
- maxMinutes: z.number().int().min(1).max(180).optional(),
30
- maxPages: z.number().int().min(1).max(100).optional(),
31
- })
32
- .optional()
33
- .describe("Override workspace limits for this crawl"),
34
- artifacts: z
35
- .object({
36
- captureScreenshots: z.boolean().default(true),
37
- captureSnapshots: z.boolean().default(true),
38
- captureDom: z.boolean().default(true),
39
- })
40
- .optional()
41
- .describe("What artifacts to capture"),
42
- resume: z
43
- .boolean()
44
- .default(false)
45
- .describe("Resume from the last checkpoint if available"),
46
- manualNextActions: z
47
- .array(
48
- z.object({
49
- tool: z.string(),
50
- args: z.record(z.string(), z.any()),
51
- })
52
- )
53
- .optional()
54
- .describe("Manual actions to execute (for fallback mode when sampling unavailable)"),
55
- });
56
-
57
- export type CrawlInput = z.infer<typeof crawlInputSchema>;
58
-
59
- interface CrawlState {
60
- step: number;
61
- currentUrl: string;
62
- startUrl: string;
63
- visitedUrls: Set<string>;
64
- actionHistory: string[];
65
- goalProgress: string;
66
- loopDetection: {
67
- domSignatures: Map<string, number>;
68
- urlVisits: Map<string, number>;
69
- recentActions: string[];
70
- };
71
- /** Track if navigation to start URL was blocked (for prompt feedback) */
72
- navigationBlocked?: {
73
- url: string;
74
- reason: string;
75
- };
76
- }
77
-
78
- export function createCrawlTool(
79
- getContext: () => ServerContext & {
80
- workspaceManager: WorkspaceManager;
81
- playwrightClient: PlaywrightClient;
82
- samplingClient: SamplingClient;
83
- elicitationClient: ElicitationClient;
84
- cancellationRegistry: CancellationRegistry;
85
- progressEmitter: ProgressEmitter;
86
- securityValidator: SecurityValidator;
87
- resourceManager: ResourceManager;
88
- }
89
- ): McpTool<CrawlInput> {
90
- return {
91
- name: "webtest_crawl_app",
92
- description: `Perform goal-directed exploration of a web application.
93
-
94
- This tool crawls the target application using AI-powered decision making:
95
- - Navigates pages based on the specified goal
96
- - Captures screenshots, snapshots, and DOM at each step
97
- - Detects and handles obstacles (cookie banners, modals)
98
- - Supports checkpointing for resume after interruption
99
- - Prevents infinite loops through multiple detection mechanisms
100
-
101
- Progress is reported throughout, and the operation can be cancelled.
102
- If sampling is unavailable, returns a prompt for manual execution.`,
103
-
104
- inputSchema: crawlInputSchema,
105
-
106
- async handler(input: CrawlInput): Promise<ToolResult> {
107
- const ctx = getContext();
108
- const {
109
- logger,
110
- config,
111
- workspaceManager,
112
- playwrightClient,
113
- samplingClient,
114
- elicitationClient,
115
- cancellationRegistry,
116
- progressEmitter,
117
- securityValidator,
118
- resourceManager,
119
- } = ctx;
120
-
121
- const requestId = `crawl-${input.analysisId}-${Date.now()}`;
122
- const crawlLogger = logger.withCorrelation({
123
- analysisId: input.analysisId,
124
- requestId,
125
- });
126
-
127
- crawlLogger.info("Starting crawl", { goal: input.goal, strategy: input.strategy });
128
-
129
- // Register for cancellation
130
- cancellationRegistry.register(requestId);
131
-
132
- try {
133
- // Validate workspace exists
134
- if (!(await workspaceManager.workspaceExists(input.analysisId))) {
135
- return {
136
- content: [
137
- {
138
- type: "text",
139
- text: `Error: Analysis workspace "${input.analysisId}" not found. Run webtest_init first.`,
140
- },
141
- ],
142
- isError: true,
143
- };
144
- }
145
-
146
- const workspace = await workspaceManager.readWorkspaceIndex(input.analysisId);
147
- const allowedDomains = [workspace.domain];
148
-
149
- // Determine limits
150
- const limits = {
151
- maxSteps: input.limits?.maxSteps ?? workspace.limits.maxSteps,
152
- maxMinutes: input.limits?.maxMinutes ?? workspace.limits.maxMinutes,
153
- maxPages: input.limits?.maxPages ?? workspace.limits.maxPages,
154
- };
155
-
156
- // Create crawl entry
157
- const { crawlId, crawlPath } = await workspaceManager.createCrawl(
158
- input.analysisId,
159
- { goal: input.goal, strategy: input.strategy, limits }
160
- );
161
-
162
- const iterationLogger = crawlLogger.withCorrelation({ crawlId });
163
- iterationLogger.info("Crawl created", { crawlId, limits });
164
-
165
- // Notify about new resource
166
- await resourceManager.notifyListChanged();
167
-
168
- // Initialize state
169
- let state: CrawlState;
170
- const startTime = Date.now();
171
-
172
- // Check for resume
173
- if (input.resume) {
174
- const checkpoint = await workspaceManager.loadCheckpoint(
175
- input.analysisId,
176
- crawlId
177
- );
178
-
179
- if (checkpoint && checkpoint.canResume) {
180
- iterationLogger.info("Resuming from checkpoint", {
181
- step: checkpoint.step,
182
- hasLoopDetection: !!checkpoint.loopDetection,
183
- });
184
-
185
- // Restore loop detection state if available, otherwise start fresh
186
- const loopDetection = checkpoint.loopDetection
187
- ? {
188
- domSignatures: new Map(checkpoint.loopDetection.domSignatures),
189
- urlVisits: new Map(checkpoint.loopDetection.urlVisits),
190
- recentActions: [...checkpoint.loopDetection.recentActions],
191
- }
192
- : {
193
- domSignatures: new Map<string, number>(),
194
- urlVisits: new Map<string, number>(),
195
- recentActions: [] as string[],
196
- };
197
-
198
- if (!checkpoint.loopDetection) {
199
- iterationLogger.warn("Resuming without loop detection history - context may be limited");
200
- }
201
-
202
- state = {
203
- step: checkpoint.step,
204
- currentUrl: checkpoint.currentUrl,
205
- startUrl: checkpoint.startUrl || workspace.url,
206
- visitedUrls: new Set(checkpoint.visitedUrls),
207
- actionHistory: [],
208
- goalProgress: checkpoint.goalProgress,
209
- loopDetection,
210
- };
211
- } else {
212
- state = createInitialState(workspace.url);
213
- }
214
- } else {
215
- state = createInitialState(workspace.url);
216
- }
217
-
218
- // Ensure Playwright is connected
219
- if (!playwrightClient.isConnected()) {
220
- await playwrightClient.connect();
221
- }
222
-
223
- // Check current browser URL before navigating
224
- // This prevents unnecessary navigation that would disrupt an existing browser session
225
- let shouldNavigate = true;
226
- try {
227
- const currentSnapshot = await playwrightClient.snapshot();
228
- const currentBrowserUrl = currentSnapshot.url;
229
-
230
- // Skip navigation if browser is already on ANY page within the allowed domain
231
- // This preserves the current browser state (e.g., logged in, items in cart)
232
- if (currentBrowserUrl && currentBrowserUrl !== "about:blank") {
233
- const currentDomain = new URL(currentBrowserUrl).hostname;
234
- const targetDomain = new URL(state.startUrl).hostname;
235
-
236
- if (currentDomain === targetDomain) {
237
- // Browser is already on the target domain - don't navigate at all
238
- // This preserves login state, cart contents, etc.
239
- iterationLogger.info("Browser already on target domain, skipping navigation", {
240
- currentBrowserUrl,
241
- startUrl: state.startUrl,
242
- });
243
- shouldNavigate = false;
244
- state.currentUrl = currentBrowserUrl;
245
- }
246
- }
247
- } catch (e) {
248
- // If we can't get current URL, proceed with navigation
249
- iterationLogger.debug("Could not check current browser URL, will navigate", {
250
- error: e instanceof Error ? e.message : "Unknown error",
251
- });
252
- }
253
-
254
- if (shouldNavigate) {
255
- // Navigate to starting URL
256
- await playwrightClient.navigate(state.currentUrl);
257
- iterationLogger.info("Navigated to starting URL", { url: state.currentUrl });
258
- }
259
-
260
- // Main crawl loop
261
- let goalComplete = false;
262
- let blocked = false;
263
- let blockedReason: string | undefined;
264
-
265
- while (!goalComplete && !blocked) {
266
- // Check cancellation
267
- cancellationRegistry.checkCancelled(requestId);
268
-
269
- // Check budget
270
- const budget = calculateBudgetStatus({
271
- stepsUsed: state.step,
272
- maxSteps: limits.maxSteps,
273
- startTime,
274
- maxMinutes: limits.maxMinutes,
275
- pagesVisited: state.visitedUrls.size,
276
- maxPages: limits.maxPages,
277
- });
278
-
279
- if (budget.isExhausted) {
280
- iterationLogger.info("Budget exhausted", { reason: budget.exhaustedReason });
281
- break;
282
- }
283
-
284
- // Emit progress
285
- progressEmitter.emit({
286
- progressToken: requestId,
287
- progress: state.step,
288
- total: limits.maxSteps,
289
- message: `Step ${state.step}: ${state.goalProgress || "Exploring..."}`,
290
- budgetStatus: budget,
291
- });
292
-
293
- // Capture current state
294
- const snapshot = await playwrightClient.snapshot();
295
- const screenshot = await playwrightClient.screenshot();
296
-
297
- state.currentUrl = snapshot.url;
298
-
299
- // Track URL visits for loop detection
300
- const urlVisits = state.loopDetection.urlVisits.get(state.currentUrl) || 0;
301
- state.loopDetection.urlVisits.set(state.currentUrl, urlVisits + 1);
302
-
303
- // Check for URL cycle
304
- if (urlVisits >= 3) {
305
- iterationLogger.warn("URL cycle detected", {
306
- url: state.currentUrl,
307
- visits: urlVisits + 1,
308
- });
309
- }
310
-
311
- // Check DOM signature for same-state loop (include URL path for semantic differentiation)
312
- let urlPath: string | undefined;
313
- try {
314
- urlPath = new URL(state.currentUrl).pathname;
315
- } catch {
316
- // If URL parsing fails, try to extract path from the string or skip it
317
- const pathMatch = state.currentUrl.match(/^(?:https?:\/\/[^\/]+)?(\/.*)$/);
318
- urlPath = pathMatch?.[1];
319
- }
320
- const domSignature = createDomSignature(snapshot.content, urlPath);
321
- const sigRepeats = state.loopDetection.domSignatures.get(domSignature) || 0;
322
- state.loopDetection.domSignatures.set(domSignature, sigRepeats + 1);
323
-
324
- if (sigRepeats >= 3) {
325
- iterationLogger.warn("Same-state loop detected", {
326
- signature: domSignature,
327
- repeats: sigRepeats + 1,
328
- });
329
- }
330
-
331
- // Save page artifacts
332
- const artifacts = input.artifacts ?? {
333
- captureScreenshots: true,
334
- captureSnapshots: true,
335
- captureDom: true,
336
- };
337
-
338
- if (
339
- artifacts.captureScreenshots ||
340
- artifacts.captureSnapshots ||
341
- artifacts.captureDom
342
- ) {
343
- await workspaceManager.savePage(input.analysisId, crawlId, {
344
- url: state.currentUrl,
345
- title: snapshot.title,
346
- snapshot: JSON.stringify(snapshot),
347
- screenshot,
348
- dom: snapshot.content,
349
- });
350
-
351
- state.visitedUrls.add(state.currentUrl);
352
- await resourceManager.notifyListChanged();
353
- }
354
-
355
- // Check for injection attempts in page content
356
- const injectionCheck = securityValidator.detectInjectionAttempt(
357
- snapshot.content
358
- );
359
- if (injectionCheck.detected) {
360
- iterationLogger.warn("Injection attempt detected in page content", {
361
- type: injectionCheck.type,
362
- evidence: injectionCheck.evidence,
363
- });
364
- // Don't act on it, but include warning in sampling prompt
365
- }
366
-
367
- // Handle manual mode (no sampling)
368
- if (input.manualNextActions && input.manualNextActions.length > 0) {
369
- const action = input.manualNextActions.shift()!;
370
-
371
- // Validate action
372
- const actionValidation = securityValidator.validateAction(
373
- action,
374
- allowedDomains
375
- );
376
- if (!actionValidation.valid) {
377
- return {
378
- content: [
379
- {
380
- type: "text",
381
- text: `Security error: ${actionValidation.reason}`,
382
- },
383
- ],
384
- isError: true,
385
- };
386
- }
387
-
388
- await executeAction(playwrightClient, action, iterationLogger);
389
- state.step++;
390
- continue;
391
- }
392
-
393
- // Calculate budget percentage for flow progress
394
- const budgetUsedPercent = Math.round((state.step / limits.maxSteps) * 100);
395
-
396
- // Build common prompt parameters
397
- const promptParams = {
398
- goal: input.goal,
399
- currentUrl: state.currentUrl,
400
- pageSnapshot: snapshot.content.slice(0, 10000),
401
- actionHistory: state.actionHistory,
402
- allowedDomains,
403
- startUrl: state.startUrl,
404
- flowProgress: {
405
- currentStep: state.step,
406
- totalSteps: limits.maxSteps,
407
- budgetUsedPercent,
408
- previousGoalProgress: state.goalProgress,
409
- },
410
- loopState: {
411
- domSignatureRepeats: sigRepeats,
412
- urlVisitCount: Object.fromEntries(state.loopDetection.urlVisits),
413
- lastActions: state.loopDetection.recentActions,
414
- },
415
- navigationBlocked: state.navigationBlocked,
416
- };
417
-
418
- // Clear navigation blocked after including in prompt
419
- state.navigationBlocked = undefined;
420
-
421
- // Request next action via sampling
422
- if (!samplingClient.hasSampling()) {
423
- // Return prompt for manual execution
424
- const prompt = buildCrawlActionPrompt(promptParams);
425
-
426
- const crawlIndex = await workspaceManager.readCrawlIndex(
427
- input.analysisId,
428
- crawlId
429
- );
430
-
431
- return {
432
- content: [
433
- {
434
- type: "text",
435
- text: JSON.stringify(
436
- {
437
- needsManualInput: true,
438
- crawlId,
439
- step: state.step,
440
- currentUrl: state.currentUrl,
441
- prompt,
442
- expectedResponseSchema: CrawlActionSchema._def,
443
- instructions:
444
- "Execute this prompt with your LLM and call webtest_crawl_app again with manualNextActions",
445
- partialResults: {
446
- pagesVisited: crawlIndex.pages.length,
447
- stepsCompleted: state.step,
448
- },
449
- },
450
- null,
451
- 2
452
- ),
453
- },
454
- ],
455
- };
456
- }
457
-
458
- // Get action from sampling
459
- const samplingResult = await samplingClient.createMessage({
460
- systemPrompt: `You are analyzing a web application to achieve the following goal: ${input.goal}`,
461
- userPrompt: buildCrawlActionPrompt(promptParams),
462
- schema: CrawlActionSchema,
463
- maxTokens: 2048,
464
- });
465
-
466
- if (!samplingResult.success || !samplingResult.data) {
467
- iterationLogger.error("Sampling failed", {
468
- error: samplingResult.error,
469
- });
470
- blocked = true;
471
- blockedReason = `Sampling error: ${samplingResult.error}`;
472
- break;
473
- }
474
-
475
- const actionResponse = samplingResult.data;
476
- state.goalProgress = actionResponse.goalProgress;
477
-
478
- // Check if goal is complete
479
- if (actionResponse.goalComplete) {
480
- goalComplete = true;
481
- iterationLogger.info("Goal completed", {
482
- reasoning: actionResponse.reasoning,
483
- });
484
- break;
485
- }
486
-
487
- // Check if blocked
488
- if (actionResponse.blocked) {
489
- blocked = true;
490
- blockedReason = actionResponse.blockedReason;
491
- iterationLogger.info("Crawl blocked", { reason: blockedReason });
492
- break;
493
- }
494
-
495
- // Handle elicitation if needed
496
- if (actionResponse.elicitationNeeded) {
497
- const elicitResult = await handleElicitation(
498
- elicitationClient,
499
- actionResponse.elicitationNeeded,
500
- iterationLogger
501
- );
502
-
503
- if (elicitResult.cancelled) {
504
- blocked = true;
505
- blockedReason = "User cancelled at elicitation prompt";
506
- break;
507
- }
508
-
509
- // Use elicitation result to modify action if needed
510
- if (
511
- elicitResult.selectedValue === "stop" ||
512
- elicitResult.selectedValue === "reject"
513
- ) {
514
- blocked = true;
515
- blockedReason = `User chose: ${elicitResult.selectedValue}`;
516
- break;
517
- }
518
- }
519
-
520
- // Execute actions
521
- for (const action of actionResponse.actions) {
522
- // Check cancellation before each action
523
- cancellationRegistry.checkCancelled(requestId);
524
-
525
- // Navigation guard: block navigation to start URL after initial steps
526
- if (action.tool === "navigate" && state.step >= 3) {
527
- const targetUrl = action.args.url as string;
528
- // Check if navigating back to start URL
529
- if (isStartUrlNavigation(targetUrl, state.startUrl)) {
530
- iterationLogger.warn("Navigation to start URL blocked", {
531
- targetUrl,
532
- startUrl: state.startUrl,
533
- step: state.step,
534
- });
535
- // Set navigation blocked for next prompt feedback
536
- state.navigationBlocked = {
537
- url: targetUrl,
538
- reason: "Cannot navigate back to start URL mid-flow. Try interacting with elements on the current page instead.",
539
- };
540
- continue;
541
- }
542
- }
543
-
544
- // Validate action
545
- const actionValidation = securityValidator.validateAction(
546
- action,
547
- allowedDomains
548
- );
549
- if (!actionValidation.valid) {
550
- iterationLogger.warn("Action blocked by security validator", {
551
- action,
552
- reason: actionValidation.reason,
553
- });
554
- continue;
555
- }
556
-
557
- // Check for exfiltration
558
- const exfilCheck = securityValidator.detectExfiltrationAttempt(
559
- action,
560
- snapshot.content
561
- );
562
- if (exfilCheck.detected) {
563
- iterationLogger.warn("Exfiltration attempt blocked", {
564
- type: exfilCheck.type,
565
- evidence: exfilCheck.evidence,
566
- });
567
- continue;
568
- }
569
-
570
- // Execute action
571
- const actionKey = `${action.tool}:${JSON.stringify(action.args)}`;
572
-
573
- // Check for action repeat
574
- const recentActions = state.loopDetection.recentActions;
575
- if (
576
- recentActions.length >= 3 &&
577
- recentActions.slice(-3).every((a) => a === actionKey)
578
- ) {
579
- iterationLogger.warn("Action repeat blocked", { action: actionKey });
580
- continue;
581
- }
582
-
583
- await executeAction(playwrightClient, action, iterationLogger);
584
-
585
- // Record action
586
- await workspaceManager.recordAction(input.analysisId, crawlId, {
587
- step: state.step,
588
- timestamp: new Date().toISOString(),
589
- tool: action.tool,
590
- args: action.args,
591
- result: "success",
592
- reasoning: actionResponse.reasoning,
593
- });
594
-
595
- state.actionHistory.push(
596
- `Step ${state.step}: ${action.tool}(${JSON.stringify(action.args)}) - ${actionResponse.reasoning}`
597
- );
598
-
599
- recentActions.push(actionKey);
600
- if (recentActions.length > 10) {
601
- recentActions.shift();
602
- }
603
- }
604
-
605
- state.step++;
606
-
607
- // Checkpoint every N steps
608
- if (state.step % config.checkpointInterval === 0) {
609
- const checkpoint: CrawlCheckpoint = {
610
- step: state.step,
611
- timestamp: new Date().toISOString(),
612
- visitedUrls: Array.from(state.visitedUrls),
613
- currentUrl: state.currentUrl,
614
- goalProgress: state.goalProgress,
615
- canResume: true,
616
- startUrl: state.startUrl,
617
- // Preserve loop detection state for context continuity
618
- loopDetection: {
619
- domSignatures: Array.from(state.loopDetection.domSignatures.entries()),
620
- urlVisits: Array.from(state.loopDetection.urlVisits.entries()),
621
- recentActions: [...state.loopDetection.recentActions],
622
- },
623
- };
624
-
625
- await workspaceManager.saveCheckpoint(
626
- input.analysisId,
627
- crawlId,
628
- checkpoint
629
- );
630
-
631
- iterationLogger.info("Checkpoint saved", { step: state.step });
632
- }
633
- }
634
-
635
- // Finalize crawl
636
- const finalStatus = goalComplete
637
- ? "completed"
638
- : blocked
639
- ? "failed"
640
- : "completed";
641
-
642
- await workspaceManager.updateCrawlIndex(input.analysisId, crawlId, {
643
- status: finalStatus,
644
- completedAt: new Date().toISOString(),
645
- });
646
-
647
- // Update workspace crawl reference
648
- const updatedWorkspace = await workspaceManager.readWorkspaceIndex(
649
- input.analysisId
650
- );
651
- const crawlRef = updatedWorkspace.crawls.find((c) => c.crawlId === crawlId);
652
- if (crawlRef) {
653
- crawlRef.status = finalStatus;
654
- crawlRef.completedAt = new Date().toISOString();
655
- crawlRef.pagesVisited = state.visitedUrls.size;
656
- crawlRef.stepsExecuted = state.step;
657
- await workspaceManager.updateWorkspaceIndex(input.analysisId, {
658
- crawls: updatedWorkspace.crawls,
659
- });
660
- }
661
-
662
- await resourceManager.notifyListChanged();
663
-
664
- const crawlIndex = await workspaceManager.readCrawlIndex(
665
- input.analysisId,
666
- crawlId
667
- );
668
-
669
- const result = {
670
- crawlId,
671
- status: finalStatus,
672
- goalComplete,
673
- blocked,
674
- blockedReason,
675
- pagesVisited: crawlIndex.pages.length,
676
- stepsExecuted: state.step,
677
- crawlIndexFilePath: join(crawlPath, "index.md"),
678
- crawlIndexUri: `webtest://${input.analysisId}/crawls/${crawlId}/index.md`,
679
- summaryUri: `webtest://${input.analysisId}/crawls/${crawlId}/summary.md`,
680
- nextSteps: goalComplete
681
- ? [
682
- `Use webtest_analyze_app with analysisId="${input.analysisId}" to analyze the application`,
683
- ]
684
- : blocked
685
- ? [`Resolve the blocker: ${blockedReason}`]
686
- : [
687
- `Crawl completed with partial results`,
688
- `Use webtest_analyze_app to analyze what was discovered`,
689
- ],
690
- };
691
-
692
- iterationLogger.info("Crawl completed", {
693
- status: finalStatus,
694
- pagesVisited: result.pagesVisited,
695
- stepsExecuted: result.stepsExecuted,
696
- });
697
-
698
- return {
699
- content: [
700
- {
701
- type: "text",
702
- text: JSON.stringify(result, null, 2),
703
- },
704
- ],
705
- };
706
- } catch (error) {
707
- if (error instanceof CancellationError) {
708
- crawlLogger.info("Crawl cancelled", { requestId: error.requestId });
709
-
710
- return {
711
- content: [
712
- {
713
- type: "text",
714
- text: JSON.stringify(
715
- {
716
- status: "cancelled",
717
- message: "Crawl was cancelled by user",
718
- partialResultsAvailable: true,
719
- },
720
- null,
721
- 2
722
- ),
723
- },
724
- ],
725
- };
726
- }
727
-
728
- const message = error instanceof Error ? error.message : "Unknown error";
729
- crawlLogger.error("Crawl failed", { error: message });
730
-
731
- return {
732
- content: [
733
- {
734
- type: "text",
735
- text: `Error during crawl: ${message}`,
736
- },
737
- ],
738
- isError: true,
739
- };
740
- } finally {
741
- cancellationRegistry.unregister(requestId);
742
- }
743
- },
744
- };
745
- }
746
-
747
- function createInitialState(startUrl: string): CrawlState {
748
- return {
749
- step: 0,
750
- currentUrl: startUrl,
751
- startUrl,
752
- visitedUrls: new Set(),
753
- actionHistory: [],
754
- goalProgress: "Starting exploration",
755
- loopDetection: {
756
- domSignatures: new Map(),
757
- urlVisits: new Map(),
758
- recentActions: [],
759
- },
760
- };
761
- }
762
-
763
- /**
764
- * Check if a target URL is the start URL (for navigation guard).
765
- * Compares normalized URLs to handle variations like trailing slashes and default paths.
766
- */
767
- function isStartUrlNavigation(targetUrl: string, startUrl: string): boolean {
768
- try {
769
- const target = new URL(targetUrl, startUrl);
770
- const start = new URL(startUrl);
771
-
772
- // Normalize paths (remove trailing slash, handle "/" as empty)
773
- const normalizePathname = (p: string) => {
774
- const normalized = p.replace(/\/$/, "") || "/";
775
- return normalized;
776
- };
777
-
778
- // Compare origin and pathname
779
- const targetPath = normalizePathname(target.pathname);
780
- const startPath = normalizePathname(start.pathname);
781
-
782
- return target.origin === start.origin && targetPath === startPath;
783
- } catch {
784
- // If URL parsing fails, do simple string comparison
785
- return targetUrl === startUrl || targetUrl === "/" || targetUrl === "";
786
- }
787
- }
788
-
789
- async function executeAction(
790
- playwright: PlaywrightClient,
791
- action: { tool: string; args: Record<string, unknown> },
792
- logger: { debug: (msg: string, data?: Record<string, unknown>) => void }
793
- ): Promise<void> {
794
- logger.debug("Executing action", { tool: action.tool, args: action.args });
795
-
796
- switch (action.tool) {
797
- case "navigate":
798
- await playwright.navigate(action.args.url as string);
799
- break;
800
- case "click":
801
- await playwright.click(
802
- action.args.element as string,
803
- action.args.ref as string
804
- );
805
- break;
806
- case "type":
807
- await playwright.type(
808
- action.args.element as string,
809
- action.args.ref as string,
810
- action.args.text as string,
811
- {
812
- submit: action.args.submit as boolean | undefined,
813
- slowly: action.args.slowly as boolean | undefined,
814
- }
815
- );
816
- break;
817
- case "fill":
818
- await playwright.fill(
819
- action.args.element as string,
820
- action.args.ref as string,
821
- action.args.value as string
822
- );
823
- break;
824
- case "hover":
825
- await playwright.hover(
826
- action.args.element as string,
827
- action.args.ref as string
828
- );
829
- break;
830
- case "select":
831
- await playwright.select(
832
- action.args.element as string,
833
- action.args.ref as string,
834
- action.args.values as string[]
835
- );
836
- break;
837
- case "press":
838
- await playwright.press(action.args.key as string);
839
- break;
840
- case "scroll":
841
- await playwright.scroll(
842
- action.args.x as number,
843
- action.args.y as number
844
- );
845
- break;
846
- case "wait":
847
- await playwright.wait(action.args.ms as number);
848
- break;
849
- default:
850
- logger.debug("Unknown action tool", { tool: action.tool });
851
- }
852
- }
853
-
854
- async function handleElicitation(
855
- elicitationClient: ElicitationClient,
856
- elicitation: NonNullable<CrawlAction["elicitationNeeded"]>,
857
- logger: { info: (msg: string, data?: Record<string, unknown>) => void }
858
- ): Promise<{ selectedValue?: string; cancelled?: boolean }> {
859
- logger.info("Handling elicitation", {
860
- type: elicitation.type,
861
- context: elicitation.context,
862
- });
863
-
864
- let request;
865
- switch (elicitation.type) {
866
- case "cookie_consent":
867
- request = elicitationClient.createCookieConsentRequest(elicitation.context);
868
- break;
869
- case "modal_blocking":
870
- request = elicitationClient.createModalBlockingRequest(elicitation.context);
871
- break;
872
- case "ambiguous_navigation":
873
- request = elicitationClient.createAmbiguousNavigationRequest(
874
- (elicitation.options || []).map(opt => ({
875
- url: opt.url || "",
876
- label: opt.label,
877
- }))
878
- );
879
- break;
880
- case "auth_required":
881
- request = elicitationClient.createAuthRequiredRequest();
882
- break;
883
- default:
884
- return {};
885
- }
886
-
887
- const result = await elicitationClient.elicit(request);
888
-
889
- if (!result.success) {
890
- // Fallback: include questions in next sampling prompt
891
- logger.info("Elicitation fallback", {
892
- fallbackQuestions: result.fallbackQuestions,
893
- });
894
- return {};
895
- }
896
-
897
- return {
898
- selectedValue: result.selectedValue,
899
- cancelled: result.cancelled,
900
- };
901
- }