mobile-debug-mcp 0.27.0 → 0.29.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.
@@ -1,9 +1,13 @@
1
1
  import type {
2
+ ActionTrace,
2
3
  ActionExecutionResult,
3
4
  ActionFailureCode,
4
5
  ActionTargetResolved,
5
6
  FailureClass,
6
- RecoveryState
7
+ RecoveryState,
8
+ TraceResult,
9
+ TraceStage,
10
+ TraceStep
7
11
  } from '../types.js'
8
12
  import { ToolsObserve } from '../observe/index.js'
9
13
 
@@ -92,6 +96,172 @@ export async function captureActionFingerprint(platform?: 'android' | 'ios', dev
92
96
  }
93
97
  }
94
98
 
99
+ export function createTraceStep({
100
+ stage,
101
+ timestamp,
102
+ result,
103
+ attemptIndex,
104
+ cycleId,
105
+ metadata
106
+ }: {
107
+ stage: TraceStage
108
+ timestamp: number
109
+ result: TraceResult
110
+ attemptIndex: number
111
+ cycleId?: number
112
+ metadata?: Record<string, unknown>
113
+ }): TraceStep {
114
+ return {
115
+ stage,
116
+ timestamp,
117
+ result,
118
+ attempt_index: attemptIndex,
119
+ ...(cycleId !== undefined ? { cycle_id: cycleId } : {}),
120
+ ...(metadata ? { metadata } : {})
121
+ }
122
+ }
123
+
124
+ export function buildActionTrace({
125
+ actionId,
126
+ actionType,
127
+ sourceModule,
128
+ selector,
129
+ resolved,
130
+ success,
131
+ failure,
132
+ details,
133
+ recovery,
134
+ attempts = 1,
135
+ steps
136
+ }: {
137
+ actionId: string
138
+ actionType: string
139
+ sourceModule: 'server' | 'interact'
140
+ selector: Record<string, unknown> | null
141
+ resolved?: Partial<ActionTargetResolved> | null
142
+ success: boolean
143
+ failure?: { failureCode: ActionFailureCode; retryable: boolean }
144
+ details?: Record<string, unknown>
145
+ recovery?: RecoveryState
146
+ attempts?: number
147
+ steps?: TraceStep[]
148
+ }): ActionTrace {
149
+ if (steps && steps.length > 0) {
150
+ return {
151
+ action_id: actionId,
152
+ steps,
153
+ final_outcome: success ? 'success' : 'failure',
154
+ attempts: Math.max(1, Math.floor(attempts || steps.length))
155
+ }
156
+ }
157
+
158
+ const start = Date.now()
159
+ const builtSteps: TraceStep[] = []
160
+ let attemptIndex = 0
161
+ const totalAttempts = Math.max(1, Math.floor(attempts || 1))
162
+
163
+ if (selector || resolved) {
164
+ const stageResult: TraceResult = resolved ? 'success' : 'failure'
165
+ builtSteps.push(createTraceStep({
166
+ stage: 'resolve',
167
+ timestamp: start,
168
+ result: stageResult,
169
+ attemptIndex: attemptIndex++,
170
+ metadata: {
171
+ action_type: actionType,
172
+ source_module: sourceModule,
173
+ selector: selector ?? null,
174
+ resolved: resolved ? normalizeResolvedTarget(resolved) : null
175
+ }
176
+ }))
177
+ }
178
+
179
+ builtSteps.push(createTraceStep({
180
+ stage: 'execute',
181
+ timestamp: Date.now(),
182
+ result: success ? 'success' : 'failure',
183
+ attemptIndex: attemptIndex++,
184
+ metadata: {
185
+ action_type: actionType,
186
+ source_module: sourceModule,
187
+ ...(failure ? { failure_code: failure.failureCode, retryable: failure.retryable } : {})
188
+ }
189
+ }))
190
+
191
+ const hasStabilizeDetails = Boolean(details && (
192
+ Object.prototype.hasOwnProperty.call(details, 'stabilization_attempts') ||
193
+ Object.prototype.hasOwnProperty.call(details, 'stable_observation_count') ||
194
+ Object.prototype.hasOwnProperty.call(details, 'snapshot_freshness_ms')
195
+ ))
196
+ const hasVerifyDetails = Boolean(details && (
197
+ Object.prototype.hasOwnProperty.call(details, 'within_tolerance') ||
198
+ Object.prototype.hasOwnProperty.call(details, 'converged') ||
199
+ Object.prototype.hasOwnProperty.call(details, 'observed_state')
200
+ ))
201
+
202
+ if (hasStabilizeDetails) {
203
+ builtSteps.push(createTraceStep({
204
+ stage: 'stabilize',
205
+ timestamp: Date.now(),
206
+ result: success ? 'success' : 'failure',
207
+ attemptIndex: attemptIndex++,
208
+ metadata: {
209
+ stabilization_attempts: (details?.stabilization_attempts as number | undefined) ?? null,
210
+ stable_observation_count: (details?.stable_observation_count as number | undefined) ?? null,
211
+ snapshot_freshness_ms: (details?.snapshot_freshness_ms as number | undefined) ?? null
212
+ }
213
+ }))
214
+ }
215
+
216
+ if (hasVerifyDetails) {
217
+ builtSteps.push(createTraceStep({
218
+ stage: 'verify',
219
+ timestamp: Date.now(),
220
+ result: success ? 'success' : 'failure',
221
+ attemptIndex: attemptIndex++,
222
+ metadata: {
223
+ within_tolerance: details?.within_tolerance ?? null,
224
+ converged: details?.converged ?? null,
225
+ actual_state: details?.actual_state ?? null,
226
+ reason: details?.reason ?? null
227
+ }
228
+ }))
229
+ }
230
+
231
+ if (failure) {
232
+ builtSteps.push(createTraceStep({
233
+ stage: 'recover',
234
+ timestamp: Date.now(),
235
+ result: failure.retryable ? 'retry' : 'failure',
236
+ attemptIndex: attemptIndex++,
237
+ metadata: {
238
+ failure_class: recovery?.failure_class ?? mapFailureCodeToFailureClass(failure.failureCode),
239
+ runtime_code: failure.failureCode,
240
+ retry_allowed: failure.retryable,
241
+ recovery_attempts: recovery?.recovery_attempts ?? 0,
242
+ retry_depth: recovery?.retry_depth ?? 0
243
+ }
244
+ }))
245
+ }
246
+
247
+ if (!builtSteps.length) {
248
+ builtSteps.push(createTraceStep({
249
+ stage: 'execute',
250
+ timestamp: start,
251
+ result: success ? 'success' : 'failure',
252
+ attemptIndex: 0,
253
+ metadata: { action_type: actionType, source_module: sourceModule }
254
+ }))
255
+ }
256
+
257
+ return {
258
+ action_id: actionId,
259
+ steps: builtSteps,
260
+ final_outcome: success ? 'success' : 'failure',
261
+ attempts: totalAttempts
262
+ }
263
+ }
264
+
95
265
  export function normalizeResolvedTarget(value: Partial<ActionTargetResolved> | null = null): ActionTargetResolved | null {
96
266
  if (!value) return null
97
267
  return {
@@ -181,7 +351,8 @@ export function buildActionExecutionResult({
181
351
  uiFingerprintAfter,
182
352
  failure,
183
353
  details,
184
- sourceModule
354
+ sourceModule,
355
+ traceSteps
185
356
  }: {
186
357
  actionType: string
187
358
  device?: ActionExecutionResult['device']
@@ -193,11 +364,17 @@ export function buildActionExecutionResult({
193
364
  failure?: { failureCode: ActionFailureCode; retryable: boolean }
194
365
  details?: Record<string, unknown>
195
366
  sourceModule: 'server' | 'interact'
367
+ traceSteps?: TraceStep[]
196
368
  }): ActionExecutionResult {
197
369
  const timestampMs = Date.now()
198
370
  const timestamp = new Date(timestampMs).toISOString()
371
+ const actionId = nextActionId(actionType, timestampMs)
372
+ const recoveryState = failure ? buildRecoveryState(failure.failureCode, failure.retryable) : undefined
373
+ const attempts = typeof details?.attempts === 'number' && Number.isFinite(details.attempts)
374
+ ? Math.max(1, Math.floor(details.attempts))
375
+ : 1
199
376
  return {
200
- action_id: nextActionId(actionType, timestampMs),
377
+ action_id: actionId,
201
378
  timestamp,
202
379
  action_type: actionType,
203
380
  lifecycle_state: determineActionLifecycleState({ success, failure }),
@@ -209,7 +386,20 @@ export function buildActionExecutionResult({
209
386
  },
210
387
  success,
211
388
  ...(failure ? { failure_code: failure.failureCode, retryable: failure.retryable } : {}),
212
- ...(failure ? { recovery: buildRecoveryState(failure.failureCode, failure.retryable) } : {}),
389
+ ...(recoveryState ? { recovery: recoveryState } : {}),
390
+ trace: buildActionTrace({
391
+ actionId,
392
+ actionType,
393
+ sourceModule,
394
+ selector,
395
+ resolved,
396
+ success,
397
+ failure,
398
+ details,
399
+ recovery: recoveryState,
400
+ attempts,
401
+ steps: traceSteps
402
+ }),
213
403
  ui_fingerprint_before: uiFingerprintBefore,
214
404
  ui_fingerprint_after: uiFingerprintAfter,
215
405
  ...(details ? { details } : {})
@@ -1,6 +1,9 @@
1
1
  import { Server } from '@modelcontextprotocol/sdk/server/index.js'
2
2
  import type { SchemaOutput } from '@modelcontextprotocol/sdk/server/zod-compat.js'
3
3
  import {
4
+ ListResourcesRequestSchema,
5
+ ListResourceTemplatesRequestSchema,
6
+ ReadResourceRequestSchema,
4
7
  ListToolsRequestSchema,
5
8
  CallToolRequestSchema
6
9
  } from '@modelcontextprotocol/sdk/types.js'
@@ -13,7 +16,7 @@ export { wrapResponse, toolDefinitions, handleToolCall }
13
16
 
14
17
  export const serverInfo = {
15
18
  name: 'mobile-debug-mcp',
16
- version: '0.27.0'
19
+ version: '0.29.0'
17
20
  }
18
21
 
19
22
  export function createServer() {
@@ -21,11 +24,24 @@ export function createServer() {
21
24
  serverInfo,
22
25
  {
23
26
  capabilities: {
27
+ resources: {},
24
28
  tools: {}
25
29
  }
26
30
  }
27
31
  )
28
32
 
33
+ server.setRequestHandler(ListResourcesRequestSchema, async () => ({
34
+ resources: []
35
+ }))
36
+
37
+ server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => ({
38
+ resourceTemplates: []
39
+ }))
40
+
41
+ server.setRequestHandler(ReadResourceRequestSchema, async () => ({
42
+ contents: []
43
+ }))
44
+
29
45
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
30
46
  tools: toolDefinitions
31
47
  }))
package/src/server.ts CHANGED
@@ -5,9 +5,11 @@ import { getSystemStatus } from './system/index.js'
5
5
 
6
6
  const server = createServer()
7
7
 
8
- getSystemStatus().then((res) => {
9
- console.debug('[startup] system status summary:', { adb: res.adbAvailable, ios: res.iosAvailable, devices: res.devices, iosDevices: res.iosDevices })
10
- }).catch((e) => console.warn('[startup] healthcheck failed:', e instanceof Error ? e.message : String(e)))
8
+ if (process.env.MOBILE_DEBUG_MCP_STARTUP_HEALTHCHECK === '1') {
9
+ getSystemStatus().then((res) => {
10
+ console.info('[startup] system status summary:', { adb: res.adbAvailable, ios: res.iosAvailable, devices: res.devices, iosDevices: res.iosDevices })
11
+ }).catch((e) => console.warn('[startup] healthcheck failed:', e instanceof Error ? e.message : String(e)))
12
+ }
11
13
 
12
14
  const transport = new StdioServerTransport()
13
15
 
package/src/types.ts CHANGED
@@ -113,6 +113,25 @@ export interface UIElementSemanticMetadata {
113
113
  state_shape?: 'continuous' | 'discrete' | 'semantic' | null;
114
114
  }
115
115
 
116
+ export type TraceStage = 'resolve' | 'execute' | 'verify' | 'stabilize' | 'recover';
117
+ export type TraceResult = 'success' | 'failure' | 'retry';
118
+
119
+ export interface TraceStep {
120
+ stage: TraceStage;
121
+ timestamp: number;
122
+ result: TraceResult;
123
+ attempt_index: number;
124
+ cycle_id?: number;
125
+ metadata?: Record<string, unknown>;
126
+ }
127
+
128
+ export interface ActionTrace {
129
+ action_id: string;
130
+ steps: TraceStep[];
131
+ final_outcome: 'success' | 'failure';
132
+ attempts: number;
133
+ }
134
+
116
135
  export type FailureClass =
117
136
  | 'TargetResolutionFailure'
118
137
  | 'ExecutionFailure'
@@ -377,6 +396,7 @@ export interface ActionExecutionResult {
377
396
  failure_code?: ActionFailureCode;
378
397
  retryable?: boolean;
379
398
  recovery?: RecoveryState;
399
+ trace: ActionTrace;
380
400
  ui_fingerprint_before?: string | null;
381
401
  ui_fingerprint_after?: string | null;
382
402
  details?: Record<string, unknown>;
@@ -400,6 +420,7 @@ export interface ExpectScreenResponse {
400
420
  matched: boolean;
401
421
  reason: string;
402
422
  };
423
+ trace: ActionTrace;
403
424
  }
404
425
 
405
426
  export interface ExpectElementVisibleResponse {
@@ -423,6 +444,7 @@ export interface ExpectElementVisibleResponse {
423
444
  reason?: string;
424
445
  failure_code?: 'TIMEOUT' | 'ELEMENT_NOT_FOUND' | 'UNKNOWN';
425
446
  retryable?: boolean;
447
+ trace: ActionTrace;
426
448
  }
427
449
 
428
450
  export interface ExpectStateResponse {
@@ -451,6 +473,7 @@ export interface ExpectStateResponse {
451
473
  stabilization_window_ms?: number;
452
474
  stable_observation_count?: number;
453
475
  snapshot_freshness_ms?: number;
476
+ trace: ActionTrace;
454
477
  }
455
478
 
456
479
  export interface AdjustControlResponse extends ActionExecutionResult {
@@ -0,0 +1,51 @@
1
+ import assert from 'assert'
2
+ import { ToolsInteract } from '../../../../src/interact/index.js'
3
+ import { ToolsObserve } from '../../../../src/observe/index.js'
4
+
5
+ async function verifyTrace(platform: 'android' | 'ios', deviceId?: string) {
6
+ const fingerprint = await ToolsObserve.getScreenFingerprintHandler({ platform, deviceId }) as any
7
+ assert.ok(fingerprint, `${platform}: missing fingerprint response`)
8
+ assert.ok(fingerprint.fingerprint || fingerprint.activity, `${platform}: missing fingerprint or activity`)
9
+
10
+ const expected = fingerprint.fingerprint
11
+ ? { fingerprint: fingerprint.fingerprint }
12
+ : { screen: fingerprint.activity }
13
+
14
+ const response = await ToolsInteract.expectScreenHandler({
15
+ platform,
16
+ deviceId,
17
+ ...expected
18
+ })
19
+
20
+ assert.strictEqual(response.success, true, `${platform}: expect_screen did not succeed`)
21
+ assert.ok(response.trace, `${platform}: trace missing`)
22
+ assert.strictEqual(response.trace.final_outcome, 'success', `${platform}: trace outcome mismatch`)
23
+ assert.ok(Array.isArray(response.trace.steps) && response.trace.steps.length > 0, `${platform}: trace steps missing`)
24
+ assert.strictEqual(response.trace.steps[0].stage, 'verify', `${platform}: expected verify stage`)
25
+
26
+ console.log(`${platform}: RFC 012 trace verified`)
27
+ }
28
+
29
+ async function main() {
30
+ const args = process.argv.slice(2)
31
+ const platform = args[0] as 'android' | 'ios' | undefined
32
+ const deviceId = args[1]
33
+
34
+ if (platform === 'android') {
35
+ await verifyTrace('android', deviceId)
36
+ return
37
+ }
38
+
39
+ if (platform === 'ios') {
40
+ await verifyTrace('ios', deviceId)
41
+ return
42
+ }
43
+
44
+ await verifyTrace('android')
45
+ await verifyTrace('ios')
46
+ }
47
+
48
+ main().catch((error) => {
49
+ console.error(error)
50
+ process.exit(1)
51
+ })
@@ -6,22 +6,23 @@ async function run() {
6
6
  console.log('Starting expect_* unit tests...')
7
7
  const originalGetScreenFingerprintHandler = (Observe as any).ToolsObserve.getScreenFingerprintHandler
8
8
  const originalGetCurrentScreenHandler = (Observe as any).ToolsObserve.getCurrentScreenHandler
9
+ const originalGetUITreeHandler = (Observe as any).ToolsObserve.getUITreeHandler
9
10
  const originalWaitForUIHandler = (ToolsInteract as any).waitForUIHandler
10
11
 
11
12
  try {
12
13
  ;(Observe as any).ToolsObserve.getScreenFingerprintHandler = async () => ({ fingerprint: 'fp_home', activity: 'com.example.HomeActivity' })
13
14
  let expectScreen = await ToolsInteract.expectScreenHandler({ platform: 'android', fingerprint: 'fp_home' })
14
- assert.deepStrictEqual(expectScreen, {
15
- success: true,
16
- observed_screen: { fingerprint: 'fp_home', screen: 'com.example.HomeActivity' },
17
- expected_screen: { fingerprint: 'fp_home', screen: null },
18
- confidence: 1,
19
- comparison: {
20
- basis: 'fingerprint',
21
- matched: true,
22
- reason: 'observed fingerprint matches expected fingerprint fp_home'
23
- }
15
+ assert.strictEqual(expectScreen.success, true)
16
+ assert.deepStrictEqual(expectScreen.observed_screen, { fingerprint: 'fp_home', screen: 'com.example.HomeActivity' })
17
+ assert.deepStrictEqual(expectScreen.expected_screen, { fingerprint: 'fp_home', screen: null })
18
+ assert.strictEqual(expectScreen.confidence, 1)
19
+ assert.deepStrictEqual(expectScreen.comparison, {
20
+ basis: 'fingerprint',
21
+ matched: true,
22
+ reason: 'observed fingerprint matches expected fingerprint fp_home'
24
23
  })
24
+ assert.strictEqual(expectScreen.trace.final_outcome, 'success')
25
+ assert.strictEqual(expectScreen.trace.steps[0].stage, 'verify')
25
26
 
26
27
  ;(Observe as any).ToolsObserve.getCurrentScreenHandler = async () => ({
27
28
  activity: 'com.example.HomeActivity',
@@ -58,6 +59,8 @@ async function run() {
58
59
  assert.strictEqual(expectElementVisible.element?.resource_id, 'rid_ready')
59
60
  assert.strictEqual(expectElementVisible.expected_condition, 'visible')
60
61
  assert.strictEqual(expectElementVisible.reason, 'selector is visible')
62
+ assert.strictEqual(expectElementVisible.trace.final_outcome, 'success')
63
+ assert.strictEqual(expectElementVisible.trace.steps[0].stage, 'verify')
61
64
 
62
65
  ;(ToolsInteract as any).waitForUIHandler = async () => ({
63
66
  status: 'timeout',
@@ -73,27 +76,56 @@ async function run() {
73
76
  selector: { text: 'Missing' },
74
77
  platform: 'android'
75
78
  })
76
- assert.deepStrictEqual(timeoutResult, {
77
- success: false,
78
- selector: { text: 'Missing' },
79
- element_id: null,
80
- expected_condition: 'visible',
81
- observed: {
82
- status: 'timeout',
83
- matched_count: 0,
84
- condition_satisfied: false,
85
- selected_index: null,
86
- last_matched_element: null
87
- },
88
- reason: 'Condition visible not satisfied within timeout; observed 0 match(es)',
89
- failure_code: 'TIMEOUT',
90
- retryable: true
79
+ assert.strictEqual(timeoutResult.success, false)
80
+ assert.deepStrictEqual(timeoutResult.selector, { text: 'Missing' })
81
+ assert.strictEqual(timeoutResult.element_id, null)
82
+ assert.strictEqual(timeoutResult.expected_condition, 'visible')
83
+ assert.deepStrictEqual(timeoutResult.observed, {
84
+ status: 'timeout',
85
+ matched_count: 0,
86
+ condition_satisfied: false,
87
+ selected_index: null,
88
+ last_matched_element: null
89
+ })
90
+ assert.strictEqual(timeoutResult.reason, 'Condition visible not satisfied within timeout; observed 0 match(es)')
91
+ assert.strictEqual(timeoutResult.failure_code, 'TIMEOUT')
92
+ assert.strictEqual(timeoutResult.retryable, true)
93
+ assert.strictEqual(timeoutResult.trace.final_outcome, 'failure')
94
+
95
+ let uiTreeCallCount = 0
96
+ ;(Observe as any).ToolsObserve.getUITreeHandler = async () => {
97
+ uiTreeCallCount++
98
+ return {
99
+ device: { platform: 'android', id: 'emulator-5554' },
100
+ captured_at_ms: Date.now(),
101
+ elements: [{
102
+ elementId: 'el_state',
103
+ text: 'Stateful',
104
+ resource_id: 'rid_state',
105
+ accessibility_id: null,
106
+ class: 'TextView',
107
+ bounds: [0, 0, 10, 10],
108
+ index: 0,
109
+ state: { enabled: true, value: 7 }
110
+ }]
111
+ }
112
+ }
113
+ const expectState = await ToolsInteract.expectStateHandler({
114
+ selector: { text: 'Stateful' },
115
+ property: 'value',
116
+ expected: 7,
117
+ platform: 'android'
91
118
  })
119
+ assert.strictEqual(expectState.success, true)
120
+ assert.strictEqual(uiTreeCallCount >= 2, true)
121
+ assert.ok(expectState.trace.steps.filter((step) => step.stage === 'stabilize').length >= 2)
122
+ assert.ok(expectState.trace.steps.filter((step) => step.stage === 'verify').length >= 2)
92
123
 
93
124
  console.log('expect_* unit tests passed')
94
125
  } finally {
95
126
  ;(Observe as any).ToolsObserve.getScreenFingerprintHandler = originalGetScreenFingerprintHandler
96
127
  ;(Observe as any).ToolsObserve.getCurrentScreenHandler = originalGetCurrentScreenHandler
128
+ ;(Observe as any).ToolsObserve.getUITreeHandler = originalGetUITreeHandler
97
129
  ;(ToolsInteract as any).waitForUIHandler = originalWaitForUIHandler
98
130
  }
99
131
  }
@@ -1,4 +1,5 @@
1
1
  import assert from 'assert'
2
+ import { createTraceStep } from '../../../src/server/common.js'
2
3
  import { buildActionExecutionResult, inferGenericFailure, requireBooleanArg } from '../../../src/server/common.js'
3
4
 
4
5
  function run() {
@@ -27,6 +28,8 @@ function run() {
27
28
  assert.strictEqual(recoveryResult.recovery?.retry_allowed, false)
28
29
  assert.strictEqual(recoveryResult.recovery?.max_recovery_attempts, 3)
29
30
  assert.strictEqual(recoveryResult.recovery?.max_retry_depth, 3)
31
+ assert.strictEqual(recoveryResult.trace.final_outcome, 'failure')
32
+ assert.strictEqual(recoveryResult.trace.steps.at(-1)?.stage, 'recover')
30
33
 
31
34
  const notInteractableResult = buildActionExecutionResult({
32
35
  actionType: 'tap',
@@ -41,6 +44,27 @@ function run() {
41
44
  assert.strictEqual(notInteractableResult.recovery?.failure_class, 'ExecutionFailure')
42
45
  assert.strictEqual(notInteractableResult.recovery?.runtime_code, 'ELEMENT_NOT_INTERACTABLE')
43
46
  assert.strictEqual(notInteractableResult.recovery?.retry_allowed, true)
47
+ assert.strictEqual(notInteractableResult.trace.steps[0].stage, 'resolve')
48
+ assert.strictEqual(notInteractableResult.trace.steps[0].result, 'failure')
49
+ assert.strictEqual(notInteractableResult.trace.steps.at(-1)?.stage, 'recover')
50
+
51
+ const traceSteps = [
52
+ createTraceStep({ stage: 'resolve', timestamp: 100, result: 'success', attemptIndex: 0 }),
53
+ createTraceStep({ stage: 'execute', timestamp: 200, result: 'retry', attemptIndex: 1 }),
54
+ createTraceStep({ stage: 'verify', timestamp: 300, result: 'success', attemptIndex: 2 })
55
+ ]
56
+ const tracedResult = buildActionExecutionResult({
57
+ actionType: 'tap',
58
+ sourceModule: 'server',
59
+ selector: { x: 1, y: 1 },
60
+ success: true,
61
+ uiFingerprintBefore: 'before',
62
+ uiFingerprintAfter: 'after',
63
+ details: { attempts: 3 },
64
+ traceSteps
65
+ })
66
+ assert.deepStrictEqual(tracedResult.trace.steps, traceSteps)
67
+ assert.strictEqual(tracedResult.trace.attempts, 3)
44
68
 
45
69
  console.log('server common tests passed')
46
70
  }