@swmansion/argent 0.31.2-next.1 → 0.31.2-next.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/argent-android-devtools-0.3.0.apk +0 -0
- package/bin/darwin/ax-service +0 -0
- package/bin/darwin/tvos-ax-service +0 -0
- package/bin/darwin/tvos-hid-daemon +0 -0
- package/bin/network-inspector/arm64-v8a/libjvmti_network_inspector.so +0 -0
- package/bin/network-inspector/network-inspector.jar +0 -0
- package/bin/network-inspector/x86_64/libjvmti_network_inspector.so +0 -0
- package/bin/tcp/ax-service +0 -0
- package/dist/cli-cmds.mjs +106 -24
- package/dist/client.js +83 -14
- package/dist/installer.mjs +25 -1
- package/dist/ios-device-runner/ArgentRunner/ArgentRunnerUITests/ArgentExceptionGuard.h +3 -0
- package/dist/ios-device-runner/ArgentRunner/ArgentRunnerUITests/ArgentExceptionGuard.m +11 -3
- package/dist/ios-device-runner/ArgentRunner/ArgentRunnerUITests/ArgentRunnerSession+Gestures.swift +27 -19
- package/dist/ios-device-runner/ArgentRunner/ArgentRunnerUITests/ArgentRunnerSession+Quiescence.swift +95 -0
- package/dist/ios-device-runner/ArgentRunner/ArgentRunnerUITests/ArgentRunnerSession+TextEntry.swift +2 -2
- package/dist/ios-device-runner/ArgentRunner/ArgentRunnerUITests/RunnerProtocol.swift +5 -5
- package/dist/mcp-server.mjs +87 -17
- package/dist/tool-args.generated.d.ts +26 -14
- package/dist/tool-server.cjs +4604 -1860
- package/dylibs/libArgentInjectionBootstrap.dylib +0 -0
- package/dylibs/libKeyboardPatch.dylib +0 -0
- package/dylibs/libNativeDevtoolsIos.dylib +0 -0
- package/dylibs/tcp/libArgentInjectionBootstrap.dylib +0 -0
- package/dylibs/tcp/libKeyboardPatch.dylib +0 -0
- package/dylibs/tcp/libNativeDevtoolsIos.dylib +0 -0
- package/dylibs/tvos/libArgentInjectionBootstrap.dylib +0 -0
- package/dylibs/tvos/libKeyboardPatch.dylib +0 -0
- package/dylibs/tvos/libNativeDevtoolsIos.dylib +0 -0
- package/package.json +1 -1
- package/skills/argent-create-e2e/references/live-authoring.md +3 -3
- package/skills/argent-ios-device-setup/SKILL.md +1 -1
package/dist/ios-device-runner/ArgentRunner/ArgentRunnerUITests/ArgentRunnerSession+Gestures.swift
CHANGED
|
@@ -21,10 +21,10 @@ extension ArgentRunnerSession {
|
|
|
21
21
|
|
|
22
22
|
/// Taps at the wire coordinates: one tap, the native double-tap when
|
|
23
23
|
/// `numberOfTaps` is 2, or a loop of single taps above that. XCUICoordinate
|
|
24
|
-
/// has no N-tap API, and the loop is not one gesture
|
|
25
|
-
///
|
|
26
|
-
///
|
|
27
|
-
///
|
|
24
|
+
/// has no N-tap API, and the loop is not one gesture: each tap is its own
|
|
25
|
+
/// synthesized event and XPC round trip, about 300 ms apart on a
|
|
26
|
+
/// simulator, which a double-tap recognizer can still pair. That is
|
|
27
|
+
/// documented, not refused.
|
|
28
28
|
func performTap(_ request: CommandRequest, on app: XCUIApplication)
|
|
29
29
|
-> Envelope
|
|
30
30
|
{
|
|
@@ -39,15 +39,17 @@ extension ArgentRunnerSession {
|
|
|
39
39
|
|
|
40
40
|
let coordinate = point(app, x, y, relativeTo: app.frame.origin)
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
case 2:
|
|
46
|
-
coordinate.doubleTap()
|
|
47
|
-
default:
|
|
48
|
-
// Separate taps, not one gesture (see the doc comment above).
|
|
49
|
-
for _ in 0..<taps {
|
|
42
|
+
skippingQuiescence(of: app) {
|
|
43
|
+
switch taps {
|
|
44
|
+
case 1:
|
|
50
45
|
coordinate.tap()
|
|
46
|
+
case 2:
|
|
47
|
+
coordinate.doubleTap()
|
|
48
|
+
default:
|
|
49
|
+
// Separate taps, not one gesture (see the doc comment above).
|
|
50
|
+
for _ in 0..<taps {
|
|
51
|
+
coordinate.tap()
|
|
52
|
+
}
|
|
51
53
|
}
|
|
52
54
|
}
|
|
53
55
|
|
|
@@ -63,7 +65,11 @@ extension ArgentRunnerSession {
|
|
|
63
65
|
}
|
|
64
66
|
|
|
65
67
|
let seconds = max(0.05, (request.durationMs ?? 800) / 1000)
|
|
66
|
-
point(app, x, y, relativeTo: app.frame.origin)
|
|
68
|
+
let coordinate = point(app, x, y, relativeTo: app.frame.origin)
|
|
69
|
+
|
|
70
|
+
skippingQuiescence(of: app) {
|
|
71
|
+
coordinate.press(forDuration: seconds)
|
|
72
|
+
}
|
|
67
73
|
|
|
68
74
|
return .success(MessagePayload(message: "long-pressed"))
|
|
69
75
|
}
|
|
@@ -112,12 +118,14 @@ extension ArgentRunnerSession {
|
|
|
112
118
|
velocity = .default
|
|
113
119
|
}
|
|
114
120
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
+
skippingQuiescence(of: app) {
|
|
122
|
+
start.press(
|
|
123
|
+
forDuration: startHold,
|
|
124
|
+
thenDragTo: end,
|
|
125
|
+
withVelocity: velocity,
|
|
126
|
+
thenHoldForDuration: endHold
|
|
127
|
+
)
|
|
128
|
+
}
|
|
121
129
|
|
|
122
130
|
return .success(MessagePayload(message: "dragged"))
|
|
123
131
|
}
|
package/dist/ios-device-runner/ArgentRunner/ArgentRunnerUITests/ArgentRunnerSession+Quiescence.swift
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import XCTest
|
|
2
|
+
|
|
3
|
+
/// XCTest waits up to 60s before and after every synthesized event for the
|
|
4
|
+
/// app to report idle. A screen that never does, such as one holding a paused
|
|
5
|
+
/// UIViewPropertyAnimator (expo-blur), stalls each gesture for two minutes,
|
|
6
|
+
/// past its watchdog budget. The runner skips those waits and runs XCTest's
|
|
7
|
+
/// idle check itself under a short cap. Every API here is XCTest SPI, probed
|
|
8
|
+
/// before use: when any is missing, the event runs with XCTest's own waits.
|
|
9
|
+
extension ArgentRunnerSession {
|
|
10
|
+
/// The longest a gesture waits for the app to go idle before its event.
|
|
11
|
+
/// Covers a navigation transition, so a gesture sent right after the one
|
|
12
|
+
/// that started it lands on the new screen instead of mid-transition.
|
|
13
|
+
static let preEventSettleTimeout: TimeInterval = 2
|
|
14
|
+
|
|
15
|
+
private static let performWithInteractionOptions = NSSelectorFromString(
|
|
16
|
+
"_performWithInteractionOptions:block:"
|
|
17
|
+
)
|
|
18
|
+
/// Bit 1 skips the pre-event wait, bit 2 the post-event one.
|
|
19
|
+
private static let skipQuiescenceOptions: UInt32 = 0b11
|
|
20
|
+
|
|
21
|
+
private static let applicationImpl = NSSelectorFromString("applicationImpl")
|
|
22
|
+
private static let currentProcess = NSSelectorFromString("currentProcess")
|
|
23
|
+
private static let initiateQuiescenceChecks = NSSelectorFromString(
|
|
24
|
+
"_initiateQuiescenceChecksIncludingAnimationsIdle:"
|
|
25
|
+
)
|
|
26
|
+
private static let makeQuiescenceExpectation = NSSelectorFromString(
|
|
27
|
+
"_makeQuiescenceExpectation"
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
/// Waits at most `preEventSettleTimeout` for `app` to go idle, then runs
|
|
31
|
+
/// `body`, which synthesizes events into it, without XCTest's waits.
|
|
32
|
+
func skippingQuiescence(of app: XCUIApplication, _ body: () -> Void) {
|
|
33
|
+
let selector = Self.performWithInteractionOptions
|
|
34
|
+
|
|
35
|
+
guard app.responds(to: selector),
|
|
36
|
+
awaitQuiescence(of: app, timeout: Self.preEventSettleTimeout)
|
|
37
|
+
else {
|
|
38
|
+
body()
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
typealias Perform = @convention(c) (
|
|
43
|
+
NSObject, Selector, UInt32, @convention(block) () -> Void
|
|
44
|
+
) -> Void
|
|
45
|
+
let perform = unsafeBitCast(app.method(for: selector), to: Perform.self)
|
|
46
|
+
|
|
47
|
+
// XCTest restores the prior options only when the block returns, and
|
|
48
|
+
// every XCUIApplication for this bundle id shares them, so an
|
|
49
|
+
// exception must not unwind through it.
|
|
50
|
+
var exception: NSException?
|
|
51
|
+
withoutActuallyEscaping(body) { body in
|
|
52
|
+
perform(app, selector, Self.skipQuiescenceOptions) {
|
|
53
|
+
exception = ArgentExceptionGuard.exception(from: body)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
exception?.raise()
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/// XCTest's own idle check (event loop and animations), under `timeout`.
|
|
60
|
+
/// False when any SPI it needs is missing.
|
|
61
|
+
private func awaitQuiescence(of app: XCUIApplication, timeout: TimeInterval)
|
|
62
|
+
-> Bool
|
|
63
|
+
{
|
|
64
|
+
guard
|
|
65
|
+
let impl = Self.object(app, Self.applicationImpl),
|
|
66
|
+
let process = Self.object(impl, Self.currentProcess),
|
|
67
|
+
process.responds(to: Self.initiateQuiescenceChecks)
|
|
68
|
+
else {
|
|
69
|
+
return false
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
typealias Initiate = @convention(c) (NSObject, Selector, Bool) -> Void
|
|
73
|
+
let initiate = unsafeBitCast(
|
|
74
|
+
process.method(for: Self.initiateQuiescenceChecks),
|
|
75
|
+
to: Initiate.self
|
|
76
|
+
)
|
|
77
|
+
initiate(process, Self.initiateQuiescenceChecks, true)
|
|
78
|
+
|
|
79
|
+
guard
|
|
80
|
+
let expectation = Self.object(process, Self.makeQuiescenceExpectation)
|
|
81
|
+
as? XCTestExpectation
|
|
82
|
+
else {
|
|
83
|
+
return false
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
_ = XCTWaiter.wait(for: [expectation], timeout: timeout)
|
|
87
|
+
return true
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/// The object a zero-argument getter returns, or nil when `target` lacks it.
|
|
91
|
+
private static func object(_ target: NSObject, _ getter: Selector) -> NSObject? {
|
|
92
|
+
guard target.responds(to: getter) else { return nil }
|
|
93
|
+
return target.perform(getter)?.takeUnretainedValue() as? NSObject
|
|
94
|
+
}
|
|
95
|
+
}
|
package/dist/ios-device-runner/ArgentRunner/ArgentRunnerUITests/ArgentRunnerSession+TextEntry.swift
CHANGED
|
@@ -77,7 +77,7 @@ extension ArgentRunnerSession {
|
|
|
77
77
|
app.keyboards.keys.matching(submitKey).firstMatch,
|
|
78
78
|
] where candidate.exists && candidate.isHittable {
|
|
79
79
|
let label = candidate.label
|
|
80
|
-
candidate.tap()
|
|
80
|
+
skippingQuiescence(of: app) { candidate.tap() }
|
|
81
81
|
|
|
82
82
|
return .success(
|
|
83
83
|
MessagePayload(message: "pressed keyboard \(label)")
|
|
@@ -131,7 +131,7 @@ extension ArgentRunnerSession {
|
|
|
131
131
|
}
|
|
132
132
|
|
|
133
133
|
let exceptionDescription = ArgentExceptionGuard.runCatching {
|
|
134
|
-
app.typeText(text)
|
|
134
|
+
self.skippingQuiescence(of: app) { app.typeText(text) }
|
|
135
135
|
}
|
|
136
136
|
|
|
137
137
|
if let exceptionDescription {
|
|
@@ -70,10 +70,10 @@ extension CommandKind {
|
|
|
70
70
|
switch self {
|
|
71
71
|
// XCTest types long strings in real time, so typing needs extra room.
|
|
72
72
|
case .type: return 55
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
//
|
|
73
|
+
// tap, longPress and drag outlast XCTest's ~60s pre-event idle wait,
|
|
74
|
+
// which runs where the runner cannot skip it
|
|
75
|
+
// (ArgentRunnerSession+Quiescence.swift). touchPaths never waits for
|
|
76
|
+
// idle but shares the gesture budget.
|
|
77
77
|
case .tap, .longPress, .drag, .touchPaths: return 75
|
|
78
78
|
default: return 30
|
|
79
79
|
}
|
|
@@ -117,7 +117,7 @@ struct CommandRequest: Codable {
|
|
|
117
117
|
let y: Double?
|
|
118
118
|
/// `tap`: number of taps. Defaults to 1; 2 is the native double-tap.
|
|
119
119
|
/// Higher counts loop single taps on the device: there is no native N-tap
|
|
120
|
-
/// API, so they
|
|
120
|
+
/// API, so they are not one multi-tap gesture.
|
|
121
121
|
let numberOfTaps: Int?
|
|
122
122
|
/// `drag`: absolute start and end points.
|
|
123
123
|
let fromX: Double?
|
package/dist/mcp-server.mjs
CHANGED
|
@@ -25593,6 +25593,36 @@ var TOOL_ID_ALIASES = Object.freeze({
|
|
|
25593
25593
|
function canonicalToolId(id) {
|
|
25594
25594
|
return Object.hasOwn(TOOL_ID_ALIASES, id) ? TOOL_ID_ALIASES[id] : id;
|
|
25595
25595
|
}
|
|
25596
|
+
var TOOL_PARAM_ALIASES = Object.freeze({
|
|
25597
|
+
"e2e-execute": Object.freeze({ flow_path: "e2e_test_path", flow_file: "e2e_test_file" }),
|
|
25598
|
+
"e2e-read-prerequisite": Object.freeze({
|
|
25599
|
+
flow_path: "e2e_test_path",
|
|
25600
|
+
flow_file: "e2e_test_file"
|
|
25601
|
+
})
|
|
25602
|
+
});
|
|
25603
|
+
var ToolParamAliasError = class extends Error {
|
|
25604
|
+
constructor(former, current) {
|
|
25605
|
+
super(`Pass ${current} or its former name ${former}, not both.`);
|
|
25606
|
+
this.name = "ToolParamAliasError";
|
|
25607
|
+
}
|
|
25608
|
+
};
|
|
25609
|
+
function toolParamAliases(tool) {
|
|
25610
|
+
const id = canonicalToolId(tool);
|
|
25611
|
+
return Object.hasOwn(TOOL_PARAM_ALIASES, id) ? TOOL_PARAM_ALIASES[id] : {};
|
|
25612
|
+
}
|
|
25613
|
+
function canonicalToolArgs(tool, args) {
|
|
25614
|
+
if (typeof args !== "object" || args === null || Array.isArray(args)) return args;
|
|
25615
|
+
const record2 = args;
|
|
25616
|
+
let out;
|
|
25617
|
+
for (const [former, current] of Object.entries(toolParamAliases(tool))) {
|
|
25618
|
+
if (record2[former] === void 0) continue;
|
|
25619
|
+
if (record2[current] !== void 0) throw new ToolParamAliasError(former, current);
|
|
25620
|
+
out ??= { ...record2 };
|
|
25621
|
+
out[current] = out[former];
|
|
25622
|
+
delete out[former];
|
|
25623
|
+
}
|
|
25624
|
+
return out ?? args;
|
|
25625
|
+
}
|
|
25596
25626
|
|
|
25597
25627
|
// ../registry/src/flow-file-refs.ts
|
|
25598
25628
|
async function canonicalFlowPath(p) {
|
|
@@ -25662,7 +25692,13 @@ async function classifyOnDiskSpelling(dir, base, addressable = FLOW_FILE_NAME_PA
|
|
|
25662
25692
|
}
|
|
25663
25693
|
function nestedFlowTarget(args) {
|
|
25664
25694
|
if (typeof args !== "object" || args === null || Array.isArray(args)) return void 0;
|
|
25665
|
-
|
|
25695
|
+
let canonical;
|
|
25696
|
+
try {
|
|
25697
|
+
canonical = canonicalToolArgs("e2e-execute", args);
|
|
25698
|
+
} catch {
|
|
25699
|
+
return void 0;
|
|
25700
|
+
}
|
|
25701
|
+
const { name, project_root: projectRoot, e2e_test_path: flowPath } = canonical;
|
|
25666
25702
|
if (flowPath === void 0) {
|
|
25667
25703
|
if (typeof name !== "string" || !FLOW_NAME_PATTERN.test(name)) return void 0;
|
|
25668
25704
|
if (!isResolvedAbsolute(projectRoot)) return void 0;
|
|
@@ -25671,7 +25707,7 @@ function nestedFlowTarget(args) {
|
|
|
25671
25707
|
}
|
|
25672
25708
|
if (name !== void 0 || !isResolvedAbsolute(flowPath)) return void 0;
|
|
25673
25709
|
if (!FLOW_FILE_NAME_PATTERN.test(path8.basename(flowPath))) return void 0;
|
|
25674
|
-
return { kind: "
|
|
25710
|
+
return { kind: "e2e_test_path", path: flowPath };
|
|
25675
25711
|
}
|
|
25676
25712
|
function isResolvedAbsolute(value) {
|
|
25677
25713
|
return typeof value === "string" && path8.isAbsolute(value) && !value.split(/[\\/]+/).includes("..");
|
|
@@ -25703,6 +25739,8 @@ var FAILURE_CODES = {
|
|
|
25703
25739
|
TOOL_CAPABILITY_UNSUPPORTED_OPERATION: "TOOL_CAPABILITY_UNSUPPORTED_OPERATION",
|
|
25704
25740
|
TOOL_PLATFORM_NOT_IMPLEMENTED: "TOOL_PLATFORM_NOT_IMPLEMENTED",
|
|
25705
25741
|
TOOL_DEPENDENCY_MISSING: "TOOL_DEPENDENCY_MISSING",
|
|
25742
|
+
// Xcode's simctl or devicectl wrapper would start its component install and wait for a password.
|
|
25743
|
+
TOOL_XCODE_FIRST_LAUNCH_PENDING: "TOOL_XCODE_FIRST_LAUNCH_PENDING",
|
|
25706
25744
|
TOOL_INPUT_INVALID: "TOOL_INPUT_INVALID",
|
|
25707
25745
|
TOOLSERVER_UNHANDLED_REJECTION: "TOOLSERVER_UNHANDLED_REJECTION",
|
|
25708
25746
|
TOOLSERVER_UNCAUGHT_EXCEPTION: "TOOLSERVER_UNCAUGHT_EXCEPTION",
|
|
@@ -25875,6 +25913,16 @@ var FAILURE_CODES = {
|
|
|
25875
25913
|
JS_RUNTIME_CDP_DISCONNECTED: "JS_RUNTIME_CDP_DISCONNECTED",
|
|
25876
25914
|
JS_RUNTIME_PAUSED: "JS_RUNTIME_PAUSED",
|
|
25877
25915
|
NETWORK_INSPECTOR_CDP_DISCONNECTED: "NETWORK_INSPECTOR_CDP_DISCONNECTED",
|
|
25916
|
+
ANDROID_NETWORK_INSPECTOR_FACTORY_OPTIONS_INVALID: "ANDROID_NETWORK_INSPECTOR_FACTORY_OPTIONS_INVALID",
|
|
25917
|
+
ANDROID_NETWORK_INSPECTOR_WRONG_PLATFORM: "ANDROID_NETWORK_INSPECTOR_WRONG_PLATFORM",
|
|
25918
|
+
ANDROID_NETWORK_INSPECTOR_UNKNOWN_PACKAGE: "ANDROID_NETWORK_INSPECTOR_UNKNOWN_PACKAGE",
|
|
25919
|
+
ANDROID_NETWORK_INSPECTOR_RUN_AS_FAILED: "ANDROID_NETWORK_INSPECTOR_RUN_AS_FAILED",
|
|
25920
|
+
ANDROID_NETWORK_INSPECTOR_DEVICE_IN_USE: "ANDROID_NETWORK_INSPECTOR_DEVICE_IN_USE",
|
|
25921
|
+
ANDROID_NETWORK_INSPECTOR_NO_DEVICE_PORT: "ANDROID_NETWORK_INSPECTOR_NO_DEVICE_PORT",
|
|
25922
|
+
NATIVE_NETWORK_LOGS_STOP_UNSUPPORTED: "NATIVE_NETWORK_LOGS_STOP_UNSUPPORTED",
|
|
25923
|
+
NDJSON_CDP_REQUEST_TIMEOUT: "NDJSON_CDP_REQUEST_TIMEOUT",
|
|
25924
|
+
NDJSON_CDP_REQUEST_FAILED: "NDJSON_CDP_REQUEST_FAILED",
|
|
25925
|
+
NDJSON_CDP_CONNECTION_CLOSED: "NDJSON_CDP_CONNECTION_CLOSED",
|
|
25878
25926
|
REACT_PROFILER_NO_ACTIVE_SESSION: "REACT_PROFILER_NO_ACTIVE_SESSION",
|
|
25879
25927
|
REACT_PROFILER_CDP_CONNECTION_LOST: "REACT_PROFILER_CDP_CONNECTION_LOST",
|
|
25880
25928
|
REACT_PROFILER_NO_CPU_PROFILE: "REACT_PROFILER_NO_CPU_PROFILE",
|
|
@@ -26611,12 +26659,18 @@ function isResolvedAbsolute2(value) {
|
|
|
26611
26659
|
return typeof value === "string" && path11.isAbsolute(value) && !value.split(/[\\/]+/).includes("..");
|
|
26612
26660
|
}
|
|
26613
26661
|
function namesValidFlow(args) {
|
|
26614
|
-
|
|
26662
|
+
let canonical;
|
|
26663
|
+
try {
|
|
26664
|
+
canonical = canonicalToolArgs("e2e-execute", args);
|
|
26665
|
+
} catch {
|
|
26666
|
+
return false;
|
|
26667
|
+
}
|
|
26668
|
+
const { project_root, e2e_test_path: testPath, name } = canonical;
|
|
26615
26669
|
if (!isResolvedAbsolute2(project_root)) return false;
|
|
26616
26670
|
if (name === void 0) {
|
|
26617
|
-
return isResolvedAbsolute2(
|
|
26671
|
+
return isResolvedAbsolute2(testPath) && FLOW_FILE_NAME_PATTERN.test(path11.basename(testPath));
|
|
26618
26672
|
}
|
|
26619
|
-
return
|
|
26673
|
+
return testPath === void 0 && typeof name === "string" && FLOW_NAME_PATTERN.test(name);
|
|
26620
26674
|
}
|
|
26621
26675
|
function savedFlowProject(file) {
|
|
26622
26676
|
if (!path11.isAbsolute(file)) return null;
|
|
@@ -26790,8 +26844,8 @@ function memberCollector(roots, opts) {
|
|
|
26790
26844
|
`));
|
|
26791
26845
|
const emit = (member, sent) => {
|
|
26792
26846
|
members2.push(member);
|
|
26793
|
-
const subject = member.role === "flow" ? member.canonical : member.key;
|
|
26794
|
-
if (logging) log(printable(`[
|
|
26847
|
+
const [label, subject] = member.role === "flow" ? ["e2e test", member.canonical] : [member.role, member.key];
|
|
26848
|
+
if (logging) log(printable(`[e2e-files] ${label} ${subject}: ${sent}`));
|
|
26795
26849
|
};
|
|
26796
26850
|
async function flow(anchorDir, target, onlyPresent) {
|
|
26797
26851
|
const key = flowMemberKey(anchorDir, target);
|
|
@@ -26957,7 +27011,7 @@ function recordedStep(args) {
|
|
|
26957
27011
|
if (typeof text !== "string") return void 0;
|
|
26958
27012
|
let parsed;
|
|
26959
27013
|
try {
|
|
26960
|
-
parsed = JSON.parse(text);
|
|
27014
|
+
parsed = canonicalToolArgs(command, JSON.parse(text));
|
|
26961
27015
|
} catch {
|
|
26962
27016
|
return void 0;
|
|
26963
27017
|
}
|
|
@@ -26980,7 +27034,7 @@ async function collectStepMembers(args, opts, recordingDirFor = e2eDirFor) {
|
|
|
26980
27034
|
const self = await collector.flow(path11.dirname(recording), path11.basename(recording));
|
|
26981
27035
|
let runArgs = step.args;
|
|
26982
27036
|
const named = nestedFlowTarget(runArgs);
|
|
26983
|
-
if (named?.kind === "
|
|
27037
|
+
if (named?.kind === "e2e_test_path") {
|
|
26984
27038
|
const stem = path11.basename(named.path, ".yaml");
|
|
26985
27039
|
const base = path11.basename(named.path);
|
|
26986
27040
|
const dir = path11.resolve(path11.dirname(named.path));
|
|
@@ -26989,7 +27043,7 @@ async function collectStepMembers(args, opts, recordingDirFor = e2eDirFor) {
|
|
|
26989
27043
|
if (dirs.some((d) => path11.resolve(d) === dir)) {
|
|
26990
27044
|
await collector.flow(path11.dirname(named.path), base);
|
|
26991
27045
|
runArgs = { ...runArgs, name: stem };
|
|
26992
|
-
delete runArgs.
|
|
27046
|
+
delete runArgs.e2e_test_path;
|
|
26993
27047
|
}
|
|
26994
27048
|
}
|
|
26995
27049
|
const target = nestedFlowTarget(runArgs);
|
|
@@ -27017,7 +27071,21 @@ async function nameToE2ePathForOlderServer(tool, fileInputs, args) {
|
|
|
27017
27071
|
const picked = await pickE2eFile(target.paths);
|
|
27018
27072
|
if (!picked.exists || picked.legacy) return args;
|
|
27019
27073
|
const { name: _name, ...rest } = args;
|
|
27020
|
-
return { ...rest,
|
|
27074
|
+
return { ...rest, e2e_test_path: picked.path };
|
|
27075
|
+
}
|
|
27076
|
+
function formerParamsForOlderServer(meta2, args) {
|
|
27077
|
+
const listed = meta2.inputSchema.properties;
|
|
27078
|
+
if (typeof args !== "object" || args === null || Array.isArray(args)) return args;
|
|
27079
|
+
if (typeof listed !== "object" || listed === null) return args;
|
|
27080
|
+
const record2 = args;
|
|
27081
|
+
let out;
|
|
27082
|
+
for (const [former, current] of Object.entries(toolParamAliases(meta2.name))) {
|
|
27083
|
+
if (record2[current] === void 0 || current in listed || !(former in listed)) continue;
|
|
27084
|
+
out ??= { ...record2 };
|
|
27085
|
+
out[former] = out[current];
|
|
27086
|
+
delete out[current];
|
|
27087
|
+
}
|
|
27088
|
+
return out ?? args;
|
|
27021
27089
|
}
|
|
27022
27090
|
async function stepForOlderServer(args, listedId) {
|
|
27023
27091
|
if (typeof args !== "object" || args === null || Array.isArray(args)) return args;
|
|
@@ -27275,12 +27343,13 @@ function createToolsClient(options = {}) {
|
|
|
27275
27343
|
async function callTool(name, args, opts) {
|
|
27276
27344
|
opts?.signal?.throwIfAborted();
|
|
27277
27345
|
const { url, token, remote } = await route();
|
|
27278
|
-
let finalArgs = args;
|
|
27346
|
+
let finalArgs = canonicalToolArgs(name, args);
|
|
27279
27347
|
const tools = await fetchTools({ signal: opts?.signal });
|
|
27280
27348
|
const meta2 = findTool(tools, name);
|
|
27281
27349
|
const olderServer = meta2 !== null && !tools.some((t) => t.name === canonicalToolId(meta2.name));
|
|
27282
27350
|
if (meta2) {
|
|
27283
|
-
finalArgs = await nameToE2ePathForOlderServer(meta2.name, meta2.fileInputs,
|
|
27351
|
+
finalArgs = await nameToE2ePathForOlderServer(meta2.name, meta2.fileInputs, finalArgs);
|
|
27352
|
+
finalArgs = formerParamsForOlderServer(meta2, finalArgs);
|
|
27284
27353
|
}
|
|
27285
27354
|
if (olderServer && canonicalToolId(meta2.name) === "e2e-add-step") {
|
|
27286
27355
|
finalArgs = await stepForOlderServer(finalArgs, (tool) => findTool(tools, tool)?.name);
|
|
@@ -27308,7 +27377,7 @@ function createToolsClient(options = {}) {
|
|
|
27308
27377
|
if (filesLogEnabled()) {
|
|
27309
27378
|
for (const file of applied.written) {
|
|
27310
27379
|
if (baselineDirs.includes(path12.dirname(file))) {
|
|
27311
|
-
diagnose(`[
|
|
27380
|
+
diagnose(`[e2e-files] baseline ${file}: written`);
|
|
27312
27381
|
}
|
|
27313
27382
|
}
|
|
27314
27383
|
}
|
|
@@ -28678,12 +28747,13 @@ function stepLabel(step) {
|
|
|
28678
28747
|
}
|
|
28679
28748
|
async function flowRunToMcpContent(result, ctx) {
|
|
28680
28749
|
const blocks = [];
|
|
28750
|
+
const name = result.e2eTest ?? result.flow;
|
|
28681
28751
|
if (result.executionPrerequisite) {
|
|
28682
28752
|
blocks.push({ type: "text", text: `Prerequisite: ${result.executionPrerequisite}` });
|
|
28683
28753
|
}
|
|
28684
28754
|
blocks.push({
|
|
28685
28755
|
type: "text",
|
|
28686
|
-
text: `Running e2e test "${
|
|
28756
|
+
text: `Running e2e test "${name}"${result.device ? ` on ${result.device}` : ""} (${result.steps.length} steps)`
|
|
28687
28757
|
});
|
|
28688
28758
|
if (typeof result.warning === "string")
|
|
28689
28759
|
blocks.push({ type: "text", text: `\u26A0 ${result.warning}` });
|
|
@@ -28728,7 +28798,7 @@ async function flowRunToMcpContent(result, ctx) {
|
|
|
28728
28798
|
text: `${result.ok ? "PASS" : "FAIL"} \u2014 ${result.passed ?? 0} passed, ${result.failed ?? 0} failed, ${result.errored ?? 0} errored, ${result.skipped ?? 0} skipped${note}${durationSuffix(result.durationMs)}`
|
|
28729
28799
|
});
|
|
28730
28800
|
} else {
|
|
28731
|
-
blocks.push({ type: "text", text: `E2E test "${
|
|
28801
|
+
blocks.push({ type: "text", text: `E2E test "${name}" complete.` });
|
|
28732
28802
|
}
|
|
28733
28803
|
return blocks;
|
|
28734
28804
|
}
|
|
@@ -29339,7 +29409,7 @@ ${desc}` }];
|
|
|
29339
29409
|
deviceId: getDeviceIdFromArgs(params.arguments)
|
|
29340
29410
|
};
|
|
29341
29411
|
let content;
|
|
29342
|
-
if (canonicalToolId(params.name) === "e2e-execute" && result && typeof result === "object" && "flow" in result && "steps" in result) {
|
|
29412
|
+
if (canonicalToolId(params.name) === "e2e-execute" && result && typeof result === "object" && ("e2eTest" in result || "flow" in result) && "steps" in result) {
|
|
29343
29413
|
content = await flowRunToMcpContent(result, ctx);
|
|
29344
29414
|
} else if (params.name === "screenshot-diff" && isScreenshotDiffResult(result)) {
|
|
29345
29415
|
content = await screenshotDiffToMcpContent(result, ctx);
|
|
@@ -223,14 +223,14 @@ export interface ArgentToolArgs {
|
|
|
223
223
|
delayMs?: number;
|
|
224
224
|
};
|
|
225
225
|
"e2e-execute": {
|
|
226
|
-
/** Name of a saved e2e test to run from `.argent/e2e`, or `.argent/flows` when it is not there (e.g. "settings-explore"). Omit when
|
|
226
|
+
/** Name of a saved e2e test to run from `.argent/e2e`, or `.argent/flows` when it is not there (e.g. "settings-explore"). Omit when e2e_test_path is set. */
|
|
227
227
|
name?: string;
|
|
228
|
-
/** Absolute path to the calling agent's project root: the cwd it is working in. With name, the saved e2e test is read from `.argent/e2e/<name>.yaml`, else `.argent/flows/<name>.yaml`, under this root; with
|
|
228
|
+
/** Absolute path to the calling agent's project root: the cwd it is working in. With name, the saved e2e test is read from `.argent/e2e/<name>.yaml`, else `.argent/flows/<name>.yaml`, under this root; with e2e_test_path, the e2e test, its run: siblings, its script: paths and baselines all resolve beside the YAML instead, so pass the agent's cwd. A script still RUNS in this root whichever source was used. */
|
|
229
229
|
project_root: string;
|
|
230
230
|
/** Path to the e2e test .yaml as readable by the tool-server. Internal: the argent client derives it from project_root and name automatically; leave unset. */
|
|
231
|
-
|
|
231
|
+
e2e_test_file?: string;
|
|
232
232
|
/** Omit when name is set. Absolute path to an e2e test .yaml on the client. Over a link (argent link or ARGENT_TOOLS_URL), the argent client uploads the file, its run: fragments and the snapshot baselines of each run, and the tool-server runs those copies. Keep run: fragments under project_root or beside the e2e test file. With updateBaselines, the client writes the new baselines when the run ends. The client also uploads each absolute .png or .yaml path that a tool: step takes as a file argument, when the file is under project_root or beside the e2e test file. A tool: e2e-execute step that names an e2e test with name and an absolute project_root without a .. segment runs: the client also uploads that e2e test and its own files. Over a link, the uploaded e2e tests must not have: script: steps; tool: steps that run an e2e test another way or record an e2e test; tool: steps that take a directory, an app or an output directory; tool: steps whose tool builds a file path from several arguments; tool: steps that name a relative path or a file other than .png or .yaml. Without a link, the tool-server reads the files in place and all step kinds run. */
|
|
233
|
-
|
|
233
|
+
e2e_test_path?: string;
|
|
234
234
|
/** Device id to run against (iOS UDID, Android/Vega serial, Chromium id), as list-devices reports it. Auto-detected when omitted, but only when exactly one booted device matches (optionally narrowed by `platform`); with several booted the run fails and lists them, so pass this explicitly whenever more than one device is up. */
|
|
235
235
|
device?: string;
|
|
236
236
|
/** Restrict auto-detection to this platform when several devices are booted. `ios` selects local simulators only; pass `ios-remote` to select a remote one. `chromium` does more than filter: with no `device` it SELECTS the self-boot branch for a standalone e2e test - the runner boots an Electron instance from the `launch` step's chromium value and tears it down after the run (a single-key `launch: { chromium: … }` map selects it on its own, without this parameter). When it selects that branch it never falls back to device auto-detection (a fragment, or a standalone e2e test's launch map with no `chromium` key, still does), and the launch value must be a real Electron app path on the tool-server host: a bare-string `launch:` - what the recorder writes - holds an installed-app bundle id, so passing `chromium` for one fails the whole run with `Electron boot: path does not exist`. Edit the launch to `{ chromium: <app path> }` first. */
|
|
@@ -239,6 +239,10 @@ export interface ArgentToolArgs {
|
|
|
239
239
|
updateBaselines?: boolean;
|
|
240
240
|
/** Set to true to confirm the execution prerequisite has been met. Required (LLM path) when a fragment defines an executionPrerequisite. */
|
|
241
241
|
prerequisiteAcknowledged?: boolean;
|
|
242
|
+
/** @deprecated Renamed to `e2e_test_path`. */
|
|
243
|
+
flow_path?: string;
|
|
244
|
+
/** @deprecated Renamed to `e2e_test_file`. */
|
|
245
|
+
flow_file?: string;
|
|
242
246
|
};
|
|
243
247
|
"e2e-finish-recording": {
|
|
244
248
|
/** Name of the e2e test being recorded: the one passed to e2e-start-recording. */
|
|
@@ -247,14 +251,18 @@ export interface ArgentToolArgs {
|
|
|
247
251
|
project_root: string;
|
|
248
252
|
};
|
|
249
253
|
"e2e-read-prerequisite": {
|
|
250
|
-
/** Name of a saved e2e test to inspect from `.argent/e2e`, or `.argent/flows` when it is not there (e.g. "settings-explore"). Omit when
|
|
254
|
+
/** Name of a saved e2e test to inspect from `.argent/e2e`, or `.argent/flows` when it is not there (e.g. "settings-explore"). Omit when e2e_test_path is set. */
|
|
251
255
|
name?: string;
|
|
252
|
-
/** Absolute path to the calling agent's project root: the cwd it is working in. With name, the saved e2e test is read from `.argent/e2e/<name>.yaml`, else `.argent/flows/<name>.yaml`, under this root; with
|
|
256
|
+
/** Absolute path to the calling agent's project root: the cwd it is working in. With name, the saved e2e test is read from `.argent/e2e/<name>.yaml`, else `.argent/flows/<name>.yaml`, under this root; with e2e_test_path, the prerequisite is read from that YAML instead, so pass the agent's cwd. */
|
|
253
257
|
project_root: string;
|
|
254
258
|
/** Path to the e2e test .yaml as readable by the tool-server. Internal: the argent client derives it from project_root and name automatically; leave unset. */
|
|
255
|
-
|
|
259
|
+
e2e_test_file?: string;
|
|
256
260
|
/** Omit when name is set. Absolute path to an e2e test .yaml on the client. Over a link (argent link or ARGENT_TOOLS_URL), the argent client uploads the file and the tool-server reads the uploaded copy; without a link, the tool-server reads the file in place. Pass the path through the argent client; a raw path that the file-input boundary did not verify is rejected. Pass the same e2e test source here as to e2e-execute, so the prerequisite you read belongs to the e2e test that will run. */
|
|
261
|
+
e2e_test_path?: string;
|
|
262
|
+
/** @deprecated Renamed to `e2e_test_path`. */
|
|
257
263
|
flow_path?: string;
|
|
264
|
+
/** @deprecated Renamed to `e2e_test_file`. */
|
|
265
|
+
flow_file?: string;
|
|
258
266
|
};
|
|
259
267
|
"e2e-start-recording": {
|
|
260
268
|
/** Name for this e2e test (e.g. "settings-explore"): letters, digits, underscore and hyphen only. */
|
|
@@ -400,7 +408,7 @@ export interface ArgentToolArgs {
|
|
|
400
408
|
x: number;
|
|
401
409
|
/** Normalized vertical position 0.0–1.0 (top=0, bottom=1), not pixels */
|
|
402
410
|
y: number;
|
|
403
|
-
/** Number of taps/clicks dispatched as ONE multi-tap gesture (2 = double-tap / double-click). The taps land inside the OS double-tap window; on Chromium each click carries an escalating CDP clickCount so dblclick actually fires; on physical iOS 2 is the native double-tap and higher counts
|
|
411
|
+
/** Number of taps/clicks dispatched as ONE multi-tap gesture (2 = double-tap / double-click). The taps land inside the OS double-tap window; on Chromium each click carries an escalating CDP clickCount so dblclick actually fires; on physical iOS 2 is the native double-tap and higher counts loop single taps, which a double-tap recognizer may pair. On HarmonyOS only 2 is one gesture; 3 or more are separate injections a device round trip apart, which the OS reads as single taps. Default 1. */
|
|
404
412
|
clickCount?: number;
|
|
405
413
|
};
|
|
406
414
|
keyboard: {
|
|
@@ -477,14 +485,18 @@ export interface ArgentToolArgs {
|
|
|
477
485
|
maxDepth?: number;
|
|
478
486
|
};
|
|
479
487
|
"native-network-logs": {
|
|
480
|
-
/**
|
|
488
|
+
/** Device ID from list-devices: an iOS simulator UDID or Android serial */
|
|
481
489
|
udid: string;
|
|
482
|
-
/**
|
|
490
|
+
/** App bundle ID on iOS or package name on Android */
|
|
483
491
|
bundleId: string;
|
|
484
|
-
/**
|
|
492
|
+
/** Android only: the app's Metro port, whose requests are left out of the listing. Omit it to use this device's port, 8081 by default. */
|
|
493
|
+
port?: number;
|
|
494
|
+
/** Maximum number of entries to return: the most recent ones, oldest first */
|
|
485
495
|
limit?: number;
|
|
486
|
-
/** Clear the log after reading */
|
|
496
|
+
/** Clear the log after reading. On Android, the IDs of cleared requests still work in view-network-request-details */
|
|
487
497
|
clear?: boolean;
|
|
498
|
+
/** Android only: turn native capture off for this app instead of listing. Other apps keep theirs. */
|
|
499
|
+
stop?: boolean;
|
|
488
500
|
};
|
|
489
501
|
"native-profiler-analyze": {
|
|
490
502
|
/** Target device id from `list-devices` (iOS UDID or Android serial). */
|
|
@@ -890,9 +902,9 @@ export interface ArgentToolArgs {
|
|
|
890
902
|
"view-network-request-details": {
|
|
891
903
|
/** Metro server port. Optional: omit it to use this device's port, 8081 by default. Ignored for Chromium, whose CDP port is encoded in device_id. */
|
|
892
904
|
port?: number;
|
|
893
|
-
/** Device id from list-devices (iOS simulator UDID or Android serial), the same id used with debugger-connect. */
|
|
905
|
+
/** Device id from list-devices (iOS simulator UDID or Android serial), the same id used with debugger-connect. An android- ID needs no debugger connection. */
|
|
894
906
|
device_id: string;
|
|
895
|
-
/** The requestId from view-network-logs
|
|
907
|
+
/** The requestId from view-network-logs, or an android- ID from native-network-logs */
|
|
896
908
|
requestId: string;
|
|
897
909
|
/** Whether to include the response body (if captured). Defaults to true. */
|
|
898
910
|
includeBody?: boolean;
|