@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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@veraxhq/verax",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "VERAX - Silent failure detection for websites",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -14,8 +14,11 @@
|
|
|
14
14
|
"LICENSE"
|
|
15
15
|
],
|
|
16
16
|
"scripts": {
|
|
17
|
-
"test": "node
|
|
18
|
-
"test:pack": "node scripts/test-pack.js"
|
|
17
|
+
"test": "node scripts/test-runner-wrapper.js",
|
|
18
|
+
"test:pack": "node scripts/test-pack.js",
|
|
19
|
+
"verify-release": "node scripts/verify-release.js",
|
|
20
|
+
"lint": "eslint . --max-warnings 0",
|
|
21
|
+
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
19
22
|
},
|
|
20
23
|
"dependencies": {
|
|
21
24
|
"glob": "^10.3.10",
|
|
@@ -30,6 +33,13 @@
|
|
|
30
33
|
"devDependencies": {
|
|
31
34
|
"@babel/parser": "^7.28.5",
|
|
32
35
|
"@babel/traverse": "^7.28.5",
|
|
33
|
-
"@
|
|
36
|
+
"@reduxjs/toolkit": "^2.11.2",
|
|
37
|
+
"@veraxhq/verax": "file:veraxhq-verax-0.2.1.tgz",
|
|
38
|
+
"eslint": "^8.57.0",
|
|
39
|
+
"next": "^16.1.1",
|
|
40
|
+
"react": "^19.2.3",
|
|
41
|
+
"react-dom": "^19.2.3",
|
|
42
|
+
"react-redux": "^9.2.0",
|
|
43
|
+
"react-router-dom": "^7.12.0"
|
|
34
44
|
}
|
|
35
45
|
}
|
|
@@ -3,7 +3,7 @@ import { existsSync, readFileSync } from 'fs';
|
|
|
3
3
|
import { fileURLToPath } from 'url';
|
|
4
4
|
import { dirname } from 'path';
|
|
5
5
|
import inquirer from 'inquirer';
|
|
6
|
-
import {
|
|
6
|
+
import { DataError } from '../util/errors.js';
|
|
7
7
|
import { generateRunId } from '../util/run-id.js';
|
|
8
8
|
import { getRunPaths, ensureRunDirectories } from '../util/paths.js';
|
|
9
9
|
import { atomicWriteJson, atomicWriteText } from '../util/atomic-write.js';
|
|
@@ -18,6 +18,7 @@ import { writeObserveJson } from '../util/observe-writer.js';
|
|
|
18
18
|
import { detectFindings } from '../util/detection-engine.js';
|
|
19
19
|
import { writeFindingsJson } from '../util/findings-writer.js';
|
|
20
20
|
import { writeSummaryJson } from '../util/summary-writer.js';
|
|
21
|
+
import { computeRuntimeBudget, withTimeout } from '../util/runtime-budget.js';
|
|
21
22
|
|
|
22
23
|
const __filename = fileURLToPath(import.meta.url);
|
|
23
24
|
const __dirname = dirname(__filename);
|
|
@@ -72,6 +73,79 @@ export async function defaultCommand(options = {}) {
|
|
|
72
73
|
});
|
|
73
74
|
}
|
|
74
75
|
|
|
76
|
+
let runId = null;
|
|
77
|
+
/** @type {ReturnType<typeof getRunPaths> | null} */
|
|
78
|
+
let paths = null;
|
|
79
|
+
let startedAt = null;
|
|
80
|
+
let watchdogTimer = null;
|
|
81
|
+
let budget = null;
|
|
82
|
+
let timedOut = false;
|
|
83
|
+
|
|
84
|
+
// Graceful finalization function
|
|
85
|
+
const finalizeOnTimeout = async (reason) => {
|
|
86
|
+
if (timedOut) return; // Prevent double finalization
|
|
87
|
+
timedOut = true;
|
|
88
|
+
|
|
89
|
+
events.stopHeartbeat();
|
|
90
|
+
|
|
91
|
+
// TypeScript narrowing: paths is guaranteed to be non-null here due to control flow
|
|
92
|
+
if (paths && runId && startedAt) {
|
|
93
|
+
try {
|
|
94
|
+
const failedAt = new Date().toISOString();
|
|
95
|
+
atomicWriteJson(paths.runStatusJson, {
|
|
96
|
+
status: 'FAILED',
|
|
97
|
+
runId,
|
|
98
|
+
startedAt,
|
|
99
|
+
failedAt,
|
|
100
|
+
error: reason,
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
atomicWriteJson(paths.runMetaJson, {
|
|
104
|
+
veraxVersion: getVersion(),
|
|
105
|
+
nodeVersion: process.version,
|
|
106
|
+
platform: process.platform,
|
|
107
|
+
cwd: projectRoot,
|
|
108
|
+
command: 'default',
|
|
109
|
+
args: { url: url || null, src },
|
|
110
|
+
url: url || null,
|
|
111
|
+
src: srcPath,
|
|
112
|
+
startedAt,
|
|
113
|
+
completedAt: failedAt,
|
|
114
|
+
error: reason,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
writeSummaryJson(paths.summaryJson, {
|
|
119
|
+
runId,
|
|
120
|
+
status: 'FAILED',
|
|
121
|
+
startedAt,
|
|
122
|
+
completedAt: failedAt,
|
|
123
|
+
command: 'default',
|
|
124
|
+
url: url || null,
|
|
125
|
+
notes: `Run timed out: ${reason}`,
|
|
126
|
+
}, {
|
|
127
|
+
expectationsTotal: 0,
|
|
128
|
+
attempted: 0,
|
|
129
|
+
observed: 0,
|
|
130
|
+
silentFailures: 0,
|
|
131
|
+
coverageGaps: 0,
|
|
132
|
+
unproven: 0,
|
|
133
|
+
informational: 0,
|
|
134
|
+
});
|
|
135
|
+
} catch (summaryError) {
|
|
136
|
+
// Ignore summary write errors during timeout handling
|
|
137
|
+
}
|
|
138
|
+
} catch (statusError) {
|
|
139
|
+
// Ignore errors when writing failure status
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
events.emit('error', {
|
|
144
|
+
message: reason,
|
|
145
|
+
type: 'timeout',
|
|
146
|
+
});
|
|
147
|
+
};
|
|
148
|
+
|
|
75
149
|
try {
|
|
76
150
|
events.emit('phase:started', {
|
|
77
151
|
phase: 'Detect Project',
|
|
@@ -231,8 +305,17 @@ export async function defaultCommand(options = {}) {
|
|
|
231
305
|
message: 'Analyzing project structure...',
|
|
232
306
|
});
|
|
233
307
|
|
|
234
|
-
|
|
235
|
-
|
|
308
|
+
events.startHeartbeat('Learn', json);
|
|
309
|
+
|
|
310
|
+
let expectations, skipped;
|
|
311
|
+
try {
|
|
312
|
+
// Extract expectations
|
|
313
|
+
const result = await extractExpectations(projectProfile, projectProfile.sourceRoot);
|
|
314
|
+
expectations = result.expectations;
|
|
315
|
+
skipped = result.skipped;
|
|
316
|
+
} finally {
|
|
317
|
+
events.stopHeartbeat();
|
|
318
|
+
}
|
|
236
319
|
|
|
237
320
|
if (!json) {
|
|
238
321
|
console.log(`Found ${expectations.length} expectations`);
|
|
@@ -256,55 +339,109 @@ export async function defaultCommand(options = {}) {
|
|
|
256
339
|
});
|
|
257
340
|
}
|
|
258
341
|
|
|
342
|
+
// Compute runtime budget based on expectations count
|
|
343
|
+
budget = computeRuntimeBudget({
|
|
344
|
+
expectationsCount: expectations.length,
|
|
345
|
+
mode: 'default',
|
|
346
|
+
framework: projectProfile.framework,
|
|
347
|
+
fileCount: projectProfile.fileCount || expectations.length,
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
// Set up global watchdog timer
|
|
351
|
+
watchdogTimer = setTimeout(async () => {
|
|
352
|
+
await finalizeOnTimeout(`Global timeout exceeded: ${budget.totalMaxMs}ms`);
|
|
353
|
+
// Exit with code 0 (tool executed, just timed out)
|
|
354
|
+
process.exit(0);
|
|
355
|
+
}, budget.totalMaxMs);
|
|
356
|
+
|
|
357
|
+
// Wrap Learn phase with timeout
|
|
358
|
+
try {
|
|
359
|
+
await withTimeout(
|
|
360
|
+
budget.learnMaxMs,
|
|
361
|
+
Promise.resolve(), // Learn phase already completed
|
|
362
|
+
'Learn'
|
|
363
|
+
);
|
|
364
|
+
} catch (error) {
|
|
365
|
+
if (error.message.includes('timeout')) {
|
|
366
|
+
await finalizeOnTimeout(`Learn phase timeout: ${budget.learnMaxMs}ms`);
|
|
367
|
+
process.exit(0);
|
|
368
|
+
}
|
|
369
|
+
throw error;
|
|
370
|
+
}
|
|
371
|
+
|
|
259
372
|
events.emit('phase:completed', {
|
|
260
373
|
phase: 'Learn',
|
|
261
374
|
message: 'Project analysis complete',
|
|
262
375
|
});
|
|
263
376
|
|
|
264
|
-
// Observe phase
|
|
377
|
+
// Observe phase with timeout
|
|
265
378
|
events.emit('phase:started', {
|
|
266
379
|
phase: 'Observe',
|
|
267
380
|
message: 'Launching browser and observing expectations...',
|
|
268
381
|
});
|
|
269
382
|
|
|
383
|
+
events.startHeartbeat('Observe', json);
|
|
384
|
+
|
|
270
385
|
let observeData = null;
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
386
|
+
try {
|
|
387
|
+
if (expectations.length > 0) {
|
|
388
|
+
try {
|
|
389
|
+
observeData = await withTimeout(
|
|
390
|
+
budget.observeMaxMs,
|
|
391
|
+
observeExpectations(
|
|
392
|
+
expectations,
|
|
393
|
+
resolvedUrl,
|
|
394
|
+
paths.evidenceDir,
|
|
395
|
+
(progress) => {
|
|
396
|
+
events.emit(progress.event, progress);
|
|
397
|
+
if (!json && progress.event === 'observe:result') {
|
|
398
|
+
const status = progress.observed ? '✓' : '✗';
|
|
399
|
+
console.log(` ${status} ${progress.index}/${expectations.length}`);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
),
|
|
403
|
+
'Observe'
|
|
404
|
+
);
|
|
405
|
+
|
|
406
|
+
if (!json) {
|
|
407
|
+
console.log(`Observed: ${observeData.stats.observed}/${expectations.length}`);
|
|
408
|
+
}
|
|
409
|
+
} catch (error) {
|
|
410
|
+
if (error.message.includes('timeout')) {
|
|
411
|
+
if (!json) {
|
|
412
|
+
console.error(`Observe error: timeout after ${budget.observeMaxMs}ms`);
|
|
413
|
+
}
|
|
414
|
+
events.emit('observe:error', {
|
|
415
|
+
message: `Observe phase timeout: ${budget.observeMaxMs}ms`,
|
|
416
|
+
});
|
|
417
|
+
observeData = {
|
|
418
|
+
observations: [],
|
|
419
|
+
stats: { attempted: 0, observed: 0, notObserved: 0 },
|
|
420
|
+
observedAt: new Date().toISOString(),
|
|
421
|
+
};
|
|
422
|
+
} else {
|
|
423
|
+
if (!json) {
|
|
424
|
+
console.error(`Observe error: ${error.message}`);
|
|
282
425
|
}
|
|
426
|
+
events.emit('observe:error', {
|
|
427
|
+
message: error.message,
|
|
428
|
+
});
|
|
429
|
+
observeData = {
|
|
430
|
+
observations: [],
|
|
431
|
+
stats: { attempted: 0, observed: 0, notObserved: 0 },
|
|
432
|
+
observedAt: new Date().toISOString(),
|
|
433
|
+
};
|
|
283
434
|
}
|
|
284
|
-
);
|
|
285
|
-
|
|
286
|
-
if (!json) {
|
|
287
|
-
console.log(`Observed: ${observeData.stats.observed}/${expectations.length}`);
|
|
288
|
-
}
|
|
289
|
-
} catch (error) {
|
|
290
|
-
if (!json) {
|
|
291
|
-
console.error(`Observe error: ${error.message}`);
|
|
292
435
|
}
|
|
293
|
-
|
|
294
|
-
message: error.message,
|
|
295
|
-
});
|
|
436
|
+
} else {
|
|
296
437
|
observeData = {
|
|
297
438
|
observations: [],
|
|
298
439
|
stats: { attempted: 0, observed: 0, notObserved: 0 },
|
|
299
440
|
observedAt: new Date().toISOString(),
|
|
300
441
|
};
|
|
301
442
|
}
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
observations: [],
|
|
305
|
-
stats: { attempted: 0, observed: 0, notObserved: 0 },
|
|
306
|
-
observedAt: new Date().toISOString(),
|
|
307
|
-
};
|
|
443
|
+
} finally {
|
|
444
|
+
events.stopHeartbeat();
|
|
308
445
|
}
|
|
309
446
|
|
|
310
447
|
events.emit('phase:completed', {
|
|
@@ -312,47 +449,71 @@ export async function defaultCommand(options = {}) {
|
|
|
312
449
|
message: 'Browser observation complete',
|
|
313
450
|
});
|
|
314
451
|
|
|
315
|
-
// Detect phase
|
|
452
|
+
// Detect phase with timeout
|
|
316
453
|
events.emit('phase:started', {
|
|
317
454
|
phase: 'Detect',
|
|
318
455
|
message: 'Analyzing findings and detecting silent failures...',
|
|
319
456
|
});
|
|
320
457
|
|
|
458
|
+
events.startHeartbeat('Detect', json);
|
|
459
|
+
|
|
321
460
|
// Load learn and observe data for detection
|
|
322
461
|
let learnData = { expectations: [] };
|
|
323
462
|
let detectData = null;
|
|
324
463
|
|
|
325
464
|
try {
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
465
|
+
try {
|
|
466
|
+
learnData = {
|
|
467
|
+
expectations,
|
|
468
|
+
skipped,
|
|
469
|
+
};
|
|
470
|
+
|
|
471
|
+
detectData = await withTimeout(
|
|
472
|
+
budget.detectMaxMs,
|
|
473
|
+
detectFindings(learnData, observeData, projectRoot, (progress) => {
|
|
474
|
+
events.emit(progress.event, progress);
|
|
475
|
+
if (!json && progress.event === 'detect:classified') {
|
|
476
|
+
const symbol = progress.classification === 'silent-failure' ? '✗' :
|
|
477
|
+
progress.classification === 'observed' ? '✓' :
|
|
478
|
+
progress.classification === 'coverage-gap' ? '⊘' : '⚠';
|
|
479
|
+
console.log(` ${symbol} ${progress.index}/${learnData.expectations.length}`);
|
|
480
|
+
}
|
|
481
|
+
}),
|
|
482
|
+
'Detect'
|
|
483
|
+
);
|
|
484
|
+
|
|
485
|
+
if (!json && detectData.stats.silentFailures > 0) {
|
|
486
|
+
console.log(`Silent failures detected: ${detectData.stats.silentFailures}`);
|
|
487
|
+
}
|
|
488
|
+
} catch (error) {
|
|
489
|
+
if (error.message.includes('timeout')) {
|
|
490
|
+
if (!json) {
|
|
491
|
+
console.error(`Detect error: timeout after ${budget.detectMaxMs}ms`);
|
|
492
|
+
}
|
|
493
|
+
events.emit('detect:error', {
|
|
494
|
+
message: `Detect phase timeout: ${budget.detectMaxMs}ms`,
|
|
495
|
+
});
|
|
496
|
+
detectData = {
|
|
497
|
+
findings: [],
|
|
498
|
+
stats: { total: 0, silentFailures: 0, observed: 0, coverageGaps: 0, unproven: 0, informational: 0 },
|
|
499
|
+
detectedAt: new Date().toISOString(),
|
|
500
|
+
};
|
|
501
|
+
} else {
|
|
502
|
+
if (!json) {
|
|
503
|
+
console.error(`Detect error: ${error.message}`);
|
|
504
|
+
}
|
|
505
|
+
events.emit('detect:error', {
|
|
506
|
+
message: error.message,
|
|
507
|
+
});
|
|
508
|
+
detectData = {
|
|
509
|
+
findings: [],
|
|
510
|
+
stats: { total: 0, silentFailures: 0, observed: 0, coverageGaps: 0, unproven: 0, informational: 0 },
|
|
511
|
+
detectedAt: new Date().toISOString(),
|
|
512
|
+
};
|
|
338
513
|
}
|
|
339
|
-
});
|
|
340
|
-
|
|
341
|
-
if (!json && detectData.stats.silentFailures > 0) {
|
|
342
|
-
console.log(`Silent failures detected: ${detectData.stats.silentFailures}`);
|
|
343
|
-
}
|
|
344
|
-
} catch (error) {
|
|
345
|
-
if (!json) {
|
|
346
|
-
console.error(`Detect error: ${error.message}`);
|
|
347
514
|
}
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
});
|
|
351
|
-
detectData = {
|
|
352
|
-
findings: [],
|
|
353
|
-
stats: { total: 0, silentFailures: 0, observed: 0, coverageGaps: 0, unproven: 0, informational: 0 },
|
|
354
|
-
detectedAt: new Date().toISOString(),
|
|
355
|
-
};
|
|
515
|
+
} finally {
|
|
516
|
+
events.stopHeartbeat();
|
|
356
517
|
}
|
|
357
518
|
|
|
358
519
|
events.emit('phase:completed', {
|
|
@@ -360,12 +521,20 @@ export async function defaultCommand(options = {}) {
|
|
|
360
521
|
message: 'Silent failure detection complete',
|
|
361
522
|
});
|
|
362
523
|
|
|
524
|
+
// Clear watchdog timer on successful completion
|
|
525
|
+
if (watchdogTimer) {
|
|
526
|
+
clearTimeout(watchdogTimer);
|
|
527
|
+
watchdogTimer = null;
|
|
528
|
+
}
|
|
529
|
+
|
|
363
530
|
// Finalize Artifacts
|
|
364
531
|
events.emit('phase:started', {
|
|
365
532
|
phase: 'Finalize Artifacts',
|
|
366
533
|
message: 'Writing run results...',
|
|
367
534
|
});
|
|
368
535
|
|
|
536
|
+
events.stopHeartbeat();
|
|
537
|
+
|
|
369
538
|
const completedAt = new Date().toISOString();
|
|
370
539
|
|
|
371
540
|
atomicWriteJson(paths.runStatusJson, {
|
|
@@ -411,30 +580,11 @@ export async function defaultCommand(options = {}) {
|
|
|
411
580
|
// Write detect results (or empty if detection failed)
|
|
412
581
|
writeFindingsJson(paths.baseDir, detectData);
|
|
413
582
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
},
|
|
420
|
-
{
|
|
421
|
-
type: 'phase:completed',
|
|
422
|
-
timestamp: new Date().toISOString(),
|
|
423
|
-
phase: 'Detect Project',
|
|
424
|
-
},
|
|
425
|
-
{
|
|
426
|
-
type: 'phase:started',
|
|
427
|
-
timestamp: new Date().toISOString(),
|
|
428
|
-
phase: 'Learn',
|
|
429
|
-
},
|
|
430
|
-
{
|
|
431
|
-
type: 'phase:completed',
|
|
432
|
-
timestamp: completedAt,
|
|
433
|
-
phase: 'Learn',
|
|
434
|
-
},
|
|
435
|
-
];
|
|
436
|
-
|
|
437
|
-
const tracesContent = traces.map(t => JSON.stringify(t)).join('\n') + '\n';
|
|
583
|
+
// Write traces (include all events including heartbeats)
|
|
584
|
+
const allEvents = events.getEvents();
|
|
585
|
+
const tracesContent = allEvents
|
|
586
|
+
.map(e => JSON.stringify(e))
|
|
587
|
+
.join('\n') + '\n';
|
|
438
588
|
atomicWriteText(paths.tracesJsonl, tracesContent);
|
|
439
589
|
|
|
440
590
|
// Write project profile
|
|
@@ -460,8 +610,16 @@ export async function defaultCommand(options = {}) {
|
|
|
460
610
|
|
|
461
611
|
return { runId, paths, url: resolvedUrl, success: true };
|
|
462
612
|
} catch (error) {
|
|
613
|
+
// Clear watchdog timer on error
|
|
614
|
+
if (watchdogTimer) {
|
|
615
|
+
clearTimeout(watchdogTimer);
|
|
616
|
+
watchdogTimer = null;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
events.stopHeartbeat();
|
|
620
|
+
|
|
463
621
|
// Mark run as FAILED if we have paths
|
|
464
|
-
if (paths && runId && startedAt) {
|
|
622
|
+
if (paths && runId && startedAt && typeof paths === 'object') {
|
|
465
623
|
try {
|
|
466
624
|
const failedAt = new Date().toISOString();
|
|
467
625
|
atomicWriteJson(paths.runStatusJson, {
|
|
@@ -92,11 +92,26 @@ export async function doctorCommand(options = {}) {
|
|
|
92
92
|
|
|
93
93
|
// 4) Headless launch smoke test
|
|
94
94
|
if (playwright && playwright.chromium && chromiumPath && existsSync(chromiumPath)) {
|
|
95
|
+
/** @type {any} */
|
|
96
|
+
let browser = null;
|
|
95
97
|
try {
|
|
96
|
-
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
98
|
+
// Configurable timeout via env (default 5000ms, CI can override to 3000ms)
|
|
99
|
+
const smokeTimeoutMs = parseInt(process.env.VERAX_DOCTOR_SMOKE_TIMEOUT_MS || '5000', 10);
|
|
100
|
+
|
|
101
|
+
// Wrap entire smoke test in hard timeout
|
|
102
|
+
const smokeTestPromise = (async () => {
|
|
103
|
+
browser = await playwright.chromium.launch({ headless: true, timeout: smokeTimeoutMs });
|
|
104
|
+
const page = await browser.newPage();
|
|
105
|
+
await page.goto('about:blank', { timeout: smokeTimeoutMs });
|
|
106
|
+
return true;
|
|
107
|
+
})();
|
|
108
|
+
|
|
109
|
+
// Race against timeout
|
|
110
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
111
|
+
setTimeout(() => reject(new Error('Smoke test timed out')), smokeTimeoutMs);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
await Promise.race([smokeTestPromise, timeoutPromise]);
|
|
100
115
|
addCheck('Headless smoke test', 'pass', 'Chromium launched headless and closed successfully');
|
|
101
116
|
} catch (error) {
|
|
102
117
|
addCheck(
|
|
@@ -107,6 +122,23 @@ export async function doctorCommand(options = {}) {
|
|
|
107
122
|
? 'Try: npx playwright install --with-deps chromium && launch with --no-sandbox in constrained environments'
|
|
108
123
|
: 'Reinstall playwright: npx playwright install --with-deps chromium'
|
|
109
124
|
);
|
|
125
|
+
} finally {
|
|
126
|
+
// CRITICAL: Always close browser to prevent hanging processes
|
|
127
|
+
// Bound close operation too (max 2s)
|
|
128
|
+
if (browser) {
|
|
129
|
+
try {
|
|
130
|
+
const closePromise = browser.close();
|
|
131
|
+
const closeTimeout = new Promise((resolve) => setTimeout(resolve, 2000));
|
|
132
|
+
await Promise.race([closePromise, closeTimeout]);
|
|
133
|
+
} catch (closeError) {
|
|
134
|
+
// Ignore close errors - force kill if needed
|
|
135
|
+
try {
|
|
136
|
+
await browser.close();
|
|
137
|
+
} catch {
|
|
138
|
+
// Final attempt failed, process will clean up
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
110
142
|
}
|
|
111
143
|
} else {
|
|
112
144
|
addCheck(
|