@swmansion/argent 0.22.1-next.7 → 0.22.1-next.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli-cmds.mjs CHANGED
@@ -7210,7 +7210,7 @@ var _CI_VENDOR_COUNT_FOR_TEST = vendors_default.length;
7210
7210
  var SESSION_ID2 = randomUUID5();
7211
7211
  function readCliVersion() {
7212
7212
  if (true) {
7213
- return "0.22.1-next.7";
7213
+ return "0.22.1-next.8";
7214
7214
  }
7215
7215
  return "0.0.0";
7216
7216
  }
@@ -16428,7 +16428,7 @@ var _CI_VENDOR_COUNT_FOR_TEST = vendors_default.length;
16428
16428
  var SESSION_ID = randomUUID4();
16429
16429
  function readCliVersion() {
16430
16430
  if (true) {
16431
- return "0.22.1-next.7";
16431
+ return "0.22.1-next.8";
16432
16432
  }
16433
16433
  return "0.0.0";
16434
16434
  }
@@ -95734,7 +95734,7 @@ var _CI_VENDOR_COUNT_FOR_TEST = vendors_default.length;
95734
95734
  var SESSION_ID = (0, import_node_crypto3.randomUUID)();
95735
95735
  function readCliVersion() {
95736
95736
  if (true) {
95737
- return "0.22.1-next.7";
95737
+ return "0.22.1-next.8";
95738
95738
  }
95739
95739
  return "0.0.0";
95740
95740
  }
@@ -111155,7 +111155,7 @@ function isNativeDevtoolsBlockResult(toolId, result) {
111155
111155
  async function precheckNativeDevtools(api, udid, bundleId) {
111156
111156
  if (bundleId !== void 0 && !isInjectableBundleId(bundleId)) {
111157
111157
  throw new FailureError(
111158
- `${bundleId} is an Apple system app: it is a platform binary with library validation, so Argent native devtools cannot be relied on to inject into it \u2014 treat it as unavailable rather than retrying. ` + NON_INJECTABLE_RECOVERY,
111158
+ `${bundleId} is an Apple system app: it is never the app under test, so Argent native devtools refuse to read one \u2014 treat it as unavailable rather than retrying. ` + NON_INJECTABLE_RECOVERY,
111159
111159
  {
111160
111160
  error_code: FAILURE_CODES.NATIVE_DEVTOOLS_NOT_INJECTABLE,
111161
111161
  failure_stage: "native_devtools_precheck",
@@ -111854,7 +111854,7 @@ function tcpArtifactHint(err) {
111854
111854
  return /TCP-transport (?:binary|dylib) not found/.test(message) ? message : void 0;
111855
111855
  }
111856
111856
  var TVOS_HINT = "This is an Apple TV (tvOS) simulator, which the iOS accessibility service does not support. Use the `describe` tool to read the focused and focusable elements, `tv-remote` (up/down/left/right/select/back/menu/home) to move focus, and `keyboard` to type. See the argent-tv-interact skill.";
111857
- var NON_INJECTABLE_HINT = "This is an Apple system app (com.apple.*), which cannot be relied on to load argent's native-devtools instrumentation \u2014 the native view hierarchy is unavailable and restarting the app will NOT help. Take a `screenshot` to see the screen and interact by coordinate. " + NON_INJECTABLE_NATIVE_WARNING;
111857
+ var NON_INJECTABLE_HINT = "This is an Apple system app (com.apple.*), which argent's native-devtools instrumentation does not support \u2014 the native view hierarchy is unavailable and restarting the app will NOT help. Take a `screenshot` to see the screen and interact by coordinate. " + NON_INJECTABLE_NATIVE_WARNING;
111858
111858
  function emptyTree() {
111859
111859
  return parseDescribeResult({
111860
111860
  role: "AXGroup",
@@ -116225,10 +116225,10 @@ Returns { envSetup, appRunning, connected, requiresRestart, state, message, next
116225
116225
  - state: why devtools are or aren't live, measured from the running process. "connected"; "not_running"; "stale_process" (the process cannot reach this simulator's devtools endpoint \u2014 launched either before argent's instrumentation was in place or against an earlier tool-server's listener \u2014 so restart-app fixes it); "unregistered" (the process IS injected and pointed at this simulator's devtools endpoint yet the service never registered it, so restarting the app cannot help); "connecting" (the process IS injected but launched moments ago and is still connecting, so waiting is what helps); "indeterminate" (the process could not be inspected). Omitted when injectable is false, which is terminal on its own.
116226
116226
  - message: the remedy for that state, in full. Omitted when connected or non-injectable. Prefer it over inferring one from the booleans \u2014 it is the only field that can tell you to stop restarting the app.
116227
116227
  - nextLaunchWillBeInjected: if you launch this bundle now, native devtools env setup is already in place (always false for a non-injectable app)
116228
- - injectable: whether native devtools can be relied on to inject into this app. Apple system apps (bundle ids under com.apple.) are platform binaries with library validation, so the dylib cannot be counted on to load into them \u2014 it has been observed both loading and not loading, depending on the simulator runtime.
116228
+ - injectable: whether this app is a supported target for Argent native devtools. Apple system apps (bundle ids under com.apple.) are not: they are never the app under test, so the native tools refuse to read one.
116229
116229
 
116230
116230
  Call this before using app-scoped native hierarchy tools or native-network-logs.
116231
- If injectable is false: treat this as TERMINAL \u2014 injection cannot be relied on for this app, and no relaunch changes which way it goes. Do NOT restart/retry. Use the standard \`describe\` tool (its accessibility path reads the screen without injection) or \`screenshot\` (then interact by coordinate). Do not fall back to the native-devtools feature tools (native-describe-screen, native-find-views, native-full-hierarchy, native-network-logs, native-view-at-point, native-user-interactable-view-at-point) \u2014 they run the same injection precheck and fail with the same non-injectable error.
116231
+ If injectable is false: treat this as TERMINAL \u2014 the app is not a supported native-devtools target, and no relaunch changes that. Do NOT restart/retry. Use the standard \`describe\` tool (its accessibility path reads the screen without injection) or \`screenshot\` (then interact by coordinate). Do not fall back to the native-devtools feature tools (native-describe-screen, native-find-views, native-full-hierarchy, native-network-logs, native-view-at-point, native-user-interactable-view-at-point) \u2014 they run the same injection precheck and fail with the same non-injectable error.
116232
116232
  If appRunning is false and nextLaunchWillBeInjected is true: use launch-app normally.
116233
116233
  If requiresRestart is true: call restart-app once, then proceed with the native feature. Read state before acting on a second such reading \u2014 indeterminate reaches this rule too, and its line below bounds it at that one restart.
116234
116234
  If state is unregistered: do NOT restart the app again \u2014 it already launched under the terms a restart would recreate. Restart the tool-server (\`argent server stop && argent server start --detach\`), then retry. If it reads unregistered again after that restart, stop: the process loads argent's dylib but never dials, and no further restart on either side changes it \u2014 treat native devtools as unavailable, then use \`describe\` or \`screenshot\` and drive by coordinate.
@@ -144653,7 +144653,7 @@ var FULL_HIERARCHY_FIELDS = [
144653
144653
  ];
144654
144654
  async function unreadableHierarchyReason(nativeApi, bundleId) {
144655
144655
  if (!isInjectableBundleId(bundleId)) {
144656
- return `${bundleId} is an Apple system app: it is a platform binary with library validation, so argent's native devtools cannot be relied on to inject into it, and without them a flow has no view hierarchy to resolve selectors against. Replace the selector steps with coordinate ones \u2014 \`tap: { x: 0.5, y: 0.35 }\` takes a point directly and reads no tree \u2014 or target an app argent installs.`;
144656
+ return `${bundleId} is an Apple system app: it is never the app under test, so argent's native devtools refuse to read one, and without them a flow has no view hierarchy to resolve selectors against. Replace the selector steps with coordinate ones \u2014 \`tap: { x: 0.5, y: 0.35 }\` takes a point directly and reads no tree \u2014 or target an app argent installs.`;
144657
144657
  }
144658
144658
  const state3 = await nativeApi.appConnectionState(bundleId).catch(() => "indeterminate");
144659
144659
  if (state3 === "connected") {
@@ -144661,7 +144661,10 @@ async function unreadableHierarchyReason(nativeApi, bundleId) {
144661
144661
  }
144662
144662
  return `${buildAppStateMessage(bundleId, state3)} Flows resolve selectors against the full view hierarchy native devtools serve.`;
144663
144663
  }
144664
- async function queryFullHierarchyTree(registry2, device, launchedNativeApp) {
144664
+ function systemAppFlowTargetRefusal(bundleId) {
144665
+ return `${bundleId} is an Apple system app (com.apple.*) - never a valid flow target: it is not the app under test, and argent's native devtools refuse to read one (a system process either never services the read, or describes offscreen UI as if it were the launched app), so this flow has no view hierarchy to resolve selectors against and no relaunch or retry changes this verdict. Replace the selector steps with coordinate ones - \`tap: { x: 0.5, y: 0.35 }\` takes a point directly and reads no tree - or point this flow's \`launch\` step at the app under test.`;
144666
+ }
144667
+ async function queryFullHierarchyTree(registry2, device, target) {
144665
144668
  let nativeApi;
144666
144669
  try {
144667
144670
  const ndRef = nativeDevtoolsRef(device);
@@ -144672,22 +144675,113 @@ async function queryFullHierarchyTree(registry2, device, launchedNativeApp) {
144672
144675
  { cause: err }
144673
144676
  );
144674
144677
  }
144675
- if (launchedNativeApp !== void 0 && nativeApi.listConnectedBundleIds().length === 0) {
144676
- throw new Error(await unreadableHierarchyReason(nativeApi, launchedNativeApp));
144677
- }
144678
- let target;
144679
- try {
144680
- target = await resolveNativeTargetApp(nativeApi, void 0);
144681
- } catch (err) {
144682
- const timedOut = getFailureSignal(err)?.error_code === FAILURE_CODES.NATIVE_DEVTOOLS_RPC_TIMEOUT;
144683
- if (timedOut && launchedNativeApp !== void 0 && nativeApi.listConnectedBundleIds().includes(launchedNativeApp)) {
144684
- target = { bundleId: launchedNativeApp };
144685
- } else {
144686
- throw err;
144678
+ let bundleId;
144679
+ if (target?.pinned) {
144680
+ bundleId = target.bundleId;
144681
+ if (!isInjectableBundleId(bundleId)) {
144682
+ throw new FailureError(systemAppFlowTargetRefusal(bundleId), {
144683
+ error_code: FAILURE_CODES.NATIVE_DEVTOOLS_NOT_INJECTABLE,
144684
+ failure_stage: "flow_tree_pinned_target",
144685
+ failure_area: "tool_server",
144686
+ error_kind: "validation"
144687
+ });
144688
+ }
144689
+ if (!nativeApi.isConnected(bundleId)) {
144690
+ throw new FailureError(
144691
+ `${bundleId} lost its devtools connection after launch (the app crashed, was terminated, or its socket closed) - restart it (restart-app, or a flow \`launch\` step) so the full view hierarchy is readable; launch-app recovers only the causes that killed the process, since on iOS it just foregrounds one that is still alive`,
144692
+ {
144693
+ error_code: FAILURE_CODES.NATIVE_DEVTOOLS_NOT_CONNECTED,
144694
+ failure_stage: "flow_tree_pinned_target",
144695
+ failure_area: "tool_server",
144696
+ error_kind: "not_found"
144697
+ }
144698
+ );
144687
144699
  }
144700
+ let pinnedState;
144701
+ try {
144702
+ pinnedState = await nativeApi.getAppState(bundleId);
144703
+ target.probeAnswered = true;
144704
+ } catch (err) {
144705
+ if (getFailureSignal(err)?.error_code !== FAILURE_CODES.NATIVE_DEVTOOLS_RPC_TIMEOUT) {
144706
+ throw err;
144707
+ }
144708
+ if (target.probeAnswered) {
144709
+ throw new FailureError(
144710
+ `${bundleId} (the launched app) stopped answering Application.getState - the probe timed out although an earlier one in this run answered, so the app's main queue is no longer being serviced. For a pinned app that is usually the suspension iOS applies once a flow leaves it (e.g. a tap that opened another app), and a suspended app's hierarchy is not what is on screen; in-app work blocking the main thread past the probe timeout looks the same from here. Reading anyway parks on that same unserviced queue: certain to time out if the app is suspended, and paying the longer hierarchy timeout to find that out if it is not. If this flow's subject IS the other app, give the flow a \`launch:\` step for that app - it re-pins reads to it, and a pinned read probes only the app it names, so the silent ${bundleId} is never touched; \`tool: launch-app\` or \`tool: restart-app\` naming that app works too - each re-targets reads at the app it starts, and is how a recorded flow switches apps. A foreground-NEUTRAL raw \`tool:\` step does not work here, because demoting the pin sends reads back to auto-resolve, which probes every connection at once and is sunk by this same silent one. If the flow left ${bundleId} but is still about it, make it return before reading the UI. If it never left, the main thread is busy: raise the step's \`timeout:\` so the poll re-reads past the work, or \`launch\` ${bundleId} again.`,
144711
+ {
144712
+ // The timeout's own code, NOT
144713
+ // NATIVE_TARGET_SINGLE_APP_NOT_FOREGROUND: nothing answered, so no
144714
+ // app state was observed to classify it by.
144715
+ error_code: FAILURE_CODES.NATIVE_DEVTOOLS_RPC_TIMEOUT,
144716
+ failure_stage: "flow_tree_pinned_target",
144717
+ failure_area: "tool_server",
144718
+ error_kind: "timeout"
144719
+ },
144720
+ err instanceof Error ? { cause: err } : void 0
144721
+ );
144722
+ }
144723
+ }
144724
+ if (pinnedState && !chooseFrontmostConnectedApp([pinnedState])) {
144725
+ throw new FailureError(
144726
+ `${bundleId} (the launched app) has no foreground presence at all (applicationState=${pinnedState.applicationState}, foregroundActiveScenes=${pinnedState.foregroundActiveSceneCount}, foregroundInactiveScenes=${pinnedState.foregroundInactiveSceneCount}) - a step in this flow left the app (e.g. a tap that opened another app), so a read of its hierarchy would describe a screen that is not on screen. Transitional states are NOT refused here: an \`inactive\` app, or one still holding a foreground scene, is read as usual - under a system alert or mid-transition it is still the app on screen. If this flow's subject IS another app, give the flow a \`launch:\` step for that app - it re-pins reads to it, and no wedged sibling connection can sink a pinned read; a raw \`tool:\` step demotes the pin and returns reads to frontmost auto-resolve, which works when the app on screen answers but is sunk by a single wedged connection. Otherwise make the flow return to ${bundleId} before reading the UI, or \`launch\` it again.`,
144727
+ {
144728
+ error_code: FAILURE_CODES.NATIVE_TARGET_SINGLE_APP_NOT_FOREGROUND,
144729
+ failure_stage: "flow_tree_pinned_target",
144730
+ failure_area: "tool_server",
144731
+ error_kind: "validation"
144732
+ }
144733
+ );
144734
+ }
144735
+ } else {
144736
+ if (target && nativeApi.listConnectedBundleIds().length === 0) {
144737
+ throw new Error(await unreadableHierarchyReason(nativeApi, target.bundleId));
144738
+ }
144739
+ let resolved;
144740
+ try {
144741
+ resolved = await resolveNativeTargetApp(nativeApi, void 0);
144742
+ } catch (err) {
144743
+ const timedOut = getFailureSignal(err)?.error_code === FAILURE_CODES.NATIVE_DEVTOOLS_RPC_TIMEOUT;
144744
+ if (!timedOut || !target) throw err;
144745
+ if (!isInjectableBundleId(target.bundleId)) {
144746
+ throw new FailureError(
144747
+ systemAppFlowTargetRefusal(target.bundleId),
144748
+ {
144749
+ error_code: FAILURE_CODES.NATIVE_DEVTOOLS_NOT_INJECTABLE,
144750
+ failure_stage: "flow_tree_unpinned_hint",
144751
+ failure_area: "tool_server",
144752
+ error_kind: "validation"
144753
+ },
144754
+ err instanceof Error ? { cause: err } : void 0
144755
+ );
144756
+ }
144757
+ if (!nativeApi.listConnectedBundleIds().includes(target.bundleId)) throw err;
144758
+ let hintState;
144759
+ try {
144760
+ hintState = await nativeApi.getAppState(target.bundleId);
144761
+ } catch (probeErr) {
144762
+ if (getFailureSignal(probeErr)?.error_code === FAILURE_CODES.NATIVE_DEVTOOLS_RPC_TIMEOUT) {
144763
+ throw err;
144764
+ }
144765
+ throw probeErr;
144766
+ }
144767
+ if (!chooseFrontmostConnectedApp([hintState])) {
144768
+ throw new FailureError(
144769
+ `${target.bundleId} (the launched app) has no foreground presence at all (applicationState=${hintState.applicationState}, foregroundActiveScenes=${hintState.foregroundActiveSceneCount}, foregroundInactiveScenes=${hintState.foregroundInactiveSceneCount}) - auto-resolve's probe of every connection timed out and the read fell back to the launched app, but a step in this flow left it (e.g. a tap that opened another app), so reading its hierarchy would describe a screen that is not on screen. Transitional states are NOT refused here: an \`inactive\` app, or one still holding a foreground scene, is read as usual. If this flow's subject IS another app, give the flow a \`launch:\` step for that app; otherwise make the flow return to ${target.bundleId} before reading the UI, or \`launch\` it again.`,
144770
+ {
144771
+ error_code: FAILURE_CODES.NATIVE_TARGET_SINGLE_APP_NOT_FOREGROUND,
144772
+ failure_stage: "flow_tree_unpinned_hint",
144773
+ failure_area: "tool_server",
144774
+ error_kind: "validation"
144775
+ },
144776
+ err instanceof Error ? { cause: err } : void 0
144777
+ );
144778
+ }
144779
+ resolved = { bundleId: target.bundleId };
144780
+ }
144781
+ bundleId = resolved.bundleId;
144688
144782
  }
144689
144783
  const rawResult = await nativeApi.queryViewHierarchy(
144690
- target.bundleId,
144784
+ bundleId,
144691
144785
  "ViewHierarchy.getFullHierarchy",
144692
144786
  {
144693
144787
  fields: FULL_HIERARCHY_FIELDS,
@@ -144695,11 +144789,11 @@ async function queryFullHierarchyTree(registry2, device, launchedNativeApp) {
144695
144789
  }
144696
144790
  );
144697
144791
  if (rawResult.error) {
144698
- throw new Error(`getFullHierarchy failed for ${target.bundleId}: ${rawResult.error}`);
144792
+ throw new Error(`getFullHierarchy failed for ${bundleId}: ${rawResult.error}`);
144699
144793
  }
144700
144794
  if (!Array.isArray(rawResult.windows) || rawResult.windows.length === 0) {
144701
144795
  throw new Error(
144702
- `getFullHierarchy returned no windows for ${target.bundleId} \u2014 the app is not injectable (e.g. an Apple system app) or has no readable foreground window, so flows cannot resolve selectors against its view hierarchy`
144796
+ `getFullHierarchy returned no windows for ${bundleId} - it has no window attached to read (backgrounded, or its first window not attached yet), so flows cannot resolve selectors against its view hierarchy; foreground or relaunch it, and if that bundle id is a com.apple.* system process the read resolved to a background system app rather than the app under test, so give this flow a \`launch\` step to pin reads to the right app`
144703
144797
  );
144704
144798
  }
144705
144799
  const { tree, screen } = adaptFullHierarchy(rawResult);
@@ -144906,13 +145000,15 @@ async function queryVegaTree(device) {
144906
145000
  }
144907
145001
 
144908
145002
  // ../tool-server/src/tools/flows/flow-tree.ts
144909
- async function fetchFlowTree(registry2, device, launchedNativeApp) {
145003
+ async function fetchFlowTree(registry2, device, target) {
144910
145004
  const source = FLOW_TREE_SOURCES[device.platform];
144911
145005
  if (!source) return fetchTree(registry2, device);
144912
- return source(registry2, device, launchedNativeApp);
145006
+ return source(registry2, device, target);
144913
145007
  }
144914
145008
  var FLOW_TREE_SOURCES = {
144915
- ios: (registry2, device, launchedNativeApp) => queryFullHierarchyTree(registry2, device, launchedNativeApp),
145009
+ // Only iOS consumes the target: the platforms below resolve their tree
145010
+ // source per-device and never auto-resolve.
145011
+ ios: (registry2, device, target) => queryFullHierarchyTree(registry2, device, target),
144916
145012
  android: (registry2, device) => queryAndroidFullHierarchy(registry2, device),
144917
145013
  chromium: (registry2, device) => queryChromiumTree(registry2, device),
144918
145014
  vega: (_registry, device) => queryVegaTree(device)
@@ -145118,7 +145214,7 @@ function provenTreeOutage(env) {
145118
145214
  return proven && proven.deviceId === env.device.id ? proven.error : void 0;
145119
145215
  }
145120
145216
  function readFlowTree(env) {
145121
- return fetchFlowTree(env.registry, env.device, env.launchedNativeApp).then((data) => {
145217
+ return fetchFlowTree(env.registry, env.device, env.treeTarget).then((data) => {
145122
145218
  if (env.treeOutage) env.treeOutage.proven = void 0;
145123
145219
  return data;
145124
145220
  });
@@ -146173,7 +146269,11 @@ async function captureTapSelector(registry2, session, udid, point) {
146173
146269
  try {
146174
146270
  const device = resolveDevice(udid);
146175
146271
  const launched = recordedLaunchedApp(session, device.platform);
146176
- const { tree, source } = await fetchFlowTree(registry2, device, launched);
146272
+ const { tree, source } = await fetchFlowTree(
146273
+ registry2,
146274
+ device,
146275
+ launched ? { bundleId: launched, pinned: false, probeAnswered: false } : void 0
146276
+ );
146177
146277
  const node = nodeAtPoint(tree, point);
146178
146278
  if (!node) return { warning: "no element found under the tap; kept coordinates (brittle)" };
146179
146279
  const selector = deriveSelector(node);
@@ -149111,6 +149211,7 @@ async function runLaunch(state3, app) {
149111
149211
  reason: `no app id declared for platform "${device.platform}" \u2014 add a launch entry for it`
149112
149212
  };
149113
149213
  }
149214
+ state3.treeTarget = void 0;
149114
149215
  let restart;
149115
149216
  try {
149116
149217
  restart = await invokeOnDevice(env, "restart-app", { bundleId });
@@ -149125,7 +149226,7 @@ async function runLaunch(state3, app) {
149125
149226
  const gate = await treeSourceGate(registry2, device, bundleId, signal);
149126
149227
  if (signal?.aborted) return ABORTED_OUTCOME;
149127
149228
  if (gate) return { ok: false, reason: gate };
149128
- state3.launchedNativeApp = bundleId;
149229
+ state3.treeTarget = { bundleId, pinned: true, probeAnswered: false };
149129
149230
  return { ok: true };
149130
149231
  }
149131
149232
  async function runChromiumLaunch(state3, app) {
@@ -149312,7 +149413,11 @@ function createRunFlowTool(registry2) {
149312
149413
  },
149313
149414
  description: `Run a saved flow from the .argent/flows/ directory, or an explicit boundary-managed flow_path.
149314
149415
  Steps run in order: \`launch\` starts an app from scratch (terminate + relaunch) and waits until it is
149315
- ready; \`tool\` calls dispatch through the registry; \`tap\`/\`long-press\`/\`type\` resolve a selector to an
149416
+ ready (on iOS it also pins later element lookups to that app rather than auto-detecting the frontmost
149417
+ one); \`tool\` calls dispatch through the registry (a raw \`tool\` step ends that iOS pin, so lookups
149418
+ auto-detect again until the next \`launch\`, though a tool that cannot change the foreground app leaves the
149419
+ launched id as a fallback for a timed-out auto-detect, and \`launch-app\`/\`restart-app\` leave the id they
149420
+ started as that fallback instead); \`tap\`/\`long-press\`/\`type\` resolve a selector to an
149316
149421
  element and act on it (\`tap: { on, times: 2 }\` double-taps; \`long-press: { on, duration }\` presses and
149317
149422
  holds; \`tap\`/\`long-press\` alternatively take a raw normalized point \u2014 bare \`{ x, y }\` or \`on: { x, y }\`;
149318
149423
  any selector may scope its matches geometrically, the CSS combinators read off frames: \`within: <selector>\`
@@ -150021,14 +150126,17 @@ async function execLeafStep(state3, step, index, scope) {
150021
150126
  if (step.delayMs && !await sleepOrAbort(step.delayMs, signal)) {
150022
150127
  return { ...base, status: "skip", tool: step.name, reason: "run aborted during delay" };
150023
150128
  }
150129
+ if (FOREGROUND_CHANGING_TOOLS.has(step.name)) {
150130
+ state3.treeTarget = void 0;
150131
+ if (state3.treeOutage) state3.treeOutage.proven = void 0;
150132
+ } else if (state3.treeTarget?.pinned) {
150133
+ state3.treeTarget = { ...state3.treeTarget, pinned: false };
150134
+ if (state3.treeOutage) state3.treeOutage.proven = void 0;
150135
+ }
150136
+ if (isNestedOrchestratorTool(step.name) && state3.treeOutage) {
150137
+ state3.treeOutage.proven = void 0;
150138
+ }
150024
150139
  try {
150025
- if (FOREGROUND_CHANGING_TOOLS.has(step.name)) {
150026
- state3.launchedNativeApp = void 0;
150027
- if (state3.treeOutage) state3.treeOutage.proven = void 0;
150028
- }
150029
- if (isNestedOrchestratorTool(step.name) && state3.treeOutage) {
150030
- state3.treeOutage.proven = void 0;
150031
- }
150032
150140
  const result = await invokeSubTool(registry2, ctx, step.name, args);
150033
150141
  if (isUnmetUiWaitResult(step.name, result)) {
150034
150142
  const note = result.note;
@@ -150075,7 +150183,9 @@ async function execLeafStep(state3, step, index, scope) {
150075
150183
  }
150076
150184
  if (step.name === "launch-app" || step.name === "restart-app") {
150077
150185
  const launched = args.bundleId;
150078
- if (typeof launched === "string") state3.launchedNativeApp = launched;
150186
+ if (typeof launched === "string") {
150187
+ state3.treeTarget = { bundleId: launched, pinned: false, probeAnswered: false };
150188
+ }
150079
150189
  }
150080
150190
  return { ...base, status: "pass", tool: step.name, result, outputHint, args };
150081
150191
  } catch (err) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swmansion/argent",
3
- "version": "0.22.1-next.7",
3
+ "version": "0.22.1-next.8",
4
4
  "mcpName": "io.github.software-mansion/argent",
5
5
  "description": "MCP server for iOS Simulator and Android Emulator control",
6
6
  "license": "Apache-2.0",
@@ -70,6 +70,12 @@ On iOS, Android, and Chromium, an id absent from `describe` can still resolve in
70
70
 
71
71
  The recorder rechecks each successful `await-ui-element` against the runner tree. Follow any `message` warning and replay each conversion. On Vega, a mismatch usually means the screen changed. A `text` check can also select different elements from the same source. See [Live waits and checks](live-authoring.md#live-waits-and-checks).
72
72
 
73
+ **On iOS, a `launch:` step also decides which app the runner reads.** A successful `launch:` pins later runner-tree reads to that app, so a read probes only that app instead of fanning out over every connected one to find the frontmost. A pinned read still refuses, naming the reason, when the app has no foreground presence left, when it stops answering after an earlier read got through, when its devtools connection dropped, or when the pinned id is a `com.apple.*` system app.
74
+
75
+ Any raw `tool:` step ends the pin, because its effect on the screen is opaque to the runner, and reads auto-detect the frontmost app again until the next `launch:` re-pins. A tool that cannot change the foreground app leaves the launched id as an unpinned fallback, which takes the read only when auto-detection times out and the launched app vouches for itself with a probe of its own. `launch-app`, `restart-app`, `reinstall-app`, `open-url`, and `button` drop even that; `launch-app` and `restart-app` replace it with the app they just started, still unpinned. Nested `run:` fragments inherit both the pin and its clearing.
76
+
77
+ So on iOS recording and replay can read different apps, not only different projections: recording has no run state and always auto-detects the frontmost connected app, while a replay read between a `launch:` and the next raw `tool:` step reads the launched app.
78
+
73
79
  **On iOS, never copy a `role` from `describe` into a flow selector.** The runner derives iOS roles from the UIView class name and `describe` from accessibility traits, so a React Native `Pressable` (class `RCTView`) is `AXGroup` to the runner and `AXButton` to `describe`. Select on `id`/`text`, or confirm the role against the runner's own tree.
74
80
 
75
81
  When several nodes match, the directive decides:
@@ -99,7 +105,7 @@ Scopes can combine and nest, with at most six scope keys. Use strict selectors f
99
105
 
100
106
  Directives stop the flow on failure and skip later steps. `flow-execute` documents their shapes. The available directives are `launch`, `tap`, `long-press`, `type`, `scroll-to`, `pinch`, `rotate`, `await`, `assert`, `wait`, `snapshot`, `run`, `when`, `echo`, and `tool`.
101
107
 
102
- Use the launch map for cross-platform flows. A bare launch applies everywhere and becomes an app path on Chromium. The map takes `native:`, `ios:`, `android:`, `vega:`, and `chromium:`. `native:` is one id shared by iOS, Android, and Vega, and a per-platform key overrides it for that platform. `chromium:` accepts a relative or absolute app path. A launch that declares no id for the run's platform is an error, not a cue to switch platforms.
108
+ Use the launch map for cross-platform flows. A bare launch applies everywhere and becomes an app path on Chromium. The map takes `native:`, `ios:`, `android:`, `vega:`, and `chromium:`. `native:` is one id shared by iOS, Android, and Vega, and a per-platform key overrides it for that platform. `chromium:` accepts a relative or absolute app path. A launch that declares no id for the run's platform is an error, not a cue to switch platforms. On iOS, a successful launch also pins later tree reads to that app until the next raw `tool:` step, so read [The runner tree is not the discovery tree](#the-runner-tree-is-not-the-discovery-tree) when a read describes the wrong screen.
103
109
 
104
110
  ```yaml
105
111
  - launch: { native: com.acme.app, chromium: ../../app }
@@ -53,9 +53,9 @@ Use the same explicit UDID throughout. Multiple booted simulators are not an inj
53
53
 
54
54
  This fallback applies only to `com.apple.*` system apps. A connection failure in another app never authorizes it.
55
55
 
56
- Apple system apps are platform binaries with library validation, so the instrumentation cannot be relied on to load into them it has been seen both loading and not loading, depending on the simulator runtime. Either way it is no basis for a selector.
56
+ Argent refuses `com.apple.*` bundle ids at every native-devtools read that names one, because a system app is never the app under test. The instrumentation has been seen both loading and not loading into one, depending on the simulator runtime either way it is no basis for a selector. `restart-app`, `launch-app`, and `describe` still work on one; it just never gets a flow tree.
57
57
 
58
- Give the flow a `launch:` step as usual. On iOS the launch waits the full devtools budget out, then passes for one of these bundle ids: starting the app is all that step is for, and a coordinate-driven flow needs nothing more. The flow stays e2e; it just pays roughly sixteen seconds at the launch. Where the impossibility bites is selector resolution, and the first selector step reports it there — terminally, naming the coordinate remedy — rather than as a launch failure. The rest of the injection-free form:
58
+ Give the flow a `launch:` step as usual. On iOS the launch waits the full devtools budget out, then passes for one of these bundle ids: starting the app is all that step is for, and a coordinate-driven flow needs nothing more. The flow stays e2e; it just pays roughly sixteen seconds at the launch. Where the refusal bites is selector resolution, and the first selector step reports it there — terminally, naming the coordinate remedy — rather than as a launch failure. The rest of the tree-free form:
59
59
 
60
60
  - Raw `tool: await-ui-element` accessibility checks.
61
61
  - Point taps or long-presses derived from `describe`, each named by an echo.
@@ -66,9 +66,9 @@ Every point tap or long-press in such a flow passes **carrying a warning** for a
66
66
 
67
67
  A recorded wait carries a different warning: it adds about one second and reports that the runner tree is unavailable. That warning is expected too. Keep the wait as a raw `tool:` step.
68
68
 
69
- Report that the flow is injection-free and its coordinates are not portable. It cannot satisfy the QA contract. Report the artifact and platform blocker instead.
69
+ Report that the flow has no flow tree and its coordinates are not portable. It cannot satisfy the QA contract. Report the artifact and platform blocker instead.
70
70
 
71
- A normally injectable app that is broken in the environment gets the same coordinate-only treatment, but not the same launch: there the `launch:` step fails, since the gate withholds its verdict only for a bundle id injection may never reach. Start such a flow with a raw `tool: restart-app`, which terminates and relaunches without the readiness gate, and accept that the result is a **fragment** — its first non-echo step is not `launch:`, so the runner never classifies it as e2e, and it cannot complete `argent-qa-flows`, which requires a leading `launch:`. Report the blocker rather than labeling that fallback a completed QA test.
71
+ A normally injectable app that is broken in the environment gets the same coordinate-only treatment, but not the same launch: there the `launch:` step fails, since the gate withholds its verdict only for a bundle id argent refuses outright. Start such a flow with a raw `tool: restart-app`, which terminates and relaunches without the readiness gate, and accept that the result is a **fragment** — its first non-echo step is not `launch:`, so the runner never classifies it as e2e, and it cannot complete `argent-qa-flows`, which requires a leading `launch:`. Report the blocker rather than labeling that fallback a completed QA test.
72
72
 
73
73
  ## Tree source recovery on Android, Chromium, and Vega
74
74