autokap 1.0.7 → 1.0.8
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/assets/cursors/macos.svg +4 -0
- package/assets/cursors/windows.svg +15 -0
- package/assets/skill/OPCODE-REFERENCE.md +607 -0
- package/assets/skill/README.md +39 -0
- package/assets/skill/SKILL.md +453 -468
- package/assets/skill/STUDIO-SKILL.md +476 -0
- package/assets/skill/references/examples.md +104 -0
- package/assets/skill/references/interactive-demo.md +225 -0
- package/assets/skill/references/mock-data.md +178 -0
- package/dist/action-verifier.d.ts +29 -0
- package/dist/action-verifier.js +133 -0
- package/dist/agent-action-recovery.d.ts +45 -0
- package/dist/agent-action-recovery.js +370 -0
- package/dist/agent-message-utils.d.ts +21 -0
- package/dist/agent-message-utils.js +77 -0
- package/dist/agent-url-utils.d.ts +30 -0
- package/dist/agent-url-utils.js +138 -0
- package/dist/agent.d.ts +92 -8
- package/dist/agent.js +2936 -781
- package/dist/ak-tree.d.ts +39 -0
- package/dist/ak-tree.js +368 -0
- package/dist/alt-text.d.ts +26 -0
- package/dist/alt-text.js +55 -0
- package/dist/auth-capture.d.ts +17 -0
- package/dist/auth-capture.js +164 -0
- package/dist/benchmark.d.ts +59 -0
- package/dist/benchmark.js +135 -0
- package/dist/browser-bar.d.ts +14 -6
- package/dist/browser-bar.js +145 -8
- package/dist/browser-pool.d.ts +7 -0
- package/dist/browser-pool.js +15 -5
- package/dist/browser-utils.d.ts +31 -0
- package/dist/browser-utils.js +97 -0
- package/dist/browser.d.ts +51 -1
- package/dist/browser.js +1481 -31
- package/dist/capture-alt-text.js +2 -1
- package/dist/capture-language-preflight.js +14 -0
- package/dist/capture-llm-page-identity.js +22 -10
- package/dist/capture-page-identity.d.ts +5 -7
- package/dist/capture-page-identity.js +211 -78
- package/dist/capture-preset-credentials.d.ts +50 -0
- package/dist/capture-preset-credentials.js +127 -0
- package/dist/capture-request-plan.d.ts +2 -2
- package/dist/capture-request-plan.js +64 -16
- package/dist/capture-run-optimizer.js +48 -33
- package/dist/capture-selector-memory.d.ts +5 -0
- package/dist/capture-selector-memory.js +18 -0
- package/dist/capture-strategy.d.ts +36 -0
- package/dist/capture-strategy.js +95 -0
- package/dist/capture-studio-sync.d.ts +1 -0
- package/dist/capture-studio-sync.js +9 -3
- package/dist/capture-surface-contract.d.ts +36 -0
- package/dist/capture-surface-contract.js +299 -0
- package/dist/capture-transition-engine.d.ts +28 -0
- package/dist/capture-transition-engine.js +292 -0
- package/dist/capture-variant-state.d.ts +2 -0
- package/dist/capture-variant-state.js +26 -0
- package/dist/capture-verification.d.ts +35 -0
- package/dist/capture-verification.js +95 -0
- package/dist/capture-viewport-lock.d.ts +48 -0
- package/dist/capture-viewport-lock.js +74 -0
- package/dist/circuit-breaker.d.ts +42 -0
- package/dist/circuit-breaker.js +119 -0
- package/dist/cli-config.d.ts +8 -1
- package/dist/cli-config.js +62 -6
- package/dist/cli-contract.d.ts +15 -0
- package/dist/cli-contract.js +167 -0
- package/dist/cli-runner-local.d.ts +12 -0
- package/dist/cli-runner-local.js +102 -0
- package/dist/cli-runner.d.ts +34 -0
- package/dist/cli-runner.js +433 -0
- package/dist/cli-utils.d.ts +0 -1
- package/dist/cli-utils.js +2 -5
- package/dist/cli.js +1005 -267
- package/dist/clip-orchestrator.js +9 -2
- package/dist/clip-postprocess.js +25 -16
- package/dist/cookie-dismiss.d.ts +2 -0
- package/dist/cookie-dismiss.js +48 -13
- package/dist/cost-logging.d.ts +8 -0
- package/dist/cost-logging.js +160 -46
- package/dist/cost-resolution-monitor.d.ts +16 -0
- package/dist/cost-resolution-monitor.js +34 -0
- package/dist/credential-templates.js +2 -2
- package/dist/cursor-overlay-script.d.ts +6 -0
- package/dist/cursor-overlay-script.js +169 -0
- package/dist/dom-css-purger.d.ts +65 -0
- package/dist/dom-css-purger.js +333 -0
- package/dist/dom-font-inliner.d.ts +45 -0
- package/dist/dom-font-inliner.js +148 -0
- package/dist/dom-patch-resolver.d.ts +52 -0
- package/dist/dom-patch-resolver.js +242 -0
- package/dist/dom-serializer.d.ts +82 -0
- package/dist/dom-serializer.js +378 -0
- package/dist/element-capture.d.ts +1 -41
- package/dist/element-capture.js +202 -446
- package/dist/env-validation.d.ts +5 -0
- package/dist/env-validation.js +29 -0
- package/dist/execution-schema.d.ts +4423 -0
- package/dist/execution-schema.js +507 -0
- package/dist/execution-types.d.ts +886 -0
- package/dist/execution-types.js +65 -0
- package/dist/fonts-loader.d.ts +14 -0
- package/dist/fonts-loader.js +55 -0
- package/dist/hybrid-navigator.js +12 -12
- package/dist/index.d.ts +9 -6
- package/dist/index.js +10 -4
- package/dist/legacy/agent-action-recovery.d.ts +45 -0
- package/dist/legacy/agent-action-recovery.js +370 -0
- package/dist/legacy/agent-message-utils.d.ts +21 -0
- package/dist/legacy/agent-message-utils.js +77 -0
- package/dist/legacy/agent-url-utils.d.ts +30 -0
- package/dist/legacy/agent-url-utils.js +138 -0
- package/dist/legacy/agent.d.ts +226 -0
- package/dist/legacy/agent.js +6666 -0
- package/dist/legacy/clip-orchestrator.d.ts +148 -0
- package/dist/legacy/clip-orchestrator.js +957 -0
- package/dist/legacy/credential-templates.d.ts +5 -0
- package/dist/legacy/credential-templates.js +60 -0
- package/dist/legacy/hybrid-navigator.d.ts +138 -0
- package/dist/legacy/hybrid-navigator.js +468 -0
- package/dist/legacy/llm-usage.d.ts +17 -0
- package/dist/legacy/llm-usage.js +45 -0
- package/dist/legacy/prompt-cache.d.ts +10 -0
- package/dist/legacy/prompt-cache.js +24 -0
- package/dist/legacy/prompts.d.ts +175 -0
- package/dist/legacy/prompts.js +1038 -0
- package/dist/legacy/tools.d.ts +4 -0
- package/dist/legacy/tools.js +216 -0
- package/dist/legacy/video-agent.d.ts +143 -0
- package/dist/legacy/video-agent.js +4788 -0
- package/dist/legacy/video-observation.d.ts +36 -0
- package/dist/legacy/video-observation.js +192 -0
- package/dist/legacy/video-planner.d.ts +12 -0
- package/dist/legacy/video-planner.js +501 -0
- package/dist/legacy/video-prompts.d.ts +37 -0
- package/dist/legacy/video-prompts.js +569 -0
- package/dist/legacy/video-tools.d.ts +3 -0
- package/dist/legacy/video-tools.js +59 -0
- package/dist/legacy/video-variant-state.d.ts +29 -0
- package/dist/legacy/video-variant-state.js +80 -0
- package/dist/legacy/vision-model.d.ts +17 -0
- package/dist/legacy/vision-model.js +74 -0
- package/dist/llm-healer.d.ts +63 -0
- package/dist/llm-healer.js +166 -0
- package/dist/llm-provider.d.ts +29 -0
- package/dist/llm-provider.js +80 -0
- package/dist/logger.d.ts +6 -2
- package/dist/logger.js +15 -1
- package/dist/mockup-html.js +35 -25
- package/dist/mockup.d.ts +95 -2
- package/dist/mockup.js +427 -166
- package/dist/mouse-animation.d.ts +2 -2
- package/dist/mouse-animation.js +34 -20
- package/dist/opcode-actions.d.ts +42 -0
- package/dist/opcode-actions.js +511 -0
- package/dist/opcode-runner.d.ts +51 -0
- package/dist/opcode-runner.js +770 -0
- package/dist/openrouter-client.d.ts +40 -0
- package/dist/openrouter-client.js +16 -0
- package/dist/overlay-engine.d.ts +24 -0
- package/dist/overlay-engine.js +176 -0
- package/dist/postcondition.d.ts +16 -0
- package/dist/postcondition.js +269 -0
- package/dist/program-patcher.d.ts +25 -0
- package/dist/program-patcher.js +44 -0
- package/dist/prompts.d.ts +13 -5
- package/dist/prompts.js +224 -351
- package/dist/provider-config.d.ts +12 -0
- package/dist/provider-config.js +15 -0
- package/dist/recovery-chain.d.ts +37 -0
- package/dist/recovery-chain.js +350 -0
- package/dist/remote-browser.d.ts +28 -4
- package/dist/remote-browser.js +60 -5
- package/dist/safari-browser-bar.d.ts +15 -0
- package/dist/safari-browser-bar.js +95 -0
- package/dist/safari-toolbar-asset.d.ts +15 -0
- package/dist/safari-toolbar-asset.js +12 -0
- package/dist/security.d.ts +2 -1
- package/dist/security.js +49 -10
- package/dist/selector-resolver.d.ts +34 -0
- package/dist/selector-resolver.js +181 -0
- package/dist/semantic-resolver.d.ts +35 -0
- package/dist/semantic-resolver.js +161 -0
- package/dist/server-capture-runtime.d.ts +5 -3
- package/dist/server-capture-runtime.js +42 -95
- package/dist/server-credit-usage.d.ts +2 -2
- package/dist/server-project-webhooks.d.ts +15 -1
- package/dist/server-project-webhooks.js +34 -8
- package/dist/server-screenshot-watermark.js +27 -5
- package/dist/session-profile.js +164 -1
- package/dist/sf-pro-symbols.d.ts +1 -0
- package/dist/sf-pro-symbols.js +55 -0
- package/dist/skill-packaging.d.ts +28 -0
- package/dist/skill-packaging.js +169 -0
- package/dist/smart-wait.d.ts +27 -0
- package/dist/smart-wait.js +81 -0
- package/dist/status-bar-render.d.ts +20 -0
- package/dist/status-bar-render.js +410 -0
- package/dist/status-bar.d.ts +9 -0
- package/dist/status-bar.js +298 -14
- package/dist/svg-browser-bar.d.ts +33 -0
- package/dist/svg-browser-bar.js +206 -0
- package/dist/svg-status-bar.d.ts +36 -0
- package/dist/svg-status-bar.js +597 -0
- package/dist/svg-text.d.ts +61 -0
- package/dist/svg-text.js +118 -0
- package/dist/tools.js +89 -451
- package/dist/types.d.ts +240 -5
- package/dist/types.js +23 -1
- package/dist/v2/action-verifier.d.ts +29 -0
- package/dist/v2/action-verifier.js +133 -0
- package/dist/v2/alt-text.d.ts +26 -0
- package/dist/v2/alt-text.js +55 -0
- package/dist/v2/benchmark.d.ts +59 -0
- package/dist/v2/benchmark.js +135 -0
- package/dist/v2/capture-strategy.d.ts +30 -0
- package/dist/v2/capture-strategy.js +67 -0
- package/dist/v2/capture-verification.d.ts +35 -0
- package/dist/v2/capture-verification.js +95 -0
- package/dist/v2/circuit-breaker.d.ts +42 -0
- package/dist/v2/circuit-breaker.js +119 -0
- package/dist/v2/cli-runner-local.d.ts +11 -0
- package/dist/v2/cli-runner-local.js +91 -0
- package/dist/v2/cli-runner.d.ts +34 -0
- package/dist/v2/cli-runner.js +300 -0
- package/dist/v2/compiler-prompts.d.ts +27 -0
- package/dist/v2/compiler-prompts.js +123 -0
- package/dist/v2/compiler.d.ts +37 -0
- package/dist/v2/compiler.js +147 -0
- package/dist/v2/explorer.d.ts +41 -0
- package/dist/v2/explorer.js +56 -0
- package/dist/v2/index.d.ts +37 -0
- package/dist/v2/index.js +31 -0
- package/dist/v2/llm-healer.d.ts +62 -0
- package/dist/v2/llm-healer.js +166 -0
- package/dist/v2/llm-provider.d.ts +29 -0
- package/dist/v2/llm-provider.js +80 -0
- package/dist/v2/opcode-runner.d.ts +47 -0
- package/dist/v2/opcode-runner.js +634 -0
- package/dist/v2/overlay-engine.d.ts +24 -0
- package/dist/v2/overlay-engine.js +150 -0
- package/dist/v2/postcondition.d.ts +16 -0
- package/dist/v2/postcondition.js +249 -0
- package/dist/v2/program-patcher.d.ts +25 -0
- package/dist/v2/program-patcher.js +44 -0
- package/dist/v2/recovery-chain.d.ts +30 -0
- package/dist/v2/recovery-chain.js +368 -0
- package/dist/v2/schema.d.ts +2580 -0
- package/dist/v2/schema.js +295 -0
- package/dist/v2/selector-resolver.d.ts +34 -0
- package/dist/v2/selector-resolver.js +181 -0
- package/dist/v2/semantic-resolver.d.ts +35 -0
- package/dist/v2/semantic-resolver.js +161 -0
- package/dist/v2/smart-wait.d.ts +27 -0
- package/dist/v2/smart-wait.js +81 -0
- package/dist/v2/types.d.ts +444 -0
- package/dist/v2/types.js +19 -0
- package/dist/v2/web-playwright-local.d.ts +69 -0
- package/dist/v2/web-playwright-local.js +392 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +5 -0
- package/dist/video-agent.js +18 -13
- package/dist/video-planner.js +2 -1
- package/dist/video-prompts.js +3 -3
- package/dist/web-playwright-local.d.ts +126 -0
- package/dist/web-playwright-local.js +819 -0
- package/dist/ws-auth.js +4 -1
- package/dist/ws-broadcast.d.ts +34 -0
- package/dist/ws-broadcast.js +85 -0
- package/dist/ws-connection-limits.d.ts +12 -0
- package/dist/ws-connection-limits.js +44 -0
- package/dist/ws-handler-utils.d.ts +32 -0
- package/dist/ws-handler-utils.js +139 -0
- package/dist/ws-handler.js +294 -164
- package/dist/ws-metrics-server.d.ts +9 -0
- package/dist/ws-metrics-server.js +31 -0
- package/dist/ws-server.js +41 -1
- package/package.json +51 -34
|
@@ -0,0 +1,770 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Capture Agent — Opcode Runner
|
|
3
|
+
*
|
|
4
|
+
* Deterministic execution engine for ExecutionProgram.
|
|
5
|
+
* Executes opcodes sequentially, verifies postconditions,
|
|
6
|
+
* delegates to recovery chain on failure, and respects circuit breaker.
|
|
7
|
+
*/
|
|
8
|
+
import { isSoftOpcodeKind } from './execution-types.js';
|
|
9
|
+
import { evaluatePostcondition } from './postcondition.js';
|
|
10
|
+
import { ActionVerifier } from './action-verifier.js';
|
|
11
|
+
import { CircuitBreaker } from './circuit-breaker.js';
|
|
12
|
+
import { smartWaitForStability } from './smart-wait.js';
|
|
13
|
+
import { verifyCaptureQuality } from './capture-verification.js';
|
|
14
|
+
import { generateAltText } from './alt-text.js';
|
|
15
|
+
import { executeOpcodeCoreAction } from './opcode-actions.js';
|
|
16
|
+
import { logger } from './logger.js';
|
|
17
|
+
function formatOpcodeDebug(opcode) {
|
|
18
|
+
const fields = [];
|
|
19
|
+
const o = opcode;
|
|
20
|
+
if (typeof o.selector === 'string')
|
|
21
|
+
fields.push(`selector="${o.selector}"`);
|
|
22
|
+
if (typeof o.url === 'string')
|
|
23
|
+
fields.push(`url="${o.url}"`);
|
|
24
|
+
if (typeof o.text === 'string')
|
|
25
|
+
fields.push(`text="${o.text.slice(0, 40)}"`);
|
|
26
|
+
if (typeof o.stateName === 'string')
|
|
27
|
+
fields.push(`stateName="${o.stateName}"`);
|
|
28
|
+
if (typeof o.groupName === 'string')
|
|
29
|
+
fields.push(`group="${o.groupName}"`);
|
|
30
|
+
if (opcode.postcondition)
|
|
31
|
+
fields.push(`post=${opcode.postcondition.type}`);
|
|
32
|
+
const recovery = opcode.recovery;
|
|
33
|
+
if (recovery) {
|
|
34
|
+
const flags = [];
|
|
35
|
+
if (recovery.retries)
|
|
36
|
+
flags.push(`retries=${recovery.retries}`);
|
|
37
|
+
if (recovery.useSelectorMemory)
|
|
38
|
+
flags.push('selMem');
|
|
39
|
+
if (recovery.useAltInteraction)
|
|
40
|
+
flags.push('alt');
|
|
41
|
+
if (recovery.allowReload)
|
|
42
|
+
flags.push('reload');
|
|
43
|
+
if (recovery.allowHealer)
|
|
44
|
+
flags.push('healer');
|
|
45
|
+
if (flags.length)
|
|
46
|
+
fields.push(`recovery=[${flags.join(',')}]`);
|
|
47
|
+
}
|
|
48
|
+
return fields.length ? ` (${fields.join(', ')})` : '';
|
|
49
|
+
}
|
|
50
|
+
/** Default recovery chain for consumers that do not inject one. */
|
|
51
|
+
export class NoOpRecoveryChain {
|
|
52
|
+
async attempt() {
|
|
53
|
+
return { recovered: false, reason: 'no recovery chain configured' };
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
// ── Main execution function ─────────────────────────────────────────
|
|
57
|
+
export async function executeProgram(program, createAdapter, options = {}) {
|
|
58
|
+
const recoveryChain = options.recoveryChain ?? new NoOpRecoveryChain();
|
|
59
|
+
const maxParallelVariants = Math.max(1, Math.floor(options.maxParallelVariants ?? 1));
|
|
60
|
+
const startTime = Date.now();
|
|
61
|
+
const variantResults = new Array(program.variants.length);
|
|
62
|
+
const healerPatches = [];
|
|
63
|
+
const telemetry = {
|
|
64
|
+
llmCallCount: 0,
|
|
65
|
+
llmCostEur: 0,
|
|
66
|
+
llmStepUsages: [],
|
|
67
|
+
totalOpcodes: 0,
|
|
68
|
+
recoveredOpcodes: 0,
|
|
69
|
+
failedOpcodes: 0,
|
|
70
|
+
skippedOpcodes: 0,
|
|
71
|
+
healerInvocations: 0,
|
|
72
|
+
circuitBreakerTrips: 0,
|
|
73
|
+
};
|
|
74
|
+
let nextVariantIndex = 0;
|
|
75
|
+
const workerCount = Math.min(maxParallelVariants, program.variants.length);
|
|
76
|
+
const workers = Array.from({ length: workerCount }, async () => {
|
|
77
|
+
while (!options.abortSignal?.aborted) {
|
|
78
|
+
const currentIndex = nextVariantIndex++;
|
|
79
|
+
if (currentIndex >= program.variants.length) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const variant = program.variants[currentIndex];
|
|
83
|
+
options.onProgress?.({
|
|
84
|
+
type: 'variant_start',
|
|
85
|
+
variantId: variant.id,
|
|
86
|
+
message: `starting variant ${variant.id}`,
|
|
87
|
+
});
|
|
88
|
+
const variantResult = await executeVariant(program, variant, createAdapter, recoveryChain, telemetry, healerPatches, options);
|
|
89
|
+
variantResults[currentIndex] = variantResult;
|
|
90
|
+
options.onProgress?.({
|
|
91
|
+
type: 'variant_end',
|
|
92
|
+
variantId: variant.id,
|
|
93
|
+
status: variantResult.success ? 'ok' : 'failed',
|
|
94
|
+
message: variantResult.success
|
|
95
|
+
? `variant ${variant.id} completed`
|
|
96
|
+
: `variant ${variant.id} failed: ${variantResult.error}`,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
await Promise.all(workers);
|
|
101
|
+
const completedVariantResults = variantResults.filter((result) => Boolean(result));
|
|
102
|
+
const aborted = options.abortSignal?.aborted && completedVariantResults.length < program.variants.length;
|
|
103
|
+
const success = !aborted && completedVariantResults.length > 0 && completedVariantResults.every(v => v.success);
|
|
104
|
+
return {
|
|
105
|
+
programId: program.presetId,
|
|
106
|
+
success,
|
|
107
|
+
variantResults: completedVariantResults,
|
|
108
|
+
telemetry,
|
|
109
|
+
healerPatches: success ? healerPatches : [], // Only propagate patches on success
|
|
110
|
+
totalDurationMs: Date.now() - startTime,
|
|
111
|
+
error: aborted ? 'aborted' : (success ? undefined : completedVariantResults.find(v => !v.success)?.error),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
// ── Variant execution ───────────────────────────────────────────────
|
|
115
|
+
async function executeVariant(program, variant, createAdapter, recoveryChain, telemetry, healerPatches, options) {
|
|
116
|
+
const startTime = Date.now();
|
|
117
|
+
const breaker = new CircuitBreaker();
|
|
118
|
+
const verifier = new ActionVerifier();
|
|
119
|
+
const opcodeResults = [];
|
|
120
|
+
const artifacts = [];
|
|
121
|
+
const executionState = {};
|
|
122
|
+
let adapter = null;
|
|
123
|
+
try {
|
|
124
|
+
adapter = await createAdapter(variant);
|
|
125
|
+
for (let i = 0; i < program.steps.length; i++) {
|
|
126
|
+
if (options.abortSignal?.aborted) {
|
|
127
|
+
return {
|
|
128
|
+
variantId: variant.id,
|
|
129
|
+
success: false,
|
|
130
|
+
opcodeResults,
|
|
131
|
+
durationMs: Date.now() - startTime,
|
|
132
|
+
artifacts,
|
|
133
|
+
error: 'aborted',
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
// Check circuit breaker before starting
|
|
137
|
+
const breakerState = breaker.isTripped();
|
|
138
|
+
if (breakerState.tripped) {
|
|
139
|
+
telemetry.circuitBreakerTrips++;
|
|
140
|
+
options.onProgress?.({
|
|
141
|
+
type: 'breaker_trip',
|
|
142
|
+
variantId: variant.id,
|
|
143
|
+
opcodeIndex: i,
|
|
144
|
+
message: breakerState.reason,
|
|
145
|
+
});
|
|
146
|
+
return {
|
|
147
|
+
variantId: variant.id,
|
|
148
|
+
success: false,
|
|
149
|
+
opcodeResults,
|
|
150
|
+
durationMs: Date.now() - startTime,
|
|
151
|
+
artifacts,
|
|
152
|
+
error: `circuit breaker tripped: ${breakerState.reason}`,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
const opcode = program.steps[i];
|
|
156
|
+
options.onProgress?.({
|
|
157
|
+
type: 'opcode_start',
|
|
158
|
+
variantId: variant.id,
|
|
159
|
+
opcodeIndex: i,
|
|
160
|
+
opcodeKind: opcode.kind,
|
|
161
|
+
message: opcode.description,
|
|
162
|
+
});
|
|
163
|
+
const result = await executeOpcode(opcode, i, adapter, verifier, breaker, recoveryChain, telemetry, healerPatches, artifacts, options, variant.id, executionState, program.artifactPlan, program.mockDataGroups, variant, program.preconditions.credentials);
|
|
164
|
+
opcodeResults.push(result);
|
|
165
|
+
telemetry.totalOpcodes++;
|
|
166
|
+
if (result.status === 'recovered')
|
|
167
|
+
telemetry.recoveredOpcodes++;
|
|
168
|
+
if (result.status === 'failed')
|
|
169
|
+
telemetry.failedOpcodes++;
|
|
170
|
+
options.onProgress?.({
|
|
171
|
+
type: 'opcode_end',
|
|
172
|
+
variantId: variant.id,
|
|
173
|
+
opcodeIndex: i,
|
|
174
|
+
opcodeKind: opcode.kind,
|
|
175
|
+
status: result.status,
|
|
176
|
+
message: result.error ?? `${opcode.kind} ${result.status}`,
|
|
177
|
+
});
|
|
178
|
+
// Abort variant on fatal opcode failure
|
|
179
|
+
if (result.status === 'failed') {
|
|
180
|
+
return {
|
|
181
|
+
variantId: variant.id,
|
|
182
|
+
success: false,
|
|
183
|
+
opcodeResults,
|
|
184
|
+
durationMs: Date.now() - startTime,
|
|
185
|
+
artifacts,
|
|
186
|
+
error: `opcode ${i} (${opcode.kind}) failed: ${result.error}`,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return {
|
|
191
|
+
variantId: variant.id,
|
|
192
|
+
success: true,
|
|
193
|
+
opcodeResults,
|
|
194
|
+
durationMs: Date.now() - startTime,
|
|
195
|
+
artifacts,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
catch (err) {
|
|
199
|
+
return {
|
|
200
|
+
variantId: variant.id,
|
|
201
|
+
success: false,
|
|
202
|
+
opcodeResults,
|
|
203
|
+
durationMs: Date.now() - startTime,
|
|
204
|
+
artifacts,
|
|
205
|
+
error: `unexpected error: ${err instanceof Error ? err.message : String(err)}`,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
finally {
|
|
209
|
+
if (adapter) {
|
|
210
|
+
try {
|
|
211
|
+
await adapter.close();
|
|
212
|
+
}
|
|
213
|
+
catch { /* ignore close errors */ }
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
// ── Single opcode execution ─────────────────────────────────────────
|
|
218
|
+
/**
|
|
219
|
+
* Mark a soft opcode as skipped without aborting the variant.
|
|
220
|
+
* Increments telemetry and (for INJECT_MOCK_DATA) records the group result.
|
|
221
|
+
* Circuit breaker is intentionally not ticked.
|
|
222
|
+
*/
|
|
223
|
+
function softSkipResult(opcode, index, startTime, reason, telemetry) {
|
|
224
|
+
telemetry.skippedOpcodes++;
|
|
225
|
+
if (opcode.kind === 'INJECT_MOCK_DATA') {
|
|
226
|
+
if (!telemetry.mockDataGroupResults)
|
|
227
|
+
telemetry.mockDataGroupResults = {};
|
|
228
|
+
telemetry.mockDataGroupResults[opcode.groupName] = 'skipped';
|
|
229
|
+
}
|
|
230
|
+
return {
|
|
231
|
+
opcodeIndex: index,
|
|
232
|
+
kind: opcode.kind,
|
|
233
|
+
status: 'skipped',
|
|
234
|
+
durationMs: Date.now() - startTime,
|
|
235
|
+
recoveryAttempts: 0,
|
|
236
|
+
error: reason,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
async function executeOpcode(opcode, index, adapter, verifier, breaker, recoveryChain, telemetry, healerPatches, artifacts, options, variantId, executionState, artifactPlan, mockDataGroups, currentVariant, credentials) {
|
|
240
|
+
const startTime = Date.now();
|
|
241
|
+
const deadlineMs = startTime + opcode.timeoutMs;
|
|
242
|
+
const isInteraction = ['CLICK', 'TYPE', 'PRESS_KEY', 'SCROLL'].includes(opcode.kind);
|
|
243
|
+
const isSoft = isSoftOpcodeKind(opcode.kind);
|
|
244
|
+
// Track page context for circuit breaker
|
|
245
|
+
try {
|
|
246
|
+
const url = await adapter.getCurrentUrl();
|
|
247
|
+
breaker.setPage(url);
|
|
248
|
+
}
|
|
249
|
+
catch { /* ignore */ }
|
|
250
|
+
// Execute with timeout
|
|
251
|
+
try {
|
|
252
|
+
logger.debug(`[opcode ${index}] ${opcode.kind} start — budget ${opcode.timeoutMs}ms${formatOpcodeDebug(opcode)}`);
|
|
253
|
+
if (isInteraction) {
|
|
254
|
+
const beforeStart = Date.now();
|
|
255
|
+
await verifier.captureBeforeState(adapter);
|
|
256
|
+
logger.debug(`[opcode ${index}] captureBeforeState took ${Date.now() - beforeStart}ms`);
|
|
257
|
+
}
|
|
258
|
+
const actionBudgetMs = getRemainingTimeMs(deadlineMs);
|
|
259
|
+
if (actionBudgetMs <= 0) {
|
|
260
|
+
const reason = `timeout after ${opcode.timeoutMs}ms`;
|
|
261
|
+
logger.debug(`[opcode ${index}] no budget left after captureBeforeState (deadline=${deadlineMs}, now=${Date.now()})`);
|
|
262
|
+
if (isSoft)
|
|
263
|
+
return softSkipResult(opcode, index, startTime, reason, telemetry);
|
|
264
|
+
return handleFailure(opcode, index, adapter, verifier, isInteraction, breaker, recoveryChain, telemetry, healerPatches, options, variantId, currentVariant, startTime, deadlineMs, reason);
|
|
265
|
+
}
|
|
266
|
+
logger.debug(`[opcode ${index}] action exec start — actionBudget ${actionBudgetMs}ms`);
|
|
267
|
+
const actionStart = Date.now();
|
|
268
|
+
const result = await withTimeout(() => executeOpcodeAction(opcode, index, adapter, artifacts, telemetry, currentVariant, executionState, artifactPlan, mockDataGroups, options, credentials), actionBudgetMs);
|
|
269
|
+
logger.debug(`[opcode ${index}] action exec end — took ${Date.now() - actionStart}ms, success=${result.success}${result.error ? `, error=${result.error}` : ''}`);
|
|
270
|
+
if (!result.success) {
|
|
271
|
+
const reason = result.error ?? 'action failed';
|
|
272
|
+
if (isSoft)
|
|
273
|
+
return softSkipResult(opcode, index, startTime, reason, telemetry);
|
|
274
|
+
return handleFailure(opcode, index, adapter, verifier, isInteraction, breaker, recoveryChain, telemetry, healerPatches, options, variantId, currentVariant, startTime, deadlineMs, reason);
|
|
275
|
+
}
|
|
276
|
+
// Verify postcondition
|
|
277
|
+
const postconditionBudgetMs = getRemainingTimeMs(deadlineMs);
|
|
278
|
+
if (postconditionBudgetMs <= 0) {
|
|
279
|
+
const reason = `timeout after ${opcode.timeoutMs}ms`;
|
|
280
|
+
logger.debug(`[opcode ${index}] no budget left for postcondition check`);
|
|
281
|
+
if (isSoft)
|
|
282
|
+
return softSkipResult(opcode, index, startTime, reason, telemetry);
|
|
283
|
+
return handleFailure(opcode, index, adapter, verifier, isInteraction, breaker, recoveryChain, telemetry, healerPatches, options, variantId, currentVariant, startTime, deadlineMs, reason);
|
|
284
|
+
}
|
|
285
|
+
const postStart = Date.now();
|
|
286
|
+
const postcondition = await evaluatePostcondition(adapter, withClampedPostconditionTimeout(opcode.postcondition, postconditionBudgetMs));
|
|
287
|
+
logger.debug(`[opcode ${index}] postcondition (${opcode.postcondition.type}) took ${Date.now() - postStart}ms — passed=${postcondition.passed}, reason="${postcondition.reason}"`);
|
|
288
|
+
if (!postcondition.passed) {
|
|
289
|
+
const reason = `postcondition failed: ${postcondition.reason}`;
|
|
290
|
+
if (isSoft)
|
|
291
|
+
return softSkipResult(opcode, index, startTime, reason, telemetry);
|
|
292
|
+
return handleFailure(opcode, index, adapter, verifier, isInteraction, breaker, recoveryChain, telemetry, healerPatches, options, variantId, currentVariant, startTime, deadlineMs, reason);
|
|
293
|
+
}
|
|
294
|
+
// Verify action had effect (for interaction opcodes)
|
|
295
|
+
if (isInteraction) {
|
|
296
|
+
const verification = await verifier.verifyAfterAction(adapter);
|
|
297
|
+
if (!verification.hadEffect && opcode.postcondition.type !== 'always' && opcode.postcondition.type !== 'any_change') {
|
|
298
|
+
return handleFailure(opcode, index, adapter, verifier, isInteraction, breaker, recoveryChain, telemetry, healerPatches, options, variantId, currentVariant, startTime, deadlineMs, `action had no effect: ${verification.summary}`);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
// Record successful mock data application
|
|
302
|
+
if (opcode.kind === 'INJECT_MOCK_DATA') {
|
|
303
|
+
if (!telemetry.mockDataGroupResults)
|
|
304
|
+
telemetry.mockDataGroupResults = {};
|
|
305
|
+
telemetry.mockDataGroupResults[opcode.groupName] = 'applied';
|
|
306
|
+
}
|
|
307
|
+
breaker.recordSuccess(index);
|
|
308
|
+
return {
|
|
309
|
+
opcodeIndex: index,
|
|
310
|
+
kind: opcode.kind,
|
|
311
|
+
status: 'ok',
|
|
312
|
+
durationMs: Date.now() - startTime,
|
|
313
|
+
recoveryAttempts: 0,
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
catch (err) {
|
|
317
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
318
|
+
if (isSoft)
|
|
319
|
+
return softSkipResult(opcode, index, startTime, errorMsg, telemetry);
|
|
320
|
+
return handleFailure(opcode, index, adapter, verifier, isInteraction, breaker, recoveryChain, telemetry, healerPatches, options, variantId, currentVariant, startTime, deadlineMs, errorMsg);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
// ── Failure handling with recovery ──────────────────────────────────
|
|
324
|
+
async function handleFailure(opcode, index, adapter, verifier, isInteraction, breaker, recoveryChain, telemetry, healerPatches, options, variantId, currentVariant, startTime, deadlineMs, errorMsg) {
|
|
325
|
+
const breakerState = breaker.recordFailure(index, opcode.maxFailures);
|
|
326
|
+
if (breakerState.tripped) {
|
|
327
|
+
telemetry.circuitBreakerTrips++;
|
|
328
|
+
return {
|
|
329
|
+
opcodeIndex: index,
|
|
330
|
+
kind: opcode.kind,
|
|
331
|
+
status: 'failed',
|
|
332
|
+
durationMs: Date.now() - startTime,
|
|
333
|
+
recoveryAttempts: 0,
|
|
334
|
+
error: `${errorMsg} (circuit breaker: ${breakerState.reason})`,
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
const remainingTimeMs = getRemainingTimeMs(deadlineMs);
|
|
338
|
+
if (remainingTimeMs <= 0) {
|
|
339
|
+
return {
|
|
340
|
+
opcodeIndex: index,
|
|
341
|
+
kind: opcode.kind,
|
|
342
|
+
status: 'failed',
|
|
343
|
+
durationMs: Date.now() - startTime,
|
|
344
|
+
recoveryAttempts: 0,
|
|
345
|
+
error: `${errorMsg} (timeout after ${opcode.timeoutMs}ms)`,
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
// Attempt recovery
|
|
349
|
+
options.onProgress?.({
|
|
350
|
+
type: 'recovery',
|
|
351
|
+
variantId,
|
|
352
|
+
opcodeIndex: index,
|
|
353
|
+
opcodeKind: opcode.kind,
|
|
354
|
+
message: `recovering from: ${errorMsg}`,
|
|
355
|
+
});
|
|
356
|
+
const recovery = await recoveryChain.attempt(opcode, index, adapter, {
|
|
357
|
+
remainingTimeMs,
|
|
358
|
+
maxDeterministicRetries: Math.max(0, opcode.maxFailures - breakerState.opcodeFailures),
|
|
359
|
+
currentVariant,
|
|
360
|
+
});
|
|
361
|
+
if (recovery.llmResult) {
|
|
362
|
+
telemetry.llmCallCount++;
|
|
363
|
+
telemetry.llmCostEur += recovery.llmResult.costEur;
|
|
364
|
+
telemetry.llmStepUsages.push({
|
|
365
|
+
stepType: 'healer_invocation',
|
|
366
|
+
generationId: recovery.llmResult.generationId,
|
|
367
|
+
model: recovery.llmResult.model,
|
|
368
|
+
promptTokens: recovery.llmResult.promptTokens,
|
|
369
|
+
completionTokens: recovery.llmResult.completionTokens,
|
|
370
|
+
});
|
|
371
|
+
telemetry.healerInvocations++;
|
|
372
|
+
}
|
|
373
|
+
if (recovery.recovered) {
|
|
374
|
+
if (recovery.patch) {
|
|
375
|
+
healerPatches.push(recovery.patch);
|
|
376
|
+
}
|
|
377
|
+
if (isInteraction) {
|
|
378
|
+
const verification = await verifier.verifyAfterAction(adapter);
|
|
379
|
+
if (!verification.hadEffect && opcode.postcondition.type !== 'always' && opcode.postcondition.type !== 'any_change') {
|
|
380
|
+
return {
|
|
381
|
+
opcodeIndex: index,
|
|
382
|
+
kind: opcode.kind,
|
|
383
|
+
status: 'failed',
|
|
384
|
+
durationMs: Date.now() - startTime,
|
|
385
|
+
recoveryAttempts: 1,
|
|
386
|
+
error: `${errorMsg} (recovery succeeded but action still had no effect: ${verification.summary})`,
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
breaker.recordSuccess(index);
|
|
391
|
+
return {
|
|
392
|
+
opcodeIndex: index,
|
|
393
|
+
kind: opcode.kind,
|
|
394
|
+
status: 'recovered',
|
|
395
|
+
durationMs: Date.now() - startTime,
|
|
396
|
+
recoveryAttempts: 1,
|
|
397
|
+
recoveryStrategy: recovery.strategy,
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
return {
|
|
401
|
+
opcodeIndex: index,
|
|
402
|
+
kind: opcode.kind,
|
|
403
|
+
status: 'failed',
|
|
404
|
+
durationMs: Date.now() - startTime,
|
|
405
|
+
recoveryAttempts: 1,
|
|
406
|
+
error: `${errorMsg} (recovery failed: ${recovery.reason})`,
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
// ── Opcode action dispatch ──────────────────────────────────────────
|
|
410
|
+
async function executeOpcodeAction(opcode, opcodeIndex, adapter, artifacts, telemetry, currentVariant, executionState, artifactPlan, mockDataGroups, runOptions, credentials) {
|
|
411
|
+
try {
|
|
412
|
+
void artifactPlan;
|
|
413
|
+
switch (opcode.kind) {
|
|
414
|
+
case 'NAVIGATE':
|
|
415
|
+
case 'DISMISS_OVERLAYS':
|
|
416
|
+
case 'CLICK':
|
|
417
|
+
case 'TYPE':
|
|
418
|
+
case 'PRESS_KEY':
|
|
419
|
+
case 'WAIT_FOR':
|
|
420
|
+
case 'SET_LOCALE':
|
|
421
|
+
case 'SET_THEME':
|
|
422
|
+
case 'SCROLL':
|
|
423
|
+
case 'HOVER':
|
|
424
|
+
case 'SELECT_OPTION':
|
|
425
|
+
case 'CHECK':
|
|
426
|
+
case 'DOUBLE_CLICK':
|
|
427
|
+
case 'CLONE_ELEMENT':
|
|
428
|
+
case 'INJECT_MOCK_DATA':
|
|
429
|
+
case 'REMOVE_ELEMENT':
|
|
430
|
+
case 'SET_ATTRIBUTE':
|
|
431
|
+
return executeOpcodeCoreAction(opcode, adapter, { currentVariant, mockDataGroups, credentials });
|
|
432
|
+
case 'ASSERT_ROUTE':
|
|
433
|
+
return evaluateImmediateAssertion(await evaluatePostcondition(adapter, {
|
|
434
|
+
type: 'route_matches',
|
|
435
|
+
pattern: opcode.urlPattern,
|
|
436
|
+
waitMs: 1,
|
|
437
|
+
}), 'ASSERT_ROUTE failed');
|
|
438
|
+
case 'ASSERT_SURFACE':
|
|
439
|
+
return evaluateSurfaceAssertion(adapter, opcode.selectors, opcode.matchAll);
|
|
440
|
+
case 'CAPTURE_SCREENSHOT': {
|
|
441
|
+
await smartWaitForStability(adapter, { maxWaitMs: 5000 });
|
|
442
|
+
const captureUrl = await adapter.getCurrentUrl();
|
|
443
|
+
const takeBuffer = async () => {
|
|
444
|
+
if (opcode.elementSelector && adapter.takeElementScreenshot) {
|
|
445
|
+
return adapter.takeElementScreenshot(opcode.elementSelector);
|
|
446
|
+
}
|
|
447
|
+
if (opcode.elementSelector) {
|
|
448
|
+
throw new Error(`element capture requires adapter support for selector "${opcode.elementSelector}"`);
|
|
449
|
+
}
|
|
450
|
+
return adapter.takeScreenshot();
|
|
451
|
+
};
|
|
452
|
+
let buffer = await takeBuffer();
|
|
453
|
+
if (runOptions?.llmConfig) {
|
|
454
|
+
const verification = await verifyCaptureQuality(buffer, {
|
|
455
|
+
expectedDescription: opcode.description,
|
|
456
|
+
url: captureUrl,
|
|
457
|
+
locale: currentVariant?.locale,
|
|
458
|
+
theme: currentVariant?.theme,
|
|
459
|
+
}, runOptions.llmConfig);
|
|
460
|
+
if (verification.llmResult) {
|
|
461
|
+
telemetry.llmCallCount++;
|
|
462
|
+
telemetry.llmCostEur += verification.llmResult.costEur;
|
|
463
|
+
telemetry.llmStepUsages.push({
|
|
464
|
+
stepType: 'capture_verification',
|
|
465
|
+
generationId: verification.llmResult.generationId,
|
|
466
|
+
model: verification.llmResult.model,
|
|
467
|
+
promptTokens: verification.llmResult.promptTokens,
|
|
468
|
+
completionTokens: verification.llmResult.completionTokens,
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
if (!verification.passed) {
|
|
472
|
+
await smartWaitForStability(adapter, { maxWaitMs: 8000 });
|
|
473
|
+
const retryBuffer = await takeBuffer();
|
|
474
|
+
const retryVerification = await verifyCaptureQuality(retryBuffer, {
|
|
475
|
+
expectedDescription: opcode.description,
|
|
476
|
+
url: captureUrl,
|
|
477
|
+
locale: currentVariant?.locale,
|
|
478
|
+
theme: currentVariant?.theme,
|
|
479
|
+
}, runOptions.llmConfig);
|
|
480
|
+
if (retryVerification.llmResult) {
|
|
481
|
+
telemetry.llmCallCount++;
|
|
482
|
+
telemetry.llmCostEur += retryVerification.llmResult.costEur;
|
|
483
|
+
telemetry.llmStepUsages.push({
|
|
484
|
+
stepType: 'capture_verification',
|
|
485
|
+
generationId: retryVerification.llmResult.generationId,
|
|
486
|
+
model: retryVerification.llmResult.model,
|
|
487
|
+
promptTokens: retryVerification.llmResult.promptTokens,
|
|
488
|
+
completionTokens: retryVerification.llmResult.completionTokens,
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
if (retryVerification.passed) {
|
|
492
|
+
buffer = retryBuffer;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
let altText;
|
|
497
|
+
if (runOptions?.llmConfig) {
|
|
498
|
+
try {
|
|
499
|
+
const altResult = await generateAltText(buffer, {
|
|
500
|
+
description: opcode.description,
|
|
501
|
+
url: captureUrl,
|
|
502
|
+
locale: currentVariant?.locale,
|
|
503
|
+
presetName: runOptions.presetName,
|
|
504
|
+
}, runOptions.llmConfig);
|
|
505
|
+
altText = altResult.altText;
|
|
506
|
+
if (altResult.llmResult) {
|
|
507
|
+
telemetry.llmCallCount++;
|
|
508
|
+
telemetry.llmCostEur += altResult.llmResult.costEur;
|
|
509
|
+
telemetry.llmStepUsages.push({
|
|
510
|
+
stepType: 'alt_text_generation',
|
|
511
|
+
generationId: altResult.llmResult.generationId,
|
|
512
|
+
model: altResult.llmResult.model,
|
|
513
|
+
promptTokens: altResult.llmResult.promptTokens,
|
|
514
|
+
completionTokens: altResult.llmResult.completionTokens,
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
catch {
|
|
519
|
+
// Alt text generation failed — non-fatal
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
// Extract page favicon for browser bar mockup
|
|
523
|
+
let tabIconData;
|
|
524
|
+
let tabIconMimeType;
|
|
525
|
+
if (adapter.extractFavicon) {
|
|
526
|
+
const favicon = await adapter.extractFavicon();
|
|
527
|
+
if (favicon) {
|
|
528
|
+
tabIconData = favicon.buffer;
|
|
529
|
+
tabIconMimeType = favicon.mimeType;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
artifacts.push({
|
|
533
|
+
mediaMode: 'screenshot',
|
|
534
|
+
buffer,
|
|
535
|
+
mimeType: 'image/png',
|
|
536
|
+
captureType: opcode.elementSelector ? 'element' : 'fullpage',
|
|
537
|
+
captureUrl,
|
|
538
|
+
dimensions: currentVariant?.viewport,
|
|
539
|
+
captureId: opcode.captureId,
|
|
540
|
+
captureName: opcode.captureName ?? opcode.description,
|
|
541
|
+
elementSelector: opcode.elementSelector,
|
|
542
|
+
altText,
|
|
543
|
+
stepDescription: opcode.description,
|
|
544
|
+
stepIndex: opcodeIndex,
|
|
545
|
+
variantId: currentVariant?.id,
|
|
546
|
+
tabIconData,
|
|
547
|
+
tabIconMimeType,
|
|
548
|
+
});
|
|
549
|
+
break;
|
|
550
|
+
}
|
|
551
|
+
case 'BEGIN_CLIP':
|
|
552
|
+
if (executionState.activeClip) {
|
|
553
|
+
return { success: false, error: 'cannot start a new clip before the previous one ends' };
|
|
554
|
+
}
|
|
555
|
+
executionState.activeClip = {
|
|
556
|
+
clipId: opcode.clipId,
|
|
557
|
+
clipName: opcode.clipName,
|
|
558
|
+
};
|
|
559
|
+
await adapter.beginRecording({ mediaMode: 'clip' });
|
|
560
|
+
break;
|
|
561
|
+
case 'END_CLIP': {
|
|
562
|
+
const clipIdentity = resolveClipIdentity(executionState.activeClip, opcode);
|
|
563
|
+
const recording = await adapter.endRecording();
|
|
564
|
+
executionState.activeClip = undefined;
|
|
565
|
+
artifacts.push({
|
|
566
|
+
mediaMode: 'clip',
|
|
567
|
+
buffer: recording.buffer,
|
|
568
|
+
mimeType: recording.mimeType,
|
|
569
|
+
durationMs: recording.durationMs,
|
|
570
|
+
trimStartMs: recording.trimStartMs,
|
|
571
|
+
dimensions: undefined,
|
|
572
|
+
captureType: 'fullpage',
|
|
573
|
+
captureUrl: await adapter.getCurrentUrl(),
|
|
574
|
+
clipId: clipIdentity.clipId,
|
|
575
|
+
clipName: clipIdentity.clipName,
|
|
576
|
+
stepDescription: opcode.description,
|
|
577
|
+
stepIndex: opcodeIndex,
|
|
578
|
+
variantId: currentVariant?.id,
|
|
579
|
+
});
|
|
580
|
+
break;
|
|
581
|
+
}
|
|
582
|
+
case 'CAPTURE_DOM': {
|
|
583
|
+
if (opcode.selector) {
|
|
584
|
+
const attached = await adapter.waitFor({
|
|
585
|
+
selector: opcode.selector,
|
|
586
|
+
state: 'attached',
|
|
587
|
+
timeoutMs: 5000,
|
|
588
|
+
});
|
|
589
|
+
if (!attached) {
|
|
590
|
+
return {
|
|
591
|
+
success: false,
|
|
592
|
+
error: `CAPTURE_DOM could not find selector "${opcode.selector}" in the DOM`,
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
await smartWaitForStability(adapter, { maxWaitMs: 5000 });
|
|
597
|
+
if (!adapter.serializeDom) {
|
|
598
|
+
return {
|
|
599
|
+
success: false,
|
|
600
|
+
error: 'CAPTURE_DOM requires an adapter that implements serializeDom() — Interactive Demos pipeline (AUT-121)',
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
const captureUrl = await adapter.getCurrentUrl();
|
|
604
|
+
const serialized = await adapter.serializeDom(opcode.selector);
|
|
605
|
+
const buffer = Buffer.from(serialized.html, 'utf8');
|
|
606
|
+
// Take a viewport screenshot for the thumbnail
|
|
607
|
+
const thumbnailBuffer = await adapter.takeScreenshot();
|
|
608
|
+
artifacts.push({
|
|
609
|
+
mediaMode: 'dom',
|
|
610
|
+
buffer,
|
|
611
|
+
mimeType: 'text/html; charset=utf-8',
|
|
612
|
+
captureType: opcode.selector ? 'element' : 'fullpage',
|
|
613
|
+
captureUrl,
|
|
614
|
+
dimensions: serialized.viewport,
|
|
615
|
+
captureId: opcode.stateName,
|
|
616
|
+
captureName: opcode.stateName,
|
|
617
|
+
stepDescription: opcode.description,
|
|
618
|
+
stepIndex: opcodeIndex,
|
|
619
|
+
variantId: currentVariant?.id,
|
|
620
|
+
elementSelector: opcode.selector,
|
|
621
|
+
stateName: opcode.stateName,
|
|
622
|
+
domHtml: serialized.html,
|
|
623
|
+
domAssetUrls: serialized.assetUrls,
|
|
624
|
+
domHtmlBytes: buffer.byteLength,
|
|
625
|
+
domThumbnailBuffer: thumbnailBuffer,
|
|
626
|
+
});
|
|
627
|
+
break;
|
|
628
|
+
}
|
|
629
|
+
case 'CAPTURE_FRAGMENT': {
|
|
630
|
+
if (!adapter.serializeFragment) {
|
|
631
|
+
return {
|
|
632
|
+
success: false,
|
|
633
|
+
error: 'CAPTURE_FRAGMENT requires an adapter that implements serializeFragment() — Interactive Demos Phase 5',
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
// Optional hidden trigger button (mirrors the mock data trigger
|
|
637
|
+
// pattern). Useful for fragments that aren't reachable via
|
|
638
|
+
// visible UI. The trigger is clicked via clickHidden() so it
|
|
639
|
+
// bypasses visibility/disabled checks.
|
|
640
|
+
if (opcode.triggerSelector) {
|
|
641
|
+
if (!adapter.clickHidden) {
|
|
642
|
+
return {
|
|
643
|
+
success: false,
|
|
644
|
+
error: `CAPTURE_FRAGMENT.triggerSelector requires adapter.clickHidden() support`,
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
try {
|
|
648
|
+
await adapter.clickHidden({ selector: opcode.triggerSelector });
|
|
649
|
+
}
|
|
650
|
+
catch (err) {
|
|
651
|
+
return {
|
|
652
|
+
success: false,
|
|
653
|
+
error: `CAPTURE_FRAGMENT trigger click failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
// Wait for the fragment selector to be present in the DOM (the
|
|
658
|
+
// trigger may have inserted it dynamically). Mirror the screenshot
|
|
659
|
+
// smart-wait so animations / portal mounts settle before serialization.
|
|
660
|
+
const visible = await adapter.waitFor({
|
|
661
|
+
selector: opcode.selector,
|
|
662
|
+
state: 'attached',
|
|
663
|
+
timeoutMs: 5000,
|
|
664
|
+
});
|
|
665
|
+
if (!visible) {
|
|
666
|
+
return {
|
|
667
|
+
success: false,
|
|
668
|
+
error: `CAPTURE_FRAGMENT could not find selector "${opcode.selector}" in the DOM`,
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
await smartWaitForStability(adapter, { maxWaitMs: 3000 });
|
|
672
|
+
const captureUrl = await adapter.getCurrentUrl();
|
|
673
|
+
const serialized = await adapter.serializeFragment(opcode.selector);
|
|
674
|
+
const buffer = Buffer.from(serialized.html, 'utf8');
|
|
675
|
+
const variantName = opcode.variantName ?? 'default';
|
|
676
|
+
artifacts.push({
|
|
677
|
+
mediaMode: 'dom',
|
|
678
|
+
buffer,
|
|
679
|
+
mimeType: 'text/html; charset=utf-8',
|
|
680
|
+
captureType: 'element',
|
|
681
|
+
captureUrl,
|
|
682
|
+
// captureId is the unique upload key — including the variant
|
|
683
|
+
// ensures multiple variants of the same fragment do not collide.
|
|
684
|
+
captureId: `${opcode.parentState}/${opcode.fragmentName}/${variantName}`,
|
|
685
|
+
captureName: opcode.fragmentName,
|
|
686
|
+
elementSelector: opcode.selector,
|
|
687
|
+
stepDescription: opcode.description,
|
|
688
|
+
stepIndex: opcodeIndex,
|
|
689
|
+
variantId: currentVariant?.id,
|
|
690
|
+
fragmentName: opcode.fragmentName,
|
|
691
|
+
fragmentVariantName: variantName,
|
|
692
|
+
parentStateName: opcode.parentState,
|
|
693
|
+
mountStrategy: opcode.mountStrategy ?? 'inline',
|
|
694
|
+
mountTargetSelector: opcode.mountTargetSelector,
|
|
695
|
+
domHtml: serialized.html,
|
|
696
|
+
domAssetUrls: serialized.assetUrls,
|
|
697
|
+
domHtmlBytes: buffer.byteLength,
|
|
698
|
+
});
|
|
699
|
+
break;
|
|
700
|
+
}
|
|
701
|
+
default: {
|
|
702
|
+
const _exhaustive = opcode;
|
|
703
|
+
return { success: false, error: `unknown opcode kind: ${opcode.kind}` };
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
return { success: true };
|
|
707
|
+
}
|
|
708
|
+
catch (err) {
|
|
709
|
+
return {
|
|
710
|
+
success: false,
|
|
711
|
+
error: err instanceof Error ? err.message : String(err),
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
// ── Helpers ─────────────────────────────────────────────────────────
|
|
716
|
+
async function withTimeout(fn, timeoutMs) {
|
|
717
|
+
return new Promise((resolve, reject) => {
|
|
718
|
+
const timer = setTimeout(() => reject(new Error(`timeout after ${timeoutMs}ms`)), timeoutMs);
|
|
719
|
+
fn()
|
|
720
|
+
.then(result => { clearTimeout(timer); resolve(result); })
|
|
721
|
+
.catch(err => { clearTimeout(timer); reject(err); });
|
|
722
|
+
});
|
|
723
|
+
}
|
|
724
|
+
function getRemainingTimeMs(deadlineMs) {
|
|
725
|
+
return Math.max(0, deadlineMs - Date.now());
|
|
726
|
+
}
|
|
727
|
+
function resolveClipIdentity(activeClip, opcode) {
|
|
728
|
+
if (activeClip?.clipId && opcode.clipId && activeClip.clipId !== opcode.clipId) {
|
|
729
|
+
throw new Error(`END_CLIP clipId "${opcode.clipId}" does not match active clip "${activeClip.clipId}"`);
|
|
730
|
+
}
|
|
731
|
+
return {
|
|
732
|
+
clipId: opcode.clipId ?? activeClip?.clipId,
|
|
733
|
+
clipName: opcode.clipName ?? activeClip?.clipName ?? opcode.description,
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
function withClampedPostconditionTimeout(spec, maxWaitMs) {
|
|
737
|
+
return {
|
|
738
|
+
...spec,
|
|
739
|
+
waitMs: Math.max(1, Math.min(spec.waitMs ?? maxWaitMs, maxWaitMs)),
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
function evaluateImmediateAssertion(result, prefix) {
|
|
743
|
+
return result.passed
|
|
744
|
+
? { success: true }
|
|
745
|
+
: { success: false, error: `${prefix}: ${result.reason}` };
|
|
746
|
+
}
|
|
747
|
+
async function evaluateSurfaceAssertion(adapter, selectors, matchAll) {
|
|
748
|
+
const checks = await Promise.all(selectors.map(async (selector) => ({
|
|
749
|
+
selector,
|
|
750
|
+
result: await evaluatePostcondition(adapter, {
|
|
751
|
+
type: 'element_visible',
|
|
752
|
+
selector,
|
|
753
|
+
waitMs: 1,
|
|
754
|
+
}),
|
|
755
|
+
})));
|
|
756
|
+
if (matchAll) {
|
|
757
|
+
const failed = checks.find((entry) => !entry.result.passed);
|
|
758
|
+
return failed
|
|
759
|
+
? { success: false, error: `ASSERT_SURFACE failed for "${failed.selector}": ${failed.result.reason}` }
|
|
760
|
+
: { success: true };
|
|
761
|
+
}
|
|
762
|
+
if (checks.some((entry) => entry.result.passed)) {
|
|
763
|
+
return { success: true };
|
|
764
|
+
}
|
|
765
|
+
return {
|
|
766
|
+
success: false,
|
|
767
|
+
error: `ASSERT_SURFACE failed: none of the selectors matched (${selectors.join(', ')})`,
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
//# sourceMappingURL=opcode-runner.js.map
|