@vibecheckai/cli 3.2.2 → 3.2.4
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/bin/.generated +25 -25
- package/bin/dev/run-v2-torture.js +30 -30
- package/bin/runners/ENHANCEMENT_GUIDE.md +121 -121
- package/bin/runners/lib/__tests__/entitlements-v2.test.js +295 -295
- package/bin/runners/lib/agent-firewall/ai/false-positive-analyzer.js +474 -0
- package/bin/runners/lib/agent-firewall/claims/extractor.js +117 -28
- package/bin/runners/lib/agent-firewall/evidence/env-evidence.js +23 -14
- package/bin/runners/lib/agent-firewall/evidence/route-evidence.js +72 -1
- package/bin/runners/lib/agent-firewall/interceptor/base.js +2 -2
- package/bin/runners/lib/agent-firewall/policy/default-policy.json +6 -0
- package/bin/runners/lib/agent-firewall/policy/engine.js +34 -3
- package/bin/runners/lib/agent-firewall/policy/rules/fake-success.js +29 -4
- package/bin/runners/lib/agent-firewall/policy/rules/ghost-route.js +12 -0
- package/bin/runners/lib/agent-firewall/truthpack/loader.js +21 -0
- package/bin/runners/lib/agent-firewall/utils/ignore-checker.js +118 -0
- package/bin/runners/lib/analyzers.js +606 -325
- package/bin/runners/lib/auth-truth.js +193 -193
- package/bin/runners/lib/backup.js +62 -62
- package/bin/runners/lib/billing.js +107 -107
- package/bin/runners/lib/claims.js +118 -118
- package/bin/runners/lib/cli-ui.js +540 -540
- package/bin/runners/lib/contracts/auth-contract.js +202 -202
- package/bin/runners/lib/contracts/env-contract.js +181 -181
- package/bin/runners/lib/contracts/external-contract.js +206 -206
- package/bin/runners/lib/contracts/guard.js +168 -168
- package/bin/runners/lib/contracts/index.js +89 -89
- package/bin/runners/lib/contracts/plan-validator.js +311 -311
- package/bin/runners/lib/contracts/route-contract.js +199 -199
- package/bin/runners/lib/contracts.js +804 -804
- package/bin/runners/lib/detect.js +89 -89
- package/bin/runners/lib/doctor/autofix.js +254 -254
- package/bin/runners/lib/doctor/index.js +37 -37
- package/bin/runners/lib/doctor/modules/dependencies.js +325 -325
- package/bin/runners/lib/doctor/modules/index.js +46 -46
- package/bin/runners/lib/doctor/modules/network.js +250 -250
- package/bin/runners/lib/doctor/modules/project.js +312 -312
- package/bin/runners/lib/doctor/modules/runtime.js +224 -224
- package/bin/runners/lib/doctor/modules/security.js +348 -348
- package/bin/runners/lib/doctor/modules/system.js +213 -213
- package/bin/runners/lib/doctor/modules/vibecheck.js +394 -394
- package/bin/runners/lib/doctor/reporter.js +262 -262
- package/bin/runners/lib/doctor/service.js +262 -262
- package/bin/runners/lib/doctor/types.js +113 -113
- package/bin/runners/lib/doctor/ui.js +263 -263
- package/bin/runners/lib/doctor-v2.js +608 -608
- package/bin/runners/lib/drift.js +425 -425
- package/bin/runners/lib/enforcement.js +72 -72
- package/bin/runners/lib/engines/accessibility-engine.js +190 -0
- package/bin/runners/lib/engines/api-consistency-engine.js +162 -0
- package/bin/runners/lib/engines/ast-cache.js +99 -0
- package/bin/runners/lib/engines/code-quality-engine.js +255 -0
- package/bin/runners/lib/engines/console-logs-engine.js +115 -0
- package/bin/runners/lib/engines/cross-file-analysis-engine.js +268 -0
- package/bin/runners/lib/engines/dead-code-engine.js +198 -0
- package/bin/runners/lib/engines/deprecated-api-engine.js +226 -0
- package/bin/runners/lib/engines/empty-catch-engine.js +150 -0
- package/bin/runners/lib/engines/file-filter.js +131 -0
- package/bin/runners/lib/engines/hardcoded-secrets-engine.js +251 -0
- package/bin/runners/lib/engines/mock-data-engine.js +272 -0
- package/bin/runners/lib/engines/parallel-processor.js +71 -0
- package/bin/runners/lib/engines/performance-issues-engine.js +265 -0
- package/bin/runners/lib/engines/security-vulnerabilities-engine.js +243 -0
- package/bin/runners/lib/engines/todo-fixme-engine.js +115 -0
- package/bin/runners/lib/engines/type-aware-engine.js +152 -0
- package/bin/runners/lib/engines/unsafe-regex-engine.js +225 -0
- package/bin/runners/lib/engines/vibecheck-engines/README.md +53 -0
- package/bin/runners/lib/engines/vibecheck-engines/index.js +15 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/ast-cache.js +164 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/code-quality-engine.js +291 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/console-logs-engine.js +83 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/dead-code-engine.js +198 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/deprecated-api-engine.js +275 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/empty-catch-engine.js +167 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/file-filter.js +217 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/hardcoded-secrets-engine.js +139 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/mock-data-engine.js +140 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/parallel-processor.js +164 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/performance-issues-engine.js +234 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/type-aware-engine.js +217 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/unsafe-regex-engine.js +78 -0
- package/bin/runners/lib/engines/vibecheck-engines/package.json +13 -0
- package/bin/runners/lib/enterprise-detect.js +603 -603
- package/bin/runners/lib/enterprise-init.js +942 -942
- package/bin/runners/lib/env-resolver.js +417 -417
- package/bin/runners/lib/env-template.js +66 -66
- package/bin/runners/lib/env.js +189 -189
- package/bin/runners/lib/extractors/client-calls.js +990 -990
- package/bin/runners/lib/extractors/fastify-route-dump.js +573 -573
- package/bin/runners/lib/extractors/fastify-routes.js +426 -426
- package/bin/runners/lib/extractors/index.js +363 -363
- package/bin/runners/lib/extractors/next-routes.js +524 -524
- package/bin/runners/lib/extractors/proof-graph.js +431 -431
- package/bin/runners/lib/extractors/route-matcher.js +451 -451
- package/bin/runners/lib/extractors/truthpack-v2.js +377 -377
- package/bin/runners/lib/extractors/ui-bindings.js +547 -547
- package/bin/runners/lib/findings-schema.js +281 -281
- package/bin/runners/lib/firewall-prompt.js +50 -50
- package/bin/runners/lib/global-flags.js +213 -213
- package/bin/runners/lib/graph/graph-builder.js +265 -265
- package/bin/runners/lib/graph/html-renderer.js +413 -413
- package/bin/runners/lib/graph/index.js +32 -32
- package/bin/runners/lib/graph/runtime-collector.js +215 -215
- package/bin/runners/lib/graph/static-extractor.js +518 -518
- package/bin/runners/lib/html-report.js +650 -650
- package/bin/runners/lib/interactive-menu.js +1496 -1496
- package/bin/runners/lib/llm.js +75 -75
- package/bin/runners/lib/meter.js +61 -61
- package/bin/runners/lib/missions/evidence.js +126 -126
- package/bin/runners/lib/patch.js +40 -40
- package/bin/runners/lib/permissions/auth-model.js +213 -213
- package/bin/runners/lib/permissions/idor-prover.js +205 -205
- package/bin/runners/lib/permissions/index.js +45 -45
- package/bin/runners/lib/permissions/matrix-builder.js +198 -198
- package/bin/runners/lib/pkgjson.js +28 -28
- package/bin/runners/lib/policy.js +295 -295
- package/bin/runners/lib/preflight.js +142 -142
- package/bin/runners/lib/reality/correlation-detectors.js +359 -359
- package/bin/runners/lib/reality/index.js +318 -318
- package/bin/runners/lib/reality/request-hashing.js +416 -416
- package/bin/runners/lib/reality/request-mapper.js +453 -453
- package/bin/runners/lib/reality/safety-rails.js +463 -463
- package/bin/runners/lib/reality/semantic-snapshot.js +408 -408
- package/bin/runners/lib/reality/toast-detector.js +393 -393
- package/bin/runners/lib/reality-findings.js +84 -84
- package/bin/runners/lib/receipts.js +179 -179
- package/bin/runners/lib/redact.js +29 -29
- package/bin/runners/lib/replay/capsule-manager.js +154 -154
- package/bin/runners/lib/replay/index.js +263 -263
- package/bin/runners/lib/replay/player.js +348 -348
- package/bin/runners/lib/replay/recorder.js +331 -331
- package/bin/runners/lib/report-output.js +187 -187
- package/bin/runners/lib/report.js +135 -135
- package/bin/runners/lib/route-detection.js +1140 -1140
- package/bin/runners/lib/sandbox/index.js +59 -59
- package/bin/runners/lib/sandbox/proof-chain.js +399 -399
- package/bin/runners/lib/sandbox/sandbox-runner.js +205 -205
- package/bin/runners/lib/sandbox/worktree.js +174 -174
- package/bin/runners/lib/scan-output.js +525 -190
- package/bin/runners/lib/schema-validator.js +350 -350
- package/bin/runners/lib/schemas/contracts.schema.json +160 -160
- package/bin/runners/lib/schemas/finding.schema.json +100 -100
- package/bin/runners/lib/schemas/mission-pack.schema.json +206 -206
- package/bin/runners/lib/schemas/proof-graph.schema.json +176 -176
- package/bin/runners/lib/schemas/reality-report.schema.json +162 -162
- package/bin/runners/lib/schemas/share-pack.schema.json +180 -180
- package/bin/runners/lib/schemas/ship-report.schema.json +117 -117
- package/bin/runners/lib/schemas/truthpack-v2.schema.json +303 -303
- package/bin/runners/lib/schemas/validator.js +438 -438
- package/bin/runners/lib/score-history.js +282 -282
- package/bin/runners/lib/share-pack.js +239 -239
- package/bin/runners/lib/snippets.js +67 -67
- package/bin/runners/lib/status-output.js +253 -253
- package/bin/runners/lib/terminal-ui.js +351 -271
- package/bin/runners/lib/upsell.js +510 -510
- package/bin/runners/lib/usage.js +153 -153
- package/bin/runners/lib/validate-patch.js +156 -156
- package/bin/runners/lib/verdict-engine.js +628 -628
- package/bin/runners/reality/engine.js +917 -917
- package/bin/runners/reality/flows.js +122 -122
- package/bin/runners/reality/report.js +378 -378
- package/bin/runners/reality/session.js +193 -193
- package/bin/runners/runGuard.js +168 -168
- package/bin/runners/runProof.zip +0 -0
- package/bin/runners/runProve.js +8 -0
- package/bin/runners/runReality.js +14 -0
- package/bin/runners/runScan.js +17 -1
- package/bin/runners/runTruth.js +15 -3
- package/mcp-server/tier-auth.js +4 -4
- package/mcp-server/tools/index.js +72 -72
- package/package.json +1 -1
|
@@ -1,348 +1,348 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Replay Player
|
|
3
|
-
* Replays recorded user interactions and verifies behavior
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
const { EventEmitter } = require('events');
|
|
7
|
-
const { performance } = require('perf_hooks');
|
|
8
|
-
const fs = require('fs').promises;
|
|
9
|
-
const path = require('path');
|
|
10
|
-
const debug = require('debug')('vibecheck:replay:player');
|
|
11
|
-
|
|
12
|
-
class ReplayPlayer extends EventEmitter {
|
|
13
|
-
constructor(options = {}) {
|
|
14
|
-
super();
|
|
15
|
-
this.options = {
|
|
16
|
-
speed: 1.0,
|
|
17
|
-
headless: true,
|
|
18
|
-
viewport: { width: 1280, height: 800 },
|
|
19
|
-
waitForNetworkIdle: true,
|
|
20
|
-
networkIdleTimeout: 500,
|
|
21
|
-
waitForSelectorTimeout: 30000,
|
|
22
|
-
...options
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
this.state = 'idle';
|
|
26
|
-
this.currentStep = 0;
|
|
27
|
-
this.stats = {
|
|
28
|
-
totalSteps: 0,
|
|
29
|
-
passed: 0,
|
|
30
|
-
failed: 0,
|
|
31
|
-
skipped: 0,
|
|
32
|
-
startTime: null,
|
|
33
|
-
endTime: null,
|
|
34
|
-
duration: 0
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
this.page = null;
|
|
38
|
-
this.capsule = null;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
async loadCapsule(capsulePath) {
|
|
42
|
-
this.state = 'loading';
|
|
43
|
-
|
|
44
|
-
try {
|
|
45
|
-
// Load from file if it's a path
|
|
46
|
-
if (typeof capsulePath === 'string') {
|
|
47
|
-
const data = await fs.readFile(capsulePath, 'utf8');
|
|
48
|
-
this.capsule = JSON.parse(data);
|
|
49
|
-
}
|
|
50
|
-
// Or use the provided capsule object
|
|
51
|
-
else if (capsulePath.metadata && Array.isArray(capsulePath.steps)) {
|
|
52
|
-
this.capsule = capsulePath;
|
|
53
|
-
}
|
|
54
|
-
// Or load from a directory containing metadata.json and steps.json
|
|
55
|
-
else if (typeof capsulePath === 'object' && capsulePath.path) {
|
|
56
|
-
const [metadata, steps] = await Promise.all([
|
|
57
|
-
fs.readFile(path.join(capsulePath.path, 'metadata.json'), 'utf8')
|
|
58
|
-
.then(JSON.parse),
|
|
59
|
-
fs.readFile(path.join(capsulePath.path, 'steps.json'), 'utf8')
|
|
60
|
-
.then(JSON.parse)
|
|
61
|
-
]);
|
|
62
|
-
this.capsule = { metadata, steps };
|
|
63
|
-
} else {
|
|
64
|
-
throw new Error('Invalid capsule format');
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
this.stats.totalSteps = this.capsule.steps.length;
|
|
68
|
-
this.state = 'loaded';
|
|
69
|
-
this.emit('loaded', { steps: this.stats.totalSteps });
|
|
70
|
-
|
|
71
|
-
return this.capsule;
|
|
72
|
-
} catch (error) {
|
|
73
|
-
this.state = 'error';
|
|
74
|
-
this.emit('error', { error, phase: 'load' });
|
|
75
|
-
throw error;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
async setupPlaywright(browser) {
|
|
80
|
-
if (this.page) return this.page;
|
|
81
|
-
|
|
82
|
-
try {
|
|
83
|
-
const context = await browser.newContext({
|
|
84
|
-
viewport: this.capsule.metadata.viewport || this.options.viewport,
|
|
85
|
-
userAgent: this.capsule.metadata.userAgent,
|
|
86
|
-
ignoreHTTPSErrors: true
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
this.page = await context.newPage();
|
|
90
|
-
|
|
91
|
-
// Set up request/response tracking
|
|
92
|
-
this._setupNetworkMonitoring();
|
|
93
|
-
|
|
94
|
-
return this.page;
|
|
95
|
-
} catch (error) {
|
|
96
|
-
this.state = 'error';
|
|
97
|
-
this.emit('error', { error, phase: 'setup' });
|
|
98
|
-
throw error;
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
_setupNetworkMonitoring() {
|
|
103
|
-
if (!this.page) return;
|
|
104
|
-
|
|
105
|
-
this.pendingRequests = new Set();
|
|
106
|
-
|
|
107
|
-
this.page.on('request', request => {
|
|
108
|
-
if (this.options.waitForNetworkIdle) {
|
|
109
|
-
this.pendingRequests.add(request);
|
|
110
|
-
}
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
this.page.on('requestfinished', request => {
|
|
114
|
-
this.pendingRequests.delete(request);
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
this.page.on('requestfailed', request => {
|
|
118
|
-
this.pendingRequests.delete(request);
|
|
119
|
-
});
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
async waitForNetworkIdle() {
|
|
123
|
-
if (!this.options.waitForNetworkIdle || !this.page) return;
|
|
124
|
-
|
|
125
|
-
const start = performance.now();
|
|
126
|
-
let hasPending = false;
|
|
127
|
-
|
|
128
|
-
do {
|
|
129
|
-
hasPending = this.pendingRequests.size > 0;
|
|
130
|
-
|
|
131
|
-
if (hasPending) {
|
|
132
|
-
await new Promise(r => setTimeout(r, 50));
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
if (performance.now() - start > this.options.networkIdleTimeout) {
|
|
136
|
-
debug(`Network idle timeout after ${this.options.networkIdleTimeout}ms`);
|
|
137
|
-
break;
|
|
138
|
-
}
|
|
139
|
-
} while (hasPending);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
async play() {
|
|
143
|
-
if (this.state !== 'loaded') {
|
|
144
|
-
throw new Error('No capsule loaded. Call loadCapsule() first.');
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
this.state = 'playing';
|
|
148
|
-
this.currentStep = 0;
|
|
149
|
-
this.stats.startTime = performance.now();
|
|
150
|
-
this.stats.passed = 0;
|
|
151
|
-
this.stats.failed = 0;
|
|
152
|
-
this.stats.skipped = 0;
|
|
153
|
-
|
|
154
|
-
this.emit('start', { steps: this.stats.totalSteps });
|
|
155
|
-
|
|
156
|
-
try {
|
|
157
|
-
for (const step of this.capsule.steps) {
|
|
158
|
-
this.currentStep++;
|
|
159
|
-
this.emit('stepStart', { step: this.currentStep, total: this.stats.totalSteps, type: step.type });
|
|
160
|
-
|
|
161
|
-
try {
|
|
162
|
-
await this._executeStep(step);
|
|
163
|
-
this.stats.passed++;
|
|
164
|
-
this.emit('stepPass', {
|
|
165
|
-
step: this.currentStep,
|
|
166
|
-
type: step.type,
|
|
167
|
-
duration: performance.now() - (step.timestamp || 0)
|
|
168
|
-
});
|
|
169
|
-
} catch (error) {
|
|
170
|
-
this.stats.failed++;
|
|
171
|
-
this.emit('stepFail', {
|
|
172
|
-
step: this.currentStep,
|
|
173
|
-
type: step.type,
|
|
174
|
-
error,
|
|
175
|
-
duration: performance.now() - (step.timestamp || 0)
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
if (this.options.stopOnFailure) {
|
|
179
|
-
throw error;
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// Apply speed factor to timing
|
|
184
|
-
if (this.currentStep < this.capsule.steps.length - 1) {
|
|
185
|
-
const nextStep = this.capsule.steps[this.currentStep];
|
|
186
|
-
const currentTime = step.timestamp || 0;
|
|
187
|
-
const nextTime = nextStep.timestamp || 0;
|
|
188
|
-
const delay = (nextTime - currentTime) / this.options.speed;
|
|
189
|
-
|
|
190
|
-
if (delay > 0) {
|
|
191
|
-
await new Promise(resolve => setTimeout(resolve, delay));
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
this.stats.endTime = performance.now();
|
|
197
|
-
this.stats.duration = this.stats.endTime - this.stats.startTime;
|
|
198
|
-
this.state = 'completed';
|
|
199
|
-
|
|
200
|
-
const result = {
|
|
201
|
-
...this.stats,
|
|
202
|
-
success: this.stats.failed === 0,
|
|
203
|
-
capsule: this.capsule.metadata
|
|
204
|
-
};
|
|
205
|
-
|
|
206
|
-
this.emit('complete', result);
|
|
207
|
-
return result;
|
|
208
|
-
} catch (error) {
|
|
209
|
-
this.state = 'error';
|
|
210
|
-
this.emit('error', {
|
|
211
|
-
error,
|
|
212
|
-
step: this.currentStep,
|
|
213
|
-
phase: 'playback'
|
|
214
|
-
});
|
|
215
|
-
throw error;
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
async _executeStep(step) {
|
|
220
|
-
if (!this.page) {
|
|
221
|
-
throw new Error('No page available. Call setupPlaywright() first.');
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
debug(`Executing step: ${step.type}`, { step });
|
|
225
|
-
|
|
226
|
-
switch (step.type) {
|
|
227
|
-
case 'navigation':
|
|
228
|
-
await this.page.goto(step.url, {
|
|
229
|
-
waitUntil: 'networkidle',
|
|
230
|
-
timeout: this.options.waitForSelectorTimeout
|
|
231
|
-
});
|
|
232
|
-
break;
|
|
233
|
-
|
|
234
|
-
case 'click':
|
|
235
|
-
if (step.selector) {
|
|
236
|
-
await this.page.waitForSelector(step.selector, {
|
|
237
|
-
state: 'visible',
|
|
238
|
-
timeout: this.options.waitForSelectorTimeout
|
|
239
|
-
});
|
|
240
|
-
await this.page.click(step.selector, {
|
|
241
|
-
button: step.button,
|
|
242
|
-
clickCount: step.clickCount || 1,
|
|
243
|
-
delay: 50
|
|
244
|
-
});
|
|
245
|
-
} else if (step.x !== undefined && step.y !== undefined) {
|
|
246
|
-
await this.page.mouse.click(step.x, step.y, {
|
|
247
|
-
button: step.button,
|
|
248
|
-
clickCount: step.clickCount || 1,
|
|
249
|
-
delay: 50
|
|
250
|
-
});
|
|
251
|
-
} else {
|
|
252
|
-
throw new Error('Click step requires either selector or x,y coordinates');
|
|
253
|
-
}
|
|
254
|
-
break;
|
|
255
|
-
|
|
256
|
-
case 'input':
|
|
257
|
-
if (!step.selector) {
|
|
258
|
-
throw new Error('Input step requires a selector');
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
await this.page.waitForSelector(step.selector, {
|
|
262
|
-
state: 'visible',
|
|
263
|
-
timeout: this.options.waitForSelectorTimeout
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
// Clear existing value first
|
|
267
|
-
await this.page.$eval(step.selector, el => {
|
|
268
|
-
if (el) el.value = '';
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
// Type the new value
|
|
272
|
-
await this.page.type(step.selector, step.value || '', {
|
|
273
|
-
delay: 20 // Simulate realistic typing
|
|
274
|
-
});
|
|
275
|
-
break;
|
|
276
|
-
|
|
277
|
-
case 'keypress':
|
|
278
|
-
if (step.key) {
|
|
279
|
-
await this.page.keyboard.press(step.key, {
|
|
280
|
-
delay: 20
|
|
281
|
-
});
|
|
282
|
-
}
|
|
283
|
-
break;
|
|
284
|
-
|
|
285
|
-
case 'network_request':
|
|
286
|
-
// Just verify the request was made, no action needed
|
|
287
|
-
break;
|
|
288
|
-
|
|
289
|
-
case 'network_response':
|
|
290
|
-
// Verify response status if expected
|
|
291
|
-
if (step.expectedStatus && step.status !== step.expectedStatus) {
|
|
292
|
-
throw new Error(`Expected status ${step.expectedStatus} but got ${step.status}`);
|
|
293
|
-
}
|
|
294
|
-
break;
|
|
295
|
-
|
|
296
|
-
case 'custom_assert':
|
|
297
|
-
// Execute custom assertion
|
|
298
|
-
if (typeof step.assert === 'function') {
|
|
299
|
-
const result = await step.assert(this.page);
|
|
300
|
-
if (result === false) {
|
|
301
|
-
throw new Error('Custom assertion failed');
|
|
302
|
-
} else if (result && result.message) {
|
|
303
|
-
throw new Error(`Assertion failed: ${result.message}`);
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
break;
|
|
307
|
-
|
|
308
|
-
default:
|
|
309
|
-
debug(`Skipping unsupported step type: ${step.type}`);
|
|
310
|
-
this.stats.skipped++;
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
// Wait for network to be idle after each step
|
|
314
|
-
await this.waitForNetworkIdle();
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
async takeScreenshot(options = {}) {
|
|
318
|
-
if (!this.page) return null;
|
|
319
|
-
|
|
320
|
-
const defaultPath = path.join(
|
|
321
|
-
process.cwd(),
|
|
322
|
-
'screenshots',
|
|
323
|
-
`screenshot-${Date.now()}.png`
|
|
324
|
-
);
|
|
325
|
-
|
|
326
|
-
const screenshotPath = options.path || defaultPath;
|
|
327
|
-
await fs.mkdir(path.dirname(screenshotPath), { recursive: true });
|
|
328
|
-
|
|
329
|
-
await this.page.screenshot({
|
|
330
|
-
path: screenshotPath,
|
|
331
|
-
fullPage: options.fullPage !== false,
|
|
332
|
-
...options
|
|
333
|
-
});
|
|
334
|
-
|
|
335
|
-
return screenshotPath;
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
async close() {
|
|
339
|
-
if (this.page) {
|
|
340
|
-
await this.page.close().catch(console.error);
|
|
341
|
-
this.page = null;
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
this.state = 'idle';
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
module.exports = ReplayPlayer;
|
|
1
|
+
/**
|
|
2
|
+
* Replay Player
|
|
3
|
+
* Replays recorded user interactions and verifies behavior
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { EventEmitter } = require('events');
|
|
7
|
+
const { performance } = require('perf_hooks');
|
|
8
|
+
const fs = require('fs').promises;
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const debug = require('debug')('vibecheck:replay:player');
|
|
11
|
+
|
|
12
|
+
class ReplayPlayer extends EventEmitter {
|
|
13
|
+
constructor(options = {}) {
|
|
14
|
+
super();
|
|
15
|
+
this.options = {
|
|
16
|
+
speed: 1.0,
|
|
17
|
+
headless: true,
|
|
18
|
+
viewport: { width: 1280, height: 800 },
|
|
19
|
+
waitForNetworkIdle: true,
|
|
20
|
+
networkIdleTimeout: 500,
|
|
21
|
+
waitForSelectorTimeout: 30000,
|
|
22
|
+
...options
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
this.state = 'idle';
|
|
26
|
+
this.currentStep = 0;
|
|
27
|
+
this.stats = {
|
|
28
|
+
totalSteps: 0,
|
|
29
|
+
passed: 0,
|
|
30
|
+
failed: 0,
|
|
31
|
+
skipped: 0,
|
|
32
|
+
startTime: null,
|
|
33
|
+
endTime: null,
|
|
34
|
+
duration: 0
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
this.page = null;
|
|
38
|
+
this.capsule = null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async loadCapsule(capsulePath) {
|
|
42
|
+
this.state = 'loading';
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
// Load from file if it's a path
|
|
46
|
+
if (typeof capsulePath === 'string') {
|
|
47
|
+
const data = await fs.readFile(capsulePath, 'utf8');
|
|
48
|
+
this.capsule = JSON.parse(data);
|
|
49
|
+
}
|
|
50
|
+
// Or use the provided capsule object
|
|
51
|
+
else if (capsulePath.metadata && Array.isArray(capsulePath.steps)) {
|
|
52
|
+
this.capsule = capsulePath;
|
|
53
|
+
}
|
|
54
|
+
// Or load from a directory containing metadata.json and steps.json
|
|
55
|
+
else if (typeof capsulePath === 'object' && capsulePath.path) {
|
|
56
|
+
const [metadata, steps] = await Promise.all([
|
|
57
|
+
fs.readFile(path.join(capsulePath.path, 'metadata.json'), 'utf8')
|
|
58
|
+
.then(JSON.parse),
|
|
59
|
+
fs.readFile(path.join(capsulePath.path, 'steps.json'), 'utf8')
|
|
60
|
+
.then(JSON.parse)
|
|
61
|
+
]);
|
|
62
|
+
this.capsule = { metadata, steps };
|
|
63
|
+
} else {
|
|
64
|
+
throw new Error('Invalid capsule format');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
this.stats.totalSteps = this.capsule.steps.length;
|
|
68
|
+
this.state = 'loaded';
|
|
69
|
+
this.emit('loaded', { steps: this.stats.totalSteps });
|
|
70
|
+
|
|
71
|
+
return this.capsule;
|
|
72
|
+
} catch (error) {
|
|
73
|
+
this.state = 'error';
|
|
74
|
+
this.emit('error', { error, phase: 'load' });
|
|
75
|
+
throw error;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async setupPlaywright(browser) {
|
|
80
|
+
if (this.page) return this.page;
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
const context = await browser.newContext({
|
|
84
|
+
viewport: this.capsule.metadata.viewport || this.options.viewport,
|
|
85
|
+
userAgent: this.capsule.metadata.userAgent,
|
|
86
|
+
ignoreHTTPSErrors: true
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
this.page = await context.newPage();
|
|
90
|
+
|
|
91
|
+
// Set up request/response tracking
|
|
92
|
+
this._setupNetworkMonitoring();
|
|
93
|
+
|
|
94
|
+
return this.page;
|
|
95
|
+
} catch (error) {
|
|
96
|
+
this.state = 'error';
|
|
97
|
+
this.emit('error', { error, phase: 'setup' });
|
|
98
|
+
throw error;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
_setupNetworkMonitoring() {
|
|
103
|
+
if (!this.page) return;
|
|
104
|
+
|
|
105
|
+
this.pendingRequests = new Set();
|
|
106
|
+
|
|
107
|
+
this.page.on('request', request => {
|
|
108
|
+
if (this.options.waitForNetworkIdle) {
|
|
109
|
+
this.pendingRequests.add(request);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
this.page.on('requestfinished', request => {
|
|
114
|
+
this.pendingRequests.delete(request);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
this.page.on('requestfailed', request => {
|
|
118
|
+
this.pendingRequests.delete(request);
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async waitForNetworkIdle() {
|
|
123
|
+
if (!this.options.waitForNetworkIdle || !this.page) return;
|
|
124
|
+
|
|
125
|
+
const start = performance.now();
|
|
126
|
+
let hasPending = false;
|
|
127
|
+
|
|
128
|
+
do {
|
|
129
|
+
hasPending = this.pendingRequests.size > 0;
|
|
130
|
+
|
|
131
|
+
if (hasPending) {
|
|
132
|
+
await new Promise(r => setTimeout(r, 50));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (performance.now() - start > this.options.networkIdleTimeout) {
|
|
136
|
+
debug(`Network idle timeout after ${this.options.networkIdleTimeout}ms`);
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
} while (hasPending);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async play() {
|
|
143
|
+
if (this.state !== 'loaded') {
|
|
144
|
+
throw new Error('No capsule loaded. Call loadCapsule() first.');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
this.state = 'playing';
|
|
148
|
+
this.currentStep = 0;
|
|
149
|
+
this.stats.startTime = performance.now();
|
|
150
|
+
this.stats.passed = 0;
|
|
151
|
+
this.stats.failed = 0;
|
|
152
|
+
this.stats.skipped = 0;
|
|
153
|
+
|
|
154
|
+
this.emit('start', { steps: this.stats.totalSteps });
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
for (const step of this.capsule.steps) {
|
|
158
|
+
this.currentStep++;
|
|
159
|
+
this.emit('stepStart', { step: this.currentStep, total: this.stats.totalSteps, type: step.type });
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
await this._executeStep(step);
|
|
163
|
+
this.stats.passed++;
|
|
164
|
+
this.emit('stepPass', {
|
|
165
|
+
step: this.currentStep,
|
|
166
|
+
type: step.type,
|
|
167
|
+
duration: performance.now() - (step.timestamp || 0)
|
|
168
|
+
});
|
|
169
|
+
} catch (error) {
|
|
170
|
+
this.stats.failed++;
|
|
171
|
+
this.emit('stepFail', {
|
|
172
|
+
step: this.currentStep,
|
|
173
|
+
type: step.type,
|
|
174
|
+
error,
|
|
175
|
+
duration: performance.now() - (step.timestamp || 0)
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
if (this.options.stopOnFailure) {
|
|
179
|
+
throw error;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Apply speed factor to timing
|
|
184
|
+
if (this.currentStep < this.capsule.steps.length - 1) {
|
|
185
|
+
const nextStep = this.capsule.steps[this.currentStep];
|
|
186
|
+
const currentTime = step.timestamp || 0;
|
|
187
|
+
const nextTime = nextStep.timestamp || 0;
|
|
188
|
+
const delay = (nextTime - currentTime) / this.options.speed;
|
|
189
|
+
|
|
190
|
+
if (delay > 0) {
|
|
191
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
this.stats.endTime = performance.now();
|
|
197
|
+
this.stats.duration = this.stats.endTime - this.stats.startTime;
|
|
198
|
+
this.state = 'completed';
|
|
199
|
+
|
|
200
|
+
const result = {
|
|
201
|
+
...this.stats,
|
|
202
|
+
success: this.stats.failed === 0,
|
|
203
|
+
capsule: this.capsule.metadata
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
this.emit('complete', result);
|
|
207
|
+
return result;
|
|
208
|
+
} catch (error) {
|
|
209
|
+
this.state = 'error';
|
|
210
|
+
this.emit('error', {
|
|
211
|
+
error,
|
|
212
|
+
step: this.currentStep,
|
|
213
|
+
phase: 'playback'
|
|
214
|
+
});
|
|
215
|
+
throw error;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async _executeStep(step) {
|
|
220
|
+
if (!this.page) {
|
|
221
|
+
throw new Error('No page available. Call setupPlaywright() first.');
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
debug(`Executing step: ${step.type}`, { step });
|
|
225
|
+
|
|
226
|
+
switch (step.type) {
|
|
227
|
+
case 'navigation':
|
|
228
|
+
await this.page.goto(step.url, {
|
|
229
|
+
waitUntil: 'networkidle',
|
|
230
|
+
timeout: this.options.waitForSelectorTimeout
|
|
231
|
+
});
|
|
232
|
+
break;
|
|
233
|
+
|
|
234
|
+
case 'click':
|
|
235
|
+
if (step.selector) {
|
|
236
|
+
await this.page.waitForSelector(step.selector, {
|
|
237
|
+
state: 'visible',
|
|
238
|
+
timeout: this.options.waitForSelectorTimeout
|
|
239
|
+
});
|
|
240
|
+
await this.page.click(step.selector, {
|
|
241
|
+
button: step.button,
|
|
242
|
+
clickCount: step.clickCount || 1,
|
|
243
|
+
delay: 50
|
|
244
|
+
});
|
|
245
|
+
} else if (step.x !== undefined && step.y !== undefined) {
|
|
246
|
+
await this.page.mouse.click(step.x, step.y, {
|
|
247
|
+
button: step.button,
|
|
248
|
+
clickCount: step.clickCount || 1,
|
|
249
|
+
delay: 50
|
|
250
|
+
});
|
|
251
|
+
} else {
|
|
252
|
+
throw new Error('Click step requires either selector or x,y coordinates');
|
|
253
|
+
}
|
|
254
|
+
break;
|
|
255
|
+
|
|
256
|
+
case 'input':
|
|
257
|
+
if (!step.selector) {
|
|
258
|
+
throw new Error('Input step requires a selector');
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
await this.page.waitForSelector(step.selector, {
|
|
262
|
+
state: 'visible',
|
|
263
|
+
timeout: this.options.waitForSelectorTimeout
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
// Clear existing value first
|
|
267
|
+
await this.page.$eval(step.selector, el => {
|
|
268
|
+
if (el) el.value = '';
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
// Type the new value
|
|
272
|
+
await this.page.type(step.selector, step.value || '', {
|
|
273
|
+
delay: 20 // Simulate realistic typing
|
|
274
|
+
});
|
|
275
|
+
break;
|
|
276
|
+
|
|
277
|
+
case 'keypress':
|
|
278
|
+
if (step.key) {
|
|
279
|
+
await this.page.keyboard.press(step.key, {
|
|
280
|
+
delay: 20
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
break;
|
|
284
|
+
|
|
285
|
+
case 'network_request':
|
|
286
|
+
// Just verify the request was made, no action needed
|
|
287
|
+
break;
|
|
288
|
+
|
|
289
|
+
case 'network_response':
|
|
290
|
+
// Verify response status if expected
|
|
291
|
+
if (step.expectedStatus && step.status !== step.expectedStatus) {
|
|
292
|
+
throw new Error(`Expected status ${step.expectedStatus} but got ${step.status}`);
|
|
293
|
+
}
|
|
294
|
+
break;
|
|
295
|
+
|
|
296
|
+
case 'custom_assert':
|
|
297
|
+
// Execute custom assertion
|
|
298
|
+
if (typeof step.assert === 'function') {
|
|
299
|
+
const result = await step.assert(this.page);
|
|
300
|
+
if (result === false) {
|
|
301
|
+
throw new Error('Custom assertion failed');
|
|
302
|
+
} else if (result && result.message) {
|
|
303
|
+
throw new Error(`Assertion failed: ${result.message}`);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
break;
|
|
307
|
+
|
|
308
|
+
default:
|
|
309
|
+
debug(`Skipping unsupported step type: ${step.type}`);
|
|
310
|
+
this.stats.skipped++;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Wait for network to be idle after each step
|
|
314
|
+
await this.waitForNetworkIdle();
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
async takeScreenshot(options = {}) {
|
|
318
|
+
if (!this.page) return null;
|
|
319
|
+
|
|
320
|
+
const defaultPath = path.join(
|
|
321
|
+
process.cwd(),
|
|
322
|
+
'screenshots',
|
|
323
|
+
`screenshot-${Date.now()}.png`
|
|
324
|
+
);
|
|
325
|
+
|
|
326
|
+
const screenshotPath = options.path || defaultPath;
|
|
327
|
+
await fs.mkdir(path.dirname(screenshotPath), { recursive: true });
|
|
328
|
+
|
|
329
|
+
await this.page.screenshot({
|
|
330
|
+
path: screenshotPath,
|
|
331
|
+
fullPage: options.fullPage !== false,
|
|
332
|
+
...options
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
return screenshotPath;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
async close() {
|
|
339
|
+
if (this.page) {
|
|
340
|
+
await this.page.close().catch(console.error);
|
|
341
|
+
this.page = null;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
this.state = 'idle';
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
module.exports = ReplayPlayer;
|