@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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { chromium } from 'playwright';
|
|
2
2
|
import { writeFileSync, mkdirSync } from 'fs';
|
|
3
|
-
import { resolve
|
|
3
|
+
import { resolve } from 'path';
|
|
4
4
|
import { redactHeaders, redactUrl, redactBody, redactConsole, getRedactionCounters } from './redact.js';
|
|
5
5
|
|
|
6
6
|
/**
|
|
@@ -60,9 +60,16 @@ export async function observeExpectations(expectations, url, evidencePath, onPro
|
|
|
60
60
|
});
|
|
61
61
|
});
|
|
62
62
|
|
|
63
|
-
// Navigate to base URL first
|
|
63
|
+
// Navigate to base URL first with explicit timeout
|
|
64
64
|
try {
|
|
65
|
-
await page.goto(url, {
|
|
65
|
+
await page.goto(url, {
|
|
66
|
+
waitUntil: 'domcontentloaded', // Use domcontentloaded instead of networkidle for faster timeout
|
|
67
|
+
timeout: 30000
|
|
68
|
+
});
|
|
69
|
+
// Wait for network idle with separate timeout
|
|
70
|
+
await page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {
|
|
71
|
+
// Network idle timeout is acceptable, continue
|
|
72
|
+
});
|
|
66
73
|
} catch (error) {
|
|
67
74
|
// Continue even if initial load fails
|
|
68
75
|
if (onProgress) {
|
|
@@ -96,6 +103,7 @@ export async function observeExpectations(expectations, url, evidencePath, onPro
|
|
|
96
103
|
type: exp.type,
|
|
97
104
|
promise: exp.promise,
|
|
98
105
|
source: exp.source,
|
|
106
|
+
attempted: false,
|
|
99
107
|
observed: false,
|
|
100
108
|
observedAt: null,
|
|
101
109
|
evidenceFiles: [],
|
|
@@ -107,6 +115,7 @@ export async function observeExpectations(expectations, url, evidencePath, onPro
|
|
|
107
115
|
let evidence = null;
|
|
108
116
|
|
|
109
117
|
if (exp.type === 'navigation') {
|
|
118
|
+
observation.attempted = true; // Mark as attempted
|
|
110
119
|
result = await observeNavigation(
|
|
111
120
|
page,
|
|
112
121
|
exp,
|
|
@@ -117,6 +126,7 @@ export async function observeExpectations(expectations, url, evidencePath, onPro
|
|
|
117
126
|
);
|
|
118
127
|
evidence = result ? `nav_${expNum}_after.png` : null;
|
|
119
128
|
} else if (exp.type === 'network') {
|
|
129
|
+
observation.attempted = true; // Mark as attempted
|
|
120
130
|
result = await observeNetwork(page, exp, networkLogs, 5000);
|
|
121
131
|
if (result) {
|
|
122
132
|
const evidenceFile = `network_${expNum}.json`;
|
|
@@ -135,6 +145,7 @@ export async function observeExpectations(expectations, url, evidencePath, onPro
|
|
|
135
145
|
evidence = null;
|
|
136
146
|
}
|
|
137
147
|
} else if (exp.type === 'state') {
|
|
148
|
+
observation.attempted = true; // Mark as attempted
|
|
138
149
|
result = await observeState(page, exp, evidencePath, expNum);
|
|
139
150
|
evidence = result ? `state_${expNum}_after.png` : null;
|
|
140
151
|
}
|
|
@@ -187,19 +198,47 @@ export async function observeExpectations(expectations, url, evidencePath, onPro
|
|
|
187
198
|
observedAt: new Date().toISOString(),
|
|
188
199
|
};
|
|
189
200
|
} finally {
|
|
190
|
-
//
|
|
201
|
+
// Robust cleanup: ensure browser/context/page are closed
|
|
202
|
+
// Remove all event listeners to prevent leaks
|
|
191
203
|
if (page) {
|
|
192
204
|
try {
|
|
193
|
-
|
|
205
|
+
// Remove all listeners
|
|
206
|
+
page.removeAllListeners();
|
|
207
|
+
// @ts-expect-error - Playwright page.close() doesn't accept timeout option, but we use it for safety
|
|
208
|
+
await page.close({ timeout: 5000 }).catch(() => {});
|
|
194
209
|
} catch (e) {
|
|
195
|
-
// Ignore close errors
|
|
210
|
+
// Ignore close errors but emit warning if onProgress available
|
|
211
|
+
if (onProgress) {
|
|
212
|
+
onProgress({
|
|
213
|
+
event: 'observe:warning',
|
|
214
|
+
message: `Page cleanup warning: ${e.message}`,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
196
217
|
}
|
|
197
218
|
}
|
|
219
|
+
|
|
220
|
+
// Close browser context if it exists
|
|
198
221
|
if (browser) {
|
|
199
222
|
try {
|
|
200
|
-
|
|
223
|
+
const contexts = browser.contexts();
|
|
224
|
+
for (const context of contexts) {
|
|
225
|
+
try {
|
|
226
|
+
// @ts-expect-error - Playwright context.close() doesn't accept timeout option, but we use it for safety
|
|
227
|
+
await context.close({ timeout: 5000 }).catch(() => {});
|
|
228
|
+
} catch (e) {
|
|
229
|
+
// Ignore context close errors
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
// @ts-expect-error - Playwright browser.close() doesn't accept timeout option, but we use it for safety
|
|
233
|
+
await browser.close({ timeout: 5000 }).catch(() => {});
|
|
201
234
|
} catch (e) {
|
|
202
|
-
// Ignore close errors
|
|
235
|
+
// Ignore browser close errors but emit warning if onProgress available
|
|
236
|
+
if (onProgress) {
|
|
237
|
+
onProgress({
|
|
238
|
+
event: 'observe:warning',
|
|
239
|
+
message: `Browser cleanup warning: ${e.message}`,
|
|
240
|
+
});
|
|
241
|
+
}
|
|
203
242
|
}
|
|
204
243
|
}
|
|
205
244
|
}
|
|
@@ -242,6 +281,7 @@ async function observeNavigation(page, expectation, baseUrl, visitedUrls, eviden
|
|
|
242
281
|
await page.click(`a[href="${element.href}"]`);
|
|
243
282
|
} catch (e2) {
|
|
244
283
|
// Try clicking by text content
|
|
284
|
+
// eslint-disable-next-line no-undef
|
|
245
285
|
const text = await page.evaluate((href) => {
|
|
246
286
|
const anchors = Array.from(document.querySelectorAll('a'));
|
|
247
287
|
const found = anchors.find(a => a.getAttribute('href') === href);
|
|
@@ -254,14 +294,23 @@ async function observeNavigation(page, expectation, baseUrl, visitedUrls, eviden
|
|
|
254
294
|
}
|
|
255
295
|
}
|
|
256
296
|
|
|
257
|
-
// Wait for navigation or SPA update
|
|
297
|
+
// Wait for navigation or SPA update with explicit timeout
|
|
258
298
|
try {
|
|
259
|
-
await page.waitForNavigation({
|
|
299
|
+
await page.waitForNavigation({
|
|
300
|
+
waitUntil: 'domcontentloaded',
|
|
301
|
+
timeout: 5000
|
|
302
|
+
}).catch(() => {
|
|
303
|
+
// Navigation timeout is acceptable for SPAs
|
|
304
|
+
});
|
|
305
|
+
// Wait for network idle with separate timeout
|
|
306
|
+
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {
|
|
307
|
+
// Network idle timeout is acceptable
|
|
308
|
+
});
|
|
260
309
|
} catch (e) {
|
|
261
|
-
// Navigation might not happen
|
|
310
|
+
// Navigation might not happen, continue
|
|
262
311
|
}
|
|
263
312
|
|
|
264
|
-
// Wait for potential SPA updates
|
|
313
|
+
// Wait for potential SPA updates (bounded)
|
|
265
314
|
await page.waitForTimeout(300);
|
|
266
315
|
|
|
267
316
|
// Screenshot after interaction
|
|
@@ -304,13 +353,21 @@ async function observeNetwork(page, expectation, networkLogs, timeoutMs) {
|
|
|
304
353
|
if (found) {
|
|
305
354
|
clearInterval(checkTimer);
|
|
306
355
|
resolve(true);
|
|
356
|
+
return;
|
|
307
357
|
}
|
|
308
358
|
|
|
309
359
|
if (Date.now() - startTime > timeoutMs) {
|
|
310
360
|
clearInterval(checkTimer);
|
|
311
361
|
resolve(false);
|
|
362
|
+
return;
|
|
312
363
|
}
|
|
313
364
|
}, 100);
|
|
365
|
+
|
|
366
|
+
// CRITICAL: Unref the interval so it doesn't keep the process alive
|
|
367
|
+
// This allows tests to exit cleanly even if interval is not cleared
|
|
368
|
+
if (checkTimer && checkTimer.unref) {
|
|
369
|
+
checkTimer.unref();
|
|
370
|
+
}
|
|
314
371
|
});
|
|
315
372
|
}
|
|
316
373
|
|
|
@@ -353,14 +410,3 @@ async function observeState(page, expectation, evidencePath, expNum) {
|
|
|
353
410
|
}
|
|
354
411
|
}
|
|
355
412
|
|
|
356
|
-
/**
|
|
357
|
-
* Check if page content changed (for SPA detection)
|
|
358
|
-
*/
|
|
359
|
-
async function checkPageContentChanged(page) {
|
|
360
|
-
try {
|
|
361
|
-
const bodyText = await page.locator('body').textContent();
|
|
362
|
-
return bodyText && bodyText.length > 0;
|
|
363
|
-
} catch (error) {
|
|
364
|
-
return false;
|
|
365
|
-
}
|
|
366
|
-
}
|
package/src/cli/util/paths.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import { join } from 'path';
|
|
1
|
+
import { join, isAbsolute } from 'path';
|
|
2
2
|
import { mkdirSync } from 'fs';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Build run artifact paths
|
|
6
6
|
*/
|
|
7
7
|
export function getRunPaths(projectRoot, outDir, runId) {
|
|
8
|
-
const
|
|
8
|
+
const outBase = isAbsolute(outDir) ? outDir : join(projectRoot, outDir);
|
|
9
|
+
const baseDir = join(outBase, 'runs', runId);
|
|
9
10
|
|
|
10
11
|
return {
|
|
11
12
|
baseDir,
|
|
@@ -4,8 +4,22 @@ import { resolve, dirname } from 'path';
|
|
|
4
4
|
/**
|
|
5
5
|
* Project Discovery Module
|
|
6
6
|
* Detects framework, router, source root, and dev server configuration
|
|
7
|
+
*
|
|
8
|
+
* @typedef {Object} ProjectProfile
|
|
9
|
+
* @property {string} framework
|
|
10
|
+
* @property {string|null} router
|
|
11
|
+
* @property {string} sourceRoot
|
|
12
|
+
* @property {string} packageManager
|
|
13
|
+
* @property {{dev: string|null, build: string|null, start: string|null}} scripts
|
|
14
|
+
* @property {string} detectedAt
|
|
15
|
+
* @property {string|null} packageJsonPath
|
|
16
|
+
* @property {number} [fileCount] - Optional file count for budget calculation
|
|
7
17
|
*/
|
|
8
18
|
|
|
19
|
+
/**
|
|
20
|
+
* @param {string} srcPath
|
|
21
|
+
* @returns {Promise<ProjectProfile>}
|
|
22
|
+
*/
|
|
9
23
|
export async function discoverProject(srcPath) {
|
|
10
24
|
const projectRoot = resolve(srcPath);
|
|
11
25
|
|
|
@@ -62,6 +76,12 @@ function findPackageJson(startPath) {
|
|
|
62
76
|
return immediatePackage;
|
|
63
77
|
}
|
|
64
78
|
|
|
79
|
+
// For static HTML projects, don't walk up - use the startPath as project root
|
|
80
|
+
// This prevents finding parent package.json files that aren't relevant
|
|
81
|
+
if (hasStaticHtml(currentPath)) {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
65
85
|
// Then walk up (limit to 5 levels for monorepos, not 10)
|
|
66
86
|
for (let i = 0; i < 5; i++) {
|
|
67
87
|
const parentPath = dirname(currentPath);
|
package/src/cli/util/redact.js
CHANGED
|
@@ -105,14 +105,14 @@ export function redactTokensInText(text, counters = { headersRedacted: 0, tokens
|
|
|
105
105
|
});
|
|
106
106
|
|
|
107
107
|
// Bearer tokens
|
|
108
|
-
output = output.replace(/Bearer\s+([A-Za-z0-9._-]+)/gi, (
|
|
108
|
+
output = output.replace(/Bearer\s+([A-Za-z0-9._-]+)/gi, (_match, _token) => {
|
|
109
109
|
c.tokensRedacted += 1;
|
|
110
110
|
return `Bearer ${REDACTED}`;
|
|
111
111
|
});
|
|
112
112
|
|
|
113
113
|
// JWT-like strings (three base64url-ish segments)
|
|
114
114
|
// More specific: require uppercase or numbers, not just domain patterns like "api.example.com"
|
|
115
|
-
output = output.replace(/[A-Z0-9][A-Za-z0-9_-]*\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g, (
|
|
115
|
+
output = output.replace(/[A-Z0-9][A-Za-z0-9_-]*\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g, (_match) => {
|
|
116
116
|
c.tokensRedacted += 1;
|
|
117
117
|
return REDACTED;
|
|
118
118
|
});
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime Budget Model
|
|
3
|
+
* Computes timeouts based on project size, execution mode, and framework
|
|
4
|
+
* Ensures deterministic, bounded execution times
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @typedef {Object} RuntimeBudgetOptions
|
|
9
|
+
* @property {number} [expectationsCount=0] - Number of expectations to process
|
|
10
|
+
* @property {string} [mode='default'] - Execution mode: 'default', 'run', 'ci'
|
|
11
|
+
* @property {string} [framework='unknown'] - Detected framework (optional)
|
|
12
|
+
* @property {number|null} [fileCount=null] - Number of files scanned (optional, fallback to expectationsCount)
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Compute runtime budgets for a VERAX run
|
|
17
|
+
* @param {RuntimeBudgetOptions} [options={}] - Budget computation options
|
|
18
|
+
* @returns {Object} Budget object with phase timeouts
|
|
19
|
+
*/
|
|
20
|
+
export function computeRuntimeBudget(options = {}) {
|
|
21
|
+
const {
|
|
22
|
+
expectationsCount = 0,
|
|
23
|
+
mode = 'default',
|
|
24
|
+
framework = 'unknown',
|
|
25
|
+
fileCount = null,
|
|
26
|
+
} = options;
|
|
27
|
+
|
|
28
|
+
// TEST MODE OVERRIDE: Fixed deterministic budgets for integration tests
|
|
29
|
+
if (process.env.VERAX_TEST_MODE === '1') {
|
|
30
|
+
return {
|
|
31
|
+
totalMaxMs: 30000, // Hard cap per run
|
|
32
|
+
learnMaxMs: 5000, // Keep learn bounded
|
|
33
|
+
observeMaxMs: 20000, // Deterministic observe budget
|
|
34
|
+
detectMaxMs: 5000, // Bounded detect
|
|
35
|
+
perExpectationMaxMs: 5000, // Deterministic per-expectation guard
|
|
36
|
+
mode: 'test',
|
|
37
|
+
framework,
|
|
38
|
+
expectationsCount,
|
|
39
|
+
projectSize: fileCount !== null ? fileCount : expectationsCount,
|
|
40
|
+
frameworkMultiplier: 1.0,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Use file count if available, otherwise use expectations count as proxy
|
|
45
|
+
const projectSize = fileCount !== null ? fileCount : expectationsCount;
|
|
46
|
+
|
|
47
|
+
// Base timeouts (milliseconds)
|
|
48
|
+
// Small project: < 10 expectations/files
|
|
49
|
+
// Medium project: 10-50 expectations/files
|
|
50
|
+
// Large project: > 50 expectations/files
|
|
51
|
+
|
|
52
|
+
// Learn phase: file scanning and AST parsing
|
|
53
|
+
const learnBaseMs = mode === 'ci' ? 30000 : 60000; // CI: 30s, default: 60s
|
|
54
|
+
const learnPerFileMs = 50; // 50ms per file
|
|
55
|
+
const learnMaxMs = mode === 'ci' ? 120000 : 300000; // CI: 2min, default: 5min
|
|
56
|
+
|
|
57
|
+
// Observe phase: browser automation
|
|
58
|
+
const observeBaseMs = mode === 'ci' ? 60000 : 120000; // CI: 1min, default: 2min
|
|
59
|
+
const observePerExpectationMs = mode === 'ci' ? 2000 : 5000; // CI: 2s, default: 5s per expectation
|
|
60
|
+
const observeMaxMs = mode === 'ci' ? 600000 : 1800000; // CI: 10min, default: 30min
|
|
61
|
+
|
|
62
|
+
// Detect phase: analysis and comparison
|
|
63
|
+
const detectBaseMs = mode === 'ci' ? 15000 : 30000; // CI: 15s, default: 30s
|
|
64
|
+
const detectPerExpectationMs = 100; // 100ms per expectation
|
|
65
|
+
const detectMaxMs = mode === 'ci' ? 120000 : 300000; // CI: 2min, default: 5min
|
|
66
|
+
|
|
67
|
+
// Per-expectation timeout during observe phase
|
|
68
|
+
const perExpectationBaseMs = mode === 'ci' ? 10000 : 30000; // CI: 10s, default: 30s
|
|
69
|
+
const perExpectationMaxMs = 120000; // 2min max per expectation
|
|
70
|
+
|
|
71
|
+
// Framework weighting (some frameworks may need more time)
|
|
72
|
+
let frameworkMultiplier = 1.0;
|
|
73
|
+
if (framework === 'nextjs' || framework === 'remix') {
|
|
74
|
+
frameworkMultiplier = 1.2; // SSR frameworks may need slightly more time
|
|
75
|
+
} else if (framework === 'react' || framework === 'vue') {
|
|
76
|
+
frameworkMultiplier = 1.1; // SPA frameworks
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Compute phase budgets
|
|
80
|
+
const computedLearnMaxMs = Math.min(
|
|
81
|
+
learnBaseMs + (projectSize * learnPerFileMs * frameworkMultiplier),
|
|
82
|
+
learnMaxMs
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
const computedObserveMaxMs = Math.min(
|
|
86
|
+
observeBaseMs + (expectationsCount * observePerExpectationMs * frameworkMultiplier),
|
|
87
|
+
observeMaxMs
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
const computedDetectMaxMs = Math.min(
|
|
91
|
+
detectBaseMs + (expectationsCount * detectPerExpectationMs * frameworkMultiplier),
|
|
92
|
+
detectMaxMs
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
const computedPerExpectationMaxMs = Math.min(
|
|
96
|
+
perExpectationBaseMs * frameworkMultiplier,
|
|
97
|
+
perExpectationMaxMs
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
// Global watchdog timeout (must be >= sum of all phases + buffer)
|
|
101
|
+
// Add 30s buffer for finalization
|
|
102
|
+
const totalMaxMs = Math.max(
|
|
103
|
+
computedLearnMaxMs + computedObserveMaxMs + computedDetectMaxMs + 30000,
|
|
104
|
+
mode === 'ci' ? 900000 : 2400000 // CI: 15min minimum, default: 40min minimum
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
// Cap global timeout
|
|
108
|
+
const totalMaxMsCap = mode === 'ci' ? 1800000 : 3600000; // CI: 30min, default: 60min
|
|
109
|
+
const finalTotalMaxMs = Math.min(totalMaxMs, totalMaxMsCap);
|
|
110
|
+
|
|
111
|
+
// Ensure minimums are met
|
|
112
|
+
const finalLearnMaxMs = Math.max(computedLearnMaxMs, 10000); // At least 10s
|
|
113
|
+
const finalObserveMaxMs = Math.max(computedObserveMaxMs, 30000); // At least 30s
|
|
114
|
+
const finalDetectMaxMs = Math.max(computedDetectMaxMs, 5000); // At least 5s
|
|
115
|
+
const finalPerExpectationMaxMs = Math.max(computedPerExpectationMaxMs, 5000); // At least 5s
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
totalMaxMs: finalTotalMaxMs,
|
|
119
|
+
learnMaxMs: finalLearnMaxMs,
|
|
120
|
+
observeMaxMs: finalObserveMaxMs,
|
|
121
|
+
detectMaxMs: finalDetectMaxMs,
|
|
122
|
+
perExpectationMaxMs: finalPerExpectationMaxMs,
|
|
123
|
+
mode,
|
|
124
|
+
framework,
|
|
125
|
+
expectationsCount,
|
|
126
|
+
projectSize,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Create a timeout wrapper that rejects after specified milliseconds
|
|
132
|
+
* @param {number} timeoutMs - Timeout in milliseconds
|
|
133
|
+
* @param {Promise} promise - Promise to wrap
|
|
134
|
+
* @param {string} phase - Phase name for error messages
|
|
135
|
+
* @returns {Promise} Promise that rejects on timeout
|
|
136
|
+
*/
|
|
137
|
+
export function withTimeout(timeoutMs, promise, phase = 'unknown') {
|
|
138
|
+
return Promise.race([
|
|
139
|
+
promise,
|
|
140
|
+
new Promise((_, reject) => {
|
|
141
|
+
setTimeout(() => {
|
|
142
|
+
reject(new Error(`Phase timeout: ${phase} exceeded ${timeoutMs}ms`));
|
|
143
|
+
}, timeoutMs);
|
|
144
|
+
}),
|
|
145
|
+
]);
|
|
146
|
+
}
|
|
147
|
+
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { atomicWriteJson } from './atomic-write.js';
|
|
2
|
-
import { resolve } from 'path';
|
|
3
2
|
|
|
4
3
|
/**
|
|
5
4
|
* Write summary.json with deterministic digest
|
|
@@ -15,6 +14,18 @@ export function writeSummaryJson(summaryPath, summaryData, stats = {}) {
|
|
|
15
14
|
command: summaryData.command,
|
|
16
15
|
url: summaryData.url,
|
|
17
16
|
notes: summaryData.notes,
|
|
17
|
+
metrics: summaryData.metrics || {
|
|
18
|
+
learnMs: stats.learnMs || 0,
|
|
19
|
+
observeMs: stats.observeMs || 0,
|
|
20
|
+
detectMs: stats.detectMs || 0,
|
|
21
|
+
totalMs: stats.totalMs || 0,
|
|
22
|
+
},
|
|
23
|
+
findingsCounts: summaryData.findingsCounts || {
|
|
24
|
+
HIGH: stats.HIGH || 0,
|
|
25
|
+
MEDIUM: stats.MEDIUM || 0,
|
|
26
|
+
LOW: stats.LOW || 0,
|
|
27
|
+
UNKNOWN: stats.UNKNOWN || 0,
|
|
28
|
+
},
|
|
18
29
|
|
|
19
30
|
// Stable digest that should be identical across repeated runs on same input
|
|
20
31
|
digest: {
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Global type declarations for VERAX
|
|
3
|
+
* These extend built-in types to support runtime-injected properties
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Extend Window interface for browser-injected properties
|
|
7
|
+
declare global {
|
|
8
|
+
interface Window {
|
|
9
|
+
__veraxNavTracking?: any;
|
|
10
|
+
next?: any;
|
|
11
|
+
__REDUX_STORE__?: any;
|
|
12
|
+
store?: any;
|
|
13
|
+
__REDUX_DEVTOOLS_EXTENSION__?: any;
|
|
14
|
+
__REACT_DEVTOOLS_GLOBAL_HOOK__?: any;
|
|
15
|
+
__VERAX_STATE_SENSOR__?: any;
|
|
16
|
+
__unhandledRejections?: any[];
|
|
17
|
+
__ZUSTAND_STORE__?: any;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Playwright Page type (imported from playwright)
|
|
22
|
+
import type { Page as PlaywrightPage } from 'playwright';
|
|
23
|
+
|
|
24
|
+
// Re-export for use in JS files
|
|
25
|
+
export type Page = PlaywrightPage;
|
|
26
|
+
|
|
27
|
+
export {};
|
|
28
|
+
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TypeScript AST Node type extensions
|
|
3
|
+
* These extend the base Node type to include properties used by VERAX
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type * as ts from 'typescript';
|
|
7
|
+
|
|
8
|
+
declare module 'typescript' {
|
|
9
|
+
interface Node {
|
|
10
|
+
attributes?: ts.NodeArray<ts.JSDocAttribute>;
|
|
11
|
+
tagName?: ts.Identifier;
|
|
12
|
+
body?: ts.Node;
|
|
13
|
+
arguments?: ts.NodeArray<ts.Expression>;
|
|
14
|
+
initializer?: ts.Expression;
|
|
15
|
+
expression?: ts.Expression;
|
|
16
|
+
children?: ts.NodeArray<ts.Node>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
namespace ts {
|
|
20
|
+
// Add isFalseKeyword if it doesn't exist (it might be in a different version)
|
|
21
|
+
function isFalseKeyword(node: ts.Node): node is ts.FalseKeyword;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
package/src/verax/cli/doctor.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Checks environment, dependencies, and project setup.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import { mkdirSync, writeFileSync, unlinkSync } from 'fs';
|
|
8
8
|
import { resolve } from 'path';
|
|
9
9
|
import { chromium } from 'playwright';
|
|
10
10
|
import { get } from 'http';
|
|
@@ -182,7 +182,7 @@ async function checkUrlReachability(url) {
|
|
|
182
182
|
* @returns {Promise<Object>} Doctor results
|
|
183
183
|
*/
|
|
184
184
|
export async function runDoctor(options = {}) {
|
|
185
|
-
const { projectRoot = process.cwd(), url = null, json = false } = options;
|
|
185
|
+
const { projectRoot = process.cwd(), url = null, json: _json = false } = options;
|
|
186
186
|
|
|
187
187
|
const checks = [];
|
|
188
188
|
let overallStatus = 'ok';
|
package/src/verax/cli/init.js
CHANGED
|
@@ -68,11 +68,21 @@ export function checkUrlSafety(url) {
|
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
/**
|
|
72
|
+
* @typedef {Object} ReadlineInterface
|
|
73
|
+
* @property {function(string): Promise<string>} question - Prompt user with question
|
|
74
|
+
* @property {function(): void} close - Close the readline interface
|
|
75
|
+
*/
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* @typedef {Object} ConfirmExternalUrlOptions
|
|
79
|
+
* @property {ReadlineInterface} [readlineInterface] - Readline interface (injectable)
|
|
80
|
+
*/
|
|
81
|
+
|
|
71
82
|
/**
|
|
72
83
|
* Prompt for external URL confirmation
|
|
73
84
|
* @param {string} hostname - Hostname to confirm
|
|
74
|
-
* @param {
|
|
75
|
-
* @param {Function} options.readlineInterface - Readline interface (injectable)
|
|
85
|
+
* @param {ConfirmExternalUrlOptions} [options={}] - Options
|
|
76
86
|
* @returns {Promise<boolean>} True if confirmed
|
|
77
87
|
*/
|
|
78
88
|
export async function confirmExternalUrl(hostname, options = {}) {
|
package/src/verax/cli/wizard.js
CHANGED
|
@@ -4,8 +4,15 @@
|
|
|
4
4
|
* Guides users through VERAX configuration with friendly prompts.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* @typedef {Object} ReadlineInterface
|
|
9
|
+
* @property {function(string): Promise<string>} question - Prompt user with question
|
|
10
|
+
* @property {function(): void} close - Close the readline interface
|
|
11
|
+
*/
|
|
12
|
+
|
|
7
13
|
/**
|
|
8
14
|
* Create a readline interface (can be injected for testing)
|
|
15
|
+
* @returns {Promise<ReadlineInterface>}
|
|
9
16
|
*/
|
|
10
17
|
async function createReadlineInterface(input = process.stdin, output = process.stdout) {
|
|
11
18
|
const readline = await import('readline/promises');
|
|
@@ -16,10 +23,14 @@ async function createReadlineInterface(input = process.stdin, output = process.s
|
|
|
16
23
|
});
|
|
17
24
|
}
|
|
18
25
|
|
|
26
|
+
/**
|
|
27
|
+
* @typedef {Object} WizardOptions
|
|
28
|
+
* @property {ReadlineInterface} [readlineInterface] - Readline interface (injectable for testing)
|
|
29
|
+
*/
|
|
30
|
+
|
|
19
31
|
/**
|
|
20
32
|
* Run interactive wizard
|
|
21
|
-
* @param {
|
|
22
|
-
* @param {Function} options.readlineInterface - Readline interface (injectable for testing)
|
|
33
|
+
* @param {WizardOptions} [options={}] - Options for wizard
|
|
23
34
|
* @returns {Promise<Object>} Wizard results
|
|
24
35
|
*/
|
|
25
36
|
export async function runWizard(options = {}) {
|
|
@@ -131,7 +131,7 @@ export function computeRouteBudget(manifest, currentUrl, baseBudget) {
|
|
|
131
131
|
const routes = manifest.routes || [];
|
|
132
132
|
const expectations = manifest.staticExpectations || [];
|
|
133
133
|
const totalRoutes = routes.length;
|
|
134
|
-
const totalExpectations = expectations.length;
|
|
134
|
+
// const totalExpectations = expectations.length; // Reserved for future use
|
|
135
135
|
|
|
136
136
|
// Count expectations per route
|
|
137
137
|
const routeExpectationCount = new Map();
|
|
@@ -175,10 +175,10 @@ function extractUnverified(detectTruth, observeTruth) {
|
|
|
175
175
|
* @param {Array} findings - Array of findings
|
|
176
176
|
* @param {Object} detectTruth - Detect phase truth
|
|
177
177
|
* @param {Object} observeTruth - Observe phase truth
|
|
178
|
-
* @param {Object}
|
|
178
|
+
* @param {Object} _silences - Silence data (unused parameter, kept for API compatibility)
|
|
179
179
|
* @returns {Object} - Decision snapshot answering 6 mandatory questions
|
|
180
180
|
*/
|
|
181
|
-
export function computeDecisionSnapshot(findings, detectTruth, observeTruth,
|
|
181
|
+
export function computeDecisionSnapshot(findings, detectTruth, observeTruth, _silences) {
|
|
182
182
|
// Question 1: Do we have confirmed SILENT FAILURES?
|
|
183
183
|
const confirmedFailures = findings.filter(f =>
|
|
184
184
|
f.outcome === 'broken' || f.type === 'silent_failure'
|