@continuum8032/playwright-ai-tools 0.2.0-oidc-bootstrap.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/LICENSE +21 -0
- package/README.md +14 -0
- package/dist/chunk-3VDDPPGG.js +927 -0
- package/dist/chunk-GSXFV7YQ.js +544 -0
- package/dist/chunk-QL3XW2UN.js +205 -0
- package/dist/cli.cjs +1750 -0
- package/dist/cli.d.cts +4 -0
- package/dist/cli.d.ts +4 -0
- package/dist/cli.js +308 -0
- package/dist/index.cjs +2030 -0
- package/dist/index.d.cts +218 -0
- package/dist/index.d.ts +218 -0
- package/dist/index.js +354 -0
- package/dist/reporter.cjs +1265 -0
- package/dist/reporter.d.cts +22 -0
- package/dist/reporter.d.ts +22 -0
- package/dist/reporter.js +133 -0
- package/dist/types-B55GjfW7.d.cts +355 -0
- package/dist/types-B55GjfW7.d.ts +355 -0
- package/package.json +58 -0
|
@@ -0,0 +1,1265 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/reporter.ts
|
|
31
|
+
var reporter_exports = {};
|
|
32
|
+
__export(reporter_exports, {
|
|
33
|
+
PlaywrightAiReporter: () => PlaywrightAiReporter,
|
|
34
|
+
default: () => reporter_default
|
|
35
|
+
});
|
|
36
|
+
module.exports = __toCommonJS(reporter_exports);
|
|
37
|
+
|
|
38
|
+
// src/signature.ts
|
|
39
|
+
var import_node_crypto = require("crypto");
|
|
40
|
+
function normalizeErrorForSignature(error) {
|
|
41
|
+
return error.replace(/\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/gi, "UUID").replace(/\d+/g, "N").replace(/(['"])(.*?)\1/g, (_match, quote, value) => {
|
|
42
|
+
const normalizedValue = value.replace(/\d+/g, "N");
|
|
43
|
+
return `${quote}${normalizedValue}${quote}`;
|
|
44
|
+
}).replace(/\/[\w./-]+/g, "PATH").slice(0, 500);
|
|
45
|
+
}
|
|
46
|
+
function failureSignature(failure) {
|
|
47
|
+
return (0, import_node_crypto.createHash)("sha256").update(normalizeErrorForSignature(failure.error)).digest("hex");
|
|
48
|
+
}
|
|
49
|
+
function readablePattern(error) {
|
|
50
|
+
const firstLine2 = error.split("\n")[0] || "Unknown failure";
|
|
51
|
+
return firstLine2.length > 120 ? `${firstLine2.slice(0, 117)}...` : firstLine2;
|
|
52
|
+
}
|
|
53
|
+
function extractSelectors(error) {
|
|
54
|
+
const selectors = /* @__PURE__ */ new Set();
|
|
55
|
+
const patterns = [
|
|
56
|
+
/locator\((['"`])(.+?)\1\)/g,
|
|
57
|
+
/selector\s+(['"`])(.+?)\1/g,
|
|
58
|
+
/(?:getByTestId|data-testid)[^\n'"`]*(['"`])(.+?)\1/g
|
|
59
|
+
];
|
|
60
|
+
for (const pattern of patterns) {
|
|
61
|
+
for (const match of error.matchAll(pattern)) {
|
|
62
|
+
const value = match[2]?.trim();
|
|
63
|
+
if (value) {
|
|
64
|
+
selectors.add(value);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return [...selectors];
|
|
69
|
+
}
|
|
70
|
+
function extractUrls(error) {
|
|
71
|
+
const urls = /* @__PURE__ */ new Set();
|
|
72
|
+
for (const match of error.matchAll(/https?:\/\/[^\s)'"]+|\/api\/[^\s)'"]+/g)) {
|
|
73
|
+
urls.add(match[0]);
|
|
74
|
+
}
|
|
75
|
+
return [...urls];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// src/taxonomy.ts
|
|
79
|
+
function firstLine(text) {
|
|
80
|
+
return text.split("\n")[0] || text;
|
|
81
|
+
}
|
|
82
|
+
function evidence(failure) {
|
|
83
|
+
const items = [firstLine(failure.error)];
|
|
84
|
+
for (const item of failure.consoleErrors || []) {
|
|
85
|
+
items.push(`console:${item.level}:${item.text}`);
|
|
86
|
+
}
|
|
87
|
+
for (const item of failure.networkFailures || []) {
|
|
88
|
+
items.push(`network:${item.method || "GET"}:${item.status || item.errorText}:${item.url}`);
|
|
89
|
+
}
|
|
90
|
+
for (const item of failure.lastActions || []) {
|
|
91
|
+
items.push(`action:${item.name}${item.selector ? `:${item.selector}` : ""}`);
|
|
92
|
+
}
|
|
93
|
+
return items.filter(Boolean);
|
|
94
|
+
}
|
|
95
|
+
function evidenceRefs(failure) {
|
|
96
|
+
const refs = ["error"];
|
|
97
|
+
if (failure.consoleErrors?.length) refs.push("consoleErrors[0]");
|
|
98
|
+
if (failure.networkFailures?.length) refs.push("networkFailures[0]");
|
|
99
|
+
if (failure.lastActions?.length) refs.push("lastActions[-1]");
|
|
100
|
+
if (failure.pageSnapshot) refs.push("pageSnapshot");
|
|
101
|
+
if (failure.artifactRefs?.length || failure.artifacts?.length) refs.push("artifactRefs");
|
|
102
|
+
return refs;
|
|
103
|
+
}
|
|
104
|
+
function matchFailure(failure) {
|
|
105
|
+
const text = [
|
|
106
|
+
failure.error,
|
|
107
|
+
...(failure.consoleErrors || []).map((item) => item.text),
|
|
108
|
+
...(failure.networkFailures || []).map((item) => `${item.status || ""} ${item.errorText || ""} ${item.url}`)
|
|
109
|
+
].join("\n").toLowerCase();
|
|
110
|
+
if (/strict mode violation|resolved to \d+ elements|locator\(.+\) resolved/.test(text)) {
|
|
111
|
+
return {
|
|
112
|
+
category: "selector",
|
|
113
|
+
risk: "medium",
|
|
114
|
+
side: "test_bug",
|
|
115
|
+
rootCause: "A locator is ambiguous or no longer points to the intended element.",
|
|
116
|
+
suggestions: ["Make the locator specific to the intended role, label, text, or test id."],
|
|
117
|
+
confidence: 94,
|
|
118
|
+
humanReview: false,
|
|
119
|
+
reason: "Playwright reported strict locator matching evidence."
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
if (/not receiving pointer events|intercepts pointer events|element is covered|element is detached|not enabled|not editable/.test(text)) {
|
|
123
|
+
return {
|
|
124
|
+
category: "interaction",
|
|
125
|
+
risk: "medium",
|
|
126
|
+
side: "test_bug",
|
|
127
|
+
rootCause: "The page changed the interaction state before Playwright could complete the action.",
|
|
128
|
+
suggestions: ["Wait for the actionable UI state and inspect overlays, disabled states, or animations."],
|
|
129
|
+
confidence: 88,
|
|
130
|
+
humanReview: true,
|
|
131
|
+
reason: "The failure describes an element actionability or interaction problem."
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
if (/screenshot|snapshot|pixel|visual|image comparison|tohavescreenshot/.test(text)) {
|
|
135
|
+
return {
|
|
136
|
+
category: "visual",
|
|
137
|
+
risk: "medium",
|
|
138
|
+
side: "unknown",
|
|
139
|
+
rootCause: "A visual assertion differs from the stored baseline or expected snapshot.",
|
|
140
|
+
suggestions: ["Inspect the screenshot diff and update the baseline only if the product change is intended."],
|
|
141
|
+
confidence: 86,
|
|
142
|
+
humanReview: true,
|
|
143
|
+
reason: "The error contains visual comparison evidence."
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
if (/no test data|fixture|seed|factory|test data|not found for .*user|missing data/.test(text)) {
|
|
147
|
+
return {
|
|
148
|
+
category: "test_data",
|
|
149
|
+
risk: "medium",
|
|
150
|
+
side: "test_bug",
|
|
151
|
+
rootCause: "The test appears to depend on missing or inconsistent data setup.",
|
|
152
|
+
suggestions: ["Make the data fixture explicit and validate setup before the user journey starts."],
|
|
153
|
+
confidence: 84,
|
|
154
|
+
humanReview: false,
|
|
155
|
+
reason: "The failure references missing seeded data or fixture setup."
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
if (/net::err_|econnrefused|enotfound|timed out connecting|500|502|503|504|internal server error|service unavailable|fetch failed/.test(text)) {
|
|
159
|
+
return {
|
|
160
|
+
category: "environment",
|
|
161
|
+
risk: "high",
|
|
162
|
+
side: "env_issue",
|
|
163
|
+
rootCause: "A backend, network, or environment dependency failed during the test.",
|
|
164
|
+
suggestions: ["Check dependency health, request logs, and CI environment configuration for the failing endpoint."],
|
|
165
|
+
confidence: 86,
|
|
166
|
+
humanReview: true,
|
|
167
|
+
reason: "Network or server failure evidence was found."
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
if (/typeerror|referenceerror|cannot read properties|is not defined|uncaught|unhandled/.test(text)) {
|
|
171
|
+
return {
|
|
172
|
+
category: "runtime",
|
|
173
|
+
risk: "high",
|
|
174
|
+
side: "app_bug",
|
|
175
|
+
rootCause: "The application or test runtime threw an exception during the scenario.",
|
|
176
|
+
suggestions: ["Inspect the console stack, failing page state, and recent runtime changes."],
|
|
177
|
+
confidence: 88,
|
|
178
|
+
humanReview: true,
|
|
179
|
+
reason: "Runtime exception evidence was found in the error or console output."
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
if (/timeout|waiting for locator|waiting for selector|tobevisible|to be visible|waiting for .*state/.test(text)) {
|
|
183
|
+
return {
|
|
184
|
+
category: "timing",
|
|
185
|
+
risk: "medium",
|
|
186
|
+
side: "test_bug",
|
|
187
|
+
rootCause: "A Playwright wait timed out before the expected element or state appeared.",
|
|
188
|
+
suggestions: [
|
|
189
|
+
"Verify the expected UI state is still correct.",
|
|
190
|
+
"Wait for the user-visible state with an assertion before acting.",
|
|
191
|
+
"Check whether navigation or data setup leaves the element hidden."
|
|
192
|
+
],
|
|
193
|
+
confidence: 90,
|
|
194
|
+
humanReview: false,
|
|
195
|
+
reason: "Playwright reported a timeout while waiting for an expected state."
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
return {
|
|
199
|
+
category: "unknown",
|
|
200
|
+
risk: "high",
|
|
201
|
+
side: "unknown",
|
|
202
|
+
rootCause: "No deterministic taxonomy rule matched this failure.",
|
|
203
|
+
suggestions: ["Review trace, screenshot, console output, network output, and recent changes."],
|
|
204
|
+
confidence: 30,
|
|
205
|
+
humanReview: true,
|
|
206
|
+
reason: "No deterministic classifier matched the available evidence."
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
function analysisWithDefaults(analysis, failure, source = analysis.source || "unknown") {
|
|
210
|
+
const classified = matchFailure(failure);
|
|
211
|
+
const urls = extractUrls(failure.error);
|
|
212
|
+
return {
|
|
213
|
+
side: analysis.side || classified.side,
|
|
214
|
+
rootCause: analysis.rootCause || classified.rootCause,
|
|
215
|
+
evidence: analysis.evidence?.length ? analysis.evidence : evidence(failure),
|
|
216
|
+
reproductionSteps: analysis.reproductionSteps?.length ? analysis.reproductionSteps : failure.steps?.length ? failure.steps : ["Run the failing Playwright test."],
|
|
217
|
+
suspects: analysis.suspects || {
|
|
218
|
+
selectors: extractSelectors(failure.error),
|
|
219
|
+
pages: urls.filter((url) => !url.startsWith("/api/")),
|
|
220
|
+
endpoints: [
|
|
221
|
+
...urls.filter((url) => url.startsWith("/api/")),
|
|
222
|
+
...(failure.networkFailures || []).map((item) => item.url)
|
|
223
|
+
]
|
|
224
|
+
},
|
|
225
|
+
fixSuggestions: analysis.fixSuggestions?.length ? analysis.fixSuggestions : classified.suggestions,
|
|
226
|
+
confidence: Number(analysis.confidence ?? classified.confidence),
|
|
227
|
+
category: analysis.category || classified.category,
|
|
228
|
+
risk: analysis.risk || classified.risk,
|
|
229
|
+
evidenceRefs: analysis.evidenceRefs?.length ? analysis.evidenceRefs : evidenceRefs(failure),
|
|
230
|
+
confidenceReasons: analysis.confidenceReasons?.length ? analysis.confidenceReasons : [classified.reason],
|
|
231
|
+
needsHumanReview: analysis.needsHumanReview ?? classified.humanReview,
|
|
232
|
+
suggestedFixes: analysis.suggestedFixes,
|
|
233
|
+
source,
|
|
234
|
+
tokenUsage: analysis.tokenUsage
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
function classifyFailure(failure) {
|
|
238
|
+
const matched = matchFailure(failure);
|
|
239
|
+
return analysisWithDefaults(
|
|
240
|
+
{
|
|
241
|
+
side: matched.side,
|
|
242
|
+
rootCause: matched.rootCause,
|
|
243
|
+
fixSuggestions: matched.suggestions,
|
|
244
|
+
confidence: matched.confidence,
|
|
245
|
+
category: matched.category,
|
|
246
|
+
risk: matched.risk,
|
|
247
|
+
needsHumanReview: matched.humanReview,
|
|
248
|
+
source: matched.category === "unknown" ? "unknown" : "deterministic"
|
|
249
|
+
},
|
|
250
|
+
failure,
|
|
251
|
+
matched.category === "unknown" ? "unknown" : "deterministic"
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// src/analyzer.ts
|
|
256
|
+
function deterministicAnalysis(failure) {
|
|
257
|
+
const classified = classifyFailure(failure);
|
|
258
|
+
return classified.category === "unknown" ? null : classified;
|
|
259
|
+
}
|
|
260
|
+
function unknownAnalysis(failure) {
|
|
261
|
+
return analysisWithDefaults(
|
|
262
|
+
{
|
|
263
|
+
rootCause: "No deterministic pattern matched and no AI provider was configured.",
|
|
264
|
+
source: "unknown"
|
|
265
|
+
},
|
|
266
|
+
failure,
|
|
267
|
+
"unknown"
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
function contextQuery(failure) {
|
|
271
|
+
return [
|
|
272
|
+
failure.title,
|
|
273
|
+
failure.file,
|
|
274
|
+
...extractSelectors(failure.error),
|
|
275
|
+
...failure.steps || [],
|
|
276
|
+
failure.error.split("\n")[0]
|
|
277
|
+
].filter(Boolean).join(" ").slice(0, 500);
|
|
278
|
+
}
|
|
279
|
+
function queryTokens(query) {
|
|
280
|
+
return [
|
|
281
|
+
...new Set(
|
|
282
|
+
query.toLowerCase().split(/[^a-z0-9_-]+/i).map((item) => item.trim()).filter((item) => item.length > 2)
|
|
283
|
+
)
|
|
284
|
+
];
|
|
285
|
+
}
|
|
286
|
+
function matchesAnyToken(value, tokens) {
|
|
287
|
+
const text = JSON.stringify(value).toLowerCase();
|
|
288
|
+
return tokens.some((token) => text.includes(token));
|
|
289
|
+
}
|
|
290
|
+
function memorySuggestion(record) {
|
|
291
|
+
const value = record.value || {};
|
|
292
|
+
const summary = typeof value.summary === "string" ? value.summary : typeof value.text === "string" ? value.text : JSON.stringify(value);
|
|
293
|
+
return summary.length > 240 ? `${summary.slice(0, 237)}...` : summary;
|
|
294
|
+
}
|
|
295
|
+
async function contextForFailure(store, failure, options) {
|
|
296
|
+
if (!store) {
|
|
297
|
+
return { memories: [], feedbackReasons: [], similarFailures: [], similarAnalyses: [] };
|
|
298
|
+
}
|
|
299
|
+
const query = contextQuery(failure);
|
|
300
|
+
const tokens = queryTokens(query);
|
|
301
|
+
const namespace = options.projectContext?.namespace || "default";
|
|
302
|
+
const limit = options.similarityLimit ?? 5;
|
|
303
|
+
const memories = [
|
|
304
|
+
...await store.searchMemory({ namespace, limit: 100 }),
|
|
305
|
+
...namespace === "default" ? [] : await store.searchMemory({ namespace: "default", limit: 100 })
|
|
306
|
+
].filter((record) => matchesAnyToken(record, tokens)).slice(0, limit);
|
|
307
|
+
const feedback = (await store.searchFeedback({ limit: 100 })).filter((record) => matchesAnyToken(record, tokens)).slice(0, limit);
|
|
308
|
+
const similarFailures = (await store.findFailures({ limit: 100 })).filter((record) => matchesAnyToken(record, tokens)).slice(0, limit);
|
|
309
|
+
const similarAnalyses = (await Promise.all(similarFailures.map((item) => store.getCachedAnalysis(failureSignature(item))))).filter((item) => Boolean(item));
|
|
310
|
+
return {
|
|
311
|
+
memories,
|
|
312
|
+
feedbackReasons: feedback.map((record) => `feedback:${record.decision}${record.reason ? `:${record.reason}` : ""}`),
|
|
313
|
+
similarFailures,
|
|
314
|
+
similarAnalyses
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
function applyContext(analysis, context) {
|
|
318
|
+
const knownFixes = context.memories.filter((record) => ["known_fix", "knowledge", "known_issue", "owner"].includes(record.kind));
|
|
319
|
+
const suggestions = knownFixes.map(memorySuggestion).filter(Boolean);
|
|
320
|
+
const confidenceReasons = [
|
|
321
|
+
...analysis.confidenceReasons,
|
|
322
|
+
...knownFixes.map((record) => `memory:${record.kind}:${record.key}`),
|
|
323
|
+
...context.feedbackReasons
|
|
324
|
+
];
|
|
325
|
+
if (context.similarFailures.length > 0) {
|
|
326
|
+
confidenceReasons.push(
|
|
327
|
+
`history:${context.similarFailures.length} similar failure${context.similarFailures.length === 1 ? "" : "s"}`
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
const flakyFeedback = context.feedbackReasons.some((reason) => reason.startsWith("feedback:mark_flaky"));
|
|
331
|
+
return {
|
|
332
|
+
...analysis,
|
|
333
|
+
side: flakyFeedback && analysis.side === "unknown" ? "flaky_test" : analysis.side,
|
|
334
|
+
fixSuggestions: [...analysis.fixSuggestions, ...suggestions],
|
|
335
|
+
evidence: analysis.evidence,
|
|
336
|
+
evidenceRefs: analysis.evidenceRefs,
|
|
337
|
+
confidenceReasons: [...new Set(confidenceReasons)],
|
|
338
|
+
needsHumanReview: analysis.needsHumanReview || flakyFeedback || knownFixes.some((record) => record.kind === "known_issue")
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
var DeterministicAnalyzer = class {
|
|
342
|
+
constructor(options = {}) {
|
|
343
|
+
this.provider = options.provider;
|
|
344
|
+
this.outputLanguage = options.outputLanguage || process.env.PW_AI_OUTPUT_LANGUAGE || "ru";
|
|
345
|
+
}
|
|
346
|
+
async analyzeFailure(failure, similarAnalyses = []) {
|
|
347
|
+
const deterministic = deterministicAnalysis(failure);
|
|
348
|
+
if (deterministic) {
|
|
349
|
+
return deterministic;
|
|
350
|
+
}
|
|
351
|
+
if (this.provider) {
|
|
352
|
+
const ai = await this.provider.analyzeFailure({
|
|
353
|
+
failure,
|
|
354
|
+
outputLanguage: this.outputLanguage,
|
|
355
|
+
similarAnalyses
|
|
356
|
+
});
|
|
357
|
+
return analysisWithDefaults(ai, failure, "ai");
|
|
358
|
+
}
|
|
359
|
+
return unknownAnalysis(failure);
|
|
360
|
+
}
|
|
361
|
+
async analyzeFailures(failures) {
|
|
362
|
+
return Promise.all(failures.map((failure) => this.analyzeFailure(failure)));
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
async function analyzeFailures(failures, options = {}) {
|
|
366
|
+
const outputLanguage = options.outputLanguage || process.env.PW_AI_OUTPUT_LANGUAGE || "ru";
|
|
367
|
+
const analyzer = new DeterministicAnalyzer({ provider: options.provider, outputLanguage });
|
|
368
|
+
const results = [];
|
|
369
|
+
for (const failure of failures) {
|
|
370
|
+
const signature = failureSignature(failure);
|
|
371
|
+
const cached = options.store ? await options.store.getCachedAnalysis(signature) : null;
|
|
372
|
+
if (cached) {
|
|
373
|
+
results.push(cached);
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
const context = await contextForFailure(options.store, failure, options);
|
|
377
|
+
const rawAnalysis = await analyzer.analyzeFailure(failure, context.similarAnalyses);
|
|
378
|
+
let analysis = applyContext(rawAnalysis, context);
|
|
379
|
+
if (options.store) {
|
|
380
|
+
await options.store.saveAnalysis(signature, analysis);
|
|
381
|
+
}
|
|
382
|
+
results.push(analysis);
|
|
383
|
+
}
|
|
384
|
+
return results;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// src/evidence.ts
|
|
388
|
+
var import_node_path2 = __toESM(require("path"), 1);
|
|
389
|
+
|
|
390
|
+
// src/trace.ts
|
|
391
|
+
var import_promises = require("fs/promises");
|
|
392
|
+
var import_node_zlib = require("zlib");
|
|
393
|
+
var import_node_path = __toESM(require("path"), 1);
|
|
394
|
+
function parseEvents(raw) {
|
|
395
|
+
const trimmed = raw.trim();
|
|
396
|
+
if (!trimmed) return [];
|
|
397
|
+
try {
|
|
398
|
+
const parsed = JSON.parse(trimmed);
|
|
399
|
+
if (Array.isArray(parsed)) return parsed;
|
|
400
|
+
if (Array.isArray(parsed.events)) return parsed.events;
|
|
401
|
+
if (Array.isArray(parsed.traceEvents)) return parsed.traceEvents;
|
|
402
|
+
return [parsed];
|
|
403
|
+
} catch {
|
|
404
|
+
return trimmed.split(/\r?\n/).map((line) => line.trim()).filter(Boolean).map((line) => JSON.parse(line));
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
function isZip(buffer, filePath) {
|
|
408
|
+
return import_node_path.default.extname(filePath).toLowerCase() === ".zip" || buffer.readUInt32LE(0) === 67324752;
|
|
409
|
+
}
|
|
410
|
+
function isTextTraceEntry(name) {
|
|
411
|
+
const lower = name.toLowerCase();
|
|
412
|
+
if (lower.startsWith("resources/")) return false;
|
|
413
|
+
return lower.endsWith(".trace") || lower.endsWith(".network") || lower.endsWith(".json") || lower.endsWith(".jsonl") || lower.includes("trace") || lower.includes("network");
|
|
414
|
+
}
|
|
415
|
+
function findEndOfCentralDirectory(buffer) {
|
|
416
|
+
for (let index = buffer.length - 22; index >= 0; index -= 1) {
|
|
417
|
+
if (buffer.readUInt32LE(index) === 101010256) return index;
|
|
418
|
+
}
|
|
419
|
+
throw new Error("ZIP central directory not found in trace artifact");
|
|
420
|
+
}
|
|
421
|
+
function extractZipTexts(buffer) {
|
|
422
|
+
const eocd = findEndOfCentralDirectory(buffer);
|
|
423
|
+
const totalEntries = buffer.readUInt16LE(eocd + 10);
|
|
424
|
+
let offset = buffer.readUInt32LE(eocd + 16);
|
|
425
|
+
const texts = [];
|
|
426
|
+
for (let entry = 0; entry < totalEntries; entry += 1) {
|
|
427
|
+
if (buffer.readUInt32LE(offset) !== 33639248) break;
|
|
428
|
+
const method = buffer.readUInt16LE(offset + 10);
|
|
429
|
+
const compressedSize = buffer.readUInt32LE(offset + 20);
|
|
430
|
+
const nameLength = buffer.readUInt16LE(offset + 28);
|
|
431
|
+
const extraLength = buffer.readUInt16LE(offset + 30);
|
|
432
|
+
const commentLength = buffer.readUInt16LE(offset + 32);
|
|
433
|
+
const localOffset = buffer.readUInt32LE(offset + 42);
|
|
434
|
+
const name = buffer.subarray(offset + 46, offset + 46 + nameLength).toString("utf8");
|
|
435
|
+
offset += 46 + nameLength + extraLength + commentLength;
|
|
436
|
+
if (!isTextTraceEntry(name) || buffer.readUInt32LE(localOffset) !== 67324752) continue;
|
|
437
|
+
const localNameLength = buffer.readUInt16LE(localOffset + 26);
|
|
438
|
+
const localExtraLength = buffer.readUInt16LE(localOffset + 28);
|
|
439
|
+
const dataStart = localOffset + 30 + localNameLength + localExtraLength;
|
|
440
|
+
const compressed = buffer.subarray(dataStart, dataStart + compressedSize);
|
|
441
|
+
if (method === 0) {
|
|
442
|
+
texts.push(compressed.toString("utf8"));
|
|
443
|
+
} else if (method === 8) {
|
|
444
|
+
texts.push((0, import_node_zlib.inflateRawSync)(compressed).toString("utf8"));
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
return texts;
|
|
448
|
+
}
|
|
449
|
+
async function readTraceTexts(filePath) {
|
|
450
|
+
const buffer = await (0, import_promises.readFile)(filePath);
|
|
451
|
+
if (isZip(buffer, filePath)) {
|
|
452
|
+
return extractZipTexts(buffer);
|
|
453
|
+
}
|
|
454
|
+
return [buffer.toString("utf8")];
|
|
455
|
+
}
|
|
456
|
+
function timestamp(event) {
|
|
457
|
+
return event.wallTime || event.timestamp || event.time;
|
|
458
|
+
}
|
|
459
|
+
function actionFromEvent(event) {
|
|
460
|
+
const name = event.apiName || event.method || event.action;
|
|
461
|
+
if (!name) return null;
|
|
462
|
+
return {
|
|
463
|
+
name: String(name),
|
|
464
|
+
selector: event.selector || event.params?.selector,
|
|
465
|
+
url: event.url || event.params?.url,
|
|
466
|
+
timestamp: timestamp(event),
|
|
467
|
+
pageId: event.pageId
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
function consoleFromEvent(event) {
|
|
471
|
+
const level = String(event.level || event.messageType || "").toLowerCase();
|
|
472
|
+
const text = event.text || event.message || event.args?.join?.(" ");
|
|
473
|
+
if (!text || !["error", "warning", "warn"].includes(level)) return null;
|
|
474
|
+
return {
|
|
475
|
+
level: level === "warn" ? "warning" : level,
|
|
476
|
+
text: String(text),
|
|
477
|
+
url: event.location?.url || event.url,
|
|
478
|
+
lineNumber: event.location?.lineNumber,
|
|
479
|
+
columnNumber: event.location?.columnNumber,
|
|
480
|
+
timestamp: timestamp(event)
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
function networkFromEvent(event) {
|
|
484
|
+
const snapshot = event.snapshot || event;
|
|
485
|
+
const request = snapshot.request || event.request || {};
|
|
486
|
+
const response = snapshot.response || event.response || {};
|
|
487
|
+
const url = request.url || event.url;
|
|
488
|
+
const status = Number(response.status ?? event.status ?? 0);
|
|
489
|
+
const errorText = event.errorText || snapshot.errorText || response.errorText;
|
|
490
|
+
if (!url || status < 400 && !errorText) return null;
|
|
491
|
+
return {
|
|
492
|
+
url: String(url),
|
|
493
|
+
method: request.method || event.method,
|
|
494
|
+
status: status || void 0,
|
|
495
|
+
statusText: response.statusText,
|
|
496
|
+
errorText,
|
|
497
|
+
timestamp: timestamp(event)
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
function trimLastActions(actions) {
|
|
501
|
+
return actions.slice(-10);
|
|
502
|
+
}
|
|
503
|
+
async function parseTraceArtifact(filePath) {
|
|
504
|
+
const texts = await readTraceTexts(filePath);
|
|
505
|
+
const events = texts.flatMap(parseEvents);
|
|
506
|
+
const summary = {
|
|
507
|
+
consoleErrors: [],
|
|
508
|
+
networkFailures: [],
|
|
509
|
+
lastActions: [],
|
|
510
|
+
artifactRefs: [{ type: "trace", path: filePath }]
|
|
511
|
+
};
|
|
512
|
+
for (const event of events) {
|
|
513
|
+
if (!event || typeof event !== "object") continue;
|
|
514
|
+
if (event.type === "context-options" || event.browserName || event.viewport) {
|
|
515
|
+
const { type: _type, ...context } = event;
|
|
516
|
+
summary.browserContext = { ...summary.browserContext || {}, ...context };
|
|
517
|
+
}
|
|
518
|
+
if (event.type === "before" || event.type === "action") {
|
|
519
|
+
const action = actionFromEvent(event);
|
|
520
|
+
if (action) summary.lastActions = trimLastActions([...summary.lastActions, action]);
|
|
521
|
+
}
|
|
522
|
+
if (event.type === "after" && event.result) {
|
|
523
|
+
summary.page = {
|
|
524
|
+
...summary.page || {},
|
|
525
|
+
url: event.result.url || summary.page?.url,
|
|
526
|
+
title: event.result.title || summary.page?.title
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
const consoleEvent = consoleFromEvent(event);
|
|
530
|
+
if (consoleEvent) summary.consoleErrors.push(consoleEvent);
|
|
531
|
+
const networkEvent = networkFromEvent(event);
|
|
532
|
+
if (networkEvent) summary.networkFailures.push(networkEvent);
|
|
533
|
+
const snapshot = event.snapshot?.html || event.snapshot?.body || event.html;
|
|
534
|
+
if (event.type === "page-snapshot" && snapshot) {
|
|
535
|
+
summary.pageSnapshot = String(snapshot);
|
|
536
|
+
summary.page = { ...summary.page || {}, snapshot: summary.pageSnapshot };
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
return summary;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// src/evidence.ts
|
|
543
|
+
function isTraceArtifact(artifact) {
|
|
544
|
+
const haystack = `${artifact.type} ${artifact.path} ${artifact.contentType || ""}`.toLowerCase();
|
|
545
|
+
return haystack.includes("trace") || haystack.includes("jsonl") || haystack.includes("application/zip");
|
|
546
|
+
}
|
|
547
|
+
function resolveArtifactPath(artifactPath, root) {
|
|
548
|
+
if (import_node_path2.default.isAbsolute(artifactPath)) return artifactPath;
|
|
549
|
+
return import_node_path2.default.resolve(root || process.cwd(), artifactPath);
|
|
550
|
+
}
|
|
551
|
+
async function enrichFailureWithArtifacts(failure, options = {}) {
|
|
552
|
+
const artifacts = failure.artifacts || [];
|
|
553
|
+
const artifactRefs = [...failure.artifactRefs || [], ...artifacts];
|
|
554
|
+
let enriched = { ...failure, artifactRefs };
|
|
555
|
+
for (const artifact of artifacts.filter(isTraceArtifact)) {
|
|
556
|
+
let trace;
|
|
557
|
+
try {
|
|
558
|
+
trace = await parseTraceArtifact(resolveArtifactPath(artifact.path, options.artifactRoot));
|
|
559
|
+
} catch (error) {
|
|
560
|
+
enriched = {
|
|
561
|
+
...enriched,
|
|
562
|
+
artifactRefs: [
|
|
563
|
+
...artifactRefs,
|
|
564
|
+
{
|
|
565
|
+
...artifact,
|
|
566
|
+
metadata: {
|
|
567
|
+
...artifact.metadata || {},
|
|
568
|
+
parseError: error?.message || String(error)
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
]
|
|
572
|
+
};
|
|
573
|
+
continue;
|
|
574
|
+
}
|
|
575
|
+
enriched = {
|
|
576
|
+
...enriched,
|
|
577
|
+
trace,
|
|
578
|
+
browserContext: trace.browserContext || enriched.browserContext,
|
|
579
|
+
page: trace.page || enriched.page,
|
|
580
|
+
pageSnapshot: trace.pageSnapshot || enriched.pageSnapshot,
|
|
581
|
+
consoleErrors: [...enriched.consoleErrors || [], ...trace.consoleErrors],
|
|
582
|
+
networkFailures: [...enriched.networkFailures || [], ...trace.networkFailures],
|
|
583
|
+
lastActions: [...enriched.lastActions || [], ...trace.lastActions].slice(-10),
|
|
584
|
+
artifactRefs: [...artifactRefs, ...trace.artifactRefs]
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
return enriched;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// src/stores/sqlite.ts
|
|
591
|
+
var import_promises2 = require("fs/promises");
|
|
592
|
+
var import_node_path3 = __toESM(require("path"), 1);
|
|
593
|
+
var import_sql = __toESM(require("sql.js"), 1);
|
|
594
|
+
var sqlModule;
|
|
595
|
+
function sql() {
|
|
596
|
+
sqlModule || (sqlModule = (0, import_sql.default)());
|
|
597
|
+
return sqlModule;
|
|
598
|
+
}
|
|
599
|
+
function now() {
|
|
600
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
601
|
+
}
|
|
602
|
+
function normalizeTags(tags) {
|
|
603
|
+
return [...new Set(tags || [])].filter(Boolean).sort();
|
|
604
|
+
}
|
|
605
|
+
function parsePayload(value) {
|
|
606
|
+
return typeof value === "string" ? JSON.parse(value) : value;
|
|
607
|
+
}
|
|
608
|
+
function hasSQLiteHeader(buffer) {
|
|
609
|
+
return buffer.subarray(0, 16).toString("utf8") === "SQLite format 3\0";
|
|
610
|
+
}
|
|
611
|
+
var SQLiteStore = class {
|
|
612
|
+
constructor(options = {}) {
|
|
613
|
+
this.loaded = false;
|
|
614
|
+
this.filePath = options.path || import_node_path3.default.join(process.cwd(), ".playwright-ai-tools", "ai-tools.sqlite");
|
|
615
|
+
this.inMemory = this.filePath === ":memory:";
|
|
616
|
+
}
|
|
617
|
+
async migrate() {
|
|
618
|
+
const db = await this.load();
|
|
619
|
+
db.run(`
|
|
620
|
+
CREATE TABLE IF NOT EXISTS runs (
|
|
621
|
+
id TEXT PRIMARY KEY,
|
|
622
|
+
payload TEXT NOT NULL,
|
|
623
|
+
created_at TEXT DEFAULT CURRENT_TIMESTAMP
|
|
624
|
+
);
|
|
625
|
+
|
|
626
|
+
CREATE TABLE IF NOT EXISTS failures (
|
|
627
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
628
|
+
signature TEXT,
|
|
629
|
+
file TEXT,
|
|
630
|
+
title TEXT,
|
|
631
|
+
run_id TEXT,
|
|
632
|
+
payload TEXT NOT NULL,
|
|
633
|
+
created_at TEXT DEFAULT CURRENT_TIMESTAMP
|
|
634
|
+
);
|
|
635
|
+
|
|
636
|
+
CREATE TABLE IF NOT EXISTS analyses (
|
|
637
|
+
signature TEXT PRIMARY KEY,
|
|
638
|
+
payload TEXT NOT NULL,
|
|
639
|
+
updated_at TEXT DEFAULT CURRENT_TIMESTAMP
|
|
640
|
+
);
|
|
641
|
+
|
|
642
|
+
CREATE TABLE IF NOT EXISTS memory_records (
|
|
643
|
+
id TEXT PRIMARY KEY,
|
|
644
|
+
namespace TEXT NOT NULL,
|
|
645
|
+
kind TEXT NOT NULL,
|
|
646
|
+
key TEXT NOT NULL,
|
|
647
|
+
tags TEXT NOT NULL,
|
|
648
|
+
payload TEXT NOT NULL,
|
|
649
|
+
updated_at TEXT DEFAULT CURRENT_TIMESTAMP
|
|
650
|
+
);
|
|
651
|
+
|
|
652
|
+
CREATE TABLE IF NOT EXISTS feedback_records (
|
|
653
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
654
|
+
signature TEXT NOT NULL,
|
|
655
|
+
decision TEXT NOT NULL,
|
|
656
|
+
reason TEXT,
|
|
657
|
+
payload TEXT NOT NULL,
|
|
658
|
+
created_at TEXT DEFAULT CURRENT_TIMESTAMP
|
|
659
|
+
);
|
|
660
|
+
|
|
661
|
+
CREATE INDEX IF NOT EXISTS idx_failures_signature ON failures(signature);
|
|
662
|
+
CREATE INDEX IF NOT EXISTS idx_failures_file ON failures(file);
|
|
663
|
+
CREATE INDEX IF NOT EXISTS idx_memory_namespace_kind ON memory_records(namespace, kind);
|
|
664
|
+
CREATE INDEX IF NOT EXISTS idx_feedback_signature ON feedback_records(signature);
|
|
665
|
+
CREATE INDEX IF NOT EXISTS idx_feedback_decision ON feedback_records(decision);
|
|
666
|
+
CREATE INDEX IF NOT EXISTS idx_analyses_category ON analyses(json_extract(payload, '$.category'));
|
|
667
|
+
CREATE INDEX IF NOT EXISTS idx_analyses_risk ON analyses(json_extract(payload, '$.risk'));
|
|
668
|
+
CREATE INDEX IF NOT EXISTS idx_failures_error ON failures(json_extract(payload, '$.error'));
|
|
669
|
+
`);
|
|
670
|
+
await this.persist();
|
|
671
|
+
}
|
|
672
|
+
async saveRun(run) {
|
|
673
|
+
const db = await this.load();
|
|
674
|
+
db.run(
|
|
675
|
+
"INSERT OR REPLACE INTO runs (id, payload) VALUES (?, ?)",
|
|
676
|
+
[run.id, JSON.stringify(run)]
|
|
677
|
+
);
|
|
678
|
+
await this.persist();
|
|
679
|
+
}
|
|
680
|
+
async saveFailure(failure) {
|
|
681
|
+
const db = await this.load();
|
|
682
|
+
const signature = failureSignature(failure);
|
|
683
|
+
const runId = typeof failure.run?.id === "string" ? failure.run.id : null;
|
|
684
|
+
db.run(
|
|
685
|
+
"INSERT INTO failures (signature, file, title, run_id, payload) VALUES (?, ?, ?, ?, ?)",
|
|
686
|
+
[signature, failure.file, failure.title, runId, JSON.stringify(failure)]
|
|
687
|
+
);
|
|
688
|
+
await this.persist();
|
|
689
|
+
}
|
|
690
|
+
async saveAnalysis(signature, analysis) {
|
|
691
|
+
const db = await this.load();
|
|
692
|
+
db.run(
|
|
693
|
+
`
|
|
694
|
+
INSERT INTO analyses (signature, payload, updated_at)
|
|
695
|
+
VALUES (?, ?, CURRENT_TIMESTAMP)
|
|
696
|
+
ON CONFLICT(signature) DO UPDATE SET payload = excluded.payload, updated_at = CURRENT_TIMESTAMP
|
|
697
|
+
`,
|
|
698
|
+
[signature, JSON.stringify(analysis)]
|
|
699
|
+
);
|
|
700
|
+
await this.persist();
|
|
701
|
+
}
|
|
702
|
+
async getCachedAnalysis(signature) {
|
|
703
|
+
const rows = await this.all("SELECT payload FROM analyses WHERE signature = ? LIMIT 1", [signature]);
|
|
704
|
+
return rows[0]?.payload ? { ...parsePayload(rows[0].payload), source: "cache" } : null;
|
|
705
|
+
}
|
|
706
|
+
async upsertMemory(record) {
|
|
707
|
+
const db = await this.load();
|
|
708
|
+
const id = `${record.namespace}:${record.kind}:${record.key}`;
|
|
709
|
+
const existing = await this.searchMemory({
|
|
710
|
+
namespace: record.namespace,
|
|
711
|
+
kind: record.kind,
|
|
712
|
+
query: record.key,
|
|
713
|
+
limit: 100
|
|
714
|
+
});
|
|
715
|
+
const previous = existing.find((item) => item.key === record.key);
|
|
716
|
+
const memory = {
|
|
717
|
+
namespace: record.namespace,
|
|
718
|
+
kind: record.kind,
|
|
719
|
+
key: record.key,
|
|
720
|
+
value: record.value,
|
|
721
|
+
tags: normalizeTags(record.tags),
|
|
722
|
+
confidence: record.confidence ?? 1,
|
|
723
|
+
source: record.source,
|
|
724
|
+
supersededBy: previous?.supersededBy ?? null,
|
|
725
|
+
createdAt: previous?.createdAt ?? now(),
|
|
726
|
+
updatedAt: now()
|
|
727
|
+
};
|
|
728
|
+
db.run(
|
|
729
|
+
`
|
|
730
|
+
INSERT INTO memory_records (id, namespace, kind, key, tags, payload, updated_at)
|
|
731
|
+
VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
|
|
732
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
733
|
+
namespace = excluded.namespace,
|
|
734
|
+
kind = excluded.kind,
|
|
735
|
+
key = excluded.key,
|
|
736
|
+
tags = excluded.tags,
|
|
737
|
+
payload = excluded.payload,
|
|
738
|
+
updated_at = CURRENT_TIMESTAMP
|
|
739
|
+
`,
|
|
740
|
+
[id, memory.namespace, memory.kind, memory.key, JSON.stringify(memory.tags), JSON.stringify(memory)]
|
|
741
|
+
);
|
|
742
|
+
await this.persist();
|
|
743
|
+
}
|
|
744
|
+
async searchMemory(input) {
|
|
745
|
+
const rows = await this.all("SELECT payload FROM memory_records ORDER BY updated_at DESC");
|
|
746
|
+
const query = input.query?.toLowerCase().trim();
|
|
747
|
+
const tags = new Set(input.tags || []);
|
|
748
|
+
return rows.map((row) => parsePayload(row.payload)).filter((record) => {
|
|
749
|
+
if (input.namespace && record.namespace !== input.namespace) return false;
|
|
750
|
+
if (input.kind && record.kind !== input.kind) return false;
|
|
751
|
+
if (tags.size > 0 && !record.tags.some((tag) => tags.has(tag))) return false;
|
|
752
|
+
if (query && !JSON.stringify([record.key, record.kind, record.source, record.tags, record.value]).toLowerCase().includes(query)) {
|
|
753
|
+
return false;
|
|
754
|
+
}
|
|
755
|
+
return !record.supersededBy;
|
|
756
|
+
}).slice(0, input.limit ?? 20);
|
|
757
|
+
}
|
|
758
|
+
async recordFeedback(input) {
|
|
759
|
+
const db = await this.load();
|
|
760
|
+
const feedback = {
|
|
761
|
+
signature: input.signature,
|
|
762
|
+
decision: input.decision,
|
|
763
|
+
reason: input.reason || "",
|
|
764
|
+
metadata: input.metadata || {},
|
|
765
|
+
createdAt: now()
|
|
766
|
+
};
|
|
767
|
+
db.run(
|
|
768
|
+
"INSERT INTO feedback_records (signature, decision, reason, payload) VALUES (?, ?, ?, ?)",
|
|
769
|
+
[feedback.signature, feedback.decision, feedback.reason, JSON.stringify(feedback)]
|
|
770
|
+
);
|
|
771
|
+
await this.persist();
|
|
772
|
+
}
|
|
773
|
+
async searchFeedback(input) {
|
|
774
|
+
const rows = await this.all("SELECT payload FROM feedback_records ORDER BY created_at DESC");
|
|
775
|
+
const query = input.query?.toLowerCase().trim();
|
|
776
|
+
return rows.map((row) => parsePayload(row.payload)).filter((record) => {
|
|
777
|
+
if (input.signature && record.signature !== input.signature) return false;
|
|
778
|
+
if (input.decision && record.decision !== input.decision) return false;
|
|
779
|
+
if (query && !JSON.stringify(record).toLowerCase().includes(query)) return false;
|
|
780
|
+
return true;
|
|
781
|
+
}).slice(0, input.limit ?? 20);
|
|
782
|
+
}
|
|
783
|
+
async findFailures(input) {
|
|
784
|
+
const rows = await this.all("SELECT payload FROM failures ORDER BY id DESC");
|
|
785
|
+
const query = input.query?.toLowerCase().trim();
|
|
786
|
+
return rows.map((row) => parsePayload(row.payload)).filter((failure) => {
|
|
787
|
+
if (input.signature && failureSignature(failure) !== input.signature) return false;
|
|
788
|
+
if (input.file && failure.file !== input.file) return false;
|
|
789
|
+
if (query && !JSON.stringify(failure).toLowerCase().includes(query)) return false;
|
|
790
|
+
return true;
|
|
791
|
+
}).slice(0, input.limit ?? 20);
|
|
792
|
+
}
|
|
793
|
+
async getRun(id) {
|
|
794
|
+
const rows = await this.all("SELECT payload FROM runs WHERE id = ? LIMIT 1", [id]);
|
|
795
|
+
return rows[0]?.payload ? parsePayload(rows[0].payload) : null;
|
|
796
|
+
}
|
|
797
|
+
async stats() {
|
|
798
|
+
const tables = ["runs", "failures", "analyses", "memory_records", "feedback_records"];
|
|
799
|
+
const stats = {};
|
|
800
|
+
for (const table of tables) {
|
|
801
|
+
const rows = await this.all(`SELECT COUNT(*) AS count FROM ${table}`);
|
|
802
|
+
stats[table === "memory_records" ? "memoryRecords" : table === "feedback_records" ? "feedbackRecords" : table] = Number(rows[0]?.count || 0);
|
|
803
|
+
}
|
|
804
|
+
return stats;
|
|
805
|
+
}
|
|
806
|
+
async close() {
|
|
807
|
+
if (!this.db) return;
|
|
808
|
+
await this.persist();
|
|
809
|
+
this.db.close();
|
|
810
|
+
this.db = void 0;
|
|
811
|
+
this.loaded = false;
|
|
812
|
+
}
|
|
813
|
+
async load() {
|
|
814
|
+
if (this.loaded && this.db) return this.db;
|
|
815
|
+
this.loaded = true;
|
|
816
|
+
const SQL = await sql();
|
|
817
|
+
if (this.inMemory) {
|
|
818
|
+
this.db = new SQL.Database();
|
|
819
|
+
await this.migrate();
|
|
820
|
+
return this.db;
|
|
821
|
+
}
|
|
822
|
+
try {
|
|
823
|
+
const buffer = await (0, import_promises2.readFile)(this.filePath);
|
|
824
|
+
if (buffer.length === 0) {
|
|
825
|
+
this.db = new SQL.Database();
|
|
826
|
+
await this.migrate();
|
|
827
|
+
} else if (hasSQLiteHeader(buffer)) {
|
|
828
|
+
this.db = new SQL.Database(buffer);
|
|
829
|
+
} else {
|
|
830
|
+
this.db = new SQL.Database();
|
|
831
|
+
await this.migrate();
|
|
832
|
+
await this.importSnapshot(JSON.parse(buffer.toString("utf8")));
|
|
833
|
+
}
|
|
834
|
+
} catch (error) {
|
|
835
|
+
if (error?.code !== "ENOENT") {
|
|
836
|
+
throw error;
|
|
837
|
+
}
|
|
838
|
+
this.db = new SQL.Database();
|
|
839
|
+
await this.migrate();
|
|
840
|
+
}
|
|
841
|
+
return this.db;
|
|
842
|
+
}
|
|
843
|
+
async importSnapshot(snapshot) {
|
|
844
|
+
for (const run of snapshot.runs || []) {
|
|
845
|
+
await this.saveRun(run);
|
|
846
|
+
}
|
|
847
|
+
for (const failure of snapshot.failures || []) {
|
|
848
|
+
await this.saveFailure(failure);
|
|
849
|
+
}
|
|
850
|
+
for (const [signature, analysis] of snapshot.analyses || []) {
|
|
851
|
+
await this.saveAnalysis(signature, analysis);
|
|
852
|
+
}
|
|
853
|
+
for (const [, memory] of snapshot.memory || []) {
|
|
854
|
+
await this.upsertMemory(memory);
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
async all(sqlText, params = []) {
|
|
858
|
+
const db = await this.load();
|
|
859
|
+
const statement = db.prepare(sqlText, params);
|
|
860
|
+
const rows = [];
|
|
861
|
+
try {
|
|
862
|
+
while (statement.step()) {
|
|
863
|
+
rows.push(statement.getAsObject());
|
|
864
|
+
}
|
|
865
|
+
} finally {
|
|
866
|
+
statement.free();
|
|
867
|
+
}
|
|
868
|
+
return rows;
|
|
869
|
+
}
|
|
870
|
+
async persist() {
|
|
871
|
+
if (!this.db || this.inMemory) return;
|
|
872
|
+
await (0, import_promises2.mkdir)(import_node_path3.default.dirname(this.filePath), { recursive: true });
|
|
873
|
+
await (0, import_promises2.writeFile)(this.filePath, Buffer.from(this.db.export()));
|
|
874
|
+
}
|
|
875
|
+
};
|
|
876
|
+
|
|
877
|
+
// src/report.ts
|
|
878
|
+
var import_promises3 = require("fs/promises");
|
|
879
|
+
var import_node_path5 = __toESM(require("path"), 1);
|
|
880
|
+
|
|
881
|
+
// src/grouper.ts
|
|
882
|
+
var FailureGrouper = class {
|
|
883
|
+
group(failures) {
|
|
884
|
+
const groups = /* @__PURE__ */ new Map();
|
|
885
|
+
for (const failure of failures) {
|
|
886
|
+
const signature = failureSignature(failure);
|
|
887
|
+
groups.set(signature, [...groups.get(signature) || [], failure]);
|
|
888
|
+
}
|
|
889
|
+
return [...groups.entries()].map(([signature, groupedFailures]) => ({
|
|
890
|
+
signature,
|
|
891
|
+
pattern: readablePattern(groupedFailures[0]?.error || ""),
|
|
892
|
+
failures: groupedFailures
|
|
893
|
+
})).sort((a, b) => b.failures.length - a.failures.length);
|
|
894
|
+
}
|
|
895
|
+
};
|
|
896
|
+
|
|
897
|
+
// src/suggestions.ts
|
|
898
|
+
var import_node_fs = require("fs");
|
|
899
|
+
var import_node_path4 = __toESM(require("path"), 1);
|
|
900
|
+
function assertionCount(text) {
|
|
901
|
+
return (text.match(/\bexpect\s*\(/g) || []).length;
|
|
902
|
+
}
|
|
903
|
+
function onlyTimeoutChanged(oldSnippet, newSnippet) {
|
|
904
|
+
const oldNormalized = oldSnippet.replace(/\d+/g, "#").replace(/\s+/g, " ").trim();
|
|
905
|
+
const newNormalized = newSnippet.replace(/\d+/g, "#").replace(/\s+/g, " ").trim();
|
|
906
|
+
return oldNormalized === newNormalized && /timeout/i.test(`${oldSnippet}
|
|
907
|
+
${newSnippet}`);
|
|
908
|
+
}
|
|
909
|
+
function testIdLocator(selector) {
|
|
910
|
+
const match = selector.match(/\[data-testid=['"]?([^'"\]]+)['"]?\]/i);
|
|
911
|
+
if (match) return `page.getByTestId("${match[1]}")`;
|
|
912
|
+
return `page.locator(${JSON.stringify(selector)})`;
|
|
913
|
+
}
|
|
914
|
+
function firstSelector(failure) {
|
|
915
|
+
return extractSelectors(failure.error)[0] || failure.lastActions?.find((action) => action.selector)?.selector;
|
|
916
|
+
}
|
|
917
|
+
function sourceLine(failure, options) {
|
|
918
|
+
if (!failure.line) return void 0;
|
|
919
|
+
const filePath = import_node_path4.default.isAbsolute(failure.file) ? failure.file : import_node_path4.default.resolve(options.workspaceRoot || process.cwd(), failure.file);
|
|
920
|
+
if (!(0, import_node_fs.existsSync)(filePath)) return void 0;
|
|
921
|
+
const lines = (0, import_node_fs.readFileSync)(filePath, "utf8").split(/\r?\n/);
|
|
922
|
+
return lines[failure.line - 1];
|
|
923
|
+
}
|
|
924
|
+
function evaluatePatchPolicy(suggestion) {
|
|
925
|
+
const oldAssertions = assertionCount(suggestion.oldSnippet);
|
|
926
|
+
const newAssertions = assertionCount(suggestion.newSnippet);
|
|
927
|
+
if (oldAssertions > 0 && newAssertions < oldAssertions) {
|
|
928
|
+
return {
|
|
929
|
+
...suggestion,
|
|
930
|
+
policyDecision: "blocked",
|
|
931
|
+
reviewReason: "Blocked: patch weakens or removes an assertion."
|
|
932
|
+
};
|
|
933
|
+
}
|
|
934
|
+
if (/assertion removed|delete assertion|remove assertion/i.test(suggestion.newSnippet)) {
|
|
935
|
+
return {
|
|
936
|
+
...suggestion,
|
|
937
|
+
policyDecision: "blocked",
|
|
938
|
+
reviewReason: "Blocked: patch appears to remove an assertion."
|
|
939
|
+
};
|
|
940
|
+
}
|
|
941
|
+
if (onlyTimeoutChanged(suggestion.oldSnippet, suggestion.newSnippet) && suggestion.evidence.length === 0) {
|
|
942
|
+
return {
|
|
943
|
+
...suggestion,
|
|
944
|
+
policyDecision: "blocked",
|
|
945
|
+
reviewReason: "Blocked: timeout-only changes require supporting evidence."
|
|
946
|
+
};
|
|
947
|
+
}
|
|
948
|
+
if (/\btest\.skip\b|\breturn;\s*$|TODO:\s*remove check/i.test(suggestion.newSnippet)) {
|
|
949
|
+
return {
|
|
950
|
+
...suggestion,
|
|
951
|
+
policyDecision: "blocked",
|
|
952
|
+
reviewReason: "Blocked: patch changes test intent instead of explaining the failure."
|
|
953
|
+
};
|
|
954
|
+
}
|
|
955
|
+
return {
|
|
956
|
+
...suggestion,
|
|
957
|
+
policyDecision: "review",
|
|
958
|
+
reviewReason: suggestion.reviewReason || "Requires human approval; core never applies patches automatically."
|
|
959
|
+
};
|
|
960
|
+
}
|
|
961
|
+
function generatePatchSuggestions(failure, analysis, options = {}) {
|
|
962
|
+
const evidence2 = [...analysis.evidence || [], ...analysis.evidenceRefs || []].filter(Boolean);
|
|
963
|
+
const selector = firstSelector(failure);
|
|
964
|
+
const locator = selector ? testIdLocator(selector) : 'page.locator("<target>")';
|
|
965
|
+
const lastAction = failure.lastActions?.slice(-1)[0];
|
|
966
|
+
const oldSourceLine = sourceLine(failure, options);
|
|
967
|
+
let suggestion;
|
|
968
|
+
if (analysis.category === "selector") {
|
|
969
|
+
suggestion = {
|
|
970
|
+
filePath: failure.file,
|
|
971
|
+
oldSnippet: `await ${locator}.click();`,
|
|
972
|
+
newSnippet: `await ${locator}.filter({ hasText: "<expected text>" }).click();`,
|
|
973
|
+
explanation: "Narrow the locator to the intended element instead of relying on an ambiguous match.",
|
|
974
|
+
evidence: evidence2,
|
|
975
|
+
risk: analysis.risk,
|
|
976
|
+
policyDecision: "review",
|
|
977
|
+
reviewReason: ""
|
|
978
|
+
};
|
|
979
|
+
} else if (analysis.category === "environment") {
|
|
980
|
+
suggestion = {
|
|
981
|
+
filePath: failure.file,
|
|
982
|
+
oldSnippet: 'await page.goto("<page>");',
|
|
983
|
+
newSnippet: 'const response = await page.goto("<page>");\nawait expect(response, "page request should succeed").toBeOK();',
|
|
984
|
+
explanation: "Expose the failing backend or environment dependency before continuing with UI assertions.",
|
|
985
|
+
evidence: evidence2,
|
|
986
|
+
risk: analysis.risk,
|
|
987
|
+
policyDecision: "review",
|
|
988
|
+
reviewReason: ""
|
|
989
|
+
};
|
|
990
|
+
} else if (analysis.category === "runtime") {
|
|
991
|
+
suggestion = {
|
|
992
|
+
filePath: failure.file,
|
|
993
|
+
oldSnippet: 'await page.goto("<page>");',
|
|
994
|
+
newSnippet: 'const consoleErrors: string[] = [];\npage.on("console", (message) => message.type() === "error" && consoleErrors.push(message.text()));\nawait page.goto("<page>");',
|
|
995
|
+
explanation: "Capture runtime console evidence so the owner can fix the thrown exception with context.",
|
|
996
|
+
evidence: evidence2,
|
|
997
|
+
risk: analysis.risk,
|
|
998
|
+
policyDecision: "review",
|
|
999
|
+
reviewReason: ""
|
|
1000
|
+
};
|
|
1001
|
+
} else {
|
|
1002
|
+
const actionLocator = lastAction?.selector ? testIdLocator(lastAction.selector) : locator;
|
|
1003
|
+
suggestion = {
|
|
1004
|
+
filePath: failure.file,
|
|
1005
|
+
line: failure.line,
|
|
1006
|
+
oldSnippet: oldSourceLine || `await ${actionLocator}.click();`,
|
|
1007
|
+
newSnippet: oldSourceLine ? `${oldSourceLine.match(/^\s*/)?.[0] || ""}await expect(${actionLocator}).toBeVisible();
|
|
1008
|
+
${oldSourceLine}` : `await expect(${actionLocator}).toBeVisible();
|
|
1009
|
+
await ${actionLocator}.click();`,
|
|
1010
|
+
explanation: "Wait for the user-visible state before the action; do not increase timeouts without evidence.",
|
|
1011
|
+
evidence: evidence2,
|
|
1012
|
+
risk: analysis.risk,
|
|
1013
|
+
policyDecision: "review",
|
|
1014
|
+
reviewReason: ""
|
|
1015
|
+
};
|
|
1016
|
+
}
|
|
1017
|
+
return [evaluatePatchPolicy(suggestion)];
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
// src/report.ts
|
|
1021
|
+
function escapeHtml(value) {
|
|
1022
|
+
return String(value ?? "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
1023
|
+
}
|
|
1024
|
+
function normalizedReport(report) {
|
|
1025
|
+
return {
|
|
1026
|
+
...report,
|
|
1027
|
+
suggestions: report.suggestions || report.failures.map((failure, index) => {
|
|
1028
|
+
const analysis = report.analyses[index];
|
|
1029
|
+
return analysis ? generatePatchSuggestions(failure, analysis) : [];
|
|
1030
|
+
})
|
|
1031
|
+
};
|
|
1032
|
+
}
|
|
1033
|
+
function markdownReport(report) {
|
|
1034
|
+
const portable = normalizedReport(report);
|
|
1035
|
+
const lines = [
|
|
1036
|
+
"# Playwright AI Analysis Report",
|
|
1037
|
+
"",
|
|
1038
|
+
`Run: ${portable.run.id}`,
|
|
1039
|
+
`Failures: ${portable.failures.length}`,
|
|
1040
|
+
""
|
|
1041
|
+
];
|
|
1042
|
+
portable.failures.forEach((failure, index) => {
|
|
1043
|
+
const analysis = portable.analyses[index];
|
|
1044
|
+
const suggestions = portable.suggestions[index] || [];
|
|
1045
|
+
lines.push(`## ${failure.title}`);
|
|
1046
|
+
lines.push("");
|
|
1047
|
+
lines.push(`- File: ${failure.file}${failure.line ? `:${failure.line}` : ""}`);
|
|
1048
|
+
lines.push(`- Category: ${analysis?.category || "unknown"}`);
|
|
1049
|
+
lines.push(`- Side: ${analysis?.side || "unknown"}`);
|
|
1050
|
+
lines.push(`- Risk: ${analysis?.risk || "high"}`);
|
|
1051
|
+
lines.push(`- Confidence: ${analysis?.confidence ?? 0}`);
|
|
1052
|
+
lines.push(`- Root cause: ${analysis?.rootCause || "n/a"}`);
|
|
1053
|
+
if (analysis?.evidenceRefs?.length) {
|
|
1054
|
+
lines.push(`- Evidence refs: ${analysis.evidenceRefs.join(", ")}`);
|
|
1055
|
+
}
|
|
1056
|
+
if (failure.consoleErrors?.length || failure.networkFailures?.length) {
|
|
1057
|
+
lines.push("- Evidence:");
|
|
1058
|
+
for (const item of failure.consoleErrors || []) lines.push(` - Console ${item.level}: ${item.text}`);
|
|
1059
|
+
for (const item of failure.networkFailures || []) {
|
|
1060
|
+
lines.push(` - Network ${item.status || item.errorText}: ${item.method || "GET"} ${item.url}`);
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
if (suggestions.length) {
|
|
1064
|
+
lines.push("");
|
|
1065
|
+
lines.push("### Suggested Fixes");
|
|
1066
|
+
for (const suggestion of suggestions) {
|
|
1067
|
+
lines.push(`- ${suggestion.policyDecision}: ${suggestion.explanation} (${suggestion.reviewReason})`);
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
lines.push("");
|
|
1071
|
+
});
|
|
1072
|
+
return `${lines.join("\n")}
|
|
1073
|
+
`;
|
|
1074
|
+
}
|
|
1075
|
+
function htmlReport(report) {
|
|
1076
|
+
const portable = normalizedReport(report);
|
|
1077
|
+
const groups = new FailureGrouper().group(portable.failures);
|
|
1078
|
+
const groupSummary = groups.map((group) => `<li><code>${escapeHtml(group.signature)}</code> ${group.failures.length} failure(s)</li>`).join("");
|
|
1079
|
+
const failures = portable.failures.map((failure, index) => {
|
|
1080
|
+
const analysis = portable.analyses[index];
|
|
1081
|
+
const suggestions = portable.suggestions[index] || [];
|
|
1082
|
+
const evidence2 = [
|
|
1083
|
+
...(failure.consoleErrors || []).map((item) => `Console ${item.level}: ${item.text}`),
|
|
1084
|
+
...(failure.networkFailures || []).map(
|
|
1085
|
+
(item) => `Network ${item.status || item.errorText}: ${item.method || "GET"} ${item.url}`
|
|
1086
|
+
),
|
|
1087
|
+
...(failure.lastActions || []).map((item) => `Action ${item.name}${item.selector ? ` ${item.selector}` : ""}`),
|
|
1088
|
+
...(failure.artifactRefs || failure.artifacts || []).map((item) => `Artifact ${item.type}: ${item.path}`)
|
|
1089
|
+
];
|
|
1090
|
+
return `<section class="failure" data-category="${escapeHtml(analysis?.category || "unknown")}">
|
|
1091
|
+
<h2>${escapeHtml(failure.title)}</h2>
|
|
1092
|
+
<p><strong>${escapeHtml(analysis?.category || "unknown")}</strong> ${escapeHtml(analysis?.side || "unknown")} risk=${escapeHtml(analysis?.risk || "high")} confidence=${escapeHtml(analysis?.confidence ?? 0)} humanReview=${escapeHtml(analysis?.needsHumanReview ?? true)}</p>
|
|
1093
|
+
<p>${escapeHtml(analysis?.rootCause || failure.error)}</p>
|
|
1094
|
+
<h3>Evidence</h3>
|
|
1095
|
+
<ul>${evidence2.map((item) => `<li>${escapeHtml(item)}</li>`).join("") || "<li>No extra evidence collected.</li>"}</ul>
|
|
1096
|
+
<h3>Suggested Fixes</h3>
|
|
1097
|
+
<ul>${suggestions.map(
|
|
1098
|
+
(suggestion) => `<li><strong>${escapeHtml(suggestion.policyDecision)}</strong> ${escapeHtml(suggestion.explanation)}<pre>${escapeHtml(suggestion.newSnippet)}</pre><small>${escapeHtml(suggestion.reviewReason)}</small></li>`
|
|
1099
|
+
).join("") || "<li>No suggestion available.</li>"}</ul>
|
|
1100
|
+
</section>`;
|
|
1101
|
+
}).join("\n");
|
|
1102
|
+
return `<!doctype html>
|
|
1103
|
+
<html lang="en">
|
|
1104
|
+
<head>
|
|
1105
|
+
<meta charset="utf-8">
|
|
1106
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
1107
|
+
<title>Playwright AI Analysis Report</title>
|
|
1108
|
+
<style>
|
|
1109
|
+
body{font-family:system-ui,-apple-system,Segoe UI,sans-serif;margin:0;background:#f7f7f4;color:#171717}
|
|
1110
|
+
header,main{max-width:1120px;margin:0 auto;padding:24px}
|
|
1111
|
+
header{border-bottom:1px solid #d8d8d2}
|
|
1112
|
+
.summary{display:flex;gap:16px;flex-wrap:wrap}
|
|
1113
|
+
.pill{border:1px solid #c9c9c1;border-radius:6px;padding:6px 10px;background:white}
|
|
1114
|
+
.failure{background:white;border:1px solid #d8d8d2;border-radius:8px;padding:18px;margin:18px 0}
|
|
1115
|
+
pre{white-space:pre-wrap;background:#111;color:#f5f5f5;border-radius:6px;padding:12px;overflow:auto}
|
|
1116
|
+
code{font-size:.9em}
|
|
1117
|
+
</style>
|
|
1118
|
+
</head>
|
|
1119
|
+
<body>
|
|
1120
|
+
<header>
|
|
1121
|
+
<h1>Playwright AI Analysis Report</h1>
|
|
1122
|
+
<div class="summary"><span class="pill">Run ${escapeHtml(portable.run.id)}</span><span class="pill">Failures ${portable.failures.length}</span><span class="pill">Cache hits ${portable.analyses.filter((analysis) => analysis.source === "cache").length}</span></div>
|
|
1123
|
+
</header>
|
|
1124
|
+
<main>
|
|
1125
|
+
<h2>Groups</h2>
|
|
1126
|
+
<ul>${groupSummary}</ul>
|
|
1127
|
+
${failures}
|
|
1128
|
+
</main>
|
|
1129
|
+
</body>
|
|
1130
|
+
</html>
|
|
1131
|
+
`;
|
|
1132
|
+
}
|
|
1133
|
+
async function writePortableReport(outputDir, report) {
|
|
1134
|
+
await (0, import_promises3.mkdir)(outputDir, { recursive: true });
|
|
1135
|
+
const portable = normalizedReport(report);
|
|
1136
|
+
await (0, import_promises3.writeFile)(import_node_path5.default.join(outputDir, "ai-analysis-report.json"), `${JSON.stringify(portable, null, 2)}
|
|
1137
|
+
`, "utf8");
|
|
1138
|
+
await (0, import_promises3.writeFile)(import_node_path5.default.join(outputDir, "ai-analysis-report.md"), markdownReport(portable), "utf8");
|
|
1139
|
+
await (0, import_promises3.writeFile)(import_node_path5.default.join(outputDir, "ai-analysis-report.html"), htmlReport(portable), "utf8");
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
// src/reporter.ts
|
|
1143
|
+
function isFailure(status) {
|
|
1144
|
+
return ["failed", "timedOut", "interrupted"].includes(status);
|
|
1145
|
+
}
|
|
1146
|
+
function extractError(result) {
|
|
1147
|
+
return result.error?.message || result.error?.stack || result.errors?.[0]?.message || "Unknown error";
|
|
1148
|
+
}
|
|
1149
|
+
function extractSteps(result) {
|
|
1150
|
+
const steps = result.steps || [];
|
|
1151
|
+
return steps.filter((step) => step.category === "test.step" || step.category === "expect").map((step) => String(step.title)).filter(Boolean);
|
|
1152
|
+
}
|
|
1153
|
+
function extractArtifacts(result) {
|
|
1154
|
+
return (result.attachments || []).filter((attachment) => attachment.path).map((attachment) => ({
|
|
1155
|
+
type: attachment.contentType?.startsWith("image/") ? "screenshot" : attachment.name,
|
|
1156
|
+
path: String(attachment.path),
|
|
1157
|
+
contentType: attachment.contentType
|
|
1158
|
+
}));
|
|
1159
|
+
}
|
|
1160
|
+
function ciMetadata() {
|
|
1161
|
+
if (process.env.GITHUB_RUN_ID && process.env.GITHUB_REPOSITORY) {
|
|
1162
|
+
const server = process.env.GITHUB_SERVER_URL || "https://github.com";
|
|
1163
|
+
return {
|
|
1164
|
+
provider: "github",
|
|
1165
|
+
runId: process.env.GITHUB_RUN_ID,
|
|
1166
|
+
repository: process.env.GITHUB_REPOSITORY,
|
|
1167
|
+
commit: process.env.GITHUB_SHA,
|
|
1168
|
+
branch: process.env.GITHUB_REF_NAME,
|
|
1169
|
+
url: `${server}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}`
|
|
1170
|
+
};
|
|
1171
|
+
}
|
|
1172
|
+
if (process.env.CI) {
|
|
1173
|
+
return {
|
|
1174
|
+
provider: "generic",
|
|
1175
|
+
commit: process.env.GIT_COMMIT || process.env.CI_COMMIT_SHA,
|
|
1176
|
+
branch: process.env.GIT_BRANCH || process.env.CI_COMMIT_REF_NAME,
|
|
1177
|
+
url: process.env.BUILD_URL || process.env.CI_JOB_URL
|
|
1178
|
+
};
|
|
1179
|
+
}
|
|
1180
|
+
return void 0;
|
|
1181
|
+
}
|
|
1182
|
+
function projectMetadata(config) {
|
|
1183
|
+
return {
|
|
1184
|
+
rootDir: config.rootDir,
|
|
1185
|
+
projects: (config.projects || []).map((project) => ({
|
|
1186
|
+
name: project.name,
|
|
1187
|
+
browserName: project.use?.browserName,
|
|
1188
|
+
viewport: project.use?.viewport,
|
|
1189
|
+
locale: project.use?.locale
|
|
1190
|
+
}))
|
|
1191
|
+
};
|
|
1192
|
+
}
|
|
1193
|
+
var PlaywrightAiReporter = class {
|
|
1194
|
+
constructor(options = {}) {
|
|
1195
|
+
this.run = { id: `local-${Date.now()}`, startedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
1196
|
+
this.failures = [];
|
|
1197
|
+
this.outputDir = options.outputDir || "playwright-ai-report";
|
|
1198
|
+
this.store = options.store || new SQLiteStore({ path: options.databasePath });
|
|
1199
|
+
this.outputLanguage = options.outputLanguage;
|
|
1200
|
+
}
|
|
1201
|
+
async onBegin(_config, suite) {
|
|
1202
|
+
this.failures = [];
|
|
1203
|
+
this.run = {
|
|
1204
|
+
id: process.env.PW_AI_RUN_ID || `local-${Date.now()}`,
|
|
1205
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1206
|
+
total: suite.allTests().length,
|
|
1207
|
+
metadata: {
|
|
1208
|
+
...projectMetadata(_config),
|
|
1209
|
+
ci: ciMetadata()
|
|
1210
|
+
}
|
|
1211
|
+
};
|
|
1212
|
+
await this.store.migrate();
|
|
1213
|
+
await this.store.saveRun(this.run);
|
|
1214
|
+
}
|
|
1215
|
+
async onTestEnd(test, result) {
|
|
1216
|
+
if (!isFailure(result.status)) {
|
|
1217
|
+
return;
|
|
1218
|
+
}
|
|
1219
|
+
const failure = {
|
|
1220
|
+
title: String(test.title),
|
|
1221
|
+
file: test.location.file,
|
|
1222
|
+
line: test.location.line,
|
|
1223
|
+
project: test.parent?.project()?.name,
|
|
1224
|
+
workerIndex: result.workerIndex,
|
|
1225
|
+
status: result.status,
|
|
1226
|
+
retry: result.retry,
|
|
1227
|
+
durationMs: result.duration,
|
|
1228
|
+
error: extractError(result),
|
|
1229
|
+
steps: extractSteps(result),
|
|
1230
|
+
artifacts: extractArtifacts(result),
|
|
1231
|
+
annotations: test.annotations.map((annotation) => ({
|
|
1232
|
+
type: annotation.type,
|
|
1233
|
+
description: annotation.description
|
|
1234
|
+
})),
|
|
1235
|
+
run: { id: this.run.id, ci: ciMetadata() },
|
|
1236
|
+
metadata: {
|
|
1237
|
+
testId: test.id,
|
|
1238
|
+
parallelIndex: result.parallelIndex
|
|
1239
|
+
}
|
|
1240
|
+
};
|
|
1241
|
+
const enriched = await enrichFailureWithArtifacts(failure);
|
|
1242
|
+
this.failures.push(enriched);
|
|
1243
|
+
await this.store.saveFailure(enriched);
|
|
1244
|
+
}
|
|
1245
|
+
async onEnd(_result) {
|
|
1246
|
+
this.run.finishedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1247
|
+
this.run.failed = this.failures.length;
|
|
1248
|
+
const analyses = await analyzeFailures(this.failures, {
|
|
1249
|
+
store: this.store,
|
|
1250
|
+
outputLanguage: this.outputLanguage
|
|
1251
|
+
});
|
|
1252
|
+
await writePortableReport(this.outputDir, {
|
|
1253
|
+
run: this.run,
|
|
1254
|
+
failures: this.failures,
|
|
1255
|
+
analyses
|
|
1256
|
+
});
|
|
1257
|
+
await this.store.saveRun(this.run);
|
|
1258
|
+
await this.store.close();
|
|
1259
|
+
}
|
|
1260
|
+
};
|
|
1261
|
+
var reporter_default = PlaywrightAiReporter;
|
|
1262
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1263
|
+
0 && (module.exports = {
|
|
1264
|
+
PlaywrightAiReporter
|
|
1265
|
+
});
|