playwright-locator-healer 0.1.3
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/CHANGELOG.md +71 -0
- package/LICENSE +21 -0
- package/README.md +338 -0
- package/USAGE.md +217 -0
- package/dist/healing/audit-log.d.ts +9 -0
- package/dist/healing/audit-log.d.ts.map +1 -0
- package/dist/healing/audit-log.js +22 -0
- package/dist/healing/audit-log.js.map +1 -0
- package/dist/healing/auto-fixture.d.ts +14 -0
- package/dist/healing/auto-fixture.d.ts.map +1 -0
- package/dist/healing/auto-fixture.js +430 -0
- package/dist/healing/auto-fixture.js.map +1 -0
- package/dist/healing/cache.d.ts +11 -0
- package/dist/healing/cache.d.ts.map +1 -0
- package/dist/healing/cache.js +128 -0
- package/dist/healing/cache.js.map +1 -0
- package/dist/healing/circuit-breaker.d.ts +11 -0
- package/dist/healing/circuit-breaker.d.ts.map +1 -0
- package/dist/healing/circuit-breaker.js +34 -0
- package/dist/healing/circuit-breaker.js.map +1 -0
- package/dist/healing/cost-tracker.d.ts +30 -0
- package/dist/healing/cost-tracker.d.ts.map +1 -0
- package/dist/healing/cost-tracker.js +101 -0
- package/dist/healing/cost-tracker.js.map +1 -0
- package/dist/healing/dom-extractor.d.ts +9 -0
- package/dist/healing/dom-extractor.d.ts.map +1 -0
- package/dist/healing/dom-extractor.js +268 -0
- package/dist/healing/dom-extractor.js.map +1 -0
- package/dist/healing/errors.d.ts +33 -0
- package/dist/healing/errors.d.ts.map +1 -0
- package/dist/healing/errors.js +40 -0
- package/dist/healing/errors.js.map +1 -0
- package/dist/healing/heal/caller-context.d.ts +8 -0
- package/dist/healing/heal/caller-context.d.ts.map +1 -0
- package/dist/healing/heal/caller-context.js +46 -0
- package/dist/healing/heal/caller-context.js.map +1 -0
- package/dist/healing/heal/orchestrator-helpers.d.ts +84 -0
- package/dist/healing/heal/orchestrator-helpers.d.ts.map +1 -0
- package/dist/healing/heal/orchestrator-helpers.js +117 -0
- package/dist/healing/heal/orchestrator-helpers.js.map +1 -0
- package/dist/healing/heal/orchestrator.d.ts +47 -0
- package/dist/healing/heal/orchestrator.d.ts.map +1 -0
- package/dist/healing/heal/orchestrator.js +644 -0
- package/dist/healing/heal/orchestrator.js.map +1 -0
- package/dist/healing/heal/playwright-code.d.ts +26 -0
- package/dist/healing/heal/playwright-code.d.ts.map +1 -0
- package/dist/healing/heal/playwright-code.js +134 -0
- package/dist/healing/heal/playwright-code.js.map +1 -0
- package/dist/healing/heal/preflight.d.ts +8 -0
- package/dist/healing/heal/preflight.d.ts.map +1 -0
- package/dist/healing/heal/preflight.js +77 -0
- package/dist/healing/heal/preflight.js.map +1 -0
- package/dist/healing/heal/prompt.d.ts +9 -0
- package/dist/healing/heal/prompt.d.ts.map +1 -0
- package/dist/healing/heal/prompt.js +22 -0
- package/dist/healing/heal/prompt.js.map +1 -0
- package/dist/healing/heal/report.d.ts +11 -0
- package/dist/healing/heal/report.d.ts.map +1 -0
- package/dist/healing/heal/report.js +28 -0
- package/dist/healing/heal/report.js.map +1 -0
- package/dist/healing/heal/source-trace.d.ts +17 -0
- package/dist/healing/heal/source-trace.d.ts.map +1 -0
- package/dist/healing/heal/source-trace.js +59 -0
- package/dist/healing/heal/source-trace.js.map +1 -0
- package/dist/healing/index.d.ts +6 -0
- package/dist/healing/index.d.ts.map +1 -0
- package/dist/healing/index.js +3 -0
- package/dist/healing/index.js.map +1 -0
- package/dist/healing/llm-client.d.ts +58 -0
- package/dist/healing/llm-client.d.ts.map +1 -0
- package/dist/healing/llm-client.js +258 -0
- package/dist/healing/llm-client.js.map +1 -0
- package/dist/healing/logger.d.ts +18 -0
- package/dist/healing/logger.d.ts.map +1 -0
- package/dist/healing/logger.js +38 -0
- package/dist/healing/logger.js.map +1 -0
- package/dist/healing/models.d.ts +26 -0
- package/dist/healing/models.d.ts.map +1 -0
- package/dist/healing/models.js +109 -0
- package/dist/healing/models.js.map +1 -0
- package/dist/healing/overlay/bridge.d.ts +46 -0
- package/dist/healing/overlay/bridge.d.ts.map +1 -0
- package/dist/healing/overlay/bridge.js +2 -0
- package/dist/healing/overlay/bridge.js.map +1 -0
- package/dist/healing/overlay/dock.css.d.ts +2 -0
- package/dist/healing/overlay/dock.css.d.ts.map +1 -0
- package/dist/healing/overlay/dock.css.js +448 -0
- package/dist/healing/overlay/dock.css.js.map +1 -0
- package/dist/healing/overlay/dock.html.d.ts +46 -0
- package/dist/healing/overlay/dock.html.d.ts.map +1 -0
- package/dist/healing/overlay/dock.html.js +248 -0
- package/dist/healing/overlay/dock.html.js.map +1 -0
- package/dist/healing/overlay/drag.d.ts +17 -0
- package/dist/healing/overlay/drag.d.ts.map +1 -0
- package/dist/healing/overlay/drag.js +68 -0
- package/dist/healing/overlay/drag.js.map +1 -0
- package/dist/healing/overlay/inject.d.ts +41 -0
- package/dist/healing/overlay/inject.d.ts.map +1 -0
- package/dist/healing/overlay/inject.js +277 -0
- package/dist/healing/overlay/inject.js.map +1 -0
- package/dist/healing/overlay/keybinds.d.ts +33 -0
- package/dist/healing/overlay/keybinds.d.ts.map +1 -0
- package/dist/healing/overlay/keybinds.js +105 -0
- package/dist/healing/overlay/keybinds.js.map +1 -0
- package/dist/healing/prompts/heal-v21.d.ts +8 -0
- package/dist/healing/prompts/heal-v21.d.ts.map +1 -0
- package/dist/healing/prompts/heal-v21.js +204 -0
- package/dist/healing/prompts/heal-v21.js.map +1 -0
- package/dist/healing/retry-policy.d.ts +37 -0
- package/dist/healing/retry-policy.d.ts.map +1 -0
- package/dist/healing/retry-policy.js +46 -0
- package/dist/healing/retry-policy.js.map +1 -0
- package/dist/healing/session-cost.d.ts +44 -0
- package/dist/healing/session-cost.d.ts.map +1 -0
- package/dist/healing/session-cost.js +95 -0
- package/dist/healing/session-cost.js.map +1 -0
- package/dist/healing/test-fixture.d.ts +9 -0
- package/dist/healing/test-fixture.d.ts.map +1 -0
- package/dist/healing/test-fixture.js +37 -0
- package/dist/healing/test-fixture.js.map +1 -0
- package/dist/healing/types.d.ts +399 -0
- package/dist/healing/types.d.ts.map +1 -0
- package/dist/healing/types.js +162 -0
- package/dist/healing/types.js.map +1 -0
- package/dist/healing/validator.d.ts +64 -0
- package/dist/healing/validator.d.ts.map +1 -0
- package/dist/healing/validator.js +286 -0
- package/dist/healing/validator.js.map +1 -0
- package/dist/scripts/heal-report.d.ts +23 -0
- package/dist/scripts/heal-report.d.ts.map +1 -0
- package/dist/scripts/heal-report.js +106 -0
- package/dist/scripts/heal-report.js.map +1 -0
- package/dist/scripts/init.d.ts +3 -0
- package/dist/scripts/init.d.ts.map +1 -0
- package/dist/scripts/init.js +132 -0
- package/dist/scripts/init.js.map +1 -0
- package/package.json +84 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { appendFile, mkdir } from 'node:fs/promises';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
|
+
/**
|
|
4
|
+
* Append one JSON line to an audit ndjson file. Failures are logged to
|
|
5
|
+
* stderr (with the failing entry's keys, never the values) and re-thrown
|
|
6
|
+
* so callers can decide to swallow or propagate. The previous
|
|
7
|
+
* `.catch(() => {})` silent-swallow pattern hid I/O issues (disk full,
|
|
8
|
+
* permission denied, read-only mount, etc.).
|
|
9
|
+
*/
|
|
10
|
+
export async function appendAuditLine(filePath, entry) {
|
|
11
|
+
try {
|
|
12
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
13
|
+
const line = JSON.stringify({ ts: new Date().toISOString(), ...entry }) + '\n';
|
|
14
|
+
await appendFile(filePath, line, 'utf-8');
|
|
15
|
+
}
|
|
16
|
+
catch (err) {
|
|
17
|
+
const keys = Object.keys(entry).join(',');
|
|
18
|
+
process.stderr.write(`[HEAL] audit append failed file=${filePath} keys=${keys} err=${err.message}\n`);
|
|
19
|
+
throw err;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=audit-log.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audit-log.js","sourceRoot":"","sources":["../../healing/audit-log.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,QAAgB,EAChB,KAA8B;IAE9B,IAAI,CAAC;QACH,MAAM,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACpD,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,EAAE,EAAE,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,EAAE,GAAG,KAAK,EAAE,CAAC,GAAG,IAAI,CAAC;QAC/E,MAAM,UAAU,CAAC,QAAQ,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC;IAC5C,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAC1C,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,mCAAmC,QAAQ,SAAS,IAAI,QAAS,GAAa,CAAC,OAAO,IAAI,CAC3F,CAAC;QACF,MAAM,GAAG,CAAC;IACZ,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Page, TestInfo } from '@playwright/test';
|
|
2
|
+
export declare function isLocatorTimeoutError(err: any): boolean;
|
|
3
|
+
/**
|
|
4
|
+
* Distinguish a strict-mode violation (multiple matches) from a
|
|
5
|
+
* missing/timeout error. Used to gate the optional HEAL_AUTO_FIRST=1
|
|
6
|
+
* fast-path: when the locator matches >1 element and the env is set,
|
|
7
|
+
* narrow to .first() instead of opening the overlay.
|
|
8
|
+
*/
|
|
9
|
+
export declare function isStrictModeViolation(err: any): boolean;
|
|
10
|
+
export declare function wrapPageWithHeal(page: Page, testInfo: TestInfo): Page;
|
|
11
|
+
export declare const test: import("@playwright/test").TestType<import("@playwright/test").PlaywrightTestArgs & import("@playwright/test").PlaywrightTestOptions, import("@playwright/test").PlaywrightWorkerArgs & import("@playwright/test").PlaywrightWorkerOptions>;
|
|
12
|
+
export declare function wrapPageWithHealAndTrack(page: Page, testInfo: TestInfo): Page;
|
|
13
|
+
export declare const expect: import("@playwright/test").Expect<{}>;
|
|
14
|
+
//# sourceMappingURL=auto-fixture.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auto-fixture.d.ts","sourceRoot":"","sources":["../../healing/auto-fixture.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,IAAI,EAAW,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AA+FhE,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,GAAG,GAAG,OAAO,CAUvD;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,GAAG,GAAG,OAAO,CAIvD;AA0MD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,QAAQ,GAAG,IAAI,CAyCrE;AAED,eAAO,MAAM,IAAI,6OAKf,CAAC;AAaH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,QAAQ,GAAG,IAAI,CAK7E;AA6DD,eAAO,MAAM,MAAM,uCAAgB,CAAC"}
|
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
import { test as base, expect as baseExpect } from '@playwright/test';
|
|
2
|
+
import { basename, join, resolve } from 'node:path';
|
|
3
|
+
import { healAction } from './heal/orchestrator.js';
|
|
4
|
+
import { cacheV2Get } from './cache.js';
|
|
5
|
+
import { buildLocator, captureFingerprint, fingerprintMatches } from './validator.js';
|
|
6
|
+
import { inferDescriptionFromCallsite } from './heal/caller-context.js';
|
|
7
|
+
import { HealingError } from './errors.js';
|
|
8
|
+
/**
|
|
9
|
+
* Transparent auto-heal Playwright fixture.
|
|
10
|
+
*
|
|
11
|
+
* Test authors import `{ test, expect }` from this module and write plain
|
|
12
|
+
* Playwright code. Any locator action or assertion-gate query that times out
|
|
13
|
+
* triggers the heal flow automatically; the action is then retried against
|
|
14
|
+
* the healed locator. ZERO modification to the test source.
|
|
15
|
+
*
|
|
16
|
+
* The legacy opt-in fixture at `healing/test-fixture.ts` is preserved.
|
|
17
|
+
*/
|
|
18
|
+
const LOCATOR_PRODUCING_METHODS = new Set([
|
|
19
|
+
'locator', 'getByRole', 'getByText', 'getByLabel', 'getByTestId',
|
|
20
|
+
'getByPlaceholder', 'getByTitle', 'getByAltText',
|
|
21
|
+
]);
|
|
22
|
+
const LOCATOR_CHAIN_METHODS = new Set([
|
|
23
|
+
'locator', 'getByRole', 'getByText', 'getByLabel', 'getByTestId',
|
|
24
|
+
'getByPlaceholder', 'getByTitle', 'getByAltText',
|
|
25
|
+
'filter', 'first', 'last', 'nth', 'and', 'or',
|
|
26
|
+
]);
|
|
27
|
+
const ACTION_METHODS = new Set([
|
|
28
|
+
'click', 'dblclick', 'fill', 'check', 'uncheck', 'hover', 'type',
|
|
29
|
+
'press', 'selectOption', 'tap', 'focus', 'blur', 'clear', 'dragTo',
|
|
30
|
+
'setInputFiles', 'pressSequentially', 'scrollIntoViewIfNeeded',
|
|
31
|
+
]);
|
|
32
|
+
const ASSERTION_GATE_METHODS = new Set([
|
|
33
|
+
'isVisible', 'isHidden', 'isEnabled', 'isDisabled', 'isChecked', 'isEditable',
|
|
34
|
+
'count', 'textContent', 'innerText', 'innerHTML', 'inputValue', 'getAttribute',
|
|
35
|
+
'boundingBox', 'evaluate', 'elementHandle', 'elementHandles', 'all',
|
|
36
|
+
'waitFor',
|
|
37
|
+
]);
|
|
38
|
+
// page-level direct-action methods (deprecated style but still common)
|
|
39
|
+
const PAGE_DIRECT_ACTIONS = new Set([
|
|
40
|
+
'click', 'dblclick', 'fill', 'check', 'uncheck', 'hover', 'type', 'press',
|
|
41
|
+
'selectOption', 'tap', 'focus', 'waitForSelector', 'setInputFiles',
|
|
42
|
+
]);
|
|
43
|
+
// Heal host elements we must never try to "heal" — recursive stall.
|
|
44
|
+
const HEAL_INTERNAL_TOKENS = ['__heal_host__', '__heal_root__', '__heal_never_match__'];
|
|
45
|
+
const HEAL_MARKER = Symbol.for('playwright-heal.autoWrapped');
|
|
46
|
+
function describeArgs(method, args) {
|
|
47
|
+
switch (method) {
|
|
48
|
+
case 'locator': return { selectorString: String(args[0] ?? ''), methodKind: 'locator' };
|
|
49
|
+
case 'getByRole': {
|
|
50
|
+
const role = args[0];
|
|
51
|
+
const opts = args[1] ?? {};
|
|
52
|
+
const name = opts && opts.name ? `[name="${opts.name}"]` : '';
|
|
53
|
+
return { selectorString: `role=${role}${name}`, methodKind: 'getByRole' };
|
|
54
|
+
}
|
|
55
|
+
case 'getByText': return { selectorString: `text=${stringifyMatcher(args[0])}`, methodKind: 'getByText' };
|
|
56
|
+
case 'getByLabel': return { selectorString: `label=${stringifyMatcher(args[0])}`, methodKind: 'getByLabel' };
|
|
57
|
+
case 'getByTestId': return { selectorString: `testid=${stringifyMatcher(args[0])}`, methodKind: 'getByTestId' };
|
|
58
|
+
case 'getByPlaceholder': return { selectorString: `placeholder=${stringifyMatcher(args[0])}`, methodKind: 'getByPlaceholder' };
|
|
59
|
+
case 'getByTitle': return { selectorString: `title=${stringifyMatcher(args[0])}`, methodKind: 'getByTitle' };
|
|
60
|
+
case 'getByAltText': return { selectorString: `alt=${stringifyMatcher(args[0])}`, methodKind: 'getByAltText' };
|
|
61
|
+
default: {
|
|
62
|
+
try {
|
|
63
|
+
return { selectorString: JSON.stringify(args), methodKind: method };
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return { selectorString: String(args[0] ?? ''), methodKind: method };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function stringifyMatcher(v) {
|
|
72
|
+
if (v instanceof RegExp)
|
|
73
|
+
return v.source;
|
|
74
|
+
return String(v ?? '');
|
|
75
|
+
}
|
|
76
|
+
function ensureTimeout(args) {
|
|
77
|
+
const DEFAULT = 5000;
|
|
78
|
+
if (args.length === 0)
|
|
79
|
+
return [{ timeout: DEFAULT }];
|
|
80
|
+
const last = args[args.length - 1];
|
|
81
|
+
if (typeof last === 'object' && last !== null && !Array.isArray(last)) {
|
|
82
|
+
if ('timeout' in last)
|
|
83
|
+
return args;
|
|
84
|
+
return [...args.slice(0, -1), { ...last, timeout: DEFAULT }];
|
|
85
|
+
}
|
|
86
|
+
return [...args, { timeout: DEFAULT }];
|
|
87
|
+
}
|
|
88
|
+
export function isLocatorTimeoutError(err) {
|
|
89
|
+
if (!err)
|
|
90
|
+
return false;
|
|
91
|
+
const msg = String(err?.message ?? err);
|
|
92
|
+
if (err.name === 'TimeoutError')
|
|
93
|
+
return true;
|
|
94
|
+
return (msg.includes('Timeout') ||
|
|
95
|
+
msg.includes('exceeded') ||
|
|
96
|
+
msg.includes('strict mode violation') ||
|
|
97
|
+
/locator\.[a-zA-Z]+:/.test(msg) && msg.includes('waiting'));
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Distinguish a strict-mode violation (multiple matches) from a
|
|
101
|
+
* missing/timeout error. Used to gate the optional HEAL_AUTO_FIRST=1
|
|
102
|
+
* fast-path: when the locator matches >1 element and the env is set,
|
|
103
|
+
* narrow to .first() instead of opening the overlay.
|
|
104
|
+
*/
|
|
105
|
+
export function isStrictModeViolation(err) {
|
|
106
|
+
if (!err)
|
|
107
|
+
return false;
|
|
108
|
+
const msg = String(err?.message ?? err);
|
|
109
|
+
return msg.includes('strict mode violation');
|
|
110
|
+
}
|
|
111
|
+
// Serialize heal calls per-page so two concurrent actions don't both open
|
|
112
|
+
// the overlay. Once the first finishes the second will (typically) cache-hit.
|
|
113
|
+
const healLockByPage = new WeakMap();
|
|
114
|
+
async function withHealLock(page, fn) {
|
|
115
|
+
const prev = healLockByPage.get(page) ?? Promise.resolve();
|
|
116
|
+
let release;
|
|
117
|
+
const next = new Promise(res => { release = res; });
|
|
118
|
+
healLockByPage.set(page, prev.then(() => next));
|
|
119
|
+
await prev;
|
|
120
|
+
try {
|
|
121
|
+
return await fn();
|
|
122
|
+
}
|
|
123
|
+
finally {
|
|
124
|
+
release();
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
async function runHealAndBuild(page, testInfo, original, action) {
|
|
128
|
+
const entry = await withHealLock(page, async () => {
|
|
129
|
+
// Re-check the cache once inside the lock — a concurrent heal may have
|
|
130
|
+
// populated it while we were waiting. Cache dir honors HEAL_CACHE_DIR
|
|
131
|
+
// env so parallel suites / different Playwright projects can isolate.
|
|
132
|
+
const cacheDir = process.env.HEAL_CACHE_DIR
|
|
133
|
+
? resolve(process.env.HEAL_CACHE_DIR)
|
|
134
|
+
: join(process.cwd(), '.healed-locators');
|
|
135
|
+
let cached = null;
|
|
136
|
+
try {
|
|
137
|
+
cached = await cacheV2Get(cacheDir, basename(testInfo.file), original.selectorString, action);
|
|
138
|
+
}
|
|
139
|
+
catch (e) {
|
|
140
|
+
if (e instanceof HealingError && e.code === 'invalid-cache') {
|
|
141
|
+
process.stderr.write(`[HEAL] ${e.message} — treating as cache miss\n`);
|
|
142
|
+
cached = null;
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
throw e;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (cached) {
|
|
149
|
+
const probe = buildLocator(page, {
|
|
150
|
+
selectorType: cached.selectorType, selector: cached.healedSelector,
|
|
151
|
+
rationale: '', tier: cached.tier ?? 6,
|
|
152
|
+
});
|
|
153
|
+
// intentionally swallowed: count() throws on invalid selector → treat as 0 matches
|
|
154
|
+
const probeCount = await probe.count().catch(() => 0);
|
|
155
|
+
if (probeCount === 1) {
|
|
156
|
+
// P0-1: cache-drift gate. If the entry carries a fingerprint, recompute
|
|
157
|
+
// it on the live element. Divergence → treat as stale → fall through
|
|
158
|
+
// to a fresh heal. Entries without a fingerprint (legacy v0.1.0)
|
|
159
|
+
// skip drift check and rely on the actionability gate at heal time.
|
|
160
|
+
const accept = async () => {
|
|
161
|
+
// P1-3: emit cache-hit signal for QA harness suites that need to
|
|
162
|
+
// observe whether a run came from the cache or live heal. Pure
|
|
163
|
+
// additive — production tests can ignore.
|
|
164
|
+
// intentionally swallowed: page may have navigated; not a hard error
|
|
165
|
+
await page.evaluate((sel) => {
|
|
166
|
+
window.__heal_cache_hit = { selector: sel, at: Date.now() };
|
|
167
|
+
window.dispatchEvent(new CustomEvent('__heal_cache_hit', { detail: { selector: sel } }));
|
|
168
|
+
}, cached.healedSelector).catch(() => { });
|
|
169
|
+
return cached;
|
|
170
|
+
};
|
|
171
|
+
if (cached.fingerprint) {
|
|
172
|
+
// intentionally swallowed: fingerprint extraction failure → assume drift, re-heal
|
|
173
|
+
const live = await probe.elementHandle().then((h) => h ? captureFingerprint(h) : null).catch(() => null);
|
|
174
|
+
if (live && fingerprintMatches(cached.fingerprint, live))
|
|
175
|
+
return accept();
|
|
176
|
+
// else: drift detected, fall through to overlay heal
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
return accept();
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
// CI fast-fail: no operator is available to drive the overlay, so we
|
|
184
|
+
// refuse to start the heal flow and surface a diagnostic error instead
|
|
185
|
+
// of hanging until Playwright's test timeout. Cache hits above still
|
|
186
|
+
// serve fine, which is the documented behaviour.
|
|
187
|
+
if (process.env.CI) {
|
|
188
|
+
throw new HealingError(`[heal] no-operator-in-ci — cache miss for "${original.selectorString}" in test "${testInfo.title}". ` +
|
|
189
|
+
`Run the heal interactively locally and commit the .healed-locators/ entry.`, 'no-operator-in-ci');
|
|
190
|
+
}
|
|
191
|
+
// Heal opens an interactive overlay and waits for the user to pick the
|
|
192
|
+
// element. That wait is unbounded by design. Playwright's test-level
|
|
193
|
+
// timeout (default 30s) would kill the worker mid-pick, so we disable
|
|
194
|
+
// it for the duration of the heal and restore it on exit. Playwright
|
|
195
|
+
// worker processes never have a TTY on stdin even when launched from a
|
|
196
|
+
// terminal, so we gate only on the CI env var: in CI we already exited.
|
|
197
|
+
const savedTimeout = testInfo.timeout;
|
|
198
|
+
testInfo.setTimeout(0);
|
|
199
|
+
try {
|
|
200
|
+
return await healAction({
|
|
201
|
+
page,
|
|
202
|
+
testFile: basename(testInfo.file),
|
|
203
|
+
originalSelector: original.selectorString,
|
|
204
|
+
action,
|
|
205
|
+
description: inferDescriptionFromCallsite(original.callerStack ?? new Error().stack),
|
|
206
|
+
cacheDir,
|
|
207
|
+
callerStack: original.callerStack,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
catch (e) {
|
|
211
|
+
if (e instanceof HealingError && e.code === 'circuit-open') {
|
|
212
|
+
throw new Error(`[heal] LLM circuit breaker open — too many consecutive failures. ` +
|
|
213
|
+
`Test "${testInfo.title}" cannot heal "${original.selectorString}". ` +
|
|
214
|
+
`Investigate OpenAI API status, then restart the test runner.`);
|
|
215
|
+
}
|
|
216
|
+
throw e;
|
|
217
|
+
}
|
|
218
|
+
finally {
|
|
219
|
+
testInfo.setTimeout(savedTimeout || 30000);
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
return buildLocator(page, {
|
|
223
|
+
selectorType: entry.selectorType,
|
|
224
|
+
selector: entry.healedSelector,
|
|
225
|
+
rationale: '',
|
|
226
|
+
tier: entry.tier ?? 6,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
function wrapLocatorWithHeal(loc, page, original, testInfo) {
|
|
230
|
+
if (loc[HEAL_MARKER])
|
|
231
|
+
return loc;
|
|
232
|
+
// Self-heal guard: never try to heal a locator that targets the overlay
|
|
233
|
+
// itself. Test code that programmatically drives the overlay must reach
|
|
234
|
+
// the heal host directly without recursing into the heal flow.
|
|
235
|
+
if (HEAL_INTERNAL_TOKENS.some((tok) => original.selectorString.includes(tok))) {
|
|
236
|
+
return loc;
|
|
237
|
+
}
|
|
238
|
+
// Reject empty/whitespace selectors at the proxy boundary so we never
|
|
239
|
+
// write a junk cache key like ` |click`.
|
|
240
|
+
if (original.selectorString.trim().length === 0) {
|
|
241
|
+
throw new HealingError(`empty/whitespace selector passed to page.locator() — refusing to heal`, 'config');
|
|
242
|
+
}
|
|
243
|
+
const proxy = new Proxy(loc, {
|
|
244
|
+
get(target, prop, receiver) {
|
|
245
|
+
if (prop === HEAL_MARKER)
|
|
246
|
+
return true;
|
|
247
|
+
if (prop === '__healPage')
|
|
248
|
+
return page;
|
|
249
|
+
if (prop === '__healOriginal')
|
|
250
|
+
return original;
|
|
251
|
+
if (prop === '__healTestInfo')
|
|
252
|
+
return testInfo;
|
|
253
|
+
const orig = Reflect.get(target, prop, receiver);
|
|
254
|
+
// Chain methods that return a Locator — keep wrapping, anchor to outermost selector
|
|
255
|
+
if (typeof orig === 'function' && typeof prop === 'string' && LOCATOR_CHAIN_METHODS.has(prop)) {
|
|
256
|
+
return function (...args) {
|
|
257
|
+
const child = orig.apply(target, args);
|
|
258
|
+
return wrapLocatorWithHeal(child, page, original, testInfo);
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
// Action methods — retry on TimeoutError via heal
|
|
262
|
+
if (typeof orig === 'function' && typeof prop === 'string' && ACTION_METHODS.has(prop)) {
|
|
263
|
+
return async function (...args) {
|
|
264
|
+
// P2-2: capture the caller stack at the SYNCHRONOUS entry point
|
|
265
|
+
// before any async hop or Proxy frame can swallow it. Stored on
|
|
266
|
+
// the OriginalLoc so runHealAndBuild can pass it into healAction.
|
|
267
|
+
if (!original.callerStack)
|
|
268
|
+
original.callerStack = new Error().stack;
|
|
269
|
+
const withTimeout = ensureTimeout(args);
|
|
270
|
+
try {
|
|
271
|
+
return await orig.apply(target, withTimeout);
|
|
272
|
+
}
|
|
273
|
+
catch (err) {
|
|
274
|
+
if (!isLocatorTimeoutError(err))
|
|
275
|
+
throw err;
|
|
276
|
+
// P1-2: strict-mode opt-in fast path. When the original locator
|
|
277
|
+
// matches multiple elements and HEAL_AUTO_FIRST=1, narrow to
|
|
278
|
+
// .first() without opening the overlay. Avoids a needless heal
|
|
279
|
+
// round when the test author's intent was "any matching".
|
|
280
|
+
if (isStrictModeViolation(err) && process.env.HEAL_AUTO_FIRST === '1') {
|
|
281
|
+
const firstLoc = target.first();
|
|
282
|
+
return await firstLoc[prop](...args);
|
|
283
|
+
}
|
|
284
|
+
const healed = await runHealAndBuild(page, testInfo, original, prop);
|
|
285
|
+
return await healed[prop](...args);
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
// Assertion-gate query methods — heal-on-timeout once
|
|
290
|
+
if (typeof orig === 'function' && typeof prop === 'string' && ASSERTION_GATE_METHODS.has(prop)) {
|
|
291
|
+
return async function (...args) {
|
|
292
|
+
try {
|
|
293
|
+
return await orig.apply(target, args);
|
|
294
|
+
}
|
|
295
|
+
catch (err) {
|
|
296
|
+
if (!isLocatorTimeoutError(err))
|
|
297
|
+
throw err;
|
|
298
|
+
const healed = await runHealAndBuild(page, testInfo, original, prop);
|
|
299
|
+
return await healed[prop](...args);
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
return typeof orig === 'function' ? orig.bind(target) : orig;
|
|
304
|
+
},
|
|
305
|
+
});
|
|
306
|
+
return proxy;
|
|
307
|
+
}
|
|
308
|
+
export function wrapPageWithHeal(page, testInfo) {
|
|
309
|
+
if (page[HEAL_MARKER])
|
|
310
|
+
return page;
|
|
311
|
+
const proxy = new Proxy(page, {
|
|
312
|
+
get(target, prop, receiver) {
|
|
313
|
+
if (prop === HEAL_MARKER)
|
|
314
|
+
return true;
|
|
315
|
+
const orig = Reflect.get(target, prop, receiver);
|
|
316
|
+
// page.locator / page.getBy* → return wrapped Locator
|
|
317
|
+
if (typeof orig === 'function' && typeof prop === 'string' && LOCATOR_PRODUCING_METHODS.has(prop)) {
|
|
318
|
+
return function (...args) {
|
|
319
|
+
const loc = orig.apply(target, args);
|
|
320
|
+
const original = describeArgs(prop, args);
|
|
321
|
+
return wrapLocatorWithHeal(loc, target, original, testInfo);
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
// page.click(selector, ...) and friends — wrap selector-string overloads
|
|
325
|
+
if (typeof orig === 'function' && typeof prop === 'string' && PAGE_DIRECT_ACTIONS.has(prop)) {
|
|
326
|
+
return async function (...args) {
|
|
327
|
+
if (typeof args[0] !== 'string') {
|
|
328
|
+
return orig.apply(target, args);
|
|
329
|
+
}
|
|
330
|
+
const selector = args[0];
|
|
331
|
+
const original = { selectorString: selector, methodKind: 'locator' };
|
|
332
|
+
// Use locator path so we get a Page-style timeout we can react to.
|
|
333
|
+
const loc = target.locator(selector);
|
|
334
|
+
const wrappedLoc = wrapLocatorWithHeal(loc, target, original, testInfo);
|
|
335
|
+
// Map the page-action to the equivalent locator action where possible.
|
|
336
|
+
if (prop === 'waitForSelector') {
|
|
337
|
+
const opts = args[1] ?? {};
|
|
338
|
+
return await wrappedLoc.waitFor({ timeout: 5000, ...opts });
|
|
339
|
+
}
|
|
340
|
+
const locArgs = args.slice(1);
|
|
341
|
+
return await wrappedLoc[prop](...locArgs);
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
return typeof orig === 'function' ? orig.bind(target) : orig;
|
|
345
|
+
},
|
|
346
|
+
});
|
|
347
|
+
return proxy;
|
|
348
|
+
}
|
|
349
|
+
export const test = base.extend({
|
|
350
|
+
page: async ({ page }, use, testInfo) => {
|
|
351
|
+
const wrapped = wrapPageWithHealAndTrack(page, testInfo);
|
|
352
|
+
await use(wrapped);
|
|
353
|
+
},
|
|
354
|
+
});
|
|
355
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
356
|
+
// expect wrapper — Playwright's web-first assertions (toBeVisible / toHaveText
|
|
357
|
+
// / etc.) call private Locator RPCs that the Locator-method Proxy never sees.
|
|
358
|
+
// Wrap the LocatorAssertions return so matcher rejections trigger heal +
|
|
359
|
+
// retry. Non-Locator inputs (page, primitives) pass through to baseExpect.
|
|
360
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
361
|
+
const TEST_INFO_BY_PAGE = new WeakMap();
|
|
362
|
+
// Hook into wrapPageWithHeal to remember the testInfo per wrapped page so the
|
|
363
|
+
// expect wrapper can resolve it later.
|
|
364
|
+
const origWrapPage = wrapPageWithHeal;
|
|
365
|
+
export function wrapPageWithHealAndTrack(page, testInfo) {
|
|
366
|
+
const wrapped = origWrapPage(page, testInfo);
|
|
367
|
+
TEST_INFO_BY_PAGE.set(wrapped, testInfo);
|
|
368
|
+
TEST_INFO_BY_PAGE.set(page, testInfo);
|
|
369
|
+
return wrapped;
|
|
370
|
+
}
|
|
371
|
+
function getHealOriginal(loc) {
|
|
372
|
+
if (!loc || !loc[HEAL_MARKER])
|
|
373
|
+
return null;
|
|
374
|
+
const page = loc.__healPage;
|
|
375
|
+
const original = loc.__healOriginal;
|
|
376
|
+
if (!page || !original)
|
|
377
|
+
return null;
|
|
378
|
+
const testInfo = TEST_INFO_BY_PAGE.get(page);
|
|
379
|
+
if (!testInfo)
|
|
380
|
+
return null;
|
|
381
|
+
return { page, original, testInfo };
|
|
382
|
+
}
|
|
383
|
+
function wrapAssertionChain(assertions, ctx) {
|
|
384
|
+
return new Proxy(assertions, {
|
|
385
|
+
get(target, prop, receiver) {
|
|
386
|
+
const orig = Reflect.get(target, prop, receiver);
|
|
387
|
+
// `.not` returns a new assertions object — wrap it too
|
|
388
|
+
if (prop === 'not' && orig && typeof orig === 'object') {
|
|
389
|
+
return wrapAssertionChain(orig, ctx);
|
|
390
|
+
}
|
|
391
|
+
// Any `to*` matcher: catch timeout, heal, retry on healed locator
|
|
392
|
+
if (typeof orig === 'function' && typeof prop === 'string' && /^to[A-Z]/.test(prop)) {
|
|
393
|
+
return async function (...args) {
|
|
394
|
+
try {
|
|
395
|
+
return await orig.apply(target, args);
|
|
396
|
+
}
|
|
397
|
+
catch (err) {
|
|
398
|
+
if (!isLocatorTimeoutError(err))
|
|
399
|
+
throw err;
|
|
400
|
+
const healed = await runHealAndBuild(ctx.page, ctx.testInfo, ctx.original, `expect.${prop}`);
|
|
401
|
+
// Re-run the matcher against the healed locator using baseExpect
|
|
402
|
+
return await baseExpect(healed)[prop](...args);
|
|
403
|
+
}
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
return typeof orig === 'function' ? orig.bind(target) : orig;
|
|
407
|
+
},
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
const wrappedExpect = ((value, message) => {
|
|
411
|
+
const ctx = getHealOriginal(value);
|
|
412
|
+
const assertions = message !== undefined ? baseExpect(value, message) : baseExpect(value);
|
|
413
|
+
if (!ctx)
|
|
414
|
+
return assertions;
|
|
415
|
+
return wrapAssertionChain(assertions, ctx);
|
|
416
|
+
});
|
|
417
|
+
// Preserve baseExpect static API (extend, configure, poll, etc.)
|
|
418
|
+
Object.assign(wrappedExpect, baseExpect);
|
|
419
|
+
// expect.soft(loc).toXxx() — also needs to wrap so heal fires.
|
|
420
|
+
// soft mode means failures are recorded, not thrown, but TimeoutError is still
|
|
421
|
+
// thrown by the matcher implementation when the underlying probe times out.
|
|
422
|
+
wrappedExpect.soft = (value, message) => {
|
|
423
|
+
const ctx = getHealOriginal(value);
|
|
424
|
+
const assertions = message !== undefined ? baseExpect.soft(value, message) : baseExpect.soft(value);
|
|
425
|
+
if (!ctx)
|
|
426
|
+
return assertions;
|
|
427
|
+
return wrapAssertionChain(assertions, ctx);
|
|
428
|
+
};
|
|
429
|
+
export const expect = wrappedExpect;
|
|
430
|
+
//# sourceMappingURL=auto-fixture.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auto-fixture.js","sourceRoot":"","sources":["../../healing/auto-fixture.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { CacheEntry, AuditLogEntry, CostLogEntry, HealEntryV2 } from './types.js';
|
|
2
|
+
export type { CacheEntry, AuditLogEntry, CostLogEntry };
|
|
3
|
+
export declare function cacheGet(dir: string, testFile: string, originalSelector: string): Promise<CacheEntry | null>;
|
|
4
|
+
export declare function cacheSet(dir: string, testFile: string, originalSelector: string, entry: CacheEntry): Promise<void>;
|
|
5
|
+
export declare function cacheInvalidate(dir: string, testFile: string, originalSelector: string): Promise<void>;
|
|
6
|
+
export declare function appendAuditLog(dir: string, entry: AuditLogEntry): Promise<void>;
|
|
7
|
+
export declare function appendCostLog(dir: string, entry: CostLogEntry): Promise<void>;
|
|
8
|
+
export declare function cacheV2Key(originalSelector: string, action: string): string;
|
|
9
|
+
export declare function cacheV2Get(dir: string, testFile: string, originalSelector: string, action: string): Promise<HealEntryV2 | null>;
|
|
10
|
+
export declare function cacheV2Set(dir: string, testFile: string, entry: HealEntryV2): Promise<void>;
|
|
11
|
+
//# sourceMappingURL=cache.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../../healing/cache.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,UAAU,EAAE,aAAa,EAAE,YAAY,EAAE,WAAW,EAAe,MAAM,YAAY,CAAC;AAIpG,YAAY,EAAE,UAAU,EAAE,aAAa,EAAE,YAAY,EAAE,CAAC;AA6BxD,wBAAsB,QAAQ,CAC5B,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,MAAM,EAChB,gBAAgB,EAAE,MAAM,GACvB,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAI5B;AAED,wBAAsB,QAAQ,CAC5B,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,MAAM,EAChB,gBAAgB,EAAE,MAAM,EACxB,KAAK,EAAE,UAAU,GAChB,OAAO,CAAC,IAAI,CAAC,CAKf;AAED,wBAAsB,eAAe,CACnC,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,MAAM,EAChB,gBAAgB,EAAE,MAAM,GACvB,OAAO,CAAC,IAAI,CAAC,CAMf;AAED,wBAAsB,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAMrF;AAED,wBAAsB,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAMnF;AAMD,wBAAgB,UAAU,CAAC,gBAAgB,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAE3E;AAoCD,wBAAsB,UAAU,CAC9B,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GACtE,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAmB7B;AAED,wBAAsB,UAAU,CAC9B,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW,GAChD,OAAO,CAAC,IAAI,CAAC,CASf"}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { readFile, writeFile, mkdir, rename, appendFile } from 'fs/promises';
|
|
2
|
+
import { existsSync } from 'fs';
|
|
3
|
+
import { join, basename } from 'path';
|
|
4
|
+
import { randomBytes } from 'crypto';
|
|
5
|
+
import { HealingError } from './errors.js';
|
|
6
|
+
import { HealEntryV2Schema } from './types.js';
|
|
7
|
+
function cacheFilePath(dir, testFile) {
|
|
8
|
+
return join(dir, `${basename(testFile)}.json`);
|
|
9
|
+
}
|
|
10
|
+
async function readCache(filePath) {
|
|
11
|
+
try {
|
|
12
|
+
const raw = await readFile(filePath, 'utf-8');
|
|
13
|
+
return JSON.parse(raw);
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return {};
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
async function writeCacheAtomic(filePath, dir, data) {
|
|
20
|
+
if (!existsSync(dir)) {
|
|
21
|
+
await mkdir(dir, { recursive: true });
|
|
22
|
+
}
|
|
23
|
+
const serialised = JSON.stringify(data, null, 2);
|
|
24
|
+
const tmp = `${filePath}.tmp.${process.pid}.${randomBytes(4).toString('hex')}`;
|
|
25
|
+
await writeFile(tmp, serialised, 'utf-8');
|
|
26
|
+
await rename(tmp, filePath);
|
|
27
|
+
}
|
|
28
|
+
export async function cacheGet(dir, testFile, originalSelector) {
|
|
29
|
+
const filePath = cacheFilePath(dir, testFile);
|
|
30
|
+
const data = await readCache(filePath);
|
|
31
|
+
return data[originalSelector] ?? null;
|
|
32
|
+
}
|
|
33
|
+
export async function cacheSet(dir, testFile, originalSelector, entry) {
|
|
34
|
+
const filePath = cacheFilePath(dir, testFile);
|
|
35
|
+
const data = await readCache(filePath);
|
|
36
|
+
data[originalSelector] = entry;
|
|
37
|
+
await writeCacheAtomic(filePath, dir, data);
|
|
38
|
+
}
|
|
39
|
+
export async function cacheInvalidate(dir, testFile, originalSelector) {
|
|
40
|
+
const filePath = cacheFilePath(dir, testFile);
|
|
41
|
+
const data = await readCache(filePath);
|
|
42
|
+
if (!(originalSelector in data))
|
|
43
|
+
return;
|
|
44
|
+
delete data[originalSelector];
|
|
45
|
+
await writeCacheAtomic(filePath, dir, data);
|
|
46
|
+
}
|
|
47
|
+
export async function appendAuditLog(dir, entry) {
|
|
48
|
+
if (!existsSync(dir)) {
|
|
49
|
+
await mkdir(dir, { recursive: true });
|
|
50
|
+
}
|
|
51
|
+
const logPath = join(dir, 'audit.log');
|
|
52
|
+
await appendFile(logPath, JSON.stringify(entry) + '\n', 'utf-8');
|
|
53
|
+
}
|
|
54
|
+
export async function appendCostLog(dir, entry) {
|
|
55
|
+
if (!existsSync(dir)) {
|
|
56
|
+
await mkdir(dir, { recursive: true });
|
|
57
|
+
}
|
|
58
|
+
const logPath = join(dir, 'cost.ndjson');
|
|
59
|
+
await appendFile(logPath, JSON.stringify(entry) + '\n', 'utf-8');
|
|
60
|
+
}
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
// Cache v2 — action-aware schema with source trace fields
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
export function cacheV2Key(originalSelector, action) {
|
|
65
|
+
return `${originalSelector}|${action}`;
|
|
66
|
+
}
|
|
67
|
+
async function readCacheV2(filePath) {
|
|
68
|
+
let raw;
|
|
69
|
+
try {
|
|
70
|
+
raw = await readFile(filePath, 'utf-8');
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
// file missing → empty cache (normal first-run case)
|
|
74
|
+
return { version: 2, entries: {} };
|
|
75
|
+
}
|
|
76
|
+
try {
|
|
77
|
+
const parsed = JSON.parse(raw);
|
|
78
|
+
if (parsed?.version === 2)
|
|
79
|
+
return parsed;
|
|
80
|
+
// File exists, parsed, but wrong shape — surface as invalid-cache.
|
|
81
|
+
throw new HealingError(`cache file ${filePath} has unexpected shape (expected version: 2)`, 'invalid-cache');
|
|
82
|
+
}
|
|
83
|
+
catch (e) {
|
|
84
|
+
if (e instanceof HealingError)
|
|
85
|
+
throw e;
|
|
86
|
+
// JSON parse error — surface so user knows the file is corrupted.
|
|
87
|
+
process.stderr.write(`[HEAL] cache file ${filePath} parse error: ${e.message}\n`);
|
|
88
|
+
throw new HealingError(`cache file ${filePath} JSON parse failed: ${e.message}`, 'invalid-cache');
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
async function writeCacheV2Atomic(filePath, dir, data) {
|
|
92
|
+
if (!existsSync(dir))
|
|
93
|
+
await mkdir(dir, { recursive: true });
|
|
94
|
+
const tmp = `${filePath}.tmp.${process.pid}.${randomBytes(4).toString('hex')}`;
|
|
95
|
+
await writeFile(tmp, JSON.stringify(data, null, 2), 'utf-8');
|
|
96
|
+
await rename(tmp, filePath);
|
|
97
|
+
}
|
|
98
|
+
export async function cacheV2Get(dir, testFile, originalSelector, action) {
|
|
99
|
+
const file = cacheFilePath(dir, testFile);
|
|
100
|
+
const data = await readCacheV2(file);
|
|
101
|
+
const entry = data.entries[cacheV2Key(originalSelector, action)];
|
|
102
|
+
if (!entry)
|
|
103
|
+
return null;
|
|
104
|
+
// Legacy entries (pre-v2.1) lack `tier`; default to 6 (attribute CSS).
|
|
105
|
+
const normalized = {
|
|
106
|
+
...entry,
|
|
107
|
+
tier: typeof entry.tier === 'number' ? entry.tier : 6,
|
|
108
|
+
};
|
|
109
|
+
const parsed = HealEntryV2Schema.safeParse(normalized);
|
|
110
|
+
if (!parsed.success) {
|
|
111
|
+
// Malformed entry — log to stderr but return null so heal proceeds fresh.
|
|
112
|
+
process.stderr.write(`[HEAL] cache entry rejected for ${testFile} key=${cacheV2Key(originalSelector, action)}: ${parsed.error.message}\n`);
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
return parsed.data;
|
|
116
|
+
}
|
|
117
|
+
export async function cacheV2Set(dir, testFile, entry) {
|
|
118
|
+
// Tier 7 (js eval) is an audit-only escape hatch — never cache it.
|
|
119
|
+
// Also reject legacy tier 9 from older HealEntryV2 schema for safety.
|
|
120
|
+
if (entry.tier === 7 || entry.tier === 9)
|
|
121
|
+
return;
|
|
122
|
+
HealEntryV2Schema.parse(entry);
|
|
123
|
+
const file = cacheFilePath(dir, testFile);
|
|
124
|
+
const data = await readCacheV2(file);
|
|
125
|
+
data.entries[cacheV2Key(entry.originalSelector, entry.action)] = entry;
|
|
126
|
+
await writeCacheV2Atomic(file, dir, data);
|
|
127
|
+
}
|
|
128
|
+
//# sourceMappingURL=cache.js.map
|