@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.
Files changed (32) hide show
  1. package/bin/argent-android-devtools-0.3.0.apk +0 -0
  2. package/bin/darwin/ax-service +0 -0
  3. package/bin/darwin/tvos-ax-service +0 -0
  4. package/bin/darwin/tvos-hid-daemon +0 -0
  5. package/bin/network-inspector/arm64-v8a/libjvmti_network_inspector.so +0 -0
  6. package/bin/network-inspector/network-inspector.jar +0 -0
  7. package/bin/network-inspector/x86_64/libjvmti_network_inspector.so +0 -0
  8. package/bin/tcp/ax-service +0 -0
  9. package/dist/cli-cmds.mjs +106 -24
  10. package/dist/client.js +83 -14
  11. package/dist/installer.mjs +25 -1
  12. package/dist/ios-device-runner/ArgentRunner/ArgentRunnerUITests/ArgentExceptionGuard.h +3 -0
  13. package/dist/ios-device-runner/ArgentRunner/ArgentRunnerUITests/ArgentExceptionGuard.m +11 -3
  14. package/dist/ios-device-runner/ArgentRunner/ArgentRunnerUITests/ArgentRunnerSession+Gestures.swift +27 -19
  15. package/dist/ios-device-runner/ArgentRunner/ArgentRunnerUITests/ArgentRunnerSession+Quiescence.swift +95 -0
  16. package/dist/ios-device-runner/ArgentRunner/ArgentRunnerUITests/ArgentRunnerSession+TextEntry.swift +2 -2
  17. package/dist/ios-device-runner/ArgentRunner/ArgentRunnerUITests/RunnerProtocol.swift +5 -5
  18. package/dist/mcp-server.mjs +87 -17
  19. package/dist/tool-args.generated.d.ts +26 -14
  20. package/dist/tool-server.cjs +4604 -1860
  21. package/dylibs/libArgentInjectionBootstrap.dylib +0 -0
  22. package/dylibs/libKeyboardPatch.dylib +0 -0
  23. package/dylibs/libNativeDevtoolsIos.dylib +0 -0
  24. package/dylibs/tcp/libArgentInjectionBootstrap.dylib +0 -0
  25. package/dylibs/tcp/libKeyboardPatch.dylib +0 -0
  26. package/dylibs/tcp/libNativeDevtoolsIos.dylib +0 -0
  27. package/dylibs/tvos/libArgentInjectionBootstrap.dylib +0 -0
  28. package/dylibs/tvos/libKeyboardPatch.dylib +0 -0
  29. package/dylibs/tvos/libNativeDevtoolsIos.dylib +0 -0
  30. package/package.json +1 -1
  31. package/skills/argent-create-e2e/references/live-authoring.md +3 -3
  32. package/skills/argent-ios-device-setup/SKILL.md +1 -1
@@ -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 on hardware: each tap
25
- /// is its own synthesized event behind XCTest's idle wait and an XPC round
26
- /// trip, hundreds of milliseconds apart, so the taps land separately and do
27
- /// not trigger a multi-tap recognizer. That is documented, not refused.
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
- switch taps {
43
- case 1:
44
- coordinate.tap()
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).press(forDuration: seconds)
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
- start.press(
116
- forDuration: startHold,
117
- thenDragTo: end,
118
- withVelocity: velocity,
119
- thenHoldForDuration: endHold
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
  }
@@ -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
+ }
@@ -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
- // Gestures must outlast XCTest's ~60s pre-event idle wait. A screen that
74
- // never reports quiescent stalls the wait until XCTest gives up and
75
- // synthesizes the event anyway, and a shorter budget would abandon a
76
- // command that still completes.
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 land as separate taps, not one multi-tap gesture.
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?
@@ -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
- const { name, project_root: projectRoot, flow_path: flowPath } = args;
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: "flow_path", path: flowPath };
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
- const { project_root, flow_path, name } = args;
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(flow_path) && FLOW_FILE_NAME_PATTERN.test(path11.basename(flow_path));
26671
+ return isResolvedAbsolute2(testPath) && FLOW_FILE_NAME_PATTERN.test(path11.basename(testPath));
26618
26672
  }
26619
- return flow_path === void 0 && typeof name === "string" && FLOW_NAME_PATTERN.test(name);
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(`[flow-files] ${member.role} ${subject}: ${sent}`));
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 === "flow_path") {
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.flow_path;
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, flow_path: picked.path };
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, args);
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(`[flow-files] baseline ${file}: written`);
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 "${result.flow}"${result.device ? ` on ${result.device}` : ""} (${result.steps.length} steps)`
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 "${result.flow}" complete.` });
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 flow_path is set. */
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 flow_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. */
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
- flow_file?: string;
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
- flow_path?: string;
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 flow_path is set. */
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 flow_path, the prerequisite is read from that YAML instead, so pass the agent's cwd. */
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
- flow_file?: string;
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 land as separate taps. 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. */
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
- /** Simulator UDID */
488
+ /** Device ID from list-devices: an iOS simulator UDID or Android serial */
481
489
  udid: string;
482
- /** Bundle ID of the app */
490
+ /** App bundle ID on iOS or package name on Android */
483
491
  bundleId: string;
484
- /** Maximum number of events to return (most recent first) */
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 to get full details for */
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;