@osfactory/har 0.48.0 → 0.49.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/templates/har-boilerplate/provision-toolchain.sh +5 -4
- package/dist/templates/har-boilerplate-cli/provision-toolchain.sh +5 -4
- package/dist/templates/har-boilerplate-ios/CLAUDE.agent.md +13 -9
- package/dist/templates/har-boilerplate-ios/README.md +34 -5
- package/dist/templates/har-boilerplate-ios/agent-cli.sh +39 -15
- package/dist/templates/har-boilerplate-ios/gitignore.template +1 -0
- package/dist/templates/har-boilerplate-ios/harness.env +28 -4
- package/dist/templates/har-boilerplate-ios/launch.sh +33 -1
- package/dist/templates/har-boilerplate-ios/provision-toolchain.sh +5 -4
- package/dist/templates/har-boilerplate-ios/setup-infra.sh +56 -39
- package/dist/templates/har-boilerplate-ios/simulator.sh +400 -0
- package/dist/templates/har-boilerplate-ios/teardown.sh +4 -0
- package/dist/templates/plugins/rocketsim/.har/stages/ROCKETSIM.md +3 -1
- package/dist/templates/plugins/rocketsim/.har/stages/rocketsim-flows.sh +2 -0
- package/dist/templates/plugins/rocketsim/flows/README.md +3 -1
- package/dist/templates/plugins/rocketsim/flows/example-smoke.sh +4 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ HAR is an open-source, agent-agnostic framework for building multi-agent coding
|
|
|
14
14
|
<tr>
|
|
15
15
|
<td align="center">
|
|
16
16
|
<a href="https://youtu.be/XKl4ZzWy7mQ">
|
|
17
|
-
<img src="
|
|
17
|
+
<img src="http://img.youtube.com/vi/XKl4ZzWy7mQ/maxresdefault.jpg" alt="HAR introduction demo" width="600"/>
|
|
18
18
|
</a>
|
|
19
19
|
</td>
|
|
20
20
|
</tr>
|
|
@@ -108,7 +108,7 @@ See [CONTRIBUTING.md](./CONTRIBUTING.md) for local setup, the dogfood harness lo
|
|
|
108
108
|
|
|
109
109
|
## Sponsors
|
|
110
110
|
|
|
111
|
-
HAR is sponsored by [Kerno](https://kerno.io), runtime code and security
|
|
111
|
+
HAR is sponsored by [Kerno](https://kerno.io), runtime code and security tester for coding agents.
|
|
112
112
|
|
|
113
113
|
<p align="left">
|
|
114
114
|
<a href="https://kerno.io">
|
|
@@ -30,7 +30,9 @@ pt_log() {
|
|
|
30
30
|
append_env() {
|
|
31
31
|
local key="$1"
|
|
32
32
|
local value="$2"
|
|
33
|
-
|
|
33
|
+
# %q, not a raw value: an unquoted value with spaces (an Xcode scheme, a
|
|
34
|
+
# simulator name, a path under /Applications) breaks `source .env.agent.<id>`.
|
|
35
|
+
printf '%s=%q\n' "$key" "$value" >> "$HAR_ENV_FILE"
|
|
34
36
|
}
|
|
35
37
|
|
|
36
38
|
append_path_prefix() {
|
|
@@ -235,9 +237,8 @@ provision_ios() {
|
|
|
235
237
|
if [ -n "${HARNESS_XCODE_SCHEME:-}" ]; then
|
|
236
238
|
append_env "HARNESS_XCODE_SCHEME" "$HARNESS_XCODE_SCHEME"
|
|
237
239
|
fi
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
fi
|
|
240
|
+
# The simulator is not recorded here: launch.sh writes the device reserved for
|
|
241
|
+
# this slot, and harness.env already carries the shared one.
|
|
241
242
|
if [ -n "${HARNESS_BUNDLE_ID:-}" ]; then
|
|
242
243
|
append_env "HARNESS_BUNDLE_ID" "$HARNESS_BUNDLE_ID"
|
|
243
244
|
fi
|
|
@@ -30,7 +30,9 @@ pt_log() {
|
|
|
30
30
|
append_env() {
|
|
31
31
|
local key="$1"
|
|
32
32
|
local value="$2"
|
|
33
|
-
|
|
33
|
+
# %q, not a raw value: an unquoted value with spaces (an Xcode scheme, a
|
|
34
|
+
# simulator name, a path under /Applications) breaks `source .env.agent.<id>`.
|
|
35
|
+
printf '%s=%q\n' "$key" "$value" >> "$HAR_ENV_FILE"
|
|
34
36
|
}
|
|
35
37
|
|
|
36
38
|
append_path_prefix() {
|
|
@@ -235,9 +237,8 @@ provision_ios() {
|
|
|
235
237
|
if [ -n "${HARNESS_XCODE_SCHEME:-}" ]; then
|
|
236
238
|
append_env "HARNESS_XCODE_SCHEME" "$HARNESS_XCODE_SCHEME"
|
|
237
239
|
fi
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
fi
|
|
240
|
+
# The simulator is not recorded here: launch.sh writes the device reserved for
|
|
241
|
+
# this slot, and harness.env already carries the shared one.
|
|
241
242
|
if [ -n "${HARNESS_BUNDLE_ID:-}" ]; then
|
|
242
243
|
append_env "HARNESS_BUNDLE_ID" "$HARNESS_BUNDLE_ID"
|
|
243
244
|
fi
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|--|--|
|
|
9
9
|
| **Agent ID** | ${AGENT_ID} |
|
|
10
10
|
| **Work dir** | Fresh session worktree per launch — see the launch output or `.har/slots/agent-${AGENT_ID}.json` |
|
|
11
|
-
| **Simulator** |
|
|
11
|
+
| **Simulator** | Created for this slot at launch — `HARNESS_SIMULATOR_UDID` in `.env.agent.${AGENT_ID}` |
|
|
12
12
|
| **Scheme** | Configured in `.har/harness.env` — `HARNESS_XCODE_SCHEME` |
|
|
13
13
|
|
|
14
14
|
**Never edit the main checkout** — launch FIRST, then make ALL file edits under the work dir from the launch output. An occupied slot always blocks a new launch — run `har env teardown <id>` (or `complete <id>`) first, then launch again.
|
|
@@ -50,21 +50,25 @@ Stages are the harness's single vocabulary for checks: templates and custom stag
|
|
|
50
50
|
|
|
51
51
|
## Project commands
|
|
52
52
|
|
|
53
|
+
Target **this slot's** simulator, never `booted` — another agent's device may also
|
|
54
|
+
be booted. `HARNESS_IOS_DESTINATION` and `HARNESS_SIMULATOR_UDID` come from
|
|
55
|
+
`.env.agent.${AGENT_ID}`.
|
|
56
|
+
|
|
53
57
|
```bash
|
|
58
|
+
source .env.agent.${AGENT_ID}
|
|
59
|
+
|
|
54
60
|
# Build
|
|
55
|
-
xcodebuild build -scheme MyApp -destination
|
|
61
|
+
xcodebuild build -scheme MyApp -destination "$HARNESS_IOS_DESTINATION" CODE_SIGNING_ALLOWED=NO
|
|
56
62
|
|
|
57
63
|
# Run unit tests
|
|
58
|
-
xcodebuild test -scheme MyApp -destination
|
|
59
|
-
|
|
60
|
-
# Install app on booted simulator
|
|
61
|
-
xcrun simctl install booted path/to/MyApp.app
|
|
64
|
+
xcodebuild test -scheme MyApp -destination "$HARNESS_IOS_DESTINATION" CODE_SIGNING_ALLOWED=NO
|
|
62
65
|
|
|
63
|
-
#
|
|
64
|
-
|
|
66
|
+
# Install and launch on this slot's device
|
|
67
|
+
./.har/agent-cli.sh ${AGENT_ID} install path/to/MyApp.app
|
|
68
|
+
./.har/agent-cli.sh ${AGENT_ID} launch-app
|
|
65
69
|
```
|
|
66
70
|
|
|
67
|
-
Adapt for your scheme
|
|
71
|
+
Adapt for your scheme — see `.har/harness.env`.
|
|
68
72
|
|
|
69
73
|
## User-flow validation (RocketSim)
|
|
70
74
|
|
|
@@ -18,8 +18,9 @@ Generated and maintained by [`har`](https://github.com/os-factory/har). Run `har
|
|
|
18
18
|
| `runs/` | Run history from `har env` / MCP — gitignored |
|
|
19
19
|
| `artifacts/` | Stage outputs: test results, screenshots, logs |
|
|
20
20
|
| `agent-slot.sh` | Shared agent-id validation and slot registry helpers |
|
|
21
|
-
| `setup-infra.sh` |
|
|
22
|
-
| `
|
|
21
|
+
| `setup-infra.sh` | Check the toolchain; start optional Docker services |
|
|
22
|
+
| `simulator.sh` | Create, boot and delete one iOS Simulator per agent slot |
|
|
23
|
+
| `launch.sh` | Launch one agent slot (git worktree, toolchain provisioning, simulator, env file) |
|
|
23
24
|
| `provision-toolchain.sh` | Write Xcode/simulator paths (`XCODEBUILD_BIN`, …) to `.env.agent.<id>` |
|
|
24
25
|
| `verify.sh` | Verification pipeline (build smoke by default; --full adds tests, lint, flows) |
|
|
25
26
|
| `teardown.sh` | Tear down one agent slot (worktree + env file) |
|
|
@@ -82,9 +83,37 @@ dev setup and add a readiness command when agents need a real workflow to pass.
|
|
|
82
83
|
Edit **`harness.env`** to set:
|
|
83
84
|
- `HARNESS_XCODE_SCHEME` — your app's shared Xcode scheme
|
|
84
85
|
- `HARNESS_XCODE_WORKSPACE` or `HARNESS_XCODE_PROJECT` — path to the project file
|
|
85
|
-
- `HARNESS_SIMULATOR_NAME` —
|
|
86
|
+
- `HARNESS_SIMULATOR_NAME` — preferred simulator, matched by exact name
|
|
86
87
|
- `HARNESS_BUNDLE_ID` — app bundle identifier
|
|
87
88
|
|
|
89
|
+
## Simulators
|
|
90
|
+
|
|
91
|
+
Slots do not share a simulator. Each launch **creates**
|
|
92
|
+
`har-<project>-agent-<id>-<model>` — `har-storefront-agent-2-iPhone-17`, say — and
|
|
93
|
+
teardown deletes it, so two agents never collide on a destination, on an installed
|
|
94
|
+
bundle id, or on a UI session, and the simulators you use by hand are never touched.
|
|
95
|
+
Every launch also starts from a pristine device: no app installed, no leftover
|
|
96
|
+
store, nothing carried over from the previous session.
|
|
97
|
+
|
|
98
|
+
| Setting | Effect |
|
|
99
|
+
|---------|--------|
|
|
100
|
+
| `HARNESS_SIMULATOR_NAME` | The model to create — `iPhone 16`, `iPhone 17 Pro`, `iPad Air 11-inch (M2)`. Empty means the newest model of the family. See `xcrun simctl list devicetypes`. |
|
|
101
|
+
| `HARNESS_SIMULATOR_FAMILY` | `auto` (read from the name) \| `iPhone` \| `iPad`. An iPad is only created when this resolves to iPad. |
|
|
102
|
+
| `HARNESS_SIMULATOR_UDID` | Run every slot on one existing device instead. Slots then share it — for one-off debugging. |
|
|
103
|
+
| `HARNESS_SIMULATOR_SHARED` | `true` restores one shared simulator for all slots, booted by `setup-infra.sh`. |
|
|
104
|
+
|
|
105
|
+
The runtime is the newest installed iOS that supports the model — a model retired
|
|
106
|
+
from recent runtimes still resolves against an older one. When nothing matches,
|
|
107
|
+
launch fails and lists the models this machine can create.
|
|
108
|
+
|
|
109
|
+
If `HARNESS_SIMULATOR_NAME` is not a model but names an existing device, that
|
|
110
|
+
device is used as-is and never deleted — how a hand-renamed simulator is targeted.
|
|
111
|
+
|
|
112
|
+
The device lands in `.env.agent.<id>` as `HARNESS_SIMULATOR_UDID`,
|
|
113
|
+
`HARNESS_SIMULATOR_DEVICE_NAME` and `HARNESS_IOS_DESTINATION` — the model stays in
|
|
114
|
+
`HARNESS_SIMULATOR_NAME`, so the two never mean the same thing in one place; what a slot holds is tracked in `.har/simulators/`.
|
|
115
|
+
Use `./.har/agent-cli.sh <id> simulator` to see it.
|
|
116
|
+
|
|
88
117
|
## Port & shared services (iOS profile)
|
|
89
118
|
|
|
90
119
|
Pure iOS apps have **no per-slot TCP ports** — agents build and run on a shared iOS Simulator. Optional backend containers (mock API, etc.) use the same shared-infra model as other profiles.
|
|
@@ -94,7 +123,7 @@ Pure iOS apps have **no per-slot TCP ports** — agents build and run on a share
|
|
|
94
123
|
| Layer | Scope | Rule | On conflict |
|
|
95
124
|
|-------|-------|------|-------------|
|
|
96
125
|
| Optional backend (compose) | Per machine | `HARNESS_*_PORT_DEFAULT` for that service | Scan configured ranges in `harness.env` |
|
|
97
|
-
| iOS Simulator |
|
|
126
|
+
| iOS Simulator | Per slot | One device created per agent — no harness port | None: `har-<project>-agent-<id>-<model>` is unique per slot |
|
|
98
127
|
|
|
99
128
|
When your app talks to a local backend, read the resolved host port from `.env.agent.<id>` (set by `setup-infra.sh`) — never hardcode `15432` or similar in tests or flow scripts.
|
|
100
129
|
|
|
@@ -102,7 +131,7 @@ When your app talks to a local backend, read the resolved host port from `.env.a
|
|
|
102
131
|
|
|
103
132
|
| Resource | Model | Configuration |
|
|
104
133
|
|----------|-------|---------------|
|
|
105
|
-
| iOS Simulator | One
|
|
134
|
+
| iOS Simulator | One device created per slot, deleted at teardown | `HARNESS_SIMULATOR_*`, `launch.sh` |
|
|
106
135
|
| Optional backend | One shared compose service | `HARNESS_INFRA_SERVICES` (e.g. `"mock-server"`) |
|
|
107
136
|
| Application code | Isolated git worktree per slot | `HARNESS_USE_WORKTREE=true` |
|
|
108
137
|
|
|
@@ -11,6 +11,8 @@ REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
|
11
11
|
source "$SCRIPT_DIR/harness.env"
|
|
12
12
|
# shellcheck source=/dev/null
|
|
13
13
|
source "$SCRIPT_DIR/agent-slot.sh"
|
|
14
|
+
# shellcheck source=/dev/null
|
|
15
|
+
source "$SCRIPT_DIR/simulator.sh"
|
|
14
16
|
|
|
15
17
|
AGENT_ID="${1:?Usage: agent-cli.sh <agent-id> <command> [args...]}"
|
|
16
18
|
COMMAND="${2:?Usage: agent-cli.sh <agent-id> <command> [args...]}"
|
|
@@ -19,6 +21,18 @@ validate_agent_id "$AGENT_ID"
|
|
|
19
21
|
|
|
20
22
|
WORKTREE_DIR="$(existing_slot_worktree "$AGENT_ID")"
|
|
21
23
|
|
|
24
|
+
# simctl target for this slot: its own device when one is reserved, else whatever
|
|
25
|
+
# is booted — never another slot's simulator.
|
|
26
|
+
slot_sim_target() {
|
|
27
|
+
local claim udid
|
|
28
|
+
claim="$(har_sim_claim_file "$AGENT_ID")"
|
|
29
|
+
if [ -f "$claim" ]; then
|
|
30
|
+
udid="$(read_slot_field "$claim" udid || true)"
|
|
31
|
+
[ -n "$udid" ] && { echo "$udid"; return; }
|
|
32
|
+
fi
|
|
33
|
+
echo "booted"
|
|
34
|
+
}
|
|
35
|
+
|
|
22
36
|
resolve_work_dir() {
|
|
23
37
|
local env_file
|
|
24
38
|
env_file="$(resolve_agent_env_file "$AGENT_ID" "$REPO_ROOT")" || {
|
|
@@ -42,7 +56,7 @@ case "$COMMAND" in
|
|
|
42
56
|
echo " Work dir: $(resolve_agent_work_dir "$ENV_FILE" "$AGENT_ID")"
|
|
43
57
|
[ -n "$WORKTREE_DIR" ] && [ -d "$WORKTREE_DIR" ] && echo " Worktree: $WORKTREE_DIR"
|
|
44
58
|
echo " Scheme: ${HARNESS_XCODE_SCHEME:-not set}"
|
|
45
|
-
echo " Simulator: ${HARNESS_SIMULATOR_NAME:-not set}"
|
|
59
|
+
echo " Simulator: ${HARNESS_SIMULATOR_DEVICE_NAME:-${HARNESS_SIMULATOR_NAME:-not set}} ($(slot_sim_target))"
|
|
46
60
|
echo " Bundle ID: ${HARNESS_BUNDLE_ID:-not set}"
|
|
47
61
|
else
|
|
48
62
|
echo "No active environment for agent ${AGENT_ID}"
|
|
@@ -51,14 +65,22 @@ case "$COMMAND" in
|
|
|
51
65
|
;;
|
|
52
66
|
|
|
53
67
|
simulator)
|
|
54
|
-
# Show
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
68
|
+
# Show the device reserved for this slot, or the configured one when idle.
|
|
69
|
+
TARGET="$(slot_sim_target)"
|
|
70
|
+
if [ "$TARGET" != "booted" ]; then
|
|
71
|
+
echo "Simulator: $(har_sim_device_name "$TARGET") (${TARGET})"
|
|
72
|
+
echo "State: $(har_sim_device_state "$TARGET")"
|
|
73
|
+
echo "Reserved: $(har_sim_claim_file "$AGENT_ID")"
|
|
74
|
+
else
|
|
75
|
+
SIM_NAME="${HARNESS_SIMULATOR_NAME:-iPhone 16}"
|
|
76
|
+
echo "Simulator: ${SIM_NAME} (no device reserved — the slot is not launched)"
|
|
77
|
+
RESOLVED="$(har_sim_resolve_configured)"
|
|
78
|
+
if [ -n "$RESOLVED" ]; then
|
|
79
|
+
echo "Configured device: ${RESOLVED#*$'\t'} (${RESOLVED%%$'\t'*})"
|
|
80
|
+
else
|
|
81
|
+
echo " (no matching simulator found — check HARNESS_SIMULATOR_NAME in harness.env)"
|
|
82
|
+
fi
|
|
83
|
+
fi
|
|
62
84
|
;;
|
|
63
85
|
|
|
64
86
|
logs)
|
|
@@ -70,7 +92,7 @@ case "$COMMAND" in
|
|
|
70
92
|
restart)
|
|
71
93
|
echo "iOS profile has no managed processes to restart." >&2
|
|
72
94
|
echo "Relaunch the slot or reinstall the app with:" >&2
|
|
73
|
-
echo " ./.har/agent-cli.sh ${AGENT_ID}
|
|
95
|
+
echo " ./.har/agent-cli.sh ${AGENT_ID} install <path-to.app>" >&2
|
|
74
96
|
exit 1
|
|
75
97
|
;;
|
|
76
98
|
|
|
@@ -80,8 +102,9 @@ case "$COMMAND" in
|
|
|
80
102
|
echo "Usage: agent-cli.sh ${AGENT_ID} install <path-to.app>" >&2
|
|
81
103
|
exit 1
|
|
82
104
|
fi
|
|
83
|
-
|
|
84
|
-
|
|
105
|
+
TARGET="$(slot_sim_target)"
|
|
106
|
+
echo "==> Installing ${APP_PATH} on ${TARGET}..."
|
|
107
|
+
xcrun simctl install "$TARGET" "$APP_PATH"
|
|
85
108
|
echo "✓ Installed"
|
|
86
109
|
;;
|
|
87
110
|
|
|
@@ -92,8 +115,9 @@ case "$COMMAND" in
|
|
|
92
115
|
echo " or set HARNESS_BUNDLE_ID in harness.env" >&2
|
|
93
116
|
exit 1
|
|
94
117
|
fi
|
|
95
|
-
|
|
96
|
-
|
|
118
|
+
TARGET="$(slot_sim_target)"
|
|
119
|
+
echo "==> Launching ${BUNDLE} on ${TARGET}..."
|
|
120
|
+
xcrun simctl launch "$TARGET" "$BUNDLE"
|
|
97
121
|
;;
|
|
98
122
|
|
|
99
123
|
url)
|
|
@@ -101,7 +125,7 @@ case "$COMMAND" in
|
|
|
101
125
|
echo "Work dir: $WORK_DIR"
|
|
102
126
|
[ -d "$WORKTREE_DIR" ] && echo "Worktree: $WORKTREE_DIR"
|
|
103
127
|
echo "Scheme: ${HARNESS_XCODE_SCHEME:-not set}"
|
|
104
|
-
echo "Simulator: ${HARNESS_SIMULATOR_NAME:-not set}"
|
|
128
|
+
echo "Simulator: ${HARNESS_SIMULATOR_DEVICE_NAME:-${HARNESS_SIMULATOR_NAME:-not set}} ($(slot_sim_target))"
|
|
105
129
|
echo "Bundle ID: ${HARNESS_BUNDLE_ID:-not set}"
|
|
106
130
|
har_infra_enabled mock-server && echo "Mock: check docker-compose.agent.yml for port"
|
|
107
131
|
;;
|
|
@@ -19,14 +19,38 @@ export HARNESS_XCODE_PROJECT="" # e.g. MyApp.xcodeproj (relative to repo r
|
|
|
19
19
|
# Xcode scheme to build and test (must be marked as shared in Xcode).
|
|
20
20
|
export HARNESS_XCODE_SCHEME="MyApp"
|
|
21
21
|
|
|
22
|
-
#
|
|
23
|
-
#
|
|
22
|
+
# Simulator model each slot runs on, e.g. "iPhone 16", "iPhone 16 Pro", "iPad Air 11-inch (M2)".
|
|
23
|
+
# Use: xcrun simctl list devicetypes. Leave empty for the newest model of the family.
|
|
24
24
|
export HARNESS_SIMULATOR_NAME="iPhone 16"
|
|
25
25
|
|
|
26
|
-
# Bundle ID of the app under test. Used by
|
|
26
|
+
# Bundle ID of the app under test. Used by agent-cli.sh / RocketSim flows.
|
|
27
27
|
export HARNESS_BUNDLE_ID="com.example.myapp"
|
|
28
28
|
|
|
29
|
-
#
|
|
29
|
+
# ── Simulator allocation ──────────────────────────────────────────────────────
|
|
30
|
+
# Every slot gets its own simulator, "har-<project>-agent-<id>-<model>", created
|
|
31
|
+
# at launch from HARNESS_SIMULATOR_NAME and deleted at teardown. Concurrent agents
|
|
32
|
+
# therefore never share a destination, an installed bundle id, or a UI session,
|
|
33
|
+
# each launch starts from a pristine device, and the simulators you use by hand
|
|
34
|
+
# are never touched. launch.sh writes the device into .env.agent.<id>.
|
|
35
|
+
#
|
|
36
|
+
# The runtime is the newest installed iOS that supports the model; launch fails
|
|
37
|
+
# with the list of available models when none does.
|
|
38
|
+
|
|
39
|
+
# Family used when HARNESS_SIMULATOR_NAME is empty, and enforced when it is set:
|
|
40
|
+
# auto (infer from the name) | iPhone | iPad. An iPad is only ever created when
|
|
41
|
+
# this resolves to iPad.
|
|
42
|
+
export HARNESS_SIMULATOR_FAMILY="auto"
|
|
43
|
+
|
|
44
|
+
# Run every slot on one existing device instead of creating any (bypasses model
|
|
45
|
+
# resolution — no fallback). Slots then share it, so use it for one-off debugging.
|
|
46
|
+
# export HARNESS_SIMULATOR_UDID=""
|
|
47
|
+
|
|
48
|
+
# Set true to go back to one shared simulator for all slots, resolved by exact
|
|
49
|
+
# name from HARNESS_SIMULATOR_NAME and booted by setup-infra.sh.
|
|
50
|
+
export HARNESS_SIMULATOR_SHARED=false
|
|
51
|
+
|
|
52
|
+
# Destination used when slots share a simulator; per-slot launches override it
|
|
53
|
+
# with the reserved device's UDID.
|
|
30
54
|
export HARNESS_IOS_DESTINATION="platform=iOS Simulator,name=${HARNESS_SIMULATOR_NAME}"
|
|
31
55
|
|
|
32
56
|
# Optional SwiftLint path (leave empty to use system-installed swiftlint).
|
|
@@ -14,6 +14,8 @@ REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
|
14
14
|
source "$SCRIPT_DIR/harness.env"
|
|
15
15
|
# shellcheck source=/dev/null
|
|
16
16
|
source "$SCRIPT_DIR/agent-slot.sh"
|
|
17
|
+
# shellcheck source=/dev/null
|
|
18
|
+
source "$SCRIPT_DIR/simulator.sh"
|
|
17
19
|
|
|
18
20
|
AGENT_ID="${1:-}"
|
|
19
21
|
USE_WORKTREE="${HARNESS_USE_WORKTREE:-true}"
|
|
@@ -86,7 +88,9 @@ fi
|
|
|
86
88
|
# whose .gitignore doesn't know about har (applies to all worktrees too).
|
|
87
89
|
GIT_EXCLUDE="$(git -C "$REPO_ROOT" rev-parse --git-common-dir 2>/dev/null)/info/exclude"
|
|
88
90
|
if [ -n "$GIT_EXCLUDE" ] && [ -d "$(dirname "$GIT_EXCLUDE")" ]; then
|
|
89
|
-
|
|
91
|
+
# .har/simulators covers harnesses scaffolded before it reached
|
|
92
|
+
# gitignore.template, which only lands on init or maintain.
|
|
93
|
+
for pattern in '.env.agent.*' 'ecosystem.agent.*.config.cjs' '.har/venv' '.har/simulators'; do
|
|
90
94
|
grep -qxF "$pattern" "$GIT_EXCLUDE" 2>/dev/null || echo "$pattern" >> "$GIT_EXCLUDE"
|
|
91
95
|
done
|
|
92
96
|
fi
|
|
@@ -107,6 +111,7 @@ mark_slot_failed() {
|
|
|
107
111
|
if [ "$exit_code" != "0" ] && [ "$REGISTRY_WRITTEN" = true ]; then
|
|
108
112
|
log "Launch failed after creating the session. Recording failed slot state..."
|
|
109
113
|
set +e
|
|
114
|
+
har_sim_release "$AGENT_ID"
|
|
110
115
|
SLOT_AGENT_ID="$AGENT_ID" \
|
|
111
116
|
SLOT_MODE="$([ "$USE_WORKTREE" = true ] && echo worktree || echo root)" \
|
|
112
117
|
SLOT_WORK_DIR="$WORK_DIR" \
|
|
@@ -147,6 +152,30 @@ HAR_REL_PREFIX="${REL_PREFIX:-}" \
|
|
|
147
152
|
HAR_AGENT_ID="$AGENT_ID" \
|
|
148
153
|
"$SCRIPT_DIR/provision-toolchain.sh"
|
|
149
154
|
|
|
155
|
+
# Give this slot its own simulator. Written after provision-toolchain.sh so the
|
|
156
|
+
# per-slot device wins over the shared default when verify.sh sources the file.
|
|
157
|
+
if har_sim_per_slot_enabled; then
|
|
158
|
+
log "Preparing this slot's simulator..."
|
|
159
|
+
SIM_RESULT=""
|
|
160
|
+
SIM_RESULT="$(har_sim_acquire "$AGENT_ID")" || {
|
|
161
|
+
log "Could not prepare a simulator — see the messages above."
|
|
162
|
+
exit 1
|
|
163
|
+
}
|
|
164
|
+
SIM_UDID="${SIM_RESULT%%$'\t'*}"
|
|
165
|
+
SIM_NAME="${SIM_RESULT#*$'\t'}"
|
|
166
|
+
# The device name goes under its own key: HARNESS_SIMULATOR_NAME means "model
|
|
167
|
+
# to create" in harness.env, and overwriting it here would make its meaning
|
|
168
|
+
# depend on which file was sourced last.
|
|
169
|
+
{
|
|
170
|
+
printf 'HARNESS_SIMULATOR_UDID=%q\n' "$SIM_UDID"
|
|
171
|
+
printf 'HARNESS_SIMULATOR_DEVICE_NAME=%q\n' "$SIM_NAME"
|
|
172
|
+
printf 'HARNESS_IOS_DESTINATION=%q\n' "platform=iOS Simulator,id=${SIM_UDID}"
|
|
173
|
+
} >> "$ENV_FILE"
|
|
174
|
+
log "Simulator: ${SIM_NAME} (${SIM_UDID})"
|
|
175
|
+
else
|
|
176
|
+
log "Shared simulator mode (HARNESS_SIMULATOR_SHARED=true) — slots share ${HARNESS_SIMULATOR_NAME:-the configured device}."
|
|
177
|
+
fi
|
|
178
|
+
|
|
150
179
|
# Record the session in the slot registry — the source of truth for where
|
|
151
180
|
# this slot's code lives (status/verify/teardown resolve through it).
|
|
152
181
|
SLOT_AGENT_ID="$AGENT_ID" \
|
|
@@ -167,6 +196,9 @@ log " ${WORK_DIR}"
|
|
|
167
196
|
if [ "$USE_WORKTREE" = true ]; then
|
|
168
197
|
log " Branch: ${BRANCH} (based on ${BASE_BRANCH} @ ${BASE_COMMIT})"
|
|
169
198
|
fi
|
|
199
|
+
if [ -n "${SIM_NAME:-}" ]; then
|
|
200
|
+
log " Simulator: ${SIM_NAME} (${SIM_UDID})"
|
|
201
|
+
fi
|
|
170
202
|
log ""
|
|
171
203
|
log " Verify: ./.har/verify.sh $AGENT_ID"
|
|
172
204
|
log " Teardown: ./.har/teardown.sh $AGENT_ID (keeps the branch)"
|
|
@@ -30,7 +30,9 @@ pt_log() {
|
|
|
30
30
|
append_env() {
|
|
31
31
|
local key="$1"
|
|
32
32
|
local value="$2"
|
|
33
|
-
|
|
33
|
+
# %q, not a raw value: an unquoted value with spaces (an Xcode scheme, a
|
|
34
|
+
# simulator name, a path under /Applications) breaks `source .env.agent.<id>`.
|
|
35
|
+
printf '%s=%q\n' "$key" "$value" >> "$HAR_ENV_FILE"
|
|
34
36
|
}
|
|
35
37
|
|
|
36
38
|
append_path_prefix() {
|
|
@@ -235,9 +237,8 @@ provision_ios() {
|
|
|
235
237
|
if [ -n "${HARNESS_XCODE_SCHEME:-}" ]; then
|
|
236
238
|
append_env "HARNESS_XCODE_SCHEME" "$HARNESS_XCODE_SCHEME"
|
|
237
239
|
fi
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
fi
|
|
240
|
+
# The simulator is not recorded here: launch.sh writes the device reserved for
|
|
241
|
+
# this slot, and harness.env already carries the shared one.
|
|
241
242
|
if [ -n "${HARNESS_BUNDLE_ID:-}" ]; then
|
|
242
243
|
append_env "HARNESS_BUNDLE_ID" "$HARNESS_BUNDLE_ID"
|
|
243
244
|
fi
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# Sets up shared infrastructure for all iOS agent slots.
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
#
|
|
3
|
+
# Checks the toolchain, and boots the configured simulator when slots share one
|
|
4
|
+
# (HARNESS_SIMULATOR_SHARED=true) — otherwise launch.sh boots each slot's own
|
|
5
|
+
# device. No Docker required for pure iOS apps — extend only when a local
|
|
6
|
+
# backend container is needed. Idempotent — safe to run multiple times.
|
|
6
7
|
#
|
|
7
8
|
# Usage: ./.har/setup-infra.sh
|
|
8
9
|
set -euo pipefail
|
|
@@ -11,6 +12,10 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
|
11
12
|
|
|
12
13
|
# shellcheck source=/dev/null
|
|
13
14
|
source "$SCRIPT_DIR/harness.env"
|
|
15
|
+
# shellcheck source=/dev/null
|
|
16
|
+
source "$SCRIPT_DIR/agent-slot.sh"
|
|
17
|
+
# shellcheck source=/dev/null
|
|
18
|
+
source "$SCRIPT_DIR/simulator.sh"
|
|
14
19
|
|
|
15
20
|
log() { echo "==> $*" >&2; }
|
|
16
21
|
|
|
@@ -26,46 +31,55 @@ log "Found: $XCODE_VERSION"
|
|
|
26
31
|
# ── Simulator ─────────────────────────────────────────────────────────────────
|
|
27
32
|
SIMULATOR_NAME="${HARNESS_SIMULATOR_NAME:-iPhone 16}"
|
|
28
33
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
34
|
+
if har_sim_per_slot_enabled; then
|
|
35
|
+
# Each slot creates and boots its own device at launch — booting a shared one
|
|
36
|
+
# here would only waste a device no slot is going to use. Resolve the model now
|
|
37
|
+
# so a missing runtime is reported before any worktree is created.
|
|
38
|
+
FAMILY="$(har_sim_preferred_family)"
|
|
39
|
+
PLAN="$(har_sim_plan_creation "${HARNESS_SIMULATOR_NAME:-}" "$FAMILY")"
|
|
40
|
+
PLAN_STATUS="${PLAN%%$'\t'*}"
|
|
41
|
+
if [ "$PLAN_STATUS" = "OK" ]; then
|
|
42
|
+
log "Per-slot simulators: launch creates one $(printf '%s' "$PLAN" | cut -f3) per agent."
|
|
43
|
+
elif [ "$PLAN_STATUS" = "NO_MODEL" ] && [ -n "$(har_sim_device_by_name "${HARNESS_SIMULATOR_NAME:-}")" ]; then
|
|
44
|
+
# Supported fallback: the name is an existing device rather than a model.
|
|
45
|
+
log "Per-slot simulators: launch reuses the existing device '${SIMULATOR_NAME}'."
|
|
46
|
+
else
|
|
47
|
+
# Fail here rather than at launch, which would already have created a
|
|
48
|
+
# worktree, a branch and an env file the user then has to tear down.
|
|
49
|
+
echo "Error: no simulator can be prepared for '${SIMULATOR_NAME}'." >&2
|
|
50
|
+
if [ "$PLAN" != "$PLAN_STATUS" ]; then
|
|
51
|
+
echo " Available ${FAMILY} models: ${PLAN#*$'\t'}" >&2
|
|
52
|
+
else
|
|
53
|
+
echo " No iOS runtime is installed — add one in Xcode → Settings → Components." >&2
|
|
54
|
+
fi
|
|
55
|
+
echo " Update HARNESS_SIMULATOR_NAME in .har/harness.env." >&2
|
|
56
|
+
exit 1
|
|
57
|
+
fi
|
|
58
|
+
else
|
|
59
|
+
log "Checking simulator: ${SIMULATOR_NAME}"
|
|
45
60
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
| head -1 || echo "unknown")"
|
|
61
|
+
# Exact name match: a substring match on "iPhone 16" also hits "iPhone 16 Pro Max".
|
|
62
|
+
RESOLVED="$(har_sim_resolve_configured)"
|
|
63
|
+
UDID="${RESOLVED%%$'\t'*}"
|
|
50
64
|
|
|
51
|
-
if [ "$
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
65
|
+
if [ -z "$UDID" ]; then
|
|
66
|
+
echo "Error: Simulator '${SIMULATOR_NAME}' not found in available devices." >&2
|
|
67
|
+
echo " Available simulators:" >&2
|
|
68
|
+
xcrun simctl list devices available 2>/dev/null | grep -E 'iPhone|iPad' | head -20 >&2
|
|
69
|
+
echo " Update HARNESS_SIMULATOR_NAME in .har/harness.env." >&2
|
|
70
|
+
exit 1
|
|
71
|
+
fi
|
|
56
72
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
73
|
+
if [ "$(har_sim_device_state "$UDID")" = "Booted" ]; then
|
|
74
|
+
log "Simulator '${SIMULATOR_NAME}' ($UDID) is already booted."
|
|
75
|
+
else
|
|
76
|
+
log "Booting simulator '${SIMULATOR_NAME}' ($UDID)..."
|
|
77
|
+
if har_sim_boot "$UDID"; then
|
|
61
78
|
log "Simulator is ready."
|
|
62
|
-
|
|
79
|
+
else
|
|
80
|
+
echo "Warning: Simulator did not reach Booted state." >&2
|
|
63
81
|
fi
|
|
64
|
-
|
|
65
|
-
echo "Warning: Simulator did not reach Booted state within 30 seconds." >&2
|
|
66
|
-
fi
|
|
67
|
-
sleep 1
|
|
68
|
-
done
|
|
82
|
+
fi
|
|
69
83
|
fi
|
|
70
84
|
|
|
71
85
|
# ── Optional Docker infra ─────────────────────────────────────────────────────
|
|
@@ -79,6 +93,9 @@ fi
|
|
|
79
93
|
|
|
80
94
|
log ""
|
|
81
95
|
log "Infrastructure is ready."
|
|
82
|
-
|
|
96
|
+
# In per-slot mode the device is reported by launch, once it exists.
|
|
97
|
+
if [ -n "${UDID:-}" ]; then
|
|
98
|
+
log " Simulator: ${SIMULATOR_NAME} ($UDID)"
|
|
99
|
+
fi
|
|
83
100
|
har_infra_enabled mock-server && log " Mock server: running (see docker-compose.agent.yml)"
|
|
84
101
|
exit 0
|
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Per-slot iOS Simulator allocation.
|
|
3
|
+
#
|
|
4
|
+
# One booted simulator shared by every agent slot means concurrent xcodebuild
|
|
5
|
+
# destinations, installs of the same bundle id, and UI flows collide. Each slot
|
|
6
|
+
# instead gets a simulator of its own, created at launch and deleted at teardown:
|
|
7
|
+
# unique by construction, pristine on every launch, and never a device the
|
|
8
|
+
# developer is using by hand.
|
|
9
|
+
#
|
|
10
|
+
# Source AFTER harness.env and agent-slot.sh — read_slot_field, slot_registry_file
|
|
11
|
+
# and SCRIPT_DIR come from there.
|
|
12
|
+
#
|
|
13
|
+
# What a slot holds is recorded in .har/simulators/agent-<id>.json, so teardown
|
|
14
|
+
# knows whether the device was HAR's to delete.
|
|
15
|
+
|
|
16
|
+
# ── Configuration ─────────────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
# Preferred device family: auto (infer from HARNESS_SIMULATOR_NAME) | iPhone | iPad
|
|
19
|
+
har_sim_preferred_family() {
|
|
20
|
+
local configured="${HARNESS_SIMULATOR_FAMILY:-auto}"
|
|
21
|
+
case "$configured" in
|
|
22
|
+
iPhone|iPad) echo "$configured"; return ;;
|
|
23
|
+
esac
|
|
24
|
+
case "${HARNESS_SIMULATOR_NAME:-}" in
|
|
25
|
+
iPad*|iPAD*|ipad*) echo "iPad" ;;
|
|
26
|
+
*) echo "iPhone" ;;
|
|
27
|
+
esac
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
har_sim_per_slot_enabled() {
|
|
31
|
+
[ "${HARNESS_SIMULATOR_SHARED:-false}" != "true" ]
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
har_sim_log() { echo "==> simulator: $*" >&2; }
|
|
35
|
+
|
|
36
|
+
# "iPad Air 11-inch (M4)" → "iPad-Air-11-inch-M4"
|
|
37
|
+
har_sim_slug() {
|
|
38
|
+
printf '%s' "$1" | tr -c 'A-Za-z0-9._-' '-' | tr -s '-' | sed 's/^-//; s/-$//'
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
# Short digest of the repository path. Two checkouts can share a folder name,
|
|
42
|
+
# and therefore HARNESS_PROJECT_NAME, but not a path — without it the prefix
|
|
43
|
+
# sweep below would delete the other checkout's live device.
|
|
44
|
+
har_sim_repo_digest() {
|
|
45
|
+
local repo
|
|
46
|
+
# -P so the same checkout reached through a symlink yields the same digest.
|
|
47
|
+
repo="$(cd -P "${SCRIPT_DIR}/.." 2>/dev/null && pwd -P || echo "${SCRIPT_DIR}")"
|
|
48
|
+
node -e 'process.stdout.write(require("crypto").createHash("sha1").update(process.argv[1]).digest("hex").slice(0, 6))' "$repo"
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
# Stable part of a slot's device name. The model is appended after it, so a
|
|
52
|
+
# device left over from a launch on another model is still recognised as this
|
|
53
|
+
# slot's and cleaned up.
|
|
54
|
+
har_sim_device_label_prefix() {
|
|
55
|
+
echo "har-$(har_sim_slug "${HARNESS_PROJECT_NAME:-har}")-$(har_sim_repo_digest)-agent-${1}"
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
har_sim_device_label() {
|
|
59
|
+
local prefix model
|
|
60
|
+
prefix="$(har_sim_device_label_prefix "$1")"
|
|
61
|
+
model="$(har_sim_slug "${2:-}")"
|
|
62
|
+
[ -n "$model" ] && echo "${prefix}-${model}" || echo "$prefix"
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
# ── Claims ────────────────────────────────────────────────────────────────────
|
|
66
|
+
# Slots never compete for a device, so a claim only records what teardown must
|
|
67
|
+
# release — no locking, no cross-slot exclusion.
|
|
68
|
+
|
|
69
|
+
har_sim_claims_dir() { echo "${SCRIPT_DIR}/simulators"; }
|
|
70
|
+
har_sim_claim_file() { echo "$(har_sim_claims_dir)/agent-${1}.json"; }
|
|
71
|
+
|
|
72
|
+
har_sim_write_claim() {
|
|
73
|
+
local agent_id="$1" udid="$2" name="$3" created="$4" file
|
|
74
|
+
file="$(har_sim_claim_file "$agent_id")"
|
|
75
|
+
mkdir -p "$(dirname "$file")"
|
|
76
|
+
node -e '
|
|
77
|
+
const fs = require("fs");
|
|
78
|
+
const [file, agentId, udid, name, created] = process.argv.slice(1);
|
|
79
|
+
fs.writeFileSync(file, JSON.stringify({
|
|
80
|
+
agentId: Number(agentId),
|
|
81
|
+
udid,
|
|
82
|
+
name,
|
|
83
|
+
createdByHar: created === "true",
|
|
84
|
+
claimedAt: new Date().toISOString(),
|
|
85
|
+
}, null, 2) + "\n");
|
|
86
|
+
' "$file" "$agent_id" "$udid" "$name" "$created"
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
# Agent id of another live slot holding this udid, if any. Only a pinned
|
|
90
|
+
# HARNESS_SIMULATOR_UDID can produce that overlap.
|
|
91
|
+
har_sim_claim_owner_of() {
|
|
92
|
+
local udid="$1" self="$2" dir file id
|
|
93
|
+
dir="$(har_sim_claims_dir)"
|
|
94
|
+
[ -d "$dir" ] || return 0
|
|
95
|
+
for file in "$dir"/agent-*.json; do
|
|
96
|
+
[ -f "$file" ] || continue
|
|
97
|
+
id="$(basename "$file")"
|
|
98
|
+
id="${id#agent-}"
|
|
99
|
+
id="${id%.json}"
|
|
100
|
+
[ "$id" = "$self" ] && continue
|
|
101
|
+
[ -f "$(slot_registry_file "$id")" ] || continue
|
|
102
|
+
if [ "$(read_slot_field "$file" udid || true)" = "$udid" ]; then
|
|
103
|
+
echo "$id"
|
|
104
|
+
return 0
|
|
105
|
+
fi
|
|
106
|
+
done
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
# ── Device discovery ──────────────────────────────────────────────────────────
|
|
110
|
+
|
|
111
|
+
# Unavailable devices are skipped: a device whose runtime was uninstalled must
|
|
112
|
+
# not pass validation only to fail at boot.
|
|
113
|
+
har_sim_device_field() {
|
|
114
|
+
xcrun simctl list devices --json 2>/dev/null | node -e '
|
|
115
|
+
const data = JSON.parse(require("fs").readFileSync(0, "utf8"));
|
|
116
|
+
const [want, field] = process.argv.slice(1);
|
|
117
|
+
const device = Object.values(data.devices || {})
|
|
118
|
+
.flat()
|
|
119
|
+
.filter((entry) => entry && entry.isAvailable !== false)
|
|
120
|
+
.find((entry) => entry.udid === want);
|
|
121
|
+
if (device && device[field]) process.stdout.write(String(device[field]));
|
|
122
|
+
' "$1" "$2" 2>/dev/null || true
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
har_sim_device_state() { har_sim_device_field "$1" state; }
|
|
126
|
+
har_sim_device_name() { har_sim_device_field "$1" name; }
|
|
127
|
+
|
|
128
|
+
# UDID of an existing device with this exact name, newest runtime first.
|
|
129
|
+
# A substring match would resolve "iPhone 16" to "iPhone 16 Pro Max".
|
|
130
|
+
har_sim_device_by_name() {
|
|
131
|
+
local want="${1:-}"
|
|
132
|
+
[ -n "$want" ] || return 0
|
|
133
|
+
xcrun simctl list devices available --json 2>/dev/null | node -e '
|
|
134
|
+
const data = JSON.parse(require("fs").readFileSync(0, "utf8"));
|
|
135
|
+
const want = process.argv[1];
|
|
136
|
+
const rows = [];
|
|
137
|
+
for (const [runtime, devices] of Object.entries(data.devices || {})) {
|
|
138
|
+
if (!runtime.includes("SimRuntime.iOS-")) continue;
|
|
139
|
+
const parts = runtime.match(/iOS-(\d+)(?:-(\d+))?/);
|
|
140
|
+
const version = parts ? Number(parts[1]) * 1000 + Number(parts[2] || 0) : 0;
|
|
141
|
+
for (const device of devices || []) {
|
|
142
|
+
if (!device.udid || device.isAvailable === false) continue;
|
|
143
|
+
if (device.name !== want) continue;
|
|
144
|
+
rows.push({ udid: device.udid, version });
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
rows.sort((a, b) => b.version - a.version);
|
|
148
|
+
if (rows[0]) process.stdout.write(rows[0].udid);
|
|
149
|
+
' "$want" 2>/dev/null || true
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
# The shared-mode device, as "<udid>\t<name>".
|
|
153
|
+
har_sim_resolve_configured() {
|
|
154
|
+
local udid
|
|
155
|
+
udid="$(har_sim_device_by_name "${HARNESS_SIMULATOR_NAME:-}")"
|
|
156
|
+
[ -n "$udid" ] || return 0
|
|
157
|
+
printf '%s\t%s' "$udid" "${HARNESS_SIMULATOR_NAME}"
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
# `simctl list` is a subprocess and a JSON parse; neither answer changes during
|
|
161
|
+
# a launch, so each is fetched at most once per process.
|
|
162
|
+
har_sim_devicetypes_json() {
|
|
163
|
+
if [ -z "${_HAR_SIM_TYPES_JSON:-}" ]; then
|
|
164
|
+
_HAR_SIM_TYPES_JSON="$(xcrun simctl list devicetypes --json 2>/dev/null || echo '{}')"
|
|
165
|
+
fi
|
|
166
|
+
printf '%s' "$_HAR_SIM_TYPES_JSON"
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
har_sim_runtimes_json() {
|
|
170
|
+
if [ -z "${_HAR_SIM_RUNTIMES_JSON:-}" ]; then
|
|
171
|
+
_HAR_SIM_RUNTIMES_JSON="$(xcrun simctl list runtimes --json 2>/dev/null || echo '{}')"
|
|
172
|
+
fi
|
|
173
|
+
printf '%s' "$_HAR_SIM_RUNTIMES_JSON"
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
# Model and runtime to create this slot's device from. Failures carry the models
|
|
177
|
+
# this machine can create, so the caller never has to re-derive that list.
|
|
178
|
+
# Prints one of:
|
|
179
|
+
# OK <deviceTypeId> <deviceTypeName> <runtimeId> <runtimeVersion>
|
|
180
|
+
# NO_RUNTIME no iOS runtime installed
|
|
181
|
+
# NO_MODEL <models> HARNESS_SIMULATOR_NAME is not a device model
|
|
182
|
+
# NO_RUNTIME_FOR_MODEL <models> model exists, no installed runtime supports it
|
|
183
|
+
har_sim_plan_creation() {
|
|
184
|
+
local types runtimes
|
|
185
|
+
types="$(har_sim_devicetypes_json)"
|
|
186
|
+
runtimes="$(har_sim_runtimes_json)"
|
|
187
|
+
node -e '
|
|
188
|
+
const [typesRaw, runtimesRaw, wantName, family] = process.argv.slice(1);
|
|
189
|
+
const types = JSON.parse(typesRaw).devicetypes || [];
|
|
190
|
+
const version = (value) => {
|
|
191
|
+
const [major = 0, minor = 0] = String(value || "0").split(".").map(Number);
|
|
192
|
+
return major * 1000 + minor;
|
|
193
|
+
};
|
|
194
|
+
const runtimes = (JSON.parse(runtimesRaw).runtimes || [])
|
|
195
|
+
.filter((runtime) => runtime.isAvailable !== false)
|
|
196
|
+
.filter((runtime) => String(runtime.identifier || "").includes("SimRuntime.iOS-"))
|
|
197
|
+
.sort((a, b) => version(b.version) - version(a.version));
|
|
198
|
+
|
|
199
|
+
const isPad = (entry) => /ipad/i.test(entry.productFamily || entry.name || "");
|
|
200
|
+
const inFamily = (entry) => (family === "iPad" ? isPad(entry) : !isPad(entry));
|
|
201
|
+
|
|
202
|
+
// Models the newest runtime could create, named so an error can list them.
|
|
203
|
+
// Truncation is stated rather than silent — a model cut off the list still exists.
|
|
204
|
+
function availableModels() {
|
|
205
|
+
const runtime = runtimes[0];
|
|
206
|
+
const supported =
|
|
207
|
+
runtime && Array.isArray(runtime.supportedDeviceTypes) && runtime.supportedDeviceTypes.length
|
|
208
|
+
? runtime.supportedDeviceTypes
|
|
209
|
+
: types;
|
|
210
|
+
const names = supported.filter(inFamily).map((entry) => entry.name);
|
|
211
|
+
const shown = names.slice(0, 8).join(", ");
|
|
212
|
+
return names.length > 8 ? `${shown}, … (${names.length - 8} more)` : shown;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function plan() {
|
|
216
|
+
if (!runtimes.length) return "NO_RUNTIME";
|
|
217
|
+
if (wantName && !types.some((type) => type.name === wantName)) {
|
|
218
|
+
return `NO_MODEL\t${availableModels()}`;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
for (const runtime of runtimes) {
|
|
222
|
+
const supported =
|
|
223
|
+
Array.isArray(runtime.supportedDeviceTypes) && runtime.supportedDeviceTypes.length
|
|
224
|
+
? runtime.supportedDeviceTypes
|
|
225
|
+
: types;
|
|
226
|
+
// Apple lists supported device types newest first within each family.
|
|
227
|
+
const pool = supported.filter(inFamily);
|
|
228
|
+
const chosen = wantName ? pool.find((type) => type.name === wantName) : pool[0];
|
|
229
|
+
if (chosen) {
|
|
230
|
+
return `OK\t${chosen.identifier}\t${chosen.name}\t${runtime.identifier}\t${runtime.version}`;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return wantName ? `NO_RUNTIME_FOR_MODEL\t${availableModels()}` : "NO_RUNTIME";
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
process.stdout.write(plan());
|
|
237
|
+
' "$types" "$runtimes" "${1:-}" "${2:-iPhone}" 2>/dev/null || true
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
# Every device whose name is <prefix> or starts with "<prefix>-", so a slot's
|
|
241
|
+
# leftovers are found whatever model they were created from.
|
|
242
|
+
har_sim_devices_with_prefix() {
|
|
243
|
+
xcrun simctl list devices --json 2>/dev/null | node -e '
|
|
244
|
+
const data = JSON.parse(require("fs").readFileSync(0, "utf8"));
|
|
245
|
+
const prefix = process.argv[1];
|
|
246
|
+
const matches = Object.values(data.devices || {})
|
|
247
|
+
.flat()
|
|
248
|
+
.filter((device) => device && device.udid)
|
|
249
|
+
.filter((device) => device.name === prefix || String(device.name || "").startsWith(`${prefix}-`))
|
|
250
|
+
.map((device) => device.udid);
|
|
251
|
+
process.stdout.write(matches.join("\n"));
|
|
252
|
+
' "$1" 2>/dev/null || true
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
har_sim_delete_devices_with_prefix() {
|
|
256
|
+
local udid
|
|
257
|
+
while read -r udid; do
|
|
258
|
+
[ -n "$udid" ] || continue
|
|
259
|
+
xcrun simctl delete "$udid" >/dev/null 2>&1 || true
|
|
260
|
+
done <<< "$(har_sim_devices_with_prefix "$1")"
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
# ── Boot ──────────────────────────────────────────────────────────────────────
|
|
264
|
+
|
|
265
|
+
har_sim_boot() {
|
|
266
|
+
local udid="$1" i
|
|
267
|
+
xcrun simctl boot "$udid" >/dev/null 2>&1 || true
|
|
268
|
+
if xcrun simctl bootstatus "$udid" -b >/dev/null 2>&1; then
|
|
269
|
+
return 0
|
|
270
|
+
fi
|
|
271
|
+
# Fallback for a toolchain without bootstatus: the plain listing is one
|
|
272
|
+
# subprocess, where the JSON form costs xcrun plus node on every poll.
|
|
273
|
+
for i in $(seq 1 30); do
|
|
274
|
+
if xcrun simctl list devices 2>/dev/null | grep -q "${udid}.*Booted"; then
|
|
275
|
+
return 0
|
|
276
|
+
fi
|
|
277
|
+
sleep 2
|
|
278
|
+
done
|
|
279
|
+
return 1
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
# ── Acquire / release ─────────────────────────────────────────────────────────
|
|
283
|
+
|
|
284
|
+
# Prints "<udid>\t<name>\t<createdByHar>".
|
|
285
|
+
_har_sim_select() {
|
|
286
|
+
local agent_id="$1"
|
|
287
|
+
local want="${HARNESS_SIMULATOR_NAME:-}"
|
|
288
|
+
local family owner name plan status models device_type model runtime label udid
|
|
289
|
+
|
|
290
|
+
# An explicit UDID is a deliberate override — never fall back off it.
|
|
291
|
+
if [ -n "${HARNESS_SIMULATOR_UDID:-}" ]; then
|
|
292
|
+
name="$(har_sim_device_name "$HARNESS_SIMULATOR_UDID")"
|
|
293
|
+
if [ -z "$name" ]; then
|
|
294
|
+
har_sim_log "HARNESS_SIMULATOR_UDID=${HARNESS_SIMULATOR_UDID} is not a known device"
|
|
295
|
+
return 1
|
|
296
|
+
fi
|
|
297
|
+
owner="$(har_sim_claim_owner_of "$HARNESS_SIMULATOR_UDID" "$agent_id")"
|
|
298
|
+
if [ -n "$owner" ]; then
|
|
299
|
+
har_sim_log "warning: agent ${owner} already holds ${name} — a pinned HARNESS_SIMULATOR_UDID"
|
|
300
|
+
har_sim_log " gives every slot the same device; unset it to get one per slot."
|
|
301
|
+
fi
|
|
302
|
+
printf '%s\t%s\t%s' "$HARNESS_SIMULATOR_UDID" "$name" "false"
|
|
303
|
+
return 0
|
|
304
|
+
fi
|
|
305
|
+
|
|
306
|
+
family="$(har_sim_preferred_family)"
|
|
307
|
+
plan="$(har_sim_plan_creation "$want" "$family")"
|
|
308
|
+
status="${plan%%$'\t'*}"
|
|
309
|
+
# Failure statuses carry the creatable models after the first tab.
|
|
310
|
+
models=""
|
|
311
|
+
[ "$plan" != "$status" ] && models="${plan#*$'\t'}"
|
|
312
|
+
|
|
313
|
+
if [ "$status" = "OK" ]; then
|
|
314
|
+
IFS=$'\t' read -r _ device_type model runtime _ <<< "$plan"
|
|
315
|
+
label="$(har_sim_device_label "$agent_id" "$model")"
|
|
316
|
+
# A crashed launch can leave the slot's device behind — start from scratch,
|
|
317
|
+
# including a device this slot created from a different model.
|
|
318
|
+
har_sim_delete_devices_with_prefix "$(har_sim_device_label_prefix "$agent_id")"
|
|
319
|
+
har_sim_log "creating ${label} (${model}, ${runtime##*SimRuntime.})"
|
|
320
|
+
udid="$(xcrun simctl create "$label" "$device_type" "$runtime" 2>/dev/null | tr -d '[:space:]')"
|
|
321
|
+
if [ -z "$udid" ]; then
|
|
322
|
+
har_sim_log "xcrun simctl create failed for ${label} (${device_type}, ${runtime})"
|
|
323
|
+
return 1
|
|
324
|
+
fi
|
|
325
|
+
printf '%s\t%s\t%s' "$udid" "$label" "true"
|
|
326
|
+
return 0
|
|
327
|
+
fi
|
|
328
|
+
|
|
329
|
+
# Not a model: fall back to an existing device carrying that name, which is
|
|
330
|
+
# how a hand-renamed simulator is targeted.
|
|
331
|
+
if [ "$status" = "NO_MODEL" ]; then
|
|
332
|
+
udid="$(har_sim_device_by_name "$want")"
|
|
333
|
+
if [ -n "$udid" ]; then
|
|
334
|
+
har_sim_log "'${want}' is not a device model — using the existing device with that name"
|
|
335
|
+
printf '%s\t%s\t%s' "$udid" "$want" "false"
|
|
336
|
+
return 0
|
|
337
|
+
fi
|
|
338
|
+
har_sim_log "HARNESS_SIMULATOR_NAME='${want}' is neither a device model nor an existing device"
|
|
339
|
+
har_sim_log " available ${family} models: ${models}"
|
|
340
|
+
return 1
|
|
341
|
+
fi
|
|
342
|
+
|
|
343
|
+
if [ "$status" = "NO_RUNTIME_FOR_MODEL" ]; then
|
|
344
|
+
har_sim_log "no installed iOS runtime supports '${want}'"
|
|
345
|
+
har_sim_log " install a newer runtime in Xcode → Settings → Components, or pick another model"
|
|
346
|
+
har_sim_log " available ${family} models: ${models}"
|
|
347
|
+
return 1
|
|
348
|
+
fi
|
|
349
|
+
|
|
350
|
+
har_sim_log "no iOS simulator runtime is installed"
|
|
351
|
+
har_sim_log " install one in Xcode → Settings → Components"
|
|
352
|
+
return 1
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
# Reserve a simulator for a slot, boot it, and record the claim.
|
|
356
|
+
# Prints "<udid>\t<name>" on success.
|
|
357
|
+
har_sim_acquire() {
|
|
358
|
+
local agent_id="$1" udid name created
|
|
359
|
+
local result="" rc=0
|
|
360
|
+
# `|| rc=$?` keeps a failed selection from tripping `set -e` in the caller.
|
|
361
|
+
result="$(_har_sim_select "$agent_id")" || rc=$?
|
|
362
|
+
[ "$rc" -eq 0 ] || return "$rc"
|
|
363
|
+
|
|
364
|
+
udid="$(printf '%s' "$result" | cut -f1)"
|
|
365
|
+
name="$(printf '%s' "$result" | cut -f2)"
|
|
366
|
+
created="$(printf '%s' "$result" | cut -f3)"
|
|
367
|
+
if [ -z "$udid" ]; then
|
|
368
|
+
har_sim_log "no simulator could be selected"
|
|
369
|
+
return 1
|
|
370
|
+
fi
|
|
371
|
+
har_sim_write_claim "$agent_id" "$udid" "$name" "$created"
|
|
372
|
+
|
|
373
|
+
if ! har_sim_boot "$udid"; then
|
|
374
|
+
har_sim_log "device ${name} (${udid}) did not reach Booted state"
|
|
375
|
+
return 1
|
|
376
|
+
fi
|
|
377
|
+
printf '%s\t%s' "$udid" "$name"
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
# Drop a slot's claim. Devices HAR created are deleted; a device the developer
|
|
381
|
+
# owns is left alone, and never even shut down.
|
|
382
|
+
har_sim_release() {
|
|
383
|
+
local agent_id="$1" file udid created
|
|
384
|
+
file="$(har_sim_claim_file "$agent_id")"
|
|
385
|
+
if [ -f "$file" ]; then
|
|
386
|
+
udid="$(read_slot_field "$file" udid || true)"
|
|
387
|
+
created="$(read_slot_field "$file" createdByHar || true)"
|
|
388
|
+
rm -f "$file"
|
|
389
|
+
if [ "$created" = "true" ] && [ -n "$udid" ]; then
|
|
390
|
+
xcrun simctl shutdown "$udid" >/dev/null 2>&1 || true
|
|
391
|
+
xcrun simctl delete "$udid" >/dev/null 2>&1 || true
|
|
392
|
+
echo "✓ Deleted simulator created for this slot ($udid)"
|
|
393
|
+
fi
|
|
394
|
+
fi
|
|
395
|
+
|
|
396
|
+
# A launch killed between `simctl create` and the claim leaves a device no
|
|
397
|
+
# claim points at. Only HAR creates devices under this prefix, so sweeping it
|
|
398
|
+
# can never remove one of the developer's.
|
|
399
|
+
har_sim_delete_devices_with_prefix "$(har_sim_device_label_prefix "$agent_id")"
|
|
400
|
+
}
|
|
@@ -13,6 +13,8 @@ REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
|
13
13
|
source "$SCRIPT_DIR/harness.env"
|
|
14
14
|
# shellcheck source=/dev/null
|
|
15
15
|
source "$SCRIPT_DIR/agent-slot.sh"
|
|
16
|
+
# shellcheck source=/dev/null
|
|
17
|
+
source "$SCRIPT_DIR/simulator.sh"
|
|
16
18
|
|
|
17
19
|
AGENT_ID="${1:?Usage: teardown.sh <agent-id> [--delete-branch]}"
|
|
18
20
|
DELETE_BRANCH=false
|
|
@@ -59,6 +61,8 @@ if [ -n "$BRANCH" ]; then
|
|
|
59
61
|
fi
|
|
60
62
|
fi
|
|
61
63
|
|
|
64
|
+
har_sim_release "$AGENT_ID"
|
|
65
|
+
|
|
62
66
|
remove_slot_registry "$AGENT_ID"
|
|
63
67
|
|
|
64
68
|
echo "✓ Agent ${AGENT_ID} torn down"
|
|
@@ -17,7 +17,9 @@ The runner sets these environment variables for each flow:
|
|
|
17
17
|
| `FLOW_ARTIFACT_DIR` | Directory for screenshots and output of this flow |
|
|
18
18
|
| `WORK_DIR` | Path to the agent's session worktree |
|
|
19
19
|
| `HARNESS_BUNDLE_ID` | App bundle identifier (from `harness.env`) |
|
|
20
|
-
| `HARNESS_SIMULATOR_NAME` |
|
|
20
|
+
| `HARNESS_SIMULATOR_NAME` | Simulator model configured in `harness.env` |
|
|
21
|
+
| `HARNESS_SIMULATOR_DEVICE_NAME` | Name of the device created for this slot |
|
|
22
|
+
| `HARNESS_SIMULATOR_UDID` | UDID of that device — target it rather than `booted` |
|
|
21
23
|
| `AGENT_ID` | Active agent slot id |
|
|
22
24
|
|
|
23
25
|
## Key RocketSim CLI commands
|
|
@@ -130,6 +130,8 @@ for FLOW_SCRIPT in "${FLOW_SCRIPTS[@]}"; do
|
|
|
130
130
|
FLOW_ARTIFACT_DIR="$FLOW_ARTIFACT_DIR" \
|
|
131
131
|
HARNESS_BUNDLE_ID="${HARNESS_BUNDLE_ID:-}" \
|
|
132
132
|
HARNESS_SIMULATOR_NAME="${HARNESS_SIMULATOR_NAME:-}" \
|
|
133
|
+
HARNESS_SIMULATOR_DEVICE_NAME="${HARNESS_SIMULATOR_DEVICE_NAME:-}" \
|
|
134
|
+
HARNESS_SIMULATOR_UDID="${HARNESS_SIMULATOR_UDID:-}" \
|
|
133
135
|
bash "$FLOW_SCRIPT" 2>&1
|
|
134
136
|
)
|
|
135
137
|
FLOW_EXIT=$?
|
|
@@ -32,5 +32,7 @@ The runner (`rocketsim-flows.sh`) sets these variables for every flow:
|
|
|
32
32
|
|----------|-------------|
|
|
33
33
|
| `FLOW_ARTIFACT_DIR` | Save screenshots here (`$FLOW_ARTIFACT_DIR/success.png`, `failure.png`) |
|
|
34
34
|
| `HARNESS_BUNDLE_ID` | App bundle ID from `harness.env` |
|
|
35
|
-
| `HARNESS_SIMULATOR_NAME` | Simulator
|
|
35
|
+
| `HARNESS_SIMULATOR_NAME` | Simulator model configured in `harness.env` |
|
|
36
|
+
| `HARNESS_SIMULATOR_DEVICE_NAME` | Name of the device created for this slot |
|
|
37
|
+
| `HARNESS_SIMULATOR_UDID` | UDID of that device — target it instead of `booted`, which may be another slot's |
|
|
36
38
|
| `WORK_DIR` | Path to the session worktree |
|
|
@@ -12,10 +12,10 @@ set -euo pipefail
|
|
|
12
12
|
log() { echo "==> [example-smoke] $*" >&2; }
|
|
13
13
|
|
|
14
14
|
# ── Preflight ─────────────────────────────────────────────────────────────────
|
|
15
|
-
# The app must be running on
|
|
16
|
-
# this flow
|
|
17
|
-
# xcrun simctl install booted path/to/MyApp.app
|
|
18
|
-
# xcrun simctl launch booted "${HARNESS_BUNDLE_ID}"
|
|
15
|
+
# The app must be running on THIS slot's simulator. Install and launch it before
|
|
16
|
+
# running this flow — target the reserved device, not `booted`. Example:
|
|
17
|
+
# xcrun simctl install "${HARNESS_SIMULATOR_UDID:-booted}" path/to/MyApp.app
|
|
18
|
+
# xcrun simctl launch "${HARNESS_SIMULATOR_UDID:-booted}" "${HARNESS_BUNDLE_ID}"
|
|
19
19
|
|
|
20
20
|
log "Checking simulator focus..."
|
|
21
21
|
FOCUSED=$(rocketsim simulator focused 2>/dev/null || echo "")
|