sela-core 0.1.0-alpha.45 → 0.1.0-alpha.46
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/config/SelaConfig.d.ts +54 -0
- package/dist/config/SelaConfig.d.ts.map +1 -1
- package/dist/config/SelaConfig.js +38 -0
- package/dist/engine/SelaEngine.d.ts +8 -0
- package/dist/engine/SelaEngine.d.ts.map +1 -1
- package/dist/engine/SelaEngine.js +55 -0
- package/dist/errors/FailureClassifier.d.ts +20 -0
- package/dist/errors/FailureClassifier.d.ts.map +1 -1
- package/dist/errors/FailureClassifier.js +31 -2
- package/dist/fixtures/index.d.ts +26 -1
- package/dist/fixtures/index.d.ts.map +1 -1
- package/dist/fixtures/index.js +316 -86
- package/dist/fixtures/proxyTag.d.ts +1 -1
- package/dist/fixtures/proxyTag.d.ts.map +1 -1
- package/dist/fixtures/proxyTag.js +2 -0
- package/dist/services/TestDeadline.d.ts +59 -0
- package/dist/services/TestDeadline.d.ts.map +1 -0
- package/dist/services/TestDeadline.js +103 -0
- package/dist/utils/DOMUtils.d.ts.map +1 -1
- package/dist/utils/DOMUtils.js +13 -0
- package/package.json +2 -1
|
@@ -296,6 +296,40 @@ export interface SelaConfig {
|
|
|
296
296
|
* env vars (backward-compatible with all existing setups).
|
|
297
297
|
*/
|
|
298
298
|
llm?: LLMConfig;
|
|
299
|
+
/**
|
|
300
|
+
* Deadline-aware healing timing (RFC timing-interception). Controls the
|
|
301
|
+
* first-attempt budget tightening near the test deadline (Mechanism A) and
|
|
302
|
+
* the anti-guillotine test-timeout extension that buys healing runway
|
|
303
|
+
* (Mechanism B). Omit for the defaults; all fields are individually
|
|
304
|
+
* overridable and the whole feature is kill-switchable via SELA_TIMING_DISABLE.
|
|
305
|
+
*/
|
|
306
|
+
timing?: SelaTimingConfig;
|
|
307
|
+
}
|
|
308
|
+
/** Deadline-aware healing timing (RFC timing-interception §8). */
|
|
309
|
+
export interface SelaTimingConfig {
|
|
310
|
+
/**
|
|
311
|
+
* Extend the running test's total timeout when a heal begins, so DOM
|
|
312
|
+
* extraction + LLM + verified retry complete before the native guillotine.
|
|
313
|
+
* Default: true.
|
|
314
|
+
*/
|
|
315
|
+
extendTestTimeout?: boolean;
|
|
316
|
+
/**
|
|
317
|
+
* Safety margin (ms) subtracted from the remaining test time when Sela
|
|
318
|
+
* tightens the first-attempt budget, guaranteeing the action fails — handing
|
|
319
|
+
* control to the heal pipeline — strictly BEFORE the page is torn down.
|
|
320
|
+
* Default: 2000.
|
|
321
|
+
*/
|
|
322
|
+
deadlineBufferMs?: number;
|
|
323
|
+
/** Milliseconds of fresh runway added to the test timeout per heal. Default: 30000. */
|
|
324
|
+
healRunwayMs?: number;
|
|
325
|
+
/**
|
|
326
|
+
* Floor (ms) below which the first-attempt budget is never tightened — keeps
|
|
327
|
+
* auto-wait able to distinguish "slow to appear" from "genuinely gone".
|
|
328
|
+
* Default: 1000.
|
|
329
|
+
*/
|
|
330
|
+
minActionFloorMs?: number;
|
|
331
|
+
/** Max test-timeout extensions per test (bounds a multi-heal test). Default: 3. */
|
|
332
|
+
maxExtensionsPerTest?: number;
|
|
299
333
|
}
|
|
300
334
|
export interface ResolvedVectorThresholds {
|
|
301
335
|
/** Minimum DNA Architectural Match score (0–100). null = disabled. */
|
|
@@ -448,6 +482,18 @@ export interface ResolvedConfig {
|
|
|
448
482
|
* `undefined` when not set.
|
|
449
483
|
*/
|
|
450
484
|
regressionManifestPath: string | undefined;
|
|
485
|
+
/** Resolved deadline-aware healing timing (always defaulted). */
|
|
486
|
+
timing: ResolvedTiming;
|
|
487
|
+
}
|
|
488
|
+
/** Resolved deadline-aware healing timing (RFC timing-interception §8). */
|
|
489
|
+
export interface ResolvedTiming {
|
|
490
|
+
/** Master switch. `false` (via SELA_TIMING_DISABLE) ⇒ pure legacy pass-through. */
|
|
491
|
+
enabled: boolean;
|
|
492
|
+
extendTestTimeout: boolean;
|
|
493
|
+
deadlineBufferMs: number;
|
|
494
|
+
healRunwayMs: number;
|
|
495
|
+
minActionFloorMs: number;
|
|
496
|
+
maxExtensionsPerTest: number;
|
|
451
497
|
}
|
|
452
498
|
export declare function resolveThresholds(config: SelaConfig): ResolvedThresholds;
|
|
453
499
|
export declare function parseEnvBoolean(varName: string, rawValue: string | undefined): boolean;
|
|
@@ -470,6 +516,14 @@ export declare function resolveSmartPrune(configValue: boolean | undefined, envV
|
|
|
470
516
|
* Trust-Engine refusal semantics. undefined => 5000.
|
|
471
517
|
*/
|
|
472
518
|
export declare function resolveLargeDomNodeThreshold(value: number | undefined): number;
|
|
519
|
+
/**
|
|
520
|
+
* Resolves deadline-aware healing timing. Env wins over config. A positive-
|
|
521
|
+
* integer env override is required for the numeric knobs (fail-fast on
|
|
522
|
+
* garbage, mirroring resolveLargeDomNodeThreshold). Master kill switch:
|
|
523
|
+
* SELA_TIMING_DISABLE=true ⇒ enabled:false ⇒ the proxy stays pure legacy
|
|
524
|
+
* pass-through (no injection, no extension).
|
|
525
|
+
*/
|
|
526
|
+
export declare function resolveTiming(config: SelaConfig, env?: NodeJS.ProcessEnv): ResolvedTiming;
|
|
473
527
|
export interface ResolvedHealingConfig {
|
|
474
528
|
enabled: boolean;
|
|
475
529
|
exclude: string[];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SelaConfig.d.ts","sourceRoot":"","sources":["../../src/config/SelaConfig.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"SelaConfig.d.ts","sourceRoot":"","sources":["../../src/config/SelaConfig.ts"],"names":[],"mappings":"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"}
|
|
@@ -7,6 +7,7 @@ exports.parseEnvBoolean = parseEnvBoolean;
|
|
|
7
7
|
exports.resolveMaxHealsPerRun = resolveMaxHealsPerRun;
|
|
8
8
|
exports.resolveSmartPrune = resolveSmartPrune;
|
|
9
9
|
exports.resolveLargeDomNodeThreshold = resolveLargeDomNodeThreshold;
|
|
10
|
+
exports.resolveTiming = resolveTiming;
|
|
10
11
|
exports.resolveHealing = resolveHealing;
|
|
11
12
|
exports.resolveConfig = resolveConfig;
|
|
12
13
|
const ArtifactNaming_1 = require("../services/ArtifactNaming");
|
|
@@ -214,6 +215,42 @@ function resolveLargeDomNodeThreshold(value) {
|
|
|
214
215
|
}
|
|
215
216
|
return value;
|
|
216
217
|
}
|
|
218
|
+
/**
|
|
219
|
+
* Resolves deadline-aware healing timing. Env wins over config. A positive-
|
|
220
|
+
* integer env override is required for the numeric knobs (fail-fast on
|
|
221
|
+
* garbage, mirroring resolveLargeDomNodeThreshold). Master kill switch:
|
|
222
|
+
* SELA_TIMING_DISABLE=true ⇒ enabled:false ⇒ the proxy stays pure legacy
|
|
223
|
+
* pass-through (no injection, no extension).
|
|
224
|
+
*/
|
|
225
|
+
function resolveTiming(config, env = process.env) {
|
|
226
|
+
const t = config.timing ?? {};
|
|
227
|
+
const numeric = (envName, rawValue, configValue, fallback) => {
|
|
228
|
+
if (rawValue !== undefined) {
|
|
229
|
+
const n = Number(rawValue.trim());
|
|
230
|
+
if (!Number.isInteger(n) || n < 0) {
|
|
231
|
+
throw new Error(`[Sela] Invalid value for env var "${envName}": "${rawValue}". ` +
|
|
232
|
+
`Expected a non-negative integer.`);
|
|
233
|
+
}
|
|
234
|
+
return n;
|
|
235
|
+
}
|
|
236
|
+
if (configValue !== undefined) {
|
|
237
|
+
if (typeof configValue !== "number" || !Number.isInteger(configValue) || configValue < 0) {
|
|
238
|
+
throw new Error(`[Sela] Invalid timing value: ${JSON.stringify(configValue)}. ` +
|
|
239
|
+
`Expected a non-negative integer.`);
|
|
240
|
+
}
|
|
241
|
+
return configValue;
|
|
242
|
+
}
|
|
243
|
+
return fallback;
|
|
244
|
+
};
|
|
245
|
+
return {
|
|
246
|
+
enabled: !parseEnvBoolean("SELA_TIMING_DISABLE", env.SELA_TIMING_DISABLE),
|
|
247
|
+
extendTestTimeout: t.extendTestTimeout ?? true,
|
|
248
|
+
deadlineBufferMs: numeric("SELA_DEADLINE_BUFFER_MS", env.SELA_DEADLINE_BUFFER_MS, t.deadlineBufferMs, 2000),
|
|
249
|
+
healRunwayMs: numeric("SELA_HEAL_RUNWAY_MS", env.SELA_HEAL_RUNWAY_MS, t.healRunwayMs, 30000),
|
|
250
|
+
minActionFloorMs: numeric("SELA_MIN_ACTION_FLOOR_MS", env.SELA_MIN_ACTION_FLOOR_MS, t.minActionFloorMs, 1000),
|
|
251
|
+
maxExtensionsPerTest: numeric(undefined, undefined, t.maxExtensionsPerTest, 3),
|
|
252
|
+
};
|
|
253
|
+
}
|
|
217
254
|
const CRITICALITY_TIERS = new Set([
|
|
218
255
|
"critical",
|
|
219
256
|
"high",
|
|
@@ -311,5 +348,6 @@ function resolveConfig(config, env = process.env) {
|
|
|
311
348
|
bypass,
|
|
312
349
|
regressionActive,
|
|
313
350
|
regressionManifestPath,
|
|
351
|
+
timing: resolveTiming(config, env),
|
|
314
352
|
};
|
|
315
353
|
}
|
|
@@ -68,6 +68,14 @@ export declare class SelaEngine {
|
|
|
68
68
|
/** Override for the human explanation (e.g. governance locks). */
|
|
69
69
|
explanation?: string;
|
|
70
70
|
}): void;
|
|
71
|
+
/**
|
|
72
|
+
* Teardown-race refusal (Mechanism C, RFC timing-interception §7). Records a
|
|
73
|
+
* FAILED report event when DOM extraction is impossible because the page is
|
|
74
|
+
* already closed, then lets heal() return "" so the caller rethrows the
|
|
75
|
+
* original Playwright error. Never throws — transparency must not itself
|
|
76
|
+
* crash the surfaced failure.
|
|
77
|
+
*/
|
|
78
|
+
private _recordTeardownRefusal;
|
|
71
79
|
heal(page: Page, fullSelector: string, elementSelectorOnly: string, stableId: string, filePath: string, line: number, healMode?: HealMode, originalMethod?: string, ancestryContext?: {
|
|
72
80
|
descendantChain: string;
|
|
73
81
|
}, failure?: FailureEvidence): Promise<string>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SelaEngine.d.ts","sourceRoot":"","sources":["../../src/engine/SelaEngine.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAsBxC,OAAO,EAAE,iBAAiB,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"SelaEngine.d.ts","sourceRoot":"","sources":["../../src/engine/SelaEngine.ts"],"names":[],"mappings":"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"}
|
|
@@ -47,6 +47,7 @@ const SelaLifecycleStateManager_1 = require("./lifecycle/SelaLifecycleStateManag
|
|
|
47
47
|
const DOMUtils_1 = require("../utils/DOMUtils");
|
|
48
48
|
const SmartDOMPruner_1 = require("../services/SmartDOMPruner");
|
|
49
49
|
const sourcePathGuard_1 = require("../utils/sourcePathGuard");
|
|
50
|
+
const FailureClassifier_1 = require("../errors/FailureClassifier");
|
|
50
51
|
const SelectorForensics_1 = require("../errors/SelectorForensics");
|
|
51
52
|
const BusinessContextParser_1 = require("../services/confidence/BusinessContextParser");
|
|
52
53
|
const LedgerAnchor_1 = require("../services/LedgerAnchor");
|
|
@@ -361,6 +362,39 @@ CRITICAL: Do NOT return a CSS selector like "#my-select". Return plain text only
|
|
|
361
362
|
logger_1.logger.debug(`recordClassificationRefusal skipped: ${err?.message}`);
|
|
362
363
|
}
|
|
363
364
|
}
|
|
365
|
+
/**
|
|
366
|
+
* Teardown-race refusal (Mechanism C, RFC timing-interception §7). Records a
|
|
367
|
+
* FAILED report event when DOM extraction is impossible because the page is
|
|
368
|
+
* already closed, then lets heal() return "" so the caller rethrows the
|
|
369
|
+
* original Playwright error. Never throws — transparency must not itself
|
|
370
|
+
* crash the surfaced failure.
|
|
371
|
+
*/
|
|
372
|
+
_recordTeardownRefusal(stableId, reportFile, line, elementSelectorOnly, testTitle, lastKnownStateForReport) {
|
|
373
|
+
const reason = "TEARDOWN_RACE — page/context closed before heal could extract the DOM " +
|
|
374
|
+
"(no runway: the broken action rode the test-level deadline). Original " +
|
|
375
|
+
"failure surfaces untouched.";
|
|
376
|
+
logger_1.logger.warn(`[Sela] Heal refused (${reason})`);
|
|
377
|
+
try {
|
|
378
|
+
HealReportService_1.sharedHealReport.record({
|
|
379
|
+
kind: "FAILED",
|
|
380
|
+
stableId,
|
|
381
|
+
deterministicKey: (0, HealReportService_1.computeEventDeterministicKey)(reportFile, line, elementSelectorOnly),
|
|
382
|
+
sourceFile: reportFile,
|
|
383
|
+
sourceLine: line,
|
|
384
|
+
testTitle,
|
|
385
|
+
timestamp: new Date().toISOString(),
|
|
386
|
+
framePath: [],
|
|
387
|
+
inIframe: false,
|
|
388
|
+
inShadowDom: false,
|
|
389
|
+
oldSelector: elementSelectorOnly,
|
|
390
|
+
reason,
|
|
391
|
+
dnaBefore: (0, HealReportService_1.summariseSnapshot)(lastKnownStateForReport),
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
catch (err) {
|
|
395
|
+
logger_1.logger.debug(`_recordTeardownRefusal skipped: ${err?.message}`);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
364
398
|
async heal(page, fullSelector, elementSelectorOnly, stableId, filePath, line, healMode = "action", originalMethod,
|
|
365
399
|
// Set by the Ancestry Lineage Engine when the broken selector is an
|
|
366
400
|
// ANCESTOR/scope of a chained locator. The verified descendant chain acts
|
|
@@ -527,6 +561,17 @@ CRITICAL: Do NOT return a CSS selector like "#my-select". Return plain text only
|
|
|
527
561
|
const dnaAncestry = lastKnownState?.ancestry ?? [];
|
|
528
562
|
const dnaAnchors = lastKnownState?.anchors;
|
|
529
563
|
logger_1.logger.debug("Step 2: extracting neighborhood DOM...");
|
|
564
|
+
// ── Teardown guard (Mechanism C, RFC timing-interception §7) ──────
|
|
565
|
+
// If the page/context is already gone, DOM extraction cannot run. This is
|
|
566
|
+
// the "guillotine race": with no actionTimeout the broken action rode the
|
|
567
|
+
// test-level deadline, Playwright fired it and tore the page down, and the
|
|
568
|
+
// heal fires against a corpse. Refuse deterministically here — record a
|
|
569
|
+
// teardown FAILED event and return "" so the caller rethrows the ORIGINAL
|
|
570
|
+
// Playwright error. Never crash the worker on a closed page.
|
|
571
|
+
if (typeof page.isClosed === "function" && page.isClosed()) {
|
|
572
|
+
this._recordTeardownRefusal(stableId, reportFile, line, elementSelectorOnly, testTitle, lastKnownStateForReport);
|
|
573
|
+
return "";
|
|
574
|
+
}
|
|
530
575
|
// ── Semantic DOM pruning (primary extractor) ──────────────────────
|
|
531
576
|
// SmartDOMPruner builds a candidate-ranked, repeat-collapsed micro-
|
|
532
577
|
// context in ONE evaluate pass (open shadow + same-origin iframes
|
|
@@ -587,6 +632,16 @@ CRITICAL: Do NOT return a CSS selector like "#my-select". Return plain text only
|
|
|
587
632
|
}
|
|
588
633
|
}
|
|
589
634
|
catch (pruneErr) {
|
|
635
|
+
// Teardown guard (Mechanism C): the legacy fallback below runs
|
|
636
|
+
// another page.evaluate. If the pruner threw because the page is
|
|
637
|
+
// gone, that fallback throws again and escapes heal(). Detect the
|
|
638
|
+
// teardown shape (or a now-closed page) and refuse cleanly instead
|
|
639
|
+
// of cascading the crash into the legacy extractor.
|
|
640
|
+
if ((0, FailureClassifier_1.isTeardownError)(pruneErr) ||
|
|
641
|
+
(typeof page.isClosed === "function" && page.isClosed())) {
|
|
642
|
+
this._recordTeardownRefusal(stableId, reportFile, line, elementSelectorOnly, testTitle, lastKnownStateForReport);
|
|
643
|
+
return "";
|
|
644
|
+
}
|
|
590
645
|
logger_1.logger.warn(`SmartPrune failed (${pruneErr?.message ?? pruneErr}) — ` +
|
|
591
646
|
`falling back to legacy neighborhood extractor.`);
|
|
592
647
|
}
|
|
@@ -38,6 +38,26 @@ export declare const TEARDOWN_SIGNATURES: readonly ["Target page, context or bro
|
|
|
38
38
|
export declare function summariseFailureMessage(err: unknown): string;
|
|
39
39
|
/** True when the failure is a post-teardown race — never heal, never retry. */
|
|
40
40
|
export declare function isTeardownError(err: unknown): boolean;
|
|
41
|
+
/** The timeout value the user WOULD have seen had Sela not tightened the budget. */
|
|
42
|
+
export type UserPerceivedTimeout = {
|
|
43
|
+
kind: "action";
|
|
44
|
+
ms: number;
|
|
45
|
+
} | {
|
|
46
|
+
kind: "test";
|
|
47
|
+
ms: number;
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* Rewrites a timeout error's headline to the user-perceived timeout, hiding the
|
|
51
|
+
* fact that Sela injected a tightened first-attempt budget (Mechanism A). ONLY
|
|
52
|
+
* the headline token is replaced — the call log, stack, and every other line
|
|
53
|
+
* survive verbatim, so the surfaced error is indistinguishable from what native
|
|
54
|
+
* Playwright would have produced. No-op for non-Error values, non-timeout
|
|
55
|
+
* messages, or a non-finite target (in which case no injection occurred).
|
|
56
|
+
*
|
|
57
|
+
* · explicit user arg timeout → "Timeout <ms>ms exceeded" (action shape)
|
|
58
|
+
* · relied on global deadline → "Test timeout of <ms>ms exceeded" (test shape)
|
|
59
|
+
*/
|
|
60
|
+
export declare function reshapeTimeoutError(err: unknown, perceived: UserPerceivedTimeout): unknown;
|
|
41
61
|
/**
|
|
42
62
|
* Maps a category to heal eligibility under the given trigger policy.
|
|
43
63
|
* Only INTERACTION_BLOCKED is policy-sensitive (decision: refuse by default,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FailureClassifier.d.ts","sourceRoot":"","sources":["../../src/errors/FailureClassifier.ts"],"names":[],"mappings":"AA2BA,MAAM,MAAM,eAAe,GACvB,uBAAuB,GACvB,sBAAsB,GACtB,qBAAqB,GACrB,gBAAgB,GAChB,oBAAoB,GACpB,mBAAmB,GACnB,eAAe,GACf,eAAe,CAAC;AAEpB,oEAAoE;AACpE,MAAM,WAAW,aAAa;IAC5B,kBAAkB,CAAC,EAAE,QAAQ,GAAG,OAAO,CAAC;CACzC;AAED,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,QAAQ,GAAG,WAAW,GAAG,YAAY,CAAC;IAC5C,0EAA0E;IAC1E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,0EAA0E;IAC1E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,8EAA8E;IAC9E,OAAO,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAA;KAAE,GAAG,IAAI,CAAC;IAClD,IAAI,CAAC,EAAE;QAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;KAAE,GAAG,IAAI,CAAC;IAC1C,6DAA6D;IAC7D,MAAM,CAAC,EAAE,aAAa,CAAC;CACxB;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,eAAe,CAAC;IAC1B,YAAY,EAAE,OAAO,CAAC;IACtB,2EAA2E;IAC3E,MAAM,EAAE,MAAM,CAAC;IACf,yDAAyD;IACzD,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,SAAS,EAAE,OAAO,CAAC;IACnB,WAAW,CAAC,EAAE,UAAU,GAAG,QAAQ,GAAG,aAAa,CAAC;CACrD;AAQD,eAAO,MAAM,mBAAmB,uHAItB,CAAC;
|
|
1
|
+
{"version":3,"file":"FailureClassifier.d.ts","sourceRoot":"","sources":["../../src/errors/FailureClassifier.ts"],"names":[],"mappings":"AA2BA,MAAM,MAAM,eAAe,GACvB,uBAAuB,GACvB,sBAAsB,GACtB,qBAAqB,GACrB,gBAAgB,GAChB,oBAAoB,GACpB,mBAAmB,GACnB,eAAe,GACf,eAAe,CAAC;AAEpB,oEAAoE;AACpE,MAAM,WAAW,aAAa;IAC5B,kBAAkB,CAAC,EAAE,QAAQ,GAAG,OAAO,CAAC;CACzC;AAED,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,QAAQ,GAAG,WAAW,GAAG,YAAY,CAAC;IAC5C,0EAA0E;IAC1E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,0EAA0E;IAC1E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,8EAA8E;IAC9E,OAAO,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAA;KAAE,GAAG,IAAI,CAAC;IAClD,IAAI,CAAC,EAAE;QAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;KAAE,GAAG,IAAI,CAAC;IAC1C,6DAA6D;IAC7D,MAAM,CAAC,EAAE,aAAa,CAAC;CACxB;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,eAAe,CAAC;IAC1B,YAAY,EAAE,OAAO,CAAC;IACtB,2EAA2E;IAC3E,MAAM,EAAE,MAAM,CAAC;IACf,yDAAyD;IACzD,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,SAAS,EAAE,OAAO,CAAC;IACnB,WAAW,CAAC,EAAE,UAAU,GAAG,QAAQ,GAAG,aAAa,CAAC;CACrD;AAQD,eAAO,MAAM,mBAAmB,uHAItB,CAAC;AAqDX;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,CAM5D;AAED,+EAA+E;AAC/E,wBAAgB,eAAe,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAGrD;AAMD,oFAAoF;AACpF,MAAM,MAAM,oBAAoB,GAC5B;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,GAC9B;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,CAAC;AAMjC;;;;;;;;;;GAUG;AACH,wBAAgB,mBAAmB,CACjC,GAAG,EAAE,OAAO,EACZ,SAAS,EAAE,oBAAoB,GAC9B,OAAO,CAST;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,eAAe,EACzB,MAAM,CAAC,EAAE,aAAa,GACrB,OAAO,CAUT;AAkID;;;;GAIG;AACH,wBAAgB,mBAAmB,CACjC,GAAG,EAAE,OAAO,EACZ,GAAG,EAAE,qBAAqB,GACzB,cAAc,CAEhB;AAID;;;;;;;GAOG;AACH,wBAAsB,eAAe,CACnC,GAAG,EAAE,OAAO,EACZ,GAAG,EAAE,qBAAqB,GACzB,OAAO,CAAC,cAAc,CAAC,CAuDzB"}
|
|
@@ -29,6 +29,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
29
29
|
exports.TEARDOWN_SIGNATURES = void 0;
|
|
30
30
|
exports.summariseFailureMessage = summariseFailureMessage;
|
|
31
31
|
exports.isTeardownError = isTeardownError;
|
|
32
|
+
exports.reshapeTimeoutError = reshapeTimeoutError;
|
|
32
33
|
exports.isVerdictHealEligible = isVerdictHealEligible;
|
|
33
34
|
exports.classifyFailureSync = classifyFailureSync;
|
|
34
35
|
exports.classifyFailure = classifyFailure;
|
|
@@ -45,8 +46,12 @@ exports.TEARDOWN_SIGNATURES = [
|
|
|
45
46
|
// Playwright timeout shapes. Matched on message (not only error.name) so
|
|
46
47
|
// synthetic timeouts from test harnesses and errors crossing dual playwright
|
|
47
48
|
// module instances (consumer installs loading a second copy of playwright)
|
|
48
|
-
// classify identically.
|
|
49
|
-
|
|
49
|
+
// classify identically. The optional "of" covers the test-level deadline
|
|
50
|
+
// shape ("Test timeout of 30000ms exceeded") alongside the locator-action
|
|
51
|
+
// shape ("Timeout 30000ms exceeded"); without it a test-level timeout — the
|
|
52
|
+
// common case when the ONLY action in a test hangs — fell through to
|
|
53
|
+
// INFRA_UNKNOWN and was wrongly refused.
|
|
54
|
+
const ACTION_TIMEOUT = /timeout\s+(?:of\s+)?\d+\s*ms exceeded/i;
|
|
50
55
|
const ASSERTION_TIMEOUT = /timed out \d+\s*ms waiting for expect/i;
|
|
51
56
|
// Navigation / network failures — checked BEFORE the timeout branch because a
|
|
52
57
|
// page.goto timeout is timeout-shaped but is never a selector problem.
|
|
@@ -97,6 +102,30 @@ function isTeardownError(err) {
|
|
|
97
102
|
const msg = messageOf(err);
|
|
98
103
|
return exports.TEARDOWN_SIGNATURES.some((sig) => msg.includes(sig));
|
|
99
104
|
}
|
|
105
|
+
// Matches either native Playwright timeout headline shape, first occurrence.
|
|
106
|
+
const TIMEOUT_HEADLINE = /Test timeout of\s+\d+\s*ms exceeded|Timeout\s+\d+\s*ms exceeded/i;
|
|
107
|
+
/**
|
|
108
|
+
* Rewrites a timeout error's headline to the user-perceived timeout, hiding the
|
|
109
|
+
* fact that Sela injected a tightened first-attempt budget (Mechanism A). ONLY
|
|
110
|
+
* the headline token is replaced — the call log, stack, and every other line
|
|
111
|
+
* survive verbatim, so the surfaced error is indistinguishable from what native
|
|
112
|
+
* Playwright would have produced. No-op for non-Error values, non-timeout
|
|
113
|
+
* messages, or a non-finite target (in which case no injection occurred).
|
|
114
|
+
*
|
|
115
|
+
* · explicit user arg timeout → "Timeout <ms>ms exceeded" (action shape)
|
|
116
|
+
* · relied on global deadline → "Test timeout of <ms>ms exceeded" (test shape)
|
|
117
|
+
*/
|
|
118
|
+
function reshapeTimeoutError(err, perceived) {
|
|
119
|
+
if (!(err instanceof Error) || !Number.isFinite(perceived.ms))
|
|
120
|
+
return err;
|
|
121
|
+
if (!TIMEOUT_HEADLINE.test(err.message))
|
|
122
|
+
return err;
|
|
123
|
+
const replacement = perceived.kind === "action"
|
|
124
|
+
? `Timeout ${perceived.ms}ms exceeded`
|
|
125
|
+
: `Test timeout of ${perceived.ms}ms exceeded`;
|
|
126
|
+
err.message = err.message.replace(TIMEOUT_HEADLINE, replacement);
|
|
127
|
+
return err;
|
|
128
|
+
}
|
|
100
129
|
/**
|
|
101
130
|
* Maps a category to heal eligibility under the given trigger policy.
|
|
102
131
|
* Only INTERACTION_BLOCKED is policy-sensitive (decision: refuse by default,
|
package/dist/fixtures/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Locator, Page } from "@playwright/test";
|
|
1
|
+
import { Browser, BrowserContext, Locator, Page } from "@playwright/test";
|
|
2
2
|
import { createHealingExpect } from "./expectProxy";
|
|
3
3
|
import { LineageLink } from "./lineage";
|
|
4
4
|
export declare function resolveSelaProxy(value: unknown): Locator | null;
|
|
@@ -8,8 +8,33 @@ export declare function createFrameLocatorProxy(rawFrameLocator: any, frameSelec
|
|
|
8
8
|
export declare function createLocatorProxy(rawLocator: Locator, selector: string, rawPage: Page, testTitle: string, actionCounter: {
|
|
9
9
|
value: number;
|
|
10
10
|
}, elementSelectorOnly?: string, originalMethod?: string, lineage?: LineageLink | null): any;
|
|
11
|
+
interface SelaTestBinding {
|
|
12
|
+
/** Test title threaded into every locator proxy for stableId derivation. */
|
|
13
|
+
title: string;
|
|
14
|
+
/** Monotonic per-test action index (shared across every page in the test). */
|
|
15
|
+
actionCounter: {
|
|
16
|
+
value: number;
|
|
17
|
+
};
|
|
18
|
+
/** Registry unsubscribe callbacks drained in the auto-fixture teardown. */
|
|
19
|
+
unsubscribers: Array<() => void>;
|
|
20
|
+
/** Governance full-bypass: hand out native objects with zero interception. */
|
|
21
|
+
optedOut: boolean;
|
|
22
|
+
}
|
|
23
|
+
/** Live binding getter used in production; overridable in unit tests. */
|
|
24
|
+
type BindingGetter = () => SelaTestBinding | null;
|
|
25
|
+
export declare function buildLivePageProxy(rawPage: Page, binding: SelaTestBinding): Page;
|
|
26
|
+
export declare function buildLiveContextProxy(rawContext: BrowserContext, getBinding?: BindingGetter): BrowserContext;
|
|
27
|
+
export declare function buildLiveBrowserProxy(rawBrowser: Browser, getBinding?: BindingGetter): Browser;
|
|
11
28
|
export declare const test: import("@playwright/test").TestType<import("@playwright/test").PlaywrightTestArgs & import("@playwright/test").PlaywrightTestOptions & {
|
|
12
29
|
expect: ReturnType<typeof createHealingExpect>;
|
|
30
|
+
/**
|
|
31
|
+
* Auto test-scoped binding anchor. Runs for EVERY test (auto) so the
|
|
32
|
+
* per-test binding + TestDeadline arming happen regardless of how — or
|
|
33
|
+
* whether — the `page` fixture is requested. This is what lets the
|
|
34
|
+
* worker-scoped browser proxy wire a manually-allocated page (Pattern C)
|
|
35
|
+
* to the current test.
|
|
36
|
+
*/
|
|
37
|
+
_selaTestBinding: void;
|
|
13
38
|
}, import("@playwright/test").PlaywrightWorkerArgs & import("@playwright/test").PlaywrightWorkerOptions & {
|
|
14
39
|
/**
|
|
15
40
|
* Worker-scoped lifecycle anchor. Its teardown runs EXACTLY ONCE per
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/fixtures/index.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/fixtures/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,OAAO,EACP,cAAc,EACd,OAAO,EACP,IAAI,EACL,MAAM,kBAAkB,CAAC;AAK1B,OAAO,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAEpD,OAAO,EACL,WAAW,EAIZ,MAAM,WAAW,CAAC;AAsDnB,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,GAAG,IAAI,CAI/D;AA46BD,wBAAgB,uBAAuB,CACrC,eAAe,EAAE,GAAG,EACpB,aAAa,EAAE,MAAM,EACrB,OAAO,EAAE,IAAI,EACb,SAAS,EAAE,MAAM,EACjB,aAAa,EAAE;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,EAChC,WAAW,EAAE,MAAM,EAEnB,OAAO,GAAE,WAAW,GAAG,IAAW,GACjC,GAAG,CA0CL;AAaD,wBAAgB,kBAAkB,CAChC,UAAU,EAAE,OAAO,EACnB,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,IAAI,EACb,SAAS,EAAE,MAAM,EACjB,aAAa,EAAE;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,EAChC,mBAAmB,CAAC,EAAE,MAAM,EAK5B,cAAc,CAAC,EAAE,MAAM,EAMvB,OAAO,GAAE,WAAW,GAAG,IAAW,GACjC,GAAG,CAoNL;AAeD,UAAU,eAAe;IACvB,4EAA4E;IAC5E,KAAK,EAAE,MAAM,CAAC;IACd,8EAA8E;IAC9E,aAAa,EAAE;QAAE,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;IACjC,2EAA2E;IAC3E,aAAa,EAAE,KAAK,CAAC,MAAM,IAAI,CAAC,CAAC;IACjC,8EAA8E;IAC9E,QAAQ,EAAE,OAAO,CAAC;CACnB;AAID,yEAAyE;AACzE,KAAK,aAAa,GAAG,MAAM,eAAe,GAAG,IAAI,CAAC;AAalD,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,IAAI,EACb,OAAO,EAAE,eAAe,GACvB,IAAI,CAwEN;AAYD,wBAAgB,qBAAqB,CACnC,UAAU,EAAE,cAAc,EAC1B,UAAU,GAAE,aAA2B,GACtC,cAAc,CAqBhB;AAYD,wBAAgB,qBAAqB,CACnC,UAAU,EAAE,OAAO,EACnB,UAAU,GAAE,aAA2B,GACtC,OAAO,CAgCT;AAQD,eAAO,MAAM,IAAI;YAEL,UAAU,CAAC,OAAO,mBAAmB,CAAC;IAC9C;;;;;;OAMG;sBACe,IAAI;;IAGtB;;;;;;;;;;;;;OAaG;0BACmB,IAAI;EAuK5B,CAAC;AAGH,OAAO,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC"}
|
package/dist/fixtures/index.js
CHANGED
|
@@ -37,6 +37,9 @@ exports.createHealingExpect = exports.test = void 0;
|
|
|
37
37
|
exports.resolveSelaProxy = resolveSelaProxy;
|
|
38
38
|
exports.createFrameLocatorProxy = createFrameLocatorProxy;
|
|
39
39
|
exports.createLocatorProxy = createLocatorProxy;
|
|
40
|
+
exports.buildLivePageProxy = buildLivePageProxy;
|
|
41
|
+
exports.buildLiveContextProxy = buildLiveContextProxy;
|
|
42
|
+
exports.buildLiveBrowserProxy = buildLiveBrowserProxy;
|
|
40
43
|
const test_1 = require("@playwright/test");
|
|
41
44
|
const StackUtils_1 = require("../utils/StackUtils");
|
|
42
45
|
const logger_1 = require("../utils/logger");
|
|
@@ -48,6 +51,8 @@ const lineage_1 = require("./lineage");
|
|
|
48
51
|
const ancestryHeal_1 = require("./ancestryHeal");
|
|
49
52
|
const errorClassification_1 = require("./errorClassification");
|
|
50
53
|
const FailureClassifier_1 = require("../errors/FailureClassifier");
|
|
54
|
+
const TestDeadline_1 = require("../services/TestDeadline");
|
|
55
|
+
const ConfigLoader_1 = require("../config/ConfigLoader");
|
|
51
56
|
const HealingGovernance_1 = require("../config/HealingGovernance");
|
|
52
57
|
const RegressionManifestService_1 = require("../services/RegressionManifestService");
|
|
53
58
|
const fs = __importStar(require("fs"));
|
|
@@ -277,6 +282,69 @@ function buildHealedArgs(prop, args, timeout = 8000) {
|
|
|
277
282
|
logger_1.logger.debug(`buildHealedArgs: unknown prop "${prop}", passing as-is`);
|
|
278
283
|
return args;
|
|
279
284
|
}
|
|
285
|
+
// ─── Deadline-aware budget injection (Mechanism A, RFC timing §4) ──
|
|
286
|
+
//
|
|
287
|
+
// Reads an explicit `{ timeout }` off the user's args at the same option-slot
|
|
288
|
+
// `buildHealedArgs` writes to — the single source of truth for where each
|
|
289
|
+
// action carries its options.
|
|
290
|
+
function parseExplicitTimeout(prop, args) {
|
|
291
|
+
const optsAt = (i) => args[i] && typeof args[i] === "object" ? args[i] : undefined;
|
|
292
|
+
let opts;
|
|
293
|
+
if (prop === "dragTo" || DIRECT_VALUE_ACTIONS.has(prop)) {
|
|
294
|
+
opts = optsAt(1);
|
|
295
|
+
}
|
|
296
|
+
else if (OPTIONS_ONLY_ACTIONS.has(prop)) {
|
|
297
|
+
opts = optsAt(0);
|
|
298
|
+
}
|
|
299
|
+
else if (NO_ARGS_ACTIONS.has(prop)) {
|
|
300
|
+
opts =
|
|
301
|
+
args.length > 0 && typeof args[0] === "string" ? optsAt(1) : optsAt(0);
|
|
302
|
+
}
|
|
303
|
+
else {
|
|
304
|
+
return undefined;
|
|
305
|
+
}
|
|
306
|
+
const t = opts?.timeout;
|
|
307
|
+
return typeof t === "number" && Number.isFinite(t) ? t : undefined;
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Computes the first-attempt argument list under the deadline-aware policy.
|
|
311
|
+
*
|
|
312
|
+
* The budget is tightened ONLY when doing so shortens the user's effective
|
|
313
|
+
* budget (`injected < userBudget`) — i.e. only in deadline proximity. Far from
|
|
314
|
+
* the deadline (or with the feature disabled / no armed deadline / disabled
|
|
315
|
+
* test timeout) the user's args pass through byte-identical.
|
|
316
|
+
*
|
|
317
|
+
* When the user set no explicit arg timeout AND no configured actionTimeout,
|
|
318
|
+
* `userBudget` is Infinity, so the injected `remaining - buffer` always wins —
|
|
319
|
+
* this is the zero-config consumer case that gives healing its runway. It stays
|
|
320
|
+
* behaviorally equivalent for passing tests (native auto-wait still resolves the
|
|
321
|
+
* element before the injected budget elapses).
|
|
322
|
+
*
|
|
323
|
+
* `reshape` names the timeout the user WOULD have seen, so a later rethrow can
|
|
324
|
+
* hide the injected number (Mechanism D).
|
|
325
|
+
*/
|
|
326
|
+
function computeDeadlineInjection(prop, args, timing) {
|
|
327
|
+
if (!timing.enabled || !TestDeadline_1.sharedTestDeadline.armed)
|
|
328
|
+
return { args, reshape: null };
|
|
329
|
+
const remaining = TestDeadline_1.sharedTestDeadline.remaining();
|
|
330
|
+
if (!Number.isFinite(remaining))
|
|
331
|
+
return { args, reshape: null };
|
|
332
|
+
const explicit = parseExplicitTimeout(prop, args);
|
|
333
|
+
const configured = TestDeadline_1.sharedTestDeadline.userActionTimeout;
|
|
334
|
+
const userBudget = explicit ?? configured ?? Infinity;
|
|
335
|
+
const injected = Math.min(userBudget, Math.max(remaining - timing.deadlineBufferMs, timing.minActionFloorMs));
|
|
336
|
+
if (!(injected < userBudget))
|
|
337
|
+
return { args, reshape: null };
|
|
338
|
+
const rebuilt = buildHealedArgs(prop, args, injected);
|
|
339
|
+
if (rebuilt === args)
|
|
340
|
+
return { args, reshape: null }; // unknown prop → cannot inject
|
|
341
|
+
const reshape = explicit !== undefined
|
|
342
|
+
? { kind: "action", ms: explicit }
|
|
343
|
+
: configured !== undefined
|
|
344
|
+
? { kind: "action", ms: configured }
|
|
345
|
+
: { kind: "test", ms: TestDeadline_1.sharedTestDeadline.originalTimeout };
|
|
346
|
+
return { args: rebuilt, reshape };
|
|
347
|
+
}
|
|
280
348
|
// ─────────────────────────────────────────────────────────────────
|
|
281
349
|
// runWithHealArgument (unchanged from original)
|
|
282
350
|
// ─────────────────────────────────────────────────────────────────
|
|
@@ -330,12 +398,19 @@ async function runWithHeal(rawPage, prop, args, fullSelector, elementSelectorOnl
|
|
|
330
398
|
const initialLocator = usingLiveLocator
|
|
331
399
|
? liveLocator
|
|
332
400
|
: buildLiveLocator(rawPage, effectiveSelector);
|
|
401
|
+
// Deadline-aware budget (Mechanism A). Far from the deadline this returns the
|
|
402
|
+
// user's args unchanged (pass-through invariant preserved); in deadline
|
|
403
|
+
// proximity — or when the consumer configured no timeout at all — it tightens
|
|
404
|
+
// the first-attempt budget so the action fails, and healing begins, strictly
|
|
405
|
+
// BEFORE the page is torn down. `reshape` records the timeout the user would
|
|
406
|
+
// have seen so a later rethrow hides the injected number (Mechanism D).
|
|
407
|
+
const timing = ConfigLoader_1.ConfigLoader.getInstance().timing;
|
|
408
|
+
const { args: firstAttemptArgs, reshape: reshapeTarget } = computeDeadlineInjection(prop, args, timing);
|
|
333
409
|
try {
|
|
334
|
-
//
|
|
335
|
-
//
|
|
336
|
-
//
|
|
337
|
-
|
|
338
|
-
const result = await initialLocator[prop](...args);
|
|
410
|
+
// Pass-through invariant: on the common far-from-deadline path the user's
|
|
411
|
+
// args reach Playwright untouched, so native auto-wait, actionability
|
|
412
|
+
// polling, and the caller's own `timeout` govern the first attempt entirely.
|
|
413
|
+
const result = await initialLocator[prop](...firstAttemptArgs);
|
|
339
414
|
// DNA baseline capture runs strictly AFTER the action succeeded — deduped
|
|
340
415
|
// per selector per session, short-capped, and never throws.
|
|
341
416
|
await singleton_1.sharedEngine.captureSuccessfulElement(rawPage, effectiveSelector, stableId, callerInfo.filePath);
|
|
@@ -346,6 +421,14 @@ async function runWithHeal(rawPage, prop, args, fullSelector, elementSelectorOnl
|
|
|
346
421
|
};
|
|
347
422
|
}
|
|
348
423
|
catch (firstError) {
|
|
424
|
+
// Mechanism D (error fidelity): if Mechanism A tightened the budget, rewrite
|
|
425
|
+
// the timeout headline back to the user-perceived value NOW — before any
|
|
426
|
+
// rethrow or classification. Reshaping only swaps the headline token (the
|
|
427
|
+
// call log survives), so classification is byte-identical, and because every
|
|
428
|
+
// downstream rethrow surfaces this same error object, all refusal paths
|
|
429
|
+
// (governance, gate, heal-fail) inherit the corrected message for free.
|
|
430
|
+
if (reshapeTarget)
|
|
431
|
+
(0, FailureClassifier_1.reshapeTimeoutError)(firstError, reshapeTarget);
|
|
349
432
|
// Governance pre-gate (Blueprint D): protected selectors/attributes and
|
|
350
433
|
// excluded files never heal — checked BEFORE classification so a locked
|
|
351
434
|
// element costs zero probes and zero LLM. Always audited.
|
|
@@ -376,6 +459,14 @@ async function runWithHeal(rawPage, prop, args, fullSelector, elementSelectorOnl
|
|
|
376
459
|
recordGateRefusal(verdict, effectiveSelector, stableId, callerInfo, prop);
|
|
377
460
|
throw firstError;
|
|
378
461
|
}
|
|
462
|
+
// Mechanism B (anti-guillotine): we are committed to a heal. Extend the
|
|
463
|
+
// running test's total timeout NOW — before DOM extraction + LLM + verified
|
|
464
|
+
// retry — so the whole heal cycle completes on a LIVE page instead of being
|
|
465
|
+
// killed by the native test deadline. No-op far from the deadline is
|
|
466
|
+
// harmless; the extension is capped and transparently annotated on the test.
|
|
467
|
+
if (timing.enabled && timing.extendTestTimeout) {
|
|
468
|
+
TestDeadline_1.sharedTestDeadline.extend(timing.healRunwayMs, timing.maxExtensionsPerTest);
|
|
469
|
+
}
|
|
379
470
|
logger_1.logger.debug(`'${prop}' failed on: "${effectiveSelector}"`);
|
|
380
471
|
// Selector is broken — any existing snapshot has stale post-action text
|
|
381
472
|
// from a prior run. Clear it now so engine.heal() starts with no DNA
|
|
@@ -875,6 +966,139 @@ lineage = null) {
|
|
|
875
966
|
lineage_1.proxyToLineage.set(proxy, lineage);
|
|
876
967
|
return proxy;
|
|
877
968
|
}
|
|
969
|
+
let currentTestBinding = null;
|
|
970
|
+
const liveBinding = () => currentTestBinding;
|
|
971
|
+
// ─────────────────────────────────────────────────────────────────
|
|
972
|
+
// buildLivePageProxy — the single page-proxy factory
|
|
973
|
+
//
|
|
974
|
+
// Extracted from the `page` fixture so EVERY page — however it was
|
|
975
|
+
// allocated (fixture, browser.newPage, context.newPage) — is wrapped
|
|
976
|
+
// identically. Idempotent: a page that is already a Sela page proxy is
|
|
977
|
+
// returned untouched, so the browser→context→page cascade never
|
|
978
|
+
// double-wraps (and never attaches a second console listener).
|
|
979
|
+
// ─────────────────────────────────────────────────────────────────
|
|
980
|
+
function buildLivePageProxy(rawPage, binding) {
|
|
981
|
+
if ((0, proxyTag_1.isSelaProxy)(rawPage, "page"))
|
|
982
|
+
return rawPage;
|
|
983
|
+
const { actionCounter, unsubscribers } = binding;
|
|
984
|
+
rawPage.on("console", (msg) => {
|
|
985
|
+
if (msg.text().includes("[DOM-EXTRACT]")) {
|
|
986
|
+
logger_1.logger.debug(`[Browser Console]: ${msg.text()}`);
|
|
987
|
+
}
|
|
988
|
+
});
|
|
989
|
+
return new Proxy(rawPage, {
|
|
990
|
+
get(target, prop) {
|
|
991
|
+
// Sela proxy identity - honored before any other branch so callers
|
|
992
|
+
// can reliably detect a Sela page proxy via Symbol.for("sela.proxy").
|
|
993
|
+
if (prop === proxyTag_1.SELA_PROXY)
|
|
994
|
+
return "page";
|
|
995
|
+
if (prop === "_rawPage")
|
|
996
|
+
return target;
|
|
997
|
+
if (typeof prop !== "string")
|
|
998
|
+
return Reflect.get(target, prop, target);
|
|
999
|
+
if (prop === "locator") {
|
|
1000
|
+
return (selector) => {
|
|
1001
|
+
const p = createLocatorProxy(target.locator(selector), selector, target, binding.title, actionCounter, selector, "locator", // developer chose a structural selector
|
|
1002
|
+
(0, lineage_1.makeLink)(null, "locator", [selector], selector));
|
|
1003
|
+
const u = p._selaUnsubscribe;
|
|
1004
|
+
if (u)
|
|
1005
|
+
unsubscribers.push(u);
|
|
1006
|
+
return p;
|
|
1007
|
+
};
|
|
1008
|
+
}
|
|
1009
|
+
if (prop === "frameLocator") {
|
|
1010
|
+
return (frameSel) => createFrameLocatorProxy(target.frameLocator(frameSel), frameSel, target, binding.title, actionCounter, "", (0, lineage_1.makeLink)(null, "frameLocator", [frameSel], frameSel));
|
|
1011
|
+
}
|
|
1012
|
+
if (prop in GET_BY_METHODS) {
|
|
1013
|
+
return (...args) => {
|
|
1014
|
+
const semanticSelector = GET_BY_METHODS[prop](args);
|
|
1015
|
+
const p = createLocatorProxy(target[prop](...args), semanticSelector, target, binding.title, actionCounter, semanticSelector, prop, // getBy* → semantic origin
|
|
1016
|
+
(0, lineage_1.makeLink)(null, prop, args, semanticSelector));
|
|
1017
|
+
const u = p._selaUnsubscribe;
|
|
1018
|
+
if (u)
|
|
1019
|
+
unsubscribers.push(u);
|
|
1020
|
+
return p;
|
|
1021
|
+
};
|
|
1022
|
+
}
|
|
1023
|
+
return Reflect.get(target, prop, target);
|
|
1024
|
+
},
|
|
1025
|
+
});
|
|
1026
|
+
}
|
|
1027
|
+
// ─────────────────────────────────────────────────────────────────
|
|
1028
|
+
// buildLiveContextProxy — intercepts context.newPage()
|
|
1029
|
+
//
|
|
1030
|
+
// Transparent BrowserContext proxy: every method forwards untouched
|
|
1031
|
+
// EXCEPT `newPage()`, whose returned Page is routed through
|
|
1032
|
+
// buildLivePageProxy so a manually-allocated page is wired into Sela.
|
|
1033
|
+
// The binding is read at CALL time (not creation) so the same proxy is
|
|
1034
|
+
// valid for the whole test. Idempotent + governance-aware.
|
|
1035
|
+
// ─────────────────────────────────────────────────────────────────
|
|
1036
|
+
function buildLiveContextProxy(rawContext, getBinding = liveBinding) {
|
|
1037
|
+
if ((0, proxyTag_1.isSelaProxy)(rawContext, "context"))
|
|
1038
|
+
return rawContext;
|
|
1039
|
+
return new Proxy(rawContext, {
|
|
1040
|
+
get(target, prop) {
|
|
1041
|
+
if (prop === proxyTag_1.SELA_PROXY)
|
|
1042
|
+
return "context";
|
|
1043
|
+
if (prop === "_rawContext")
|
|
1044
|
+
return target;
|
|
1045
|
+
if (typeof prop !== "string")
|
|
1046
|
+
return Reflect.get(target, prop, target);
|
|
1047
|
+
if (prop === "newPage") {
|
|
1048
|
+
return async (...args) => {
|
|
1049
|
+
const page = await target.newPage(...args);
|
|
1050
|
+
const binding = getBinding();
|
|
1051
|
+
if (!binding || binding.optedOut)
|
|
1052
|
+
return page;
|
|
1053
|
+
return buildLivePageProxy(page, binding);
|
|
1054
|
+
};
|
|
1055
|
+
}
|
|
1056
|
+
return Reflect.get(target, prop, target);
|
|
1057
|
+
},
|
|
1058
|
+
});
|
|
1059
|
+
}
|
|
1060
|
+
// ─────────────────────────────────────────────────────────────────
|
|
1061
|
+
// buildLiveBrowserProxy — intercepts browser.newContext()/newPage()
|
|
1062
|
+
//
|
|
1063
|
+
// Transparent Browser proxy (worker-scoped): forwards everything
|
|
1064
|
+
// untouched EXCEPT the two page-factory seams. `newContext()` returns a
|
|
1065
|
+
// proxied context (so its later `.newPage()` is caught too) and
|
|
1066
|
+
// `newPage()` returns a proxied page directly. Both consult the live
|
|
1067
|
+
// per-test binding at call time and respect the governance opt-out.
|
|
1068
|
+
// ─────────────────────────────────────────────────────────────────
|
|
1069
|
+
function buildLiveBrowserProxy(rawBrowser, getBinding = liveBinding) {
|
|
1070
|
+
if ((0, proxyTag_1.isSelaProxy)(rawBrowser, "browser"))
|
|
1071
|
+
return rawBrowser;
|
|
1072
|
+
return new Proxy(rawBrowser, {
|
|
1073
|
+
get(target, prop) {
|
|
1074
|
+
if (prop === proxyTag_1.SELA_PROXY)
|
|
1075
|
+
return "browser";
|
|
1076
|
+
if (prop === "_rawBrowser")
|
|
1077
|
+
return target;
|
|
1078
|
+
if (typeof prop !== "string")
|
|
1079
|
+
return Reflect.get(target, prop, target);
|
|
1080
|
+
if (prop === "newContext") {
|
|
1081
|
+
return async (...args) => {
|
|
1082
|
+
const context = await target.newContext(...args);
|
|
1083
|
+
const binding = getBinding();
|
|
1084
|
+
if (!binding || binding.optedOut)
|
|
1085
|
+
return context;
|
|
1086
|
+
return buildLiveContextProxy(context, getBinding);
|
|
1087
|
+
};
|
|
1088
|
+
}
|
|
1089
|
+
if (prop === "newPage") {
|
|
1090
|
+
return async (...args) => {
|
|
1091
|
+
const page = await target.newPage(...args);
|
|
1092
|
+
const binding = getBinding();
|
|
1093
|
+
if (!binding || binding.optedOut)
|
|
1094
|
+
return page;
|
|
1095
|
+
return buildLivePageProxy(page, binding);
|
|
1096
|
+
};
|
|
1097
|
+
}
|
|
1098
|
+
return Reflect.get(target, prop, target);
|
|
1099
|
+
},
|
|
1100
|
+
});
|
|
1101
|
+
}
|
|
878
1102
|
// ─────────────────────────────────────────────────────────────────
|
|
879
1103
|
// test fixture - teardown added
|
|
880
1104
|
// ─────────────────────────────────────────────────────────────────
|
|
@@ -920,90 +1144,96 @@ exports.test = test_1.test.extend({
|
|
|
920
1144
|
},
|
|
921
1145
|
{ scope: "worker", auto: true },
|
|
922
1146
|
],
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
1147
|
+
// ── Per-test binding + deadline arming (auto) ───────────────────────
|
|
1148
|
+
// Runs for EVERY test, so arming and intent harvesting no longer depend on
|
|
1149
|
+
// the `page` fixture being requested. This is the linchpin that closes the
|
|
1150
|
+
// page-instantiation leak: a test that only takes `{ browser }` still arms
|
|
1151
|
+
// the deadline and publishes a binding the browser/context proxies read.
|
|
1152
|
+
_selaTestBinding: [
|
|
1153
|
+
async ({}, use, testInfo) => {
|
|
1154
|
+
// Governance full bypass (Blueprint D): master switch off, excluded spec
|
|
1155
|
+
// file, or a per-test opt-out (@sela-no-heal). When opted out we still
|
|
1156
|
+
// publish a binding (so proxies see `optedOut` and hand out native
|
|
1157
|
+
// objects) but skip all arming / intent capture.
|
|
1158
|
+
const optedOut = !!governanceTestOptOut(testInfo);
|
|
1159
|
+
const binding = {
|
|
1160
|
+
title: testInfo.title,
|
|
1161
|
+
actionCounter: { value: 0 },
|
|
1162
|
+
unsubscribers: [],
|
|
1163
|
+
optedOut,
|
|
1164
|
+
};
|
|
1165
|
+
currentTestBinding = binding;
|
|
1166
|
+
if (!optedOut) {
|
|
1167
|
+
// Flush transient no-heal locks when Playwright starts a NEW retry
|
|
1168
|
+
// generation, so an element that was momentarily unhealable gets a
|
|
1169
|
+
// clean chance to heal on the retry (never persist a transient block
|
|
1170
|
+
// across the retry loop — CLAUDE.md §4).
|
|
1171
|
+
singleton_1.sharedEngine.beginTest(testInfo.retry);
|
|
1172
|
+
// Arm the deadline registry (RFC timing-interception §3). `testInfo` is
|
|
1173
|
+
// structurally a DeadlineTestInfo (timeout / setTimeout / annotations /
|
|
1174
|
+
// project.use.actionTimeout). The action proxy reads it via the module
|
|
1175
|
+
// singleton.
|
|
1176
|
+
TestDeadline_1.sharedTestDeadline.arm(testInfo);
|
|
1177
|
+
// Harvest the full test intent (Blueprint C): describe chain,
|
|
1178
|
+
// annotations and tags flow into the heal prompt + confidence gate.
|
|
1179
|
+
// String refs only — zero cost on passing runs.
|
|
1180
|
+
singleton_1.sharedEngine.setTestContext({
|
|
1181
|
+
title: testInfo.title,
|
|
1182
|
+
titlePath: [...testInfo.titlePath],
|
|
1183
|
+
annotations: testInfo.annotations.map((a) => ({
|
|
1184
|
+
type: a.type,
|
|
1185
|
+
description: a.description ?? undefined,
|
|
1186
|
+
})),
|
|
1187
|
+
tags: (testInfo.tags ?? []).slice(),
|
|
1188
|
+
file: testInfo.file,
|
|
1189
|
+
projectName: testInfo.project?.name ?? "",
|
|
1190
|
+
retry: testInfo.retry,
|
|
1191
|
+
});
|
|
1192
|
+
}
|
|
1193
|
+
await use();
|
|
1194
|
+
// ── Per-test teardown ─────────────────────────────────────────
|
|
1195
|
+
// Release every proxy registry subscription accumulated by this test's
|
|
1196
|
+
// pages (fixture-injected AND manually-allocated) and disarm the
|
|
1197
|
+
// deadline. We deliberately DO NOT call engine.commitUpdates() here — the
|
|
1198
|
+
// suite-wide flush lives in the worker-scoped `_selaWorkerLifecycle`.
|
|
1199
|
+
for (const u of binding.unsubscribers)
|
|
1200
|
+
u();
|
|
1201
|
+
registry.clearForTest();
|
|
1202
|
+
// Disarm so a stale deadline never bleeds into the next test (harmless if
|
|
1203
|
+
// skipped — the next arm() overwrites — but hygienic).
|
|
1204
|
+
TestDeadline_1.sharedTestDeadline.disarm();
|
|
1205
|
+
currentTestBinding = null;
|
|
1206
|
+
},
|
|
1207
|
+
{ auto: true },
|
|
1208
|
+
],
|
|
1209
|
+
// ── Browser proxy (worker-scoped) ───────────────────────────────────
|
|
1210
|
+
// Wrapped ONCE per worker. Intercepts browser.newContext()/newPage() so a
|
|
1211
|
+
// page allocated OUTSIDE the fixture injection (Pattern C — the true leak)
|
|
1212
|
+
// is still routed through Sela. Reads the live per-test binding at call time.
|
|
1213
|
+
browser: [
|
|
1214
|
+
async ({ browser }, use) => {
|
|
1215
|
+
await use(buildLiveBrowserProxy(browser));
|
|
1216
|
+
},
|
|
1217
|
+
{ scope: "worker" },
|
|
1218
|
+
],
|
|
1219
|
+
// ── Context proxy (test-scoped) ─────────────────────────────────────
|
|
1220
|
+
// Covers `context.newPage()` off the fixture-provided context. The built-in
|
|
1221
|
+
// context is itself created via the proxied browser above; the idempotent
|
|
1222
|
+
// guard means this override never double-wraps.
|
|
1223
|
+
context: async ({ context }, use) => {
|
|
1224
|
+
await use(buildLiveContextProxy(context));
|
|
1225
|
+
},
|
|
1226
|
+
page: async ({ page, _selaTestBinding }, use) => {
|
|
1227
|
+
const binding = currentTestBinding;
|
|
1228
|
+
// Governance opt-out (binding null is impossible here — the auto fixture
|
|
1229
|
+
// always ran — but treat it as native for safety).
|
|
1230
|
+
if (!binding || binding.optedOut) {
|
|
928
1231
|
await use(page);
|
|
929
1232
|
return;
|
|
930
1233
|
}
|
|
931
|
-
page
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
}
|
|
935
|
-
});
|
|
936
|
-
const actionCounter = { value: 0 };
|
|
937
|
-
const rawPage = page;
|
|
938
|
-
// Flush transient no-heal locks when Playwright starts a NEW retry
|
|
939
|
-
// generation, so an element that was momentarily unhealable gets a clean
|
|
940
|
-
// chance to heal on the retry (never persist a transient block across the
|
|
941
|
-
// retry loop — CLAUDE.md §4).
|
|
942
|
-
singleton_1.sharedEngine.beginTest(testInfo.retry);
|
|
943
|
-
// Harvest the full test intent (Blueprint C): describe chain, annotations
|
|
944
|
-
// and tags flow into the heal prompt + confidence gate. String refs only —
|
|
945
|
-
// zero cost on passing runs.
|
|
946
|
-
singleton_1.sharedEngine.setTestContext({
|
|
947
|
-
title: testInfo.title,
|
|
948
|
-
titlePath: [...testInfo.titlePath],
|
|
949
|
-
annotations: testInfo.annotations.map((a) => ({
|
|
950
|
-
type: a.type,
|
|
951
|
-
description: a.description ?? undefined,
|
|
952
|
-
})),
|
|
953
|
-
tags: (testInfo.tags ?? []).slice(),
|
|
954
|
-
file: testInfo.file,
|
|
955
|
-
projectName: testInfo.project?.name ?? "",
|
|
956
|
-
retry: testInfo.retry,
|
|
957
|
-
});
|
|
958
|
-
const unsubscribers = [];
|
|
959
|
-
const pageProxy = new Proxy(page, {
|
|
960
|
-
get(target, prop) {
|
|
961
|
-
// Sela proxy identity - honored before any other branch so callers
|
|
962
|
-
// can reliably detect a Sela page proxy via Symbol.for("sela.proxy").
|
|
963
|
-
if (prop === proxyTag_1.SELA_PROXY)
|
|
964
|
-
return "page";
|
|
965
|
-
if (prop === "_rawPage")
|
|
966
|
-
return target;
|
|
967
|
-
if (typeof prop !== "string")
|
|
968
|
-
return Reflect.get(target, prop, target);
|
|
969
|
-
if (prop === "locator") {
|
|
970
|
-
return (selector) => {
|
|
971
|
-
const p = createLocatorProxy(target.locator(selector), selector, rawPage, testInfo.title, actionCounter, selector, "locator", // developer chose a structural selector
|
|
972
|
-
(0, lineage_1.makeLink)(null, "locator", [selector], selector));
|
|
973
|
-
const u = p._selaUnsubscribe;
|
|
974
|
-
if (u)
|
|
975
|
-
unsubscribers.push(u);
|
|
976
|
-
return p;
|
|
977
|
-
};
|
|
978
|
-
}
|
|
979
|
-
if (prop === "frameLocator") {
|
|
980
|
-
return (frameSel) => createFrameLocatorProxy(target.frameLocator(frameSel), frameSel, rawPage, testInfo.title, actionCounter, "", (0, lineage_1.makeLink)(null, "frameLocator", [frameSel], frameSel));
|
|
981
|
-
}
|
|
982
|
-
if (prop in GET_BY_METHODS) {
|
|
983
|
-
return (...args) => {
|
|
984
|
-
const semanticSelector = GET_BY_METHODS[prop](args);
|
|
985
|
-
const p = createLocatorProxy(target[prop](...args), semanticSelector, rawPage, testInfo.title, actionCounter, semanticSelector, prop, // getBy* → semantic origin
|
|
986
|
-
(0, lineage_1.makeLink)(null, prop, args, semanticSelector));
|
|
987
|
-
const u = p._selaUnsubscribe;
|
|
988
|
-
if (u)
|
|
989
|
-
unsubscribers.push(u);
|
|
990
|
-
return p;
|
|
991
|
-
};
|
|
992
|
-
}
|
|
993
|
-
return Reflect.get(target, prop, target);
|
|
994
|
-
},
|
|
995
|
-
});
|
|
996
|
-
await use(pageProxy);
|
|
997
|
-
// ── Per-test teardown ─────────────────────────────────────────
|
|
998
|
-
// Release this test's proxy registry subscriptions and drop per-test
|
|
999
|
-
// listeners. We deliberately DO NOT call engine.commitUpdates() here -
|
|
1000
|
-
// flushing per test is what produced the per-test report writes, the
|
|
1001
|
-
// duplicated .sela-history.json entries (each "healed":1), and the
|
|
1002
|
-
// last-test-only DX diff. The suite-wide flush lives in the
|
|
1003
|
-
// worker-scoped `_selaWorkerLifecycle` teardown above.
|
|
1004
|
-
for (const u of unsubscribers)
|
|
1005
|
-
u();
|
|
1006
|
-
registry.clearForTest();
|
|
1234
|
+
// Idempotent: when the built-in page already arrived proxied through the
|
|
1235
|
+
// browser/context cascade, buildLivePageProxy returns it unchanged.
|
|
1236
|
+
await use(buildLivePageProxy(page, binding));
|
|
1007
1237
|
},
|
|
1008
1238
|
expect: async ({ page }, use, testInfo) => {
|
|
1009
1239
|
// Mirror the page fixture's governance bypass with the native expect.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export declare const SELA_PROXY: unique symbol;
|
|
2
|
-
export type SelaProxyKind = "locator" | "framelocator" | "page" | "expect";
|
|
2
|
+
export type SelaProxyKind = "locator" | "framelocator" | "page" | "context" | "browser" | "expect";
|
|
3
3
|
/**
|
|
4
4
|
* Returns the proxy kind if `v` is a Sela proxy, otherwise `null`.
|
|
5
5
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"proxyTag.d.ts","sourceRoot":"","sources":["../../src/fixtures/proxyTag.ts"],"names":[],"mappings":"AAaA,eAAO,MAAM,UAAU,eAA2B,CAAC;AAEnD,MAAM,MAAM,aAAa,
|
|
1
|
+
{"version":3,"file":"proxyTag.d.ts","sourceRoot":"","sources":["../../src/fixtures/proxyTag.ts"],"names":[],"mappings":"AAaA,eAAO,MAAM,UAAU,eAA2B,CAAC;AAEnD,MAAM,MAAM,aAAa,GACrB,SAAS,GACT,cAAc,GACd,MAAM,GACN,SAAS,GACT,SAAS,GACT,QAAQ,CAAC;AAEb;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,CAAC,EAAE,OAAO,GAAG,aAAa,GAAG,IAAI,CAe9D;AAED,wBAAgB,WAAW,CAAC,CAAC,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,aAAa,GAAG,OAAO,CAIrE"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/** The minimal slice of Playwright's TestInfo this registry depends on. */
|
|
2
|
+
export interface DeadlineTestInfo {
|
|
3
|
+
/** Live total test timeout in ms; reflects test.slow()/setTimeout mutations. 0 ⇒ disabled. */
|
|
4
|
+
timeout: number;
|
|
5
|
+
/** Sets the TOTAL test timeout (absolute, not additive). */
|
|
6
|
+
setTimeout(ms: number): void;
|
|
7
|
+
/** Playwright's per-test annotations array (mutated for transparency). */
|
|
8
|
+
annotations: Array<{
|
|
9
|
+
type: string;
|
|
10
|
+
description?: string;
|
|
11
|
+
}>;
|
|
12
|
+
/** Resolved project config — source of the configured actionTimeout, if any. */
|
|
13
|
+
project?: {
|
|
14
|
+
use?: {
|
|
15
|
+
actionTimeout?: number;
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/** Annotation type stamped onto the test when its deadline is extended for healing. */
|
|
20
|
+
export declare const EXTENSION_ANNOTATION = "sela-timeout-extended";
|
|
21
|
+
export declare class TestDeadline {
|
|
22
|
+
private info;
|
|
23
|
+
private startMs;
|
|
24
|
+
private original;
|
|
25
|
+
private extensions;
|
|
26
|
+
private addedMs;
|
|
27
|
+
private readonly now;
|
|
28
|
+
constructor(now?: () => number);
|
|
29
|
+
/** Arm at page-fixture setup: snapshot the start instant and original timeout. */
|
|
30
|
+
arm(info: DeadlineTestInfo): void;
|
|
31
|
+
/** Disarm at teardown so a stale deadline never bleeds into the next test. */
|
|
32
|
+
disarm(): void;
|
|
33
|
+
get armed(): boolean;
|
|
34
|
+
/** The timeout the test STARTED with (pre-extension) — the user-perceived deadline. */
|
|
35
|
+
get originalTimeout(): number;
|
|
36
|
+
get extensionsUsed(): number;
|
|
37
|
+
/** Configured project actionTimeout (undefined when the consumer set none). */
|
|
38
|
+
get userActionTimeout(): number | undefined;
|
|
39
|
+
/**
|
|
40
|
+
* Milliseconds left before the current test-level deadline. Reads the LIVE
|
|
41
|
+
* `timeout` each call so test.slow()/setTimeout mutations are honored.
|
|
42
|
+
* Returns Infinity when not armed or when the deadline is disabled (timeout 0)
|
|
43
|
+
* — in both cases there is no guillotine to race, so callers treat the budget
|
|
44
|
+
* as unbounded and stay pure pass-through.
|
|
45
|
+
*/
|
|
46
|
+
remaining(): number;
|
|
47
|
+
/**
|
|
48
|
+
* Extend the current test's TOTAL timeout by `ms` to buy healing runway.
|
|
49
|
+
* Idempotency-guarded: no-op when unarmed, when the deadline is disabled
|
|
50
|
+
* (original 0), when the per-test extension cap is reached, or for a
|
|
51
|
+
* non-positive delta. Returns true iff an extension was applied. Also stamps
|
|
52
|
+
* a single, updated transparency annotation onto the test.
|
|
53
|
+
*/
|
|
54
|
+
extend(ms: number, maxExtensions: number): boolean;
|
|
55
|
+
private _annotate;
|
|
56
|
+
}
|
|
57
|
+
/** Process-wide singleton — one active test per worker (Playwright serial invariant). */
|
|
58
|
+
export declare const sharedTestDeadline: TestDeadline;
|
|
59
|
+
//# sourceMappingURL=TestDeadline.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TestDeadline.d.ts","sourceRoot":"","sources":["../../src/services/TestDeadline.ts"],"names":[],"mappings":"AAeA,2EAA2E;AAC3E,MAAM,WAAW,gBAAgB;IAC/B,8FAA8F;IAC9F,OAAO,EAAE,MAAM,CAAC;IAChB,4DAA4D;IAC5D,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,0EAA0E;IAC1E,WAAW,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC3D,gFAAgF;IAChF,OAAO,CAAC,EAAE;QAAE,GAAG,CAAC,EAAE;YAAE,aAAa,CAAC,EAAE,MAAM,CAAA;SAAE,CAAA;KAAE,CAAC;CAChD;AAED,uFAAuF;AACvF,eAAO,MAAM,oBAAoB,0BAA0B,CAAC;AAE5D,qBAAa,YAAY;IACvB,OAAO,CAAC,IAAI,CAAiC;IAC7C,OAAO,CAAC,OAAO,CAAK;IACpB,OAAO,CAAC,QAAQ,CAAK;IACrB,OAAO,CAAC,UAAU,CAAK;IACvB,OAAO,CAAC,OAAO,CAAK;IACpB,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;gBAEvB,GAAG,GAAE,MAAM,MAAiB;IAIxC,kFAAkF;IAClF,GAAG,CAAC,IAAI,EAAE,gBAAgB,GAAG,IAAI;IAQjC,8EAA8E;IAC9E,MAAM,IAAI,IAAI;IAQd,IAAI,KAAK,IAAI,OAAO,CAEnB;IAED,uFAAuF;IACvF,IAAI,eAAe,IAAI,MAAM,CAE5B;IAED,IAAI,cAAc,IAAI,MAAM,CAE3B;IAED,+EAA+E;IAC/E,IAAI,iBAAiB,IAAI,MAAM,GAAG,SAAS,CAE1C;IAED;;;;;;OAMG;IACH,SAAS,IAAI,MAAM;IAKnB;;;;;;OAMG;IACH,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,OAAO;IAUlD,OAAO,CAAC,SAAS;CASlB;AAED,yFAAyF;AACzF,eAAO,MAAM,kBAAkB,cAAqB,CAAC"}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// src/services/TestDeadline.ts
|
|
3
|
+
//
|
|
4
|
+
// Per-worker test-deadline registry (RFC timing-interception §3).
|
|
5
|
+
//
|
|
6
|
+
// Playwright runs tests SERIALLY within a worker process, so a single
|
|
7
|
+
// module-level singleton holds exactly one active test's deadline at a time.
|
|
8
|
+
// The `page` fixture arms it at setup and disarms it at teardown; the action
|
|
9
|
+
// proxy reads `remaining()` to decide whether to tighten the first-attempt
|
|
10
|
+
// budget (Mechanism A) and calls `extend()` to buy healing runway before the
|
|
11
|
+
// native test-timeout guillotine fires (Mechanism B).
|
|
12
|
+
//
|
|
13
|
+
// This class owns MECHANICS ONLY — no policy. Buffer/runway/cap constants live
|
|
14
|
+
// in resolved config and are passed in by the caller, keeping the deadline
|
|
15
|
+
// registry pure and trivially unit-testable via an injectable clock.
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.sharedTestDeadline = exports.TestDeadline = exports.EXTENSION_ANNOTATION = void 0;
|
|
18
|
+
/** Annotation type stamped onto the test when its deadline is extended for healing. */
|
|
19
|
+
exports.EXTENSION_ANNOTATION = "sela-timeout-extended";
|
|
20
|
+
class TestDeadline {
|
|
21
|
+
info = null;
|
|
22
|
+
startMs = 0;
|
|
23
|
+
original = 0;
|
|
24
|
+
extensions = 0;
|
|
25
|
+
addedMs = 0;
|
|
26
|
+
now;
|
|
27
|
+
constructor(now = Date.now) {
|
|
28
|
+
this.now = now;
|
|
29
|
+
}
|
|
30
|
+
/** Arm at page-fixture setup: snapshot the start instant and original timeout. */
|
|
31
|
+
arm(info) {
|
|
32
|
+
this.info = info;
|
|
33
|
+
this.startMs = this.now();
|
|
34
|
+
this.original = info.timeout;
|
|
35
|
+
this.extensions = 0;
|
|
36
|
+
this.addedMs = 0;
|
|
37
|
+
}
|
|
38
|
+
/** Disarm at teardown so a stale deadline never bleeds into the next test. */
|
|
39
|
+
disarm() {
|
|
40
|
+
this.info = null;
|
|
41
|
+
this.startMs = 0;
|
|
42
|
+
this.original = 0;
|
|
43
|
+
this.extensions = 0;
|
|
44
|
+
this.addedMs = 0;
|
|
45
|
+
}
|
|
46
|
+
get armed() {
|
|
47
|
+
return this.info !== null;
|
|
48
|
+
}
|
|
49
|
+
/** The timeout the test STARTED with (pre-extension) — the user-perceived deadline. */
|
|
50
|
+
get originalTimeout() {
|
|
51
|
+
return this.original;
|
|
52
|
+
}
|
|
53
|
+
get extensionsUsed() {
|
|
54
|
+
return this.extensions;
|
|
55
|
+
}
|
|
56
|
+
/** Configured project actionTimeout (undefined when the consumer set none). */
|
|
57
|
+
get userActionTimeout() {
|
|
58
|
+
return this.info?.project?.use?.actionTimeout;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Milliseconds left before the current test-level deadline. Reads the LIVE
|
|
62
|
+
* `timeout` each call so test.slow()/setTimeout mutations are honored.
|
|
63
|
+
* Returns Infinity when not armed or when the deadline is disabled (timeout 0)
|
|
64
|
+
* — in both cases there is no guillotine to race, so callers treat the budget
|
|
65
|
+
* as unbounded and stay pure pass-through.
|
|
66
|
+
*/
|
|
67
|
+
remaining() {
|
|
68
|
+
if (!this.info || this.info.timeout === 0)
|
|
69
|
+
return Infinity;
|
|
70
|
+
return this.info.timeout - (this.now() - this.startMs);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Extend the current test's TOTAL timeout by `ms` to buy healing runway.
|
|
74
|
+
* Idempotency-guarded: no-op when unarmed, when the deadline is disabled
|
|
75
|
+
* (original 0), when the per-test extension cap is reached, or for a
|
|
76
|
+
* non-positive delta. Returns true iff an extension was applied. Also stamps
|
|
77
|
+
* a single, updated transparency annotation onto the test.
|
|
78
|
+
*/
|
|
79
|
+
extend(ms, maxExtensions) {
|
|
80
|
+
if (!this.info || this.original === 0)
|
|
81
|
+
return false;
|
|
82
|
+
if (this.extensions >= maxExtensions || ms <= 0)
|
|
83
|
+
return false;
|
|
84
|
+
this.info.setTimeout(this.info.timeout + ms);
|
|
85
|
+
this.extensions += 1;
|
|
86
|
+
this.addedMs += ms;
|
|
87
|
+
this._annotate();
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
_annotate() {
|
|
91
|
+
if (!this.info)
|
|
92
|
+
return;
|
|
93
|
+
const description = `+${this.addedMs}ms ×${this.extensions} for healing`;
|
|
94
|
+
const existing = this.info.annotations.find((a) => a.type === exports.EXTENSION_ANNOTATION);
|
|
95
|
+
if (existing)
|
|
96
|
+
existing.description = description;
|
|
97
|
+
else
|
|
98
|
+
this.info.annotations.push({ type: exports.EXTENSION_ANNOTATION, description });
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
exports.TestDeadline = TestDeadline;
|
|
102
|
+
/** Process-wide singleton — one active test per worker (Playwright serial invariant). */
|
|
103
|
+
exports.sharedTestDeadline = new TestDeadline();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DOMUtils.d.ts","sourceRoot":"","sources":["../../src/utils/DOMUtils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AACxC,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"DOMUtils.d.ts","sourceRoot":"","sources":["../../src/utils/DOMUtils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AACxC,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAItC,qBAAa,QAAQ;IACnB;;;;OAIG;IACH,MAAM,CAAC,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM;IAKjD;;;;OAIG;IACH,MAAM,CAAC,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,EAAE;WAM5B,kBAAkB,CACpC,IAAI,EAAE,IAAI,EACV,YAAY,EAAE,MAAM,GACnB,OAAO,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,cAAc,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;IAerD;;;;;OAKG;WACiB,YAAY,CAC9B,IAAI,EAAE,IAAI,EACV,aAAa,EAAE,MAAM,EAAE,GACtB,OAAO,CAAC;QAAE,OAAO,EAAE,GAAG,CAAC;QAAC,cAAc,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;IA8BtD;;;;OAIG;mBACkB,yBAAyB;IAwC9C;;;;;;;;;;;;;;;;;;;OAmBG;mBACkB,qBAAqB;IA2d1C,MAAM,CAAC,aAAa,GAClB,MAAM,IAAI,EACV,UAAU,MAAM,KACf,OAAO,CAAC,UAAU,CAAC,CAqCpB;CACH"}
|
package/dist/utils/DOMUtils.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.DOMUtils = void 0;
|
|
4
4
|
const logger_1 = require("./logger");
|
|
5
|
+
const FailureClassifier_1 = require("../errors/FailureClassifier");
|
|
5
6
|
class DOMUtils {
|
|
6
7
|
/**
|
|
7
8
|
* Sanitizes a Playwright selector (which may contain `>>`) into a CSS-safe
|
|
@@ -75,6 +76,18 @@ class DOMUtils {
|
|
|
75
76
|
}
|
|
76
77
|
catch (e) {
|
|
77
78
|
logger_1.logger.debug(`DOM extraction error: ${e.message}`);
|
|
79
|
+
// Teardown guard (Mechanism C): the top-page fallback below re-runs
|
|
80
|
+
// `_evaluateNeighborhood` against the SAME page. When the page/context is
|
|
81
|
+
// already closed (the guillotine race — the test-level deadline fired and
|
|
82
|
+
// Playwright tore the page down underneath an in-flight heal) that retry
|
|
83
|
+
// throws the identical "Target page, context or browser has been closed"
|
|
84
|
+
// error a second time, uncaught, escaping heal() as a hard crash. Detect
|
|
85
|
+
// the teardown shape (or a closed page) and rethrow the ORIGINAL error
|
|
86
|
+
// once — the engine's extraction guard turns this into a clean refusal.
|
|
87
|
+
if ((0, FailureClassifier_1.isTeardownError)(e) ||
|
|
88
|
+
(typeof page.isClosed === "function" && page.isClosed())) {
|
|
89
|
+
throw e;
|
|
90
|
+
}
|
|
78
91
|
return {
|
|
79
92
|
dom: await DOMUtils._evaluateNeighborhood(page, targetSelector),
|
|
80
93
|
successfulPath: [],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sela-core",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.46",
|
|
4
4
|
"description": "AI self-healing Playwright wrapper - drop-in replacement for @playwright/test",
|
|
5
5
|
"main": "./dist/index.cjs",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -50,6 +50,7 @@
|
|
|
50
50
|
"test-shadow-dom": "npx playwright test tests/ShadowDom/ShadowDom.spec.ts --headed --retries=0",
|
|
51
51
|
"test-ut": "npx vitest run tests/unit",
|
|
52
52
|
"test-timing": "npx playwright test --config=playwright.timing.config.ts",
|
|
53
|
+
"test-page-allocation": "npx playwright test --config=playwright.page-allocation.config.ts",
|
|
53
54
|
"test-consumer": "node tests/e2e/consumer-simulation/run.mjs",
|
|
54
55
|
"test:chaos": "npx playwright test --config=playwright.chaos.config.ts --retries=0",
|
|
55
56
|
"test:chaos-ci": "npx playwright test --config=playwright.chaos.config.ts --retries=0",
|