Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions .claude/skills/ios-sim-navigation/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,32 @@ Run these from the project root that should own the
`.build/WebDriverAgent` cache. `wda-start.rb` resolves the path
relative to its working directory and clones into it on first run.

## Launching the app with custom options (caller-supplied)

By default you don't launch the app yourself — the first `tap.rb` binds a
session to whatever app is in the foreground. But some callers need the app
launched with **specific launch arguments or environment variables**: test
configuration, feature flags, or instrumentation that an external instrument
reads from the app's environment (a profiler, a leak detector, etc.).

This skill is agnostic about *what* those options are. It just gives the caller
a way to inject them: launch the app through WDA with `scripts/wda-session.rb`
**before** any `tap.rb` call, so the instrumented process is the one WDA drives.

```bash
# Launch arguments (order-preserving; a `-key value` pair is two --arg tokens).
ruby scripts/wda-session.rb --bundle com.example.app --arg -some-flag --arg value

# Environment variables (e.g. to enable an instrument the caller cares about).
ruby scripts/wda-session.rb --bundle com.example.app --env SOME_INSTRUMENT_VAR=1
```

Don't substitute `simctl launch` for this — its options are silently discarded
when WDA binds the session. Establish the `wda-session.rb` session first, then
drive normally; don't `simctl launch` again or delete the session file mid-run
(either relaunches the app without the options). `references/sessions.md`
explains why.

## Tap — the default action

**Use `scripts/tap.rb` for every tap.** It collapses session creation
Expand Down
34 changes: 25 additions & 9 deletions .claude/skills/ios-sim-navigation/references/sessions.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,24 +35,40 @@ Don't mint a fresh session with `alwaysMatch:{}` for these calls — that
produces an unbound session whose `/actions` requests return HTTP 200
but never reach the UI.

## Launching with arguments: use `wda-session.rb`, not `simctl`
## Launching with arguments or environment: use `wda-session.rb`, not `simctl`

Creating a session relaunches the target app even when it's already running
(`forceAppLaunch` defaults to `YES`), so any arguments passed via
`simctl launch -key value` are lost — they belong to the original process, and
the relaunch starts a new one.
(`forceAppLaunch` defaults to `YES`), so any arguments or environment passed via
`simctl launch` are lost — they belong to the original process, and the relaunch
starts a new one.

So when the app needs launch arguments, don't pass them with `simctl`. Let WDA
launch the app with them in the call that creates the session, using
`scripts/wda-session.rb`. It persists the session so `tap.rb` reuses it (no
further relaunch):
So when the app needs launch arguments or environment variables, don't pass them
with `simctl`. Let WDA launch the app with them in the call that creates the
session, using `scripts/wda-session.rb`. It persists the session so `tap.rb`
reuses it (no further relaunch):

```bash
# Launch arguments (order-preserving):
ruby scripts/wda-session.rb --bundle com.example.app \
--arg -some-flag --arg value

# Environment variables (--env is repeatable; split on the first '='):
ruby scripts/wda-session.rb --bundle com.example.app \
--env MallocStackLogging=1 --env SOME_FLAG=on
```

Once that session exists, avoid forcing another relaunch before the arguments
`--arg` populates the app's `launchArguments`; `--env` populates its
`launchEnvironment`. Both ride along as WDA session capabilities (`arguments`
and `environment` under `alwaysMatch`), so WDA's launch is the one that carries
them.

The skill is agnostic about what these are — they're whatever the *caller*
needs. A common use of `--env` is enabling an external instrument that reads the
app's environment at launch (for example, a memory tool that needs malloc stack
logging turned on in the target process). The skill doesn't need to know which
instrument; the caller passes the variable.

Once that session exists, avoid forcing another relaunch before the options
are consumed (don't `simctl launch` again, don't delete the session file).
`--wait-quiescence` is off by default because a screen with a spinner can keep
the app from going quiescent and stall the call.
Expand Down
52 changes: 39 additions & 13 deletions .claude/skills/ios-sim-navigation/scripts/wda-session.rb
Original file line number Diff line number Diff line change
@@ -1,26 +1,40 @@
#!/usr/bin/env ruby
# wda-session.rb — Create a WDA session bound to an app, launching it with
# launch arguments, and persist the session so tap.rb reuses it.
# caller-supplied launch arguments and/or environment variables, and persist
# the session so tap.rb reuses it.
#
# Why this exists: creating a WDA session relaunches the target app by default
# (forceAppLaunch defaults to YES), which discards any arguments passed via
# `simctl launch -key value` (they belong to the original process). This script
# instead lets WDA launch the app with the arguments, so the process WDA drives
# has them, and persists the session so tap.rb reuses it (no further relaunch).
# (forceAppLaunch defaults to YES), which discards any arguments or environment
# passed via `simctl launch` (they belong to the original process, and the
# relaunch starts a fresh one). This script instead lets WDA launch the app with
# the arguments and environment, so the process WDA drives has them, and persists
# the session so tap.rb reuses it (no further relaunch).
#
# This script is agnostic about WHAT the arguments and environment are — the
# caller decides. They might be test configuration, feature flags, or
# instrumentation an instrument needs in the app's environment (e.g. a profiler
# or leak detector toggled by an env var). See references/sessions.md.
#
# Usage:
# # Launch arguments (e.g. test configuration):
# wda-session.rb --bundle com.automattic.jetpack \
# --arg -ui-test-site-url --arg https://example.com \
# --arg -ui-test-site-user --arg demo \
# --arg -ui-test-site-pass --arg secret
#
# # Environment variables (e.g. enabling an instrument):
# wda-session.rb --bundle com.automattic.jetpack --env MallocStackLogging=1
#
# Each --arg contributes one token to launchArguments, in order. A
# `-key value` pair is two --arg values, exactly as you'd pass them to
# `simctl launch`.
# `simctl launch`. Each --env contributes one KEY=VALUE pair to the app's
# launch environment (split on the first '=').
#
# Options:
# --bundle ID Required. App bundle id to launch and bind to.
# --arg VALUE Repeatable. One launch-argument token (order preserved).
# --env KEY=VALUE Repeatable. One environment variable for the app's
# launch environment.
# --port PORT WDA port (default: 8100, or $WDA_PORT).
# --wait-quiescence Wait for app quiescence after launch (default: off;
# a spinning login screen can keep an app from going
Expand All @@ -36,12 +50,21 @@
port = (ENV["WDA_PORT"] || 8100).to_i
bundle = nil
args = []
env = {}
wait_quiescence = false

parser = OptionParser.new do |opts|
opts.banner = "Usage: wda-session.rb --bundle ID [--arg VALUE ...] [--port PORT] [--wait-quiescence]"
opts.banner = "Usage: wda-session.rb --bundle ID [--arg VALUE ...] [--env KEY=VALUE ...] [--port PORT] [--wait-quiescence]"
opts.on("--bundle ID") { |v| bundle = v }
opts.on("--arg VALUE") { |v| args << v }
opts.on("--env KEY=VALUE") do |v|
key, value = v.split("=", 2)
if key.nil? || key.empty? || value.nil?
$stderr.puts "error: --env expects KEY=VALUE, got: #{v}"
exit 2
end
env[key] = value
end
opts.on("--port PORT", Integer) { |v| port = v }
opts.on("--wait-quiescence") { wait_quiescence = true }
end
Expand All @@ -52,13 +75,16 @@
exit 2
end

caps = {
"alwaysMatch" => {
"bundleId" => bundle,
"arguments" => args,
"shouldWaitForQuiescence" => wait_quiescence
}
always_match = {
"bundleId" => bundle,
"arguments" => args,
"shouldWaitForQuiescence" => wait_quiescence
}
# Only include the environment key when set, so a plain session isn't given an
# empty launch environment.
always_match["environment"] = env unless env.empty?

caps = { "alwaysMatch" => always_match }

uri = URI("http://localhost:#{port}/session")
req = Net::HTTP::Post.new(uri, "Content-Type" => "application/json")
Expand Down