@veraxhq/verax 0.2.0 → 0.2.1
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/package.json +14 -4
- package/src/cli/commands/default.js +244 -86
- package/src/cli/commands/doctor.js +36 -4
- package/src/cli/commands/run.js +253 -69
- package/src/cli/entry.js +5 -5
- package/src/cli/util/detection-engine.js +4 -3
- package/src/cli/util/events.js +76 -0
- package/src/cli/util/expectation-extractor.js +11 -1
- package/src/cli/util/findings-writer.js +1 -0
- package/src/cli/util/observation-engine.js +69 -23
- package/src/cli/util/paths.js +3 -2
- package/src/cli/util/project-discovery.js +20 -0
- package/src/cli/util/redact.js +2 -2
- package/src/cli/util/runtime-budget.js +147 -0
- package/src/cli/util/summary-writer.js +12 -1
- package/src/types/global.d.ts +28 -0
- package/src/types/ts-ast.d.ts +24 -0
- package/src/verax/cli/doctor.js +2 -2
- package/src/verax/cli/init.js +1 -1
- package/src/verax/cli/url-safety.js +12 -2
- package/src/verax/cli/wizard.js +13 -2
- package/src/verax/core/budget-engine.js +1 -1
- package/src/verax/core/decision-snapshot.js +2 -2
- package/src/verax/core/determinism-model.js +35 -6
- package/src/verax/core/incremental-store.js +15 -7
- package/src/verax/core/replay-validator.js +4 -4
- package/src/verax/core/replay.js +1 -1
- package/src/verax/core/silence-impact.js +1 -1
- package/src/verax/core/silence-model.js +9 -7
- package/src/verax/detect/comparison.js +8 -3
- package/src/verax/detect/confidence-engine.js +17 -17
- package/src/verax/detect/detection-engine.js +1 -1
- package/src/verax/detect/evidence-index.js +15 -65
- package/src/verax/detect/expectation-model.js +54 -3
- package/src/verax/detect/explanation-helpers.js +1 -1
- package/src/verax/detect/finding-detector.js +2 -2
- package/src/verax/detect/findings-writer.js +9 -16
- package/src/verax/detect/flow-detector.js +4 -4
- package/src/verax/detect/index.js +37 -11
- package/src/verax/detect/interactive-findings.js +3 -4
- package/src/verax/detect/signal-mapper.js +2 -2
- package/src/verax/detect/skip-classifier.js +4 -4
- package/src/verax/detect/verdict-engine.js +4 -6
- package/src/verax/flow/flow-engine.js +3 -2
- package/src/verax/flow/flow-spec.js +1 -2
- package/src/verax/index.js +15 -3
- package/src/verax/intel/effect-detector.js +1 -1
- package/src/verax/intel/index.js +2 -2
- package/src/verax/intel/route-extractor.js +3 -3
- package/src/verax/intel/vue-navigation-extractor.js +81 -18
- package/src/verax/intel/vue-router-extractor.js +4 -2
- package/src/verax/learn/action-contract-extractor.js +3 -3
- package/src/verax/learn/ast-contract-extractor.js +53 -1
- package/src/verax/learn/index.js +36 -2
- package/src/verax/learn/manifest-writer.js +28 -14
- package/src/verax/learn/route-extractor.js +1 -1
- package/src/verax/learn/route-validator.js +8 -7
- package/src/verax/learn/state-extractor.js +1 -1
- package/src/verax/learn/static-extractor-navigation.js +1 -1
- package/src/verax/learn/static-extractor-validation.js +2 -2
- package/src/verax/learn/static-extractor.js +8 -7
- package/src/verax/learn/ts-contract-resolver.js +14 -12
- package/src/verax/observe/browser.js +22 -3
- package/src/verax/observe/console-sensor.js +2 -2
- package/src/verax/observe/expectation-executor.js +2 -1
- package/src/verax/observe/focus-sensor.js +1 -1
- package/src/verax/observe/human-driver.js +29 -10
- package/src/verax/observe/index.js +10 -7
- package/src/verax/observe/interaction-discovery.js +27 -15
- package/src/verax/observe/interaction-runner.js +6 -6
- package/src/verax/observe/loading-sensor.js +6 -0
- package/src/verax/observe/navigation-sensor.js +1 -1
- package/src/verax/observe/settle.js +1 -0
- package/src/verax/observe/state-sensor.js +8 -4
- package/src/verax/observe/state-ui-sensor.js +7 -1
- package/src/verax/observe/traces-writer.js +27 -16
- package/src/verax/observe/ui-signal-sensor.js +7 -0
- package/src/verax/scan-summary-writer.js +5 -2
- package/src/verax/shared/artifact-manager.js +1 -1
- package/src/verax/shared/budget-profiles.js +2 -2
- package/src/verax/shared/caching.js +1 -1
- package/src/verax/shared/config-loader.js +1 -2
- package/src/verax/shared/dynamic-route-utils.js +12 -6
- package/src/verax/shared/retry-policy.js +1 -6
- package/src/verax/shared/root-artifacts.js +1 -1
- package/src/verax/shared/zip-artifacts.js +1 -0
- package/src/verax/validate/context-validator.js +1 -1
- package/src/verax/observe/index.js.backup +0 -1
- package/src/verax/validate/context-validator.js.bak +0 -0
|
@@ -16,12 +16,31 @@ export async function navigateToUrl(page, url, scanBudget = DEFAULT_SCAN_BUDGET)
|
|
|
16
16
|
if (url.startsWith('file:') || url.includes('localhost:') || url.includes('127.0.0.1')) {
|
|
17
17
|
stableWait = 200; // Short wait for local fixtures
|
|
18
18
|
}
|
|
19
|
-
} catch {
|
|
20
|
-
|
|
19
|
+
} catch {
|
|
20
|
+
// Ignore config errors
|
|
21
|
+
}
|
|
22
|
+
// Use domcontentloaded first for faster timeout, then wait for networkidle separately
|
|
23
|
+
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: scanBudget.initialNavigationTimeoutMs });
|
|
24
|
+
await page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {
|
|
25
|
+
// Network idle timeout is acceptable, continue
|
|
26
|
+
});
|
|
21
27
|
await page.waitForTimeout(stableWait);
|
|
22
28
|
}
|
|
23
29
|
|
|
24
30
|
export async function closeBrowser(browser) {
|
|
25
|
-
|
|
31
|
+
try {
|
|
32
|
+
// Close all contexts first
|
|
33
|
+
const contexts = browser.contexts();
|
|
34
|
+
for (const context of contexts) {
|
|
35
|
+
try {
|
|
36
|
+
await context.close({ timeout: 5000 }).catch(() => {});
|
|
37
|
+
} catch (e) {
|
|
38
|
+
// Ignore context close errors
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
await browser.close({ timeout: 5000 }).catch(() => {});
|
|
42
|
+
} catch (e) {
|
|
43
|
+
// Ignore browser close errors - best effort cleanup
|
|
44
|
+
}
|
|
26
45
|
}
|
|
27
46
|
|
|
@@ -57,7 +57,7 @@ export class ConsoleSensor {
|
|
|
57
57
|
};
|
|
58
58
|
|
|
59
59
|
// Capture unhandled promise rejections
|
|
60
|
-
const
|
|
60
|
+
const _onUnhandledRejection = (promise, reason) => {
|
|
61
61
|
const message = (reason?.toString?.() || String(reason)).slice(0, 200);
|
|
62
62
|
state.unhandledRejections.push({
|
|
63
63
|
message: message,
|
|
@@ -104,7 +104,7 @@ export class ConsoleSensor {
|
|
|
104
104
|
/**
|
|
105
105
|
* Stop monitoring and return a summary for the window.
|
|
106
106
|
*/
|
|
107
|
-
stopWindow(windowId,
|
|
107
|
+
stopWindow(windowId, _page) {
|
|
108
108
|
const state = this.windows.get(windowId);
|
|
109
109
|
if (!state) {
|
|
110
110
|
return this.getEmptySummary();
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Executes every PROVEN expectation from the manifest.
|
|
5
5
|
* Each expectation must result in: VERIFIED, SILENT_FAILURE, or COVERAGE_GAP.
|
|
6
|
+
* @typedef {import('playwright').Page} Page
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
9
|
import { isProvenExpectation } from '../shared/expectation-prover.js';
|
|
@@ -11,7 +12,7 @@ import { hasMeaningfulUrlChange, hasVisibleChange, hasDomChange } from '../detec
|
|
|
11
12
|
import { runInteraction } from './interaction-runner.js';
|
|
12
13
|
import { captureScreenshot } from './evidence-capture.js';
|
|
13
14
|
import { getBaseOrigin } from './domain-boundary.js';
|
|
14
|
-
import { resolve
|
|
15
|
+
import { resolve } from 'path';
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
18
|
* Execute a single PROVEN expectation.
|
|
@@ -155,7 +155,7 @@ export class FocusSensor {
|
|
|
155
155
|
/**
|
|
156
156
|
* Detect if focus didn't move into modal (expected but didn't happen)
|
|
157
157
|
*/
|
|
158
|
-
detectModalFocusFailure(
|
|
158
|
+
detectModalFocusFailure(_page) {
|
|
159
159
|
// This is checked via modal detection - focus should move to modal
|
|
160
160
|
// Presence of modal without focus change = failure
|
|
161
161
|
return false; // Caller will check if modal opened
|
|
@@ -158,7 +158,9 @@ export class HumanBehaviorDriver {
|
|
|
158
158
|
} else if (url.includes('localhost:') || url.includes('127.0.0.1')) {
|
|
159
159
|
timeoutMs = 200; // Short wait for local http fixtures
|
|
160
160
|
}
|
|
161
|
-
} catch {
|
|
161
|
+
} catch {
|
|
162
|
+
// Ignore config errors
|
|
163
|
+
}
|
|
162
164
|
|
|
163
165
|
const waitForUiIdle = async () => {
|
|
164
166
|
const start = Date.now();
|
|
@@ -336,7 +338,10 @@ export class HumanBehaviorDriver {
|
|
|
336
338
|
|
|
337
339
|
const focusableSelectors = await page.evaluate(() => {
|
|
338
340
|
const focusables = Array.from(document.querySelectorAll('a[href], button, input, select, textarea, [tabindex], [role="button"], [role="menuitem"], [contenteditable="true"]'))
|
|
339
|
-
.filter(el =>
|
|
341
|
+
.filter(el => {
|
|
342
|
+
const htmlEl = /** @type {HTMLElement} */ (el);
|
|
343
|
+
return !el.hasAttribute('disabled') && htmlEl.tabIndex >= 0 && htmlEl.offsetParent !== null;
|
|
344
|
+
});
|
|
340
345
|
const describe = (el) => {
|
|
341
346
|
if (!el) return 'body';
|
|
342
347
|
if (el.id) return `#${el.id}`;
|
|
@@ -609,7 +614,9 @@ export class HumanBehaviorDriver {
|
|
|
609
614
|
page.waitForTimeout(CLICK_TIMEOUT_MS)
|
|
610
615
|
]);
|
|
611
616
|
await this.waitAfterAction(page, 600);
|
|
612
|
-
} catch {
|
|
617
|
+
} catch {
|
|
618
|
+
// Ignore form submission errors
|
|
619
|
+
}
|
|
613
620
|
|
|
614
621
|
const afterUrl = page.url();
|
|
615
622
|
const afterStorageKeys = await page.evaluate(() => Object.keys(localStorage));
|
|
@@ -660,7 +667,9 @@ export class HumanBehaviorDriver {
|
|
|
660
667
|
clicked = true;
|
|
661
668
|
await this.waitAfterAction(page, 400);
|
|
662
669
|
break;
|
|
663
|
-
} catch {
|
|
670
|
+
} catch {
|
|
671
|
+
// Ignore interaction errors
|
|
672
|
+
}
|
|
664
673
|
}
|
|
665
674
|
}
|
|
666
675
|
|
|
@@ -686,17 +695,23 @@ export class HumanBehaviorDriver {
|
|
|
686
695
|
const beforeUrl = page.url();
|
|
687
696
|
try {
|
|
688
697
|
await page.goto(url, { waitUntil: 'load', timeout: CLICK_TIMEOUT_MS }).catch(() => null);
|
|
689
|
-
} catch {
|
|
698
|
+
} catch {
|
|
699
|
+
// Ignore navigation errors
|
|
700
|
+
}
|
|
690
701
|
|
|
691
702
|
const afterUrl = page.url();
|
|
692
|
-
const
|
|
703
|
+
const redirectedToLogin = beforeUrl !== afterUrl && (afterUrl.includes('/login') || afterUrl.includes('/signin'));
|
|
693
704
|
const content = await page.content();
|
|
694
705
|
const hasAccessDenied = content.includes('401') || content.includes('403') || content.includes('unauthorized') || content.includes('forbidden');
|
|
706
|
+
const isProtected = redirectedToLogin || hasAccessDenied;
|
|
695
707
|
|
|
696
708
|
return {
|
|
697
709
|
url,
|
|
698
|
-
|
|
699
|
-
|
|
710
|
+
beforeUrl,
|
|
711
|
+
afterUrl,
|
|
712
|
+
isProtected,
|
|
713
|
+
redirectedToLogin,
|
|
714
|
+
hasAccessDenied,
|
|
700
715
|
httpStatus: hasAccessDenied ? (content.includes('403') ? 403 : 401) : 200
|
|
701
716
|
};
|
|
702
717
|
}
|
|
@@ -710,7 +725,9 @@ export class HumanBehaviorDriver {
|
|
|
710
725
|
const key = window.localStorage.key(i);
|
|
711
726
|
if (key) result[key] = window.localStorage.getItem(key);
|
|
712
727
|
}
|
|
713
|
-
} catch
|
|
728
|
+
} catch {
|
|
729
|
+
// Ignore localStorage access errors
|
|
730
|
+
}
|
|
714
731
|
return result;
|
|
715
732
|
});
|
|
716
733
|
|
|
@@ -721,7 +738,9 @@ export class HumanBehaviorDriver {
|
|
|
721
738
|
const key = window.sessionStorage.key(i);
|
|
722
739
|
if (key) result[key] = window.sessionStorage.getItem(key);
|
|
723
740
|
}
|
|
724
|
-
} catch
|
|
741
|
+
} catch {
|
|
742
|
+
// Ignore sessionStorage access errors
|
|
743
|
+
}
|
|
725
744
|
return result;
|
|
726
745
|
});
|
|
727
746
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { resolve, dirname } from 'path';
|
|
2
|
-
import { mkdirSync, readFileSync, existsSync } from 'fs';
|
|
2
|
+
import { mkdirSync, readFileSync, existsSync, writeFileSync } from 'fs';
|
|
3
3
|
import { createBrowser, navigateToUrl, closeBrowser } from './browser.js';
|
|
4
4
|
import { discoverAllInteractions } from './interaction-discovery.js';
|
|
5
5
|
import { captureScreenshot } from './evidence-capture.js';
|
|
@@ -15,7 +15,7 @@ import { deriveObservedExpectation, shouldAttemptRepeatObservedExpectation, eval
|
|
|
15
15
|
import { computeRouteBudget } from '../core/budget-engine.js';
|
|
16
16
|
import { loadPreviousSnapshot, saveSnapshot, buildSnapshot, compareSnapshots, shouldSkipInteractionIncremental } from '../core/incremental-store.js';
|
|
17
17
|
import SilenceTracker from '../core/silence-model.js';
|
|
18
|
-
import { DecisionRecorder, recordBudgetProfile, recordTimeoutConfig,
|
|
18
|
+
import { DecisionRecorder, recordBudgetProfile, recordTimeoutConfig, recordTruncation, recordEnvironment } from '../core/determinism-model.js';
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
21
|
* OBSERVE PHASE - Execute interactions and capture runtime behavior
|
|
@@ -134,8 +134,11 @@ export async function observe(url, manifestPath = null, scanBudgetOverride = nul
|
|
|
134
134
|
await navigateToUrl(page, url, scanBudget);
|
|
135
135
|
|
|
136
136
|
const projectDir = manifestPath ? dirname(dirname(dirname(manifestPath))) : process.cwd();
|
|
137
|
-
|
|
138
|
-
|
|
137
|
+
if (!runId) {
|
|
138
|
+
throw new Error('runId is required');
|
|
139
|
+
}
|
|
140
|
+
const { getScreenshotDir } = await import('../core/run-id.js');
|
|
141
|
+
const screenshotsDir = getScreenshotDir(projectDir, runId);
|
|
139
142
|
mkdirSync(screenshotsDir, { recursive: true });
|
|
140
143
|
|
|
141
144
|
const timestamp = Date.now();
|
|
@@ -307,7 +310,7 @@ export async function observe(url, manifestPath = null, scanBudgetOverride = nul
|
|
|
307
310
|
|
|
308
311
|
// Discover ALL interactions on this page
|
|
309
312
|
// Note: discoverAllInteractions already returns sorted interactions deterministically
|
|
310
|
-
const { interactions
|
|
313
|
+
const { interactions } = await discoverAllInteractions(page, baseOrigin, routeBudget);
|
|
311
314
|
totalInteractionsDiscovered += interactions.length;
|
|
312
315
|
|
|
313
316
|
// SCALE INTELLIGENCE: Apply adaptive budget cap (interactions are already sorted deterministically)
|
|
@@ -847,7 +850,7 @@ export async function observe(url, manifestPath = null, scanBudgetOverride = nul
|
|
|
847
850
|
}));
|
|
848
851
|
|
|
849
852
|
const currentSnapshot = buildSnapshot(manifest, observedInteractions);
|
|
850
|
-
saveSnapshot(projectDir, currentSnapshot);
|
|
853
|
+
saveSnapshot(projectDir, currentSnapshot, runId);
|
|
851
854
|
|
|
852
855
|
// Add incremental mode metadata to observation
|
|
853
856
|
observation.incremental = {
|
|
@@ -865,7 +868,7 @@ export async function observe(url, manifestPath = null, scanBudgetOverride = nul
|
|
|
865
868
|
mkdirSync(runsDir, { recursive: true });
|
|
866
869
|
const decisionsPath = resolve(runsDir, 'decisions.json');
|
|
867
870
|
const decisionsData = JSON.stringify(decisionRecorder.export(), null, 2);
|
|
868
|
-
|
|
871
|
+
writeFileSync(decisionsPath, decisionsData, 'utf-8');
|
|
869
872
|
}
|
|
870
873
|
|
|
871
874
|
// Phase 4: Add safety mode statistics
|
|
@@ -73,7 +73,6 @@ async function extractLabel(element) {
|
|
|
73
73
|
|
|
74
74
|
export async function discoverInteractions(page, baseOrigin, scanBudget = DEFAULT_SCAN_BUDGET) {
|
|
75
75
|
const currentUrl = page.url();
|
|
76
|
-
const interactions = [];
|
|
77
76
|
const seenElements = new Set();
|
|
78
77
|
|
|
79
78
|
const allInteractions = [];
|
|
@@ -246,7 +245,6 @@ export async function discoverInteractions(page, baseOrigin, scanBudget = DEFAUL
|
|
|
246
245
|
if (item.type === 'button' || item.type === 'link') {
|
|
247
246
|
const text = (item.text || '').trim().toLowerCase();
|
|
248
247
|
const label = (item.label || '').trim().toLowerCase();
|
|
249
|
-
const combined = `${text} ${label}`;
|
|
250
248
|
|
|
251
249
|
const isLogout = /^(logout|sign\s*out|signout|log\s*out)$/i.test(text) ||
|
|
252
250
|
/^(logout|sign\s*out|signout|log\s*out)$/i.test(label) ||
|
|
@@ -419,12 +417,16 @@ export async function discoverInteractions(page, baseOrigin, scanBudget = DEFAUL
|
|
|
419
417
|
try {
|
|
420
418
|
const box = await item.element.boundingBox();
|
|
421
419
|
if (box) {
|
|
420
|
+
// @ts-expect-error - Adding runtime properties to interaction object
|
|
422
421
|
item.boundingY = box.y;
|
|
422
|
+
// @ts-expect-error - Adding runtime properties to interaction object
|
|
423
423
|
item.boundingAvailable = true;
|
|
424
424
|
}
|
|
425
425
|
} catch (error) {
|
|
426
|
+
// @ts-expect-error - Adding runtime properties to interaction object
|
|
426
427
|
item.boundingAvailable = false;
|
|
427
428
|
}
|
|
429
|
+
// @ts-expect-error - Adding runtime properties to interaction object
|
|
428
430
|
item.priority = computePriority(item, viewportHeight);
|
|
429
431
|
}
|
|
430
432
|
|
|
@@ -457,7 +459,7 @@ export async function discoverInteractions(page, baseOrigin, scanBudget = DEFAUL
|
|
|
457
459
|
* Discover ALL interactions on a page (no priority cap).
|
|
458
460
|
* Used for full-site coverage traversal.
|
|
459
461
|
*/
|
|
460
|
-
export async function discoverAllInteractions(page, baseOrigin,
|
|
462
|
+
export async function discoverAllInteractions(page, baseOrigin, _scanBudget = DEFAULT_SCAN_BUDGET) {
|
|
461
463
|
const currentUrl = page.url();
|
|
462
464
|
const seenElements = new Set();
|
|
463
465
|
const allInteractions = [];
|
|
@@ -597,18 +599,28 @@ export async function discoverAllInteractions(page, baseOrigin, scanBudget = DEF
|
|
|
597
599
|
|
|
598
600
|
// Return ALL interactions (no priority cap)
|
|
599
601
|
return {
|
|
600
|
-
interactions: ordered.map(item =>
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
602
|
+
interactions: ordered.map(item => {
|
|
603
|
+
const mapped = {
|
|
604
|
+
type: item.type,
|
|
605
|
+
selector: item.selector,
|
|
606
|
+
label: item.label,
|
|
607
|
+
element: item.element,
|
|
608
|
+
isExternal: item.isExternal || false,
|
|
609
|
+
href: item.href,
|
|
610
|
+
text: item.text,
|
|
611
|
+
dataHref: item.dataHref,
|
|
612
|
+
// @ts-expect-error - dataDanger and dataDestructive are optional runtime properties on interaction objects
|
|
613
|
+
dataDanger: item.dataDanger || false,
|
|
614
|
+
// @ts-expect-error - dataDestructive is an optional runtime property on interaction objects
|
|
615
|
+
dataDestructive: item.dataDestructive || false
|
|
616
|
+
};
|
|
617
|
+
// hasPasswordInput only exists on form types
|
|
618
|
+
if (item.type === 'form' || item.type === 'login') {
|
|
619
|
+
// @ts-expect-error - hasPasswordInput is only on form/login types at runtime
|
|
620
|
+
mapped.hasPasswordInput = item.hasPasswordInput || false;
|
|
621
|
+
}
|
|
622
|
+
return mapped;
|
|
623
|
+
}),
|
|
612
624
|
coverage: {
|
|
613
625
|
candidatesDiscovered: allInteractions.length,
|
|
614
626
|
candidatesSelected: allInteractions.length,
|
|
@@ -12,7 +12,9 @@ import { FocusSensor } from './focus-sensor.js';
|
|
|
12
12
|
import { AriaSensor } from './aria-sensor.js';
|
|
13
13
|
import { TimingSensor } from './timing-sensor.js';
|
|
14
14
|
import { HumanBehaviorDriver } from './human-driver.js';
|
|
15
|
-
|
|
15
|
+
|
|
16
|
+
// Import CLICK_TIMEOUT_MS from human-driver (re-export needed)
|
|
17
|
+
const CLICK_TIMEOUT_MS = 2000;
|
|
16
18
|
|
|
17
19
|
/**
|
|
18
20
|
* SILENCE TRACKING: Mark timeout and record to silence tracker.
|
|
@@ -136,13 +138,11 @@ export async function runInteraction(page, interaction, timestamp, i, screenshot
|
|
|
136
138
|
let loadingWindowData = null;
|
|
137
139
|
|
|
138
140
|
let uiBefore = {};
|
|
139
|
-
let stateBefore = null;
|
|
140
|
-
let sessionStateBefore = null;
|
|
141
141
|
|
|
142
142
|
try {
|
|
143
143
|
// Capture session state before interaction for auth-aware interactions
|
|
144
144
|
if (interaction.type === 'login' || interaction.type === 'logout') {
|
|
145
|
-
|
|
145
|
+
await humanDriver.captureSessionState(page);
|
|
146
146
|
}
|
|
147
147
|
|
|
148
148
|
if (Date.now() - startTime > scanBudget.maxScanDurationMs) {
|
|
@@ -263,7 +263,7 @@ export async function runInteraction(page, interaction, timestamp, i, screenshot
|
|
|
263
263
|
const locator = interaction.element;
|
|
264
264
|
// On file:// origins, avoid long navigation waits for simple link clicks
|
|
265
265
|
const isFileOrigin = baseOrigin && baseOrigin.startsWith('file:');
|
|
266
|
-
|
|
266
|
+
let shouldWaitForNavigation = (interaction.type === 'link' || interaction.type === 'form') && !isFileOrigin;
|
|
267
267
|
let navigationResult = null;
|
|
268
268
|
|
|
269
269
|
try {
|
|
@@ -300,7 +300,7 @@ export async function runInteraction(page, interaction, timestamp, i, screenshot
|
|
|
300
300
|
}
|
|
301
301
|
} else if (interaction.type === 'logout') {
|
|
302
302
|
// Logout action: click logout and observe session changes
|
|
303
|
-
const logoutResult = await humanDriver.
|
|
303
|
+
const logoutResult = await humanDriver.performLogout(page);
|
|
304
304
|
const sessionStateAfter = await humanDriver.captureSessionState(page);
|
|
305
305
|
trace.logout = {
|
|
306
306
|
clicked: logoutResult.clicked,
|
|
@@ -84,6 +84,12 @@ export class LoadingSensor {
|
|
|
84
84
|
// Set up interval to check loading state (every 100ms for deterministic detection)
|
|
85
85
|
const intervalId = setInterval(checkLoading, 100);
|
|
86
86
|
|
|
87
|
+
// CRITICAL: Unref the interval so it doesn't keep the process alive
|
|
88
|
+
// This allows tests to exit cleanly even if stopWindow() is not called
|
|
89
|
+
if (intervalId && intervalId.unref) {
|
|
90
|
+
intervalId.unref();
|
|
91
|
+
}
|
|
92
|
+
|
|
87
93
|
// Immediately check once
|
|
88
94
|
checkLoading();
|
|
89
95
|
|
|
@@ -109,7 +109,7 @@ export class NavigationSensor {
|
|
|
109
109
|
*
|
|
110
110
|
* @param {number} windowId - Window ID
|
|
111
111
|
* @param {Object} page - Playwright page
|
|
112
|
-
* @returns {
|
|
112
|
+
* @returns {Promise<any>} - Navigation summary
|
|
113
113
|
*/
|
|
114
114
|
async stopWindow(windowId, page) {
|
|
115
115
|
const state = this.windows.get(windowId);
|
|
@@ -58,7 +58,10 @@ class ReduxSensor {
|
|
|
58
58
|
resolve();
|
|
59
59
|
} else {
|
|
60
60
|
// Wait up to 5 seconds for store initialization
|
|
61
|
-
const timeout = setTimeout(() =>
|
|
61
|
+
const timeout = setTimeout(() => {
|
|
62
|
+
clearInterval(check);
|
|
63
|
+
resolve();
|
|
64
|
+
}, 5000);
|
|
62
65
|
const check = setInterval(() => {
|
|
63
66
|
if (window.__REDUX_STORE__) {
|
|
64
67
|
clearInterval(check);
|
|
@@ -125,7 +128,7 @@ class ReduxSensor {
|
|
|
125
128
|
// Shallow copy of top-level keys only (privacy: no values)
|
|
126
129
|
const snapshot = {};
|
|
127
130
|
for (const key in state) {
|
|
128
|
-
if (
|
|
131
|
+
if (Object.prototype.hasOwnProperty.call(state, key)) {
|
|
129
132
|
snapshot[key] = '[REDACTED]'; // Never store values, only keys
|
|
130
133
|
}
|
|
131
134
|
}
|
|
@@ -222,8 +225,9 @@ class ZustandSensor {
|
|
|
222
225
|
// Look for common Zustand patterns in window object
|
|
223
226
|
for (const key in window) {
|
|
224
227
|
if (key.startsWith('use') && typeof window[key] === 'function') {
|
|
225
|
-
|
|
226
|
-
|
|
228
|
+
// Dynamic property access on window for Zustand store detection (runtime property)
|
|
229
|
+
const store = /** @type {any} */ (window[key]);
|
|
230
|
+
if (store && typeof store === 'object' && 'getState' in store && typeof store.getState === 'function') {
|
|
227
231
|
return true;
|
|
228
232
|
}
|
|
229
233
|
}
|
|
@@ -22,13 +22,14 @@ export class StateUISensor {
|
|
|
22
22
|
*/
|
|
23
23
|
async snapshot(page, contextSelector = null) {
|
|
24
24
|
try {
|
|
25
|
-
const snapshot = await page.evaluate((
|
|
25
|
+
const snapshot = await page.evaluate(() => {
|
|
26
26
|
const signals = {};
|
|
27
27
|
const rawSnapshot = {};
|
|
28
28
|
|
|
29
29
|
// 1. Dialog/Modal signals
|
|
30
30
|
signals.dialogs = [];
|
|
31
31
|
const dialogs = document.querySelectorAll('[role="dialog"]');
|
|
32
|
+
// @ts-expect-error - NodeListOf is iterable in browser context
|
|
32
33
|
for (const dialog of dialogs) {
|
|
33
34
|
const isVisible = dialog.offsetParent !== null || dialog.hasAttribute('open');
|
|
34
35
|
const ariaModal = dialog.getAttribute('aria-modal');
|
|
@@ -46,6 +47,7 @@ export class StateUISensor {
|
|
|
46
47
|
// 2. Expansion state (aria-expanded)
|
|
47
48
|
signals.expandedElements = [];
|
|
48
49
|
const expandables = document.querySelectorAll('[aria-expanded]');
|
|
50
|
+
// @ts-expect-error - NodeListOf is iterable in browser context
|
|
49
51
|
for (const el of expandables) {
|
|
50
52
|
const expanded = el.getAttribute('aria-expanded') === 'true';
|
|
51
53
|
signals.expandedElements.push({
|
|
@@ -58,6 +60,7 @@ export class StateUISensor {
|
|
|
58
60
|
// 3. Tab selection (role=tab with aria-selected)
|
|
59
61
|
signals.selectedTabs = [];
|
|
60
62
|
const tabs = document.querySelectorAll('[role="tab"]');
|
|
63
|
+
// @ts-expect-error - NodeListOf is iterable in browser context
|
|
61
64
|
for (const tab of tabs) {
|
|
62
65
|
const selected = tab.getAttribute('aria-selected') === 'true';
|
|
63
66
|
signals.selectedTabs.push({
|
|
@@ -70,6 +73,7 @@ export class StateUISensor {
|
|
|
70
73
|
// 4. Checkbox/toggle state (aria-checked)
|
|
71
74
|
signals.checkedElements = [];
|
|
72
75
|
const checkables = document.querySelectorAll('[aria-checked]');
|
|
76
|
+
// @ts-expect-error - NodeListOf is iterable in browser context
|
|
73
77
|
for (const el of checkables) {
|
|
74
78
|
const checked = el.getAttribute('aria-checked') === 'true';
|
|
75
79
|
signals.checkedElements.push({
|
|
@@ -82,6 +86,7 @@ export class StateUISensor {
|
|
|
82
86
|
// 5. Alert/Status changes (role=alert, role=status)
|
|
83
87
|
signals.alerts = [];
|
|
84
88
|
const alerts = document.querySelectorAll('[role="alert"], [role="status"]');
|
|
89
|
+
// @ts-expect-error - NodeListOf is iterable in browser context
|
|
85
90
|
for (const alert of alerts) {
|
|
86
91
|
const text = alert.textContent?.trim() || '';
|
|
87
92
|
signals.alerts.push({
|
|
@@ -96,6 +101,7 @@ export class StateUISensor {
|
|
|
96
101
|
// Count visible nodes and text nodes (excluding style/comment nodes)
|
|
97
102
|
const countMeaningfulNodes = () => {
|
|
98
103
|
let count = 0;
|
|
104
|
+
// @ts-expect-error - NodeListOf is iterable in browser context
|
|
99
105
|
for (const node of document.querySelectorAll('*')) {
|
|
100
106
|
if (node.offsetParent !== null) { // Visible
|
|
101
107
|
count++;
|
|
@@ -1,7 +1,23 @@
|
|
|
1
|
-
import { resolve } from 'path';
|
|
2
1
|
import { writeFileSync, mkdirSync } from 'fs';
|
|
3
2
|
import { getArtifactPath, getRunArtifactDir } from '../core/run-id.js';
|
|
4
3
|
|
|
4
|
+
/**
|
|
5
|
+
* @typedef {Object} WriteTracesResult
|
|
6
|
+
* @property {number} version
|
|
7
|
+
* @property {string} observedAt
|
|
8
|
+
* @property {string} url
|
|
9
|
+
* @property {Array} traces
|
|
10
|
+
* @property {Array} [observedExpectations]
|
|
11
|
+
* @property {Object} [coverage]
|
|
12
|
+
* @property {Array} [warnings]
|
|
13
|
+
* @property {Object} [silences] - Added by writeTraces if silenceTracker provided
|
|
14
|
+
* @property {string} tracesPath
|
|
15
|
+
* @property {Object} observeTruth
|
|
16
|
+
* @property {Object} [expectationExecution] - Added by caller after writeTraces
|
|
17
|
+
* @property {Array} [expectationCoverageGaps] - Added by caller after writeTraces
|
|
18
|
+
* @property {Object} [incremental] - Added by caller after writeTraces
|
|
19
|
+
*/
|
|
20
|
+
|
|
5
21
|
/**
|
|
6
22
|
* SILENCE TRACKING: Write observation traces with explicit silence tracking.
|
|
7
23
|
* All gaps, skips, caps, and unknowns must be recorded and surfaced.
|
|
@@ -11,24 +27,19 @@ import { getArtifactPath, getRunArtifactDir } from '../core/run-id.js';
|
|
|
11
27
|
* @param {string} projectDir - Project directory
|
|
12
28
|
* @param {string} url - URL observed
|
|
13
29
|
* @param {Array} traces - Execution traces
|
|
14
|
-
* @param {Object} coverage - Coverage data (if capped, this is a silence)
|
|
15
|
-
* @param {Array} warnings - Warnings (caps are silences)
|
|
16
|
-
* @param {Array} observedExpectations - Observed expectations
|
|
17
|
-
* @param {Object} silenceTracker - Silence tracker (optional)
|
|
18
|
-
* @param {string} runId - Run identifier (Phase 5)
|
|
30
|
+
* @param {Object} [coverage] - Coverage data (if capped, this is a silence)
|
|
31
|
+
* @param {Array} [warnings] - Warnings (caps are silences)
|
|
32
|
+
* @param {Array} [observedExpectations] - Observed expectations
|
|
33
|
+
* @param {Object} [silenceTracker] - Silence tracker (optional)
|
|
34
|
+
* @param {string} [runId] - Run identifier (Phase 5) - required but optional in signature for type compatibility
|
|
35
|
+
* @returns {WriteTracesResult}
|
|
19
36
|
*/
|
|
20
37
|
export function writeTraces(projectDir, url, traces, coverage = null, warnings = [], observedExpectations = [], silenceTracker = null, runId = null) {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
if (runId) {
|
|
24
|
-
observeDir = getRunArtifactDir(projectDir, runId);
|
|
25
|
-
tracesPath = getArtifactPath(projectDir, runId, 'traces.json');
|
|
26
|
-
} else {
|
|
27
|
-
// Backwards compatibility for tests
|
|
28
|
-
observeDir = resolve(projectDir, '.veraxverax', 'observe');
|
|
29
|
-
tracesPath = resolve(observeDir, 'observation-traces.json');
|
|
38
|
+
if (!runId) {
|
|
39
|
+
throw new Error('runId is required');
|
|
30
40
|
}
|
|
31
|
-
|
|
41
|
+
const observeDir = getRunArtifactDir(projectDir, runId);
|
|
42
|
+
const tracesPath = getArtifactPath(projectDir, runId, 'traces.json');
|
|
32
43
|
mkdirSync(observeDir, { recursive: true });
|
|
33
44
|
|
|
34
45
|
const observation = {
|
|
@@ -41,6 +41,7 @@ export class UISignalSensor {
|
|
|
41
41
|
const statusRegions = Array.from(document.querySelectorAll('[role="status"], [role="alert"]'));
|
|
42
42
|
const visibleStatusRegions = statusRegions.filter((el) => {
|
|
43
43
|
const style = window.getComputedStyle(el);
|
|
44
|
+
// @ts-expect-error - offsetParent exists on HTMLElement in browser context
|
|
44
45
|
return el.offsetParent !== null && style.visibility !== 'hidden' && style.display !== 'none' && style.opacity !== '0';
|
|
45
46
|
});
|
|
46
47
|
if (visibleStatusRegions.length > 0) {
|
|
@@ -57,6 +58,7 @@ export class UISignalSensor {
|
|
|
57
58
|
|
|
58
59
|
// Check for dialogs
|
|
59
60
|
const dialog = document.querySelector('[role="dialog"], [aria-modal="true"]');
|
|
61
|
+
// @ts-expect-error - offsetParent exists on HTMLElement in browser context
|
|
60
62
|
if (dialog && dialog.offsetParent !== null) {
|
|
61
63
|
// offsetParent is null if element is hidden
|
|
62
64
|
result.hasDialog = true;
|
|
@@ -90,6 +92,7 @@ export class UISignalSensor {
|
|
|
90
92
|
|
|
91
93
|
for (const invalidEl of invalidElements) {
|
|
92
94
|
const style = window.getComputedStyle(invalidEl);
|
|
95
|
+
// @ts-expect-error - offsetParent exists on HTMLElement in browser context
|
|
93
96
|
const isVisible = invalidEl.offsetParent !== null &&
|
|
94
97
|
style.visibility !== 'hidden' &&
|
|
95
98
|
style.display !== 'none' &&
|
|
@@ -120,6 +123,7 @@ export class UISignalSensor {
|
|
|
120
123
|
const errorText = Array.from(parent.querySelectorAll('[role="alert"], .error, .invalid-feedback'))
|
|
121
124
|
.find(el => {
|
|
122
125
|
const elStyle = window.getComputedStyle(el);
|
|
126
|
+
// @ts-expect-error - offsetParent exists on HTMLElement in browser context
|
|
123
127
|
return el.offsetParent !== null &&
|
|
124
128
|
elStyle.visibility !== 'hidden' &&
|
|
125
129
|
elStyle.display !== 'none' &&
|
|
@@ -138,6 +142,7 @@ export class UISignalSensor {
|
|
|
138
142
|
const alertRegions = Array.from(document.querySelectorAll('[role="alert"], [role="status"]'));
|
|
139
143
|
const visibleAlertRegions = alertRegions.filter((el) => {
|
|
140
144
|
const style = window.getComputedStyle(el);
|
|
145
|
+
// @ts-expect-error - offsetParent exists on HTMLElement in browser context
|
|
141
146
|
const isVisible = el.offsetParent !== null &&
|
|
142
147
|
style.visibility !== 'hidden' &&
|
|
143
148
|
style.display !== 'none' &&
|
|
@@ -149,6 +154,7 @@ export class UISignalSensor {
|
|
|
149
154
|
const liveRegions = Array.from(document.querySelectorAll('[aria-live]'));
|
|
150
155
|
const visibleLiveRegions = liveRegions.filter((el) => {
|
|
151
156
|
const style = window.getComputedStyle(el);
|
|
157
|
+
// @ts-expect-error - offsetParent exists on HTMLElement in browser context
|
|
152
158
|
const isVisible = el.offsetParent !== null &&
|
|
153
159
|
style.visibility !== 'hidden' &&
|
|
154
160
|
style.display !== 'none' &&
|
|
@@ -176,6 +182,7 @@ export class UISignalSensor {
|
|
|
176
182
|
'[role="alert"], [class*="error"], [class*="danger"]'
|
|
177
183
|
);
|
|
178
184
|
if (errorMessages.length > 0) {
|
|
185
|
+
// @ts-expect-error - NodeListOf is iterable in browser context
|
|
179
186
|
for (const elem of errorMessages) {
|
|
180
187
|
const text = elem.textContent.trim().slice(0, 50);
|
|
181
188
|
if (text && (text.toLowerCase().includes('error') || text.toLowerCase().includes('fail'))) {
|
|
@@ -4,8 +4,11 @@ import { computeExpectationsSummary } from './shared/artifact-manager.js';
|
|
|
4
4
|
import { createImpactSummary } from './core/silence-impact.js';
|
|
5
5
|
import { computeDecisionSnapshot } from './core/decision-snapshot.js';
|
|
6
6
|
|
|
7
|
-
export function writeScanSummary(projectDir, url, projectType, learnTruth, observeTruth, detectTruth, manifestPath, tracesPath, findingsPath, runDirOpt
|
|
8
|
-
|
|
7
|
+
export function writeScanSummary(projectDir, url, projectType, learnTruth, observeTruth, detectTruth, manifestPath, tracesPath, findingsPath, runDirOpt, findingsArray = null) {
|
|
8
|
+
if (!runDirOpt) {
|
|
9
|
+
throw new Error('runDirOpt is required');
|
|
10
|
+
}
|
|
11
|
+
const scanDir = resolve(runDirOpt);
|
|
9
12
|
mkdirSync(scanDir, { recursive: true });
|
|
10
13
|
|
|
11
14
|
// Compute expectations summary from manifest
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* - EXHAUSTIVE: 300 seconds, maximum coverage (deep audit)
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import {
|
|
14
|
+
import { createScanBudget } from './scan-budget.js';
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* QUICK profile: Fast feedback for development
|
|
@@ -107,7 +107,7 @@ export function getActiveBudgetProfile() {
|
|
|
107
107
|
|
|
108
108
|
/**
|
|
109
109
|
* Create a scan budget with the active profile applied.
|
|
110
|
-
* @returns {
|
|
110
|
+
* @returns {Object} Complete scan budget with profile applied
|
|
111
111
|
*/
|
|
112
112
|
export function createScanBudgetWithProfile() {
|
|
113
113
|
const profile = getActiveBudgetProfile();
|