mobile-debug-mcp 0.31.0 → 0.32.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/interact/android.js +13 -8
- package/dist/interact/index.js +33 -22
- package/dist/interact/ios.js +5 -2
- package/dist/manage/android.js +20 -10
- package/dist/manage/ios.js +19 -10
- package/dist/observe/android-tree-bridge.js +192 -0
- package/dist/observe/android.js +53 -14
- package/dist/observe/index.js +22 -5
- package/dist/observe/ios.js +50 -29
- package/dist/observe/performance-metrics.js +124 -0
- package/dist/observe/snapshot-metadata.js +94 -15
- package/dist/server/common.js +14 -7
- package/dist/server/journey-schema.js +60 -0
- package/dist/server/tool-definitions.js +59 -6
- package/dist/server/tool-handlers.js +381 -56
- package/dist/server-core.js +1 -1
- package/dist/utils/exec.js +18 -5
- package/dist/utils/ios/utils.js +5 -2
- package/dist/utils/operation-budget.js +25 -0
- package/dist/utils/resolve-device.js +12 -1
- package/dist/utils/timing.js +19 -0
- package/dist/utils/ui/index.js +2 -0
- package/docs/CHANGELOG.md +6 -0
- package/docs/ROADMAP.md +9 -0
- package/docs/benchmarks/interaction-latency/README.md +26 -0
- package/docs/benchmarks/interaction-latency/final.json +1810 -0
- package/docs/benchmarks/interaction-latency/final.md +17 -0
- package/docs/benchmarks/interaction-latency/ios-validation.md +11 -0
- package/docs/benchmarks/ui-tree-performance/README.md +51 -0
- package/docs/benchmarks/ui-tree-performance/baseline.json +59546 -0
- package/docs/benchmarks/ui-tree-performance/bridge-probe.json +28 -0
- package/docs/benchmarks/ui-tree-performance/candidate.json +51691 -0
- package/docs/benchmarks/ui-tree-performance/comparison-attempt-1.json +110679 -0
- package/docs/benchmarks/ui-tree-performance/comparison.json +111230 -0
- package/docs/benchmarks/ui-tree-performance/comparison.md +41 -0
- package/docs/benchmarks/ui-tree-performance/initial-baseline.json +59560 -0
- package/docs/specs/018-interaction-latency-and-batched-journeys/spec.md +301 -0
- package/docs/specs/019-ui-tree-observation-performance/spec.md +148 -0
- package/docs/tools/interact.md +30 -0
- package/docs/tools/observe.md +1 -0
- package/package.json +1 -1
- package/skills/mobile-debug-tools-usage/SKILL.md +74 -0
- package/src/interact/android.ts +13 -8
- package/src/interact/index.ts +32 -23
- package/src/interact/ios.ts +5 -2
- package/src/manage/android.ts +19 -10
- package/src/manage/ios.ts +18 -10
- package/src/observe/android-tree-bridge.ts +170 -0
- package/src/observe/android.ts +48 -17
- package/src/observe/index.ts +21 -5
- package/src/observe/ios.ts +49 -36
- package/src/observe/performance-metrics.ts +125 -0
- package/src/observe/snapshot-metadata.ts +94 -15
- package/src/server/common.ts +13 -6
- package/src/server/journey-schema.ts +63 -0
- package/src/server/tool-definitions.ts +60 -7
- package/src/server/tool-handlers.ts +349 -54
- package/src/server-core.ts +1 -1
- package/src/types.ts +31 -2
- package/src/utils/exec.ts +13 -5
- package/src/utils/ios/utils.ts +5 -2
- package/src/utils/operation-budget.ts +22 -0
- package/src/utils/resolve-device.ts +13 -1
- package/src/utils/timing.ts +15 -0
- package/src/utils/ui/index.ts +2 -0
- package/test/device/automated/latency/benchmark.ts +136 -0
- package/test/device/automated/latency/report.ts +6 -0
- package/test/device/automated/ui-tree-performance/benchmark.ts +401 -0
- package/test/device/automated/ui-tree-performance/bridge-probe.ts +183 -0
- package/test/fixtures/latency-journey-app/AndroidManifest.xml +7 -0
- package/test/fixtures/latency-journey-app/README.md +28 -0
- package/test/fixtures/latency-journey-app/build.ts +26 -0
- package/test/fixtures/latency-journey-app/src/MainActivity.java +104 -0
- package/test/fixtures/ui-tree-bridge/AndroidManifest.xml +6 -0
- package/test/fixtures/ui-tree-bridge/build.ts +34 -0
- package/test/fixtures/ui-tree-bridge/src/TreeInstrumentation.java +119 -0
- package/test/unit/manage/restart_budget.test.ts +37 -0
- package/test/unit/observe/android_tree_acquisition.test.ts +70 -0
- package/test/unit/observe/android_tree_bridge_cleanup.test.ts +76 -0
- package/test/unit/observe/capture_debug_snapshot.test.ts +6 -0
- package/test/unit/observe/ios_tree_acquisition.test.ts +77 -0
- package/test/unit/observe/performance_metrics.test.ts +40 -0
- package/test/unit/observe/snapshot_metadata.test.ts +20 -1
- package/test/unit/observe/state_delta.test.ts +15 -0
- package/test/unit/server/benchmark_report.test.ts +4 -0
- package/test/unit/server/contract.test.ts +1 -1
- package/test/unit/server/interaction_modes.test.ts +60 -0
- package/test/unit/server/operation_budget.test.ts +45 -0
- package/test/unit/server/response_shapes.test.ts +3 -3
- package/test/unit/server/run_journey.test.ts +120 -0
- package/test/unit/server/tap_selector.test.ts +68 -0
- /package/docs/{specs-deltas/001-state-verification.md → specs/001-state-verification/spec.md} +0 -0
- /package/docs/{specs-deltas/002-richer-element-identity → specs/002-richer-element-identity/spec.md} +0 -0
- /package/docs/{specs-deltas/003-wait-and-synchronization-reliability.md → specs/003-wait-and-synchronization-reliability/spec.md} +0 -0
- /package/docs/{specs-deltas/004-action-verification-routing.md → specs/004-action-verification-routing/spec.md} +0 -0
- /package/docs/{specs-deltas/005-unified-action-execution-and-verification-model.md → specs/005-unified-action-execution-and-verification-model/spec.md} +0 -0
- /package/docs/{specs-deltas/006-runtime-action-instrumentation-and-binding-layer.md → specs/006-runtime-action-instrumentation-and-binding-layer/spec.md} +0 -0
- /package/docs/{specs-deltas/007-actionability-resolution-and-executable-target-selection.md → specs/007-actionability-resolution-and-executable-target-selection/spec.md} +0 -0
- /package/docs/{specs-deltas/008-adjustable-control-support-and-semantic-value-manipulation.md → specs/008-adjustable-control-support-and-semantic-value-manipulation/spec.md} +0 -0
- /package/docs/{specs-deltas/009-semantic-control-modeling-for-custom-and-composite-controls.md → specs/009-semantic-control-modeling-for-custom-and-composite-controls/spec.md} +0 -0
- /package/docs/{specs-deltas/010-verification-stabilization-and-temporal-convergence.md → specs/010-verification-stabilization-and-temporal-convergence/spec.md} +0 -0
- /package/docs/{specs-deltas/011-recovery-and-replanning-for-failed-or-ambiguous-interaction-flows.md → specs/011-recovery-and-replanning-for-failed-or-ambiguous-interaction-flows/spec.md} +0 -0
- /package/docs/{specs-deltas/011.1-recovery-contract-types-and-runtime-wiring-spec.md → specs/011.1-recovery-contract-types-and-runtime-wiring-spec/spec.md} +0 -0
- /package/docs/{specs-deltas/012-action-trace-and-xecution-observability.md → specs/012-action-trace-and-xecution-observability/spec.md} +0 -0
- /package/docs/{specs-deltas/013-wait-and-synchronization-reliability.md → specs/013-wait-and-synchronization-reliability/spec.md} +0 -0
- /package/docs/{specs-deltas/014-actionability-resolution.md → specs/014-actionability-resolution/spec.md} +0 -0
- /package/docs/{specs-deltas/015-adjustable-control-support.md → specs/015-adjustable-control-support/spec.md} +0 -0
- /package/docs/{specs-deltas/016-environment-tools.md → specs/016-environment-tools/spec.md} +0 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { handleToolCall } from '../../../src/server-core.js'
|
|
3
|
+
import { ToolsInteract } from '../../../src/interact/index.js'
|
|
4
|
+
import { ToolsObserve } from '../../../src/observe/index.js'
|
|
5
|
+
import { AndroidManage } from '../../../src/manage/android.js'
|
|
6
|
+
import { markDispatchStarted } from '../../../src/utils/operation-budget.js'
|
|
7
|
+
|
|
8
|
+
async function run() {
|
|
9
|
+
const treeOriginal = ToolsObserve.getUITreeHandler
|
|
10
|
+
const fingerprintOriginal = ToolsObserve.getScreenFingerprintHandler
|
|
11
|
+
const tapOriginal = ToolsInteract.tapHandler
|
|
12
|
+
const launchOriginal = AndroidManage.prototype.startApp
|
|
13
|
+
const device: any = { platform: 'android', id: 'fixture' }
|
|
14
|
+
let dispatched = false
|
|
15
|
+
let postReads = 0
|
|
16
|
+
let fingerprints = 0
|
|
17
|
+
ToolsObserve.getUITreeHandler = async () => {
|
|
18
|
+
if (dispatched) postReads++
|
|
19
|
+
return { device, elements: [{ text: 'Go', stable_id: 'go', bounds: [0, 0, 40, 40], visible: true, enabled: true, clickable: true }] } as any
|
|
20
|
+
}
|
|
21
|
+
ToolsObserve.getScreenFingerprintHandler = async () => { fingerprints++; return { fingerprint: 'fixture' } as any }
|
|
22
|
+
ToolsInteract.tapHandler = async () => { dispatched = true; return { success: true, device } as any }
|
|
23
|
+
AndroidManage.prototype.startApp = async () => { dispatched = true; return { appStarted: true, device } as any }
|
|
24
|
+
try {
|
|
25
|
+
for (const tool of ['tap_element', 'start_app']) {
|
|
26
|
+
for (const mode of ['none', 'light', 'full']) {
|
|
27
|
+
dispatched = false; postReads = 0; fingerprints = 0
|
|
28
|
+
const response = await handleToolCall(tool, { platform: 'android', deviceId: 'fixture', ...(tool === 'tap_element' ? { selector: { text: 'Go' } } : { appId: 'fixture' }), verificationMode: mode, responseMode: 'compact' })
|
|
29
|
+
const result = JSON.parse(response.content[0].text!)
|
|
30
|
+
assert.equal(result.success, true, JSON.stringify(result))
|
|
31
|
+
assert.equal(postReads, mode === 'light' ? 1 : 0)
|
|
32
|
+
assert.equal(fingerprints, mode === 'full' ? 2 : 0)
|
|
33
|
+
assert.equal(result.device, undefined)
|
|
34
|
+
assert.equal(result.trace, undefined)
|
|
35
|
+
assert.equal(result.dispatch_started, true)
|
|
36
|
+
assert.deepEqual(Object.keys(result.timing), ['total_ms'])
|
|
37
|
+
if (mode !== 'full') assert.equal(result.lifecycle_state, 'pending_verification')
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
ToolsObserve.getUITreeHandler = async () => { throw new Error('offline') }
|
|
41
|
+
const result = JSON.parse((await handleToolCall('start_app', { platform: 'android', appId: 'fixture', verificationMode: 'light', responseMode: 'compact' })).content[0].text!)
|
|
42
|
+
assert.equal(result.success, true)
|
|
43
|
+
assert.equal(result.verification.status, 'unavailable')
|
|
44
|
+
assert.equal(result.verification_diagnostic.retryable, true)
|
|
45
|
+
ToolsInteract.tapHandler = async () => { markDispatchStarted(); dispatched = true; return { success: false, error: 'ACTION_TIMEOUT', device } as any }
|
|
46
|
+
const timedOut = JSON.parse((await handleToolCall('tap', { platform: 'android', deviceId: 'fixture', x: 1, y: 1, verificationMode: 'none', responseMode: 'compact' })).content[0].text!)
|
|
47
|
+
assert.equal(timedOut.failure_code, 'ACTION_TIMEOUT')
|
|
48
|
+
assert.equal(timedOut.delivery_status, 'unknown')
|
|
49
|
+
assert.equal(timedOut.retryable, false)
|
|
50
|
+
AndroidManage.prototype.startApp = async () => ({ appStarted: false, device, error: 'launch failed', diagnostics: { runResult: { exitCode: 1, stdout: '', stderr: 'launch failed' } } } as any)
|
|
51
|
+
const failedLaunch = JSON.parse((await handleToolCall('start_app', { platform: 'android', appId: 'fixture' })).content[0].text!)
|
|
52
|
+
assert.equal(failedLaunch.details.diagnostics.runResult.exitCode, 1)
|
|
53
|
+
} finally {
|
|
54
|
+
ToolsObserve.getUITreeHandler = treeOriginal
|
|
55
|
+
ToolsObserve.getScreenFingerprintHandler = fingerprintOriginal
|
|
56
|
+
ToolsInteract.tapHandler = tapOriginal
|
|
57
|
+
AndroidManage.prototype.startApp = launchOriginal
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
run().catch(error => { console.error(error); process.exitCode = 1 })
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { existsSync, mkdtempSync, rmSync } from 'node:fs'
|
|
3
|
+
import { tmpdir } from 'node:os'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import { runWithBudget, remainingBudget, markDispatchStarted } from '../../../src/utils/operation-budget.js'
|
|
6
|
+
import { execCmd } from '../../../src/utils/exec.js'
|
|
7
|
+
import { ToolsInteract } from '../../../src/interact/index.js'
|
|
8
|
+
import { ToolsObserve } from '../../../src/observe/index.js'
|
|
9
|
+
|
|
10
|
+
async function run() {
|
|
11
|
+
const markerDir = mkdtempSync(path.join(tmpdir(), 'mcp-exec-close-'))
|
|
12
|
+
try {
|
|
13
|
+
const marker = path.join(markerDir, 'closed')
|
|
14
|
+
const command = `process.on('SIGTERM', () => setTimeout(() => { require('fs').writeFileSync(${JSON.stringify(marker)}, 'closed'); process.exit(0) }, 50)); setInterval(() => {}, 1000)`
|
|
15
|
+
const response = await execCmd(process.execPath, ['-e', command], { timeout: 250 })
|
|
16
|
+
assert.equal(response.stderr, 'ACTION_TIMEOUT')
|
|
17
|
+
assert.equal(existsSync(marker), true, 'timeout must await child close before returning')
|
|
18
|
+
} finally {
|
|
19
|
+
rmSync(markerDir, { recursive: true, force: true })
|
|
20
|
+
}
|
|
21
|
+
await runWithBudget(100, async budget => {
|
|
22
|
+
assert.equal(budget.dispatchStarted, false)
|
|
23
|
+
markDispatchStarted()
|
|
24
|
+
assert.equal(budget.dispatchStarted, true)
|
|
25
|
+
const response = await execCmd(process.execPath, ['-e', 'setTimeout(() => {}, 10000)'])
|
|
26
|
+
assert.equal(response.stderr, 'ACTION_TIMEOUT')
|
|
27
|
+
assert.throws(() => remainingBudget(), /ACTION_TIMEOUT/)
|
|
28
|
+
})
|
|
29
|
+
await runWithBudget(1, async budget => {
|
|
30
|
+
await new Promise(resolve => setTimeout(resolve, 5))
|
|
31
|
+
assert.throws(() => markDispatchStarted(), /ACTION_TIMEOUT/)
|
|
32
|
+
assert.equal(budget.dispatchStarted, false)
|
|
33
|
+
})
|
|
34
|
+
const originalTree = ToolsObserve.getUITreeHandler
|
|
35
|
+
ToolsObserve.getUITreeHandler = async () => { throw new Error('transient observation error') }
|
|
36
|
+
try {
|
|
37
|
+
await assert.rejects(
|
|
38
|
+
() => runWithBudget(30, () => ToolsInteract.waitForUIHandler({ selector: { text: 'missing' }, timeout_ms: 1000, poll_interval_ms: 5, platform: 'android', deviceId: 'fixture' })),
|
|
39
|
+
/ACTION_TIMEOUT/
|
|
40
|
+
)
|
|
41
|
+
} finally {
|
|
42
|
+
ToolsObserve.getUITreeHandler = originalTree
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
run().catch(error => { console.error(error); process.exitCode = 1 })
|
|
@@ -37,7 +37,7 @@ async function run() {
|
|
|
37
37
|
|
|
38
38
|
const missingBuildResponse = await handleToolCall('build_app', { projectPath: '/tmp/project' })
|
|
39
39
|
const missingBuildPayload = JSON.parse((missingBuildResponse as any).content[0].text)
|
|
40
|
-
assert.deepStrictEqual(missingBuildPayload, {
|
|
40
|
+
assert.deepStrictEqual({ error: missingBuildPayload.error }, {
|
|
41
41
|
error: {
|
|
42
42
|
tool: 'build_app',
|
|
43
43
|
message: 'Missing or invalid string argument: platform'
|
|
@@ -236,7 +236,7 @@ async function run() {
|
|
|
236
236
|
const failingTapResponse = await handleToolCall('tap', { platform: 'android', x: 1, y: 2 })
|
|
237
237
|
assert.strictEqual((failingTapResponse as any).content.length, 1)
|
|
238
238
|
const failingTapPayload = JSON.parse((failingTapResponse as any).content[0].text)
|
|
239
|
-
assert.deepStrictEqual(failingTapPayload, {
|
|
239
|
+
assert.deepStrictEqual({ error: failingTapPayload.error }, {
|
|
240
240
|
error: {
|
|
241
241
|
tool: 'tap',
|
|
242
242
|
message: 'boom'
|
|
@@ -255,7 +255,7 @@ async function run() {
|
|
|
255
255
|
|
|
256
256
|
const missingArgResponse = await handleToolCall('tap', { platform: 'android', x: 1 })
|
|
257
257
|
const missingArgPayload = JSON.parse((missingArgResponse as any).content[0].text)
|
|
258
|
-
assert.deepStrictEqual(missingArgPayload, {
|
|
258
|
+
assert.deepStrictEqual({ error: missingArgPayload.error }, {
|
|
259
259
|
error: {
|
|
260
260
|
tool: 'tap',
|
|
261
261
|
message: 'Missing or invalid number argument: y'
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { _setDeviceListersForTests, _resetDeviceListersForTests } from '../../../src/utils/resolve-device.js'
|
|
2
|
+
import assert from 'assert'
|
|
3
|
+
import { handleToolCall } from '../../../src/server-core.js'
|
|
4
|
+
import { ToolsInteract } from '../../../src/interact/index.js'
|
|
5
|
+
import { ToolsObserve } from '../../../src/observe/index.js'
|
|
6
|
+
|
|
7
|
+
async function run() {
|
|
8
|
+
const originalWait = (ToolsInteract as any).waitForUIHandler
|
|
9
|
+
const originalExpectVisible = (ToolsInteract as any).expectElementVisibleHandler
|
|
10
|
+
const originalTap = (ToolsInteract as any).tapElementHandler
|
|
11
|
+
const originalTree = (ToolsObserve as any).getUITreeHandler
|
|
12
|
+
let resolutions = 0
|
|
13
|
+
_setDeviceListersForTests({ listAndroidDevices: async () => { resolutions++; return [{ id: 'fixture', platform: 'android' } as any] } })
|
|
14
|
+
try {
|
|
15
|
+
const calls: string[] = []
|
|
16
|
+
;(ToolsInteract as any).waitForUIHandler = async () => {
|
|
17
|
+
calls.push('wait')
|
|
18
|
+
return { status: 'success', element: { elementId: 'ready' } }
|
|
19
|
+
}
|
|
20
|
+
;(ToolsInteract as any).expectElementVisibleHandler = async () => {
|
|
21
|
+
calls.push('assert')
|
|
22
|
+
return { success: false, failure_code: 'TIMEOUT' }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const response: any = await handleToolCall('run_journey', {
|
|
26
|
+
platform: 'android',
|
|
27
|
+
steps: [
|
|
28
|
+
{ id: 'wait-ready', type: 'wait', selector: { text: 'Ready' } },
|
|
29
|
+
{ id: 'assert-ready', type: 'assert', assertion: { kind: 'element_visible', selector: { text: 'Ready' } } },
|
|
30
|
+
{ id: 'not-run', type: 'wait', selector: { text: 'Later' } }
|
|
31
|
+
]
|
|
32
|
+
})
|
|
33
|
+
const result = JSON.parse(response.content[0].text)
|
|
34
|
+
assert.strictEqual(resolutions, 1)
|
|
35
|
+
assert.deepStrictEqual(calls, ['wait', 'assert'])
|
|
36
|
+
await handleToolCall('run_journey', { platform: 'android', steps: [{ id: 'valid', type: 'wait', selector: { text: 'Ready' } }, { id: 'bad', type: 'tap' }] })
|
|
37
|
+
assert.strictEqual(resolutions, 1, 'Invalid later steps must fail before device work')
|
|
38
|
+
assert.deepStrictEqual(calls, ['wait', 'assert'])
|
|
39
|
+
assert.strictEqual(result.success, false)
|
|
40
|
+
assert.strictEqual(result.stopped_at_step_id, 'assert-ready')
|
|
41
|
+
assert.strictEqual(result.steps[0].status, 'passed')
|
|
42
|
+
assert.strictEqual(result.steps[1].status, 'failed')
|
|
43
|
+
assert.deepStrictEqual(result.steps[2], {
|
|
44
|
+
id: 'not-run', type: 'wait', status: 'not_run', success: false,
|
|
45
|
+
reason: 'PREVIOUS_STEP_FAILED', result: null, timing: { total_ms: 0 }
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
let postActionReads = 0
|
|
49
|
+
;(ToolsObserve as any).getUITreeHandler = async () => { postActionReads++; throw new Error('Unexpected duplicate observation') }
|
|
50
|
+
;(ToolsInteract as any).waitForUIHandler = async () => ({ status: 'success', element: { elementId: 'target' }, _tree: {} })
|
|
51
|
+
;(ToolsInteract as any).tapElementHandler = async (args: any) => {
|
|
52
|
+
assert.strictEqual(args.verificationMode, 'none')
|
|
53
|
+
return { success: true, action_id: 'tap', action_type: 'tap_element' }
|
|
54
|
+
}
|
|
55
|
+
;(ToolsInteract as any).expectElementVisibleHandler = async () => ({ success: true, observed: { matched_count: 1 } })
|
|
56
|
+
const lightResponse: any = await handleToolCall('run_journey', {
|
|
57
|
+
platform: 'android', responseMode: 'compact', defaults: { verificationMode: 'light' },
|
|
58
|
+
steps: [
|
|
59
|
+
{ id: 'tap-target', type: 'tap', selector: { accessibility_id: 'target' } },
|
|
60
|
+
{ id: 'assert-result', type: 'assert', assertion: { kind: 'element_visible', selector: { accessibility_id: 'result' } } }
|
|
61
|
+
]
|
|
62
|
+
})
|
|
63
|
+
const light = JSON.parse(lightResponse.content[0].text)
|
|
64
|
+
assert.strictEqual(light.success, true)
|
|
65
|
+
assert.deepStrictEqual(Object.keys(light.timing), ['total_ms'])
|
|
66
|
+
assert.deepStrictEqual(light.steps[0].result.verification, { mode: 'light', status: 'observed' })
|
|
67
|
+
assert.strictEqual(light.steps[0].result.verification_diagnostic, undefined)
|
|
68
|
+
assert.strictEqual(postActionReads, 0, 'The following assertion supplies the fresh observation')
|
|
69
|
+
;(ToolsInteract as any).expectElementVisibleHandler = async () => ({ success: false, failure_code: 'TIMEOUT', observed: { matched_count: 0 } })
|
|
70
|
+
const failedAssertionResponse: any = await handleToolCall('run_journey', {
|
|
71
|
+
platform: 'android', responseMode: 'compact', defaults: { verificationMode: 'light' },
|
|
72
|
+
steps: [
|
|
73
|
+
{ id: 'tap-target', type: 'tap', selector: { accessibility_id: 'target' } },
|
|
74
|
+
{ id: 'assert-result', type: 'assert', assertion: { kind: 'element_visible', selector: { accessibility_id: 'result' } } }
|
|
75
|
+
]
|
|
76
|
+
})
|
|
77
|
+
const failedAssertion = JSON.parse(failedAssertionResponse.content[0].text)
|
|
78
|
+
assert.strictEqual(failedAssertion.success, false)
|
|
79
|
+
assert.deepStrictEqual(failedAssertion.steps[0].result.verification, { mode: 'light', status: 'observed' })
|
|
80
|
+
assert.strictEqual(postActionReads, 0)
|
|
81
|
+
|
|
82
|
+
;(ToolsInteract as any).waitForUIHandler = originalWait
|
|
83
|
+
;(ToolsInteract as any).expectElementVisibleHandler = originalExpectVisible
|
|
84
|
+
let dispatchedAt = 0
|
|
85
|
+
const readTimes: number[] = []
|
|
86
|
+
;(ToolsObserve as any).getUITreeHandler = async () => {
|
|
87
|
+
readTimes.push(performance.now())
|
|
88
|
+
const target = readTimes.length === 1
|
|
89
|
+
return {
|
|
90
|
+
device: { platform: 'android', id: 'fixture' },
|
|
91
|
+
captured_at_ms: Date.now(),
|
|
92
|
+
elements: [{ contentDescription: target ? 'target' : 'result', bounds: [0, 0, 20, 20], visible: true, enabled: true, clickable: true }]
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
;(ToolsInteract as any).tapElementHandler = async () => {
|
|
96
|
+
dispatchedAt = performance.now()
|
|
97
|
+
return { success: true, action_id: 'tap', action_type: 'tap_element' }
|
|
98
|
+
}
|
|
99
|
+
const freshResponse: any = await handleToolCall('run_journey', {
|
|
100
|
+
platform: 'android', responseMode: 'compact', defaults: { verificationMode: 'light' },
|
|
101
|
+
steps: [
|
|
102
|
+
{ id: 'tap-target', type: 'tap', selector: { accessibility_id: 'target' } },
|
|
103
|
+
{ id: 'assert-result', type: 'assert', assertion: { kind: 'element_visible', selector: { accessibility_id: 'result' } } }
|
|
104
|
+
]
|
|
105
|
+
})
|
|
106
|
+
const fresh = JSON.parse(freshResponse.content[0].text)
|
|
107
|
+
assert.strictEqual(fresh.success, true)
|
|
108
|
+
assert.strictEqual(readTimes.length, 3, 'target selection and two stability samples require separate reads')
|
|
109
|
+
assert.ok(readTimes[0] < dispatchedAt)
|
|
110
|
+
assert.ok(readTimes[1] >= dispatchedAt && readTimes[2] >= dispatchedAt, 'assertion reads must follow dispatch')
|
|
111
|
+
} finally {
|
|
112
|
+
_resetDeviceListersForTests()
|
|
113
|
+
;(ToolsInteract as any).waitForUIHandler = originalWait
|
|
114
|
+
;(ToolsInteract as any).expectElementVisibleHandler = originalExpectVisible
|
|
115
|
+
;(ToolsInteract as any).tapElementHandler = originalTap
|
|
116
|
+
;(ToolsObserve as any).getUITreeHandler = originalTree
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
run().catch((error) => { console.error(error); process.exit(1) })
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { handleToolCall } from '../../../src/server-core.js'
|
|
3
|
+
import { ToolsInteract } from '../../../src/interact/index.js'
|
|
4
|
+
|
|
5
|
+
async function run() {
|
|
6
|
+
const originalWait = ToolsInteract.waitForUIHandler
|
|
7
|
+
const originalTap = ToolsInteract.tapElementHandler
|
|
8
|
+
let tapCount = 0
|
|
9
|
+
let lastWait: any
|
|
10
|
+
let resolution: any
|
|
11
|
+
ToolsInteract.waitForUIHandler = async (args: any) => { lastWait = args; return resolution }
|
|
12
|
+
ToolsInteract.tapElementHandler = async (args: any) => {
|
|
13
|
+
tapCount++
|
|
14
|
+
assert.equal(args.elementId, 'selected')
|
|
15
|
+
assert.deepEqual(args.freshTree, { elements: [] })
|
|
16
|
+
return { action_id: 'tap', action_type: 'tap_element', success: true, lifecycle_state: 'pending_verification' } as any
|
|
17
|
+
}
|
|
18
|
+
try {
|
|
19
|
+
const selector = { accessibility_id: 'save' }
|
|
20
|
+
resolution = { status: 'success', element: { elementId: 'selected' }, _tree: { elements: [] } }
|
|
21
|
+
const success = JSON.parse((await handleToolCall('tap_element', {
|
|
22
|
+
selector, platform: 'android', verificationMode: 'none', responseMode: 'compact'
|
|
23
|
+
})).content[0].text!)
|
|
24
|
+
assert.equal(success.success, true)
|
|
25
|
+
assert.equal(tapCount, 1)
|
|
26
|
+
assert.equal(lastWait.condition, 'clickable')
|
|
27
|
+
assert.equal(lastWait.singleObservation, true)
|
|
28
|
+
assert.equal(lastWait.singleAttempt, true)
|
|
29
|
+
assert.equal(lastWait.rejectAmbiguous, true)
|
|
30
|
+
|
|
31
|
+
await handleToolCall('tap_element', {
|
|
32
|
+
selector, waitFor: { condition: 'visible', timeoutMs: 700, pollIntervalMs: 80 },
|
|
33
|
+
platform: 'android', verificationMode: 'none', responseMode: 'compact'
|
|
34
|
+
})
|
|
35
|
+
assert.equal(lastWait.condition, 'visible')
|
|
36
|
+
assert.equal(lastWait.timeout_ms, 700)
|
|
37
|
+
assert.equal(lastWait.poll_interval_ms, 80)
|
|
38
|
+
assert.equal(lastWait.singleAttempt, false)
|
|
39
|
+
assert.equal(lastWait.rejectAmbiguous, false)
|
|
40
|
+
assert.equal(tapCount, 2)
|
|
41
|
+
|
|
42
|
+
resolution = { status: 'timeout', error: { code: 'AMBIGUOUS_TARGET' } }
|
|
43
|
+
const ambiguous = JSON.parse((await handleToolCall('tap_element', {
|
|
44
|
+
selector, platform: 'android', verificationMode: 'none', responseMode: 'compact'
|
|
45
|
+
})).content[0].text!)
|
|
46
|
+
assert.equal(ambiguous.failure_code, 'AMBIGUOUS_TARGET')
|
|
47
|
+
assert.equal(tapCount, 2)
|
|
48
|
+
|
|
49
|
+
resolution = { status: 'timeout', error: { code: 'ELEMENT_NOT_FOUND' } }
|
|
50
|
+
const missing = JSON.parse((await handleToolCall('tap_element', {
|
|
51
|
+
selector, platform: 'android', verificationMode: 'none', responseMode: 'compact'
|
|
52
|
+
})).content[0].text!)
|
|
53
|
+
assert.equal(missing.failure_code, 'ELEMENT_NOT_FOUND')
|
|
54
|
+
assert.equal(missing.dispatch_started, false)
|
|
55
|
+
assert.equal(missing.retryable, true)
|
|
56
|
+
assert.equal(tapCount, 2)
|
|
57
|
+
|
|
58
|
+
const invalid = await handleToolCall('tap_element', {
|
|
59
|
+
elementId: 'old', selector, waitFor: {}, platform: 'android'
|
|
60
|
+
})
|
|
61
|
+
assert.match(JSON.parse(invalid.content[0].text!).error.message, /Invalid input/)
|
|
62
|
+
assert.equal(tapCount, 2)
|
|
63
|
+
} finally {
|
|
64
|
+
ToolsInteract.waitForUIHandler = originalWait
|
|
65
|
+
ToolsInteract.tapElementHandler = originalTap
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
run().catch(error => { console.error(error); process.exitCode = 1 })
|
/package/docs/{specs-deltas/001-state-verification.md → specs/001-state-verification/spec.md}
RENAMED
|
File without changes
|
/package/docs/{specs-deltas/002-richer-element-identity → specs/002-richer-element-identity/spec.md}
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|