assuremind 1.0.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/CONTRIBUTING.md +254 -0
- package/LICENSE +21 -0
- package/README.md +367 -0
- package/dist/cli/index.js +14933 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/index.d.mts +2950 -0
- package/dist/index.d.ts +2950 -0
- package/dist/index.js +1628 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1525 -0
- package/dist/index.mjs.map +1 -0
- package/docs/CLI-REFERENCE.md +312 -0
- package/docs/GETTING-STARTED.md +378 -0
- package/docs/STUDIO.md +390 -0
- package/package.json +118 -0
- package/templates/AUTOMIND.md +275 -0
- package/templates/autotest.config.ts +25 -0
- package/templates/docs/CLI-REFERENCE.md +413 -0
- package/templates/docs/GETTING-STARTED.md +417 -0
- package/templates/docs/STUDIO.md +625 -0
- package/templates/env.example +112 -0
- package/templates/env.minimal +103 -0
- package/templates/gitignore +17 -0
- package/templates/global-variables.json +5 -0
- package/ui/dist/assets/index-CdtAorWT.js +819 -0
- package/ui/dist/assets/index-KjpMCzao.css +1 -0
- package/ui/dist/favicon.svg +36 -0
- package/ui/dist/index.html +15 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1628 @@
|
|
|
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/index.ts
|
|
31
|
+
var src_exports = {};
|
|
32
|
+
__export(src_exports, {
|
|
33
|
+
AssuremindError: () => AssuremindError,
|
|
34
|
+
ConfigError: () => ConfigError,
|
|
35
|
+
ExecutionError: () => ExecutionError,
|
|
36
|
+
HealingError: () => HealingError,
|
|
37
|
+
ProviderError: () => ProviderError,
|
|
38
|
+
StorageError: () => StorageError,
|
|
39
|
+
ValidationError: () => ValidationError,
|
|
40
|
+
acceptHealingEvent: () => acceptHealingEvent,
|
|
41
|
+
appendPendingEvent: () => appendPendingEvent,
|
|
42
|
+
clearEnvCache: () => clearEnvCache,
|
|
43
|
+
configExists: () => configExists,
|
|
44
|
+
createCase: () => createCase,
|
|
45
|
+
createChildLogger: () => createChildLogger,
|
|
46
|
+
createSuite: () => createSuite,
|
|
47
|
+
defineConfig: () => defineConfig,
|
|
48
|
+
deleteCase: () => deleteCase,
|
|
49
|
+
deleteGlobalVariable: () => deleteGlobalVariable,
|
|
50
|
+
deleteResult: () => deleteResult,
|
|
51
|
+
deleteSuite: () => deleteSuite,
|
|
52
|
+
formatError: () => formatError,
|
|
53
|
+
generateCacheKey: () => generateCacheKey,
|
|
54
|
+
getCasePath: () => getCasePath,
|
|
55
|
+
getHealingStats: () => getHealingStats,
|
|
56
|
+
isAssuremindError: () => isAssuremindError,
|
|
57
|
+
listCasePaths: () => listCasePaths,
|
|
58
|
+
listCases: () => listCases,
|
|
59
|
+
listHealingReportIds: () => listHealingReportIds,
|
|
60
|
+
listResultIds: () => listResultIds,
|
|
61
|
+
listResults: () => listResults,
|
|
62
|
+
listSuiteDirs: () => listSuiteDirs,
|
|
63
|
+
listSuites: () => listSuites,
|
|
64
|
+
listSuitesWithCounts: () => listSuitesWithCounts,
|
|
65
|
+
listVariableFiles: () => listVariableFiles,
|
|
66
|
+
logger: () => logger,
|
|
67
|
+
normalizeUrl: () => normalizeUrl,
|
|
68
|
+
pruneResolvedEvents: () => pruneResolvedEvents,
|
|
69
|
+
readCase: () => readCase,
|
|
70
|
+
readConfig: () => readConfig,
|
|
71
|
+
readEnvVariables: () => readEnvVariables,
|
|
72
|
+
readGlobalVariables: () => readGlobalVariables,
|
|
73
|
+
readHealingReport: () => readHealingReport,
|
|
74
|
+
readPendingEvents: () => readPendingEvents,
|
|
75
|
+
readResult: () => readResult,
|
|
76
|
+
readSuite: () => readSuite,
|
|
77
|
+
readVariables: () => readVariables,
|
|
78
|
+
redactSecrets: () => redactSecrets,
|
|
79
|
+
rejectHealingEvent: () => rejectHealingEvent,
|
|
80
|
+
resolveVariables: () => resolveVariables,
|
|
81
|
+
sanitizeGeneratedCode: () => sanitizeGeneratedCode,
|
|
82
|
+
screenshotsDir: () => screenshotsDir,
|
|
83
|
+
setGlobalVariable: () => setGlobalVariable,
|
|
84
|
+
sha256: () => sha256,
|
|
85
|
+
toSlug: () => toSlug,
|
|
86
|
+
tracesDir: () => tracesDir,
|
|
87
|
+
tryGetEnv: () => tryGetEnv,
|
|
88
|
+
updateCase: () => updateCase,
|
|
89
|
+
updateConfig: () => updateConfig,
|
|
90
|
+
updateSuite: () => updateSuite,
|
|
91
|
+
validateConfig: () => validateConfig,
|
|
92
|
+
validateEnv: () => validateEnv,
|
|
93
|
+
videosDir: () => videosDir,
|
|
94
|
+
writeCase: () => writeCase,
|
|
95
|
+
writeConfig: () => writeConfig,
|
|
96
|
+
writeHealingReport: () => writeHealingReport,
|
|
97
|
+
writeResult: () => writeResult,
|
|
98
|
+
writeSuite: () => writeSuite,
|
|
99
|
+
writeVariables: () => writeVariables
|
|
100
|
+
});
|
|
101
|
+
module.exports = __toCommonJS(src_exports);
|
|
102
|
+
|
|
103
|
+
// src/types/config.ts
|
|
104
|
+
var import_zod = require("zod");
|
|
105
|
+
var ScreenshotModeSchema = import_zod.z.enum(["off", "on", "only-on-failure"]);
|
|
106
|
+
var VideoModeSchema = import_zod.z.enum(["off", "on", "on-first-retry", "retain-on-failure"]);
|
|
107
|
+
var TraceModeSchema = import_zod.z.enum(["off", "on", "on-first-retry", "retain-on-failure"]);
|
|
108
|
+
var BrowserNameSchema = import_zod.z.enum(["chromium", "firefox", "webkit"]);
|
|
109
|
+
var PageLoadStrategySchema = import_zod.z.enum(["commit", "domcontentloaded", "load", "networkidle"]);
|
|
110
|
+
var EnvironmentSchema = import_zod.z.enum(["dev", "stage", "test", "prod"]);
|
|
111
|
+
var EnvironmentUrlsSchema = import_zod.z.object({
|
|
112
|
+
dev: import_zod.z.string().url().or(import_zod.z.literal("")).default(""),
|
|
113
|
+
stage: import_zod.z.string().url().or(import_zod.z.literal("")).default(""),
|
|
114
|
+
test: import_zod.z.string().url().or(import_zod.z.literal("")).default(""),
|
|
115
|
+
prod: import_zod.z.string().url().or(import_zod.z.literal("")).default("")
|
|
116
|
+
});
|
|
117
|
+
var HealingConfigSchema = import_zod.z.object({
|
|
118
|
+
enabled: import_zod.z.boolean(),
|
|
119
|
+
maxLevel: import_zod.z.number().int().min(1).max(6),
|
|
120
|
+
dailyBudget: import_zod.z.number().positive(),
|
|
121
|
+
autoPR: import_zod.z.boolean()
|
|
122
|
+
});
|
|
123
|
+
var ReportingConfigSchema = import_zod.z.object({
|
|
124
|
+
allure: import_zod.z.boolean(),
|
|
125
|
+
html: import_zod.z.boolean(),
|
|
126
|
+
json: import_zod.z.boolean()
|
|
127
|
+
});
|
|
128
|
+
var ViewportSchema = import_zod.z.object({
|
|
129
|
+
width: import_zod.z.number().int().positive(),
|
|
130
|
+
height: import_zod.z.number().int().positive()
|
|
131
|
+
});
|
|
132
|
+
var EnvironmentProfileSchema = import_zod.z.object({
|
|
133
|
+
name: import_zod.z.string().min(1),
|
|
134
|
+
environment: EnvironmentSchema,
|
|
135
|
+
baseUrl: import_zod.z.string().url(),
|
|
136
|
+
browsers: import_zod.z.array(BrowserNameSchema).min(1),
|
|
137
|
+
headless: import_zod.z.boolean().optional()
|
|
138
|
+
});
|
|
139
|
+
var AutotestConfigSchema = import_zod.z.object({
|
|
140
|
+
baseUrl: import_zod.z.string().url(),
|
|
141
|
+
environment: EnvironmentSchema.default("stage"),
|
|
142
|
+
environmentUrls: EnvironmentUrlsSchema.default({
|
|
143
|
+
dev: "",
|
|
144
|
+
stage: "",
|
|
145
|
+
test: "",
|
|
146
|
+
prod: ""
|
|
147
|
+
}),
|
|
148
|
+
browsers: import_zod.z.array(BrowserNameSchema).min(1),
|
|
149
|
+
headless: import_zod.z.boolean(),
|
|
150
|
+
viewport: ViewportSchema.default({ width: 1280, height: 720 }),
|
|
151
|
+
timeout: import_zod.z.number().int().positive(),
|
|
152
|
+
retries: import_zod.z.number().int().min(0),
|
|
153
|
+
parallel: import_zod.z.number().int().positive(),
|
|
154
|
+
pageLoad: PageLoadStrategySchema.default("domcontentloaded"),
|
|
155
|
+
screenshot: ScreenshotModeSchema,
|
|
156
|
+
video: VideoModeSchema,
|
|
157
|
+
trace: TraceModeSchema,
|
|
158
|
+
healing: HealingConfigSchema,
|
|
159
|
+
reporting: ReportingConfigSchema,
|
|
160
|
+
studioPort: import_zod.z.number().int().min(1024).max(65535),
|
|
161
|
+
profiles: import_zod.z.array(EnvironmentProfileSchema).default([]),
|
|
162
|
+
activeProfile: import_zod.z.string().optional(),
|
|
163
|
+
/** Playwright device descriptor name for emulation (e.g. 'iPhone 15 Pro'). */
|
|
164
|
+
device: import_zod.z.string().optional()
|
|
165
|
+
});
|
|
166
|
+
var DEFAULT_CONFIG = {
|
|
167
|
+
baseUrl: "http://localhost:3000",
|
|
168
|
+
environment: "stage",
|
|
169
|
+
environmentUrls: {
|
|
170
|
+
dev: "",
|
|
171
|
+
stage: "http://localhost:3000",
|
|
172
|
+
test: "",
|
|
173
|
+
prod: ""
|
|
174
|
+
},
|
|
175
|
+
browsers: ["chromium"],
|
|
176
|
+
headless: true,
|
|
177
|
+
viewport: { width: 1280, height: 720 },
|
|
178
|
+
timeout: 3e4,
|
|
179
|
+
retries: 1,
|
|
180
|
+
parallel: 1,
|
|
181
|
+
pageLoad: "domcontentloaded",
|
|
182
|
+
screenshot: "only-on-failure",
|
|
183
|
+
video: "off",
|
|
184
|
+
trace: "on-first-retry",
|
|
185
|
+
healing: {
|
|
186
|
+
enabled: true,
|
|
187
|
+
maxLevel: 5,
|
|
188
|
+
dailyBudget: 5,
|
|
189
|
+
autoPR: false
|
|
190
|
+
},
|
|
191
|
+
reporting: {
|
|
192
|
+
allure: true,
|
|
193
|
+
html: true,
|
|
194
|
+
json: true
|
|
195
|
+
},
|
|
196
|
+
studioPort: 4400,
|
|
197
|
+
profiles: []
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
// src/storage/suite-store.ts
|
|
201
|
+
var import_path2 = __toESM(require("path"));
|
|
202
|
+
var import_fs_extra3 = __toESM(require("fs-extra"));
|
|
203
|
+
var import_uuid = require("uuid");
|
|
204
|
+
|
|
205
|
+
// src/types/suite.ts
|
|
206
|
+
var import_zod2 = require("zod");
|
|
207
|
+
var TestStepSchema = import_zod2.z.object({
|
|
208
|
+
id: import_zod2.z.string().min(1),
|
|
209
|
+
order: import_zod2.z.number().int().positive(),
|
|
210
|
+
instruction: import_zod2.z.string().min(1),
|
|
211
|
+
generatedCode: import_zod2.z.string(),
|
|
212
|
+
strategy: import_zod2.z.enum(["template", "cache", "batch", "fast", "primary"]),
|
|
213
|
+
stepType: import_zod2.z.enum(["ui", "api", "mock"]).default("ui"),
|
|
214
|
+
lastHealed: import_zod2.z.string().nullable(),
|
|
215
|
+
timeout: import_zod2.z.number().int().positive().optional(),
|
|
216
|
+
retries: import_zod2.z.number().int().min(0).optional(),
|
|
217
|
+
mockUrl: import_zod2.z.string().optional(),
|
|
218
|
+
mockResponse: import_zod2.z.string().optional(),
|
|
219
|
+
mockStatus: import_zod2.z.number().int().optional(),
|
|
220
|
+
runAudit: import_zod2.z.boolean().optional()
|
|
221
|
+
// Mark this step as a Lighthouse audit checkpoint
|
|
222
|
+
});
|
|
223
|
+
var DataSourceSchema = import_zod2.z.object({
|
|
224
|
+
type: import_zod2.z.enum(["inline", "json-file", "csv-file"]),
|
|
225
|
+
path: import_zod2.z.string().optional(),
|
|
226
|
+
data: import_zod2.z.array(import_zod2.z.record(import_zod2.z.string())).optional()
|
|
227
|
+
}).optional();
|
|
228
|
+
var CaseHookStepSchema = import_zod2.z.object({
|
|
229
|
+
id: import_zod2.z.string(),
|
|
230
|
+
instruction: import_zod2.z.string(),
|
|
231
|
+
generatedCode: import_zod2.z.string().default(""),
|
|
232
|
+
order: import_zod2.z.number().int().default(0)
|
|
233
|
+
});
|
|
234
|
+
var CaseHooksSchema = import_zod2.z.object({
|
|
235
|
+
before: import_zod2.z.array(CaseHookStepSchema).default([]),
|
|
236
|
+
after: import_zod2.z.array(CaseHookStepSchema).default([])
|
|
237
|
+
}).default({ before: [], after: [] });
|
|
238
|
+
var TestCaseSchema = import_zod2.z.object({
|
|
239
|
+
id: import_zod2.z.string().min(1),
|
|
240
|
+
name: import_zod2.z.string().min(1),
|
|
241
|
+
description: import_zod2.z.string(),
|
|
242
|
+
tags: import_zod2.z.array(import_zod2.z.string()),
|
|
243
|
+
priority: import_zod2.z.enum(["critical", "high", "medium", "low"]),
|
|
244
|
+
timeout: import_zod2.z.number().int().positive().optional(),
|
|
245
|
+
dataSource: DataSourceSchema,
|
|
246
|
+
steps: import_zod2.z.array(TestStepSchema),
|
|
247
|
+
caseHooks: CaseHooksSchema,
|
|
248
|
+
lighthouseCategories: import_zod2.z.array(import_zod2.z.enum(["performance", "accessibility", "seo"])).default(["performance", "accessibility", "seo"]),
|
|
249
|
+
createdAt: import_zod2.z.string().datetime(),
|
|
250
|
+
updatedAt: import_zod2.z.string().datetime()
|
|
251
|
+
});
|
|
252
|
+
var TestSuiteSchema = import_zod2.z.object({
|
|
253
|
+
id: import_zod2.z.string().min(1),
|
|
254
|
+
name: import_zod2.z.string().min(1),
|
|
255
|
+
description: import_zod2.z.string(),
|
|
256
|
+
tags: import_zod2.z.array(import_zod2.z.string()),
|
|
257
|
+
type: import_zod2.z.enum(["ui", "api", "audit", "performance"]).default("ui"),
|
|
258
|
+
timeout: import_zod2.z.number().int().positive().optional(),
|
|
259
|
+
createdAt: import_zod2.z.string().datetime(),
|
|
260
|
+
updatedAt: import_zod2.z.string().datetime()
|
|
261
|
+
});
|
|
262
|
+
var HookTypeEnum = import_zod2.z.enum(["before_all", "before_each", "after_each", "after_all"]);
|
|
263
|
+
var SuiteHooksSchema = import_zod2.z.object({
|
|
264
|
+
before_all: import_zod2.z.array(TestStepSchema).default([]),
|
|
265
|
+
before_each: import_zod2.z.array(TestStepSchema).default([]),
|
|
266
|
+
after_each: import_zod2.z.array(TestStepSchema).default([]),
|
|
267
|
+
after_all: import_zod2.z.array(TestStepSchema).default([])
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
// src/utils/errors.ts
|
|
271
|
+
var AssuremindError = class extends Error {
|
|
272
|
+
code;
|
|
273
|
+
constructor(message, code) {
|
|
274
|
+
super(message);
|
|
275
|
+
this.name = "AssuremindError";
|
|
276
|
+
this.code = code;
|
|
277
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
var ProviderError = class extends AssuremindError {
|
|
281
|
+
provider;
|
|
282
|
+
constructor(message, provider, code = "PROVIDER_ERROR") {
|
|
283
|
+
super(message, code);
|
|
284
|
+
this.name = "ProviderError";
|
|
285
|
+
this.provider = provider;
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
var ExecutionError = class extends AssuremindError {
|
|
289
|
+
stepId;
|
|
290
|
+
constructor(message, stepId, code = "EXECUTION_ERROR") {
|
|
291
|
+
super(message, code);
|
|
292
|
+
this.name = "ExecutionError";
|
|
293
|
+
this.stepId = stepId;
|
|
294
|
+
}
|
|
295
|
+
};
|
|
296
|
+
var ConfigError = class extends AssuremindError {
|
|
297
|
+
constructor(message, code = "CONFIG_ERROR") {
|
|
298
|
+
super(message, code);
|
|
299
|
+
this.name = "ConfigError";
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
var ValidationError = class extends AssuremindError {
|
|
303
|
+
field;
|
|
304
|
+
constructor(message, field, code = "VALIDATION_ERROR") {
|
|
305
|
+
super(message, code);
|
|
306
|
+
this.name = "ValidationError";
|
|
307
|
+
this.field = field;
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
var HealingError = class extends AssuremindError {
|
|
311
|
+
level;
|
|
312
|
+
constructor(message, level, code = "HEALING_ERROR") {
|
|
313
|
+
super(message, code);
|
|
314
|
+
this.name = "HealingError";
|
|
315
|
+
this.level = level;
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
var StorageError = class extends AssuremindError {
|
|
319
|
+
path;
|
|
320
|
+
constructor(message, path8, code = "STORAGE_ERROR") {
|
|
321
|
+
super(message, code);
|
|
322
|
+
this.name = "StorageError";
|
|
323
|
+
this.path = path8;
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
function isAssuremindError(error) {
|
|
327
|
+
return error instanceof AssuremindError;
|
|
328
|
+
}
|
|
329
|
+
function formatError(error) {
|
|
330
|
+
if (error instanceof AssuremindError) {
|
|
331
|
+
return `[${error.code}] ${error.message}`;
|
|
332
|
+
}
|
|
333
|
+
if (error instanceof Error) {
|
|
334
|
+
return error.message;
|
|
335
|
+
}
|
|
336
|
+
return String(error);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// src/utils/logger.ts
|
|
340
|
+
var import_pino = __toESM(require("pino"));
|
|
341
|
+
var import_fs_extra = __toESM(require("fs-extra"));
|
|
342
|
+
var isDevelopment = process.env["NODE_ENV"] !== "production";
|
|
343
|
+
var transport = isDevelopment ? {
|
|
344
|
+
target: "pino-pretty",
|
|
345
|
+
options: {
|
|
346
|
+
colorize: true,
|
|
347
|
+
translateTime: "HH:MM:ss",
|
|
348
|
+
ignore: "pid,hostname",
|
|
349
|
+
messageFormat: "[assuremind] {msg}"
|
|
350
|
+
}
|
|
351
|
+
} : void 0;
|
|
352
|
+
var logger = (0, import_pino.default)(
|
|
353
|
+
{
|
|
354
|
+
level: process.env["LOG_LEVEL"] ?? "info",
|
|
355
|
+
base: { name: "assuremind" }
|
|
356
|
+
},
|
|
357
|
+
transport ? import_pino.default.transport(transport) : void 0
|
|
358
|
+
);
|
|
359
|
+
function createChildLogger(component) {
|
|
360
|
+
return logger.child({ component });
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// src/utils/sanitize.ts
|
|
364
|
+
function stripCodeFences(raw) {
|
|
365
|
+
const fencePattern = /^```(?:typescript|javascript|ts|js)?\n?([\s\S]*?)```\s*$/m;
|
|
366
|
+
const match = raw.match(fencePattern);
|
|
367
|
+
if (match?.[1] !== void 0) {
|
|
368
|
+
return match[1].trim();
|
|
369
|
+
}
|
|
370
|
+
return raw.trim();
|
|
371
|
+
}
|
|
372
|
+
var FORBIDDEN_PATTERNS = [
|
|
373
|
+
/\brequire\s*\(/,
|
|
374
|
+
/\bimport\s*\(/,
|
|
375
|
+
/\bprocess\b/,
|
|
376
|
+
/\bchild_process\b/,
|
|
377
|
+
/\bexec\s*\(/,
|
|
378
|
+
/\bspawn\s*\(/,
|
|
379
|
+
/\beval\s*\(/,
|
|
380
|
+
/\bFunction\s*\(/,
|
|
381
|
+
/\b__dirname\b/,
|
|
382
|
+
/\b__filename\b/,
|
|
383
|
+
/\bglobal\b/,
|
|
384
|
+
/\bwindow\.location\.href\s*=/,
|
|
385
|
+
/\bdocument\.cookie\b/,
|
|
386
|
+
/\blocalStorage\b/,
|
|
387
|
+
/\bsessionStorage\b/,
|
|
388
|
+
/\bIndexedDB\b/,
|
|
389
|
+
/\bXMLHttpRequest\b/,
|
|
390
|
+
/\bfetch\s*\(/,
|
|
391
|
+
/\bWebSocket\s*\(/
|
|
392
|
+
];
|
|
393
|
+
function validateGeneratedCode(code) {
|
|
394
|
+
for (const pattern of FORBIDDEN_PATTERNS) {
|
|
395
|
+
if (pattern.test(code)) {
|
|
396
|
+
throw new ValidationError(
|
|
397
|
+
`Generated code contains forbidden pattern: ${pattern.source}. This may be a prompt injection attempt. Please regenerate the step.`,
|
|
398
|
+
"generatedCode",
|
|
399
|
+
"UNSAFE_CODE"
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
function fixAntiPatterns(code) {
|
|
405
|
+
let result = code;
|
|
406
|
+
result = result.replace(
|
|
407
|
+
/\.or\((?:[^()]*|\([^()]*\))*\)(?:\.(?:first|last)\(\))?/g,
|
|
408
|
+
""
|
|
409
|
+
);
|
|
410
|
+
result = result.replace(/\.(?:first|last)\(\)(?=\.\w)/g, "");
|
|
411
|
+
return result;
|
|
412
|
+
}
|
|
413
|
+
function sanitizeGeneratedCode(raw) {
|
|
414
|
+
const stripped = stripCodeFences(raw);
|
|
415
|
+
if (!stripped) {
|
|
416
|
+
throw new ValidationError(
|
|
417
|
+
"AI returned an empty code response. Please try regenerating.",
|
|
418
|
+
"generatedCode",
|
|
419
|
+
"EMPTY_CODE"
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
const fixed = fixAntiPatterns(stripped);
|
|
423
|
+
validateGeneratedCode(fixed);
|
|
424
|
+
return fixed;
|
|
425
|
+
}
|
|
426
|
+
function toSlug(input) {
|
|
427
|
+
return input.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
428
|
+
}
|
|
429
|
+
function redactSecrets(text, secrets) {
|
|
430
|
+
let result = text;
|
|
431
|
+
for (const secret of secrets) {
|
|
432
|
+
if (secret.length > 0) {
|
|
433
|
+
result = result.replaceAll(secret, "[REDACTED]");
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
return result;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// src/storage/utils.ts
|
|
440
|
+
var import_path = __toESM(require("path"));
|
|
441
|
+
var import_fs_extra2 = __toESM(require("fs-extra"));
|
|
442
|
+
async function atomicWriteJson(filePath, data) {
|
|
443
|
+
const dir = import_path.default.dirname(filePath);
|
|
444
|
+
await import_fs_extra2.default.ensureDir(dir);
|
|
445
|
+
const tmpPath = `${filePath}.${process.pid}.tmp`;
|
|
446
|
+
try {
|
|
447
|
+
await import_fs_extra2.default.writeJson(tmpPath, data, { spaces: 2 });
|
|
448
|
+
await import_fs_extra2.default.rename(tmpPath, filePath);
|
|
449
|
+
} catch (err) {
|
|
450
|
+
await import_fs_extra2.default.remove(tmpPath).catch(() => void 0);
|
|
451
|
+
throw new StorageError(
|
|
452
|
+
`Failed to write file "${filePath}": ${err instanceof Error ? err.message : String(err)}`,
|
|
453
|
+
filePath,
|
|
454
|
+
"WRITE_FAILED"
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
async function readJson(filePath) {
|
|
459
|
+
try {
|
|
460
|
+
return await import_fs_extra2.default.readJson(filePath);
|
|
461
|
+
} catch (err) {
|
|
462
|
+
throw new StorageError(
|
|
463
|
+
`Failed to read JSON file "${filePath}": ${err instanceof Error ? err.message : String(err)}`,
|
|
464
|
+
filePath,
|
|
465
|
+
"READ_FAILED"
|
|
466
|
+
);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
async function atomicWriteText(filePath, content) {
|
|
470
|
+
const dir = import_path.default.dirname(filePath);
|
|
471
|
+
await import_fs_extra2.default.ensureDir(dir);
|
|
472
|
+
const tmpPath = `${filePath}.${process.pid}.tmp`;
|
|
473
|
+
try {
|
|
474
|
+
await import_fs_extra2.default.writeFile(tmpPath, content, "utf8");
|
|
475
|
+
await import_fs_extra2.default.rename(tmpPath, filePath);
|
|
476
|
+
} catch (err) {
|
|
477
|
+
await import_fs_extra2.default.remove(tmpPath).catch(() => void 0);
|
|
478
|
+
throw new StorageError(
|
|
479
|
+
`Failed to write file "${filePath}": ${err instanceof Error ? err.message : String(err)}`,
|
|
480
|
+
filePath,
|
|
481
|
+
"WRITE_FAILED"
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// src/storage/suite-store.ts
|
|
487
|
+
var logger2 = createChildLogger("suite-store");
|
|
488
|
+
var SUITE_FILE = "suite.json";
|
|
489
|
+
async function readSuite(suiteDir) {
|
|
490
|
+
const filePath = import_path2.default.join(suiteDir, SUITE_FILE);
|
|
491
|
+
if (!await import_fs_extra3.default.pathExists(filePath)) {
|
|
492
|
+
throw new StorageError(
|
|
493
|
+
`Suite file not found at "${filePath}". Ensure the suite directory exists and contains a suite.json file.`,
|
|
494
|
+
filePath,
|
|
495
|
+
"SUITE_NOT_FOUND"
|
|
496
|
+
);
|
|
497
|
+
}
|
|
498
|
+
const raw = await readJson(filePath);
|
|
499
|
+
const result = TestSuiteSchema.safeParse(raw);
|
|
500
|
+
if (!result.success) {
|
|
501
|
+
const issues = result.error.issues.map((i) => ` \u2022 ${i.path.join(".")}: ${i.message}`).join("\n");
|
|
502
|
+
throw new ValidationError(
|
|
503
|
+
`Invalid suite.json at "${filePath}":
|
|
504
|
+
${issues}`,
|
|
505
|
+
"suite",
|
|
506
|
+
"INVALID_SUITE"
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
const rawObj = raw;
|
|
510
|
+
if (!rawObj.type) {
|
|
511
|
+
const parentDir = import_path2.default.basename(import_path2.default.dirname(suiteDir));
|
|
512
|
+
if (parentDir === "api") result.data.type = "api";
|
|
513
|
+
else if (parentDir === "audit") result.data.type = "audit";
|
|
514
|
+
else if (parentDir === "performance") result.data.type = "audit";
|
|
515
|
+
else result.data.type = "ui";
|
|
516
|
+
}
|
|
517
|
+
return result.data;
|
|
518
|
+
}
|
|
519
|
+
async function writeSuite(suiteDir, suite) {
|
|
520
|
+
await import_fs_extra3.default.ensureDir(suiteDir);
|
|
521
|
+
const filePath = import_path2.default.join(suiteDir, SUITE_FILE);
|
|
522
|
+
const result = TestSuiteSchema.safeParse(suite);
|
|
523
|
+
if (!result.success) {
|
|
524
|
+
const issues = result.error.issues.map((i) => ` \u2022 ${i.path.join(".")}: ${i.message}`).join("\n");
|
|
525
|
+
throw new ValidationError(
|
|
526
|
+
`Cannot write invalid suite to "${filePath}":
|
|
527
|
+
${issues}`,
|
|
528
|
+
"suite",
|
|
529
|
+
"INVALID_SUITE"
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
await atomicWriteJson(filePath, result.data);
|
|
533
|
+
logger2.debug({ suiteId: suite.id, path: filePath }, "Suite written");
|
|
534
|
+
}
|
|
535
|
+
async function createSuite(testsDir, suite) {
|
|
536
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
537
|
+
const suiteType = suite.type ?? "ui";
|
|
538
|
+
const newSuite = {
|
|
539
|
+
...suite,
|
|
540
|
+
type: suiteType,
|
|
541
|
+
id: (0, import_uuid.v4)(),
|
|
542
|
+
createdAt: now,
|
|
543
|
+
updatedAt: now
|
|
544
|
+
};
|
|
545
|
+
const targetDir = import_path2.default.join(testsDir, suiteType);
|
|
546
|
+
const suiteDir = import_path2.default.join(targetDir, toSlug(newSuite.name));
|
|
547
|
+
if (await import_fs_extra3.default.pathExists(import_path2.default.join(suiteDir, SUITE_FILE))) {
|
|
548
|
+
throw new StorageError(
|
|
549
|
+
`Suite directory already exists at "${suiteDir}". Choose a different name or delete the existing suite first.`,
|
|
550
|
+
suiteDir,
|
|
551
|
+
"SUITE_ALREADY_EXISTS"
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
await writeSuite(suiteDir, newSuite);
|
|
555
|
+
logger2.info({ suiteId: newSuite.id, path: suiteDir }, "Suite created");
|
|
556
|
+
return { suiteDir, suiteId: newSuite.id };
|
|
557
|
+
}
|
|
558
|
+
async function updateSuite(suiteDir, updates) {
|
|
559
|
+
const existing = await readSuite(suiteDir);
|
|
560
|
+
const updated = {
|
|
561
|
+
...existing,
|
|
562
|
+
...updates,
|
|
563
|
+
id: existing.id,
|
|
564
|
+
createdAt: existing.createdAt,
|
|
565
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
566
|
+
};
|
|
567
|
+
await writeSuite(suiteDir, updated);
|
|
568
|
+
return updated;
|
|
569
|
+
}
|
|
570
|
+
async function deleteSuite(suiteDir) {
|
|
571
|
+
if (!await import_fs_extra3.default.pathExists(suiteDir)) {
|
|
572
|
+
throw new StorageError(
|
|
573
|
+
`Suite directory not found at "${suiteDir}".`,
|
|
574
|
+
suiteDir,
|
|
575
|
+
"SUITE_NOT_FOUND"
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
await import_fs_extra3.default.remove(suiteDir);
|
|
579
|
+
logger2.info({ path: suiteDir }, "Suite deleted");
|
|
580
|
+
}
|
|
581
|
+
async function listSuiteDirs(testsDir) {
|
|
582
|
+
const suiteDirs = [];
|
|
583
|
+
const searchDirs = [
|
|
584
|
+
import_path2.default.join(testsDir, "ui"),
|
|
585
|
+
import_path2.default.join(testsDir, "api"),
|
|
586
|
+
import_path2.default.join(testsDir, "audit"),
|
|
587
|
+
import_path2.default.join(testsDir, "performance"),
|
|
588
|
+
// legacy: keep scanning for backward compat
|
|
589
|
+
testsDir
|
|
590
|
+
// legacy: suites at root level
|
|
591
|
+
];
|
|
592
|
+
for (const baseDir of searchDirs) {
|
|
593
|
+
if (!await import_fs_extra3.default.pathExists(baseDir)) continue;
|
|
594
|
+
const entries = await import_fs_extra3.default.readdir(baseDir, { withFileTypes: true });
|
|
595
|
+
for (const entry of entries) {
|
|
596
|
+
if (!entry.isDirectory()) continue;
|
|
597
|
+
if (baseDir === testsDir && (entry.name === "ui" || entry.name === "api" || entry.name === "audit" || entry.name === "performance")) continue;
|
|
598
|
+
const suiteFile = import_path2.default.join(baseDir, entry.name, SUITE_FILE);
|
|
599
|
+
if (await import_fs_extra3.default.pathExists(suiteFile)) {
|
|
600
|
+
suiteDirs.push(import_path2.default.join(baseDir, entry.name));
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
return suiteDirs;
|
|
605
|
+
}
|
|
606
|
+
async function listSuites(testsDir) {
|
|
607
|
+
const dirs = await listSuiteDirs(testsDir);
|
|
608
|
+
const suites = [];
|
|
609
|
+
for (const dir of dirs) {
|
|
610
|
+
try {
|
|
611
|
+
suites.push(await readSuite(dir));
|
|
612
|
+
} catch (err) {
|
|
613
|
+
logger2.warn({ path: dir, err }, "Failed to read suite \u2014 skipping");
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
return suites;
|
|
617
|
+
}
|
|
618
|
+
async function listSuitesWithCounts(testsDir) {
|
|
619
|
+
const dirs = await listSuiteDirs(testsDir);
|
|
620
|
+
const suites = [];
|
|
621
|
+
for (const dir of dirs) {
|
|
622
|
+
try {
|
|
623
|
+
const suite = await readSuite(dir);
|
|
624
|
+
const entries = await import_fs_extra3.default.readdir(dir, { withFileTypes: true });
|
|
625
|
+
const caseCount = entries.filter((e) => e.isFile() && e.name.endsWith(".test.json")).length;
|
|
626
|
+
suites.push({ ...suite, caseCount });
|
|
627
|
+
} catch (err) {
|
|
628
|
+
logger2.warn({ path: dir, err }, "Failed to read suite \u2014 skipping");
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
return suites;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// src/storage/case-store.ts
|
|
635
|
+
var import_path3 = __toESM(require("path"));
|
|
636
|
+
var import_fs_extra4 = __toESM(require("fs-extra"));
|
|
637
|
+
var import_uuid2 = require("uuid");
|
|
638
|
+
var logger3 = createChildLogger("case-store");
|
|
639
|
+
var CASE_EXTENSION = ".test.json";
|
|
640
|
+
async function readCase(casePath) {
|
|
641
|
+
if (!await import_fs_extra4.default.pathExists(casePath)) {
|
|
642
|
+
throw new StorageError(
|
|
643
|
+
`Test case file not found at "${casePath}".`,
|
|
644
|
+
casePath,
|
|
645
|
+
"CASE_NOT_FOUND"
|
|
646
|
+
);
|
|
647
|
+
}
|
|
648
|
+
const raw = await readJson(casePath);
|
|
649
|
+
const result = TestCaseSchema.safeParse(raw);
|
|
650
|
+
if (!result.success) {
|
|
651
|
+
const issues = result.error.issues.map((i) => ` \u2022 ${i.path.join(".")}: ${i.message}`).join("\n");
|
|
652
|
+
throw new ValidationError(
|
|
653
|
+
`Invalid test case file at "${casePath}":
|
|
654
|
+
${issues}`,
|
|
655
|
+
"case",
|
|
656
|
+
"INVALID_CASE"
|
|
657
|
+
);
|
|
658
|
+
}
|
|
659
|
+
return result.data;
|
|
660
|
+
}
|
|
661
|
+
async function writeCase(casePath, testCase) {
|
|
662
|
+
const result = TestCaseSchema.safeParse(testCase);
|
|
663
|
+
if (!result.success) {
|
|
664
|
+
const issues = result.error.issues.map((i) => ` \u2022 ${i.path.join(".")}: ${i.message}`).join("\n");
|
|
665
|
+
throw new ValidationError(
|
|
666
|
+
`Cannot write invalid test case to "${casePath}":
|
|
667
|
+
${issues}`,
|
|
668
|
+
"case",
|
|
669
|
+
"INVALID_CASE"
|
|
670
|
+
);
|
|
671
|
+
}
|
|
672
|
+
await atomicWriteJson(casePath, result.data);
|
|
673
|
+
logger3.debug({ caseId: testCase.id, path: casePath }, "Test case written");
|
|
674
|
+
}
|
|
675
|
+
async function createCase(suiteDir, testCase) {
|
|
676
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
677
|
+
const newCase = {
|
|
678
|
+
...testCase,
|
|
679
|
+
id: (0, import_uuid2.v4)(),
|
|
680
|
+
createdAt: now,
|
|
681
|
+
updatedAt: now
|
|
682
|
+
};
|
|
683
|
+
const casePath = import_path3.default.join(suiteDir, `${toSlug(newCase.name)}${CASE_EXTENSION}`);
|
|
684
|
+
await writeCase(casePath, newCase);
|
|
685
|
+
logger3.info({ caseId: newCase.id, path: casePath }, "Test case created");
|
|
686
|
+
return casePath;
|
|
687
|
+
}
|
|
688
|
+
async function updateCase(casePath, updates) {
|
|
689
|
+
const existing = await readCase(casePath);
|
|
690
|
+
const updated = {
|
|
691
|
+
...existing,
|
|
692
|
+
...updates,
|
|
693
|
+
id: existing.id,
|
|
694
|
+
createdAt: existing.createdAt,
|
|
695
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
696
|
+
};
|
|
697
|
+
await writeCase(casePath, updated);
|
|
698
|
+
return updated;
|
|
699
|
+
}
|
|
700
|
+
async function deleteCase(casePath) {
|
|
701
|
+
if (!await import_fs_extra4.default.pathExists(casePath)) {
|
|
702
|
+
throw new StorageError(
|
|
703
|
+
`Test case file not found at "${casePath}".`,
|
|
704
|
+
casePath,
|
|
705
|
+
"CASE_NOT_FOUND"
|
|
706
|
+
);
|
|
707
|
+
}
|
|
708
|
+
await import_fs_extra4.default.remove(casePath);
|
|
709
|
+
logger3.info({ path: casePath }, "Test case deleted");
|
|
710
|
+
}
|
|
711
|
+
async function listCasePaths(suiteDir) {
|
|
712
|
+
if (!await import_fs_extra4.default.pathExists(suiteDir)) return [];
|
|
713
|
+
const entries = await import_fs_extra4.default.readdir(suiteDir, { withFileTypes: true });
|
|
714
|
+
return entries.filter((e) => e.isFile() && e.name.endsWith(CASE_EXTENSION)).map((e) => import_path3.default.join(suiteDir, e.name));
|
|
715
|
+
}
|
|
716
|
+
async function listCases(suiteDir) {
|
|
717
|
+
const paths = await listCasePaths(suiteDir);
|
|
718
|
+
const cases = [];
|
|
719
|
+
for (const casePath of paths) {
|
|
720
|
+
try {
|
|
721
|
+
cases.push(await readCase(casePath));
|
|
722
|
+
} catch (err) {
|
|
723
|
+
logger3.warn({ path: casePath, err }, "Failed to read test case \u2014 skipping");
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
return cases.sort((a, b) => a.name.localeCompare(b.name));
|
|
727
|
+
}
|
|
728
|
+
function getCasePath(suiteDir, caseName) {
|
|
729
|
+
return import_path3.default.join(suiteDir, `${toSlug(caseName)}${CASE_EXTENSION}`);
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// src/storage/variable-store.ts
|
|
733
|
+
var import_path4 = __toESM(require("path"));
|
|
734
|
+
var import_fs_extra5 = __toESM(require("fs-extra"));
|
|
735
|
+
|
|
736
|
+
// src/types/variable.ts
|
|
737
|
+
var import_zod3 = require("zod");
|
|
738
|
+
var SecretVariableSchema = import_zod3.z.object({
|
|
739
|
+
value: import_zod3.z.string(),
|
|
740
|
+
secret: import_zod3.z.literal(true)
|
|
741
|
+
});
|
|
742
|
+
var VariableValueSchema = import_zod3.z.union([import_zod3.z.string(), SecretVariableSchema]);
|
|
743
|
+
var VariableStoreSchema = import_zod3.z.record(import_zod3.z.string(), VariableValueSchema);
|
|
744
|
+
function isSecretVariable(value) {
|
|
745
|
+
return typeof value === "object" && value.secret === true;
|
|
746
|
+
}
|
|
747
|
+
function resolveVariableValue(value) {
|
|
748
|
+
if (isSecretVariable(value)) {
|
|
749
|
+
return value.value;
|
|
750
|
+
}
|
|
751
|
+
return value;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
// src/storage/variable-store.ts
|
|
755
|
+
var logger4 = createChildLogger("variable-store");
|
|
756
|
+
var VARIABLES_DIR = "variables";
|
|
757
|
+
var GLOBAL_FILE = "global.json";
|
|
758
|
+
function envFileName(env) {
|
|
759
|
+
return `${env}.env.json`;
|
|
760
|
+
}
|
|
761
|
+
async function readVariables(filePath) {
|
|
762
|
+
if (!await import_fs_extra5.default.pathExists(filePath)) {
|
|
763
|
+
return {};
|
|
764
|
+
}
|
|
765
|
+
const raw = await readJson(filePath);
|
|
766
|
+
const result = VariableStoreSchema.safeParse(raw);
|
|
767
|
+
if (!result.success) {
|
|
768
|
+
const issues = result.error.issues.map((i) => ` \u2022 ${i.path.join(".")}: ${i.message}`).join("\n");
|
|
769
|
+
throw new ValidationError(
|
|
770
|
+
`Invalid variables file at "${filePath}":
|
|
771
|
+
${issues}`,
|
|
772
|
+
"variables",
|
|
773
|
+
"INVALID_VARIABLES"
|
|
774
|
+
);
|
|
775
|
+
}
|
|
776
|
+
return result.data;
|
|
777
|
+
}
|
|
778
|
+
async function writeVariables(filePath, store) {
|
|
779
|
+
const result = VariableStoreSchema.safeParse(store);
|
|
780
|
+
if (!result.success) {
|
|
781
|
+
const issues = result.error.issues.map((i) => ` \u2022 ${i.path.join(".")}: ${i.message}`).join("\n");
|
|
782
|
+
throw new ValidationError(
|
|
783
|
+
`Cannot write invalid variables to "${filePath}":
|
|
784
|
+
${issues}`,
|
|
785
|
+
"variables",
|
|
786
|
+
"INVALID_VARIABLES"
|
|
787
|
+
);
|
|
788
|
+
}
|
|
789
|
+
await atomicWriteJson(filePath, result.data);
|
|
790
|
+
logger4.debug({ path: filePath }, "Variables written");
|
|
791
|
+
}
|
|
792
|
+
async function readGlobalVariables(rootDir) {
|
|
793
|
+
const filePath = import_path4.default.join(rootDir, VARIABLES_DIR, GLOBAL_FILE);
|
|
794
|
+
return readVariables(filePath);
|
|
795
|
+
}
|
|
796
|
+
async function readEnvVariables(rootDir, env) {
|
|
797
|
+
const filePath = import_path4.default.join(rootDir, VARIABLES_DIR, envFileName(env));
|
|
798
|
+
return readVariables(filePath);
|
|
799
|
+
}
|
|
800
|
+
async function resolveVariables(rootDir, env) {
|
|
801
|
+
const global = await readGlobalVariables(rootDir);
|
|
802
|
+
const envSpecific = env ? await readEnvVariables(rootDir, env) : {};
|
|
803
|
+
const merged = { ...global, ...envSpecific };
|
|
804
|
+
const resolved = {};
|
|
805
|
+
for (const [key, value] of Object.entries(merged)) {
|
|
806
|
+
resolved[key] = resolveVariableValue(value);
|
|
807
|
+
}
|
|
808
|
+
return resolved;
|
|
809
|
+
}
|
|
810
|
+
async function setGlobalVariable(rootDir, key, value) {
|
|
811
|
+
const filePath = import_path4.default.join(rootDir, VARIABLES_DIR, GLOBAL_FILE);
|
|
812
|
+
await import_fs_extra5.default.ensureDir(import_path4.default.dirname(filePath));
|
|
813
|
+
const existing = await readVariables(filePath);
|
|
814
|
+
existing[key] = value;
|
|
815
|
+
await writeVariables(filePath, existing);
|
|
816
|
+
}
|
|
817
|
+
async function deleteGlobalVariable(rootDir, key) {
|
|
818
|
+
const filePath = import_path4.default.join(rootDir, VARIABLES_DIR, GLOBAL_FILE);
|
|
819
|
+
const existing = await readVariables(filePath);
|
|
820
|
+
if (!(key in existing)) {
|
|
821
|
+
throw new StorageError(
|
|
822
|
+
`Variable "${key}" not found in global variables.`,
|
|
823
|
+
filePath,
|
|
824
|
+
"VARIABLE_NOT_FOUND"
|
|
825
|
+
);
|
|
826
|
+
}
|
|
827
|
+
delete existing[key];
|
|
828
|
+
await writeVariables(filePath, existing);
|
|
829
|
+
}
|
|
830
|
+
async function listVariableFiles(rootDir) {
|
|
831
|
+
const dir = import_path4.default.join(rootDir, VARIABLES_DIR);
|
|
832
|
+
if (!await import_fs_extra5.default.pathExists(dir)) return [];
|
|
833
|
+
const entries = await import_fs_extra5.default.readdir(dir, { withFileTypes: true });
|
|
834
|
+
return entries.filter((e) => e.isFile() && e.name.endsWith(".json")).map((e) => import_path4.default.join(dir, e.name));
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
// src/storage/config-store.ts
|
|
838
|
+
var import_path5 = __toESM(require("path"));
|
|
839
|
+
var import_fs_extra6 = __toESM(require("fs-extra"));
|
|
840
|
+
var logger5 = createChildLogger("config-store");
|
|
841
|
+
var CONFIG_JSON = "autotest.config.json";
|
|
842
|
+
var CONFIG_TS = "autotest.config.ts";
|
|
843
|
+
async function readConfig(rootDir) {
|
|
844
|
+
const jsonPath = import_path5.default.join(rootDir, CONFIG_JSON);
|
|
845
|
+
if (!await import_fs_extra6.default.pathExists(jsonPath)) {
|
|
846
|
+
logger5.debug({ rootDir }, "No autotest.config.json found \u2014 using defaults");
|
|
847
|
+
return DEFAULT_CONFIG;
|
|
848
|
+
}
|
|
849
|
+
const raw = await readJson(jsonPath);
|
|
850
|
+
const result = AutotestConfigSchema.safeParse(raw);
|
|
851
|
+
if (!result.success) {
|
|
852
|
+
const issues = result.error.issues.map((i) => ` \u2022 ${i.path.join(".")}: ${i.message}`).join("\n");
|
|
853
|
+
throw new ValidationError(
|
|
854
|
+
`Invalid autotest.config.json at "${jsonPath}":
|
|
855
|
+
${issues}
|
|
856
|
+
|
|
857
|
+
How to fix: Run "npx assuremind init" to reset to defaults, or manually correct the file using autotest.config.ts as reference.`,
|
|
858
|
+
"config",
|
|
859
|
+
"INVALID_CONFIG"
|
|
860
|
+
);
|
|
861
|
+
}
|
|
862
|
+
return result.data;
|
|
863
|
+
}
|
|
864
|
+
async function writeConfig(rootDir, config) {
|
|
865
|
+
const result = AutotestConfigSchema.safeParse(config);
|
|
866
|
+
if (!result.success) {
|
|
867
|
+
const issues = result.error.issues.map((i) => ` \u2022 ${i.path.join(".")}: ${i.message}`).join("\n");
|
|
868
|
+
throw new ValidationError(
|
|
869
|
+
`Cannot write invalid config:
|
|
870
|
+
${issues}`,
|
|
871
|
+
"config",
|
|
872
|
+
"INVALID_CONFIG"
|
|
873
|
+
);
|
|
874
|
+
}
|
|
875
|
+
const jsonPath = import_path5.default.join(rootDir, CONFIG_JSON);
|
|
876
|
+
await atomicWriteJson(jsonPath, result.data);
|
|
877
|
+
const tsPath = import_path5.default.join(rootDir, CONFIG_TS);
|
|
878
|
+
await atomicWriteText(tsPath, generateConfigTs(result.data));
|
|
879
|
+
logger5.info({ rootDir }, "Config saved");
|
|
880
|
+
}
|
|
881
|
+
async function updateConfig(rootDir, updates) {
|
|
882
|
+
const current = await readConfig(rootDir);
|
|
883
|
+
const merged = {
|
|
884
|
+
...current,
|
|
885
|
+
...updates,
|
|
886
|
+
healing: { ...current.healing, ...updates.healing ?? {} },
|
|
887
|
+
reporting: { ...current.reporting, ...updates.reporting ?? {} },
|
|
888
|
+
environmentUrls: { ...current.environmentUrls, ...updates.environmentUrls ?? {} }
|
|
889
|
+
};
|
|
890
|
+
await writeConfig(rootDir, merged);
|
|
891
|
+
return merged;
|
|
892
|
+
}
|
|
893
|
+
async function configExists(rootDir) {
|
|
894
|
+
const jsonPath = import_path5.default.join(rootDir, CONFIG_JSON);
|
|
895
|
+
const tsPath = import_path5.default.join(rootDir, CONFIG_TS);
|
|
896
|
+
return await import_fs_extra6.default.pathExists(jsonPath) || await import_fs_extra6.default.pathExists(tsPath);
|
|
897
|
+
}
|
|
898
|
+
async function validateConfig(rootDir) {
|
|
899
|
+
const config = await readConfig(rootDir);
|
|
900
|
+
if (!config.baseUrl) {
|
|
901
|
+
throw new ConfigError(
|
|
902
|
+
'baseUrl is required in autotest.config.ts. Set it to your application URL, e.g. "http://localhost:3000".',
|
|
903
|
+
"CONFIG_BASE_URL_MISSING"
|
|
904
|
+
);
|
|
905
|
+
}
|
|
906
|
+
logger5.debug({ config }, "Config validated successfully");
|
|
907
|
+
}
|
|
908
|
+
function generateConfigTs(config) {
|
|
909
|
+
return `import { defineConfig } from 'assuremind';
|
|
910
|
+
|
|
911
|
+
export default defineConfig({
|
|
912
|
+
baseUrl: '${config.baseUrl}',
|
|
913
|
+
browsers: ${JSON.stringify(config.browsers)},
|
|
914
|
+
headless: ${config.headless},
|
|
915
|
+
timeout: ${config.timeout},
|
|
916
|
+
retries: ${config.retries},
|
|
917
|
+
parallel: ${config.parallel},
|
|
918
|
+
screenshot: '${config.screenshot}',
|
|
919
|
+
video: '${config.video}',
|
|
920
|
+
trace: '${config.trace}',
|
|
921
|
+
healing: {
|
|
922
|
+
enabled: ${config.healing.enabled},
|
|
923
|
+
maxLevel: ${config.healing.maxLevel},
|
|
924
|
+
dailyBudget: ${config.healing.dailyBudget},
|
|
925
|
+
autoPR: ${config.healing.autoPR},
|
|
926
|
+
},
|
|
927
|
+
reporting: {
|
|
928
|
+
allure: ${config.reporting.allure},
|
|
929
|
+
html: ${config.reporting.html},
|
|
930
|
+
json: ${config.reporting.json},
|
|
931
|
+
},
|
|
932
|
+
studioPort: ${config.studioPort},
|
|
933
|
+
});
|
|
934
|
+
`;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
// src/storage/result-store.ts
|
|
938
|
+
var import_path6 = __toESM(require("path"));
|
|
939
|
+
var import_fs_extra7 = __toESM(require("fs-extra"));
|
|
940
|
+
|
|
941
|
+
// src/types/run.ts
|
|
942
|
+
var import_zod4 = require("zod");
|
|
943
|
+
var RunStatusSchema = import_zod4.z.enum(["pending", "running", "passed", "failed", "skipped"]);
|
|
944
|
+
var ApiRequestSchema = import_zod4.z.object({
|
|
945
|
+
method: import_zod4.z.string(),
|
|
946
|
+
url: import_zod4.z.string(),
|
|
947
|
+
headers: import_zod4.z.record(import_zod4.z.string()).optional(),
|
|
948
|
+
body: import_zod4.z.string().optional()
|
|
949
|
+
}).optional();
|
|
950
|
+
var ApiResponseSchema = import_zod4.z.object({
|
|
951
|
+
status: import_zod4.z.number(),
|
|
952
|
+
statusText: import_zod4.z.string(),
|
|
953
|
+
headers: import_zod4.z.record(import_zod4.z.string()).optional(),
|
|
954
|
+
body: import_zod4.z.string().optional(),
|
|
955
|
+
duration: import_zod4.z.number()
|
|
956
|
+
}).optional();
|
|
957
|
+
var optionalNum = import_zod4.z.number().nullish().transform((v) => v ?? void 0);
|
|
958
|
+
var AuditItemSchema = import_zod4.z.object({
|
|
959
|
+
id: import_zod4.z.string(),
|
|
960
|
+
title: import_zod4.z.string(),
|
|
961
|
+
passed: import_zod4.z.boolean(),
|
|
962
|
+
// true = score === 1
|
|
963
|
+
partial: import_zod4.z.boolean().optional(),
|
|
964
|
+
// true = 0 < score < 1 (needs work)
|
|
965
|
+
na: import_zod4.z.boolean().optional()
|
|
966
|
+
// true = score === null (not applicable)
|
|
967
|
+
});
|
|
968
|
+
var PageLoadMetricSchema = import_zod4.z.object({
|
|
969
|
+
url: import_zod4.z.string(),
|
|
970
|
+
stepIndex: import_zod4.z.number().int(),
|
|
971
|
+
stepInstruction: import_zod4.z.string(),
|
|
972
|
+
score: optionalNum,
|
|
973
|
+
// Performance score 0-100 (pre-multiplied in runner)
|
|
974
|
+
a11yScore: optionalNum,
|
|
975
|
+
// Accessibility score 0-100 (pre-multiplied in runner)
|
|
976
|
+
seoScore: optionalNum,
|
|
977
|
+
// SEO score 0-100 (pre-multiplied in runner)
|
|
978
|
+
fcp: optionalNum,
|
|
979
|
+
// First Contentful Paint (ms)
|
|
980
|
+
lcp: optionalNum,
|
|
981
|
+
// Largest Contentful Paint (ms)
|
|
982
|
+
cls: optionalNum,
|
|
983
|
+
// Cumulative Layout Shift (score)
|
|
984
|
+
ttfb: optionalNum,
|
|
985
|
+
// Time to First Byte (ms)
|
|
986
|
+
tbt: optionalNum,
|
|
987
|
+
// Total Blocking Time (ms)
|
|
988
|
+
si: optionalNum,
|
|
989
|
+
// Speed Index (ms)
|
|
990
|
+
tti: optionalNum,
|
|
991
|
+
// Time to Interactive (ms)
|
|
992
|
+
inp: optionalNum,
|
|
993
|
+
// Interaction to Next Paint (ms)
|
|
994
|
+
// Individual audit items for Accessibility and SEO categories
|
|
995
|
+
a11yAudits: import_zod4.z.array(AuditItemSchema).optional(),
|
|
996
|
+
seoAudits: import_zod4.z.array(AuditItemSchema).optional(),
|
|
997
|
+
lighthouseError: import_zod4.z.string().optional()
|
|
998
|
+
});
|
|
999
|
+
var StepResultSchema = import_zod4.z.object({
|
|
1000
|
+
stepId: import_zod4.z.string(),
|
|
1001
|
+
instruction: import_zod4.z.string(),
|
|
1002
|
+
status: RunStatusSchema,
|
|
1003
|
+
code: import_zod4.z.string(),
|
|
1004
|
+
error: import_zod4.z.string().optional(),
|
|
1005
|
+
duration: import_zod4.z.number(),
|
|
1006
|
+
screenshotPath: import_zod4.z.string().optional(),
|
|
1007
|
+
healed: import_zod4.z.boolean().optional(),
|
|
1008
|
+
healedCode: import_zod4.z.string().optional(),
|
|
1009
|
+
stepType: import_zod4.z.enum(["ui", "api", "mock"]).optional(),
|
|
1010
|
+
apiRequest: ApiRequestSchema,
|
|
1011
|
+
apiResponse: ApiResponseSchema,
|
|
1012
|
+
navigatedToUrl: import_zod4.z.string().optional(),
|
|
1013
|
+
auditUrl: import_zod4.z.string().optional()
|
|
1014
|
+
// URL captured when step has runAudit=true (may not have navigated)
|
|
1015
|
+
});
|
|
1016
|
+
var TestCaseResultSchema = import_zod4.z.object({
|
|
1017
|
+
caseId: import_zod4.z.string(),
|
|
1018
|
+
caseName: import_zod4.z.string(),
|
|
1019
|
+
status: RunStatusSchema,
|
|
1020
|
+
steps: import_zod4.z.array(StepResultSchema),
|
|
1021
|
+
duration: import_zod4.z.number(),
|
|
1022
|
+
browser: import_zod4.z.string(),
|
|
1023
|
+
/** Playwright device descriptor used for this case (undefined = no emulation). */
|
|
1024
|
+
device: import_zod4.z.string().optional(),
|
|
1025
|
+
startedAt: import_zod4.z.string().datetime(),
|
|
1026
|
+
finishedAt: import_zod4.z.string().datetime(),
|
|
1027
|
+
videoPath: import_zod4.z.string().optional(),
|
|
1028
|
+
tracePath: import_zod4.z.string().optional(),
|
|
1029
|
+
dataRowIndex: import_zod4.z.number().int().optional(),
|
|
1030
|
+
dataRow: import_zod4.z.record(import_zod4.z.string()).optional(),
|
|
1031
|
+
pageLoads: import_zod4.z.array(PageLoadMetricSchema).default([])
|
|
1032
|
+
});
|
|
1033
|
+
var SuiteResultSchema = import_zod4.z.object({
|
|
1034
|
+
suiteId: import_zod4.z.string(),
|
|
1035
|
+
suiteName: import_zod4.z.string(),
|
|
1036
|
+
suiteType: import_zod4.z.enum(["ui", "api", "audit", "performance"]).optional(),
|
|
1037
|
+
status: RunStatusSchema,
|
|
1038
|
+
cases: import_zod4.z.array(TestCaseResultSchema),
|
|
1039
|
+
duration: import_zod4.z.number(),
|
|
1040
|
+
browser: import_zod4.z.string(),
|
|
1041
|
+
startedAt: import_zod4.z.string().datetime(),
|
|
1042
|
+
finishedAt: import_zod4.z.string().datetime()
|
|
1043
|
+
});
|
|
1044
|
+
var RunResultSchema = import_zod4.z.object({
|
|
1045
|
+
runId: import_zod4.z.string(),
|
|
1046
|
+
status: RunStatusSchema,
|
|
1047
|
+
environment: import_zod4.z.string().optional(),
|
|
1048
|
+
suites: import_zod4.z.array(SuiteResultSchema),
|
|
1049
|
+
duration: import_zod4.z.number(),
|
|
1050
|
+
startedAt: import_zod4.z.string().datetime(),
|
|
1051
|
+
finishedAt: import_zod4.z.string().datetime(),
|
|
1052
|
+
totalTests: import_zod4.z.number().int(),
|
|
1053
|
+
passed: import_zod4.z.number().int(),
|
|
1054
|
+
failed: import_zod4.z.number().int(),
|
|
1055
|
+
skipped: import_zod4.z.number().int(),
|
|
1056
|
+
logFilePath: import_zod4.z.string().optional()
|
|
1057
|
+
});
|
|
1058
|
+
|
|
1059
|
+
// src/storage/result-store.ts
|
|
1060
|
+
var logger6 = createChildLogger("result-store");
|
|
1061
|
+
var RESULTS_DIR = "results";
|
|
1062
|
+
var RUNS_DIR = "runs";
|
|
1063
|
+
function runFilePath(resultsDir, runId) {
|
|
1064
|
+
return import_path6.default.join(resultsDir, RUNS_DIR, `${runId}.json`);
|
|
1065
|
+
}
|
|
1066
|
+
async function writeResult(rootDir, result) {
|
|
1067
|
+
const schema = RunResultSchema.safeParse(result);
|
|
1068
|
+
if (!schema.success) {
|
|
1069
|
+
const issues = schema.error.issues.map((i) => ` \u2022 ${i.path.join(".")}: ${i.message}`).join("\n");
|
|
1070
|
+
throw new ValidationError(
|
|
1071
|
+
`Cannot write invalid run result:
|
|
1072
|
+
${issues}`,
|
|
1073
|
+
"result",
|
|
1074
|
+
"INVALID_RESULT"
|
|
1075
|
+
);
|
|
1076
|
+
}
|
|
1077
|
+
const filePath = runFilePath(import_path6.default.join(rootDir, RESULTS_DIR), result.runId);
|
|
1078
|
+
await atomicWriteJson(filePath, schema.data);
|
|
1079
|
+
logger6.info({ runId: result.runId, status: result.status }, "Run result saved");
|
|
1080
|
+
}
|
|
1081
|
+
async function readResult(rootDir, runId) {
|
|
1082
|
+
const filePath = runFilePath(import_path6.default.join(rootDir, RESULTS_DIR), runId);
|
|
1083
|
+
if (!await import_fs_extra7.default.pathExists(filePath)) {
|
|
1084
|
+
throw new StorageError(
|
|
1085
|
+
`Run result not found for runId "${runId}".`,
|
|
1086
|
+
filePath,
|
|
1087
|
+
"RESULT_NOT_FOUND"
|
|
1088
|
+
);
|
|
1089
|
+
}
|
|
1090
|
+
const raw = await readJson(filePath);
|
|
1091
|
+
const result = RunResultSchema.safeParse(raw);
|
|
1092
|
+
if (!result.success) {
|
|
1093
|
+
const issues = result.error.issues.map((i) => ` \u2022 ${i.path.join(".")}: ${i.message}`).join("\n");
|
|
1094
|
+
throw new ValidationError(
|
|
1095
|
+
`Invalid run result at "${filePath}":
|
|
1096
|
+
${issues}`,
|
|
1097
|
+
"result",
|
|
1098
|
+
"INVALID_RESULT"
|
|
1099
|
+
);
|
|
1100
|
+
}
|
|
1101
|
+
return result.data;
|
|
1102
|
+
}
|
|
1103
|
+
async function listResultIds(rootDir) {
|
|
1104
|
+
const runsDir = import_path6.default.join(rootDir, RESULTS_DIR, RUNS_DIR);
|
|
1105
|
+
if (!await import_fs_extra7.default.pathExists(runsDir)) return [];
|
|
1106
|
+
const entries = await import_fs_extra7.default.readdir(runsDir, { withFileTypes: true });
|
|
1107
|
+
const ids = entries.filter((e) => e.isFile() && e.name.endsWith(".json")).map((e) => e.name.replace(".json", ""));
|
|
1108
|
+
const withStats = await Promise.all(
|
|
1109
|
+
ids.map(async (id) => {
|
|
1110
|
+
const stat = await import_fs_extra7.default.stat(import_path6.default.join(runsDir, `${id}.json`));
|
|
1111
|
+
return { id, mtime: stat.mtimeMs };
|
|
1112
|
+
})
|
|
1113
|
+
);
|
|
1114
|
+
return withStats.sort((a, b) => b.mtime - a.mtime).map((x) => x.id);
|
|
1115
|
+
}
|
|
1116
|
+
async function listResults(rootDir, limit = 20) {
|
|
1117
|
+
const ids = await listResultIds(rootDir);
|
|
1118
|
+
const results = [];
|
|
1119
|
+
for (const id of ids.slice(0, limit)) {
|
|
1120
|
+
try {
|
|
1121
|
+
const result = await readResult(rootDir, id);
|
|
1122
|
+
results.push({
|
|
1123
|
+
runId: result.runId,
|
|
1124
|
+
status: result.status,
|
|
1125
|
+
environment: result.environment,
|
|
1126
|
+
startedAt: result.startedAt,
|
|
1127
|
+
finishedAt: result.finishedAt,
|
|
1128
|
+
totalTests: result.totalTests,
|
|
1129
|
+
passed: result.passed,
|
|
1130
|
+
failed: result.failed,
|
|
1131
|
+
skipped: result.skipped,
|
|
1132
|
+
duration: result.duration,
|
|
1133
|
+
suiteIds: result.suites.map((s) => s.suiteId)
|
|
1134
|
+
});
|
|
1135
|
+
} catch (err) {
|
|
1136
|
+
logger6.warn({ runId: id, err }, "Failed to read run result \u2014 skipping");
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
return results;
|
|
1140
|
+
}
|
|
1141
|
+
async function deleteResult(rootDir, runId) {
|
|
1142
|
+
const filePath = runFilePath(import_path6.default.join(rootDir, RESULTS_DIR), runId);
|
|
1143
|
+
if (!await import_fs_extra7.default.pathExists(filePath)) {
|
|
1144
|
+
throw new StorageError(
|
|
1145
|
+
`Run result not found for runId "${runId}".`,
|
|
1146
|
+
filePath,
|
|
1147
|
+
"RESULT_NOT_FOUND"
|
|
1148
|
+
);
|
|
1149
|
+
}
|
|
1150
|
+
await import_fs_extra7.default.remove(filePath);
|
|
1151
|
+
logger6.info({ runId }, "Run result deleted");
|
|
1152
|
+
}
|
|
1153
|
+
function screenshotsDir(rootDir) {
|
|
1154
|
+
return import_path6.default.join(rootDir, RESULTS_DIR, "screenshots");
|
|
1155
|
+
}
|
|
1156
|
+
function videosDir(rootDir) {
|
|
1157
|
+
return import_path6.default.join(rootDir, RESULTS_DIR, "videos");
|
|
1158
|
+
}
|
|
1159
|
+
function tracesDir(rootDir) {
|
|
1160
|
+
return import_path6.default.join(rootDir, RESULTS_DIR, "traces");
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
// src/storage/healing-store.ts
|
|
1164
|
+
var import_path7 = __toESM(require("path"));
|
|
1165
|
+
var import_fs_extra8 = __toESM(require("fs-extra"));
|
|
1166
|
+
|
|
1167
|
+
// src/types/healing.ts
|
|
1168
|
+
var import_zod5 = require("zod");
|
|
1169
|
+
var HealingStatusSchema = import_zod5.z.enum(["pending", "accepted", "rejected"]);
|
|
1170
|
+
var HealingStrategySchema = import_zod5.z.enum([
|
|
1171
|
+
"retry",
|
|
1172
|
+
"regenerate",
|
|
1173
|
+
"multi-selector",
|
|
1174
|
+
"visual",
|
|
1175
|
+
"decompose",
|
|
1176
|
+
"manual"
|
|
1177
|
+
]);
|
|
1178
|
+
var HealingEventSchema = import_zod5.z.object({
|
|
1179
|
+
id: import_zod5.z.string(),
|
|
1180
|
+
runId: import_zod5.z.string(),
|
|
1181
|
+
suiteId: import_zod5.z.string(),
|
|
1182
|
+
caseId: import_zod5.z.string(),
|
|
1183
|
+
stepId: import_zod5.z.string(),
|
|
1184
|
+
stepInstruction: import_zod5.z.string(),
|
|
1185
|
+
failedCode: import_zod5.z.string(),
|
|
1186
|
+
healedCode: import_zod5.z.string(),
|
|
1187
|
+
error: import_zod5.z.string(),
|
|
1188
|
+
strategy: HealingStrategySchema,
|
|
1189
|
+
level: import_zod5.z.number().int().min(1).max(6),
|
|
1190
|
+
status: HealingStatusSchema,
|
|
1191
|
+
pageUrl: import_zod5.z.string(),
|
|
1192
|
+
timestamp: import_zod5.z.string().datetime(),
|
|
1193
|
+
acceptedAt: import_zod5.z.string().datetime().optional(),
|
|
1194
|
+
rejectedAt: import_zod5.z.string().datetime().optional()
|
|
1195
|
+
});
|
|
1196
|
+
var HealingReportSchema = import_zod5.z.object({
|
|
1197
|
+
runId: import_zod5.z.string(),
|
|
1198
|
+
generatedAt: import_zod5.z.string().datetime(),
|
|
1199
|
+
totalHeals: import_zod5.z.number().int(),
|
|
1200
|
+
accepted: import_zod5.z.number().int(),
|
|
1201
|
+
rejected: import_zod5.z.number().int(),
|
|
1202
|
+
pending: import_zod5.z.number().int(),
|
|
1203
|
+
events: import_zod5.z.array(HealingEventSchema)
|
|
1204
|
+
});
|
|
1205
|
+
|
|
1206
|
+
// src/storage/healing-store.ts
|
|
1207
|
+
var logger7 = createChildLogger("healing-store");
|
|
1208
|
+
var HEALING_DIR = import_path7.default.join("results", "healing");
|
|
1209
|
+
var PENDING_FILE = "pending.json";
|
|
1210
|
+
var REPORT_PREFIX = "healing-report-";
|
|
1211
|
+
function reportFilePath(healingDir, runId) {
|
|
1212
|
+
return import_path7.default.join(healingDir, `${REPORT_PREFIX}${runId}.json`);
|
|
1213
|
+
}
|
|
1214
|
+
function pendingFilePath(healingDir) {
|
|
1215
|
+
return import_path7.default.join(healingDir, PENDING_FILE);
|
|
1216
|
+
}
|
|
1217
|
+
async function writeHealingReport(rootDir, report) {
|
|
1218
|
+
const schema = HealingReportSchema.safeParse(report);
|
|
1219
|
+
if (!schema.success) {
|
|
1220
|
+
const issues = schema.error.issues.map((i) => ` \u2022 ${i.path.join(".")}: ${i.message}`).join("\n");
|
|
1221
|
+
throw new ValidationError(
|
|
1222
|
+
`Cannot write invalid healing report:
|
|
1223
|
+
${issues}`,
|
|
1224
|
+
"healingReport",
|
|
1225
|
+
"INVALID_HEALING_REPORT"
|
|
1226
|
+
);
|
|
1227
|
+
}
|
|
1228
|
+
const healingDir = import_path7.default.join(rootDir, HEALING_DIR);
|
|
1229
|
+
const filePath = reportFilePath(healingDir, report.runId);
|
|
1230
|
+
await atomicWriteJson(filePath, schema.data);
|
|
1231
|
+
logger7.info({ runId: report.runId, totalHeals: report.totalHeals }, "Healing report saved");
|
|
1232
|
+
}
|
|
1233
|
+
async function readHealingReport(rootDir, runId) {
|
|
1234
|
+
const healingDir = import_path7.default.join(rootDir, HEALING_DIR);
|
|
1235
|
+
const filePath = reportFilePath(healingDir, runId);
|
|
1236
|
+
if (!await import_fs_extra8.default.pathExists(filePath)) {
|
|
1237
|
+
throw new StorageError(
|
|
1238
|
+
`Healing report not found for runId "${runId}".`,
|
|
1239
|
+
filePath,
|
|
1240
|
+
"HEALING_REPORT_NOT_FOUND"
|
|
1241
|
+
);
|
|
1242
|
+
}
|
|
1243
|
+
const raw = await readJson(filePath);
|
|
1244
|
+
const result = HealingReportSchema.safeParse(raw);
|
|
1245
|
+
if (!result.success) {
|
|
1246
|
+
const issues = result.error.issues.map((i) => ` \u2022 ${i.path.join(".")}: ${i.message}`).join("\n");
|
|
1247
|
+
throw new ValidationError(
|
|
1248
|
+
`Invalid healing report at "${filePath}":
|
|
1249
|
+
${issues}`,
|
|
1250
|
+
"healingReport",
|
|
1251
|
+
"INVALID_HEALING_REPORT"
|
|
1252
|
+
);
|
|
1253
|
+
}
|
|
1254
|
+
return result.data;
|
|
1255
|
+
}
|
|
1256
|
+
async function readPendingEvents(rootDir) {
|
|
1257
|
+
const healingDir = import_path7.default.join(rootDir, HEALING_DIR);
|
|
1258
|
+
const filePath = pendingFilePath(healingDir);
|
|
1259
|
+
if (!await import_fs_extra8.default.pathExists(filePath)) return [];
|
|
1260
|
+
const raw = await readJson(filePath);
|
|
1261
|
+
const result = HealingEventSchema.array().safeParse(raw);
|
|
1262
|
+
if (!result.success) {
|
|
1263
|
+
logger7.warn({ path: filePath }, "Pending healing file is malformed \u2014 resetting");
|
|
1264
|
+
return [];
|
|
1265
|
+
}
|
|
1266
|
+
return result.data;
|
|
1267
|
+
}
|
|
1268
|
+
var MAX_HEALING_EVENTS = 50;
|
|
1269
|
+
var PRUNE_PRIORITY = ["pending", "rejected", "accepted"];
|
|
1270
|
+
function pruneToLimit(events) {
|
|
1271
|
+
if (events.length <= MAX_HEALING_EVENTS) return events;
|
|
1272
|
+
const byPriority = [...events].sort((a, b) => {
|
|
1273
|
+
const pa = PRUNE_PRIORITY.indexOf(a.status);
|
|
1274
|
+
const pb = PRUNE_PRIORITY.indexOf(b.status);
|
|
1275
|
+
if (pa !== pb) return pa - pb;
|
|
1276
|
+
return a.timestamp < b.timestamp ? -1 : 1;
|
|
1277
|
+
});
|
|
1278
|
+
const excess = events.length - MAX_HEALING_EVENTS;
|
|
1279
|
+
const toDelete = new Set(byPriority.slice(0, excess).map((e) => e.id));
|
|
1280
|
+
logger7.info(
|
|
1281
|
+
{ excess, deleted: toDelete.size },
|
|
1282
|
+
"Auto-pruning healing events to enforce cap"
|
|
1283
|
+
);
|
|
1284
|
+
return events.filter((e) => !toDelete.has(e.id));
|
|
1285
|
+
}
|
|
1286
|
+
async function appendPendingEvent(rootDir, event) {
|
|
1287
|
+
const schema = HealingEventSchema.safeParse(event);
|
|
1288
|
+
if (!schema.success) {
|
|
1289
|
+
throw new ValidationError(
|
|
1290
|
+
`Invalid healing event: ${schema.error.message}`,
|
|
1291
|
+
"healingEvent",
|
|
1292
|
+
"INVALID_HEALING_EVENT"
|
|
1293
|
+
);
|
|
1294
|
+
}
|
|
1295
|
+
const healingDir = import_path7.default.join(rootDir, HEALING_DIR);
|
|
1296
|
+
await import_fs_extra8.default.ensureDir(healingDir);
|
|
1297
|
+
const existing = await readPendingEvents(rootDir);
|
|
1298
|
+
existing.push(schema.data);
|
|
1299
|
+
await atomicWriteJson(pendingFilePath(healingDir), pruneToLimit(existing));
|
|
1300
|
+
}
|
|
1301
|
+
async function acceptHealingEvent(rootDir, eventId) {
|
|
1302
|
+
const pending = await readPendingEvents(rootDir);
|
|
1303
|
+
const idx = pending.findIndex((e) => e.id === eventId);
|
|
1304
|
+
if (idx === -1) {
|
|
1305
|
+
throw new StorageError(
|
|
1306
|
+
`Healing event "${eventId}" not found in pending list.`,
|
|
1307
|
+
eventId,
|
|
1308
|
+
"HEALING_EVENT_NOT_FOUND"
|
|
1309
|
+
);
|
|
1310
|
+
}
|
|
1311
|
+
pending[idx] = {
|
|
1312
|
+
...pending[idx],
|
|
1313
|
+
status: "accepted",
|
|
1314
|
+
acceptedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1315
|
+
};
|
|
1316
|
+
const healingDir = import_path7.default.join(rootDir, HEALING_DIR);
|
|
1317
|
+
await atomicWriteJson(pendingFilePath(healingDir), pending);
|
|
1318
|
+
logger7.info({ eventId }, "Healing event accepted");
|
|
1319
|
+
return pending[idx];
|
|
1320
|
+
}
|
|
1321
|
+
async function rejectHealingEvent(rootDir, eventId) {
|
|
1322
|
+
const pending = await readPendingEvents(rootDir);
|
|
1323
|
+
const idx = pending.findIndex((e) => e.id === eventId);
|
|
1324
|
+
if (idx === -1) {
|
|
1325
|
+
throw new StorageError(
|
|
1326
|
+
`Healing event "${eventId}" not found in pending list.`,
|
|
1327
|
+
eventId,
|
|
1328
|
+
"HEALING_EVENT_NOT_FOUND"
|
|
1329
|
+
);
|
|
1330
|
+
}
|
|
1331
|
+
pending[idx] = {
|
|
1332
|
+
...pending[idx],
|
|
1333
|
+
status: "rejected",
|
|
1334
|
+
rejectedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1335
|
+
};
|
|
1336
|
+
const healingDir = import_path7.default.join(rootDir, HEALING_DIR);
|
|
1337
|
+
await atomicWriteJson(pendingFilePath(healingDir), pending);
|
|
1338
|
+
logger7.info({ eventId }, "Healing event rejected");
|
|
1339
|
+
return pending[idx];
|
|
1340
|
+
}
|
|
1341
|
+
async function pruneResolvedEvents(rootDir, retentionDays = 30) {
|
|
1342
|
+
const pending = await readPendingEvents(rootDir);
|
|
1343
|
+
const cutoff = Date.now() - retentionDays * 24 * 60 * 60 * 1e3;
|
|
1344
|
+
const before = pending.length;
|
|
1345
|
+
const kept = pending.filter((e) => {
|
|
1346
|
+
if (e.status === "pending") return true;
|
|
1347
|
+
const resolvedAt = e.acceptedAt ?? e.rejectedAt;
|
|
1348
|
+
if (!resolvedAt) return true;
|
|
1349
|
+
return new Date(resolvedAt).getTime() > cutoff;
|
|
1350
|
+
});
|
|
1351
|
+
if (kept.length < before) {
|
|
1352
|
+
const healingDir = import_path7.default.join(rootDir, HEALING_DIR);
|
|
1353
|
+
await atomicWriteJson(pendingFilePath(healingDir), kept);
|
|
1354
|
+
}
|
|
1355
|
+
return before - kept.length;
|
|
1356
|
+
}
|
|
1357
|
+
async function getHealingStats(rootDir) {
|
|
1358
|
+
const events = await readPendingEvents(rootDir);
|
|
1359
|
+
const pending = events.filter((e) => e.status === "pending").length;
|
|
1360
|
+
const accepted = events.filter((e) => e.status === "accepted").length;
|
|
1361
|
+
const rejected = events.filter((e) => e.status === "rejected").length;
|
|
1362
|
+
return { pending, accepted, rejected, total: events.length };
|
|
1363
|
+
}
|
|
1364
|
+
async function listHealingReportIds(rootDir) {
|
|
1365
|
+
const healingDir = import_path7.default.join(rootDir, HEALING_DIR);
|
|
1366
|
+
if (!await import_fs_extra8.default.pathExists(healingDir)) return [];
|
|
1367
|
+
const entries = await import_fs_extra8.default.readdir(healingDir, { withFileTypes: true });
|
|
1368
|
+
const ids = entries.filter((e) => e.isFile() && e.name.startsWith(REPORT_PREFIX) && e.name.endsWith(".json")).map((e) => e.name.slice(REPORT_PREFIX.length, -".json".length));
|
|
1369
|
+
const withStats = await Promise.all(
|
|
1370
|
+
ids.map(async (id) => {
|
|
1371
|
+
const stat = await import_fs_extra8.default.stat(import_path7.default.join(healingDir, `${REPORT_PREFIX}${id}.json`));
|
|
1372
|
+
return { id, mtime: stat.mtimeMs };
|
|
1373
|
+
})
|
|
1374
|
+
);
|
|
1375
|
+
return withStats.sort((a, b) => b.mtime - a.mtime).map((x) => x.id);
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
// src/utils/hash.ts
|
|
1379
|
+
var import_crypto = require("crypto");
|
|
1380
|
+
function sha256(input) {
|
|
1381
|
+
return (0, import_crypto.createHash)("sha256").update(input, "utf8").digest("hex");
|
|
1382
|
+
}
|
|
1383
|
+
function normalizeUrl(url) {
|
|
1384
|
+
try {
|
|
1385
|
+
const parsed = new URL(url);
|
|
1386
|
+
const normalizedPath = parsed.pathname.split("/").map((segment) => {
|
|
1387
|
+
if (/^\d+$/.test(segment)) return "*";
|
|
1388
|
+
if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(segment)) {
|
|
1389
|
+
return "*";
|
|
1390
|
+
}
|
|
1391
|
+
if (/^[a-z0-9-]{8,}$/i.test(segment) && /-/.test(segment)) return "*";
|
|
1392
|
+
return segment;
|
|
1393
|
+
}).join("/");
|
|
1394
|
+
return normalizedPath;
|
|
1395
|
+
} catch {
|
|
1396
|
+
return url;
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
function generateCacheKey(instruction, url) {
|
|
1400
|
+
const normalizedInstruction = instruction.toLowerCase().replace(/\s+/g, " ").trim();
|
|
1401
|
+
const urlPattern = normalizeUrl(url);
|
|
1402
|
+
return sha256(`${normalizedInstruction}|${urlPattern}`).slice(0, 16);
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
// src/utils/env.ts
|
|
1406
|
+
var import_dotenv = require("dotenv");
|
|
1407
|
+
var import_zod6 = require("zod");
|
|
1408
|
+
(0, import_dotenv.config)();
|
|
1409
|
+
var AI_PROVIDERS = [
|
|
1410
|
+
"anthropic",
|
|
1411
|
+
"openai",
|
|
1412
|
+
"google",
|
|
1413
|
+
"azure-openai",
|
|
1414
|
+
"bedrock",
|
|
1415
|
+
"deepseek",
|
|
1416
|
+
"groq",
|
|
1417
|
+
"together",
|
|
1418
|
+
"qwen",
|
|
1419
|
+
"perplexity",
|
|
1420
|
+
"ollama",
|
|
1421
|
+
"custom"
|
|
1422
|
+
];
|
|
1423
|
+
var BaseEnvSchema = import_zod6.z.object({
|
|
1424
|
+
NODE_ENV: import_zod6.z.enum(["development", "production", "test"]).default("development"),
|
|
1425
|
+
LOG_LEVEL: import_zod6.z.enum(["trace", "debug", "info", "warn", "error", "fatal"]).default("info"),
|
|
1426
|
+
AI_PROVIDER: import_zod6.z.enum(AI_PROVIDERS),
|
|
1427
|
+
AI_TIERED_ENABLED: import_zod6.z.string().transform((v) => v === "true").default("false"),
|
|
1428
|
+
AI_TIERED_FAST_PROVIDER: import_zod6.z.enum(AI_PROVIDERS).optional(),
|
|
1429
|
+
AI_TIERED_FAST_MODEL: import_zod6.z.string().optional(),
|
|
1430
|
+
AI_TIMEOUT: import_zod6.z.coerce.number().int().positive().default(30),
|
|
1431
|
+
AI_MAX_RETRIES: import_zod6.z.coerce.number().int().min(0).max(10).default(2)
|
|
1432
|
+
});
|
|
1433
|
+
var AnthropicEnvSchema = import_zod6.z.object({
|
|
1434
|
+
ANTHROPIC_API_KEY: import_zod6.z.string().min(1),
|
|
1435
|
+
ANTHROPIC_MODEL: import_zod6.z.string().default("claude-sonnet-4-20250514")
|
|
1436
|
+
});
|
|
1437
|
+
var OpenAIEnvSchema = import_zod6.z.object({
|
|
1438
|
+
OPENAI_API_KEY: import_zod6.z.string().min(1),
|
|
1439
|
+
OPENAI_MODEL: import_zod6.z.string().default("gpt-4o")
|
|
1440
|
+
});
|
|
1441
|
+
var GoogleEnvSchema = import_zod6.z.object({
|
|
1442
|
+
GOOGLE_API_KEY: import_zod6.z.string().min(1),
|
|
1443
|
+
GOOGLE_MODEL: import_zod6.z.string().default("gemini-2.5-pro")
|
|
1444
|
+
});
|
|
1445
|
+
var AzureOpenAIEnvSchema = import_zod6.z.object({
|
|
1446
|
+
AZURE_OPENAI_API_KEY: import_zod6.z.string().min(1),
|
|
1447
|
+
AZURE_OPENAI_ENDPOINT: import_zod6.z.string().url(),
|
|
1448
|
+
AZURE_OPENAI_DEPLOYMENT: import_zod6.z.string().min(1),
|
|
1449
|
+
AZURE_OPENAI_API_VERSION: import_zod6.z.string().default("2024-10-21")
|
|
1450
|
+
});
|
|
1451
|
+
var BedrockEnvSchema = import_zod6.z.object({
|
|
1452
|
+
AWS_ACCESS_KEY_ID: import_zod6.z.string().min(1),
|
|
1453
|
+
AWS_SECRET_ACCESS_KEY: import_zod6.z.string().min(1),
|
|
1454
|
+
AWS_SESSION_TOKEN: import_zod6.z.string().optional(),
|
|
1455
|
+
AWS_REGION: import_zod6.z.string().default("us-east-1"),
|
|
1456
|
+
BEDROCK_MODEL: import_zod6.z.string().default("anthropic.claude-sonnet-4-20250514-v1:0")
|
|
1457
|
+
});
|
|
1458
|
+
var DeepSeekEnvSchema = import_zod6.z.object({
|
|
1459
|
+
DEEPSEEK_API_KEY: import_zod6.z.string().min(1),
|
|
1460
|
+
DEEPSEEK_MODEL: import_zod6.z.string().default("deepseek-chat")
|
|
1461
|
+
});
|
|
1462
|
+
var GroqEnvSchema = import_zod6.z.object({
|
|
1463
|
+
GROQ_API_KEY: import_zod6.z.string().min(1),
|
|
1464
|
+
GROQ_MODEL: import_zod6.z.string().default("llama-3.3-70b-versatile")
|
|
1465
|
+
});
|
|
1466
|
+
var TogetherEnvSchema = import_zod6.z.object({
|
|
1467
|
+
TOGETHER_API_KEY: import_zod6.z.string().min(1),
|
|
1468
|
+
TOGETHER_MODEL: import_zod6.z.string().default("meta-llama/Llama-3.3-70B-Instruct-Turbo")
|
|
1469
|
+
});
|
|
1470
|
+
var QwenEnvSchema = import_zod6.z.object({
|
|
1471
|
+
QWEN_API_KEY: import_zod6.z.string().min(1),
|
|
1472
|
+
QWEN_BASE_URL: import_zod6.z.string().url().default("https://dashscope-intl.aliyuncs.com/compatible-mode/v1"),
|
|
1473
|
+
QWEN_MODEL: import_zod6.z.string().default("qwen-max")
|
|
1474
|
+
});
|
|
1475
|
+
var PerplexityEnvSchema = import_zod6.z.object({
|
|
1476
|
+
PERPLEXITY_API_KEY: import_zod6.z.string().min(1),
|
|
1477
|
+
PERPLEXITY_MODEL: import_zod6.z.string().default("sonar-pro")
|
|
1478
|
+
});
|
|
1479
|
+
var OllamaEnvSchema = import_zod6.z.object({
|
|
1480
|
+
OLLAMA_BASE_URL: import_zod6.z.string().url().default("http://localhost:11434"),
|
|
1481
|
+
OLLAMA_MODEL: import_zod6.z.string().default("llama3.3")
|
|
1482
|
+
});
|
|
1483
|
+
var CustomEnvSchema = import_zod6.z.object({
|
|
1484
|
+
CUSTOM_API_KEY: import_zod6.z.string().min(1),
|
|
1485
|
+
CUSTOM_BASE_URL: import_zod6.z.string().url(),
|
|
1486
|
+
CUSTOM_MODEL: import_zod6.z.string().min(1)
|
|
1487
|
+
});
|
|
1488
|
+
var PROVIDER_SCHEMAS = {
|
|
1489
|
+
anthropic: AnthropicEnvSchema,
|
|
1490
|
+
openai: OpenAIEnvSchema,
|
|
1491
|
+
google: GoogleEnvSchema,
|
|
1492
|
+
"azure-openai": AzureOpenAIEnvSchema,
|
|
1493
|
+
bedrock: BedrockEnvSchema,
|
|
1494
|
+
deepseek: DeepSeekEnvSchema,
|
|
1495
|
+
groq: GroqEnvSchema,
|
|
1496
|
+
together: TogetherEnvSchema,
|
|
1497
|
+
qwen: QwenEnvSchema,
|
|
1498
|
+
perplexity: PerplexityEnvSchema,
|
|
1499
|
+
ollama: OllamaEnvSchema,
|
|
1500
|
+
custom: CustomEnvSchema
|
|
1501
|
+
};
|
|
1502
|
+
var _cachedEnv = null;
|
|
1503
|
+
function validateEnv() {
|
|
1504
|
+
if (_cachedEnv !== null) return _cachedEnv;
|
|
1505
|
+
const baseResult = BaseEnvSchema.safeParse(process.env);
|
|
1506
|
+
if (!baseResult.success) {
|
|
1507
|
+
const issues = baseResult.error.issues.map((i) => ` \u2022 ${i.path.join(".")}: ${i.message}`).join("\n");
|
|
1508
|
+
throw new ConfigError(
|
|
1509
|
+
`Missing or invalid environment variables:
|
|
1510
|
+
${issues}
|
|
1511
|
+
|
|
1512
|
+
How to fix: Copy .env.example to .env and fill in your AI provider credentials.`,
|
|
1513
|
+
"ENV_VALIDATION_FAILED"
|
|
1514
|
+
);
|
|
1515
|
+
}
|
|
1516
|
+
const providerName = baseResult.data.AI_PROVIDER;
|
|
1517
|
+
const providerSchema = PROVIDER_SCHEMAS[providerName];
|
|
1518
|
+
const providerResult = providerSchema.safeParse(process.env);
|
|
1519
|
+
if (!providerResult.success) {
|
|
1520
|
+
const issues = providerResult.error.issues.map((i) => ` \u2022 ${i.path.join(".")}: ${i.message}`).join("\n");
|
|
1521
|
+
throw new ConfigError(
|
|
1522
|
+
`Provider "${providerName}" is missing required environment variables:
|
|
1523
|
+
${issues}
|
|
1524
|
+
|
|
1525
|
+
How to fix: Check .env.example for the ${providerName} section and add the required keys to .env.`,
|
|
1526
|
+
"PROVIDER_ENV_MISSING"
|
|
1527
|
+
);
|
|
1528
|
+
}
|
|
1529
|
+
const merged = {
|
|
1530
|
+
...baseResult.data,
|
|
1531
|
+
...providerResult.data
|
|
1532
|
+
};
|
|
1533
|
+
_cachedEnv = merged;
|
|
1534
|
+
return _cachedEnv;
|
|
1535
|
+
}
|
|
1536
|
+
function clearEnvCache() {
|
|
1537
|
+
_cachedEnv = null;
|
|
1538
|
+
}
|
|
1539
|
+
function tryGetEnv() {
|
|
1540
|
+
try {
|
|
1541
|
+
return validateEnv();
|
|
1542
|
+
} catch {
|
|
1543
|
+
return null;
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
// src/index.ts
|
|
1548
|
+
function defineConfig(config) {
|
|
1549
|
+
const merged = { ...DEFAULT_CONFIG, ...config };
|
|
1550
|
+
const result = AutotestConfigSchema.safeParse(merged);
|
|
1551
|
+
if (!result.success) {
|
|
1552
|
+
const issues = result.error.issues.map((i) => ` \u2022 ${i.path.join(".")}: ${i.message}`).join("\n");
|
|
1553
|
+
throw new Error(`Invalid assuremind config:
|
|
1554
|
+
${issues}`);
|
|
1555
|
+
}
|
|
1556
|
+
return result.data;
|
|
1557
|
+
}
|
|
1558
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1559
|
+
0 && (module.exports = {
|
|
1560
|
+
AssuremindError,
|
|
1561
|
+
ConfigError,
|
|
1562
|
+
ExecutionError,
|
|
1563
|
+
HealingError,
|
|
1564
|
+
ProviderError,
|
|
1565
|
+
StorageError,
|
|
1566
|
+
ValidationError,
|
|
1567
|
+
acceptHealingEvent,
|
|
1568
|
+
appendPendingEvent,
|
|
1569
|
+
clearEnvCache,
|
|
1570
|
+
configExists,
|
|
1571
|
+
createCase,
|
|
1572
|
+
createChildLogger,
|
|
1573
|
+
createSuite,
|
|
1574
|
+
defineConfig,
|
|
1575
|
+
deleteCase,
|
|
1576
|
+
deleteGlobalVariable,
|
|
1577
|
+
deleteResult,
|
|
1578
|
+
deleteSuite,
|
|
1579
|
+
formatError,
|
|
1580
|
+
generateCacheKey,
|
|
1581
|
+
getCasePath,
|
|
1582
|
+
getHealingStats,
|
|
1583
|
+
isAssuremindError,
|
|
1584
|
+
listCasePaths,
|
|
1585
|
+
listCases,
|
|
1586
|
+
listHealingReportIds,
|
|
1587
|
+
listResultIds,
|
|
1588
|
+
listResults,
|
|
1589
|
+
listSuiteDirs,
|
|
1590
|
+
listSuites,
|
|
1591
|
+
listSuitesWithCounts,
|
|
1592
|
+
listVariableFiles,
|
|
1593
|
+
logger,
|
|
1594
|
+
normalizeUrl,
|
|
1595
|
+
pruneResolvedEvents,
|
|
1596
|
+
readCase,
|
|
1597
|
+
readConfig,
|
|
1598
|
+
readEnvVariables,
|
|
1599
|
+
readGlobalVariables,
|
|
1600
|
+
readHealingReport,
|
|
1601
|
+
readPendingEvents,
|
|
1602
|
+
readResult,
|
|
1603
|
+
readSuite,
|
|
1604
|
+
readVariables,
|
|
1605
|
+
redactSecrets,
|
|
1606
|
+
rejectHealingEvent,
|
|
1607
|
+
resolveVariables,
|
|
1608
|
+
sanitizeGeneratedCode,
|
|
1609
|
+
screenshotsDir,
|
|
1610
|
+
setGlobalVariable,
|
|
1611
|
+
sha256,
|
|
1612
|
+
toSlug,
|
|
1613
|
+
tracesDir,
|
|
1614
|
+
tryGetEnv,
|
|
1615
|
+
updateCase,
|
|
1616
|
+
updateConfig,
|
|
1617
|
+
updateSuite,
|
|
1618
|
+
validateConfig,
|
|
1619
|
+
validateEnv,
|
|
1620
|
+
videosDir,
|
|
1621
|
+
writeCase,
|
|
1622
|
+
writeConfig,
|
|
1623
|
+
writeHealingReport,
|
|
1624
|
+
writeResult,
|
|
1625
|
+
writeSuite,
|
|
1626
|
+
writeVariables
|
|
1627
|
+
});
|
|
1628
|
+
//# sourceMappingURL=index.js.map
|