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.
@@ -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":"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"}
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;AAiDX;;;;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;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"}
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
- const ACTION_TIMEOUT = /timeout \d+\s*ms exceeded/i;
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,
@@ -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,EAAwC,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAKvF,OAAO,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAEpD,OAAO,EACL,WAAW,EAIZ,MAAM,WAAW,CAAC;AAiDnB,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,GAAG,IAAI,CAI/D;AA20BD,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;AAQD,eAAO,MAAM,IAAI;YAEL,UAAU,CAAC,OAAO,mBAAmB,CAAC;;IAG9C;;;;;;;;;;;;;OAaG;0BACmB,IAAI;EAwL5B,CAAC;AAGH,OAAO,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC"}
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"}
@@ -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
- // Pure pass-through invariant: the user's args reach Playwright untouched
335
- // (no timeout injection, no pre-action DOM probe), so native auto-wait,
336
- // actionability polling, and the caller's own `timeout` govern the first
337
- // attempt entirely. Sela is invisible until this promise terminally rejects.
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
- page: async ({ page }, use, testInfo) => {
924
- // Governance full bypass (Blueprint D): master switch off, excluded spec
925
- // file, or a per-test opt-out (@sela-no-heal) hands out the NATIVE page
926
- // zero interception, zero proxy cost, zero DNA capture.
927
- if (governanceTestOptOut(testInfo)) {
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.on("console", (msg) => {
932
- if (msg.text().includes("[DOM-EXTRACT]")) {
933
- logger_1.logger.debug(`[Browser Console]: ${msg.text()}`);
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,GAAG,SAAS,GAAG,cAAc,GAAG,MAAM,GAAG,QAAQ,CAAC;AAE3E;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,CAAC,EAAE,OAAO,GAAG,aAAa,GAAG,IAAI,CAa9D;AAED,wBAAgB,WAAW,CAAC,CAAC,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,aAAa,GAAG,OAAO,CAIrE"}
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"}
@@ -32,6 +32,8 @@ function selaProxyKind(v) {
32
32
  if (tag === "locator" ||
33
33
  tag === "framelocator" ||
34
34
  tag === "page" ||
35
+ tag === "context" ||
36
+ tag === "browser" ||
35
37
  tag === "expect") {
36
38
  return tag;
37
39
  }
@@ -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;AAGtC,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;IA0B9C;;;;;;;;;;;;;;;;;;;OAmBG;mBACkB,qBAAqB;IA2d1C,MAAM,CAAC,aAAa,GAClB,MAAM,IAAI,EACV,UAAU,MAAM,KACf,OAAO,CAAC,UAAU,CAAC,CAqCpB;CACH"}
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"}
@@ -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.45",
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",