@yasserkhanorg/e2e-agents 0.3.2
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/LICENSE +168 -0
- package/README.md +620 -0
- package/dist/agent/analysis.d.ts +62 -0
- package/dist/agent/analysis.d.ts.map +1 -0
- package/dist/agent/analysis.js +292 -0
- package/dist/agent/blast_radius.d.ts +4 -0
- package/dist/agent/blast_radius.d.ts.map +1 -0
- package/dist/agent/blast_radius.js +37 -0
- package/dist/agent/cache_utils.d.ts +38 -0
- package/dist/agent/cache_utils.d.ts.map +1 -0
- package/dist/agent/cache_utils.js +67 -0
- package/dist/agent/config.d.ts +148 -0
- package/dist/agent/config.d.ts.map +1 -0
- package/dist/agent/config.js +640 -0
- package/dist/agent/dependency_graph.d.ts +14 -0
- package/dist/agent/dependency_graph.d.ts.map +1 -0
- package/dist/agent/dependency_graph.js +227 -0
- package/dist/agent/feedback.d.ts +55 -0
- package/dist/agent/feedback.d.ts.map +1 -0
- package/dist/agent/feedback.js +257 -0
- package/dist/agent/flags.d.ts +23 -0
- package/dist/agent/flags.d.ts.map +1 -0
- package/dist/agent/flags.js +171 -0
- package/dist/agent/flow_catalog.d.ts +25 -0
- package/dist/agent/flow_catalog.d.ts.map +1 -0
- package/dist/agent/flow_catalog.js +106 -0
- package/dist/agent/flow_mapping.d.ts +10 -0
- package/dist/agent/flow_mapping.d.ts.map +1 -0
- package/dist/agent/flow_mapping.js +84 -0
- package/dist/agent/framework.d.ts +13 -0
- package/dist/agent/framework.d.ts.map +1 -0
- package/dist/agent/framework.js +149 -0
- package/dist/agent/gap_suggestions.d.ts +14 -0
- package/dist/agent/gap_suggestions.d.ts.map +1 -0
- package/dist/agent/gap_suggestions.js +101 -0
- package/dist/agent/generator.d.ts +10 -0
- package/dist/agent/generator.d.ts.map +1 -0
- package/dist/agent/generator.js +115 -0
- package/dist/agent/git.d.ts +11 -0
- package/dist/agent/git.d.ts.map +1 -0
- package/dist/agent/git.js +90 -0
- package/dist/agent/handoff.d.ts +22 -0
- package/dist/agent/handoff.d.ts.map +1 -0
- package/dist/agent/handoff.js +180 -0
- package/dist/agent/impact-analyzer.d.ts +114 -0
- package/dist/agent/impact-analyzer.d.ts.map +1 -0
- package/dist/agent/impact-analyzer.js +557 -0
- package/dist/agent/index.d.ts +21 -0
- package/dist/agent/index.d.ts.map +1 -0
- package/dist/agent/index.js +38 -0
- package/dist/agent/model-router.d.ts +57 -0
- package/dist/agent/model-router.d.ts.map +1 -0
- package/dist/agent/model-router.js +154 -0
- package/dist/agent/operational_insights.d.ts +41 -0
- package/dist/agent/operational_insights.d.ts.map +1 -0
- package/dist/agent/operational_insights.js +126 -0
- package/dist/agent/pipeline.d.ts +23 -0
- package/dist/agent/pipeline.d.ts.map +1 -0
- package/dist/agent/pipeline.js +609 -0
- package/dist/agent/plan.d.ts +91 -0
- package/dist/agent/plan.d.ts.map +1 -0
- package/dist/agent/plan.js +331 -0
- package/dist/agent/playwright_report.d.ts +8 -0
- package/dist/agent/playwright_report.d.ts.map +1 -0
- package/dist/agent/playwright_report.js +126 -0
- package/dist/agent/report-generator.d.ts +24 -0
- package/dist/agent/report-generator.d.ts.map +1 -0
- package/dist/agent/report-generator.js +250 -0
- package/dist/agent/report.d.ts +81 -0
- package/dist/agent/report.d.ts.map +1 -0
- package/dist/agent/report.js +147 -0
- package/dist/agent/runner.d.ts +7 -0
- package/dist/agent/runner.d.ts.map +1 -0
- package/dist/agent/runner.js +576 -0
- package/dist/agent/selectors.d.ts +10 -0
- package/dist/agent/selectors.d.ts.map +1 -0
- package/dist/agent/selectors.js +75 -0
- package/dist/agent/spec-bridge.d.ts +101 -0
- package/dist/agent/spec-bridge.d.ts.map +1 -0
- package/dist/agent/spec-bridge.js +273 -0
- package/dist/agent/spec-builder.d.ts +102 -0
- package/dist/agent/spec-builder.d.ts.map +1 -0
- package/dist/agent/spec-builder.js +273 -0
- package/dist/agent/subsystem_risk.d.ts +23 -0
- package/dist/agent/subsystem_risk.d.ts.map +1 -0
- package/dist/agent/subsystem_risk.js +207 -0
- package/dist/agent/telemetry.d.ts +84 -0
- package/dist/agent/telemetry.d.ts.map +1 -0
- package/dist/agent/telemetry.js +220 -0
- package/dist/agent/test_path.d.ts +2 -0
- package/dist/agent/test_path.d.ts.map +1 -0
- package/dist/agent/test_path.js +23 -0
- package/dist/agent/tests.d.ts +18 -0
- package/dist/agent/tests.d.ts.map +1 -0
- package/dist/agent/tests.js +106 -0
- package/dist/agent/traceability.d.ts +22 -0
- package/dist/agent/traceability.d.ts.map +1 -0
- package/dist/agent/traceability.js +183 -0
- package/dist/agent/traceability_capture.d.ts +18 -0
- package/dist/agent/traceability_capture.d.ts.map +1 -0
- package/dist/agent/traceability_capture.js +313 -0
- package/dist/agent/traceability_ingest.d.ts +21 -0
- package/dist/agent/traceability_ingest.d.ts.map +1 -0
- package/dist/agent/traceability_ingest.js +237 -0
- package/dist/agent/utils.d.ts +13 -0
- package/dist/agent/utils.d.ts.map +1 -0
- package/dist/agent/utils.js +152 -0
- package/dist/agent/validators/selector-validator.d.ts +74 -0
- package/dist/agent/validators/selector-validator.d.ts.map +1 -0
- package/dist/agent/validators/selector-validator.js +165 -0
- package/dist/anthropic_provider.d.ts +65 -0
- package/dist/anthropic_provider.d.ts.map +1 -0
- package/dist/anthropic_provider.js +332 -0
- package/dist/api.d.ts +48 -0
- package/dist/api.d.ts.map +1 -0
- package/dist/api.js +113 -0
- package/dist/base_provider.d.ts +53 -0
- package/dist/base_provider.d.ts.map +1 -0
- package/dist/base_provider.js +81 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +843 -0
- package/dist/custom_provider.d.ts +20 -0
- package/dist/custom_provider.d.ts.map +1 -0
- package/dist/custom_provider.js +276 -0
- package/dist/e2e-test-gen/index.d.ts +51 -0
- package/dist/e2e-test-gen/index.d.ts.map +1 -0
- package/dist/e2e-test-gen/index.js +57 -0
- package/dist/e2e-test-gen/spec_parser.d.ts +142 -0
- package/dist/e2e-test-gen/spec_parser.d.ts.map +1 -0
- package/dist/e2e-test-gen/spec_parser.js +786 -0
- package/dist/e2e-test-gen/types.d.ts +185 -0
- package/dist/e2e-test-gen/types.d.ts.map +1 -0
- package/dist/e2e-test-gen/types.js +4 -0
- package/dist/esm/agent/analysis.js +287 -0
- package/dist/esm/agent/blast_radius.js +34 -0
- package/dist/esm/agent/cache_utils.js +63 -0
- package/dist/esm/agent/config.js +637 -0
- package/dist/esm/agent/dependency_graph.js +224 -0
- package/dist/esm/agent/feedback.js +253 -0
- package/dist/esm/agent/flags.js +160 -0
- package/dist/esm/agent/flow_catalog.js +103 -0
- package/dist/esm/agent/flow_mapping.js +81 -0
- package/dist/esm/agent/framework.js +145 -0
- package/dist/esm/agent/gap_suggestions.js +98 -0
- package/dist/esm/agent/generator.js +112 -0
- package/dist/esm/agent/git.js +87 -0
- package/dist/esm/agent/handoff.js +177 -0
- package/dist/esm/agent/impact-analyzer.js +548 -0
- package/dist/esm/agent/index.js +22 -0
- package/dist/esm/agent/model-router.js +150 -0
- package/dist/esm/agent/operational_insights.js +123 -0
- package/dist/esm/agent/pipeline.js +605 -0
- package/dist/esm/agent/plan.js +324 -0
- package/dist/esm/agent/playwright_report.js +123 -0
- package/dist/esm/agent/report-generator.js +247 -0
- package/dist/esm/agent/report.js +144 -0
- package/dist/esm/agent/runner.js +572 -0
- package/dist/esm/agent/selectors.js +71 -0
- package/dist/esm/agent/spec-bridge.js +267 -0
- package/dist/esm/agent/spec-builder.js +267 -0
- package/dist/esm/agent/subsystem_risk.js +204 -0
- package/dist/esm/agent/telemetry.js +216 -0
- package/dist/esm/agent/test_path.js +20 -0
- package/dist/esm/agent/tests.js +101 -0
- package/dist/esm/agent/traceability.js +180 -0
- package/dist/esm/agent/traceability_capture.js +310 -0
- package/dist/esm/agent/traceability_ingest.js +234 -0
- package/dist/esm/agent/utils.js +138 -0
- package/dist/esm/agent/validators/selector-validator.js +160 -0
- package/dist/esm/anthropic_provider.js +324 -0
- package/dist/esm/api.js +105 -0
- package/dist/esm/base_provider.js +77 -0
- package/dist/esm/cli.js +841 -0
- package/dist/esm/custom_provider.js +272 -0
- package/dist/esm/e2e-test-gen/index.js +50 -0
- package/dist/esm/e2e-test-gen/spec_parser.js +782 -0
- package/dist/esm/e2e-test-gen/types.js +3 -0
- package/dist/esm/index.js +16 -0
- package/dist/esm/logger.js +89 -0
- package/dist/esm/mcp-server.js +465 -0
- package/dist/esm/ollama_provider.js +300 -0
- package/dist/esm/openai_provider.js +242 -0
- package/dist/esm/package.json +3 -0
- package/dist/esm/plan-and-test-constants.js +126 -0
- package/dist/esm/provider_factory.js +336 -0
- package/dist/esm/provider_interface.js +23 -0
- package/dist/esm/provider_utils.js +96 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +41 -0
- package/dist/logger.d.ts +23 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +93 -0
- package/dist/mcp-server.d.ts +35 -0
- package/dist/mcp-server.d.ts.map +1 -0
- package/dist/mcp-server.js +469 -0
- package/dist/ollama_provider.d.ts +65 -0
- package/dist/ollama_provider.d.ts.map +1 -0
- package/dist/ollama_provider.js +308 -0
- package/dist/openai_provider.d.ts +23 -0
- package/dist/openai_provider.d.ts.map +1 -0
- package/dist/openai_provider.js +250 -0
- package/dist/plan-and-test-constants.d.ts +110 -0
- package/dist/plan-and-test-constants.d.ts.map +1 -0
- package/dist/plan-and-test-constants.js +132 -0
- package/dist/provider_factory.d.ts +99 -0
- package/dist/provider_factory.d.ts.map +1 -0
- package/dist/provider_factory.js +341 -0
- package/dist/provider_interface.d.ts +358 -0
- package/dist/provider_interface.d.ts.map +1 -0
- package/dist/provider_interface.js +28 -0
- package/dist/provider_utils.d.ts +39 -0
- package/dist/provider_utils.d.ts.map +1 -0
- package/dist/provider_utils.js +103 -0
- package/package.json +101 -0
- package/schemas/gap.schema.json +18 -0
- package/schemas/impact.schema.json +418 -0
- package/schemas/plan.schema.json +285 -0
- package/schemas/subsystem-risk-map.schema.json +62 -0
- package/schemas/traceability-input.schema.json +122 -0
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
2
|
+
// See LICENSE.txt for license information.
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
4
|
+
import { dirname, isAbsolute, join } from 'path';
|
|
5
|
+
import { normalizePath } from './utils.js';
|
|
6
|
+
const DEFAULT_OPTIONS = {
|
|
7
|
+
minHits: 1,
|
|
8
|
+
maxFilesPerTest: 200,
|
|
9
|
+
maxAgeDays: 120,
|
|
10
|
+
};
|
|
11
|
+
function resolvePath(root, value) {
|
|
12
|
+
if (isAbsolute(value)) {
|
|
13
|
+
return value;
|
|
14
|
+
}
|
|
15
|
+
return join(root, value);
|
|
16
|
+
}
|
|
17
|
+
function parseDate(value) {
|
|
18
|
+
const parsed = Date.parse(value);
|
|
19
|
+
if (Number.isNaN(parsed)) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
return parsed;
|
|
23
|
+
}
|
|
24
|
+
function safeReadJson(path) {
|
|
25
|
+
if (!existsSync(path)) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
return JSON.parse(readFileSync(path, 'utf-8'));
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function normalizeFiles(value) {
|
|
36
|
+
if (!Array.isArray(value)) {
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
return Array.from(new Set(value
|
|
40
|
+
.filter((entry) => typeof entry === 'string')
|
|
41
|
+
.map((entry) => normalizePath(entry))));
|
|
42
|
+
}
|
|
43
|
+
function normalizeTest(value) {
|
|
44
|
+
if (typeof value !== 'string') {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
const normalized = normalizePath(value);
|
|
48
|
+
return normalized ? normalized : null;
|
|
49
|
+
}
|
|
50
|
+
function buildEntriesFromInput(payload) {
|
|
51
|
+
const warnings = [];
|
|
52
|
+
const entries = [];
|
|
53
|
+
const pushEntry = (testValue, filesValue, timestampValue) => {
|
|
54
|
+
const test = normalizeTest(testValue);
|
|
55
|
+
const files = normalizeFiles(filesValue);
|
|
56
|
+
if (!test || files.length === 0) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
entries.push({
|
|
60
|
+
test,
|
|
61
|
+
touchedFiles: files,
|
|
62
|
+
timestamp: typeof timestampValue === 'string' ? timestampValue : undefined,
|
|
63
|
+
});
|
|
64
|
+
};
|
|
65
|
+
if (Array.isArray(payload)) {
|
|
66
|
+
for (const item of payload) {
|
|
67
|
+
if (!item || typeof item !== 'object') {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
const entry = item;
|
|
71
|
+
pushEntry(entry.test, entry.touchedFiles, entry.timestamp);
|
|
72
|
+
}
|
|
73
|
+
if (entries.length === 0) {
|
|
74
|
+
warnings.push('Traceability input array had no valid entries.');
|
|
75
|
+
}
|
|
76
|
+
return { entries, warnings };
|
|
77
|
+
}
|
|
78
|
+
if (!payload || typeof payload !== 'object') {
|
|
79
|
+
warnings.push('Traceability input must be an object or array.');
|
|
80
|
+
return { entries, warnings };
|
|
81
|
+
}
|
|
82
|
+
const input = payload;
|
|
83
|
+
if (Array.isArray(input.tests)) {
|
|
84
|
+
for (const item of input.tests) {
|
|
85
|
+
pushEntry(item?.test, item?.touchedFiles, item?.timestamp);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (Array.isArray(input.runs)) {
|
|
89
|
+
for (const item of input.runs) {
|
|
90
|
+
const files = Array.isArray(item?.touchedFiles) ? item?.touchedFiles : (Array.isArray(item?.coveredFiles) ? item?.coveredFiles : item?.files);
|
|
91
|
+
pushEntry(item?.test, files, item?.timestamp);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (input.fileToTests && typeof input.fileToTests === 'object') {
|
|
95
|
+
for (const [file, tests] of Object.entries(input.fileToTests)) {
|
|
96
|
+
if (!Array.isArray(tests)) {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
const normalizedFile = normalizePath(file);
|
|
100
|
+
for (const test of tests) {
|
|
101
|
+
pushEntry(test, [normalizedFile]);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (Array.isArray(input.mappings)) {
|
|
106
|
+
for (const mapping of input.mappings) {
|
|
107
|
+
const file = typeof mapping?.file === 'string' ? normalizePath(mapping.file) : null;
|
|
108
|
+
if (!file || !Array.isArray(mapping.tests)) {
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
for (const test of mapping.tests) {
|
|
112
|
+
pushEntry(test, [file]);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (entries.length === 0) {
|
|
117
|
+
warnings.push('Traceability input had no valid test<->file entries.');
|
|
118
|
+
}
|
|
119
|
+
return { entries, warnings };
|
|
120
|
+
}
|
|
121
|
+
function defaultState() {
|
|
122
|
+
return {
|
|
123
|
+
schemaVersion: '1.0.0',
|
|
124
|
+
updatedAt: new Date().toISOString(),
|
|
125
|
+
tests: {},
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
function loadState(path) {
|
|
129
|
+
const existing = safeReadJson(path);
|
|
130
|
+
if (!existing || typeof existing !== 'object' || !existing.tests) {
|
|
131
|
+
return defaultState();
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
schemaVersion: '1.0.0',
|
|
135
|
+
updatedAt: existing.updatedAt || new Date().toISOString(),
|
|
136
|
+
tests: existing.tests,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
function pruneByAge(state, maxAgeDays) {
|
|
140
|
+
const cutoff = Date.now() - maxAgeDays * 24 * 60 * 60 * 1000;
|
|
141
|
+
for (const [test, entry] of Object.entries(state.tests)) {
|
|
142
|
+
const lastSeen = parseDate(entry.lastSeen);
|
|
143
|
+
if (lastSeen === null) {
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
if (lastSeen < cutoff) {
|
|
147
|
+
delete state.tests[test];
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
function buildManifest(state, minHits, maxFilesPerTest) {
|
|
152
|
+
const tests = Object.entries(state.tests)
|
|
153
|
+
.map(([test, entry]) => {
|
|
154
|
+
const touchedFiles = Object.entries(entry.files)
|
|
155
|
+
.filter(([, hits]) => hits >= minHits)
|
|
156
|
+
.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
|
|
157
|
+
.slice(0, maxFilesPerTest)
|
|
158
|
+
.map(([file]) => file);
|
|
159
|
+
const signalCount = Object.values(entry.files).reduce((acc, value) => acc + value, 0);
|
|
160
|
+
return {
|
|
161
|
+
test,
|
|
162
|
+
touchedFiles,
|
|
163
|
+
signalCount,
|
|
164
|
+
lastSeen: entry.lastSeen,
|
|
165
|
+
};
|
|
166
|
+
})
|
|
167
|
+
.filter((entry) => entry.touchedFiles.length > 0)
|
|
168
|
+
.sort((a, b) => a.test.localeCompare(b.test));
|
|
169
|
+
return {
|
|
170
|
+
schemaVersion: '1.0.0',
|
|
171
|
+
generatedAt: new Date().toISOString(),
|
|
172
|
+
tests,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
function ensureParent(path) {
|
|
176
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
177
|
+
}
|
|
178
|
+
export function ingestTraceabilityInput(rootPath, traceabilityConfig, inputPayload, options) {
|
|
179
|
+
const resolvedOptions = {
|
|
180
|
+
minHits: options?.minHits ?? DEFAULT_OPTIONS.minHits,
|
|
181
|
+
maxFilesPerTest: options?.maxFilesPerTest ?? DEFAULT_OPTIONS.maxFilesPerTest,
|
|
182
|
+
maxAgeDays: options?.maxAgeDays ?? DEFAULT_OPTIONS.maxAgeDays,
|
|
183
|
+
};
|
|
184
|
+
const warnings = [];
|
|
185
|
+
const manifestPath = resolvePath(rootPath, traceabilityConfig.manifestPath);
|
|
186
|
+
const statePath = join(dirname(manifestPath), 'traceability-state.json');
|
|
187
|
+
if (!traceabilityConfig.enabled) {
|
|
188
|
+
warnings.push('Traceability is disabled in config. Input was not ingested.');
|
|
189
|
+
return {
|
|
190
|
+
manifestPath,
|
|
191
|
+
statePath,
|
|
192
|
+
entriesIngested: 0,
|
|
193
|
+
testsTracked: 0,
|
|
194
|
+
edgesTracked: 0,
|
|
195
|
+
warnings,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
const parsed = buildEntriesFromInput(inputPayload);
|
|
199
|
+
warnings.push(...parsed.warnings);
|
|
200
|
+
const state = loadState(statePath);
|
|
201
|
+
const now = new Date().toISOString();
|
|
202
|
+
for (const entry of parsed.entries) {
|
|
203
|
+
const bucket = state.tests[entry.test] || {
|
|
204
|
+
files: {},
|
|
205
|
+
seenCount: 0,
|
|
206
|
+
lastSeen: now,
|
|
207
|
+
};
|
|
208
|
+
bucket.seenCount += 1;
|
|
209
|
+
bucket.lastSeen = entry.timestamp || now;
|
|
210
|
+
for (const file of entry.touchedFiles) {
|
|
211
|
+
bucket.files[file] = (bucket.files[file] || 0) + 1;
|
|
212
|
+
}
|
|
213
|
+
state.tests[entry.test] = bucket;
|
|
214
|
+
}
|
|
215
|
+
pruneByAge(state, Math.max(1, resolvedOptions.maxAgeDays));
|
|
216
|
+
state.updatedAt = now;
|
|
217
|
+
const manifest = buildManifest(state, Math.max(1, resolvedOptions.minHits), Math.max(1, resolvedOptions.maxFilesPerTest));
|
|
218
|
+
let edgesTracked = 0;
|
|
219
|
+
for (const entry of manifest.tests) {
|
|
220
|
+
edgesTracked += entry.touchedFiles.length;
|
|
221
|
+
}
|
|
222
|
+
ensureParent(statePath);
|
|
223
|
+
ensureParent(manifestPath);
|
|
224
|
+
writeFileSync(statePath, JSON.stringify(state, null, 2), 'utf-8');
|
|
225
|
+
writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), 'utf-8');
|
|
226
|
+
return {
|
|
227
|
+
manifestPath,
|
|
228
|
+
statePath,
|
|
229
|
+
entriesIngested: parsed.entries.length,
|
|
230
|
+
testsTracked: manifest.tests.length,
|
|
231
|
+
edgesTracked,
|
|
232
|
+
warnings,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
2
|
+
// See LICENSE.txt for license information.
|
|
3
|
+
import { readFileSync, statSync } from 'fs';
|
|
4
|
+
import { basename, extname, posix, relative, resolve } from 'path';
|
|
5
|
+
const MAX_READ_BYTES = 1024 * 1024; // 1MB
|
|
6
|
+
const STOP_WORDS = new Set([
|
|
7
|
+
'index',
|
|
8
|
+
'component',
|
|
9
|
+
'components',
|
|
10
|
+
'page',
|
|
11
|
+
'pages',
|
|
12
|
+
'screen',
|
|
13
|
+
'screens',
|
|
14
|
+
'view',
|
|
15
|
+
'views',
|
|
16
|
+
'route',
|
|
17
|
+
'routes',
|
|
18
|
+
'feature',
|
|
19
|
+
'features',
|
|
20
|
+
'module',
|
|
21
|
+
'modules',
|
|
22
|
+
'flow',
|
|
23
|
+
'flows',
|
|
24
|
+
'test',
|
|
25
|
+
'tests',
|
|
26
|
+
'spec',
|
|
27
|
+
'specs',
|
|
28
|
+
'hooks',
|
|
29
|
+
'hook',
|
|
30
|
+
'context',
|
|
31
|
+
'state',
|
|
32
|
+
'store',
|
|
33
|
+
]);
|
|
34
|
+
const GLOB_CHARS = /[*?[\]{}()!]/;
|
|
35
|
+
export function hasGlobChars(value) {
|
|
36
|
+
return GLOB_CHARS.test(value);
|
|
37
|
+
}
|
|
38
|
+
export function globToRegExp(pattern) {
|
|
39
|
+
const normalized = normalizePath(pattern);
|
|
40
|
+
let regex = '^';
|
|
41
|
+
let i = 0;
|
|
42
|
+
while (i < normalized.length) {
|
|
43
|
+
const char = normalized[i];
|
|
44
|
+
if (char === '*') {
|
|
45
|
+
const next = normalized[i + 1];
|
|
46
|
+
if (next === '*') {
|
|
47
|
+
regex += '.*';
|
|
48
|
+
i += 2;
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
regex += '[^/]*';
|
|
52
|
+
i += 1;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (char === '?') {
|
|
56
|
+
regex += '[^/]';
|
|
57
|
+
i += 1;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if ('\\.[]{}()+-^$|'.includes(char)) {
|
|
61
|
+
regex += `\\${char}`;
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
regex += char;
|
|
65
|
+
}
|
|
66
|
+
i += 1;
|
|
67
|
+
}
|
|
68
|
+
regex += '$';
|
|
69
|
+
return new RegExp(regex);
|
|
70
|
+
}
|
|
71
|
+
export function matchGlob(pathValue, pattern) {
|
|
72
|
+
const normalizedPath = normalizePath(pathValue);
|
|
73
|
+
const normalizedPattern = normalizePath(pattern);
|
|
74
|
+
if (!hasGlobChars(normalizedPattern)) {
|
|
75
|
+
if (normalizedPattern.endsWith('/')) {
|
|
76
|
+
return normalizedPath.startsWith(normalizedPattern);
|
|
77
|
+
}
|
|
78
|
+
return normalizedPath === normalizedPattern || normalizedPath.startsWith(`${normalizedPattern}/`);
|
|
79
|
+
}
|
|
80
|
+
const regex = globToRegExp(normalizedPattern);
|
|
81
|
+
return regex.test(normalizedPath);
|
|
82
|
+
}
|
|
83
|
+
export function safeReadTextFile(path) {
|
|
84
|
+
try {
|
|
85
|
+
const stats = statSync(path);
|
|
86
|
+
if (stats.size > MAX_READ_BYTES) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
return readFileSync(path, 'utf-8');
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
export function normalizePath(pathValue) {
|
|
96
|
+
return pathValue.split('\\').join('/');
|
|
97
|
+
}
|
|
98
|
+
export function toRelativePosix(root, filePath) {
|
|
99
|
+
const relative = posix.relative(normalizePath(root), normalizePath(filePath));
|
|
100
|
+
return relative.startsWith('../') ? normalizePath(filePath) : relative;
|
|
101
|
+
}
|
|
102
|
+
export function isPathWithinRoot(root, target) {
|
|
103
|
+
const rootAbs = resolve(root);
|
|
104
|
+
const targetAbs = resolve(target);
|
|
105
|
+
const rel = relative(rootAbs, targetAbs);
|
|
106
|
+
return rel === '' || (!rel.startsWith('..') && !rel.includes(`..${posix.sep}`) && !rel.includes('..\\'));
|
|
107
|
+
}
|
|
108
|
+
export function fileExtension(pathValue) {
|
|
109
|
+
return extname(pathValue).replace('.', '').toLowerCase();
|
|
110
|
+
}
|
|
111
|
+
export function baseNameWithoutExt(pathValue) {
|
|
112
|
+
const base = basename(pathValue);
|
|
113
|
+
const ext = extname(base);
|
|
114
|
+
return ext ? base.slice(0, -ext.length) : base;
|
|
115
|
+
}
|
|
116
|
+
function splitCamelCase(value) {
|
|
117
|
+
return value.replace(/([a-z])([A-Z])/g, '$1 $2');
|
|
118
|
+
}
|
|
119
|
+
export function tokenize(value) {
|
|
120
|
+
const normalized = splitCamelCase(value)
|
|
121
|
+
.replace(/[_\-.]/g, ' ')
|
|
122
|
+
.replace(/[^a-zA-Z0-9\s]/g, ' ')
|
|
123
|
+
.toLowerCase();
|
|
124
|
+
return normalized
|
|
125
|
+
.split(/\s+/)
|
|
126
|
+
.map((token) => token.trim())
|
|
127
|
+
.filter((token) => token.length > 2 && !STOP_WORDS.has(token));
|
|
128
|
+
}
|
|
129
|
+
export function uniqueTokens(tokens) {
|
|
130
|
+
return Array.from(new Set(tokens.filter(Boolean)));
|
|
131
|
+
}
|
|
132
|
+
export function titleCase(value) {
|
|
133
|
+
return value
|
|
134
|
+
.split(/[\s_-]+/)
|
|
135
|
+
.filter(Boolean)
|
|
136
|
+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
137
|
+
.join(' ');
|
|
138
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
2
|
+
// See LICENSE.txt for license information.
|
|
3
|
+
/**
|
|
4
|
+
* SelectorValidator: Enforces whitelist matching on generated selectors
|
|
5
|
+
* Comments out unobserved selectors instead of letting tests fail randomly
|
|
6
|
+
*/
|
|
7
|
+
export class SelectorValidator {
|
|
8
|
+
constructor(globalSelectors, minConfidence = 50) {
|
|
9
|
+
this.whitelist = new Set();
|
|
10
|
+
this.semanticWhitelist = new Map();
|
|
11
|
+
this.minConfidence = minConfidence;
|
|
12
|
+
// Build flat whitelist from semantic map
|
|
13
|
+
for (const [semantic, elements] of Object.entries(globalSelectors)) {
|
|
14
|
+
for (const elem of elements) {
|
|
15
|
+
if (elem.confidence >= minConfidence) {
|
|
16
|
+
this.whitelist.add(elem.selector);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
this.semanticWhitelist.set(semantic, elements.map((e) => e.selector));
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Validate a selector against the whitelist
|
|
24
|
+
*/
|
|
25
|
+
validateSelector(selector) {
|
|
26
|
+
if (this.whitelist.has(selector)) {
|
|
27
|
+
return {
|
|
28
|
+
isValid: true,
|
|
29
|
+
confidence: 100,
|
|
30
|
+
reason: 'Found in whitelist',
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
// Check for similar selectors (lenient matching)
|
|
34
|
+
const normalized = this.normalizeSelector(selector);
|
|
35
|
+
for (const whitelisted of this.whitelist) {
|
|
36
|
+
if (this.normalizeSelector(whitelisted).includes(normalized)) {
|
|
37
|
+
return {
|
|
38
|
+
isValid: true,
|
|
39
|
+
confidence: 75,
|
|
40
|
+
reason: 'Found similar whitelisted selector',
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
isValid: false,
|
|
46
|
+
confidence: 0,
|
|
47
|
+
reason: 'Not found in whitelist',
|
|
48
|
+
suggestedComment: `// UNOBSERVED SELECTOR - Not found in UI map. Use test.fixme() if needed.`,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Validate and comment out invalid selectors in generated test code
|
|
53
|
+
*/
|
|
54
|
+
validateTestCode(code) {
|
|
55
|
+
const results = [];
|
|
56
|
+
const selectorRegex = /page\.(getByTestId|getByLabel|getByRole|locator)\(['"`]([^'"`]+)['"`]\)/g;
|
|
57
|
+
let match;
|
|
58
|
+
while ((match = selectorRegex.exec(code)) !== null) {
|
|
59
|
+
const [fullMatch, , selector] = match;
|
|
60
|
+
const validation = this.validateSelector(selector);
|
|
61
|
+
results.push({
|
|
62
|
+
selector,
|
|
63
|
+
isWhitelisted: validation.isValid,
|
|
64
|
+
confidence: validation.confidence,
|
|
65
|
+
originalCode: fullMatch,
|
|
66
|
+
validatedCode: validation.isValid
|
|
67
|
+
? fullMatch
|
|
68
|
+
: `// ${validation.suggestedComment}\n // ${fullMatch}`,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
return results;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Apply validation to test code, commenting out unwhitelisted selectors
|
|
75
|
+
*/
|
|
76
|
+
applyValidation(code) {
|
|
77
|
+
let validated = code;
|
|
78
|
+
const results = this.validateTestCode(code);
|
|
79
|
+
// Apply in reverse order to preserve indices
|
|
80
|
+
for (const result of results.reverse()) {
|
|
81
|
+
if (!result.isWhitelisted) {
|
|
82
|
+
validated = validated.replace(result.originalCode, result.validatedCode);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return validated;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Get validation summary
|
|
89
|
+
*/
|
|
90
|
+
getSummary(code) {
|
|
91
|
+
const results = this.validateTestCode(code);
|
|
92
|
+
const unobserved = results.filter((r) => !r.isWhitelisted).map((r) => r.selector);
|
|
93
|
+
return {
|
|
94
|
+
total: results.length,
|
|
95
|
+
whitelisted: results.filter((r) => r.isWhitelisted).length,
|
|
96
|
+
coverage: results.length > 0
|
|
97
|
+
? Math.round((results.filter((r) => r.isWhitelisted).length / results.length) * 100)
|
|
98
|
+
: 100,
|
|
99
|
+
unobserved,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
normalizeSelector(selector) {
|
|
103
|
+
// Normalize selector for lenient matching
|
|
104
|
+
return selector.toLowerCase().replace(/[^a-z0-9-_]/g, '');
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* APIFallbackResolver: Provides fallback strategies when UI selectors fail
|
|
109
|
+
* Converts test methods to API calls when UI elements aren't available
|
|
110
|
+
*/
|
|
111
|
+
export class APIFallbackResolver {
|
|
112
|
+
constructor() {
|
|
113
|
+
this.apiMapping = new Map([
|
|
114
|
+
// UI action -> API endpoint mapping
|
|
115
|
+
['click.*button.*submit', 'POST /api/v4/posts'],
|
|
116
|
+
['fill.*search', 'GET /api/v4/users'],
|
|
117
|
+
['click.*profile', 'GET /api/v4/users/me'],
|
|
118
|
+
['navigate.*channel', 'GET /api/v4/channels'],
|
|
119
|
+
['click.*settings', 'PATCH /api/v4/users/me'],
|
|
120
|
+
]);
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Check if a test should fall back to API testing
|
|
124
|
+
*/
|
|
125
|
+
shouldFallback(selector, confidence) {
|
|
126
|
+
return confidence < 50;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Generate API-based fallback for unobserved selector
|
|
130
|
+
*/
|
|
131
|
+
generateAPIFallback(selector, action) {
|
|
132
|
+
// Find matching API endpoint
|
|
133
|
+
let endpoint = 'GET /api/v4/';
|
|
134
|
+
for (const [pattern, api] of this.apiMapping) {
|
|
135
|
+
if (new RegExp(pattern, 'i').test(action)) {
|
|
136
|
+
endpoint = api;
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return `
|
|
141
|
+
// UI selector not found - falling back to API
|
|
142
|
+
const response = await fetch(\`\${baseUrl}${endpoint.split(' ')[1]}\`, {
|
|
143
|
+
method: '${endpoint.split(' ')[0]}',
|
|
144
|
+
headers: {'Authorization': \`Bearer \${token}\`},
|
|
145
|
+
});
|
|
146
|
+
expect(response.ok).toBe(true);`;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Wrap unobserved test in try-catch with API fallback
|
|
150
|
+
*/
|
|
151
|
+
wrapWithFallback(testCode) {
|
|
152
|
+
return `
|
|
153
|
+
try {
|
|
154
|
+
${testCode}
|
|
155
|
+
} catch (error) {
|
|
156
|
+
// UI element not found - using API fallback
|
|
157
|
+
${this.generateAPIFallback('unknown', testCode)}
|
|
158
|
+
}`;
|
|
159
|
+
}
|
|
160
|
+
}
|