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