@aarwitz/tapp 0.16.5 → 0.17.0-rc.10

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.
@@ -3,13 +3,15 @@
3
3
  #
4
4
  # Boots a simulator if needed, installs the app build under test, runs the autonomous exploration
5
5
  # harness, replays every committed Flow (deterministic E2E tests), diffs the findings against a
6
- # stored baseline, writes a GitHub Actions step summary, and exits non-zero when the gate fails
7
- # (new high/critical findings vs. baseline, or any failed Flow). Wrapped by ../action.yml for
8
- # GitHub Actions; equally usable from any other CI or locally.
6
+ # stored baseline, writes a GitHub Actions step summary, and exits with the public outcome contract:
7
+ # pass 0, deterministic fail 1, infrastructure/usage error 2, inconclusive evidence 3. Absolute
8
+ # blockers and reviewed Flow/Scenario/Contract failures are enforced even without a baseline;
9
+ # baseline comparisons additionally catch new high/critical regressions. Wrapped by ../action.yml
10
+ # for GitHub Actions; equally usable from any other CI or locally.
9
11
  #
10
12
  # Usage:
11
13
  # scripts/ci-gate.sh [--platform ios] --app <path/to/App.app> [--bundle-id <com.example.app>]
12
- # scripts/ci-gate.sh --platform android --apk <path/to/app.apk> --app-id <com.example.app>
14
+ # scripts/ci-gate.sh --platform android --apk <path/to/app.apk> --app-id <com.example.app> [--serial <adb-serial>]
13
15
  # scripts/ci-gate.sh --platform web [--url <http(s)://owned-app>]
14
16
  # # omit --url with --project-dir to detect/build/start/stop one owned web target
15
17
  # # bundle id is detected from the .app when omitted
@@ -25,17 +27,18 @@
25
27
  # [--pr-plan-out <file.json>] # persist the reviewable selection plan
26
28
  # [--baseline <file.json>] # prior report to diff against (skipped if absent)
27
29
  # [--target-key <stable-id>] # isolates target-specific baselines in monorepos
28
- # [--fail-on gate|blocked|any] # gate policy (default gate; see ci-report.js)
30
+ # [--fail-on gate|absolute|any] # gate policy (default gate; see ci-report.js)
29
31
  # [--json-out <file.json>] # write the full report (use as the next baseline)
30
32
  # [--md-out <file.md>] # write the rendered markdown report (for a PR comment)
31
33
  # [--device <name>] # simulator device to boot if none is (default "iPhone 16 Pro")
34
+ # [--serial <adb-serial>] # Android emulator/device (default: first connected device)
32
35
  #
33
36
  # The app must be a SIMULATOR build (xcodebuild ... -destination 'generic/platform=iOS Simulator').
34
37
  set -uo pipefail
35
38
  ROOT="$(cd "$(dirname "$0")/.." && pwd)"
36
39
 
37
40
  usage() {
38
- sed -n '2,30p' "$0" | sed 's/^# \{0,1\}//'
41
+ sed -n '2,/^set -uo pipefail$/p' "$0" | sed '$d; s/^# \{0,1\}//'
39
42
  }
40
43
 
41
44
  PLATFORM="ios" APP_PATH="" BUNDLE_ID="" APK_PATH="" APP_ID="" URL="" WEB_TARGET="" TARGET_KEY="" SERIAL="" ACTIONS=40 TIMEOUT=600 FLOWS="" SCENARIOS="" CONTRACTS="" PROJECT_DIR="" BASELINE="" FAIL_ON="gate" JSON_OUT="" MD_OUT="" DEVICE="iPhone 16 Pro" PR_BASE="" PR_HEAD="HEAD" CHANGED_FILES_FILE="" PR_PLAN_OUT=""
@@ -74,7 +77,7 @@ done
74
77
  [[ "$PLATFORM" == "ios" || "$PLATFORM" == "android" || "$PLATFORM" == "web" ]] || { echo "❌ --platform must be ios|android|web" >&2; exit 2; }
75
78
  [[ "$ACTIONS" =~ ^[1-9][0-9]*$ ]] || { echo "❌ --actions must be a positive integer" >&2; exit 2; }
76
79
  [[ "$TIMEOUT" =~ ^[1-9][0-9]*$ ]] || { echo "❌ --timeout must be a positive integer" >&2; exit 2; }
77
- [[ "$FAIL_ON" == "gate" || "$FAIL_ON" == "blocked" || "$FAIL_ON" == "any" ]] || { echo "❌ --fail-on must be gate|blocked|any" >&2; exit 2; }
80
+ [[ "$FAIL_ON" == "gate" || "$FAIL_ON" == "absolute" || "$FAIL_ON" == "any" ]] || { echo "❌ --fail-on must be gate|absolute|any" >&2; exit 2; }
78
81
  if [[ -n "$PROJECT_DIR" ]]; then
79
82
  [[ -d "$PROJECT_DIR" ]] || { echo "❌ Project directory not found: $PROJECT_DIR" >&2; exit 2; }
80
83
  PROJECT_DIR="$(cd "$PROJECT_DIR" && pwd)"
@@ -82,7 +85,6 @@ fi
82
85
  TAPP_PROJECT_ARTIFACTS=""
83
86
  if [[ -n "$PROJECT_DIR" ]]; then
84
87
  [[ -d "$PROJECT_DIR/.tapp" ]] && TAPP_PROJECT_ARTIFACTS="$PROJECT_DIR/.tapp"
85
- [[ -z "$TAPP_PROJECT_ARTIFACTS" && -d "$PROJECT_DIR/.autotap" ]] && TAPP_PROJECT_ARTIFACTS="$PROJECT_DIR/.autotap"
86
88
  fi
87
89
  [[ -z "$FLOWS" && -n "$TAPP_PROJECT_ARTIFACTS" && -d "$TAPP_PROJECT_ARTIFACTS/flows" ]] && FLOWS="$TAPP_PROJECT_ARTIFACTS/flows/*.yml"
88
90
  [[ "$PLATFORM" == "web" && -z "$SCENARIOS" && -n "$TAPP_PROJECT_ARTIFACTS" && -d "$TAPP_PROJECT_ARTIFACTS/scenarios" ]] && SCENARIOS="$TAPP_PROJECT_ARTIFACTS/scenarios/*.yml"
@@ -274,7 +276,7 @@ xcrun simctl install "$UDID" "$APP_PATH" || { echo "❌ simctl install failed
274
276
  # ── Autonomous exploration (quick-capture builds the harness itself if needed).
275
277
  step "Explore ($ACTIONS actions, ${TIMEOUT}s watchdog)"
276
278
  set +e
277
- CAPTURE_ROOT="${TAPP_HOME:-${AUTOTAP_HOME:-$ROOT}}/captures"
279
+ CAPTURE_ROOT="${TAPP_HOME:-$ROOT}/captures"
278
280
  mkdir -p "$CAPTURE_ROOT"
279
281
  CAPTURE_DIR="$(mktemp -d "$CAPTURE_ROOT/ci.XXXXXX")"
280
282
  TAPP_CAPTURE_DIR="$CAPTURE_DIR" OCQA_PR_TARGET_JSON="$IOS_PR_TARGET_JSON" "$ROOT/scripts/quick-capture.sh" explore "$BUNDLE_ID" --actions "$ACTIONS" --timeout "$TIMEOUT"
@@ -26,7 +26,7 @@ def _ctx():
26
26
 
27
27
 
28
28
  SSL_CTX = _ctx()
29
- MODEL = os.environ.get("TAPP_VISION_MODEL") or os.environ.get("AUTOTAP_VISION_MODEL", "claude-haiku-4-5-20251001")
29
+ MODEL = os.environ.get("TAPP_VISION_MODEL", "claude-haiku-4-5-20251001")
30
30
  SYSTEM = (
31
31
  "You are a QA test oracle. You are shown one iOS app screenshot and a CLAIM the test asserts about "
32
32
  "it. Decide if the claim is TRUE of what is actually visible. Be strict and literal — only pass if "
@@ -37,7 +37,10 @@ for (let i = 2; i < process.argv.length; i += 1) {
37
37
  else if (key === "--json-out") args.jsonOut = value;
38
38
  else if (key === "--md-out") args.mdOut = value;
39
39
  else if (key === "--pr-plan") args.prPlan = value;
40
- else throw new Error(`Unknown argument: ${key}`);
40
+ else {
41
+ console.error(`Unknown argument: ${key}`);
42
+ process.exit(2);
43
+ }
41
44
  }
42
45
  if (!["web", "android"].includes(args.platform)) throw new Error("--platform must be web|android");
43
46
  if (args.platform === "web" && !args.url && !args.projectDir) throw new Error("Web gate requires --url or --project-dir for managed build/start");
@@ -94,7 +97,7 @@ if (args.prPlan) {
94
97
  }
95
98
 
96
99
  let managedRuntime = null;
97
- let exitCode = 1;
100
+ let exitCode = 2;
98
101
  try {
99
102
  if (args.platform === "web" && !args.url) {
100
103
  const started = await startManagedWebTarget({
@@ -106,7 +109,7 @@ try {
106
109
  if (started.error) {
107
110
  console.error(`❌ ${started.error}`);
108
111
  if (started.details?.remediation) console.error(` ${started.details.remediation}`);
109
- process.exitCode = 1;
112
+ exitCode = 2;
110
113
  } else {
111
114
  managedRuntime = started;
112
115
  args.url = started.url;
@@ -114,7 +117,7 @@ try {
114
117
  }
115
118
  }
116
119
  if (args.platform === "web" && !args.url) {
117
- exitCode = 1;
120
+ exitCode = 2;
118
121
  } else {
119
122
  const qa = args.platform === "web"
120
123
  ? await runQaWeb({ url: args.url, maxActions: args.actions, timeout: args.timeout, testEmail: process.env.OCQA_TEST_EMAIL, testPassword: process.env.OCQA_TEST_PASSWORD, seedTargets: prExplorationTargets })
@@ -122,7 +125,7 @@ try {
122
125
  testEmail: process.env.OCQA_TEST_EMAIL, testPassword: process.env.OCQA_TEST_PASSWORD, seedTargets: prExplorationTargets });
123
126
  if (qa.error) {
124
127
  console.error(`❌ ${qa.error}`);
125
- exitCode = 1;
128
+ exitCode = 2;
126
129
  } else {
127
130
  const captureDir = qa.structured.capture.path;
128
131
  const markers = path.join(captureDir, "ocqa-markers.txt");
@@ -177,6 +180,9 @@ try {
177
180
  exitCode = report.status ?? 1;
178
181
  }
179
182
  }
183
+ } catch (error) {
184
+ console.error(`❌ ${error?.message || String(error)}`);
185
+ exitCode = 2;
180
186
  } finally {
181
187
  if (managedRuntime) {
182
188
  await stopManagedWebTarget(managedRuntime);
@@ -19,7 +19,7 @@ PROJECT_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
19
19
  # accidentally reuse stale markers). TAPP_HOME redirects all writable output otherwise —
20
20
  # set by the `tapp` CLI when running as an installed npm package, where the package dir
21
21
  # must stay read-only. Unset (repo dev flow), everything lands in the repo as before.
22
- TAPP_RUNTIME_HOME="${TAPP_HOME:-${AUTOTAP_HOME:-}}"
22
+ TAPP_RUNTIME_HOME="${TAPP_HOME:-}"
23
23
  if [[ -n "${TAPP_CAPTURE_DIR:-}" ]]; then
24
24
  CAPTURE_DIR="$TAPP_CAPTURE_DIR"
25
25
  elif [[ -n "$TAPP_RUNTIME_HOME" ]]; then
@@ -262,58 +262,92 @@ case "$MODE" in
262
262
  echo "Running autonomous exploration ($MAX_ACTIONS actions, timeout: ${EXPLORE_TIMEOUT}s, app: $APP_BUNDLE)..."
263
263
  ensure_harness_built "$SIM_NAME"
264
264
 
265
- # Start video recording in background
266
- cleanup_stale_recorders "$UDID"
267
- RECORD_PID=""
268
- if xcrun simctl io "$UDID" recordVideo --codec=h264 "$CAPTURE_DIR/exploration.mov" & then
269
- RECORD_PID=$!
270
- fi
271
- sleep 0.5
272
- if ! kill -0 "$RECORD_PID" 2>/dev/null; then
273
- echo "WARNING: Could not start simulator video recording. Continuing without video." >&2
274
- RECORD_PID=""
265
+ # XCUITest can block inside `app.launch()` until the outer watchdog when the target dies in its
266
+ # initializer, producing a misleading timeout after minutes. For an ordinary unconfigured
267
+ # launch, probe the target directly first and verify that the returned PID survives a short
268
+ # settle window. (Configured launch args/env skip this probe because simctl would not reproduce
269
+ # that launch contract.) The harness still performs its own fresh launch for healthy apps.
270
+ PREFLIGHT_CRASH=0
271
+ PREFLIGHT_OUTPUT=""
272
+ if [[ -z "${OCQA_APP_LAUNCH_ARGS_JSON:-}" && -z "${OCQA_APP_LAUNCH_ENV_JSON:-}" ]]; then
273
+ xcrun simctl terminate "$UDID" "$APP_BUNDLE" >/dev/null 2>&1 || true
274
+ PREFLIGHT_OUTPUT="$(xcrun simctl launch "$UDID" "$APP_BUNDLE" 2>&1 || true)"
275
+ PREFLIGHT_PID="$(echo "$PREFLIGHT_OUTPUT" | sed -n 's/.*: \([0-9][0-9]*\)$/\1/p' | tail -1)"
276
+ if [[ -n "$PREFLIGHT_PID" ]]; then
277
+ sleep 2
278
+ if ! kill -0 "$PREFLIGHT_PID" 2>/dev/null; then PREFLIGHT_CRASH=1; fi
279
+ fi
280
+ xcrun simctl terminate "$UDID" "$APP_BUNDLE" >/dev/null 2>&1 || true
275
281
  fi
276
282
 
277
- # Run exploration with watchdog timeout to avoid silent hangs.
278
283
  local_output_file="$CAPTURE_DIR/harness-output.txt"
279
- run_harness_test "testAutonomousExploration" "$SIM_NAME" "$APP_BUNDLE" "$MAX_ACTIONS" "$EXPLORE_TIMEOUT" > "$local_output_file" 2>&1 &
280
- HARNESS_PID=$!
281
-
282
- START_TS=$(date +%s)
283
284
  TIMED_OUT=0
284
- while kill -0 "$HARNESS_PID" 2>/dev/null; do
285
- NOW_TS=$(date +%s)
286
- ELAPSED=$((NOW_TS - START_TS))
287
- if [[ "$ELAPSED" -ge "$EXPLORE_TIMEOUT" ]]; then
288
- TIMED_OUT=1
289
- kill -TERM "$HARNESS_PID" 2>/dev/null || true
290
- sleep 2
291
- kill -KILL "$HARNESS_PID" 2>/dev/null || true
292
- break
285
+ RECORD_PID=""
286
+ if [[ "$PREFLIGHT_CRASH" -eq 1 ]]; then
287
+ OUTPUT="OCQA_ISSUE:{\"type\":\"crash\",\"severity\":\"critical\",\"title\":\"App crashed during launch preflight\",\"screen\":\"Launch\",\"step\":0}
288
+ OCQA_COMPLETE:{\"actions\":0,\"states\":0,\"issues\":1,\"screens\":\"\",\"outcome\":\"launch_crash\"}"
289
+ printf '%s\n%s\n' "$PREFLIGHT_OUTPUT" "$OUTPUT" > "$local_output_file"
290
+ echo "WARNING: Target process exited during launch preflight; recorded a crash instead of waiting for the exploration timeout." >&2
291
+ else
292
+
293
+ # Start video recording in background
294
+ cleanup_stale_recorders "$UDID"
295
+ if xcrun simctl io "$UDID" recordVideo --codec=h264 "$CAPTURE_DIR/exploration.mov" & then
296
+ RECORD_PID=$!
297
+ fi
298
+ sleep 0.5
299
+ if ! kill -0 "$RECORD_PID" 2>/dev/null; then
300
+ echo "WARNING: Could not start simulator video recording. Continuing without video." >&2
301
+ RECORD_PID=""
293
302
  fi
294
- sleep 2
295
- done
296
303
 
297
- wait "$HARNESS_PID" 2>/dev/null || true
298
- OUTPUT="$(cat "$local_output_file" 2>/dev/null || true)"
304
+ # Run exploration with watchdog timeout to avoid silent hangs.
305
+ run_harness_test "testAutonomousExploration" "$SIM_NAME" "$APP_BUNDLE" "$MAX_ACTIONS" "$EXPLORE_TIMEOUT" > "$local_output_file" 2>&1 &
306
+ HARNESS_PID=$!
307
+
308
+ START_TS=$(date +%s)
309
+ while kill -0 "$HARNESS_PID" 2>/dev/null; do
310
+ NOW_TS=$(date +%s)
311
+ ELAPSED=$((NOW_TS - START_TS))
312
+ if [[ "$ELAPSED" -ge "$EXPLORE_TIMEOUT" ]]; then
313
+ TIMED_OUT=1
314
+ kill -TERM "$HARNESS_PID" 2>/dev/null || true
315
+ sleep 2
316
+ kill -KILL "$HARNESS_PID" 2>/dev/null || true
317
+ break
318
+ fi
319
+ sleep 2
320
+ done
321
+
322
+ wait "$HARNESS_PID" 2>/dev/null || true
323
+ OUTPUT="$(cat "$local_output_file" 2>/dev/null || true)"
299
324
 
300
- # Stop recording
301
- if [[ -n "$RECORD_PID" ]]; then
302
- kill -INT "$RECORD_PID" 2>/dev/null || true
303
- wait "$RECORD_PID" 2>/dev/null || true
325
+ # Stop recording
326
+ if [[ -n "$RECORD_PID" ]]; then
327
+ kill -INT "$RECORD_PID" 2>/dev/null || true
328
+ wait "$RECORD_PID" 2>/dev/null || true
329
+ fi
330
+ sleep 1
304
331
  fi
305
- sleep 1
306
332
 
307
333
  # Parse OCQA_ markers
308
334
  echo "$OUTPUT" | grep "^OCQA_" > "$CAPTURE_DIR/ocqa-markers.txt" || true
309
335
  if [[ "$TIMED_OUT" -eq 1 ]]; then
310
- echo "OCQA_ISSUE:{\"type\":\"explore_timeout\",\"severity\":\"high\",\"title\":\"Exploration timed out\",\"timeoutSeconds\":$EXPLORE_TIMEOUT}" >> "$CAPTURE_DIR/ocqa-markers.txt"
311
- echo "OCQA_COMPLETE:{\"actions\":0,\"states\":0,\"issues\":1,\"screens\":\"\",\"timedOut\":true,\"timeoutSeconds\":$EXPLORE_TIMEOUT}" >> "$CAPTURE_DIR/ocqa-markers.txt"
312
- echo "WARNING: Exploration hit timeout after ${EXPLORE_TIMEOUT}s" >&2
336
+ # The caller's wall-clock budget expiring means the evidence is partial; it is not proof that
337
+ # the app itself is slow or hung. Preserve observed coverage in the terminal marker and let
338
+ # report.js classify the run as inconclusive instead of inventing an app finding.
339
+ TIMED_ACTIONS="$(echo "$OUTPUT" | sed -n 's/^OCQA_PROGRESS:{"action":\([0-9][0-9]*\).*/\1/p' | tail -1)"
340
+ TIMED_STATES="$(echo "$OUTPUT" | sed -n 's/^OCQA_PROGRESS:.*"states":\([0-9][0-9]*\).*/\1/p' | tail -1)"
341
+ TIMED_ISSUES="$(echo "$OUTPUT" | grep -c '^OCQA_ISSUE:' || true)"
342
+ TIMED_ACTIONS="${TIMED_ACTIONS:-0}"
343
+ TIMED_STATES="${TIMED_STATES:-0}"
344
+ TIMED_ISSUES="${TIMED_ISSUES:-0}"
345
+ echo "OCQA_COMPLETE:{\"actions\":$TIMED_ACTIONS,\"states\":$TIMED_STATES,\"issues\":$TIMED_ISSUES,\"screens\":\"\",\"timedOut\":true,\"timeoutSeconds\":$EXPLORE_TIMEOUT}" >> "$CAPTURE_DIR/ocqa-markers.txt"
346
+ echo "WARNING: Exploration reached its ${EXPLORE_TIMEOUT}s time budget; evidence is partial" >&2
313
347
  fi
314
348
  echo "$OUTPUT" > "$CAPTURE_DIR/full-output.txt"
315
349
 
316
- COMPLETE_LINE=$(echo "$OUTPUT" | grep "OCQA_COMPLETE" | tail -1)
350
+ COMPLETE_LINE=$(grep "OCQA_COMPLETE" "$CAPTURE_DIR/ocqa-markers.txt" | tail -1 || true)
317
351
  if [[ -n "$COMPLETE_LINE" ]]; then
318
352
  echo ""
319
353
  echo "Exploration complete: $COMPLETE_LINE"
@@ -26,7 +26,7 @@ UDID="$(xcrun simctl list devices booted -j 2>/dev/null | python3 -c 'import sys
26
26
  [ -z "$UDID" ] && { echo "❌ No booted simulator."; exit 2; }
27
27
  # When run through the installed `tapp` CLI, the harness cache lives under TAPP_HOME.
28
28
  XCTR=""
29
- TAPP_RUNTIME_HOME="${TAPP_HOME:-${AUTOTAP_HOME:-}}"
29
+ TAPP_RUNTIME_HOME="${TAPP_HOME:-}"
30
30
  [ -n "$TAPP_RUNTIME_HOME" ] && XCTR="$(find "$TAPP_RUNTIME_HOME/harness-derived/Build/Products" -name '*.xctestrun' 2>/dev/null | head -1)"
31
31
  [ -z "$XCTR" ] && XCTR="$(find "$HOME/Library/Developer/Xcode/DerivedData/OCQAHarness-"*/Build/Products -name '*.xctestrun' 2>/dev/null | head -1)"
32
32
  [ -z "$XCTR" ] && XCTR="$(find /tmp/tapp-harness-derived/Build/Products -name '*.xctestrun' 2>/dev/null | head -1)"