mobile-debug-mcp 0.26.5 → 0.28.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/dist/interact/index.js +586 -192
- package/dist/server/common.js +172 -2
- package/dist/server-core.js +1 -1
- package/docs/CHANGELOG.md +6 -0
- package/docs/ROADMAP.md +109 -11
- package/docs/rfcs/010-verification-stabilization-and-temporal-convergence.md +265 -0
- package/docs/rfcs/011-recovery-and-replanning-for-failed-or-ambiguous-interaction-flows.md +321 -0
- package/docs/rfcs/011.1-recovery-contract-types-and-runtime-wiring-spec.md +253 -0
- package/docs/rfcs/012-action-trace-and-xecution-observability.md +242 -0
- package/docs/specs/mcp-tooling-spec-v1.md +26 -0
- package/docs/tools/interact.md +54 -0
- package/package.json +1 -1
- package/src/interact/index.ts +657 -194
- package/src/server/common.ts +236 -3
- package/src/server-core.ts +1 -1
- package/src/types.ts +59 -0
- package/test/device/manual/observe/rfc012_trace.manual.ts +51 -0
- package/test/unit/interact/adjust_control.test.ts +77 -1
- package/test/unit/interact/expect_tools.test.ts +57 -25
- package/test/unit/interact/verification_stabilization.test.ts +94 -0
- package/test/unit/server/common.test.ts +60 -1
package/dist/interact/index.js
CHANGED
|
@@ -5,7 +5,26 @@ export { AndroidInteract, iOSInteract };
|
|
|
5
5
|
import { resolveTargetDevice } from '../utils/resolve-device.js';
|
|
6
6
|
import { ToolsObserve } from '../observe/index.js';
|
|
7
7
|
import { computeSnapshotSignature } from '../observe/snapshot-metadata.js';
|
|
8
|
-
import { buildActionExecutionResult } from '../server/common.js';
|
|
8
|
+
import { buildActionExecutionResult, createTraceStep, nextActionId } from '../server/common.js';
|
|
9
|
+
function buildObservationTrace({ actionType, stage, success, attempts, metadata }) {
|
|
10
|
+
const now = Date.now();
|
|
11
|
+
const actionId = nextActionId(actionType, now);
|
|
12
|
+
const steps = [
|
|
13
|
+
createTraceStep({
|
|
14
|
+
stage,
|
|
15
|
+
timestamp: now,
|
|
16
|
+
result: success ? 'success' : 'failure',
|
|
17
|
+
attemptIndex: 0,
|
|
18
|
+
metadata
|
|
19
|
+
})
|
|
20
|
+
];
|
|
21
|
+
return {
|
|
22
|
+
action_id: actionId,
|
|
23
|
+
steps,
|
|
24
|
+
final_outcome: success ? 'success' : 'failure',
|
|
25
|
+
attempts: Math.max(1, Math.floor(attempts || 1))
|
|
26
|
+
};
|
|
27
|
+
}
|
|
9
28
|
export class ToolsInteract {
|
|
10
29
|
static _maxResolvedUiElements = 256;
|
|
11
30
|
static _uiChangeKinds = ['hierarchy_diff', 'text_change', 'state_change'];
|
|
@@ -291,6 +310,38 @@ export class ToolsInteract {
|
|
|
291
310
|
const safeRatio = Math.min(1 - (endpointMargin * 0.25), Math.max(endpointMargin, targetRatio + centerBias + directionBias + edgeBias));
|
|
292
311
|
return ToolsInteract._buildControlPoint(bounds, safeRatio, axis);
|
|
293
312
|
}
|
|
313
|
+
static _buildAdjustmentProbePoints(bounds, targetValue, currentValue, min, max, axis) {
|
|
314
|
+
const targetPoint = ToolsInteract._buildConservativeControlPoint(bounds, targetValue, currentValue, min, max, axis);
|
|
315
|
+
const currentPoint = currentValue !== null
|
|
316
|
+
? ToolsInteract._buildControlPoint(bounds, (currentValue - min) / (max - min), axis)
|
|
317
|
+
: ToolsInteract._buildControlPoint(bounds, 0.5, axis);
|
|
318
|
+
const [left, top, right, bottom] = bounds;
|
|
319
|
+
const width = Math.max(1, right - left);
|
|
320
|
+
const height = Math.max(1, bottom - top);
|
|
321
|
+
const crossAxisBumps = axis === 'horizontal'
|
|
322
|
+
? [Math.max(24, Math.floor(height * 0.75)), Math.max(40, Math.floor(height * 1.5))]
|
|
323
|
+
: [Math.max(24, Math.floor(width * 0.75)), Math.max(40, Math.floor(width * 1.5))];
|
|
324
|
+
const clampPoint = (point) => ({
|
|
325
|
+
x: axis === 'horizontal'
|
|
326
|
+
? Math.max(left, Math.min(right, point.x))
|
|
327
|
+
: Math.max(left, Math.min(right + Math.max(width, height), point.x)),
|
|
328
|
+
y: axis === 'vertical'
|
|
329
|
+
? Math.max(top, Math.min(bottom, point.y))
|
|
330
|
+
: Math.max(top, Math.min(bottom + Math.max(height, width), point.y))
|
|
331
|
+
});
|
|
332
|
+
const probes = [targetPoint, currentPoint];
|
|
333
|
+
for (const bump of crossAxisBumps) {
|
|
334
|
+
if (axis === 'horizontal') {
|
|
335
|
+
probes.push({ x: targetPoint.x, y: bottom + bump }, { x: currentPoint.x, y: bottom + bump });
|
|
336
|
+
}
|
|
337
|
+
else {
|
|
338
|
+
probes.push({ x: right + bump, y: targetPoint.y }, { x: right + bump, y: currentPoint.y });
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return Array.from(new Map(probes
|
|
342
|
+
.map(clampPoint)
|
|
343
|
+
.map((point) => [`${point.x}:${point.y}`, point])).values());
|
|
344
|
+
}
|
|
294
345
|
static _controlAxis(el, bounds) {
|
|
295
346
|
const type = ToolsInteract._normalize(el.type ?? el.class ?? '');
|
|
296
347
|
const role = ToolsInteract._normalize(el.role ?? '');
|
|
@@ -501,7 +552,33 @@ export class ToolsInteract {
|
|
|
501
552
|
let resolvedDeviceId = deviceId;
|
|
502
553
|
const fingerprintBefore = await ToolsInteract._captureFingerprint(resolvedPlatform, resolvedDeviceId);
|
|
503
554
|
let semanticFallbackElement = null;
|
|
555
|
+
const traceSteps = [];
|
|
556
|
+
let traceAttemptIndex = 0;
|
|
557
|
+
const recordTraceStep = (stage, result, metadata) => {
|
|
558
|
+
traceSteps.push(createTraceStep({
|
|
559
|
+
stage,
|
|
560
|
+
timestamp: Date.now(),
|
|
561
|
+
result,
|
|
562
|
+
attemptIndex: traceAttemptIndex++,
|
|
563
|
+
metadata
|
|
564
|
+
}));
|
|
565
|
+
};
|
|
504
566
|
const buildFailure = (failureCode, reason, resolved, device, actualState, attempts, adjustmentMode = 'gesture', retryable = false, uiFingerprintAfter = null) => {
|
|
567
|
+
if (!traceSteps.some((step) => step.stage === 'resolve')) {
|
|
568
|
+
recordTraceStep('resolve', 'failure', {
|
|
569
|
+
reason,
|
|
570
|
+
failure_code: failureCode
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
if (!traceSteps.some((step) => step.stage === 'recover')) {
|
|
574
|
+
recordTraceStep('recover', retryable ? 'retry' : 'failure', {
|
|
575
|
+
reason,
|
|
576
|
+
failure_code: failureCode,
|
|
577
|
+
retry_allowed: retryable,
|
|
578
|
+
recovery_attempts: attempts,
|
|
579
|
+
retry_depth: attempts
|
|
580
|
+
});
|
|
581
|
+
}
|
|
505
582
|
const base = buildActionExecutionResult({
|
|
506
583
|
actionType,
|
|
507
584
|
sourceModule: 'interact',
|
|
@@ -522,7 +599,8 @@ export class ToolsInteract {
|
|
|
522
599
|
converged: false,
|
|
523
600
|
within_tolerance: false,
|
|
524
601
|
reason
|
|
525
|
-
}
|
|
602
|
+
},
|
|
603
|
+
traceSteps
|
|
526
604
|
});
|
|
527
605
|
return {
|
|
528
606
|
...base,
|
|
@@ -684,10 +762,23 @@ export class ToolsInteract {
|
|
|
684
762
|
? { property, value: currentValue, raw_value: typeof currentEl.state?.raw_value === 'number' ? currentEl.state.raw_value : undefined }
|
|
685
763
|
: null;
|
|
686
764
|
lastObservedState = actualState;
|
|
765
|
+
if (!traceSteps.some((step) => step.stage === 'resolve')) {
|
|
766
|
+
recordTraceStep('resolve', 'success', {
|
|
767
|
+
resolved_target: resolvedTarget,
|
|
768
|
+
current_value: currentValue,
|
|
769
|
+
adjustment_mode: lastAdjustmentMode
|
|
770
|
+
});
|
|
771
|
+
}
|
|
687
772
|
if (property !== 'value' && property !== 'raw_value') {
|
|
688
773
|
return buildFailure('ELEMENT_NOT_INTERACTABLE', 'adjust_control currently supports numeric value and raw_value properties only', resolvedTarget, currentDevice, actualState, attemptCount, lastAdjustmentMode, false);
|
|
689
774
|
}
|
|
690
775
|
if (currentValue !== null && Math.abs(currentValue - targetValue) <= normalizedTolerance) {
|
|
776
|
+
recordTraceStep('verify', 'success', {
|
|
777
|
+
property,
|
|
778
|
+
target_value: targetValue,
|
|
779
|
+
actual_state: actualState,
|
|
780
|
+
reason: 'control already within tolerance'
|
|
781
|
+
});
|
|
691
782
|
const uiFingerprintAfter = await ToolsInteract._captureFingerprint(resolvedPlatform, resolvedDeviceId);
|
|
692
783
|
const base = buildActionExecutionResult({
|
|
693
784
|
actionType,
|
|
@@ -708,7 +799,8 @@ export class ToolsInteract {
|
|
|
708
799
|
converged: true,
|
|
709
800
|
within_tolerance: true,
|
|
710
801
|
reason: 'control already within tolerance'
|
|
711
|
-
}
|
|
802
|
+
},
|
|
803
|
+
traceSteps
|
|
712
804
|
});
|
|
713
805
|
return {
|
|
714
806
|
...base,
|
|
@@ -743,6 +835,7 @@ export class ToolsInteract {
|
|
|
743
835
|
const currentPoint = currentValue !== null
|
|
744
836
|
? ToolsInteract._buildControlPoint(bounds, (currentValue - min) / (max - min), axis)
|
|
745
837
|
: ToolsInteract._buildControlPoint(bounds, 0.5, axis);
|
|
838
|
+
const probePoints = ToolsInteract._buildAdjustmentProbePoints(bounds, targetValue, currentValue, min, max, axis);
|
|
746
839
|
const runVerification = async () => {
|
|
747
840
|
const verification = await ToolsInteract.expectStateHandler({
|
|
748
841
|
element_id: resolvedTarget?.elementId ?? element_id,
|
|
@@ -770,37 +863,92 @@ export class ToolsInteract {
|
|
|
770
863
|
withinTolerance: observedValue !== null && Math.abs(observedValue - targetValue) <= normalizedTolerance
|
|
771
864
|
};
|
|
772
865
|
};
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
866
|
+
let actionDevice = currentDevice;
|
|
867
|
+
let observedState = actualState;
|
|
868
|
+
let verification = null;
|
|
869
|
+
let verificationResult = { verification: null, observedState: actualState, withinTolerance: false };
|
|
870
|
+
for (let i = 0; i < probePoints.length; i++) {
|
|
871
|
+
const probePoint = probePoints[i];
|
|
872
|
+
lastAdjustmentMode = 'coordinate';
|
|
873
|
+
recordTraceStep('execute', 'retry', {
|
|
874
|
+
attempt: attemptCount + 1,
|
|
875
|
+
mode: 'coordinate',
|
|
876
|
+
point: probePoint
|
|
877
|
+
});
|
|
878
|
+
const actionResult = await ToolsInteract.tapHandler({
|
|
785
879
|
platform: resolvedPlatform,
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
x2: targetPoint.x,
|
|
789
|
-
y2: targetPoint.y,
|
|
790
|
-
duration: 220,
|
|
880
|
+
x: probePoint.x,
|
|
881
|
+
y: probePoint.y,
|
|
791
882
|
deviceId: resolvedDeviceId
|
|
792
883
|
});
|
|
793
884
|
attemptCount++;
|
|
794
|
-
|
|
795
|
-
|
|
885
|
+
actionDevice = actionResult.device ?? actionDevice;
|
|
886
|
+
if (!actionResult.success) {
|
|
887
|
+
recordTraceStep('execute', 'retry', {
|
|
888
|
+
attempt: attemptCount,
|
|
889
|
+
mode: 'coordinate',
|
|
890
|
+
point: probePoint,
|
|
891
|
+
success: false
|
|
892
|
+
});
|
|
893
|
+
continue;
|
|
894
|
+
}
|
|
895
|
+
verificationResult = await runVerification();
|
|
896
|
+
observedState = verificationResult.observedState;
|
|
897
|
+
lastObservedState = observedState;
|
|
898
|
+
recordTraceStep('verify', verificationResult.withinTolerance ? 'success' : 'retry', {
|
|
899
|
+
attempt: attemptCount,
|
|
900
|
+
property,
|
|
901
|
+
target_value: targetValue,
|
|
902
|
+
actual_state: observedState,
|
|
903
|
+
reason: verificationResult.verification?.reason ?? 'control did not converge yet'
|
|
904
|
+
});
|
|
905
|
+
if (verificationResult.withinTolerance) {
|
|
906
|
+
const uiFingerprintAfter = await ToolsInteract._captureFingerprint(resolvedPlatform, resolvedDeviceId);
|
|
907
|
+
const base = buildActionExecutionResult({
|
|
908
|
+
actionType,
|
|
909
|
+
sourceModule: 'interact',
|
|
910
|
+
device: actionDevice ?? currentDevice,
|
|
911
|
+
selector: targetSelector,
|
|
912
|
+
resolved: resolvedTarget,
|
|
913
|
+
success: true,
|
|
914
|
+
uiFingerprintBefore: fingerprintBefore,
|
|
915
|
+
uiFingerprintAfter,
|
|
916
|
+
details: {
|
|
917
|
+
target_value: targetValue,
|
|
918
|
+
tolerance: normalizedTolerance,
|
|
919
|
+
property,
|
|
920
|
+
attempts: attemptCount,
|
|
921
|
+
adjustment_mode: lastAdjustmentMode,
|
|
922
|
+
actual_state: observedState,
|
|
923
|
+
converged: true,
|
|
924
|
+
within_tolerance: true,
|
|
925
|
+
reason: verificationResult.verification?.reason ?? 'control converged to target value'
|
|
926
|
+
},
|
|
927
|
+
traceSteps
|
|
928
|
+
});
|
|
929
|
+
return {
|
|
930
|
+
...base,
|
|
931
|
+
target_state: {
|
|
932
|
+
property,
|
|
933
|
+
target_value: targetValue,
|
|
934
|
+
tolerance: normalizedTolerance
|
|
935
|
+
},
|
|
936
|
+
actual_state: observedState,
|
|
937
|
+
within_tolerance: true,
|
|
938
|
+
converged: true,
|
|
939
|
+
attempts: attemptCount,
|
|
940
|
+
adjustment_mode: lastAdjustmentMode
|
|
941
|
+
};
|
|
796
942
|
}
|
|
797
|
-
actionDevice = fallbackActionResult.device ?? actionDevice;
|
|
798
943
|
}
|
|
799
|
-
|
|
800
|
-
let observedState = verificationResult.observedState;
|
|
801
|
-
lastObservedState = observedState;
|
|
802
|
-
if (!verificationResult.withinTolerance && currentValue !== null) {
|
|
944
|
+
if (currentValue !== null) {
|
|
803
945
|
lastAdjustmentMode = 'gesture';
|
|
946
|
+
recordTraceStep('execute', 'retry', {
|
|
947
|
+
attempt: attemptCount + 1,
|
|
948
|
+
mode: 'gesture',
|
|
949
|
+
start: currentPoint,
|
|
950
|
+
end: targetPoint
|
|
951
|
+
});
|
|
804
952
|
const fallbackActionResult = await ToolsInteract.swipeHandler({
|
|
805
953
|
platform: resolvedPlatform,
|
|
806
954
|
x1: currentPoint.x,
|
|
@@ -812,14 +960,75 @@ export class ToolsInteract {
|
|
|
812
960
|
});
|
|
813
961
|
attemptCount++;
|
|
814
962
|
if (!fallbackActionResult.success) {
|
|
815
|
-
|
|
963
|
+
recordTraceStep('execute', 'failure', {
|
|
964
|
+
attempt: attemptCount,
|
|
965
|
+
mode: 'gesture',
|
|
966
|
+
start: currentPoint,
|
|
967
|
+
end: targetPoint,
|
|
968
|
+
success: false
|
|
969
|
+
});
|
|
970
|
+
return buildFailure('UNKNOWN', fallbackActionResult.error ?? 'adjustment gesture failed', resolvedTarget, fallbackActionResult.device ?? actionDevice, observedState ?? actualState, attemptCount, lastAdjustmentMode, false);
|
|
816
971
|
}
|
|
972
|
+
actionDevice = fallbackActionResult.device ?? actionDevice;
|
|
817
973
|
verificationResult = await runVerification();
|
|
818
974
|
observedState = verificationResult.observedState;
|
|
975
|
+
lastObservedState = observedState;
|
|
976
|
+
recordTraceStep('verify', verificationResult.withinTolerance ? 'success' : 'retry', {
|
|
977
|
+
attempt: attemptCount,
|
|
978
|
+
property,
|
|
979
|
+
target_value: targetValue,
|
|
980
|
+
actual_state: observedState,
|
|
981
|
+
reason: verificationResult.verification?.reason ?? 'gesture adjustment did not converge yet'
|
|
982
|
+
});
|
|
983
|
+
if (verificationResult.withinTolerance) {
|
|
984
|
+
const uiFingerprintAfter = await ToolsInteract._captureFingerprint(resolvedPlatform, resolvedDeviceId);
|
|
985
|
+
const base = buildActionExecutionResult({
|
|
986
|
+
actionType,
|
|
987
|
+
sourceModule: 'interact',
|
|
988
|
+
device: actionDevice ?? currentDevice,
|
|
989
|
+
selector: targetSelector,
|
|
990
|
+
resolved: resolvedTarget,
|
|
991
|
+
success: true,
|
|
992
|
+
uiFingerprintBefore: fingerprintBefore,
|
|
993
|
+
uiFingerprintAfter,
|
|
994
|
+
details: {
|
|
995
|
+
target_value: targetValue,
|
|
996
|
+
tolerance: normalizedTolerance,
|
|
997
|
+
property,
|
|
998
|
+
attempts: attemptCount,
|
|
999
|
+
adjustment_mode: lastAdjustmentMode,
|
|
1000
|
+
actual_state: observedState,
|
|
1001
|
+
converged: true,
|
|
1002
|
+
within_tolerance: true,
|
|
1003
|
+
reason: verificationResult.verification?.reason ?? 'control converged to target value'
|
|
1004
|
+
},
|
|
1005
|
+
traceSteps
|
|
1006
|
+
});
|
|
1007
|
+
return {
|
|
1008
|
+
...base,
|
|
1009
|
+
target_state: {
|
|
1010
|
+
property,
|
|
1011
|
+
target_value: targetValue,
|
|
1012
|
+
tolerance: normalizedTolerance
|
|
1013
|
+
},
|
|
1014
|
+
actual_state: observedState,
|
|
1015
|
+
within_tolerance: true,
|
|
1016
|
+
converged: true,
|
|
1017
|
+
attempts: attemptCount,
|
|
1018
|
+
adjustment_mode: lastAdjustmentMode
|
|
1019
|
+
};
|
|
1020
|
+
}
|
|
819
1021
|
}
|
|
820
|
-
|
|
1022
|
+
verification = verificationResult.verification;
|
|
821
1023
|
lastObservedState = observedState;
|
|
822
1024
|
if (verificationResult.withinTolerance) {
|
|
1025
|
+
recordTraceStep('verify', 'success', {
|
|
1026
|
+
attempt: attemptCount,
|
|
1027
|
+
property,
|
|
1028
|
+
target_value: targetValue,
|
|
1029
|
+
actual_state: observedState,
|
|
1030
|
+
reason: verification?.reason ?? 'control converged to target value'
|
|
1031
|
+
});
|
|
823
1032
|
const uiFingerprintAfter = await ToolsInteract._captureFingerprint(resolvedPlatform, resolvedDeviceId);
|
|
824
1033
|
const base = buildActionExecutionResult({
|
|
825
1034
|
actionType,
|
|
@@ -840,7 +1049,8 @@ export class ToolsInteract {
|
|
|
840
1049
|
converged: true,
|
|
841
1050
|
within_tolerance: true,
|
|
842
1051
|
reason: verification?.reason ?? 'control converged to target value'
|
|
843
|
-
}
|
|
1052
|
+
},
|
|
1053
|
+
traceSteps
|
|
844
1054
|
});
|
|
845
1055
|
return {
|
|
846
1056
|
...base,
|
|
@@ -1224,6 +1434,10 @@ export class ToolsInteract {
|
|
|
1224
1434
|
let lastMatchedCount = 0;
|
|
1225
1435
|
let lastMatchedElement = null;
|
|
1226
1436
|
let lastConditionSatisfied = false;
|
|
1437
|
+
let matchedAt = null;
|
|
1438
|
+
let stableMatchCount = 0;
|
|
1439
|
+
const stableObservationCount = 2;
|
|
1440
|
+
const snapshotStaleThresholdMs = 500;
|
|
1227
1441
|
// Precompute normalized selector values and helpers (constant across polls)
|
|
1228
1442
|
const normalize = ToolsInteract._normalize;
|
|
1229
1443
|
const containsFlag = !!selector?.contains;
|
|
@@ -1338,23 +1552,34 @@ export class ToolsInteract {
|
|
|
1338
1552
|
lastMatchedCount = matchedCount;
|
|
1339
1553
|
lastConditionSatisfied = conditionMet;
|
|
1340
1554
|
lastMatchedElement = matchedElement ? ToolsInteract._buildResolvedElement(resolvedPlatform, resolvedDeviceId, matchedElement.el, matchedElement.idx) : null;
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1555
|
+
const now = Date.now();
|
|
1556
|
+
const snapshotAgeMs = typeof tree?.captured_at_ms === 'number' ? now - tree.captured_at_ms : null;
|
|
1557
|
+
const snapshotFresh = snapshotAgeMs === null || snapshotAgeMs <= snapshotStaleThresholdMs;
|
|
1558
|
+
if (conditionMet && snapshotFresh) {
|
|
1559
|
+
if (matchedAt === null)
|
|
1560
|
+
matchedAt = now;
|
|
1561
|
+
stableMatchCount++;
|
|
1562
|
+
if (stableMatchCount >= stableObservationCount) {
|
|
1563
|
+
const latency_ms = now - overallStart;
|
|
1564
|
+
const outEl = lastMatchedElement;
|
|
1565
|
+
return {
|
|
1566
|
+
status: 'success',
|
|
1567
|
+
matched: matchedCount,
|
|
1568
|
+
element: outEl,
|
|
1569
|
+
metrics: { latency_ms, poll_count: totalPollCount, attempts },
|
|
1570
|
+
requested,
|
|
1571
|
+
observed: {
|
|
1572
|
+
matched_count: matchedCount,
|
|
1573
|
+
condition_satisfied: true,
|
|
1574
|
+
selected_index: outEl?.index ?? null,
|
|
1575
|
+
last_matched_element: outEl
|
|
1576
|
+
}
|
|
1577
|
+
};
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
else {
|
|
1581
|
+
stableMatchCount = 0;
|
|
1582
|
+
matchedAt = null;
|
|
1358
1583
|
}
|
|
1359
1584
|
}
|
|
1360
1585
|
catch (e) {
|
|
@@ -1599,7 +1824,24 @@ export class ToolsInteract {
|
|
|
1599
1824
|
basis,
|
|
1600
1825
|
matched: success,
|
|
1601
1826
|
reason
|
|
1602
|
-
}
|
|
1827
|
+
},
|
|
1828
|
+
trace: buildObservationTrace({
|
|
1829
|
+
actionType: 'expect_screen',
|
|
1830
|
+
stage: 'verify',
|
|
1831
|
+
success,
|
|
1832
|
+
attempts: 1,
|
|
1833
|
+
metadata: {
|
|
1834
|
+
expected_screen: expectedScreen,
|
|
1835
|
+
observed_screen: {
|
|
1836
|
+
fingerprint: observedScreen.fingerprint,
|
|
1837
|
+
screen: observedScreenLabel
|
|
1838
|
+
},
|
|
1839
|
+
comparison: {
|
|
1840
|
+
basis,
|
|
1841
|
+
matched: success
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
})
|
|
1603
1845
|
};
|
|
1604
1846
|
}
|
|
1605
1847
|
static async expectElementVisibleHandler({ selector, element_id, timeout_ms = 5000, poll_interval_ms = 300, platform, deviceId }) {
|
|
@@ -1653,7 +1895,18 @@ export class ToolsInteract {
|
|
|
1653
1895
|
semantic: result.element.semantic ?? null
|
|
1654
1896
|
}
|
|
1655
1897
|
},
|
|
1656
|
-
reason: 'selector is visible'
|
|
1898
|
+
reason: 'selector is visible',
|
|
1899
|
+
trace: buildObservationTrace({
|
|
1900
|
+
actionType: 'expect_element_visible',
|
|
1901
|
+
stage: 'verify',
|
|
1902
|
+
success: true,
|
|
1903
|
+
attempts: 1,
|
|
1904
|
+
metadata: {
|
|
1905
|
+
selector,
|
|
1906
|
+
element_id: result.element.elementId ?? element_id ?? null,
|
|
1907
|
+
status: result.status
|
|
1908
|
+
}
|
|
1909
|
+
})
|
|
1657
1910
|
};
|
|
1658
1911
|
}
|
|
1659
1912
|
const errorCode = result?.error?.code === 'INTERNAL_ERROR' ? 'UNKNOWN' : 'TIMEOUT';
|
|
@@ -1671,177 +1924,318 @@ export class ToolsInteract {
|
|
|
1671
1924
|
},
|
|
1672
1925
|
reason: result?.error?.message ?? 'selector is not visible',
|
|
1673
1926
|
failure_code: errorCode,
|
|
1674
|
-
retryable: errorCode === 'TIMEOUT'
|
|
1927
|
+
retryable: errorCode === 'TIMEOUT',
|
|
1928
|
+
trace: buildObservationTrace({
|
|
1929
|
+
actionType: 'expect_element_visible',
|
|
1930
|
+
stage: 'verify',
|
|
1931
|
+
success: false,
|
|
1932
|
+
attempts: 1,
|
|
1933
|
+
metadata: {
|
|
1934
|
+
selector,
|
|
1935
|
+
element_id: element_id ?? null,
|
|
1936
|
+
status: result?.status ?? null,
|
|
1937
|
+
reason: result?.error?.message ?? 'selector is not visible'
|
|
1938
|
+
}
|
|
1939
|
+
})
|
|
1675
1940
|
};
|
|
1676
1941
|
}
|
|
1677
|
-
static async expectStateHandler({ selector, element_id, property, expected, platform, deviceId }) {
|
|
1678
|
-
const tree = await ToolsObserve.getUITreeHandler({ platform, deviceId });
|
|
1679
|
-
const elements = Array.isArray(tree?.elements) ? tree.elements : [];
|
|
1680
|
-
const treePlatform = tree?.device?.platform === 'ios' ? 'ios' : (platform || 'android');
|
|
1681
|
-
const treeDeviceId = tree?.device?.id || deviceId;
|
|
1682
|
-
let matched = null;
|
|
1683
|
-
if (element_id) {
|
|
1684
|
-
const resolved = ToolsInteract._resolvedUiElements.get(element_id);
|
|
1685
|
-
if (resolved) {
|
|
1686
|
-
const current = ToolsInteract._findCurrentResolvedElement(elements, treePlatform, treeDeviceId, resolved);
|
|
1687
|
-
if (current)
|
|
1688
|
-
matched = { el: current.el, idx: current.index };
|
|
1689
|
-
}
|
|
1690
|
-
}
|
|
1691
|
-
if (!matched && selector) {
|
|
1692
|
-
matched = ToolsInteract._findFirstMatchingElement(elements, selector);
|
|
1693
|
-
}
|
|
1694
|
-
if (!matched) {
|
|
1695
|
-
return {
|
|
1696
|
-
success: false,
|
|
1697
|
-
selector,
|
|
1698
|
-
element_id: element_id ?? null,
|
|
1699
|
-
expected_state: { property, expected },
|
|
1700
|
-
reason: 'element not found',
|
|
1701
|
-
failure_code: 'ELEMENT_NOT_FOUND',
|
|
1702
|
-
retryable: true
|
|
1703
|
-
};
|
|
1704
|
-
}
|
|
1705
|
-
const resolvedElement = ToolsInteract._resolvedTargetFromElement(ToolsInteract._computeElementId(treePlatform, treeDeviceId, matched.el, matched.idx), matched.el, matched.idx);
|
|
1706
|
-
const observedState = matched.el.state ?? null;
|
|
1707
|
-
const actual = observedState?.[property] ?? null;
|
|
1942
|
+
static async expectStateHandler({ selector, element_id, property, expected, platform, deviceId, stabilization_window_ms = 1000, stable_observation_count = 2, snapshot_stale_threshold_ms = 500, poll_interval_ms = 150 }) {
|
|
1708
1943
|
const compareBoolean = (value) => typeof value === 'boolean' ? value : null;
|
|
1709
1944
|
const compareString = (value) => typeof value === 'string' ? value : null;
|
|
1710
1945
|
const compareNumber = (value) => typeof value === 'number' && Number.isFinite(value) ? value : null;
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1946
|
+
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
|
1947
|
+
const start = Date.now();
|
|
1948
|
+
const deadline = start + Math.max(500, stabilization_window_ms);
|
|
1949
|
+
const stableTarget = Math.max(1, Math.floor(stable_observation_count || 2));
|
|
1950
|
+
const pollDelay = Math.max(100, Math.min(poll_interval_ms || 150, 200));
|
|
1951
|
+
const staleThreshold = Math.max(300, Math.min(snapshot_stale_threshold_ms || 500, 800));
|
|
1952
|
+
let attempts = 0;
|
|
1953
|
+
let stableCount = 0;
|
|
1954
|
+
let lastReason = 'element not found';
|
|
1955
|
+
let lastFailureCode = 'ELEMENT_NOT_FOUND';
|
|
1956
|
+
let lastObservedElement = null;
|
|
1957
|
+
let lastObservedValue = null;
|
|
1958
|
+
let lastRawValue = null;
|
|
1959
|
+
let lastResolvedElementId = element_id ?? null;
|
|
1960
|
+
const traceSteps = [];
|
|
1961
|
+
let traceAttemptIndex = 0;
|
|
1962
|
+
let resolveRecorded = false;
|
|
1963
|
+
const recordTraceStep = (stage, result, metadata) => {
|
|
1964
|
+
traceSteps.push(createTraceStep({
|
|
1965
|
+
stage,
|
|
1966
|
+
timestamp: Date.now(),
|
|
1967
|
+
result,
|
|
1968
|
+
attemptIndex: traceAttemptIndex++,
|
|
1969
|
+
metadata
|
|
1970
|
+
}));
|
|
1971
|
+
};
|
|
1972
|
+
const buildStateTrace = (outcome) => ({
|
|
1973
|
+
action_id: nextActionId('expect_state', Date.now()),
|
|
1974
|
+
steps: traceSteps,
|
|
1975
|
+
final_outcome: outcome,
|
|
1976
|
+
attempts
|
|
1977
|
+
});
|
|
1978
|
+
while (Date.now() <= deadline) {
|
|
1979
|
+
attempts++;
|
|
1980
|
+
const tree = await ToolsObserve.getUITreeHandler({ platform, deviceId });
|
|
1981
|
+
const elements = Array.isArray(tree?.elements) ? tree.elements : [];
|
|
1982
|
+
const treePlatform = tree?.device?.platform === 'ios' ? 'ios' : (platform || 'android');
|
|
1983
|
+
const treeDeviceId = tree?.device?.id || deviceId;
|
|
1984
|
+
const treeAgeMs = typeof tree?.captured_at_ms === 'number' ? Date.now() - tree.captured_at_ms : null;
|
|
1985
|
+
let matched = null;
|
|
1986
|
+
if (element_id) {
|
|
1987
|
+
const resolved = ToolsInteract._resolvedUiElements.get(element_id);
|
|
1988
|
+
if (resolved) {
|
|
1989
|
+
const current = ToolsInteract._findCurrentResolvedElement(elements, treePlatform, treeDeviceId, resolved);
|
|
1990
|
+
if (current)
|
|
1991
|
+
matched = { el: current.el, idx: current.index };
|
|
1732
1992
|
}
|
|
1733
|
-
observedValue = actualBool;
|
|
1734
|
-
break;
|
|
1735
1993
|
}
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
const expectedNumber = compareNumber(expected);
|
|
1739
|
-
const actualNumber = compareNumber(actual);
|
|
1740
|
-
if (expectedNumber !== null && actualNumber !== null) {
|
|
1741
|
-
success = actualNumber === expectedNumber;
|
|
1742
|
-
rawValue = actualNumber;
|
|
1743
|
-
observedValue = actualNumber;
|
|
1744
|
-
reason = success ? 'value matches expected value' : `expected value=${expectedNumber} but observed ${actualNumber}`;
|
|
1745
|
-
break;
|
|
1746
|
-
}
|
|
1747
|
-
const expectedString = typeof expected === 'string' ? expected : null;
|
|
1748
|
-
const actualString = compareString(actual);
|
|
1749
|
-
if (expectedString !== null && actualString !== null) {
|
|
1750
|
-
success = actualString === expectedString;
|
|
1751
|
-
rawValue = actualString;
|
|
1752
|
-
observedValue = actualString;
|
|
1753
|
-
reason = success ? 'value matches expected value' : `expected value=${expectedString} but observed ${actualString}`;
|
|
1754
|
-
}
|
|
1755
|
-
else {
|
|
1756
|
-
reason = 'value state unavailable';
|
|
1757
|
-
}
|
|
1758
|
-
break;
|
|
1994
|
+
if (!matched && selector) {
|
|
1995
|
+
matched = ToolsInteract._findFirstMatchingElement(elements, selector);
|
|
1759
1996
|
}
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
reason
|
|
1997
|
+
if (!matched) {
|
|
1998
|
+
lastReason = 'element not found';
|
|
1999
|
+
lastFailureCode = 'ELEMENT_NOT_FOUND';
|
|
2000
|
+
stableCount = 0;
|
|
2001
|
+
recordTraceStep('resolve', 'retry', {
|
|
2002
|
+
selector: selector ?? null,
|
|
2003
|
+
element_id: lastResolvedElementId,
|
|
2004
|
+
matched: false,
|
|
2005
|
+
reason: lastReason,
|
|
2006
|
+
attempt: attempts
|
|
2007
|
+
});
|
|
2008
|
+
recordTraceStep('stabilize', 'retry', {
|
|
2009
|
+
stabilization_attempts: attempts,
|
|
2010
|
+
stable_observation_count: stableCount,
|
|
2011
|
+
snapshot_freshness_ms: treeAgeMs,
|
|
2012
|
+
reason: lastReason
|
|
2013
|
+
});
|
|
2014
|
+
await sleep(pollDelay);
|
|
2015
|
+
continue;
|
|
2016
|
+
}
|
|
2017
|
+
const resolvedElement = ToolsInteract._resolvedTargetFromElement(ToolsInteract._computeElementId(treePlatform, treeDeviceId, matched.el, matched.idx), matched.el, matched.idx);
|
|
2018
|
+
lastResolvedElementId = resolvedElement.elementId;
|
|
2019
|
+
lastObservedElement = { ...resolvedElement, state: matched.el.state ?? null };
|
|
2020
|
+
if (treeAgeMs !== null && treeAgeMs > staleThreshold) {
|
|
2021
|
+
lastReason = 'stale snapshot';
|
|
2022
|
+
lastFailureCode = 'UNKNOWN';
|
|
2023
|
+
stableCount = 0;
|
|
2024
|
+
recordTraceStep('resolve', 'retry', {
|
|
2025
|
+
selector: selector ?? null,
|
|
2026
|
+
element_id: lastResolvedElementId,
|
|
2027
|
+
matched: true,
|
|
2028
|
+
reason: lastReason,
|
|
2029
|
+
attempt: attempts
|
|
2030
|
+
});
|
|
2031
|
+
recordTraceStep('stabilize', 'retry', {
|
|
2032
|
+
stabilization_attempts: attempts,
|
|
2033
|
+
stable_observation_count: stableCount,
|
|
2034
|
+
snapshot_freshness_ms: treeAgeMs,
|
|
2035
|
+
reason: lastReason
|
|
2036
|
+
});
|
|
2037
|
+
await sleep(pollDelay);
|
|
2038
|
+
continue;
|
|
2039
|
+
}
|
|
2040
|
+
if (!resolveRecorded) {
|
|
2041
|
+
recordTraceStep('resolve', 'success', {
|
|
2042
|
+
selector: selector ?? null,
|
|
2043
|
+
element_id: lastResolvedElementId,
|
|
2044
|
+
matched: true,
|
|
2045
|
+
reason: 'element resolved'
|
|
2046
|
+
});
|
|
2047
|
+
resolveRecorded = true;
|
|
2048
|
+
}
|
|
2049
|
+
const observedState = matched.el.state ?? null;
|
|
2050
|
+
const actual = observedState?.[property] ?? null;
|
|
2051
|
+
let success = false;
|
|
2052
|
+
let reason = '';
|
|
2053
|
+
let rawValue = null;
|
|
2054
|
+
let observedValue = actual;
|
|
2055
|
+
switch (property) {
|
|
2056
|
+
case 'checked':
|
|
2057
|
+
case 'focused':
|
|
2058
|
+
case 'expanded':
|
|
2059
|
+
case 'enabled': {
|
|
2060
|
+
const expectedBool = compareBoolean(expected);
|
|
2061
|
+
const actualBool = compareBoolean(actual);
|
|
2062
|
+
if (expectedBool === null) {
|
|
2063
|
+
reason = `expected ${property} must be boolean`;
|
|
2064
|
+
}
|
|
2065
|
+
else if (actualBool === null) {
|
|
2066
|
+
reason = `${property} state unavailable`;
|
|
2067
|
+
}
|
|
2068
|
+
else {
|
|
2069
|
+
rawValue = actualBool;
|
|
2070
|
+
success = actualBool === expectedBool;
|
|
2071
|
+
reason = success ? `${property} matches expected value` : `expected ${property}=${expectedBool} but observed ${actualBool}`;
|
|
2072
|
+
}
|
|
2073
|
+
observedValue = actualBool;
|
|
1769
2074
|
break;
|
|
1770
2075
|
}
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
2076
|
+
case 'value':
|
|
2077
|
+
case 'raw_value': {
|
|
2078
|
+
const expectedNumber = compareNumber(expected);
|
|
2079
|
+
const actualNumber = compareNumber(actual);
|
|
2080
|
+
if (expectedNumber !== null && actualNumber !== null) {
|
|
2081
|
+
success = actualNumber === expectedNumber;
|
|
2082
|
+
rawValue = actualNumber;
|
|
2083
|
+
observedValue = actualNumber;
|
|
2084
|
+
reason = success ? 'value matches expected value' : `expected value=${expectedNumber} but observed ${actualNumber}`;
|
|
1775
2085
|
break;
|
|
1776
2086
|
}
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
2087
|
+
const expectedString = typeof expected === 'string' ? expected : null;
|
|
2088
|
+
const actualString = compareString(actual);
|
|
2089
|
+
if (expectedString !== null && actualString !== null) {
|
|
2090
|
+
success = actualString === expectedString;
|
|
2091
|
+
rawValue = actualString;
|
|
2092
|
+
observedValue = actualString;
|
|
2093
|
+
reason = success ? 'value matches expected value' : `expected value=${expectedString} but observed ${actualString}`;
|
|
2094
|
+
}
|
|
2095
|
+
else {
|
|
2096
|
+
reason = 'value state unavailable';
|
|
2097
|
+
}
|
|
1781
2098
|
break;
|
|
1782
2099
|
}
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
2100
|
+
case 'selected': {
|
|
2101
|
+
const expectedBool = typeof expected === 'boolean' ? expected : null;
|
|
2102
|
+
const expectedString = typeof expected === 'string'
|
|
2103
|
+
? expected
|
|
2104
|
+
: expected && typeof expected === 'object'
|
|
2105
|
+
? String(expected.id ?? expected.label ?? '')
|
|
2106
|
+
: null;
|
|
2107
|
+
if (!observedState || observedState.selected === undefined || observedState.selected === null) {
|
|
2108
|
+
reason = 'selected state unavailable';
|
|
2109
|
+
break;
|
|
2110
|
+
}
|
|
2111
|
+
if (expectedBool !== null) {
|
|
2112
|
+
const actualBool = typeof observedState.selected === 'boolean' ? observedState.selected : null;
|
|
2113
|
+
if (actualBool === null) {
|
|
2114
|
+
reason = 'selected state is not boolean';
|
|
2115
|
+
break;
|
|
2116
|
+
}
|
|
2117
|
+
rawValue = actualBool;
|
|
2118
|
+
observedValue = actualBool;
|
|
2119
|
+
success = actualBool === expectedBool;
|
|
2120
|
+
reason = success ? 'selected matches expected value' : `expected selected=${expectedBool} but observed ${actualBool}`;
|
|
2121
|
+
break;
|
|
2122
|
+
}
|
|
2123
|
+
const actualSelected = typeof observedState.selected === 'object' && observedState.selected !== null
|
|
2124
|
+
? String(observedState.selected.id ?? observedState.selected.label ?? '')
|
|
2125
|
+
: String(observedState.selected);
|
|
2126
|
+
const actualString = actualSelected.trim();
|
|
2127
|
+
if (!expectedString) {
|
|
2128
|
+
reason = 'expected selected must be boolean, string, or object with id/label';
|
|
2129
|
+
break;
|
|
2130
|
+
}
|
|
2131
|
+
rawValue = actualString;
|
|
2132
|
+
observedValue = actualString;
|
|
2133
|
+
success = actualString === expectedString;
|
|
2134
|
+
reason = success ? 'selected matches expected value' : `expected selected=${expectedString} but observed ${actualString}`;
|
|
1789
2135
|
break;
|
|
1790
2136
|
}
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
2137
|
+
case 'text_value': {
|
|
2138
|
+
const expectedString = typeof expected === 'string' ? expected : null;
|
|
2139
|
+
const actualString = compareString(actual);
|
|
2140
|
+
if (!expectedString) {
|
|
2141
|
+
reason = 'expected text_value must be string';
|
|
2142
|
+
}
|
|
2143
|
+
else if (!actualString) {
|
|
2144
|
+
reason = 'text_value state unavailable';
|
|
2145
|
+
}
|
|
2146
|
+
else {
|
|
2147
|
+
success = actualString === expectedString;
|
|
2148
|
+
rawValue = actualString;
|
|
2149
|
+
observedValue = actualString;
|
|
2150
|
+
reason = success ? 'text_value matches expected value' : `expected text_value=${expectedString} but observed ${actualString}`;
|
|
2151
|
+
}
|
|
2152
|
+
break;
|
|
1805
2153
|
}
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
2154
|
+
default: {
|
|
2155
|
+
if (actual !== null && actual !== undefined) {
|
|
2156
|
+
success = actual === expected;
|
|
2157
|
+
observedValue = actual;
|
|
2158
|
+
rawValue = typeof actual === 'string' || typeof actual === 'number' || typeof actual === 'boolean' ? actual : null;
|
|
2159
|
+
reason = success ? `${property} matches expected value` : `expected ${property} to match but observed ${String(actual)}`;
|
|
2160
|
+
}
|
|
2161
|
+
else {
|
|
2162
|
+
reason = `unsupported or unavailable state property: ${property}`;
|
|
2163
|
+
}
|
|
1811
2164
|
}
|
|
1812
|
-
break;
|
|
1813
2165
|
}
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
2166
|
+
if (success) {
|
|
2167
|
+
stableCount++;
|
|
2168
|
+
recordTraceStep('stabilize', 'success', {
|
|
2169
|
+
stabilization_attempts: attempts,
|
|
2170
|
+
stable_observation_count: stableCount,
|
|
2171
|
+
snapshot_freshness_ms: treeAgeMs,
|
|
2172
|
+
reason
|
|
2173
|
+
});
|
|
2174
|
+
recordTraceStep('verify', 'success', {
|
|
2175
|
+
property,
|
|
2176
|
+
expected,
|
|
2177
|
+
observed_value: observedValue,
|
|
2178
|
+
reason
|
|
2179
|
+
});
|
|
2180
|
+
if (stableCount >= stableTarget) {
|
|
2181
|
+
return {
|
|
2182
|
+
success: true,
|
|
2183
|
+
selector,
|
|
2184
|
+
element_id: lastResolvedElementId,
|
|
2185
|
+
expected_state: { property, expected },
|
|
2186
|
+
element: lastObservedElement,
|
|
2187
|
+
observed_state: {
|
|
2188
|
+
property,
|
|
2189
|
+
value: observedValue,
|
|
2190
|
+
...(rawValue !== null ? { raw_value: rawValue } : {})
|
|
2191
|
+
},
|
|
2192
|
+
reason,
|
|
2193
|
+
stabilization_attempts: attempts,
|
|
2194
|
+
stabilization_window_ms: Date.now() - start,
|
|
2195
|
+
stable_observation_count: stableCount,
|
|
2196
|
+
snapshot_freshness_ms: treeAgeMs ?? undefined,
|
|
2197
|
+
trace: buildStateTrace('success')
|
|
2198
|
+
};
|
|
1823
2199
|
}
|
|
1824
2200
|
}
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
2201
|
+
else {
|
|
2202
|
+
stableCount = 0;
|
|
2203
|
+
lastReason = reason || lastReason;
|
|
2204
|
+
lastFailureCode = 'UNKNOWN';
|
|
2205
|
+
recordTraceStep('stabilize', 'retry', {
|
|
2206
|
+
stabilization_attempts: attempts,
|
|
2207
|
+
stable_observation_count: stableCount,
|
|
2208
|
+
snapshot_freshness_ms: treeAgeMs,
|
|
2209
|
+
reason: lastReason
|
|
2210
|
+
});
|
|
2211
|
+
recordTraceStep('verify', 'retry', {
|
|
2212
|
+
property,
|
|
2213
|
+
expected,
|
|
2214
|
+
observed_value: observedValue,
|
|
2215
|
+
reason: lastReason
|
|
2216
|
+
});
|
|
2217
|
+
}
|
|
2218
|
+
if (!success) {
|
|
2219
|
+
lastObservedValue = observedValue;
|
|
2220
|
+
lastRawValue = rawValue;
|
|
2221
|
+
}
|
|
2222
|
+
await sleep(pollDelay);
|
|
1828
2223
|
}
|
|
1829
2224
|
return {
|
|
1830
|
-
success,
|
|
2225
|
+
success: false,
|
|
1831
2226
|
selector,
|
|
1832
|
-
element_id:
|
|
2227
|
+
element_id: lastResolvedElementId,
|
|
1833
2228
|
expected_state: { property, expected },
|
|
1834
|
-
element:
|
|
1835
|
-
...resolvedElement,
|
|
1836
|
-
state: observedState
|
|
1837
|
-
},
|
|
2229
|
+
element: lastObservedElement,
|
|
1838
2230
|
observed_state: {
|
|
1839
2231
|
property,
|
|
1840
|
-
value:
|
|
1841
|
-
...(
|
|
2232
|
+
value: lastObservedValue,
|
|
2233
|
+
...(lastRawValue !== null ? { raw_value: lastRawValue } : {})
|
|
1842
2234
|
},
|
|
1843
|
-
reason,
|
|
1844
|
-
|
|
2235
|
+
reason: lastReason,
|
|
2236
|
+
failure_code: lastFailureCode,
|
|
2237
|
+
retryable: true,
|
|
2238
|
+
trace: buildStateTrace('failure')
|
|
1845
2239
|
};
|
|
1846
2240
|
}
|
|
1847
2241
|
static async waitForUICore({ type = 'ui', query, timeoutMs = 30000, pollIntervalMs = 300, includeSnapshotOnFailure = true, match = 'present', stability_ms = 700, observationDelayMs = 0, platform, deviceId }) {
|