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.
- package/README.md +59 -40
- package/dist/config.js +8 -8
- package/dist/config.js.map +1 -1
- package/dist/logger.js +1 -1
- package/dist/logger.js.map +1 -1
- package/dist/prompts/index.d.ts +1 -1
- package/dist/prompts/index.d.ts.map +1 -1
- package/dist/prompts/index.js +21 -21
- package/dist/prompts/index.js.map +1 -1
- package/dist/prompts/templates/mcp/retest-crawl.md +7 -0
- package/{src/prompts/templates/mcp/webtest-discover-flows.md → dist/prompts/templates/mcp/retest-discover-flows.md} +1 -1
- package/{src/prompts/templates/mcp/webtest-discover.md → dist/prompts/templates/mcp/retest-discover.md} +2 -2
- package/dist/prompts/templates/mcp/retest-full-workflow.md +12 -0
- package/{src/prompts/templates/mcp/webtest-generate-tests.md → dist/prompts/templates/mcp/retest-generate-tests.md} +1 -1
- package/{src/prompts/templates/mcp/webtest-run-test.md → dist/prompts/templates/mcp/retest-run-test.md} +1 -1
- package/{src/prompts/templates/mcp/webtest-start.md → dist/prompts/templates/mcp/retest-start.md} +1 -1
- package/{src → dist}/prompts/templates/sampling/system-prefix.md +1 -1
- package/dist/resources/index.js +7 -7
- package/dist/resources/index.js.map +1 -1
- package/dist/schemas/config.js +2 -2
- package/dist/schemas/config.js.map +1 -1
- package/dist/security/index.js +1 -1
- package/dist/security/index.js.map +1 -1
- package/dist/server.js +3 -3
- package/dist/server.js.map +1 -1
- package/dist/test-utils/mock-context.js +22 -22
- package/dist/test-utils/mock-context.js.map +1 -1
- package/dist/tools/index.d.ts +1 -1
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +5 -5
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/retest/crawl.d.ts.map +1 -0
- package/dist/tools/{webtest → retest}/crawl.js +7 -7
- package/dist/tools/retest/crawl.js.map +1 -0
- package/dist/tools/retest/discover-features.d.ts.map +1 -0
- package/dist/tools/{webtest → retest}/discover-features.js +6 -6
- package/dist/tools/retest/discover-features.js.map +1 -0
- package/dist/tools/retest/discover-flows.d.ts.map +1 -0
- package/dist/tools/{webtest → retest}/discover-flows.js +6 -6
- package/dist/tools/retest/discover-flows.js.map +1 -0
- package/dist/tools/retest/generate-tests.d.ts.map +1 -0
- package/dist/tools/{webtest → retest}/generate-tests.js +5 -5
- package/dist/tools/retest/generate-tests.js.map +1 -0
- package/dist/tools/retest/index.d.ts.map +1 -0
- package/dist/tools/retest/index.js.map +1 -0
- package/dist/tools/retest/run-test-case.d.ts.map +1 -0
- package/dist/tools/{webtest → retest}/run-test-case.js +3 -3
- package/dist/tools/retest/run-test-case.js.map +1 -0
- package/dist/tools/retest/schemas.d.ts.map +1 -0
- package/dist/tools/retest/schemas.js.map +1 -0
- package/dist/tools/retest/start-analysis.d.ts.map +1 -0
- package/dist/tools/{webtest → retest}/start-analysis.js +5 -5
- package/dist/tools/retest/start-analysis.js.map +1 -0
- package/dist/workspace/index.js +8 -8
- package/dist/workspace/index.js.map +1 -1
- package/dist/workspace/types.d.ts +2 -2
- package/dist/workspace/types.d.ts.map +1 -1
- package/package.json +6 -2
- package/.claude/commands/openspec/apply.md +0 -23
- package/.claude/commands/openspec/archive.md +0 -27
- package/.claude/commands/openspec/proposal.md +0 -28
- package/.gemini/commands/openspec/apply.toml +0 -21
- package/.gemini/commands/openspec/archive.toml +0 -25
- package/.gemini/commands/openspec/proposal.toml +0 -26
- package/.github/prompts/openspec-apply.prompt.md +0 -22
- package/.github/prompts/openspec-archive.prompt.md +0 -26
- package/.github/prompts/openspec-proposal.prompt.md +0 -27
- package/.github/workflows/release.yml +0 -33
- package/.kilocode/workflows/openspec-apply.md +0 -17
- package/.kilocode/workflows/openspec-archive.md +0 -21
- package/.kilocode/workflows/openspec-proposal.md +0 -22
- package/.mcp.json +0 -23
- package/.opencode/command/openspec-apply.md +0 -25
- package/.opencode/command/openspec-archive.md +0 -28
- package/.opencode/command/openspec-proposal.md +0 -30
- package/.roo/commands/openspec-apply.md +0 -20
- package/.roo/commands/openspec-archive.md +0 -24
- package/.roo/commands/openspec-proposal.md +0 -25
- package/.vscode/mcp.json +0 -23
- package/AGENTS.md +0 -18
- package/CLAUDE.md +0 -18
- package/dist/tools/webtest/crawl.d.ts.map +0 -1
- package/dist/tools/webtest/crawl.js.map +0 -1
- package/dist/tools/webtest/discover-features.d.ts.map +0 -1
- package/dist/tools/webtest/discover-features.js.map +0 -1
- package/dist/tools/webtest/discover-flows.d.ts.map +0 -1
- package/dist/tools/webtest/discover-flows.js.map +0 -1
- package/dist/tools/webtest/generate-tests.d.ts.map +0 -1
- package/dist/tools/webtest/generate-tests.js.map +0 -1
- package/dist/tools/webtest/index.d.ts.map +0 -1
- package/dist/tools/webtest/index.js.map +0 -1
- package/dist/tools/webtest/run-test-case.d.ts.map +0 -1
- package/dist/tools/webtest/run-test-case.js.map +0 -1
- package/dist/tools/webtest/schemas.d.ts.map +0 -1
- package/dist/tools/webtest/schemas.js.map +0 -1
- package/dist/tools/webtest/start-analysis.d.ts.map +0 -1
- package/dist/tools/webtest/start-analysis.js.map +0 -1
- package/openspec/AGENTS.md +0 -456
- package/openspec/changes/archive/2025-12-18-add-hybrid-artifact-paths/proposal.md +0 -33
- package/openspec/changes/archive/2025-12-18-add-hybrid-artifact-paths/specs/webtest-resources/spec.md +0 -27
- package/openspec/changes/archive/2025-12-18-add-hybrid-artifact-paths/specs/webtest-tools/spec.md +0 -304
- package/openspec/changes/archive/2025-12-18-add-hybrid-artifact-paths/tasks.md +0 -43
- package/openspec/changes/archive/2025-12-18-add-mcp-server-foundation/design.md +0 -209
- package/openspec/changes/archive/2025-12-18-add-mcp-server-foundation/proposal.md +0 -41
- package/openspec/changes/archive/2025-12-18-add-mcp-server-foundation/specs/mcp-server-core/spec.md +0 -183
- package/openspec/changes/archive/2025-12-18-add-mcp-server-foundation/tasks.md +0 -112
- package/openspec/changes/archive/2025-12-18-add-webtest-orchestrator/design.md +0 -333
- package/openspec/changes/archive/2025-12-18-add-webtest-orchestrator/proposal.md +0 -66
- package/openspec/changes/archive/2025-12-18-add-webtest-orchestrator/specs/mcp-server-core/spec.md +0 -129
- package/openspec/changes/archive/2025-12-18-add-webtest-orchestrator/specs/webtest-lifecycle/spec.md +0 -138
- package/openspec/changes/archive/2025-12-18-add-webtest-orchestrator/specs/webtest-logging/spec.md +0 -211
- package/openspec/changes/archive/2025-12-18-add-webtest-orchestrator/specs/webtest-prompts/spec.md +0 -157
- package/openspec/changes/archive/2025-12-18-add-webtest-orchestrator/specs/webtest-resources/spec.md +0 -213
- package/openspec/changes/archive/2025-12-18-add-webtest-orchestrator/specs/webtest-sampling/spec.md +0 -257
- package/openspec/changes/archive/2025-12-18-add-webtest-orchestrator/specs/webtest-tools/spec.md +0 -501
- package/openspec/changes/archive/2025-12-18-add-webtest-orchestrator/tasks.md +0 -264
- package/openspec/changes/archive/2025-12-18-allow-analysis-of-incomplete-crawls/proposal.md +0 -24
- package/openspec/changes/archive/2025-12-18-allow-analysis-of-incomplete-crawls/specs/webtest-tools/spec.md +0 -80
- package/openspec/changes/archive/2025-12-18-allow-analysis-of-incomplete-crawls/tasks.md +0 -8
- package/openspec/changes/archive/2025-12-18-fix-crawl-loop-stability/design.md +0 -90
- package/openspec/changes/archive/2025-12-18-fix-crawl-loop-stability/proposal.md +0 -28
- package/openspec/changes/archive/2025-12-18-fix-crawl-loop-stability/specs/webtest-sampling/spec.md +0 -90
- package/openspec/changes/archive/2025-12-18-fix-crawl-loop-stability/tasks.md +0 -33
- package/openspec/changes/archive/2025-12-18-use-markdown-artifacts/design.md +0 -558
- package/openspec/changes/archive/2025-12-18-use-markdown-artifacts/proposal.md +0 -119
- package/openspec/changes/archive/2025-12-18-use-markdown-artifacts/specs/webtest-resources/spec.md +0 -109
- package/openspec/changes/archive/2025-12-18-use-markdown-artifacts/specs/webtest-tools/spec.md +0 -121
- package/openspec/changes/archive/2025-12-18-use-markdown-artifacts/tasks.md +0 -133
- package/openspec/changes/extract-prompts-to-markdown/design.md +0 -86
- package/openspec/changes/extract-prompts-to-markdown/proposal.md +0 -50
- package/openspec/changes/extract-prompts-to-markdown/specs/webtest-prompts/spec.md +0 -74
- package/openspec/changes/extract-prompts-to-markdown/tasks.md +0 -40
- package/openspec/changes/refactor-webtest-naming/design.md +0 -95
- package/openspec/changes/refactor-webtest-naming/proposal.md +0 -66
- package/openspec/changes/refactor-webtest-naming/specs/webtest-prompts/spec.md +0 -79
- package/openspec/changes/refactor-webtest-naming/specs/webtest-resources/spec.md +0 -80
- package/openspec/changes/refactor-webtest-naming/specs/webtest-sampling/spec.md +0 -122
- package/openspec/changes/refactor-webtest-naming/specs/webtest-tools/spec.md +0 -113
- package/openspec/changes/refactor-webtest-naming/tasks.md +0 -119
- package/openspec/changes/rename-package-to-retest/proposal.md +0 -52
- package/openspec/changes/rename-package-to-retest/specs/mcp-server-core/spec.md +0 -53
- package/openspec/changes/rename-package-to-retest/specs/retest-lifecycle/spec.md +0 -68
- package/openspec/changes/rename-package-to-retest/specs/retest-logging/spec.md +0 -35
- package/openspec/changes/rename-package-to-retest/specs/retest-prompts/spec.md +0 -159
- package/openspec/changes/rename-package-to-retest/specs/retest-resources/spec.md +0 -251
- package/openspec/changes/rename-package-to-retest/specs/retest-sampling/spec.md +0 -99
- package/openspec/changes/rename-package-to-retest/specs/retest-tools/spec.md +0 -295
- package/openspec/changes/rename-package-to-retest/tasks.md +0 -71
- package/openspec/project.md +0 -31
- package/openspec/specs/mcp-server-core/spec.md +0 -178
- package/openspec/specs/webtest-lifecycle/spec.md +0 -136
- package/openspec/specs/webtest-logging/spec.md +0 -209
- package/openspec/specs/webtest-prompts/spec.md +0 -155
- package/openspec/specs/webtest-resources/spec.md +0 -248
- package/openspec/specs/webtest-sampling/spec.md +0 -344
- package/openspec/specs/webtest-tools/spec.md +0 -282
- package/release.config.js +0 -9
- package/src/config.test.ts +0 -96
- package/src/config.ts +0 -32
- package/src/elicitation/index.test.ts +0 -399
- package/src/elicitation/index.ts +0 -171
- package/src/elicitation/types.ts +0 -68
- package/src/index.ts +0 -83
- package/src/lifecycle/index.test.ts +0 -260
- package/src/lifecycle/index.ts +0 -101
- package/src/logger.redaction.test.ts +0 -322
- package/src/logger.test.ts +0 -123
- package/src/logger.ts +0 -229
- package/src/playwright-client/index.ts +0 -392
- package/src/playwright-client/types.ts +0 -99
- package/src/progress/index.test.ts +0 -327
- package/src/progress/index.ts +0 -170
- package/src/progress/types.ts +0 -25
- package/src/prompts/index.test.ts +0 -451
- package/src/prompts/index.ts +0 -246
- package/src/prompts/loader.test.ts +0 -100
- package/src/prompts/loader.ts +0 -59
- package/src/prompts/templates/mcp/webtest-crawl.md +0 -7
- package/src/prompts/templates/mcp/webtest-full-workflow.md +0 -12
- package/src/resources/index.ts +0 -250
- package/src/resources/subscriptions.ts +0 -37
- package/src/sampling/index.test.ts +0 -414
- package/src/sampling/index.ts +0 -286
- package/src/sampling/prompts.ts +0 -194
- package/src/sampling/types.ts +0 -60
- package/src/schemas/config.ts +0 -39
- package/src/security/index.test.ts +0 -441
- package/src/security/index.ts +0 -361
- package/src/security/security-scenarios.test.ts +0 -468
- package/src/server.ts +0 -211
- package/src/test-utils/index.ts +0 -6
- package/src/test-utils/mock-context.ts +0 -426
- package/src/test-utils/mock-playwright-client.ts +0 -422
- package/src/tools/index.ts +0 -11
- package/src/tools/webtest/crawl.test.ts +0 -834
- package/src/tools/webtest/crawl.ts +0 -901
- package/src/tools/webtest/discover-features.ts +0 -412
- package/src/tools/webtest/discover-flows.ts +0 -408
- package/src/tools/webtest/generate-tests.test.ts +0 -532
- package/src/tools/webtest/generate-tests.ts +0 -425
- package/src/tools/webtest/index.ts +0 -7
- package/src/tools/webtest/integration.test.ts +0 -536
- package/src/tools/webtest/run-test-case.test.ts +0 -659
- package/src/tools/webtest/run-test-case.ts +0 -508
- package/src/tools/webtest/schemas.ts +0 -201
- package/src/tools/webtest/start-analysis.test.ts +0 -151
- package/src/tools/webtest/start-analysis.ts +0 -158
- package/src/transports/http.ts +0 -19
- package/src/transports/index.ts +0 -30
- package/src/transports/stdio.ts +0 -7
- package/src/types/capabilities.test.ts +0 -193
- package/src/types/capabilities.ts +0 -50
- package/src/types/context.ts +0 -21
- package/src/types/tool.ts +0 -11
- package/src/workspace/index.ts +0 -945
- package/src/workspace/markdown.ts +0 -272
- package/src/workspace/types.ts +0 -186
- package/tests/integration/server.test.ts +0 -89
- package/tests/integration/tools.test.ts +0 -99
- package/tsconfig.json +0 -20
- package/vitest.config.ts +0 -9
- package/vitest.integration.config.ts +0 -10
- /package/{src → dist}/prompts/templates/sampling/crawl-action.md +0 -0
- /package/{src → dist}/prompts/templates/sampling/feature-discovery.md +0 -0
- /package/{src → dist}/prompts/templates/sampling/flow-discovery.md +0 -0
- /package/{src → dist}/prompts/templates/sampling/page-content-wrapper.md +0 -0
- /package/{src → dist}/prompts/templates/sampling/test-evaluation.md +0 -0
- /package/{src → dist}/prompts/templates/sampling/test-generation.md +0 -0
- /package/dist/tools/{webtest → retest}/crawl.d.ts +0 -0
- /package/dist/tools/{webtest → retest}/discover-features.d.ts +0 -0
- /package/dist/tools/{webtest → retest}/discover-flows.d.ts +0 -0
- /package/dist/tools/{webtest → retest}/generate-tests.d.ts +0 -0
- /package/dist/tools/{webtest → retest}/index.d.ts +0 -0
- /package/dist/tools/{webtest → retest}/index.js +0 -0
- /package/dist/tools/{webtest → retest}/run-test-case.d.ts +0 -0
- /package/dist/tools/{webtest → retest}/schemas.d.ts +0 -0
- /package/dist/tools/{webtest → retest}/schemas.js +0 -0
- /package/dist/tools/{webtest → retest}/start-analysis.d.ts +0 -0
|
@@ -1,272 +0,0 @@
|
|
|
1
|
-
import matter from "gray-matter";
|
|
2
|
-
import { readFile, writeFile } from "node:fs/promises";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Write a markdown file with YAML frontmatter
|
|
6
|
-
*/
|
|
7
|
-
export async function writeMarkdownWithFrontmatter<T extends object>(
|
|
8
|
-
filePath: string,
|
|
9
|
-
frontmatter: T,
|
|
10
|
-
content: string
|
|
11
|
-
): Promise<void> {
|
|
12
|
-
const file = matter.stringify(content, frontmatter as object);
|
|
13
|
-
await writeFile(filePath, file, "utf-8");
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Read YAML frontmatter from a markdown file
|
|
18
|
-
*/
|
|
19
|
-
export async function readMarkdownFrontmatter<T>(filePath: string): Promise<T> {
|
|
20
|
-
const fileContent = await readFile(filePath, "utf-8");
|
|
21
|
-
const { data } = matter(fileContent);
|
|
22
|
-
return data as T;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Read both frontmatter and content from a markdown file
|
|
27
|
-
*/
|
|
28
|
-
export async function readMarkdownFile<T>(
|
|
29
|
-
filePath: string
|
|
30
|
-
): Promise<{ data: T; content: string }> {
|
|
31
|
-
const fileContent = await readFile(filePath, "utf-8");
|
|
32
|
-
const { data, content } = matter(fileContent);
|
|
33
|
-
return { data: data as T, content };
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Format an accessibility tree snapshot as a human-readable tree
|
|
38
|
-
*/
|
|
39
|
-
export function formatAccessibilityTree(
|
|
40
|
-
snapshot: AccessibilityNode,
|
|
41
|
-
indent: number = 0
|
|
42
|
-
): string {
|
|
43
|
-
const lines: string[] = [];
|
|
44
|
-
const prefix = indent === 0 ? "" : "│ ".repeat(indent - 1) + "├── ";
|
|
45
|
-
|
|
46
|
-
// Format the node
|
|
47
|
-
let nodeStr = snapshot.role || "unknown";
|
|
48
|
-
if (snapshot.name) {
|
|
49
|
-
nodeStr += ` "${snapshot.name}"`;
|
|
50
|
-
}
|
|
51
|
-
if (snapshot.ref) {
|
|
52
|
-
nodeStr += ` [ref: ${snapshot.ref}]`;
|
|
53
|
-
}
|
|
54
|
-
if (snapshot.level) {
|
|
55
|
-
nodeStr += ` (h${snapshot.level})`;
|
|
56
|
-
}
|
|
57
|
-
if (snapshot.checked !== undefined) {
|
|
58
|
-
nodeStr += snapshot.checked ? " [checked]" : " [unchecked]";
|
|
59
|
-
}
|
|
60
|
-
if (snapshot.selected !== undefined) {
|
|
61
|
-
nodeStr += snapshot.selected ? " [selected]" : "";
|
|
62
|
-
}
|
|
63
|
-
if (snapshot.disabled) {
|
|
64
|
-
nodeStr += " [disabled]";
|
|
65
|
-
}
|
|
66
|
-
if (snapshot.value) {
|
|
67
|
-
nodeStr += ` = "${truncate(snapshot.value, 30)}"`;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
lines.push(prefix + nodeStr);
|
|
71
|
-
|
|
72
|
-
// Format children
|
|
73
|
-
if (snapshot.children && snapshot.children.length > 0) {
|
|
74
|
-
for (let i = 0; i < snapshot.children.length; i++) {
|
|
75
|
-
const child = snapshot.children[i];
|
|
76
|
-
const isLast = i === snapshot.children.length - 1;
|
|
77
|
-
const childPrefix = indent === 0 ? "" : "│ ".repeat(indent - 1) + (isLast ? "└── " : "├── ");
|
|
78
|
-
lines.push(
|
|
79
|
-
...formatAccessibilityTreeInternal(child, indent + 1, isLast).split("\n")
|
|
80
|
-
);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
return lines.join("\n");
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function formatAccessibilityTreeInternal(
|
|
88
|
-
snapshot: AccessibilityNode,
|
|
89
|
-
indent: number,
|
|
90
|
-
isLast: boolean
|
|
91
|
-
): string {
|
|
92
|
-
const lines: string[] = [];
|
|
93
|
-
const connector = isLast ? "└── " : "├── ";
|
|
94
|
-
const childIndent = isLast ? " " : "│ ";
|
|
95
|
-
const prefix = "│ ".repeat(Math.max(0, indent - 1)) + connector;
|
|
96
|
-
|
|
97
|
-
// Format the node
|
|
98
|
-
let nodeStr = snapshot.role || "unknown";
|
|
99
|
-
if (snapshot.name) {
|
|
100
|
-
nodeStr += ` "${snapshot.name}"`;
|
|
101
|
-
}
|
|
102
|
-
if (snapshot.ref) {
|
|
103
|
-
nodeStr += ` [ref: ${snapshot.ref}]`;
|
|
104
|
-
}
|
|
105
|
-
if (snapshot.level) {
|
|
106
|
-
nodeStr += ` (h${snapshot.level})`;
|
|
107
|
-
}
|
|
108
|
-
if (snapshot.checked !== undefined) {
|
|
109
|
-
nodeStr += snapshot.checked ? " [checked]" : " [unchecked]";
|
|
110
|
-
}
|
|
111
|
-
if (snapshot.selected !== undefined) {
|
|
112
|
-
nodeStr += snapshot.selected ? " [selected]" : "";
|
|
113
|
-
}
|
|
114
|
-
if (snapshot.disabled) {
|
|
115
|
-
nodeStr += " [disabled]";
|
|
116
|
-
}
|
|
117
|
-
if (snapshot.value) {
|
|
118
|
-
nodeStr += ` = "${truncate(snapshot.value, 30)}"`;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
lines.push(prefix + nodeStr);
|
|
122
|
-
|
|
123
|
-
// Format children
|
|
124
|
-
if (snapshot.children && snapshot.children.length > 0) {
|
|
125
|
-
for (let i = 0; i < snapshot.children.length; i++) {
|
|
126
|
-
const child = snapshot.children[i];
|
|
127
|
-
const childIsLast = i === snapshot.children.length - 1;
|
|
128
|
-
const baseIndent = "│ ".repeat(Math.max(0, indent - 1)) + childIndent;
|
|
129
|
-
lines.push(
|
|
130
|
-
formatAccessibilityTreeWithBase(child, baseIndent, childIsLast)
|
|
131
|
-
);
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
return lines.join("\n");
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function formatAccessibilityTreeWithBase(
|
|
139
|
-
snapshot: AccessibilityNode,
|
|
140
|
-
baseIndent: string,
|
|
141
|
-
isLast: boolean
|
|
142
|
-
): string {
|
|
143
|
-
const lines: string[] = [];
|
|
144
|
-
const connector = isLast ? "└── " : "├── ";
|
|
145
|
-
const childIndent = isLast ? " " : "│ ";
|
|
146
|
-
|
|
147
|
-
// Format the node
|
|
148
|
-
let nodeStr = snapshot.role || "unknown";
|
|
149
|
-
if (snapshot.name) {
|
|
150
|
-
nodeStr += ` "${snapshot.name}"`;
|
|
151
|
-
}
|
|
152
|
-
if (snapshot.ref) {
|
|
153
|
-
nodeStr += ` [ref: ${snapshot.ref}]`;
|
|
154
|
-
}
|
|
155
|
-
if (snapshot.level) {
|
|
156
|
-
nodeStr += ` (h${snapshot.level})`;
|
|
157
|
-
}
|
|
158
|
-
if (snapshot.checked !== undefined) {
|
|
159
|
-
nodeStr += snapshot.checked ? " [checked]" : " [unchecked]";
|
|
160
|
-
}
|
|
161
|
-
if (snapshot.selected !== undefined) {
|
|
162
|
-
nodeStr += snapshot.selected ? " [selected]" : "";
|
|
163
|
-
}
|
|
164
|
-
if (snapshot.disabled) {
|
|
165
|
-
nodeStr += " [disabled]";
|
|
166
|
-
}
|
|
167
|
-
if (snapshot.value) {
|
|
168
|
-
nodeStr += ` = "${truncate(snapshot.value, 30)}"`;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
lines.push(baseIndent + connector + nodeStr);
|
|
172
|
-
|
|
173
|
-
// Format children
|
|
174
|
-
if (snapshot.children && snapshot.children.length > 0) {
|
|
175
|
-
for (let i = 0; i < snapshot.children.length; i++) {
|
|
176
|
-
const child = snapshot.children[i];
|
|
177
|
-
const childIsLast = i === snapshot.children.length - 1;
|
|
178
|
-
lines.push(
|
|
179
|
-
formatAccessibilityTreeWithBase(child, baseIndent + childIndent, childIsLast)
|
|
180
|
-
);
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
return lines.join("\n");
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
/**
|
|
188
|
-
* Extract interactive elements from an accessibility snapshot
|
|
189
|
-
*/
|
|
190
|
-
export function extractInteractiveElements(
|
|
191
|
-
snapshot: AccessibilityNode
|
|
192
|
-
): Array<{ element: string; type: string; ref?: string }> {
|
|
193
|
-
const elements: Array<{ element: string; type: string; ref?: string }> = [];
|
|
194
|
-
const interactiveRoles = [
|
|
195
|
-
"link",
|
|
196
|
-
"button",
|
|
197
|
-
"textbox",
|
|
198
|
-
"checkbox",
|
|
199
|
-
"radio",
|
|
200
|
-
"combobox",
|
|
201
|
-
"menuitem",
|
|
202
|
-
"tab",
|
|
203
|
-
"slider",
|
|
204
|
-
"spinbutton",
|
|
205
|
-
"searchbox",
|
|
206
|
-
];
|
|
207
|
-
|
|
208
|
-
function traverse(node: AccessibilityNode): void {
|
|
209
|
-
if (node.role && interactiveRoles.includes(node.role)) {
|
|
210
|
-
elements.push({
|
|
211
|
-
element: node.name || node.role,
|
|
212
|
-
type: node.role,
|
|
213
|
-
ref: node.ref,
|
|
214
|
-
});
|
|
215
|
-
}
|
|
216
|
-
if (node.children) {
|
|
217
|
-
for (const child of node.children) {
|
|
218
|
-
traverse(child);
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
traverse(snapshot);
|
|
224
|
-
return elements;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
function truncate(str: string, maxLength: number): string {
|
|
228
|
-
if (str.length <= maxLength) return str;
|
|
229
|
-
return str.slice(0, maxLength - 3) + "...";
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
/**
|
|
233
|
-
* Format a date for human-readable display
|
|
234
|
-
*/
|
|
235
|
-
export function formatDate(isoDate: string): string {
|
|
236
|
-
const date = new Date(isoDate);
|
|
237
|
-
return date.toLocaleString("en-US", {
|
|
238
|
-
month: "long",
|
|
239
|
-
day: "numeric",
|
|
240
|
-
year: "numeric",
|
|
241
|
-
hour: "numeric",
|
|
242
|
-
minute: "2-digit",
|
|
243
|
-
hour12: true,
|
|
244
|
-
});
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
/**
|
|
248
|
-
* Format a short time for display
|
|
249
|
-
*/
|
|
250
|
-
export function formatTime(isoDate: string): string {
|
|
251
|
-
const date = new Date(isoDate);
|
|
252
|
-
return date.toLocaleTimeString("en-US", {
|
|
253
|
-
hour: "numeric",
|
|
254
|
-
minute: "2-digit",
|
|
255
|
-
hour12: true,
|
|
256
|
-
});
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
/**
|
|
260
|
-
* Type definition for accessibility tree nodes
|
|
261
|
-
*/
|
|
262
|
-
export interface AccessibilityNode {
|
|
263
|
-
role?: string;
|
|
264
|
-
name?: string;
|
|
265
|
-
ref?: string;
|
|
266
|
-
level?: number;
|
|
267
|
-
checked?: boolean;
|
|
268
|
-
selected?: boolean;
|
|
269
|
-
disabled?: boolean;
|
|
270
|
-
value?: string;
|
|
271
|
-
children?: AccessibilityNode[];
|
|
272
|
-
}
|
package/src/workspace/types.ts
DELETED
|
@@ -1,186 +0,0 @@
|
|
|
1
|
-
export interface WorkspaceIndex {
|
|
2
|
-
analysisId: string;
|
|
3
|
-
url: string;
|
|
4
|
-
domain: string;
|
|
5
|
-
focus?: string;
|
|
6
|
-
createdAt: string;
|
|
7
|
-
updatedAt: string;
|
|
8
|
-
status: "active" | "completed" | "failed";
|
|
9
|
-
limits: {
|
|
10
|
-
maxSteps: number;
|
|
11
|
-
maxMinutes: number;
|
|
12
|
-
maxPages: number;
|
|
13
|
-
};
|
|
14
|
-
crawls: CrawlReference[];
|
|
15
|
-
/** Discovered features from webtest_discover_features */
|
|
16
|
-
features?: FeaturesReference;
|
|
17
|
-
/** Discovered flows per feature from webtest_discover_flows */
|
|
18
|
-
featureFlows?: FlowsReference[];
|
|
19
|
-
tests?: TestsReference;
|
|
20
|
-
runs: TestRunReference[];
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export interface CrawlReference {
|
|
24
|
-
crawlId: string;
|
|
25
|
-
goal: string;
|
|
26
|
-
status: "in_progress" | "completed" | "failed" | "cancelled";
|
|
27
|
-
startedAt: string;
|
|
28
|
-
completedAt?: string;
|
|
29
|
-
pagesVisited: number;
|
|
30
|
-
stepsExecuted: number;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export interface Feature {
|
|
34
|
-
slug: string;
|
|
35
|
-
name: string;
|
|
36
|
-
description: string;
|
|
37
|
-
entities: string[];
|
|
38
|
-
entryPoints: string[];
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export interface FeaturesReference {
|
|
42
|
-
createdAt: string;
|
|
43
|
-
featuresFilePath: string;
|
|
44
|
-
featuresUri: string;
|
|
45
|
-
featureCount: number;
|
|
46
|
-
features: Feature[];
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export interface FlowsReference {
|
|
50
|
-
featureSlug: string;
|
|
51
|
-
createdAt: string;
|
|
52
|
-
flowsFilePath: string;
|
|
53
|
-
flowsUri: string;
|
|
54
|
-
flowCount: number;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export interface TestsReference {
|
|
58
|
-
createdAt: string;
|
|
59
|
-
testsFilePath: string;
|
|
60
|
-
testsUri: string;
|
|
61
|
-
testCount: number;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export interface TestRunReference {
|
|
65
|
-
runId: string;
|
|
66
|
-
testCaseId: string;
|
|
67
|
-
status: "in_progress" | "passed" | "failed" | "error";
|
|
68
|
-
startedAt: string;
|
|
69
|
-
completedAt?: string;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export interface CrawlIndex {
|
|
73
|
-
crawlId: string;
|
|
74
|
-
analysisId: string;
|
|
75
|
-
goal: string;
|
|
76
|
-
strategy: string;
|
|
77
|
-
status: "in_progress" | "completed" | "failed" | "cancelled";
|
|
78
|
-
startedAt: string;
|
|
79
|
-
completedAt?: string;
|
|
80
|
-
budget: {
|
|
81
|
-
maxSteps: number;
|
|
82
|
-
maxMinutes: number;
|
|
83
|
-
maxPages: number;
|
|
84
|
-
stepsUsed: number;
|
|
85
|
-
pagesVisited: number;
|
|
86
|
-
};
|
|
87
|
-
pages: PageReference[];
|
|
88
|
-
actionHistory: ActionRecord[];
|
|
89
|
-
checkpoint?: CrawlCheckpoint;
|
|
90
|
-
loopDetection: {
|
|
91
|
-
domSignatures: Record<string, number>;
|
|
92
|
-
urlVisits: Record<string, number>;
|
|
93
|
-
recentActions: string[];
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
export interface PageReference {
|
|
98
|
-
pageId: string;
|
|
99
|
-
url: string;
|
|
100
|
-
title: string;
|
|
101
|
-
capturedAt: string;
|
|
102
|
-
snapshotFilePath: string;
|
|
103
|
-
snapshotUri: string;
|
|
104
|
-
screenshotFilePath: string;
|
|
105
|
-
screenshotUri: string;
|
|
106
|
-
domFilePath: string;
|
|
107
|
-
domUri: string;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
export interface ActionRecord {
|
|
111
|
-
step: number;
|
|
112
|
-
timestamp: string;
|
|
113
|
-
tool: string;
|
|
114
|
-
args: Record<string, unknown>;
|
|
115
|
-
result: "success" | "error";
|
|
116
|
-
reasoning?: string;
|
|
117
|
-
error?: string;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
export interface CrawlCheckpoint {
|
|
121
|
-
step: number;
|
|
122
|
-
timestamp: string;
|
|
123
|
-
visitedUrls: string[];
|
|
124
|
-
currentUrl: string;
|
|
125
|
-
goalProgress: string;
|
|
126
|
-
canResume: boolean;
|
|
127
|
-
/** Start URL for navigation guard (blocks return to start after initial steps) */
|
|
128
|
-
startUrl?: string;
|
|
129
|
-
/** Preserved loop detection state for context continuity across resume */
|
|
130
|
-
loopDetection?: {
|
|
131
|
-
/** DOM signature visit counts as [signature, count] entries */
|
|
132
|
-
domSignatures: Array<[string, number]>;
|
|
133
|
-
/** URL visit counts as [url, count] entries */
|
|
134
|
-
urlVisits: Array<[string, number]>;
|
|
135
|
-
/** Recent action keys for repeat detection */
|
|
136
|
-
recentActions: string[];
|
|
137
|
-
};
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
export interface TestCase {
|
|
141
|
-
id: string;
|
|
142
|
-
name: string;
|
|
143
|
-
purpose: string;
|
|
144
|
-
category: "happy_path" | "edge_case" | "error_handling" | "boundary";
|
|
145
|
-
preconditions: string[];
|
|
146
|
-
steps: TestStep[];
|
|
147
|
-
expectedOutcomes: string[];
|
|
148
|
-
tags?: string[];
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
export interface TestStep {
|
|
152
|
-
stepNumber: number;
|
|
153
|
-
action: string;
|
|
154
|
-
target?: string;
|
|
155
|
-
value?: string;
|
|
156
|
-
expected?: string;
|
|
157
|
-
/** Playwright MCP element description */
|
|
158
|
-
element?: string;
|
|
159
|
-
/** Playwright MCP accessibility snapshot ref */
|
|
160
|
-
ref?: string;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
export interface TestRunIndex {
|
|
164
|
-
runId: string;
|
|
165
|
-
analysisId: string;
|
|
166
|
-
testCaseId: string;
|
|
167
|
-
testName: string;
|
|
168
|
-
status: "in_progress" | "passed" | "failed" | "error";
|
|
169
|
-
startedAt: string;
|
|
170
|
-
completedAt?: string;
|
|
171
|
-
steps: TestStepResult[];
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
export interface TestStepResult {
|
|
175
|
-
stepNumber: number;
|
|
176
|
-
status: "passed" | "failed" | "error" | "skipped";
|
|
177
|
-
executedAt: string;
|
|
178
|
-
evidence: {
|
|
179
|
-
screenshotFilePath?: string;
|
|
180
|
-
screenshotUri?: string;
|
|
181
|
-
snapshotFilePath?: string;
|
|
182
|
-
snapshotUri?: string;
|
|
183
|
-
};
|
|
184
|
-
actualResult?: string;
|
|
185
|
-
errorMessage?: string;
|
|
186
|
-
}
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
|
2
|
-
import { spawn, type ChildProcess } from "node:child_process";
|
|
3
|
-
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
4
|
-
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
5
|
-
import { fileURLToPath } from "node:url";
|
|
6
|
-
import { dirname, join } from "node:path";
|
|
7
|
-
|
|
8
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
9
|
-
const serverPath = join(__dirname, "../../src/index.ts");
|
|
10
|
-
|
|
11
|
-
describe("MCP Server Integration", () => {
|
|
12
|
-
let client: Client;
|
|
13
|
-
let transport: StdioClientTransport;
|
|
14
|
-
let serverProcess: ChildProcess;
|
|
15
|
-
|
|
16
|
-
beforeAll(async () => {
|
|
17
|
-
// Spawn the server using tsx
|
|
18
|
-
serverProcess = spawn("npx", ["tsx", serverPath], {
|
|
19
|
-
env: {
|
|
20
|
-
...process.env,
|
|
21
|
-
TRANSPORT: "stdio",
|
|
22
|
-
LOG_LEVEL: "error", // Minimize log noise during tests
|
|
23
|
-
},
|
|
24
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
// Create transport connected to server's stdin/stdout
|
|
28
|
-
transport = new StdioClientTransport({
|
|
29
|
-
command: "npx",
|
|
30
|
-
args: ["tsx", serverPath],
|
|
31
|
-
env: {
|
|
32
|
-
...process.env,
|
|
33
|
-
TRANSPORT: "stdio",
|
|
34
|
-
LOG_LEVEL: "error",
|
|
35
|
-
},
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
// Create and connect client
|
|
39
|
-
client = new Client({
|
|
40
|
-
name: "test-client",
|
|
41
|
-
version: "1.0.0",
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
await client.connect(transport);
|
|
45
|
-
}, 15000);
|
|
46
|
-
|
|
47
|
-
afterAll(async () => {
|
|
48
|
-
await client.close();
|
|
49
|
-
serverProcess?.kill();
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
describe("server identification", () => {
|
|
53
|
-
it("reports correct server name", async () => {
|
|
54
|
-
const info = client.getServerVersion();
|
|
55
|
-
expect(info?.name).toBe("testing-mcp");
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it("reports server version", async () => {
|
|
59
|
-
const info = client.getServerVersion();
|
|
60
|
-
expect(info?.version).toBeDefined();
|
|
61
|
-
});
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
describe("tool discovery", () => {
|
|
65
|
-
it("lists available tools", async () => {
|
|
66
|
-
const result = await client.listTools();
|
|
67
|
-
expect(result.tools).toBeDefined();
|
|
68
|
-
expect(result.tools.length).toBeGreaterThan(0);
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it("includes hello tool", async () => {
|
|
72
|
-
const result = await client.listTools();
|
|
73
|
-
const helloTool = result.tools.find((t) => t.name === "hello");
|
|
74
|
-
expect(helloTool).toBeDefined();
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it("hello tool has description", async () => {
|
|
78
|
-
const result = await client.listTools();
|
|
79
|
-
const helloTool = result.tools.find((t) => t.name === "hello");
|
|
80
|
-
expect(helloTool?.description).toBeTruthy();
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it("hello tool has input schema", async () => {
|
|
84
|
-
const result = await client.listTools();
|
|
85
|
-
const helloTool = result.tools.find((t) => t.name === "hello");
|
|
86
|
-
expect(helloTool?.inputSchema).toBeDefined();
|
|
87
|
-
});
|
|
88
|
-
});
|
|
89
|
-
});
|
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
|
2
|
-
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
3
|
-
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
4
|
-
import { fileURLToPath } from "node:url";
|
|
5
|
-
import { dirname, join } from "node:path";
|
|
6
|
-
|
|
7
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
|
-
const serverPath = join(__dirname, "../../src/index.ts");
|
|
9
|
-
|
|
10
|
-
describe("Tool Execution Integration", () => {
|
|
11
|
-
let client: Client;
|
|
12
|
-
let transport: StdioClientTransport;
|
|
13
|
-
|
|
14
|
-
beforeAll(async () => {
|
|
15
|
-
transport = new StdioClientTransport({
|
|
16
|
-
command: "npx",
|
|
17
|
-
args: ["tsx", serverPath],
|
|
18
|
-
env: {
|
|
19
|
-
...process.env,
|
|
20
|
-
TRANSPORT: "stdio",
|
|
21
|
-
LOG_LEVEL: "error",
|
|
22
|
-
},
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
client = new Client({
|
|
26
|
-
name: "test-client",
|
|
27
|
-
version: "1.0.0",
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
await client.connect(transport);
|
|
31
|
-
}, 15000);
|
|
32
|
-
|
|
33
|
-
afterAll(async () => {
|
|
34
|
-
await client.close();
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
describe("hello tool", () => {
|
|
38
|
-
it("returns greeting with provided name", async () => {
|
|
39
|
-
const result = await client.callTool({
|
|
40
|
-
name: "hello",
|
|
41
|
-
arguments: { name: "World" },
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
expect(result.content).toBeDefined();
|
|
45
|
-
expect(result.content.length).toBeGreaterThan(0);
|
|
46
|
-
|
|
47
|
-
const textContent = result.content.find(
|
|
48
|
-
(c): c is { type: "text"; text: string } => c.type === "text"
|
|
49
|
-
);
|
|
50
|
-
expect(textContent?.text).toBe("Hello, World!");
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it("works with different names", async () => {
|
|
54
|
-
const result = await client.callTool({
|
|
55
|
-
name: "hello",
|
|
56
|
-
arguments: { name: "Alice" },
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
const textContent = result.content.find(
|
|
60
|
-
(c): c is { type: "text"; text: string } => c.type === "text"
|
|
61
|
-
);
|
|
62
|
-
expect(textContent?.text).toBe("Hello, Alice!");
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it("handles special characters in name", async () => {
|
|
66
|
-
const result = await client.callTool({
|
|
67
|
-
name: "hello",
|
|
68
|
-
arguments: { name: "Bob & Jane" },
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
const textContent = result.content.find(
|
|
72
|
-
(c): c is { type: "text"; text: string } => c.type === "text"
|
|
73
|
-
);
|
|
74
|
-
expect(textContent?.text).toBe("Hello, Bob & Jane!");
|
|
75
|
-
});
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
describe("error handling", () => {
|
|
79
|
-
it("returns error for missing required argument", async () => {
|
|
80
|
-
const result = await client.callTool({
|
|
81
|
-
name: "hello",
|
|
82
|
-
arguments: {},
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
// SDK validation should reject this
|
|
86
|
-
expect(result.isError).toBe(true);
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
it("returns error for invalid argument type", async () => {
|
|
90
|
-
const result = await client.callTool({
|
|
91
|
-
name: "hello",
|
|
92
|
-
// @ts-expect-error - intentionally passing wrong type
|
|
93
|
-
arguments: { name: 123 },
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
expect(result.isError).toBe(true);
|
|
97
|
-
});
|
|
98
|
-
});
|
|
99
|
-
});
|
package/tsconfig.json
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ES2022",
|
|
4
|
-
"module": "NodeNext",
|
|
5
|
-
"moduleResolution": "NodeNext",
|
|
6
|
-
"lib": ["ES2022"],
|
|
7
|
-
"outDir": "./dist",
|
|
8
|
-
"rootDir": "./src",
|
|
9
|
-
"strict": true,
|
|
10
|
-
"esModuleInterop": true,
|
|
11
|
-
"skipLibCheck": true,
|
|
12
|
-
"forceConsistentCasingInFileNames": true,
|
|
13
|
-
"declaration": true,
|
|
14
|
-
"declarationMap": true,
|
|
15
|
-
"sourceMap": true,
|
|
16
|
-
"resolveJsonModule": true
|
|
17
|
-
},
|
|
18
|
-
"include": ["src/**/*"],
|
|
19
|
-
"exclude": ["node_modules", "dist", "**/*.test.ts"]
|
|
20
|
-
}
|
package/vitest.config.ts
DELETED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|