@veraxhq/verax 0.3.0 → 0.4.0
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/README.md +28 -20
- package/bin/verax.js +11 -18
- package/package.json +28 -7
- package/src/cli/commands/baseline.js +1 -2
- package/src/cli/commands/default.js +72 -81
- package/src/cli/commands/doctor.js +29 -0
- package/src/cli/commands/ga.js +3 -0
- package/src/cli/commands/gates.js +1 -1
- package/src/cli/commands/inspect.js +6 -133
- package/src/cli/commands/release-check.js +2 -0
- package/src/cli/commands/run.js +74 -246
- package/src/cli/commands/security-check.js +2 -1
- package/src/cli/commands/truth.js +0 -1
- package/src/cli/entry.js +82 -309
- package/src/cli/util/angular-component-extractor.js +2 -2
- package/src/cli/util/angular-navigation-detector.js +2 -2
- package/src/cli/util/ast-interactive-detector.js +4 -6
- package/src/cli/util/ast-network-detector.js +3 -3
- package/src/cli/util/ast-promise-extractor.js +581 -0
- package/src/cli/util/ast-usestate-detector.js +3 -3
- package/src/cli/util/atomic-write.js +12 -1
- package/src/cli/util/console-reporter.js +72 -0
- package/src/cli/util/detection-engine.js +105 -41
- package/src/cli/util/determinism-runner.js +2 -1
- package/src/cli/util/determinism-writer.js +1 -1
- package/src/cli/util/digest-engine.js +359 -0
- package/src/cli/util/dom-diff.js +226 -0
- package/src/cli/util/env-url.js +0 -4
- package/src/cli/util/evidence-engine.js +287 -0
- package/src/cli/util/expectation-extractor.js +217 -367
- package/src/cli/util/findings-writer.js +19 -126
- package/src/cli/util/framework-detector.js +572 -0
- package/src/cli/util/idgen.js +1 -1
- package/src/cli/util/interaction-planner.js +529 -0
- package/src/cli/util/learn-writer.js +2 -2
- package/src/cli/util/ledger-writer.js +110 -0
- package/src/cli/util/monorepo-resolver.js +162 -0
- package/src/cli/util/observation-engine.js +127 -278
- package/src/cli/util/observe-writer.js +2 -2
- package/src/cli/util/paths.js +12 -3
- package/src/cli/util/project-discovery.js +284 -3
- package/src/cli/util/project-writer.js +2 -2
- package/src/cli/util/run-id.js +23 -27
- package/src/cli/util/run-result.js +778 -0
- package/src/cli/util/selector-resolver.js +235 -0
- package/src/cli/util/summary-writer.js +2 -1
- package/src/cli/util/svelte-navigation-detector.js +3 -3
- package/src/cli/util/svelte-sfc-extractor.js +0 -1
- package/src/cli/util/svelte-state-detector.js +1 -2
- package/src/cli/util/trust-activation-integration.js +496 -0
- package/src/cli/util/trust-activation-wrapper.js +85 -0
- package/src/cli/util/trust-integration-hooks.js +164 -0
- package/src/cli/util/types.js +153 -0
- package/src/cli/util/url-validation.js +40 -0
- package/src/cli/util/vue-navigation-detector.js +4 -3
- package/src/cli/util/vue-sfc-extractor.js +1 -2
- package/src/cli/util/vue-state-detector.js +1 -1
- package/src/types/fs-augment.d.ts +23 -0
- package/src/types/global.d.ts +137 -0
- package/src/types/internal-types.d.ts +35 -0
- package/src/verax/cli/finding-explainer.js +3 -56
- package/src/verax/cli/init.js +4 -18
- package/src/verax/core/action-classifier.js +4 -3
- package/src/verax/core/artifacts/registry.js +0 -15
- package/src/verax/core/artifacts/verifier.js +18 -8
- package/src/verax/core/baseline/baseline.snapshot.js +2 -0
- package/src/verax/core/capabilities/gates.js +7 -1
- package/src/verax/core/confidence/confidence-compute.js +14 -7
- package/src/verax/core/confidence/confidence.loader.js +1 -0
- package/src/verax/core/confidence-engine-refactor.js +8 -3
- package/src/verax/core/confidence-engine.js +162 -23
- package/src/verax/core/contracts/types.js +1 -0
- package/src/verax/core/contracts/validators.js +79 -4
- package/src/verax/core/decision-snapshot.js +3 -30
- package/src/verax/core/decisions/decision.trace.js +2 -0
- package/src/verax/core/determinism/contract-writer.js +2 -2
- package/src/verax/core/determinism/contract.js +1 -1
- package/src/verax/core/determinism/diff.js +42 -1
- package/src/verax/core/determinism/engine.js +7 -6
- package/src/verax/core/determinism/finding-identity.js +3 -2
- package/src/verax/core/determinism/normalize.js +32 -4
- package/src/verax/core/determinism/report-writer.js +1 -0
- package/src/verax/core/determinism/run-fingerprint.js +7 -2
- package/src/verax/core/dynamic-route-intelligence.js +8 -7
- package/src/verax/core/evidence/evidence-capture-service.js +1 -0
- package/src/verax/core/evidence/evidence-intent-ledger.js +2 -1
- package/src/verax/core/evidence-builder.js +2 -2
- package/src/verax/core/execution-mode-context.js +1 -1
- package/src/verax/core/execution-mode-detector.js +5 -3
- package/src/verax/core/failures/exit-codes.js +39 -37
- package/src/verax/core/failures/failure-summary.js +1 -1
- package/src/verax/core/failures/failure.factory.js +3 -3
- package/src/verax/core/failures/failure.ledger.js +3 -2
- package/src/verax/core/ga/ga.artifact.js +1 -1
- package/src/verax/core/ga/ga.contract.js +3 -2
- package/src/verax/core/ga/ga.enforcer.js +1 -0
- package/src/verax/core/guardrails/policy.loader.js +1 -0
- package/src/verax/core/guardrails/truth-reconciliation.js +1 -1
- package/src/verax/core/guardrails-engine.js +2 -2
- package/src/verax/core/incremental-store.js +1 -0
- package/src/verax/core/integrity/budget.js +138 -0
- package/src/verax/core/integrity/determinism.js +342 -0
- package/src/verax/core/integrity/integrity.js +208 -0
- package/src/verax/core/integrity/poisoning.js +108 -0
- package/src/verax/core/integrity/transaction.js +140 -0
- package/src/verax/core/observe/run-timeline.js +2 -0
- package/src/verax/core/perf/perf.report.js +2 -0
- package/src/verax/core/pipeline-tracker.js +5 -0
- package/src/verax/core/release/provenance.builder.js +73 -214
- package/src/verax/core/release/release.enforcer.js +14 -9
- package/src/verax/core/release/reproducibility.check.js +1 -0
- package/src/verax/core/release/sbom.builder.js +32 -23
- package/src/verax/core/replay-validator.js +2 -0
- package/src/verax/core/replay.js +4 -0
- package/src/verax/core/report/cross-index.js +6 -3
- package/src/verax/core/report/human-summary.js +141 -1
- package/src/verax/core/route-intelligence.js +4 -3
- package/src/verax/core/run-id.js +6 -3
- package/src/verax/core/run-manifest.js +4 -3
- package/src/verax/core/security/secrets.scan.js +10 -7
- package/src/verax/core/security/security.enforcer.js +4 -0
- package/src/verax/core/security/supplychain.policy.js +9 -1
- package/src/verax/core/security/vuln.scan.js +2 -2
- package/src/verax/core/truth/truth.certificate.js +3 -1
- package/src/verax/core/ui-feedback-intelligence.js +12 -46
- package/src/verax/detect/conditional-ui-silent-failure.js +84 -0
- package/src/verax/detect/confidence-engine.js +100 -660
- package/src/verax/detect/confidence-helper.js +1 -0
- package/src/verax/detect/detection-engine.js +1 -18
- package/src/verax/detect/dynamic-route-findings.js +17 -14
- package/src/verax/detect/expectation-chain-detector.js +1 -1
- package/src/verax/detect/expectation-model.js +3 -5
- package/src/verax/detect/failure-cause-inference.js +293 -0
- package/src/verax/detect/findings-writer.js +126 -166
- package/src/verax/detect/flow-detector.js +2 -2
- package/src/verax/detect/form-silent-failure.js +98 -0
- package/src/verax/detect/index.js +51 -234
- package/src/verax/detect/invariants-enforcer.js +147 -0
- package/src/verax/detect/journey-stall-detector.js +4 -4
- package/src/verax/detect/navigation-silent-failure.js +82 -0
- package/src/verax/detect/problem-aggregator.js +361 -0
- package/src/verax/detect/route-findings.js +7 -6
- package/src/verax/detect/summary-writer.js +477 -0
- package/src/verax/detect/test-failure-cause-inference.js +314 -0
- package/src/verax/detect/ui-feedback-findings.js +18 -18
- package/src/verax/detect/verdict-engine.js +3 -57
- package/src/verax/detect/view-switch-correlator.js +2 -2
- package/src/verax/flow/flow-engine.js +2 -1
- package/src/verax/flow/flow-spec.js +0 -6
- package/src/verax/index.js +48 -412
- package/src/verax/intel/ts-program.js +1 -0
- package/src/verax/intel/vue-navigation-extractor.js +3 -0
- package/src/verax/learn/action-contract-extractor.js +67 -682
- package/src/verax/learn/ast-contract-extractor.js +1 -1
- package/src/verax/learn/flow-extractor.js +1 -0
- package/src/verax/learn/project-detector.js +5 -0
- package/src/verax/learn/react-router-extractor.js +2 -0
- package/src/verax/learn/route-validator.js +1 -4
- package/src/verax/learn/source-instrumenter.js +1 -0
- package/src/verax/learn/state-extractor.js +2 -1
- package/src/verax/learn/static-extractor.js +1 -0
- package/src/verax/observe/coverage-gaps.js +132 -0
- package/src/verax/observe/expectation-handler.js +126 -0
- package/src/verax/observe/incremental-skip.js +46 -0
- package/src/verax/observe/index.js +735 -84
- package/src/verax/observe/interaction-executor.js +192 -0
- package/src/verax/observe/interaction-runner.js +782 -530
- package/src/verax/observe/network-firewall.js +86 -0
- package/src/verax/observe/observation-builder.js +169 -0
- package/src/verax/observe/observe-context.js +1 -1
- package/src/verax/observe/observe-helpers.js +2 -1
- package/src/verax/observe/observe-runner.js +28 -24
- package/src/verax/observe/observers/budget-observer.js +3 -3
- package/src/verax/observe/observers/console-observer.js +4 -4
- package/src/verax/observe/observers/coverage-observer.js +4 -4
- package/src/verax/observe/observers/interaction-observer.js +3 -3
- package/src/verax/observe/observers/navigation-observer.js +4 -4
- package/src/verax/observe/observers/network-observer.js +4 -4
- package/src/verax/observe/observers/safety-observer.js +1 -1
- package/src/verax/observe/observers/ui-feedback-observer.js +4 -4
- package/src/verax/observe/page-traversal.js +138 -0
- package/src/verax/observe/snapshot-ops.js +94 -0
- package/src/verax/observe/ui-signal-sensor.js +2 -148
- package/src/verax/scan-summary-writer.js +10 -42
- package/src/verax/shared/artifact-manager.js +30 -13
- package/src/verax/shared/caching.js +1 -0
- package/src/verax/shared/expectation-tracker.js +1 -0
- package/src/verax/shared/zip-artifacts.js +6 -0
- package/src/verax/core/confidence-engine.js.backup +0 -471
- package/src/verax/shared/config-loader.js +0 -169
- /package/src/verax/shared/{expectation-proof.js → expectation-validation.js} +0 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PHASE H6 - Monorepo Resolution Module
|
|
3
|
+
*
|
|
4
|
+
* Deterministically selects the correct app root in a monorepo.
|
|
5
|
+
* Records all decision logic in metadata for auditability.
|
|
6
|
+
*
|
|
7
|
+
* Algorithm:
|
|
8
|
+
* 1. Scan workspace for app root candidates
|
|
9
|
+
* 2. For single candidate: return it
|
|
10
|
+
* 3. For multiple: apply deterministic tie-breaks
|
|
11
|
+
* 4. Record decision trail in run.meta.json
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { existsSync, readFileSync } from 'fs';
|
|
15
|
+
import { resolve, relative } from 'path';
|
|
16
|
+
import { findAppRootCandidates, detectFramework as _detectFramework } from './framework-detector.js';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Detect if path contains monorepo workspace markers
|
|
20
|
+
*/
|
|
21
|
+
function isMonorepo(rootPath) {
|
|
22
|
+
// Check for workspace configuration files
|
|
23
|
+
const workspaceMarkers = [
|
|
24
|
+
'pnpm-workspace.yaml',
|
|
25
|
+
'lerna.json',
|
|
26
|
+
'nx.json',
|
|
27
|
+
'turbo.json',
|
|
28
|
+
// package.json with workspaces field
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
for (const marker of workspaceMarkers) {
|
|
32
|
+
if (existsSync(resolve(rootPath, marker))) {
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Check package.json for workspaces
|
|
38
|
+
try {
|
|
39
|
+
const pkgPath = resolve(rootPath, 'package.json');
|
|
40
|
+
if (existsSync(pkgPath)) {
|
|
41
|
+
// @ts-expect-error - readFileSync with encoding returns string
|
|
42
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
43
|
+
if (pkg.workspaces || pkg.packages) {
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
} catch {
|
|
48
|
+
// ignore
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Detect package manager used in workspace
|
|
56
|
+
*/
|
|
57
|
+
function detectPackageManager(rootPath) {
|
|
58
|
+
const markers = [
|
|
59
|
+
{ file: 'pnpm-lock.yaml', manager: 'pnpm' },
|
|
60
|
+
{ file: 'yarn.lock', manager: 'yarn' },
|
|
61
|
+
{ file: 'package-lock.json', manager: 'npm' },
|
|
62
|
+
{ file: 'bun.lockb', manager: 'bun' },
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
for (const { file, manager } of markers) {
|
|
66
|
+
if (existsSync(resolve(rootPath, file))) {
|
|
67
|
+
return manager;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return 'npm'; // default
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Resolve app root in a monorepo or single-app workspace
|
|
76
|
+
* Returns: { appRoot, isMonorepo, decision, candidates, evidence }
|
|
77
|
+
*/
|
|
78
|
+
export function resolveAppRoot(workspaceRoot) {
|
|
79
|
+
const isMonorepoWorkspace = isMonorepo(workspaceRoot);
|
|
80
|
+
const packageManager = detectPackageManager(workspaceRoot);
|
|
81
|
+
|
|
82
|
+
const result = {
|
|
83
|
+
appRoot: workspaceRoot, // default
|
|
84
|
+
isMonorepo: isMonorepoWorkspace,
|
|
85
|
+
packageManager,
|
|
86
|
+
decision: null,
|
|
87
|
+
candidates: [],
|
|
88
|
+
evidence: [],
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
if (!isMonorepoWorkspace) {
|
|
92
|
+
result.evidence.push('No monorepo markers found; treating as single-app workspace');
|
|
93
|
+
result.decision = 'single-app-default';
|
|
94
|
+
return result;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Scan for app candidates
|
|
98
|
+
const candidates = findAppRootCandidates(workspaceRoot);
|
|
99
|
+
result.candidates = candidates.map(c => ({
|
|
100
|
+
path: relative(workspaceRoot, c.path),
|
|
101
|
+
framework: c.framework,
|
|
102
|
+
confidence: c.confidence,
|
|
103
|
+
hasDevScript: c.hasDevScript,
|
|
104
|
+
}));
|
|
105
|
+
|
|
106
|
+
result.evidence.push(`Monorepo workspace detected (${packageManager})`);
|
|
107
|
+
result.evidence.push(`Found ${candidates.length} app candidates`);
|
|
108
|
+
|
|
109
|
+
if (candidates.length === 0) {
|
|
110
|
+
result.evidence.push('No app candidates found; using workspace root');
|
|
111
|
+
result.decision = 'no-candidates-fallback-root';
|
|
112
|
+
return result;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (candidates.length === 1) {
|
|
116
|
+
result.appRoot = candidates[0].path;
|
|
117
|
+
result.evidence.push(`Single candidate found: ${relative(workspaceRoot, candidates[0].path)}`);
|
|
118
|
+
result.decision = 'single-candidate';
|
|
119
|
+
return result;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Multiple candidates: apply deterministic tie-breaking
|
|
123
|
+
// Already sorted by findAppRootCandidates:
|
|
124
|
+
// 1. Highest framework confidence
|
|
125
|
+
// 2. Has dev script
|
|
126
|
+
// 3. Shallowest depth
|
|
127
|
+
// 4. Alphabetical path
|
|
128
|
+
|
|
129
|
+
const best = candidates[0];
|
|
130
|
+
const tieBreakers = [];
|
|
131
|
+
|
|
132
|
+
// Reason code
|
|
133
|
+
if (best.confidence > candidates[1].confidence) {
|
|
134
|
+
tieBreakers.push(`highest-framework-confidence:${best.framework}-${best.confidence}%`);
|
|
135
|
+
}
|
|
136
|
+
if (best.hasDevScript && !candidates[1].hasDevScript) {
|
|
137
|
+
tieBreakers.push('has-dev-script');
|
|
138
|
+
}
|
|
139
|
+
if (best.depth < candidates[1].depth) {
|
|
140
|
+
tieBreakers.push('shallowest-depth');
|
|
141
|
+
}
|
|
142
|
+
if (best.path < candidates[1].path) {
|
|
143
|
+
tieBreakers.push('alphabetical-first');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
result.appRoot = best.path;
|
|
147
|
+
result.evidence.push(
|
|
148
|
+
`Multiple candidates (${candidates.length}); selected: ${relative(workspaceRoot, best.path)}`
|
|
149
|
+
);
|
|
150
|
+
result.evidence.push(`Tie-breakers applied: ${tieBreakers.join(' > ')}`);
|
|
151
|
+
result.decision = 'monorepo-tie-break';
|
|
152
|
+
|
|
153
|
+
return result;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Export for testing
|
|
158
|
+
*/
|
|
159
|
+
export const _internal = {
|
|
160
|
+
isMonorepo,
|
|
161
|
+
detectPackageManager,
|
|
162
|
+
};
|
|
@@ -1,20 +1,63 @@
|
|
|
1
1
|
import { chromium } from 'playwright';
|
|
2
|
-
import { writeFileSync, mkdirSync } from 'fs';
|
|
3
|
-
import { resolve } from 'path';
|
|
2
|
+
import { writeFileSync as _writeFileSync, mkdirSync as _mkdirSync } from 'fs';
|
|
3
|
+
import { resolve as _resolve } from 'path';
|
|
4
4
|
import { redactHeaders, redactUrl, redactBody, redactConsole, getRedactionCounters } from './redact.js';
|
|
5
|
+
import { InteractionPlanner } from './interaction-planner.js';
|
|
6
|
+
import { computeDigest } from './digest-engine.js';
|
|
5
7
|
|
|
6
8
|
/**
|
|
7
|
-
* Real Browser Observation Engine
|
|
8
|
-
*
|
|
9
|
+
* PHASE H3/M3 - Real Browser Observation Engine
|
|
10
|
+
* Uses Interaction Planner to execute promises with evidence capture
|
|
11
|
+
* H5: Added read-only safety mode by default
|
|
9
12
|
*/
|
|
10
13
|
|
|
11
|
-
export async function observeExpectations(expectations, url, evidencePath, onProgress) {
|
|
14
|
+
export async function observeExpectations(expectations, url, evidencePath, onProgress, _options = {}) {
|
|
15
|
+
// TEST MODE FAST PATH: In VERAX_TEST_MODE, skip browser work entirely for determinism and speed.
|
|
16
|
+
if (process.env.VERAX_TEST_MODE === '1') {
|
|
17
|
+
const observations = (expectations || []).map((exp, idx) => ({
|
|
18
|
+
id: exp.id,
|
|
19
|
+
expectationId: exp.id,
|
|
20
|
+
type: exp.type,
|
|
21
|
+
category: exp.category,
|
|
22
|
+
promise: exp.promise,
|
|
23
|
+
source: exp.source,
|
|
24
|
+
attempted: false,
|
|
25
|
+
observed: false,
|
|
26
|
+
reason: 'test-mode-skip',
|
|
27
|
+
observedAt: new Date().toISOString(),
|
|
28
|
+
evidenceFiles: [],
|
|
29
|
+
signals: {},
|
|
30
|
+
action: null,
|
|
31
|
+
cause: null,
|
|
32
|
+
index: idx + 1,
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
const digest = computeDigest(expectations || [], observations, {
|
|
36
|
+
framework: 'unknown',
|
|
37
|
+
url,
|
|
38
|
+
version: '1.0',
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
observations,
|
|
43
|
+
stats: {
|
|
44
|
+
attempted: 0,
|
|
45
|
+
observed: 0,
|
|
46
|
+
notObserved: 0,
|
|
47
|
+
blockedWrites: 0,
|
|
48
|
+
},
|
|
49
|
+
blockedWrites: [],
|
|
50
|
+
digest,
|
|
51
|
+
redaction: getRedactionCounters({ headersRedacted: 0, tokensRedacted: 0 }),
|
|
52
|
+
observedAt: new Date().toISOString(),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
12
56
|
const observations = [];
|
|
13
|
-
let observed = 0;
|
|
14
|
-
let notObserved = 0;
|
|
15
57
|
const redactionCounters = { headersRedacted: 0, tokensRedacted: 0 };
|
|
16
58
|
let browser = null;
|
|
17
59
|
let page = null;
|
|
60
|
+
let planner = null;
|
|
18
61
|
|
|
19
62
|
try {
|
|
20
63
|
// Launch browser
|
|
@@ -27,10 +70,32 @@ export async function observeExpectations(expectations, url, evidencePath, onPro
|
|
|
27
70
|
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
|
28
71
|
});
|
|
29
72
|
|
|
30
|
-
//
|
|
31
|
-
|
|
32
|
-
const consoleLogs = [];
|
|
73
|
+
// Create interaction planner (read-only mode enforced)
|
|
74
|
+
planner = new InteractionPlanner(page, evidencePath, {});
|
|
33
75
|
|
|
76
|
+
// Set up network monitoring (H5: with write-blocking)
|
|
77
|
+
const networkRecordingStartTime = Date.now();
|
|
78
|
+
|
|
79
|
+
// H5: Route handler for blocking mutating requests (MUST be before page.on)
|
|
80
|
+
await page.route('**/*', async (route) => {
|
|
81
|
+
const request = route.request();
|
|
82
|
+
const method = request.method();
|
|
83
|
+
|
|
84
|
+
if (planner.shouldBlockRequest(method)) {
|
|
85
|
+
const redactedUrl = redactUrl(request.url(), redactionCounters);
|
|
86
|
+
planner.blockedRequests.push({
|
|
87
|
+
url: redactedUrl,
|
|
88
|
+
method: method,
|
|
89
|
+
reason: 'write-blocked-read-only-mode',
|
|
90
|
+
timestamp: new Date().toISOString(),
|
|
91
|
+
});
|
|
92
|
+
await route.abort('blockedbyclient');
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
await route.continue();
|
|
97
|
+
});
|
|
98
|
+
|
|
34
99
|
page.on('request', (request) => {
|
|
35
100
|
const redactedHeaders = redactHeaders(request.headers(), redactionCounters);
|
|
36
101
|
const redactedUrl = redactUrl(request.url(), redactionCounters);
|
|
@@ -42,36 +107,37 @@ export async function observeExpectations(expectations, url, evidencePath, onPro
|
|
|
42
107
|
redactedBody = null;
|
|
43
108
|
}
|
|
44
109
|
|
|
45
|
-
|
|
110
|
+
const event = {
|
|
46
111
|
url: redactedUrl,
|
|
47
112
|
method: request.method(),
|
|
48
113
|
headers: redactedHeaders,
|
|
49
114
|
body: redactedBody,
|
|
50
115
|
timestamp: new Date().toISOString(),
|
|
51
|
-
|
|
116
|
+
relativeMs: Date.now() - networkRecordingStartTime,
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
planner.recordNetworkEvent(event);
|
|
52
120
|
});
|
|
53
121
|
|
|
54
122
|
page.on('console', (msg) => {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
123
|
+
if (msg.type() === 'error' || msg.type() === 'warning') {
|
|
124
|
+
const redactedText = redactConsole(msg.text(), redactionCounters);
|
|
125
|
+
planner.recordConsoleEvent({
|
|
126
|
+
type: msg.type(),
|
|
127
|
+
text: redactedText,
|
|
128
|
+
timestamp: new Date().toISOString(),
|
|
129
|
+
});
|
|
130
|
+
}
|
|
61
131
|
});
|
|
62
132
|
|
|
63
|
-
// Navigate to base URL
|
|
133
|
+
// Navigate to base URL
|
|
64
134
|
try {
|
|
65
135
|
await page.goto(url, {
|
|
66
|
-
waitUntil: 'domcontentloaded',
|
|
136
|
+
waitUntil: 'domcontentloaded',
|
|
67
137
|
timeout: 30000
|
|
68
138
|
});
|
|
69
|
-
|
|
70
|
-
await page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {
|
|
71
|
-
// Network idle timeout is acceptable, continue
|
|
72
|
-
});
|
|
139
|
+
await page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {});
|
|
73
140
|
} catch (error) {
|
|
74
|
-
// Continue even if initial load fails
|
|
75
141
|
if (onProgress) {
|
|
76
142
|
onProgress({
|
|
77
143
|
event: 'observe:warning',
|
|
@@ -80,10 +146,7 @@ export async function observeExpectations(expectations, url, evidencePath, onPro
|
|
|
80
146
|
}
|
|
81
147
|
}
|
|
82
148
|
|
|
83
|
-
//
|
|
84
|
-
const visitedUrls = new Set([url]);
|
|
85
|
-
|
|
86
|
-
// Process each expectation
|
|
149
|
+
// Execute each promise via interaction planner
|
|
87
150
|
for (let i = 0; i < expectations.length; i++) {
|
|
88
151
|
const exp = expectations[i];
|
|
89
152
|
const expNum = i + 1;
|
|
@@ -94,120 +157,76 @@ export async function observeExpectations(expectations, url, evidencePath, onPro
|
|
|
94
157
|
index: expNum,
|
|
95
158
|
total: expectations.length,
|
|
96
159
|
type: exp.type,
|
|
160
|
+
category: exp.category,
|
|
97
161
|
promise: exp.promise,
|
|
98
162
|
});
|
|
99
163
|
}
|
|
100
164
|
|
|
165
|
+
// Execute the promise
|
|
166
|
+
const attempt = await planner.executeSinglePromise(exp, expNum);
|
|
167
|
+
|
|
168
|
+
// Convert attempt to observation
|
|
101
169
|
const observation = {
|
|
102
170
|
id: exp.id,
|
|
103
171
|
type: exp.type,
|
|
172
|
+
category: exp.category,
|
|
104
173
|
promise: exp.promise,
|
|
105
174
|
source: exp.source,
|
|
106
|
-
attempted:
|
|
107
|
-
observed: false,
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
175
|
+
attempted: attempt.attempted,
|
|
176
|
+
observed: attempt.signals ? Object.values(attempt.signals).some(v => v === true) : false,
|
|
177
|
+
action: attempt.action,
|
|
178
|
+
reason: attempt.reason,
|
|
179
|
+
observedAt: new Date().toISOString(),
|
|
180
|
+
evidenceFiles: attempt.evidence?.files || [],
|
|
181
|
+
signals: attempt.signals,
|
|
111
182
|
};
|
|
112
183
|
|
|
113
|
-
try {
|
|
114
|
-
let result = false;
|
|
115
|
-
let evidence = null;
|
|
116
|
-
|
|
117
|
-
if (exp.type === 'navigation') {
|
|
118
|
-
observation.attempted = true; // Mark as attempted
|
|
119
|
-
result = await observeNavigation(
|
|
120
|
-
page,
|
|
121
|
-
exp,
|
|
122
|
-
url,
|
|
123
|
-
visitedUrls,
|
|
124
|
-
evidencePath,
|
|
125
|
-
expNum
|
|
126
|
-
);
|
|
127
|
-
evidence = result ? `nav_${expNum}_after.png` : null;
|
|
128
|
-
} else if (exp.type === 'network') {
|
|
129
|
-
observation.attempted = true; // Mark as attempted
|
|
130
|
-
result = await observeNetwork(page, exp, networkLogs, 5000);
|
|
131
|
-
if (result) {
|
|
132
|
-
const evidenceFile = `network_${expNum}.json`;
|
|
133
|
-
try {
|
|
134
|
-
mkdirSync(evidencePath, { recursive: true });
|
|
135
|
-
const targetUrl = exp.promise.value;
|
|
136
|
-
const relevant = networkLogs.filter((log) =>
|
|
137
|
-
log.url === targetUrl || log.url.includes(targetUrl) || targetUrl.includes(log.url)
|
|
138
|
-
);
|
|
139
|
-
writeFileSync(resolve(evidencePath, evidenceFile), JSON.stringify(relevant, null, 2), 'utf-8');
|
|
140
|
-
} catch {
|
|
141
|
-
// best effort
|
|
142
|
-
}
|
|
143
|
-
evidence = evidenceFile;
|
|
144
|
-
} else {
|
|
145
|
-
evidence = null;
|
|
146
|
-
}
|
|
147
|
-
} else if (exp.type === 'state') {
|
|
148
|
-
observation.attempted = true; // Mark as attempted
|
|
149
|
-
result = await observeState(page, exp, evidencePath, expNum);
|
|
150
|
-
evidence = result ? `state_${expNum}_after.png` : null;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
if (result) {
|
|
154
|
-
observation.observed = true;
|
|
155
|
-
observation.observedAt = new Date().toISOString();
|
|
156
|
-
if (evidence) observation.evidenceFiles.push(evidence);
|
|
157
|
-
observed++;
|
|
158
|
-
} else {
|
|
159
|
-
observation.reason = 'No matching event observed';
|
|
160
|
-
notObserved++;
|
|
161
|
-
}
|
|
162
|
-
} catch (error) {
|
|
163
|
-
observation.reason = `Error: ${error.message}`;
|
|
164
|
-
notObserved++;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
184
|
observations.push(observation);
|
|
168
185
|
|
|
169
186
|
if (onProgress) {
|
|
170
187
|
onProgress({
|
|
171
188
|
event: 'observe:result',
|
|
172
189
|
index: expNum,
|
|
190
|
+
attempted: observation.attempted,
|
|
173
191
|
observed: observation.observed,
|
|
174
192
|
reason: observation.reason,
|
|
175
193
|
});
|
|
176
194
|
}
|
|
177
195
|
}
|
|
178
196
|
|
|
179
|
-
//
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
197
|
+
// Count results
|
|
198
|
+
const observed = observations.filter(o => o.observed).length;
|
|
199
|
+
const attempted = observations.filter(o => o.attempted).length;
|
|
200
|
+
const notObserved = attempted - observed;
|
|
201
|
+
|
|
202
|
+
// H5: Compute deterministic digest for reproducibility proof
|
|
203
|
+
const digest = computeDigest(expectations, observations, {
|
|
204
|
+
framework: 'unknown', // Will be populated by caller
|
|
205
|
+
url,
|
|
206
|
+
version: '1.0',
|
|
207
|
+
});
|
|
189
208
|
|
|
190
209
|
return {
|
|
191
210
|
observations,
|
|
192
211
|
stats: {
|
|
193
|
-
attempted
|
|
212
|
+
attempted,
|
|
194
213
|
observed,
|
|
195
214
|
notObserved,
|
|
215
|
+
blockedWrites: planner.getBlockedRequests().length, // H5: Include write-blocking stats
|
|
196
216
|
},
|
|
217
|
+
blockedWrites: planner.getBlockedRequests(), // H5: Include details of blocked writes
|
|
218
|
+
digest, // H5: Deterministic digest
|
|
197
219
|
redaction: getRedactionCounters(redactionCounters),
|
|
198
220
|
observedAt: new Date().toISOString(),
|
|
199
221
|
};
|
|
222
|
+
|
|
200
223
|
} finally {
|
|
201
|
-
//
|
|
202
|
-
// Remove all event listeners to prevent leaks
|
|
224
|
+
// Cleanup
|
|
203
225
|
if (page) {
|
|
204
226
|
try {
|
|
205
|
-
// Remove all listeners
|
|
206
227
|
page.removeAllListeners();
|
|
207
|
-
|
|
208
|
-
await page.close({ timeout: 5000 }).catch(() => {});
|
|
228
|
+
await page.close().catch(() => {});
|
|
209
229
|
} catch (e) {
|
|
210
|
-
// Ignore close errors but emit warning if onProgress available
|
|
211
230
|
if (onProgress) {
|
|
212
231
|
onProgress({
|
|
213
232
|
event: 'observe:warning',
|
|
@@ -217,22 +236,18 @@ export async function observeExpectations(expectations, url, evidencePath, onPro
|
|
|
217
236
|
}
|
|
218
237
|
}
|
|
219
238
|
|
|
220
|
-
// Close browser context if it exists
|
|
221
239
|
if (browser) {
|
|
222
240
|
try {
|
|
223
241
|
const contexts = browser.contexts();
|
|
224
242
|
for (const context of contexts) {
|
|
225
243
|
try {
|
|
226
|
-
|
|
227
|
-
await context.close({ timeout: 5000 }).catch(() => {});
|
|
244
|
+
await context.close().catch(() => {});
|
|
228
245
|
} catch (e) {
|
|
229
|
-
// Ignore
|
|
246
|
+
// Ignore
|
|
230
247
|
}
|
|
231
248
|
}
|
|
232
|
-
|
|
233
|
-
await browser.close({ timeout: 5000 }).catch(() => {});
|
|
249
|
+
await browser.close().catch(() => {});
|
|
234
250
|
} catch (e) {
|
|
235
|
-
// Ignore browser close errors but emit warning if onProgress available
|
|
236
251
|
if (onProgress) {
|
|
237
252
|
onProgress({
|
|
238
253
|
event: 'observe:warning',
|
|
@@ -244,169 +259,3 @@ export async function observeExpectations(expectations, url, evidencePath, onPro
|
|
|
244
259
|
}
|
|
245
260
|
}
|
|
246
261
|
|
|
247
|
-
/**
|
|
248
|
-
* Observe navigation expectation
|
|
249
|
-
* Attempts to find and click element, observes URL/SPA changes
|
|
250
|
-
*/
|
|
251
|
-
async function observeNavigation(page, expectation, baseUrl, visitedUrls, evidencePath, expNum) {
|
|
252
|
-
const targetPath = expectation.promise.value;
|
|
253
|
-
|
|
254
|
-
try {
|
|
255
|
-
// Screenshot before interaction
|
|
256
|
-
const beforePath = resolve(evidencePath, `nav_${expNum}_before.png`);
|
|
257
|
-
await page.screenshot({ path: beforePath }).catch(() => {});
|
|
258
|
-
|
|
259
|
-
// Find element by searching all anchor tags
|
|
260
|
-
const element = await page.evaluate((path) => {
|
|
261
|
-
const anchors = Array.from(document.querySelectorAll('a'));
|
|
262
|
-
const found = anchors.find(a => {
|
|
263
|
-
const href = a.getAttribute('href');
|
|
264
|
-
return href === path || href.includes(path);
|
|
265
|
-
});
|
|
266
|
-
return found ? { tag: 'a', href: found.getAttribute('href') } : null;
|
|
267
|
-
}, targetPath);
|
|
268
|
-
|
|
269
|
-
if (!element) {
|
|
270
|
-
return false;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
const urlBefore = page.url();
|
|
274
|
-
const contentBefore = await page.content();
|
|
275
|
-
|
|
276
|
-
// Click the element - try multiple approaches
|
|
277
|
-
try {
|
|
278
|
-
await page.locator(`a[href="${element.href}"]`).click({ timeout: 3000 });
|
|
279
|
-
} catch (e) {
|
|
280
|
-
try {
|
|
281
|
-
await page.click(`a[href="${element.href}"]`);
|
|
282
|
-
} catch (e2) {
|
|
283
|
-
// Try clicking by text content
|
|
284
|
-
// eslint-disable-next-line no-undef
|
|
285
|
-
const text = await page.evaluate((href) => {
|
|
286
|
-
const anchors = Array.from(document.querySelectorAll('a'));
|
|
287
|
-
const found = anchors.find(a => a.getAttribute('href') === href);
|
|
288
|
-
return found ? found.textContent : null;
|
|
289
|
-
}, element.href);
|
|
290
|
-
|
|
291
|
-
if (text) {
|
|
292
|
-
await page.click(`a:has-text("${text}")`).catch(() => {});
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
// Wait for navigation or SPA update with explicit timeout
|
|
298
|
-
try {
|
|
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
|
-
});
|
|
309
|
-
} catch (e) {
|
|
310
|
-
// Navigation might not happen, continue
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
// Wait for potential SPA updates (bounded)
|
|
314
|
-
await page.waitForTimeout(300);
|
|
315
|
-
|
|
316
|
-
// Screenshot after interaction
|
|
317
|
-
const afterPath = resolve(evidencePath, `nav_${expNum}_after.png`);
|
|
318
|
-
await page.screenshot({ path: afterPath }).catch(() => {});
|
|
319
|
-
|
|
320
|
-
const urlAfter = page.url();
|
|
321
|
-
const contentAfter = await page.content();
|
|
322
|
-
|
|
323
|
-
// Check if URL changed or content changed
|
|
324
|
-
if (urlBefore !== urlAfter || contentBefore !== contentAfter) {
|
|
325
|
-
visitedUrls.add(urlAfter);
|
|
326
|
-
return true;
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
return false;
|
|
330
|
-
} catch (error) {
|
|
331
|
-
return false;
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
/**
|
|
336
|
-
* Observe network expectation
|
|
337
|
-
* Checks if matching request was made
|
|
338
|
-
*/
|
|
339
|
-
async function observeNetwork(page, expectation, networkLogs, timeoutMs) {
|
|
340
|
-
const targetUrl = expectation.promise.value;
|
|
341
|
-
const startTime = Date.now();
|
|
342
|
-
|
|
343
|
-
return new Promise((resolve) => {
|
|
344
|
-
const checkTimer = setInterval(() => {
|
|
345
|
-
const found = networkLogs.some((log) => {
|
|
346
|
-
return (
|
|
347
|
-
log.url === targetUrl ||
|
|
348
|
-
log.url.includes(targetUrl) ||
|
|
349
|
-
targetUrl.includes(log.url)
|
|
350
|
-
);
|
|
351
|
-
});
|
|
352
|
-
|
|
353
|
-
if (found) {
|
|
354
|
-
clearInterval(checkTimer);
|
|
355
|
-
resolve(true);
|
|
356
|
-
return;
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
if (Date.now() - startTime > timeoutMs) {
|
|
360
|
-
clearInterval(checkTimer);
|
|
361
|
-
resolve(false);
|
|
362
|
-
return;
|
|
363
|
-
}
|
|
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
|
-
}
|
|
371
|
-
});
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
/**
|
|
375
|
-
* Observe state expectation
|
|
376
|
-
* Detects DOM changes or loading indicators
|
|
377
|
-
*/
|
|
378
|
-
async function observeState(page, expectation, evidencePath, expNum) {
|
|
379
|
-
try {
|
|
380
|
-
// Screenshot before
|
|
381
|
-
const beforePath = resolve(evidencePath, `state_${expNum}_before.png`);
|
|
382
|
-
await page.screenshot({ path: beforePath });
|
|
383
|
-
|
|
384
|
-
const htmlBefore = await page.content();
|
|
385
|
-
|
|
386
|
-
// Wait briefly for potential state changes
|
|
387
|
-
await page.waitForTimeout(2000);
|
|
388
|
-
|
|
389
|
-
const htmlAfter = await page.content();
|
|
390
|
-
|
|
391
|
-
// Screenshot after
|
|
392
|
-
const afterPath = resolve(evidencePath, `state_${expNum}_after.png`);
|
|
393
|
-
await page.screenshot({ path: afterPath });
|
|
394
|
-
|
|
395
|
-
// Check if DOM changed
|
|
396
|
-
if (htmlBefore !== htmlAfter) {
|
|
397
|
-
return true;
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
// Check for common state indicators (loading, error, success messages)
|
|
401
|
-
const hasStateIndicators =
|
|
402
|
-
(await page.$('.loading')) ||
|
|
403
|
-
(await page.$('[role="status"]')) ||
|
|
404
|
-
(await page.$('.toast')) ||
|
|
405
|
-
(await page.$('[aria-live]'));
|
|
406
|
-
|
|
407
|
-
return !!hasStateIndicators;
|
|
408
|
-
} catch (error) {
|
|
409
|
-
return false;
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
|
|
@@ -6,10 +6,10 @@ import { ARTIFACT_REGISTRY } from '../../verax/core/artifacts/registry.js';
|
|
|
6
6
|
* Write observe.json artifact
|
|
7
7
|
*/
|
|
8
8
|
export function writeObserveJson(runDir, observeData) {
|
|
9
|
-
const observePath = resolve(runDir,
|
|
9
|
+
const observePath = resolve(runDir, 'observe.json');
|
|
10
10
|
|
|
11
11
|
const payload = {
|
|
12
|
-
contractVersion:
|
|
12
|
+
contractVersion: ARTIFACT_REGISTRY.observe.contractVersion,
|
|
13
13
|
observations: observeData.observations || [],
|
|
14
14
|
stats: {
|
|
15
15
|
attempted: observeData.stats?.attempted || 0,
|