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.
- package/.claude/commands/openspec/apply.md +23 -0
- package/.claude/commands/openspec/archive.md +27 -0
- package/.claude/commands/openspec/proposal.md +28 -0
- package/.gemini/commands/openspec/apply.toml +21 -0
- package/.gemini/commands/openspec/archive.toml +25 -0
- package/.gemini/commands/openspec/proposal.toml +26 -0
- package/.github/prompts/openspec-apply.prompt.md +22 -0
- package/.github/prompts/openspec-archive.prompt.md +26 -0
- package/.github/prompts/openspec-proposal.prompt.md +27 -0
- package/.github/workflows/release.yml +33 -0
- package/.kilocode/workflows/openspec-apply.md +17 -0
- package/.kilocode/workflows/openspec-archive.md +21 -0
- package/.kilocode/workflows/openspec-proposal.md +22 -0
- package/.mcp.json +23 -0
- package/.opencode/command/openspec-apply.md +25 -0
- package/.opencode/command/openspec-archive.md +28 -0
- package/.opencode/command/openspec-proposal.md +30 -0
- package/.roo/commands/openspec-apply.md +20 -0
- package/.roo/commands/openspec-archive.md +24 -0
- package/.roo/commands/openspec-proposal.md +25 -0
- package/.vscode/mcp.json +23 -0
- package/AGENTS.md +18 -0
- package/CLAUDE.md +18 -0
- package/LICENSE +65 -0
- package/README.md +303 -0
- package/dist/config.d.ts +4 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +27 -0
- package/dist/config.js.map +1 -0
- package/dist/elicitation/index.d.ts +17 -0
- package/dist/elicitation/index.d.ts.map +1 -0
- package/dist/elicitation/index.js +118 -0
- package/dist/elicitation/index.js.map +1 -0
- package/dist/elicitation/types.d.ts +35 -0
- package/dist/elicitation/types.d.ts.map +1 -0
- package/dist/elicitation/types.js +39 -0
- package/dist/elicitation/types.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +76 -0
- package/dist/index.js.map +1 -0
- package/dist/lifecycle/index.d.ts +31 -0
- package/dist/lifecycle/index.d.ts.map +1 -0
- package/dist/lifecycle/index.js +61 -0
- package/dist/lifecycle/index.js.map +1 -0
- package/dist/logger.d.ts +21 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +182 -0
- package/dist/logger.js.map +1 -0
- package/dist/playwright-client/index.d.ts +29 -0
- package/dist/playwright-client/index.d.ts.map +1 -0
- package/dist/playwright-client/index.js +288 -0
- package/dist/playwright-client/index.js.map +1 -0
- package/dist/playwright-client/types.d.ts +44 -0
- package/dist/playwright-client/types.d.ts.map +1 -0
- package/dist/playwright-client/types.js +49 -0
- package/dist/playwright-client/types.js.map +1 -0
- package/dist/progress/index.d.ts +39 -0
- package/dist/progress/index.d.ts.map +1 -0
- package/dist/progress/index.js +106 -0
- package/dist/progress/index.js.map +1 -0
- package/dist/progress/types.d.ts +24 -0
- package/dist/progress/types.d.ts.map +1 -0
- package/dist/progress/types.js +2 -0
- package/dist/progress/types.js.map +1 -0
- package/dist/prompts/index.d.ts +19 -0
- package/dist/prompts/index.d.ts.map +1 -0
- package/dist/prompts/index.js +207 -0
- package/dist/prompts/index.js.map +1 -0
- package/dist/prompts/loader.d.ts +20 -0
- package/dist/prompts/loader.d.ts.map +1 -0
- package/dist/prompts/loader.js +47 -0
- package/dist/prompts/loader.js.map +1 -0
- package/dist/resources/index.d.ts +27 -0
- package/dist/resources/index.d.ts.map +1 -0
- package/dist/resources/index.js +186 -0
- package/dist/resources/index.js.map +1 -0
- package/dist/resources/subscriptions.d.ts +10 -0
- package/dist/resources/subscriptions.d.ts.map +1 -0
- package/dist/resources/subscriptions.js +23 -0
- package/dist/resources/subscriptions.js.map +1 -0
- package/dist/sampling/index.d.ts +11 -0
- package/dist/sampling/index.d.ts.map +1 -0
- package/dist/sampling/index.js +201 -0
- package/dist/sampling/index.js.map +1 -0
- package/dist/sampling/prompts.d.ts +56 -0
- package/dist/sampling/prompts.d.ts.map +1 -0
- package/dist/sampling/prompts.js +124 -0
- package/dist/sampling/prompts.js.map +1 -0
- package/dist/sampling/types.d.ts +57 -0
- package/dist/sampling/types.d.ts.map +1 -0
- package/dist/sampling/types.js +2 -0
- package/dist/sampling/types.js.map +1 -0
- package/dist/schemas/config.d.ts +40 -0
- package/dist/schemas/config.d.ts.map +1 -0
- package/dist/schemas/config.js +30 -0
- package/dist/schemas/config.js.map +1 -0
- package/dist/security/index.d.ts +38 -0
- package/dist/security/index.d.ts.map +1 -0
- package/dist/security/index.js +281 -0
- package/dist/security/index.js.map +1 -0
- package/dist/server.d.ts +9 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +142 -0
- package/dist/server.js.map +1 -0
- package/dist/test-utils/index.d.ts +6 -0
- package/dist/test-utils/index.d.ts.map +1 -0
- package/dist/test-utils/index.js +6 -0
- package/dist/test-utils/index.js.map +1 -0
- package/dist/test-utils/mock-context.d.ts +64 -0
- package/dist/test-utils/mock-context.d.ts.map +1 -0
- package/dist/test-utils/mock-context.js +347 -0
- package/dist/test-utils/mock-context.js.map +1 -0
- package/dist/test-utils/mock-playwright-client.d.ts +62 -0
- package/dist/test-utils/mock-playwright-client.d.ts.map +1 -0
- package/dist/test-utils/mock-playwright-client.js +315 -0
- package/dist/test-utils/mock-playwright-client.js.map +1 -0
- package/dist/tools/index.d.ts +4 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +8 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/webtest/crawl.d.ts +46 -0
- package/dist/tools/webtest/crawl.d.ts.map +1 -0
- package/dist/tools/webtest/crawl.js +678 -0
- package/dist/tools/webtest/crawl.js.map +1 -0
- package/dist/tools/webtest/discover-features.d.ts +30 -0
- package/dist/tools/webtest/discover-features.d.ts.map +1 -0
- package/dist/tools/webtest/discover-features.js +343 -0
- package/dist/tools/webtest/discover-features.js.map +1 -0
- package/dist/tools/webtest/discover-flows.d.ts +29 -0
- package/dist/tools/webtest/discover-flows.d.ts.map +1 -0
- package/dist/tools/webtest/discover-flows.js +341 -0
- package/dist/tools/webtest/discover-flows.js.map +1 -0
- package/dist/tools/webtest/generate-tests.d.ts +54 -0
- package/dist/tools/webtest/generate-tests.d.ts.map +1 -0
- package/dist/tools/webtest/generate-tests.js +364 -0
- package/dist/tools/webtest/generate-tests.js.map +1 -0
- package/dist/tools/webtest/index.d.ts +8 -0
- package/dist/tools/webtest/index.d.ts.map +1 -0
- package/dist/tools/webtest/index.js +8 -0
- package/dist/tools/webtest/index.js.map +1 -0
- package/dist/tools/webtest/run-test-case.d.ts +28 -0
- package/dist/tools/webtest/run-test-case.d.ts.map +1 -0
- package/dist/tools/webtest/run-test-case.js +420 -0
- package/dist/tools/webtest/run-test-case.js.map +1 -0
- package/dist/tools/webtest/schemas.d.ts +175 -0
- package/dist/tools/webtest/schemas.d.ts.map +1 -0
- package/dist/tools/webtest/schemas.js +156 -0
- package/dist/tools/webtest/schemas.js.map +1 -0
- package/dist/tools/webtest/start-analysis.d.ts +16 -0
- package/dist/tools/webtest/start-analysis.d.ts.map +1 -0
- package/dist/tools/webtest/start-analysis.js +137 -0
- package/dist/tools/webtest/start-analysis.js.map +1 -0
- package/dist/transports/http.d.ts +8 -0
- package/dist/transports/http.d.ts.map +1 -0
- package/dist/transports/http.js +9 -0
- package/dist/transports/http.js.map +1 -0
- package/dist/transports/index.d.ts +14 -0
- package/dist/transports/index.d.ts.map +1 -0
- package/dist/transports/index.js +20 -0
- package/dist/transports/index.js.map +1 -0
- package/dist/transports/stdio.d.ts +4 -0
- package/dist/transports/stdio.d.ts.map +1 -0
- package/dist/transports/stdio.js +6 -0
- package/dist/transports/stdio.js.map +1 -0
- package/dist/types/capabilities.d.ts +18 -0
- package/dist/types/capabilities.d.ts.map +1 -0
- package/dist/types/capabilities.js +35 -0
- package/dist/types/capabilities.js.map +1 -0
- package/dist/types/context.d.ts +20 -0
- package/dist/types/context.d.ts.map +1 -0
- package/dist/types/context.js +2 -0
- package/dist/types/context.js.map +1 -0
- package/dist/types/tool.d.ts +10 -0
- package/dist/types/tool.d.ts.map +1 -0
- package/dist/types/tool.js +2 -0
- package/dist/types/tool.js.map +1 -0
- package/dist/workspace/index.d.ts +99 -0
- package/dist/workspace/index.d.ts.map +1 -0
- package/dist/workspace/index.js +648 -0
- package/dist/workspace/index.js.map +1 -0
- package/dist/workspace/markdown.d.ts +50 -0
- package/dist/workspace/markdown.d.ts.map +1 -0
- package/dist/workspace/markdown.js +210 -0
- package/dist/workspace/markdown.js.map +1 -0
- package/dist/workspace/types.d.ts +173 -0
- package/dist/workspace/types.d.ts.map +1 -0
- package/dist/workspace/types.js +2 -0
- package/dist/workspace/types.js.map +1 -0
- package/openspec/AGENTS.md +456 -0
- package/openspec/changes/archive/2025-12-18-add-hybrid-artifact-paths/proposal.md +33 -0
- package/openspec/changes/archive/2025-12-18-add-hybrid-artifact-paths/specs/webtest-resources/spec.md +27 -0
- package/openspec/changes/archive/2025-12-18-add-hybrid-artifact-paths/specs/webtest-tools/spec.md +304 -0
- package/openspec/changes/archive/2025-12-18-add-hybrid-artifact-paths/tasks.md +43 -0
- package/openspec/changes/archive/2025-12-18-add-mcp-server-foundation/design.md +209 -0
- package/openspec/changes/archive/2025-12-18-add-mcp-server-foundation/proposal.md +41 -0
- package/openspec/changes/archive/2025-12-18-add-mcp-server-foundation/specs/mcp-server-core/spec.md +183 -0
- package/openspec/changes/archive/2025-12-18-add-mcp-server-foundation/tasks.md +112 -0
- package/openspec/changes/archive/2025-12-18-add-webtest-orchestrator/design.md +333 -0
- package/openspec/changes/archive/2025-12-18-add-webtest-orchestrator/proposal.md +66 -0
- package/openspec/changes/archive/2025-12-18-add-webtest-orchestrator/specs/mcp-server-core/spec.md +129 -0
- package/openspec/changes/archive/2025-12-18-add-webtest-orchestrator/specs/webtest-lifecycle/spec.md +138 -0
- package/openspec/changes/archive/2025-12-18-add-webtest-orchestrator/specs/webtest-logging/spec.md +211 -0
- package/openspec/changes/archive/2025-12-18-add-webtest-orchestrator/specs/webtest-prompts/spec.md +157 -0
- package/openspec/changes/archive/2025-12-18-add-webtest-orchestrator/specs/webtest-resources/spec.md +213 -0
- package/openspec/changes/archive/2025-12-18-add-webtest-orchestrator/specs/webtest-sampling/spec.md +257 -0
- package/openspec/changes/archive/2025-12-18-add-webtest-orchestrator/specs/webtest-tools/spec.md +501 -0
- package/openspec/changes/archive/2025-12-18-add-webtest-orchestrator/tasks.md +264 -0
- package/openspec/changes/archive/2025-12-18-allow-analysis-of-incomplete-crawls/proposal.md +24 -0
- package/openspec/changes/archive/2025-12-18-allow-analysis-of-incomplete-crawls/specs/webtest-tools/spec.md +80 -0
- package/openspec/changes/archive/2025-12-18-allow-analysis-of-incomplete-crawls/tasks.md +8 -0
- package/openspec/changes/archive/2025-12-18-fix-crawl-loop-stability/design.md +90 -0
- package/openspec/changes/archive/2025-12-18-fix-crawl-loop-stability/proposal.md +28 -0
- package/openspec/changes/archive/2025-12-18-fix-crawl-loop-stability/specs/webtest-sampling/spec.md +90 -0
- package/openspec/changes/archive/2025-12-18-fix-crawl-loop-stability/tasks.md +33 -0
- package/openspec/changes/archive/2025-12-18-use-markdown-artifacts/design.md +558 -0
- package/openspec/changes/archive/2025-12-18-use-markdown-artifacts/proposal.md +119 -0
- package/openspec/changes/archive/2025-12-18-use-markdown-artifacts/specs/webtest-resources/spec.md +109 -0
- package/openspec/changes/archive/2025-12-18-use-markdown-artifacts/specs/webtest-tools/spec.md +121 -0
- package/openspec/changes/archive/2025-12-18-use-markdown-artifacts/tasks.md +133 -0
- package/openspec/changes/extract-prompts-to-markdown/design.md +86 -0
- package/openspec/changes/extract-prompts-to-markdown/proposal.md +50 -0
- package/openspec/changes/extract-prompts-to-markdown/specs/webtest-prompts/spec.md +74 -0
- package/openspec/changes/extract-prompts-to-markdown/tasks.md +40 -0
- package/openspec/changes/refactor-webtest-naming/design.md +95 -0
- package/openspec/changes/refactor-webtest-naming/proposal.md +66 -0
- package/openspec/changes/refactor-webtest-naming/specs/webtest-prompts/spec.md +79 -0
- package/openspec/changes/refactor-webtest-naming/specs/webtest-resources/spec.md +80 -0
- package/openspec/changes/refactor-webtest-naming/specs/webtest-sampling/spec.md +122 -0
- package/openspec/changes/refactor-webtest-naming/specs/webtest-tools/spec.md +113 -0
- package/openspec/changes/refactor-webtest-naming/tasks.md +119 -0
- package/openspec/changes/rename-package-to-retest/proposal.md +52 -0
- package/openspec/changes/rename-package-to-retest/specs/mcp-server-core/spec.md +53 -0
- package/openspec/changes/rename-package-to-retest/specs/retest-lifecycle/spec.md +68 -0
- package/openspec/changes/rename-package-to-retest/specs/retest-logging/spec.md +35 -0
- package/openspec/changes/rename-package-to-retest/specs/retest-prompts/spec.md +159 -0
- package/openspec/changes/rename-package-to-retest/specs/retest-resources/spec.md +251 -0
- package/openspec/changes/rename-package-to-retest/specs/retest-sampling/spec.md +99 -0
- package/openspec/changes/rename-package-to-retest/specs/retest-tools/spec.md +295 -0
- package/openspec/changes/rename-package-to-retest/tasks.md +71 -0
- package/openspec/project.md +31 -0
- package/openspec/specs/mcp-server-core/spec.md +178 -0
- package/openspec/specs/webtest-lifecycle/spec.md +136 -0
- package/openspec/specs/webtest-logging/spec.md +209 -0
- package/openspec/specs/webtest-prompts/spec.md +155 -0
- package/openspec/specs/webtest-resources/spec.md +248 -0
- package/openspec/specs/webtest-sampling/spec.md +344 -0
- package/openspec/specs/webtest-tools/spec.md +282 -0
- package/package.json +54 -0
- package/release.config.js +9 -0
- package/src/config.test.ts +96 -0
- package/src/config.ts +32 -0
- package/src/elicitation/index.test.ts +399 -0
- package/src/elicitation/index.ts +171 -0
- package/src/elicitation/types.ts +68 -0
- package/src/index.ts +83 -0
- package/src/lifecycle/index.test.ts +260 -0
- package/src/lifecycle/index.ts +101 -0
- package/src/logger.redaction.test.ts +322 -0
- package/src/logger.test.ts +123 -0
- package/src/logger.ts +229 -0
- package/src/playwright-client/index.ts +392 -0
- package/src/playwright-client/types.ts +99 -0
- package/src/progress/index.test.ts +327 -0
- package/src/progress/index.ts +170 -0
- package/src/progress/types.ts +25 -0
- package/src/prompts/index.test.ts +451 -0
- package/src/prompts/index.ts +246 -0
- package/src/prompts/loader.test.ts +100 -0
- package/src/prompts/loader.ts +59 -0
- package/src/prompts/templates/mcp/webtest-crawl.md +7 -0
- package/src/prompts/templates/mcp/webtest-discover-flows.md +11 -0
- package/src/prompts/templates/mcp/webtest-discover.md +12 -0
- package/src/prompts/templates/mcp/webtest-full-workflow.md +12 -0
- package/src/prompts/templates/mcp/webtest-generate-tests.md +11 -0
- package/src/prompts/templates/mcp/webtest-run-test.md +11 -0
- package/src/prompts/templates/mcp/webtest-start.md +8 -0
- package/src/prompts/templates/sampling/crawl-action.md +35 -0
- package/src/prompts/templates/sampling/feature-discovery.md +27 -0
- package/src/prompts/templates/sampling/flow-discovery.md +29 -0
- package/src/prompts/templates/sampling/page-content-wrapper.md +5 -0
- package/src/prompts/templates/sampling/system-prefix.md +12 -0
- package/src/prompts/templates/sampling/test-evaluation.md +17 -0
- package/src/prompts/templates/sampling/test-generation.md +31 -0
- package/src/resources/index.ts +250 -0
- package/src/resources/subscriptions.ts +37 -0
- package/src/sampling/index.test.ts +414 -0
- package/src/sampling/index.ts +286 -0
- package/src/sampling/prompts.ts +194 -0
- package/src/sampling/types.ts +60 -0
- package/src/schemas/config.ts +39 -0
- package/src/security/index.test.ts +441 -0
- package/src/security/index.ts +361 -0
- package/src/security/security-scenarios.test.ts +468 -0
- package/src/server.ts +211 -0
- package/src/test-utils/index.ts +6 -0
- package/src/test-utils/mock-context.ts +426 -0
- package/src/test-utils/mock-playwright-client.ts +422 -0
- package/src/tools/index.ts +11 -0
- package/src/tools/webtest/crawl.test.ts +834 -0
- package/src/tools/webtest/crawl.ts +901 -0
- package/src/tools/webtest/discover-features.ts +412 -0
- package/src/tools/webtest/discover-flows.ts +408 -0
- package/src/tools/webtest/generate-tests.test.ts +532 -0
- package/src/tools/webtest/generate-tests.ts +425 -0
- package/src/tools/webtest/index.ts +7 -0
- package/src/tools/webtest/integration.test.ts +536 -0
- package/src/tools/webtest/run-test-case.test.ts +659 -0
- package/src/tools/webtest/run-test-case.ts +508 -0
- package/src/tools/webtest/schemas.ts +201 -0
- package/src/tools/webtest/start-analysis.test.ts +151 -0
- package/src/tools/webtest/start-analysis.ts +158 -0
- package/src/transports/http.ts +19 -0
- package/src/transports/index.ts +30 -0
- package/src/transports/stdio.ts +7 -0
- package/src/types/capabilities.test.ts +193 -0
- package/src/types/capabilities.ts +50 -0
- package/src/types/context.ts +21 -0
- package/src/types/tool.ts +11 -0
- package/src/workspace/index.ts +945 -0
- package/src/workspace/markdown.ts +272 -0
- package/src/workspace/types.ts +186 -0
- package/tests/integration/server.test.ts +89 -0
- package/tests/integration/tools.test.ts +99 -0
- package/tsconfig.json +20 -0
- package/vitest.config.ts +9 -0
- package/vitest.integration.config.ts +10 -0
|
@@ -0,0 +1,945 @@
|
|
|
1
|
+
import { mkdir, writeFile, access, readdir } from "node:fs/promises";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import type { Logger } from "../logger.js";
|
|
5
|
+
import type { Config } from "../schemas/config.js";
|
|
6
|
+
import type {
|
|
7
|
+
WorkspaceIndex,
|
|
8
|
+
CrawlIndex,
|
|
9
|
+
CrawlCheckpoint,
|
|
10
|
+
PageReference,
|
|
11
|
+
ActionRecord,
|
|
12
|
+
TestRunIndex,
|
|
13
|
+
TestStepResult,
|
|
14
|
+
Feature,
|
|
15
|
+
FlowsReference,
|
|
16
|
+
} from "./types.js";
|
|
17
|
+
import {
|
|
18
|
+
writeMarkdownWithFrontmatter,
|
|
19
|
+
readMarkdownFrontmatter,
|
|
20
|
+
formatAccessibilityTree,
|
|
21
|
+
extractInteractiveElements,
|
|
22
|
+
formatDate,
|
|
23
|
+
formatTime,
|
|
24
|
+
type AccessibilityNode,
|
|
25
|
+
} from "./markdown.js";
|
|
26
|
+
|
|
27
|
+
export * from "./types.js";
|
|
28
|
+
export * from "./markdown.js";
|
|
29
|
+
|
|
30
|
+
export interface WorkspaceManager {
|
|
31
|
+
createWorkspace(params: {
|
|
32
|
+
url: string;
|
|
33
|
+
focus?: string;
|
|
34
|
+
limits: {
|
|
35
|
+
maxSteps: number;
|
|
36
|
+
maxMinutes: number;
|
|
37
|
+
maxPages: number;
|
|
38
|
+
};
|
|
39
|
+
}): Promise<{ analysisId: string; workspacePath: string }>;
|
|
40
|
+
|
|
41
|
+
getWorkspacePath(analysisId: string): string;
|
|
42
|
+
workspaceExists(analysisId: string): Promise<boolean>;
|
|
43
|
+
readWorkspaceIndex(analysisId: string): Promise<WorkspaceIndex>;
|
|
44
|
+
updateWorkspaceIndex(
|
|
45
|
+
analysisId: string,
|
|
46
|
+
update: Partial<WorkspaceIndex>
|
|
47
|
+
): Promise<void>;
|
|
48
|
+
|
|
49
|
+
createCrawl(
|
|
50
|
+
analysisId: string,
|
|
51
|
+
params: { goal: string; strategy: string; limits: WorkspaceIndex["limits"] }
|
|
52
|
+
): Promise<{ crawlId: string; crawlPath: string }>;
|
|
53
|
+
getCrawlPath(analysisId: string, crawlId: string): string;
|
|
54
|
+
readCrawlIndex(analysisId: string, crawlId: string): Promise<CrawlIndex>;
|
|
55
|
+
updateCrawlIndex(
|
|
56
|
+
analysisId: string,
|
|
57
|
+
crawlId: string,
|
|
58
|
+
update: Partial<CrawlIndex>
|
|
59
|
+
): Promise<void>;
|
|
60
|
+
|
|
61
|
+
savePage(
|
|
62
|
+
analysisId: string,
|
|
63
|
+
crawlId: string,
|
|
64
|
+
page: {
|
|
65
|
+
url: string;
|
|
66
|
+
title: string;
|
|
67
|
+
snapshot: string;
|
|
68
|
+
screenshot: { data: string; mimeType: string };
|
|
69
|
+
dom: string;
|
|
70
|
+
}
|
|
71
|
+
): Promise<PageReference>;
|
|
72
|
+
|
|
73
|
+
saveCheckpoint(
|
|
74
|
+
analysisId: string,
|
|
75
|
+
crawlId: string,
|
|
76
|
+
checkpoint: CrawlCheckpoint
|
|
77
|
+
): Promise<void>;
|
|
78
|
+
loadCheckpoint(
|
|
79
|
+
analysisId: string,
|
|
80
|
+
crawlId: string
|
|
81
|
+
): Promise<CrawlCheckpoint | null>;
|
|
82
|
+
|
|
83
|
+
recordAction(
|
|
84
|
+
analysisId: string,
|
|
85
|
+
crawlId: string,
|
|
86
|
+
action: ActionRecord
|
|
87
|
+
): Promise<void>;
|
|
88
|
+
|
|
89
|
+
saveFeatures(
|
|
90
|
+
analysisId: string,
|
|
91
|
+
features: { markdown: string; frontmatter: { features: Feature[] } }
|
|
92
|
+
): Promise<{
|
|
93
|
+
featuresFilePath: string;
|
|
94
|
+
featuresUri: string;
|
|
95
|
+
featureCount: number;
|
|
96
|
+
}>;
|
|
97
|
+
|
|
98
|
+
saveFlows(
|
|
99
|
+
analysisId: string,
|
|
100
|
+
featureSlug: string,
|
|
101
|
+
flows: { markdown: string; frontmatter: unknown }
|
|
102
|
+
): Promise<{
|
|
103
|
+
flowsFilePath: string;
|
|
104
|
+
flowsUri: string;
|
|
105
|
+
flowCount: number;
|
|
106
|
+
}>;
|
|
107
|
+
|
|
108
|
+
getFeaturePath(analysisId: string, featureSlug: string): string;
|
|
109
|
+
|
|
110
|
+
saveTests(
|
|
111
|
+
analysisId: string,
|
|
112
|
+
tests: { markdown: string; frontmatter: unknown }
|
|
113
|
+
): Promise<{
|
|
114
|
+
testsFilePath: string;
|
|
115
|
+
testsUri: string;
|
|
116
|
+
testCount: number;
|
|
117
|
+
}>;
|
|
118
|
+
|
|
119
|
+
createTestRun(
|
|
120
|
+
analysisId: string,
|
|
121
|
+
params: { testCaseId: string; testName: string }
|
|
122
|
+
): Promise<{ runId: string; runPath: string }>;
|
|
123
|
+
getTestRunPath(analysisId: string, runId: string): string;
|
|
124
|
+
readTestRunIndex(analysisId: string, runId: string): Promise<TestRunIndex>;
|
|
125
|
+
updateTestRunIndex(
|
|
126
|
+
analysisId: string,
|
|
127
|
+
runId: string,
|
|
128
|
+
update: Partial<TestRunIndex>
|
|
129
|
+
): Promise<void>;
|
|
130
|
+
|
|
131
|
+
saveTestStepEvidence(
|
|
132
|
+
analysisId: string,
|
|
133
|
+
runId: string,
|
|
134
|
+
stepNumber: number,
|
|
135
|
+
evidence: {
|
|
136
|
+
screenshot?: { data: string; mimeType: string };
|
|
137
|
+
snapshot?: string;
|
|
138
|
+
}
|
|
139
|
+
): Promise<{
|
|
140
|
+
screenshotFilePath?: string;
|
|
141
|
+
screenshotUri?: string;
|
|
142
|
+
snapshotFilePath?: string;
|
|
143
|
+
snapshotUri?: string;
|
|
144
|
+
}>;
|
|
145
|
+
|
|
146
|
+
listWorkspaces(): Promise<string[]>;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function createWorkspaceManager(
|
|
150
|
+
config: Config,
|
|
151
|
+
logger: Logger
|
|
152
|
+
): WorkspaceManager {
|
|
153
|
+
const baseDir = resolve(config.workspaceDir);
|
|
154
|
+
|
|
155
|
+
function getWorkspacePath(analysisId: string): string {
|
|
156
|
+
return join(baseDir, analysisId);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function getCrawlPath(analysisId: string, crawlId: string): string {
|
|
160
|
+
return join(getWorkspacePath(analysisId), "crawls", crawlId);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function getTestRunPath(analysisId: string, runId: string): string {
|
|
164
|
+
return join(getWorkspacePath(analysisId), "runs", runId);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async function ensureDir(path: string): Promise<void> {
|
|
168
|
+
await mkdir(path, { recursive: true });
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async function fileExists(path: string): Promise<boolean> {
|
|
172
|
+
try {
|
|
173
|
+
await access(path);
|
|
174
|
+
return true;
|
|
175
|
+
} catch {
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Helper to generate workspace markdown content
|
|
181
|
+
function generateWorkspaceMarkdown(index: WorkspaceIndex): string {
|
|
182
|
+
const lines: string[] = [];
|
|
183
|
+
lines.push(`# Analysis Workspace`);
|
|
184
|
+
lines.push("");
|
|
185
|
+
lines.push(`**Target**: ${index.url}`);
|
|
186
|
+
if (index.focus) {
|
|
187
|
+
lines.push(`**Focus**: ${index.focus}`);
|
|
188
|
+
}
|
|
189
|
+
lines.push(`**Status**: ${index.status}`);
|
|
190
|
+
lines.push(`**Created**: ${formatDate(index.createdAt)}`);
|
|
191
|
+
lines.push("");
|
|
192
|
+
lines.push("## Configuration");
|
|
193
|
+
lines.push("");
|
|
194
|
+
lines.push("| Setting | Value |");
|
|
195
|
+
lines.push("|---------|-------|");
|
|
196
|
+
lines.push(`| Max Steps | ${index.limits.maxSteps} |`);
|
|
197
|
+
lines.push(`| Max Pages | ${index.limits.maxPages} |`);
|
|
198
|
+
lines.push(`| Max Minutes | ${index.limits.maxMinutes} |`);
|
|
199
|
+
lines.push("");
|
|
200
|
+
|
|
201
|
+
if (index.crawls.length > 0) {
|
|
202
|
+
lines.push("## Crawls");
|
|
203
|
+
lines.push("");
|
|
204
|
+
for (const crawl of index.crawls) {
|
|
205
|
+
lines.push(`### ${crawl.crawlId.slice(0, 8)}`);
|
|
206
|
+
lines.push(`- **Goal**: ${crawl.goal}`);
|
|
207
|
+
lines.push(`- **Status**: ${crawl.status}`);
|
|
208
|
+
lines.push(`- **Pages**: ${crawl.pagesVisited} discovered`);
|
|
209
|
+
lines.push(`- **Steps**: ${crawl.stepsExecuted} executed`);
|
|
210
|
+
lines.push(`- **Started**: ${formatTime(crawl.startedAt)}`);
|
|
211
|
+
if (crawl.completedAt) {
|
|
212
|
+
lines.push(`- **Completed**: ${formatTime(crawl.completedAt)}`);
|
|
213
|
+
}
|
|
214
|
+
lines.push("");
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (index.features) {
|
|
219
|
+
lines.push("## Features");
|
|
220
|
+
lines.push("");
|
|
221
|
+
lines.push(`${index.features.featureCount} features discovered at ${formatTime(index.features.createdAt)}.`);
|
|
222
|
+
lines.push(`- [View Features](./features.md)`);
|
|
223
|
+
lines.push("");
|
|
224
|
+
if (index.featureFlows && index.featureFlows.length > 0) {
|
|
225
|
+
lines.push("### Feature Flows");
|
|
226
|
+
lines.push("");
|
|
227
|
+
for (const flow of index.featureFlows) {
|
|
228
|
+
lines.push(`- [${flow.featureSlug}](./features/${flow.featureSlug}/flows.md) - ${flow.flowCount} flows`);
|
|
229
|
+
}
|
|
230
|
+
lines.push("");
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
if (index.tests) {
|
|
236
|
+
lines.push("## Tests");
|
|
237
|
+
lines.push("");
|
|
238
|
+
lines.push(`${index.tests.testCount} test cases generated at ${formatTime(index.tests.createdAt)}.`);
|
|
239
|
+
lines.push(`- [View Test Cases](./tests/tests.md)`);
|
|
240
|
+
lines.push("");
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (index.runs.length > 0) {
|
|
244
|
+
lines.push("## Test Runs");
|
|
245
|
+
lines.push("");
|
|
246
|
+
lines.push("| Run | Test | Status | Started |");
|
|
247
|
+
lines.push("|-----|------|--------|---------|");
|
|
248
|
+
for (const run of index.runs) {
|
|
249
|
+
const status = run.status === "passed" ? "Passed" : run.status === "failed" ? "Failed" : run.status;
|
|
250
|
+
lines.push(`| ${run.runId.slice(0, 8)} | ${run.testCaseId} | ${status} | ${formatTime(run.startedAt)} |`);
|
|
251
|
+
}
|
|
252
|
+
lines.push("");
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return lines.join("\n");
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Helper to generate crawl markdown content
|
|
259
|
+
function generateCrawlMarkdown(index: CrawlIndex): string {
|
|
260
|
+
const lines: string[] = [];
|
|
261
|
+
lines.push(`# Crawl Report`);
|
|
262
|
+
lines.push("");
|
|
263
|
+
lines.push(`**Goal**: ${index.goal}`);
|
|
264
|
+
lines.push(`**Strategy**: ${index.strategy}`);
|
|
265
|
+
lines.push(`**Status**: ${index.status}`);
|
|
266
|
+
lines.push(`**Started**: ${formatDate(index.startedAt)}`);
|
|
267
|
+
if (index.completedAt) {
|
|
268
|
+
lines.push(`**Completed**: ${formatDate(index.completedAt)}`);
|
|
269
|
+
}
|
|
270
|
+
lines.push("");
|
|
271
|
+
lines.push("## Budget Usage");
|
|
272
|
+
lines.push("");
|
|
273
|
+
lines.push("| Metric | Used | Limit | % |");
|
|
274
|
+
lines.push("|--------|------|-------|---|");
|
|
275
|
+
const stepsPercent = Math.round((index.budget.stepsUsed / index.budget.maxSteps) * 100);
|
|
276
|
+
const pagesPercent = Math.round((index.budget.pagesVisited / index.budget.maxPages) * 100);
|
|
277
|
+
lines.push(`| Steps | ${index.budget.stepsUsed} | ${index.budget.maxSteps} | ${stepsPercent}% |`);
|
|
278
|
+
lines.push(`| Pages | ${index.budget.pagesVisited} | ${index.budget.maxPages} | ${pagesPercent}% |`);
|
|
279
|
+
lines.push("");
|
|
280
|
+
|
|
281
|
+
if (index.pages.length > 0) {
|
|
282
|
+
lines.push("## Pages Discovered");
|
|
283
|
+
lines.push("");
|
|
284
|
+
lines.push("| # | Page | URL | Captured |");
|
|
285
|
+
lines.push("|---|------|-----|----------|");
|
|
286
|
+
for (let i = 0; i < index.pages.length; i++) {
|
|
287
|
+
const page = index.pages[i];
|
|
288
|
+
lines.push(`| ${i + 1} | ${page.title || "Untitled"} | ${page.url} | ${formatTime(page.capturedAt)} |`);
|
|
289
|
+
}
|
|
290
|
+
lines.push("");
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (index.actionHistory.length > 0) {
|
|
294
|
+
lines.push("## Action History");
|
|
295
|
+
lines.push("");
|
|
296
|
+
const recentActions = index.actionHistory.slice(-20); // Show last 20 actions
|
|
297
|
+
for (const action of recentActions) {
|
|
298
|
+
const status = action.result === "success" ? "Success" : "Error";
|
|
299
|
+
lines.push(`${action.step}. **${action.tool}** → ${status}`);
|
|
300
|
+
if (action.reasoning) {
|
|
301
|
+
lines.push(` - ${action.reasoning}`);
|
|
302
|
+
}
|
|
303
|
+
if (action.error) {
|
|
304
|
+
lines.push(` - Error: ${action.error}`);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
if (index.actionHistory.length > 20) {
|
|
308
|
+
lines.push(`\n*...and ${index.actionHistory.length - 20} more actions*`);
|
|
309
|
+
}
|
|
310
|
+
lines.push("");
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return lines.join("\n");
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Helper to generate checkpoint markdown content
|
|
317
|
+
function generateCheckpointMarkdown(checkpoint: CrawlCheckpoint): string {
|
|
318
|
+
const lines: string[] = [];
|
|
319
|
+
lines.push(`# Crawl Checkpoint`);
|
|
320
|
+
lines.push("");
|
|
321
|
+
lines.push(`**Step**: ${checkpoint.step}`);
|
|
322
|
+
lines.push(`**Saved**: ${formatDate(checkpoint.timestamp)}`);
|
|
323
|
+
lines.push(`**Current URL**: ${checkpoint.currentUrl}`);
|
|
324
|
+
lines.push(`**Can Resume**: ${checkpoint.canResume ? "Yes" : "No"}`);
|
|
325
|
+
lines.push("");
|
|
326
|
+
lines.push("## Progress");
|
|
327
|
+
lines.push("");
|
|
328
|
+
lines.push(checkpoint.goalProgress);
|
|
329
|
+
lines.push("");
|
|
330
|
+
lines.push("## Visited URLs");
|
|
331
|
+
lines.push("");
|
|
332
|
+
for (const url of checkpoint.visitedUrls) {
|
|
333
|
+
lines.push(`- ${url}`);
|
|
334
|
+
}
|
|
335
|
+
lines.push("");
|
|
336
|
+
|
|
337
|
+
if (checkpoint.loopDetection) {
|
|
338
|
+
lines.push("## Loop Detection State");
|
|
339
|
+
lines.push("");
|
|
340
|
+
if (checkpoint.loopDetection.urlVisits.length > 0) {
|
|
341
|
+
lines.push("### URL Visit Counts");
|
|
342
|
+
for (const [url, count] of checkpoint.loopDetection.urlVisits) {
|
|
343
|
+
if (count > 1) {
|
|
344
|
+
lines.push(`- \`${url}\`: ${count} visits`);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
lines.push("");
|
|
348
|
+
}
|
|
349
|
+
lines.push(`- Recent action signatures tracked: ${checkpoint.loopDetection.recentActions.length}`);
|
|
350
|
+
lines.push(`- DOM signatures in window: ${checkpoint.loopDetection.domSignatures.length}`);
|
|
351
|
+
lines.push("");
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return lines.join("\n");
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Helper to generate page snapshot markdown content
|
|
358
|
+
function generateSnapshotMarkdown(
|
|
359
|
+
pageId: string,
|
|
360
|
+
url: string,
|
|
361
|
+
title: string,
|
|
362
|
+
capturedAt: string,
|
|
363
|
+
snapshotData: AccessibilityNode
|
|
364
|
+
): string {
|
|
365
|
+
const lines: string[] = [];
|
|
366
|
+
lines.push(`# Page Snapshot: ${title || "Untitled"}`);
|
|
367
|
+
lines.push("");
|
|
368
|
+
lines.push(`**URL**: ${url}`);
|
|
369
|
+
lines.push(`**Captured**: ${formatTime(capturedAt)}`);
|
|
370
|
+
lines.push("");
|
|
371
|
+
lines.push("## Accessibility Tree");
|
|
372
|
+
lines.push("");
|
|
373
|
+
lines.push("```");
|
|
374
|
+
lines.push(formatAccessibilityTree(snapshotData));
|
|
375
|
+
lines.push("```");
|
|
376
|
+
lines.push("");
|
|
377
|
+
|
|
378
|
+
const interactiveElements = extractInteractiveElements(snapshotData);
|
|
379
|
+
if (interactiveElements.length > 0) {
|
|
380
|
+
lines.push("## Interactive Elements");
|
|
381
|
+
lines.push("");
|
|
382
|
+
lines.push("| Element | Type | Ref |");
|
|
383
|
+
lines.push("|---------|------|-----|");
|
|
384
|
+
for (const el of interactiveElements.slice(0, 50)) { // Limit to 50
|
|
385
|
+
lines.push(`| ${el.element} | ${el.type} | ${el.ref || "-"} |`);
|
|
386
|
+
}
|
|
387
|
+
if (interactiveElements.length > 50) {
|
|
388
|
+
lines.push(`\n*...and ${interactiveElements.length - 50} more elements*`);
|
|
389
|
+
}
|
|
390
|
+
lines.push("");
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
return lines.join("\n");
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Helper to generate test run report markdown content
|
|
397
|
+
function generateTestRunMarkdown(index: TestRunIndex): string {
|
|
398
|
+
const lines: string[] = [];
|
|
399
|
+
const statusEmoji = index.status === "passed" ? "PASSED" : index.status === "failed" ? "FAILED" : index.status.toUpperCase();
|
|
400
|
+
|
|
401
|
+
lines.push(`# Test Run Report`);
|
|
402
|
+
lines.push("");
|
|
403
|
+
lines.push(`**Test**: ${index.testName} (${index.testCaseId})`);
|
|
404
|
+
lines.push(`**Status**: ${statusEmoji}`);
|
|
405
|
+
lines.push(`**Started**: ${formatDate(index.startedAt)}`);
|
|
406
|
+
if (index.completedAt) {
|
|
407
|
+
lines.push(`**Completed**: ${formatDate(index.completedAt)}`);
|
|
408
|
+
}
|
|
409
|
+
lines.push(`**Run ID**: ${index.runId}`);
|
|
410
|
+
lines.push("");
|
|
411
|
+
|
|
412
|
+
if (index.steps.length > 0) {
|
|
413
|
+
const passedSteps = index.steps.filter(s => s.status === "passed").length;
|
|
414
|
+
const failedSteps = index.steps.filter(s => s.status === "failed").length;
|
|
415
|
+
const errorSteps = index.steps.filter(s => s.status === "error").length;
|
|
416
|
+
|
|
417
|
+
lines.push("## Summary");
|
|
418
|
+
lines.push("");
|
|
419
|
+
lines.push("| Metric | Value |");
|
|
420
|
+
lines.push("|--------|-------|");
|
|
421
|
+
lines.push(`| Total Steps | ${index.steps.length} |`);
|
|
422
|
+
lines.push(`| Passed | ${passedSteps} |`);
|
|
423
|
+
lines.push(`| Failed | ${failedSteps} |`);
|
|
424
|
+
lines.push(`| Errors | ${errorSteps} |`);
|
|
425
|
+
const successRate = Math.round((passedSteps / index.steps.length) * 100);
|
|
426
|
+
lines.push(`| Success Rate | ${successRate}% |`);
|
|
427
|
+
lines.push("");
|
|
428
|
+
|
|
429
|
+
lines.push("## Step Results");
|
|
430
|
+
lines.push("");
|
|
431
|
+
|
|
432
|
+
for (const step of index.steps) {
|
|
433
|
+
const stepStatus = step.status === "passed" ? "PASSED" : step.status === "failed" ? "FAILED" : step.status.toUpperCase();
|
|
434
|
+
lines.push(`### Step ${step.stepNumber}: ${stepStatus}`);
|
|
435
|
+
lines.push("");
|
|
436
|
+
lines.push(`**Executed**: ${formatTime(step.executedAt)}`);
|
|
437
|
+
if (step.actualResult) {
|
|
438
|
+
lines.push(`**Result**: ${step.actualResult}`);
|
|
439
|
+
}
|
|
440
|
+
if (step.errorMessage) {
|
|
441
|
+
lines.push(`**Error**: ${step.errorMessage}`);
|
|
442
|
+
}
|
|
443
|
+
lines.push("");
|
|
444
|
+
|
|
445
|
+
if (step.evidence.screenshotUri || step.evidence.snapshotUri) {
|
|
446
|
+
lines.push("**Evidence**:");
|
|
447
|
+
if (step.evidence.screenshotUri) {
|
|
448
|
+
lines.push(`- [Screenshot](./steps/${step.stepNumber}/screenshot.png)`);
|
|
449
|
+
}
|
|
450
|
+
if (step.evidence.snapshotUri) {
|
|
451
|
+
lines.push(`- [Snapshot](./steps/${step.stepNumber}/snapshot.md)`);
|
|
452
|
+
}
|
|
453
|
+
lines.push("");
|
|
454
|
+
}
|
|
455
|
+
lines.push("---");
|
|
456
|
+
lines.push("");
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
return lines.join("\n");
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// Generate a date-time based folder name: YYYY-MM-DD_HH-mm
|
|
464
|
+
function generateWorkspaceFolderName(): string {
|
|
465
|
+
const now = new Date();
|
|
466
|
+
const year = now.getFullYear();
|
|
467
|
+
const month = String(now.getMonth() + 1).padStart(2, "0");
|
|
468
|
+
const day = String(now.getDate()).padStart(2, "0");
|
|
469
|
+
const hours = String(now.getHours()).padStart(2, "0");
|
|
470
|
+
const minutes = String(now.getMinutes()).padStart(2, "0");
|
|
471
|
+
return `${year}-${month}-${day}_${hours}-${minutes}`;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// Generate a date-time based folder name with seconds: YYYY-MM-DD_HH-mm-ss
|
|
475
|
+
function generateCrawlFolderName(): string {
|
|
476
|
+
const now = new Date();
|
|
477
|
+
const year = now.getFullYear();
|
|
478
|
+
const month = String(now.getMonth() + 1).padStart(2, "0");
|
|
479
|
+
const day = String(now.getDate()).padStart(2, "0");
|
|
480
|
+
const hours = String(now.getHours()).padStart(2, "0");
|
|
481
|
+
const minutes = String(now.getMinutes()).padStart(2, "0");
|
|
482
|
+
const seconds = String(now.getSeconds()).padStart(2, "0");
|
|
483
|
+
return `${year}-${month}-${day}_${hours}-${minutes}-${seconds}`;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
return {
|
|
487
|
+
async createWorkspace(params) {
|
|
488
|
+
const analysisId = generateWorkspaceFolderName();
|
|
489
|
+
const workspacePath = getWorkspacePath(analysisId);
|
|
490
|
+
|
|
491
|
+
logger.info("Creating workspace", { analysisId, url: params.url });
|
|
492
|
+
|
|
493
|
+
await ensureDir(workspacePath);
|
|
494
|
+
await ensureDir(join(workspacePath, "crawls"));
|
|
495
|
+
await ensureDir(join(workspacePath, "features"));
|
|
496
|
+
await ensureDir(join(workspacePath, "tests"));
|
|
497
|
+
await ensureDir(join(workspacePath, "runs"));
|
|
498
|
+
|
|
499
|
+
const domain = new URL(params.url).hostname;
|
|
500
|
+
|
|
501
|
+
const index: WorkspaceIndex = {
|
|
502
|
+
analysisId,
|
|
503
|
+
url: params.url,
|
|
504
|
+
domain,
|
|
505
|
+
focus: params.focus,
|
|
506
|
+
createdAt: new Date().toISOString(),
|
|
507
|
+
updatedAt: new Date().toISOString(),
|
|
508
|
+
status: "active",
|
|
509
|
+
limits: params.limits,
|
|
510
|
+
crawls: [],
|
|
511
|
+
featureFlows: [],
|
|
512
|
+
runs: [],
|
|
513
|
+
};
|
|
514
|
+
|
|
515
|
+
const markdown = generateWorkspaceMarkdown(index);
|
|
516
|
+
await writeMarkdownWithFrontmatter(join(workspacePath, "index.md"), index, markdown);
|
|
517
|
+
|
|
518
|
+
logger.info("Workspace created", { analysisId, workspacePath });
|
|
519
|
+
|
|
520
|
+
return { analysisId, workspacePath };
|
|
521
|
+
},
|
|
522
|
+
|
|
523
|
+
getWorkspacePath,
|
|
524
|
+
|
|
525
|
+
async workspaceExists(analysisId) {
|
|
526
|
+
return fileExists(join(getWorkspacePath(analysisId), "index.md"));
|
|
527
|
+
},
|
|
528
|
+
|
|
529
|
+
async readWorkspaceIndex(analysisId) {
|
|
530
|
+
return readMarkdownFrontmatter<WorkspaceIndex>(
|
|
531
|
+
join(getWorkspacePath(analysisId), "index.md")
|
|
532
|
+
);
|
|
533
|
+
},
|
|
534
|
+
|
|
535
|
+
async updateWorkspaceIndex(analysisId, update) {
|
|
536
|
+
const indexPath = join(getWorkspacePath(analysisId), "index.md");
|
|
537
|
+
const current = await readMarkdownFrontmatter<WorkspaceIndex>(indexPath);
|
|
538
|
+
const updated: WorkspaceIndex = {
|
|
539
|
+
...current,
|
|
540
|
+
...update,
|
|
541
|
+
updatedAt: new Date().toISOString(),
|
|
542
|
+
};
|
|
543
|
+
const markdown = generateWorkspaceMarkdown(updated);
|
|
544
|
+
await writeMarkdownWithFrontmatter(indexPath, updated, markdown);
|
|
545
|
+
},
|
|
546
|
+
|
|
547
|
+
async createCrawl(analysisId, params) {
|
|
548
|
+
const crawlId = generateCrawlFolderName();
|
|
549
|
+
const crawlPath = getCrawlPath(analysisId, crawlId);
|
|
550
|
+
|
|
551
|
+
logger.info("Creating crawl", { analysisId, crawlId, goal: params.goal });
|
|
552
|
+
|
|
553
|
+
await ensureDir(crawlPath);
|
|
554
|
+
await ensureDir(join(crawlPath, "pages"));
|
|
555
|
+
|
|
556
|
+
const index: CrawlIndex = {
|
|
557
|
+
crawlId,
|
|
558
|
+
analysisId,
|
|
559
|
+
goal: params.goal,
|
|
560
|
+
strategy: params.strategy,
|
|
561
|
+
status: "in_progress",
|
|
562
|
+
startedAt: new Date().toISOString(),
|
|
563
|
+
budget: {
|
|
564
|
+
...params.limits,
|
|
565
|
+
stepsUsed: 0,
|
|
566
|
+
pagesVisited: 0,
|
|
567
|
+
},
|
|
568
|
+
pages: [],
|
|
569
|
+
actionHistory: [],
|
|
570
|
+
loopDetection: {
|
|
571
|
+
domSignatures: {},
|
|
572
|
+
urlVisits: {},
|
|
573
|
+
recentActions: [],
|
|
574
|
+
},
|
|
575
|
+
};
|
|
576
|
+
|
|
577
|
+
const markdown = generateCrawlMarkdown(index);
|
|
578
|
+
await writeMarkdownWithFrontmatter(join(crawlPath, "index.md"), index, markdown);
|
|
579
|
+
|
|
580
|
+
// Update workspace index
|
|
581
|
+
const workspace = await this.readWorkspaceIndex(analysisId);
|
|
582
|
+
workspace.crawls.push({
|
|
583
|
+
crawlId,
|
|
584
|
+
goal: params.goal,
|
|
585
|
+
status: "in_progress",
|
|
586
|
+
startedAt: index.startedAt,
|
|
587
|
+
pagesVisited: 0,
|
|
588
|
+
stepsExecuted: 0,
|
|
589
|
+
});
|
|
590
|
+
await this.updateWorkspaceIndex(analysisId, { crawls: workspace.crawls });
|
|
591
|
+
|
|
592
|
+
return { crawlId, crawlPath };
|
|
593
|
+
},
|
|
594
|
+
|
|
595
|
+
getCrawlPath,
|
|
596
|
+
|
|
597
|
+
async readCrawlIndex(analysisId, crawlId) {
|
|
598
|
+
return readMarkdownFrontmatter<CrawlIndex>(
|
|
599
|
+
join(getCrawlPath(analysisId, crawlId), "index.md")
|
|
600
|
+
);
|
|
601
|
+
},
|
|
602
|
+
|
|
603
|
+
async updateCrawlIndex(analysisId, crawlId, update) {
|
|
604
|
+
const indexPath = join(getCrawlPath(analysisId, crawlId), "index.md");
|
|
605
|
+
const current = await readMarkdownFrontmatter<CrawlIndex>(indexPath);
|
|
606
|
+
const updated: CrawlIndex = { ...current, ...update };
|
|
607
|
+
const markdown = generateCrawlMarkdown(updated);
|
|
608
|
+
await writeMarkdownWithFrontmatter(indexPath, updated, markdown);
|
|
609
|
+
},
|
|
610
|
+
|
|
611
|
+
async savePage(analysisId, crawlId, page) {
|
|
612
|
+
const pageId = randomUUID();
|
|
613
|
+
const pagesDir = join(getCrawlPath(analysisId, crawlId), "pages", pageId);
|
|
614
|
+
const capturedAt = new Date().toISOString();
|
|
615
|
+
|
|
616
|
+
await ensureDir(pagesDir);
|
|
617
|
+
|
|
618
|
+
// Parse snapshot JSON to create markdown
|
|
619
|
+
let snapshotData: AccessibilityNode;
|
|
620
|
+
try {
|
|
621
|
+
snapshotData = JSON.parse(page.snapshot) as AccessibilityNode;
|
|
622
|
+
} catch {
|
|
623
|
+
snapshotData = { role: "document", name: "Parse error" };
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// Save snapshot as markdown with frontmatter
|
|
627
|
+
const snapshotFilePath = join(pagesDir, "snapshot.md");
|
|
628
|
+
const snapshotFrontmatter = {
|
|
629
|
+
pageId,
|
|
630
|
+
url: page.url,
|
|
631
|
+
title: page.title,
|
|
632
|
+
capturedAt,
|
|
633
|
+
snapshot: snapshotData,
|
|
634
|
+
};
|
|
635
|
+
const snapshotMarkdown = generateSnapshotMarkdown(pageId, page.url, page.title, capturedAt, snapshotData);
|
|
636
|
+
await writeMarkdownWithFrontmatter(snapshotFilePath, snapshotFrontmatter, snapshotMarkdown);
|
|
637
|
+
|
|
638
|
+
// Save screenshot
|
|
639
|
+
const ext = page.screenshot.mimeType.includes("png") ? "png" : "jpg";
|
|
640
|
+
const screenshotFilePath = join(pagesDir, `screenshot.${ext}`);
|
|
641
|
+
const screenshotBuffer = Buffer.from(page.screenshot.data, "base64");
|
|
642
|
+
await writeFile(screenshotFilePath, screenshotBuffer);
|
|
643
|
+
|
|
644
|
+
// Save DOM
|
|
645
|
+
const domFilePath = join(pagesDir, "dom.html");
|
|
646
|
+
await writeFile(domFilePath, page.dom, "utf-8");
|
|
647
|
+
|
|
648
|
+
const pageRef: PageReference = {
|
|
649
|
+
pageId,
|
|
650
|
+
url: page.url,
|
|
651
|
+
title: page.title,
|
|
652
|
+
capturedAt,
|
|
653
|
+
snapshotFilePath,
|
|
654
|
+
snapshotUri: `webtest://${analysisId}/crawls/${crawlId}/pages/${pageId}/snapshot.md`,
|
|
655
|
+
screenshotFilePath,
|
|
656
|
+
screenshotUri: `webtest://${analysisId}/crawls/${crawlId}/pages/${pageId}/screenshot.${ext}`,
|
|
657
|
+
domFilePath,
|
|
658
|
+
domUri: `webtest://${analysisId}/crawls/${crawlId}/pages/${pageId}/dom.html`,
|
|
659
|
+
};
|
|
660
|
+
|
|
661
|
+
// Update crawl index
|
|
662
|
+
const crawlIndex = await this.readCrawlIndex(analysisId, crawlId);
|
|
663
|
+
crawlIndex.pages.push(pageRef);
|
|
664
|
+
crawlIndex.budget.pagesVisited = crawlIndex.pages.length;
|
|
665
|
+
await this.updateCrawlIndex(analysisId, crawlId, {
|
|
666
|
+
pages: crawlIndex.pages,
|
|
667
|
+
budget: crawlIndex.budget,
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
logger.debug("Page saved", { analysisId, crawlId, pageId, url: page.url });
|
|
671
|
+
|
|
672
|
+
return pageRef;
|
|
673
|
+
},
|
|
674
|
+
|
|
675
|
+
async saveCheckpoint(analysisId, crawlId, checkpoint) {
|
|
676
|
+
const crawlPath = getCrawlPath(analysisId, crawlId);
|
|
677
|
+
const markdown = generateCheckpointMarkdown(checkpoint);
|
|
678
|
+
await writeMarkdownWithFrontmatter(join(crawlPath, "checkpoint.md"), checkpoint, markdown);
|
|
679
|
+
|
|
680
|
+
await this.updateCrawlIndex(analysisId, crawlId, { checkpoint });
|
|
681
|
+
|
|
682
|
+
logger.info("Checkpoint saved", {
|
|
683
|
+
analysisId,
|
|
684
|
+
crawlId,
|
|
685
|
+
step: checkpoint.step,
|
|
686
|
+
});
|
|
687
|
+
},
|
|
688
|
+
|
|
689
|
+
async loadCheckpoint(analysisId, crawlId) {
|
|
690
|
+
const checkpointPath = join(
|
|
691
|
+
getCrawlPath(analysisId, crawlId),
|
|
692
|
+
"checkpoint.md"
|
|
693
|
+
);
|
|
694
|
+
|
|
695
|
+
if (await fileExists(checkpointPath)) {
|
|
696
|
+
return readMarkdownFrontmatter<CrawlCheckpoint>(checkpointPath);
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
return null;
|
|
700
|
+
},
|
|
701
|
+
|
|
702
|
+
async recordAction(analysisId, crawlId, action) {
|
|
703
|
+
const crawlIndex = await this.readCrawlIndex(analysisId, crawlId);
|
|
704
|
+
crawlIndex.actionHistory.push(action);
|
|
705
|
+
crawlIndex.budget.stepsUsed = crawlIndex.actionHistory.length;
|
|
706
|
+
|
|
707
|
+
// Update loop detection
|
|
708
|
+
const actionKey = `${action.tool}:${JSON.stringify(action.args)}`;
|
|
709
|
+
crawlIndex.loopDetection.recentActions.push(actionKey);
|
|
710
|
+
if (crawlIndex.loopDetection.recentActions.length > 10) {
|
|
711
|
+
crawlIndex.loopDetection.recentActions.shift();
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
await this.updateCrawlIndex(analysisId, crawlId, {
|
|
715
|
+
actionHistory: crawlIndex.actionHistory,
|
|
716
|
+
budget: crawlIndex.budget,
|
|
717
|
+
loopDetection: crawlIndex.loopDetection,
|
|
718
|
+
});
|
|
719
|
+
},
|
|
720
|
+
|
|
721
|
+
getFeaturePath(analysisId: string, featureSlug: string): string {
|
|
722
|
+
return join(getWorkspacePath(analysisId), "features", featureSlug);
|
|
723
|
+
},
|
|
724
|
+
|
|
725
|
+
async saveFeatures(analysisId, features) {
|
|
726
|
+
const workspacePath = getWorkspacePath(analysisId);
|
|
727
|
+
|
|
728
|
+
// Save features.md at workspace root
|
|
729
|
+
const featuresFilePath = join(workspacePath, "features.md");
|
|
730
|
+
await writeMarkdownWithFrontmatter(
|
|
731
|
+
featuresFilePath,
|
|
732
|
+
features.frontmatter,
|
|
733
|
+
features.markdown
|
|
734
|
+
);
|
|
735
|
+
|
|
736
|
+
const featuresUri = `webtest://${analysisId}/features.md`;
|
|
737
|
+
const featureCount = features.frontmatter.features?.length || 0;
|
|
738
|
+
|
|
739
|
+
await this.updateWorkspaceIndex(analysisId, {
|
|
740
|
+
features: {
|
|
741
|
+
createdAt: new Date().toISOString(),
|
|
742
|
+
featuresFilePath,
|
|
743
|
+
featuresUri,
|
|
744
|
+
featureCount,
|
|
745
|
+
features: features.frontmatter.features || [],
|
|
746
|
+
},
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
logger.info("Features saved", { analysisId, featureCount });
|
|
750
|
+
|
|
751
|
+
return { featuresFilePath, featuresUri, featureCount };
|
|
752
|
+
},
|
|
753
|
+
|
|
754
|
+
async saveFlows(analysisId, featureSlug, flows) {
|
|
755
|
+
const featureDir = join(getWorkspacePath(analysisId), "features", featureSlug);
|
|
756
|
+
await ensureDir(featureDir);
|
|
757
|
+
|
|
758
|
+
// Save flows.md under features/<slug>/
|
|
759
|
+
const flowsFilePath = join(featureDir, "flows.md");
|
|
760
|
+
await writeMarkdownWithFrontmatter(
|
|
761
|
+
flowsFilePath,
|
|
762
|
+
flows.frontmatter as object,
|
|
763
|
+
flows.markdown
|
|
764
|
+
);
|
|
765
|
+
|
|
766
|
+
const flowsUri = `webtest://${analysisId}/features/${featureSlug}/flows.md`;
|
|
767
|
+
const flowsData = flows.frontmatter as { flows?: unknown[] };
|
|
768
|
+
const flowCount = flowsData.flows?.length || 0;
|
|
769
|
+
|
|
770
|
+
// Update workspace featureFlows array
|
|
771
|
+
const workspace = await this.readWorkspaceIndex(analysisId);
|
|
772
|
+
const featureFlows = workspace.featureFlows || [];
|
|
773
|
+
|
|
774
|
+
// Remove existing entry for this feature if present
|
|
775
|
+
const existingIdx = featureFlows.findIndex(f => f.featureSlug === featureSlug);
|
|
776
|
+
if (existingIdx >= 0) {
|
|
777
|
+
featureFlows.splice(existingIdx, 1);
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
featureFlows.push({
|
|
781
|
+
featureSlug,
|
|
782
|
+
createdAt: new Date().toISOString(),
|
|
783
|
+
flowsFilePath,
|
|
784
|
+
flowsUri,
|
|
785
|
+
flowCount,
|
|
786
|
+
});
|
|
787
|
+
|
|
788
|
+
await this.updateWorkspaceIndex(analysisId, { featureFlows });
|
|
789
|
+
|
|
790
|
+
logger.info("Flows saved", { analysisId, featureSlug, flowCount });
|
|
791
|
+
|
|
792
|
+
return { flowsFilePath, flowsUri, flowCount };
|
|
793
|
+
},
|
|
794
|
+
|
|
795
|
+
async saveTests(analysisId, tests) {
|
|
796
|
+
const testsDir = join(getWorkspacePath(analysisId), "tests");
|
|
797
|
+
|
|
798
|
+
// Save tests as single markdown file with frontmatter
|
|
799
|
+
const testsFilePath = join(testsDir, "tests.md");
|
|
800
|
+
await writeMarkdownWithFrontmatter(
|
|
801
|
+
testsFilePath,
|
|
802
|
+
tests.frontmatter as object,
|
|
803
|
+
tests.markdown
|
|
804
|
+
);
|
|
805
|
+
|
|
806
|
+
const testsUri = `webtest://${analysisId}/tests/tests.md`;
|
|
807
|
+
const testArray = tests.frontmatter as { tests?: unknown[] };
|
|
808
|
+
const testCount = testArray.tests?.length || 0;
|
|
809
|
+
|
|
810
|
+
await this.updateWorkspaceIndex(analysisId, {
|
|
811
|
+
tests: {
|
|
812
|
+
createdAt: new Date().toISOString(),
|
|
813
|
+
testsFilePath,
|
|
814
|
+
testsUri,
|
|
815
|
+
testCount,
|
|
816
|
+
},
|
|
817
|
+
});
|
|
818
|
+
|
|
819
|
+
logger.info("Tests saved", { analysisId, testCount });
|
|
820
|
+
|
|
821
|
+
return { testsFilePath, testsUri, testCount };
|
|
822
|
+
},
|
|
823
|
+
|
|
824
|
+
async createTestRun(analysisId, params) {
|
|
825
|
+
const runId = randomUUID();
|
|
826
|
+
const runPath = getTestRunPath(analysisId, runId);
|
|
827
|
+
|
|
828
|
+
await ensureDir(runPath);
|
|
829
|
+
await ensureDir(join(runPath, "steps"));
|
|
830
|
+
|
|
831
|
+
const index: TestRunIndex = {
|
|
832
|
+
runId,
|
|
833
|
+
analysisId,
|
|
834
|
+
testCaseId: params.testCaseId,
|
|
835
|
+
testName: params.testName,
|
|
836
|
+
status: "in_progress",
|
|
837
|
+
startedAt: new Date().toISOString(),
|
|
838
|
+
steps: [],
|
|
839
|
+
};
|
|
840
|
+
|
|
841
|
+
const markdown = generateTestRunMarkdown(index);
|
|
842
|
+
await writeMarkdownWithFrontmatter(join(runPath, "report.md"), index, markdown);
|
|
843
|
+
|
|
844
|
+
// Update workspace index
|
|
845
|
+
const workspace = await this.readWorkspaceIndex(analysisId);
|
|
846
|
+
workspace.runs.push({
|
|
847
|
+
runId,
|
|
848
|
+
testCaseId: params.testCaseId,
|
|
849
|
+
status: "in_progress",
|
|
850
|
+
startedAt: index.startedAt,
|
|
851
|
+
});
|
|
852
|
+
await this.updateWorkspaceIndex(analysisId, { runs: workspace.runs });
|
|
853
|
+
|
|
854
|
+
logger.info("Test run created", { analysisId, runId, testCaseId: params.testCaseId });
|
|
855
|
+
|
|
856
|
+
return { runId, runPath };
|
|
857
|
+
},
|
|
858
|
+
|
|
859
|
+
getTestRunPath,
|
|
860
|
+
|
|
861
|
+
async readTestRunIndex(analysisId, runId) {
|
|
862
|
+
return readMarkdownFrontmatter<TestRunIndex>(
|
|
863
|
+
join(getTestRunPath(analysisId, runId), "report.md")
|
|
864
|
+
);
|
|
865
|
+
},
|
|
866
|
+
|
|
867
|
+
async updateTestRunIndex(analysisId, runId, update) {
|
|
868
|
+
const indexPath = join(getTestRunPath(analysisId, runId), "report.md");
|
|
869
|
+
const current = await readMarkdownFrontmatter<TestRunIndex>(indexPath);
|
|
870
|
+
const updated: TestRunIndex = { ...current, ...update };
|
|
871
|
+
const markdown = generateTestRunMarkdown(updated);
|
|
872
|
+
await writeMarkdownWithFrontmatter(indexPath, updated, markdown);
|
|
873
|
+
},
|
|
874
|
+
|
|
875
|
+
async saveTestStepEvidence(analysisId, runId, stepNumber, evidence) {
|
|
876
|
+
const stepDir = join(
|
|
877
|
+
getTestRunPath(analysisId, runId),
|
|
878
|
+
"steps",
|
|
879
|
+
String(stepNumber)
|
|
880
|
+
);
|
|
881
|
+
await ensureDir(stepDir);
|
|
882
|
+
|
|
883
|
+
const result: {
|
|
884
|
+
screenshotFilePath?: string;
|
|
885
|
+
screenshotUri?: string;
|
|
886
|
+
snapshotFilePath?: string;
|
|
887
|
+
snapshotUri?: string;
|
|
888
|
+
} = {};
|
|
889
|
+
|
|
890
|
+
if (evidence.screenshot) {
|
|
891
|
+
const ext = evidence.screenshot.mimeType.includes("png") ? "png" : "jpg";
|
|
892
|
+
const screenshotBuffer = Buffer.from(
|
|
893
|
+
evidence.screenshot.data,
|
|
894
|
+
"base64"
|
|
895
|
+
);
|
|
896
|
+
const screenshotFilePath = join(stepDir, `screenshot.${ext}`);
|
|
897
|
+
await writeFile(screenshotFilePath, screenshotBuffer);
|
|
898
|
+
result.screenshotFilePath = screenshotFilePath;
|
|
899
|
+
result.screenshotUri = `webtest://${analysisId}/runs/${runId}/steps/${stepNumber}/screenshot.${ext}`;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
if (evidence.snapshot) {
|
|
903
|
+
// Parse snapshot JSON to create markdown
|
|
904
|
+
let snapshotData: AccessibilityNode;
|
|
905
|
+
try {
|
|
906
|
+
snapshotData = JSON.parse(evidence.snapshot) as AccessibilityNode;
|
|
907
|
+
} catch {
|
|
908
|
+
snapshotData = { role: "document", name: "Parse error" };
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
const snapshotFilePath = join(stepDir, "snapshot.md");
|
|
912
|
+
const snapshotFrontmatter = {
|
|
913
|
+
stepNumber,
|
|
914
|
+
runId,
|
|
915
|
+
analysisId,
|
|
916
|
+
capturedAt: new Date().toISOString(),
|
|
917
|
+
snapshot: snapshotData,
|
|
918
|
+
};
|
|
919
|
+
const snapshotMarkdown = generateSnapshotMarkdown(
|
|
920
|
+
`step-${stepNumber}`,
|
|
921
|
+
"",
|
|
922
|
+
`Step ${stepNumber} Snapshot`,
|
|
923
|
+
snapshotFrontmatter.capturedAt,
|
|
924
|
+
snapshotData
|
|
925
|
+
);
|
|
926
|
+
await writeMarkdownWithFrontmatter(snapshotFilePath, snapshotFrontmatter, snapshotMarkdown);
|
|
927
|
+
result.snapshotFilePath = snapshotFilePath;
|
|
928
|
+
result.snapshotUri = `webtest://${analysisId}/runs/${runId}/steps/${stepNumber}/snapshot.md`;
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
return result;
|
|
932
|
+
},
|
|
933
|
+
|
|
934
|
+
async listWorkspaces() {
|
|
935
|
+
try {
|
|
936
|
+
const entries = await readdir(baseDir, { withFileTypes: true });
|
|
937
|
+
return entries
|
|
938
|
+
.filter((e) => e.isDirectory())
|
|
939
|
+
.map((e) => e.name);
|
|
940
|
+
} catch {
|
|
941
|
+
return [];
|
|
942
|
+
}
|
|
943
|
+
},
|
|
944
|
+
};
|
|
945
|
+
}
|