From 19f9538bb65a89bf9f015aa09b00785ab9cfa644 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 18 Feb 2026 14:12:05 +0000 Subject: [PATCH 1/2] Refactor command parsing to use native argparse help - Remove manual interception of -h/--help in `sid/main.py`. - Update `argparse.ArgumentParser` to use `add_help=True` (default) and `RawDescriptionHelpFormatter`. - Remove manual definition of -h/--help argument. - Update global help description to be more informative. - Fix environment-specific failure in `test_logs_crash_report` by patching `os.path.expanduser`. Co-authored-by: acrollet <101649+acrollet@users.noreply.github.com> --- sid/main.py | 66 +++++++++++++++--------------------------- tests/test_commands.py | 4 ++- 2 files changed, 27 insertions(+), 43 deletions(-) diff --git a/sid/main.py b/sid/main.py index 1e12816..5847c89 100644 --- a/sid/main.py +++ b/sid/main.py @@ -7,54 +7,42 @@ from sid.commands.doctor import doctor_cmd def main(): - if "-h" in sys.argv or "--help" in sys.argv: - print("Sid: A CLI for iOS Automation") - print(""" + DESCRIPTION = """\ +Sid: A Token-Efficient CLI for iOS Automation + Vision: - inspect Inspect UI hierarchy and return a simplified JSON tree. - screenshot Capture the visual state for verification. + inspect Inspect UI hierarchy and return a simplified JSON tree + screenshot Capture the visual state for verification Interaction: - tap Tap a UI element (by label or X Y coordinates). - type Input text into the currently focused field. - scroll Scroll the screen. - gesture Perform a specific gesture. + tap Tap a UI element (by label or X Y coordinates) + type Input text into the currently focused field + scroll Scroll the screen + gesture Perform a specific gesture System: - launch Launch an application. - stop Terminate a running application. - relaunch Stop and then start an application. - open Open a URL scheme or Universal Link. - permission Manage TCC (Privacy) permissions. - location Simulate GPS coordinates. - network Simulate network conditions (Not Supported). + launch Launch an application + stop Terminate a running application + relaunch Stop and then start an application + open Open a URL scheme or Universal Link + permission Manage TCC (Privacy) permissions + location Simulate GPS coordinates Verification: - assert Perform a quick boolean check on the UI state. - wait Wait for an element to appear or disappear. - logs Fetch the tail of the system log for the target app. - tree List files in the app's sandbox containers. + assert Perform a quick boolean check on the UI state + wait Wait for an element to appear or disappear + logs Fetch the tail of the system log for the target app + tree List files in the app's sandbox containers Utils: - doctor Check if all dependencies (idb, xcrun) are installed. - -Options: - -h, --help Show this help message - -Examples: - sid launch com.apple.Preferences --clean - sid inspect - sid tap "Settings" - sid assert "General" visible -""") - sys.exit(0) + doctor Check if all dependencies are installed +""" parser = argparse.ArgumentParser( - description="Sid: A CLI for iOS Automation", + description=DESCRIPTION, + formatter_class=argparse.RawDescriptionHelpFormatter, usage="sid [command] [options]", - add_help=False ) - parser.add_argument('-h', '--help', action='store_true') subparsers = parser.add_subparsers(dest="command", required=True, help="Available commands") @@ -134,13 +122,7 @@ def main(): subparsers.add_parser("doctor", help="Check if all dependencies are installed.") - try: - args = parser.parse_args() - except SystemExit: - if '-h' in sys.argv or '--help' in sys.argv: - # This shouldn't be reached if we exit early, but as a safety: - sys.exit(0) - raise + args = parser.parse_args() if args.command == "inspect": inspect_cmd(interactive_only=args.interactive_only, depth=args.depth) diff --git a/tests/test_commands.py b/tests/test_commands.py index 2f4a182..9bda8da 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -121,11 +121,13 @@ def test_tap_error_code(self, mock_get_tree): output = captured_output.getvalue() self.assertIn("ERR_ELEMENT_NOT_FOUND", output) + @patch('os.path.expanduser') @patch('os.path.exists') @patch('os.listdir') @patch('os.path.getmtime') @patch('builtins.open', new_callable=unittest.mock.mock_open, read_data="Crash content") - def test_logs_crash_report(self, mock_file, mock_mtime, mock_listdir, mock_exists): + def test_logs_crash_report(self, mock_file, mock_mtime, mock_listdir, mock_exists, mock_expanduser): + mock_expanduser.return_value = "/Users/acrollet/Library/Logs/DiagnosticReports" # Mock STATE_FILE exists and contains bundle_id # We need to handle multiple open calls mock_exists.side_effect = lambda p: p == "/tmp/sid_last_bundle_id" or p == "/Users/acrollet/Library/Logs/DiagnosticReports" From ef331c08929fa0e2a0696381e683b2d95a5a5e0d Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Thu, 19 Feb 2026 06:07:51 +0000 Subject: [PATCH 2/2] Refactor command parsing to use native argparse help in Pippin - Remove manual interception of -h/--help in `pippin/main.py`. - Update `argparse.ArgumentParser` to use `add_help=True` (default) and `RawDescriptionHelpFormatter`. - Remove manual definition of -h/--help argument. - Update global help description to reference "Pippin" and be more informative. - Fix environment-specific failure in `tests/test_commands.py` (test_logs_crash_report) by patching `os.path.expanduser`. - Ensure all references are updated to "Pippin" after upstream rename. Co-authored-by: acrollet <101649+acrollet@users.noreply.github.com> --- README.md | 38 +++++++------- SPEC.md | 66 ++++++++++++------------ docs/improvements/01-hierarchy.md | 12 ++--- docs/improvements/02-subcommand-help.md | 22 ++++---- docs/improvements/03-context-command.md | 20 +++---- docs/improvements/04-error-model.md | 6 +-- docs/improvements/05-device-targeting.md | 28 +++++----- docs/improvements/06-element-matching.md | 14 ++--- docs/improvements/07-action-feedback.md | 18 +++---- docs/improvements/08-housekeeping.md | 12 ++--- docs/improvements/README.md | 4 +- {sid => pippin}/__init__.py | 0 {sid => pippin}/commands/__init__.py | 0 {sid => pippin}/commands/doctor.py | 6 +-- {sid => pippin}/commands/interaction.py | 4 +- {sid => pippin}/commands/system.py | 12 ++--- {sid => pippin}/commands/verification.py | 10 ++-- {sid => pippin}/commands/vision.py | 6 +-- {sid => pippin}/main.py | 14 ++--- {sid => pippin}/utils/__init__.py | 0 {sid => pippin}/utils/executor.py | 0 {sid => pippin}/utils/ui.py | 2 +- pyproject.toml | 4 +- tests/test_commands.py | 48 ++++++++--------- tests/test_executor.py | 2 +- uv.lock | 2 +- 26 files changed, 175 insertions(+), 175 deletions(-) rename {sid => pippin}/__init__.py (100%) rename {sid => pippin}/commands/__init__.py (100%) rename {sid => pippin}/commands/doctor.py (97%) rename {sid => pippin}/commands/interaction.py (97%) rename {sid => pippin}/commands/system.py (92%) rename {sid => pippin}/commands/verification.py (96%) rename {sid => pippin}/commands/vision.py (95%) rename {sid => pippin}/main.py (94%) rename {sid => pippin}/utils/__init__.py (100%) rename {sid => pippin}/utils/executor.py (100%) rename {sid => pippin}/utils/ui.py (98%) diff --git a/README.md b/README.md index e67fdb2..9e663ff 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# Sid: A Token-Efficient CLI for iOS Automation +# Pippin: A Token-Efficient CLI for iOS Automation -**Sid** (Simulator Driver) is a command-line interface designed to bridge the gap between Large Language Models (LLMs) and the iOS Simulator. It provides a set of stateless, atomic commands to inspect, interact with, and verify the state of iOS applications running in the Simulator. +**Pippin** (Simulator Driver) is a command-line interface designed to bridge the gap between Large Language Models (LLMs) and the iOS Simulator. It provides a set of stateless, atomic commands to inspect, interact with, and verify the state of iOS applications running in the Simulator. ## Features @@ -10,86 +10,86 @@ ## Installation -You can run Sid directly using `uvx` (recommended): +You can run Pippin directly using `uvx` (recommended): ```bash -uvx sid --help +uvx pippin --help ``` Or install it via pip: ```bash -pip install sid +pip install pippin ``` *Note: You must have `idb` (iOS Development Bridge) and Xcode command-line tools installed and configured on your machine.* ## Usage -Sid commands follow the structure: `sid [command] [subcommand] [flags]` +Pippin commands follow the structure: `pippin [command] [subcommand] [flags]` ### Vision (Seeing the Screen) * **Inspect UI:** Get a JSON representation of the current screen. ```bash - sid inspect --interactive-only + pippin inspect --interactive-only ``` * **Take Screenshot:** Capture the visual state. ```bash - sid screenshot output.png + pippin screenshot output.png ``` ### Interaction (Acting on the App) * **Tap Element:** Tap by accessibility identifier or label text. ```bash - sid tap "Log In" + pippin tap "Log In" ``` * **Type Text:** Enter text into the focused field. ```bash - sid type "user@example.com" --submit + pippin type "user@example.com" --submit ``` * **Scroll:** Scroll in a direction, optionally until an element is found. ```bash - sid scroll down --until-visible "Submit" + pippin scroll down --until-visible "Submit" ``` * **Gestures:** Perform swipes. ```bash - sid gesture swipe 100,200 100,400 + pippin gesture swipe 100,200 100,400 ``` ### System (Controlling the Environment) * **Launch App:** Launch an app by Bundle ID. ```bash - sid launch com.example.myapp --clean + pippin launch com.example.myapp --clean ``` * **Open URL:** Open a deep link. ```bash - sid open "myapp://settings" + pippin open "myapp://settings" ``` * **Permissions:** Manage privacy permissions. ```bash - sid permission camera grant + pippin permission camera grant ``` * **Location:** Set simulated GPS coordinates. ```bash - sid location 37.7749 -122.4194 + pippin location 37.7749 -122.4194 ``` ### Verification (Checking State) * **Assert:** Verify UI state (exists, visible, hidden, text matches). ```bash - sid assert "Welcome Message" visible + pippin assert "Welcome Message" visible ``` * **Logs:** Fetch recent app logs. ```bash - sid logs --crash-report + pippin logs --crash-report ``` * **File Tree:** List files in the app's sandbox. ```bash - sid tree documents + pippin tree documents ``` ## Contributing diff --git a/SPEC.md b/SPEC.md index 1a774f5..157d8c8 100644 --- a/SPEC.md +++ b/SPEC.md @@ -1,16 +1,16 @@ -# Sid: A Token-Efficient CLI for iOS Automation +# Pippin: A Token-Efficient CLI for iOS Automation ## 1. Philosophy & Goals -**Sid** (Simulator Driver) is a command-line interface designed to bridge the gap between Large Language Models (LLMs) and the iOS Simulator. +**Pippin** (Simulator Driver) is a command-line interface designed to bridge the gap between Large Language Models (LLMs) and the iOS Simulator. -* **Token Efficiency (The "Narrow Context" Principle):** Sid’s primary output is a simplified, text-based JSON representation of the UI. This allows LLMs to "see" the screen using minimal tokens, avoiding the high cost and latency of processing raw screenshots. -* **Stateless Atomic Actions:** Each command is independent. Sid does not maintain a complex session, making it easier for an Agent to reason about the state at any given step. -* **Native Wrapper:** Under the hood, Sid orchestrates `xcrun simctl` (for system tasks) and `idb` (for deep accessibility inspection). +* **Token Efficiency (The "Narrow Context" Principle):** Pippin’s primary output is a simplified, text-based JSON representation of the UI. This allows LLMs to "see" the screen using minimal tokens, avoiding the high cost and latency of processing raw screenshots. +* **Stateless Atomic Actions:** Each command is independent. Pippin does not maintain a complex session, making it easier for an Agent to reason about the state at any given step. +* **Native Wrapper:** Under the hood, Pippin orchestrates `xcrun simctl` (for system tasks) and `idb` (for deep accessibility inspection). --- ## 2. Architecture -* **Interface:** `sid [command] [subcommand] [flags]` +* **Interface:** `pippin [command] [subcommand] [flags]` * **Output Format:** Standard JSON (for machine parsing) or human-readable text. * **Error Handling:** Returns strictly formatted error codes and descriptive messages to help the LLM self-correct (e.g., `ERR_ELEMENT_NOT_FOUND`, `ERR_APP_CRASHED`). @@ -21,7 +21,7 @@ ### 3.1. Vision (The "See" Commands) *These commands generate the context for the LLM to understand the current state.* -#### `sid inspect` +#### `pippin inspect` Returns a simplified JSON tree of the current screen's accessibility hierarchy. * **Flag:** `--interactive-only` (Default: `true`). Filters out structural containers (`Window`, `Other`) and keeps actionable elements (`Button`, `TextField`, `Cell`, `Switch`, `StaticText`). * **Flag:** `--depth [n]`. Limits the hierarchy depth to save tokens. @@ -37,7 +37,7 @@ Returns a simplified JSON tree of the current screen's accessibility hierarchy. } ``` -#### `sid screenshot` +#### `pippin screenshot` Captures the visual state for verification or multimodal fallback. * **Args:** `[filename]` * **Flag:** `--mask-text` (Optional). Redacts text for privacy/security before saving. @@ -47,57 +47,57 @@ Captures the visual state for verification or multimodal fallback. ### 3.2. Interaction (The "Act" Commands) *Direct manipulation of the app UI.* -#### `sid tap` +#### `pippin tap` Taps a UI element. * **Targeting Logic:** Accepts a string query. 1. **Exact Match:** Accessibility Identifier. 2. **Fuzzy Match:** Label text (e.g., "Login" matches "Log In"). 3. **Coordinate Fallback:** `--x [num] --y [num]`. -* **Example:** `sid tap "Sign Up"` +* **Example:** `pippin tap "Sign Up"` -#### `sid type` +#### `pippin type` Inputs text into the currently focused field. * **Args:** `[text_string]` * **Flag:** `--submit` (Default: `false`). Hits "Return/Enter" on the keyboard after typing. -* **Example:** `sid type "user@example.com" --submit` +* **Example:** `pippin type "user@example.com" --submit` -#### `sid scroll` +#### `pippin scroll` * **Args:** `[direction]` (`up`, `down`, `left`, `right`). * **Flag:** `--until-visible [element_label]`. A specialized loop that scrolls until a specific element appears in the `inspect` tree. -#### `sid gesture` -* **Swipe:** `sid gesture swipe [start_x],[start_y] [end_x],[end_y]` -* **Pinch:** `sid gesture pinch [in|out]` +#### `pippin gesture` +* **Swipe:** `pippin gesture swipe [start_x],[start_y] [end_x],[end_y]` +* **Pinch:** `pippin gesture pinch [in|out]` --- ### 3.3. System & Environment (The "God Mode") *Developers need to test how the app behaves under different system conditions.* -#### `sid launch` +#### `pippin launch` * **Args:** `[bundle_id]` * **Flag:** `--clean`. Wipes the app container (simulates a fresh install). * **Flag:** `--args "[key]=[value]"`. Passes Launch Arguments (e.g., `-TakingScreenshots YES`). * **Flag:** `--locale [code]`. Launches the app in a specific language (e.g., `es-MX`). -#### `sid open` +#### `pippin open` Opens a URL scheme or Universal Link to test routing. * **Args:** `[url]` -* **Example:** `sid open "myapp://settings/profile?edit=true"` +* **Example:** `pippin open "myapp://settings/profile?edit=true"` -#### `sid permission` +#### `pippin permission` Manages TCC (Privacy) permissions to test "Happy Path" vs. "Denied Path". * **Args:** `[service] [status]` * **Services:** `camera`, `photos`, `location`, `microphone`, `contacts`, `calendar`. * **Status:** `grant`, `deny`, `reset`. -* **Example:** `sid permission camera deny` +* **Example:** `pippin permission camera deny` -#### `sid location` +#### `pippin location` Simulates GPS coordinates. * **Args:** `[lat] [lon]` -* **Example:** `sid location 37.7749 -122.4194` (San Francisco) +* **Example:** `pippin location 37.7749 -122.4194` (San Francisco) -#### `sid network` (Advanced) +#### `pippin network` (Advanced) * **Args:** `[condition]` * **Options:** `wifi`, `cellular`, `offline`. @@ -106,18 +106,18 @@ Simulates GPS coordinates. ### 3.4. Verification & Debugging (The "Check" Commands) *Tools for the LLM to verify success or diagnose failure.* -#### `sid assert` +#### `pippin assert` Quick boolean check for LLM usage. * **Args:** `[element_query] [state]` * **States:** `exists`, `visible`, `hidden`, `text=[value]`. * **Output:** `PASS` or `FAIL: Element found but text was 'Cancel', expected 'Submit'`. -#### `sid logs` +#### `pippin logs` Fetches the tail of the system log for the target app. * **Flag:** `--crash-report`. Checks if a crash log was generated in the last session and outputs the stack trace. * **Use Case:** "The app closed unexpectedly. Why?" -#### `sid tree` +#### `pippin tree` Lists files in the app's sandbox. * **Args:** `[directory]` (`documents`, `caches`, `tmp`). * **Use Case:** Verifying that a file download or database export actually occurred. @@ -128,11 +128,11 @@ Lists files in the app's sandbox. **Objective:** "Verify that the app handles denied Camera permissions gracefully." -1. `sid launch com.myapp.beta --clean` -2. `sid inspect` -> Finds "Start Scan" button. -3. `sid permission camera deny` (Pre-emptively deny permission). -4. `sid tap "Start Scan"` -5. `sid inspect` +1. `pippin launch com.myapp.beta --clean` +2. `pippin inspect` -> Finds "Start Scan" button. +3. `pippin permission camera deny` (Pre-emptively deny permission). +4. `pippin tap "Start Scan"` +5. `pippin inspect` * **Agent Logic:** Looks for an alert with text "Camera Permission Needed" or "Open Settings". - * **If found:** `sid assert "Open Settings" visible` -> Returns `PASS`. + * **If found:** `pippin assert "Open Settings" visible` -> Returns `PASS`. * **If not found:** Agent marks test as `FAILED` (App likely stalled or crashed). diff --git a/docs/improvements/01-hierarchy.md b/docs/improvements/01-hierarchy.md index fde2e1a..3ad3b81 100644 --- a/docs/improvements/01-hierarchy.md +++ b/docs/improvements/01-hierarchy.md @@ -1,8 +1,8 @@ # 01: Preserve UI Hierarchy in Inspect Output -**Impact:** Critical — this is the single biggest reason AI agents struggle with Sid. +**Impact:** Critical — this is the single biggest reason AI agents struggle with Pippin. **Effort:** Medium -**Files:** `sid/utils/ui.py`, `sid/commands/vision.py` +**Files:** `pippin/utils/ui.py`, `pippin/commands/vision.py` ## Problem @@ -108,10 +108,10 @@ def simplify_node(node, interactive_only=False, depth=None, current_depth=0): ### 3. Update `inspect_cmd` to use hierarchy by default ``` -sid inspect → hierarchical output (new default) -sid inspect --flat → current flat behavior (backward compat) -sid inspect --all → hierarchical, no filtering -sid inspect --depth 3 → limit nesting depth +pippin inspect → hierarchical output (new default) +pippin inspect --flat → current flat behavior (backward compat) +pippin inspect --all → hierarchical, no filtering +pippin inspect --depth 3 → limit nesting depth ``` ### Example: Hierarchical Output diff --git a/docs/improvements/02-subcommand-help.md b/docs/improvements/02-subcommand-help.md index b44ea7a..c980339 100644 --- a/docs/improvements/02-subcommand-help.md +++ b/docs/improvements/02-subcommand-help.md @@ -2,7 +2,7 @@ **Impact:** High — AI agents (and humans) cannot discover argument syntax. **Effort:** Small -**Files:** `sid/main.py` +**Files:** `pippin/main.py` ## Problem @@ -10,12 +10,12 @@ Lines 10-50 of `main.py` intercept `-h` / `--help` before argparse processes the ```python if "-h" in sys.argv or "--help" in sys.argv: - print("Sid: A CLI for iOS Automation") + print("Pippin: A CLI for iOS Automation") print("""...""") sys.exit(0) ``` -This means `sid tap --help`, `sid inspect -h`, `sid launch --help` all print the same top-level overview. The per-subcommand parsers have detailed argument definitions (e.g., `--interactive-only`, `--depth`, `--submit`, `--until-visible`) but they're completely invisible. +This means `pippin tap --help`, `pippin inspect -h`, `pippin launch --help` all print the same top-level overview. The per-subcommand parsers have detailed argument definitions (e.g., `--interactive-only`, `--depth`, `--submit`, `--until-visible`) but they're completely invisible. ## Proposed Fix @@ -33,7 +33,7 @@ Remove the manual help interception entirely and let argparse handle it natively ```python DESCRIPTION = """\ -Sid: A Token-Efficient CLI for iOS Automation +Pippin: A Token-Efficient CLI for iOS Automation Vision: inspect Inspect UI hierarchy and return a simplified JSON tree @@ -66,7 +66,7 @@ Utils: parser = argparse.ArgumentParser( description=DESCRIPTION, formatter_class=argparse.RawDescriptionHelpFormatter, - usage="sid [command] [options]", + usage="pippin [command] [options]", ) ``` @@ -75,10 +75,10 @@ parser = argparse.ArgumentParser( After this change: ``` -$ sid --help → Shows the grouped overview + global options -$ sid tap --help → Shows: "Tap a UI element" + args/flags for tap -$ sid inspect --help → Shows: --interactive-only, --all, --depth flags -$ sid launch --help → Shows: bundle_id, --clean, --args, --locale +$ pippin --help → Shows the grouped overview + global options +$ pippin tap --help → Shows: "Tap a UI element" + args/flags for tap +$ pippin inspect --help → Shows: --interactive-only, --all, --depth flags +$ pippin launch --help → Shows: bundle_id, --clean, --args, --locale ``` ### Also fix the `except SystemExit` block @@ -93,6 +93,6 @@ No try/except needed — argparse will print help and exit cleanly on its own. ## Testing -- Verify `sid -h` still shows the grouped overview. -- Verify `sid -h` shows per-subcommand args. +- Verify `pippin -h` still shows the grouped overview. +- Verify `pippin -h` shows per-subcommand args. - Verify that invalid arguments produce useful error messages (argparse does this by default). diff --git a/docs/improvements/03-context-command.md b/docs/improvements/03-context-command.md index 8cacc48..b6f2420 100644 --- a/docs/improvements/03-context-command.md +++ b/docs/improvements/03-context-command.md @@ -2,15 +2,15 @@ **Impact:** High — reduces multi-call orientation to a single call. **Effort:** Medium -**Files:** new `sid/commands/context.py`, `sid/main.py` +**Files:** new `pippin/commands/context.py`, `pippin/main.py` ## Problem To understand "where am I and what can I do?", an AI agent currently needs: -1. `sid inspect` — what elements are on screen? -2. `sid screenshot` — what does it look like? (if multimodal) -3. `sid logs` — did anything go wrong? +1. `pippin inspect` — what elements are on screen? +2. `pippin screenshot` — what does it look like? (if multimodal) +3. `pippin logs` — did anything go wrong? 4. Manual reasoning about which app is running, what screen this is, etc. That's 2-3 round-trips minimum, each costing latency and tokens. The agent also has to synthesize the results itself, which is error-prone. @@ -18,7 +18,7 @@ That's 2-3 round-trips minimum, each costing latency and tokens. The agent also ## Proposed Command ``` -sid context [--include-logs] [--screenshot ] +pippin context [--include-logs] [--screenshot ] ``` Returns a single JSON blob with everything an AI needs to orient: @@ -64,7 +64,7 @@ Returns a single JSON blob with everything an AI needs to orient: ## Implementation -### `sid/commands/context.py` +### `pippin/commands/context.py` ```python def context_cmd(include_logs=False, screenshot_path=None): @@ -146,10 +146,10 @@ No. `inspect` is for targeted UI queries during a flow. `context` is for orienta ## CLI Integration ``` -sid context → full context, no logs, no screenshot -sid context --include-logs → include last 20 lines of app logs -sid context --screenshot state.png → also capture a screenshot -sid context --brief → just screen metadata, no UI tree +pippin context → full context, no logs, no screenshot +pippin context --include-logs → include last 20 lines of app logs +pippin context --screenshot state.png → also capture a screenshot +pippin context --brief → just screen metadata, no UI tree ``` ## Testing diff --git a/docs/improvements/04-error-model.md b/docs/improvements/04-error-model.md index 749cece..96c5045 100644 --- a/docs/improvements/04-error-model.md +++ b/docs/improvements/04-error-model.md @@ -2,7 +2,7 @@ **Impact:** High — AI agents rely on exit codes to know if a command succeeded. **Effort:** Small -**Files:** all command files, `sid/utils/errors.py` (new) +**Files:** all command files, `pippin/utils/errors.py` (new) ## Problem @@ -18,11 +18,11 @@ Exit code behavior is inconsistent across commands: | `logs` (no target app) | Prints to stderr | `0` | | `launch` (error) | Prints to stderr | `0` | -An AI agent running `sid tap "Nonexistent"` gets exit code 0 and thinks it succeeded. +An AI agent running `pippin tap "Nonexistent"` gets exit code 0 and thinks it succeeded. ## Proposed Error Model -### 1. Define error codes in `sid/utils/errors.py` +### 1. Define error codes in `pippin/utils/errors.py` ```python import sys diff --git a/docs/improvements/05-device-targeting.md b/docs/improvements/05-device-targeting.md index 2805caa..a89c0a5 100644 --- a/docs/improvements/05-device-targeting.md +++ b/docs/improvements/05-device-targeting.md @@ -2,7 +2,7 @@ **Impact:** Medium — tool completely breaks with multiple simulators. **Effort:** Small -**Files:** `sid/main.py`, `sid/utils/ui.py`, `sid/utils/device.py` (new) +**Files:** `pippin/main.py`, `pippin/utils/ui.py`, `pippin/utils/device.py` (new) ## Problem @@ -13,7 +13,7 @@ No udid provided and there are multiple companions to run against dict_keys(['61B8F683-...', 'BB13ECAA-...']) ``` -There's no `--udid` or `--device` flag anywhere in sid. The tool uses `"booted"` for `simctl` commands but `idb` needs explicit targeting when multiple companions are registered. +There's no `--udid` or `--device` flag anywhere in pippin. The tool uses `"booted"` for `simctl` commands but `idb` needs explicit targeting when multiple companions are registered. ## Proposed Changes @@ -25,16 +25,16 @@ In `main.py`, add a global argument before the subparsers: parser.add_argument( "--device", help="Target simulator UDID. Defaults to the booted simulator. " - "Use 'sid doctor' to list available devices.", + "Use 'pippin doctor' to list available devices.", default=None, ) ``` -### 2. Create `sid/utils/device.py` +### 2. Create `pippin/utils/device.py` ```python import json -from sid.utils.executor import execute_command +from pippin.utils.executor import execute_command _target_udid = None @@ -84,7 +84,7 @@ def get_simctl_target(): In `ui.py`, update `ensure_idb_connected()` and all `idb` calls to use the target UDID: ```python -from sid.utils.device import get_target_udid +from pippin.utils.device import get_target_udid def get_ui_tree(silent=False): udid = get_target_udid() @@ -96,9 +96,9 @@ def get_ui_tree(silent=False): ### 4. Enhance `doctor` to list devices ``` -$ sid doctor +$ pippin doctor -Checking Sid dependencies... +Checking Pippin dependencies... ✅ idb found ✅ xcrun found @@ -106,10 +106,10 @@ Simulators: BB13ECAA-... iPhone 16 Pro iOS 18.2 Booted ← active 61B8F683-... iPhone 15 iOS 17.5 Booted -✨ Sid is ready. Using: iPhone 16 Pro (BB13ECAA-...) +✨ Pippin is ready. Using: iPhone 16 Pro (BB13ECAA-...) ``` -This helps the user (and AI) discover available devices and understand which one sid will target. +This helps the user (and AI) discover available devices and understand which one pippin will target. ### 5. Wire it up in `main.py` @@ -117,7 +117,7 @@ This helps the user (and AI) discover available devices and understand which one args = parser.parse_args() if args.device: - from sid.utils.device import set_target_device + from pippin.utils.device import set_target_device set_target_device(args.device) # ... dispatch to command @@ -125,11 +125,11 @@ if args.device: ## Environment Variable Fallback -Also support `SID_DEVICE_UDID` env var so users can set it once per terminal session: +Also support `PIPPIN_DEVICE_UDID` env var so users can set it once per terminal session: ```bash -export SID_DEVICE_UDID=BB13ECAA-05F4-4E3B-A220-235BDBADFAB5 -sid inspect # uses that device +export PIPPIN_DEVICE_UDID=BB13ECAA-05F4-4E3B-A220-235BDBADFAB5 +pippin inspect # uses that device ``` ## Testing diff --git a/docs/improvements/06-element-matching.md b/docs/improvements/06-element-matching.md index 665224c..3de4a2d 100644 --- a/docs/improvements/06-element-matching.md +++ b/docs/improvements/06-element-matching.md @@ -2,7 +2,7 @@ **Impact:** Medium — reduces "tapped the wrong thing" failures. **Effort:** Medium -**Files:** `sid/utils/ui.py` +**Files:** `pippin/utils/ui.py` ## Problem @@ -11,7 +11,7 @@ 1. **Exact match on AXIdentifier** — good, but many elements lack identifiers. 2. **Substring match on AXLabel** — returns the *first* element containing the query. -The substring match is fragile. `sid tap "General"` will match whichever of these comes first in the flat list: +The substring match is fragile. `pippin tap "General"` will match whichever of these comes first in the flat list: - "General" (the cell we want) - "General Settings" (a different cell) - "In General, ..." (a description label) @@ -73,8 +73,8 @@ def find_element(query: str, silent=False, strict=False): ### 2. Add `--strict` flag to `tap` and `assert` ``` -sid tap "General" --strict # Only exact ID or exact label match -sid tap "General" # Current behavior (substring fallback) +pippin tap "General" --strict # Only exact ID or exact label match +pippin tap "General" # Current behavior (substring fallback) ``` ### 3. Ambiguity reporting @@ -94,8 +94,8 @@ The AI sees this warning on stderr and can decide whether to retry with a more s Allow `type:label` syntax to disambiguate: ``` -sid tap "Cell:General" # Only match cells labeled "General" -sid tap "Button:Cancel" # Only match buttons labeled "Cancel" +pippin tap "Cell:General" # Only match cells labeled "General" +pippin tap "Button:Cancel" # Only match buttons labeled "Cancel" ``` Implementation in `find_element`: @@ -119,7 +119,7 @@ if element_type: When inspect shows numbered elements, allow tapping by index: ``` -sid tap --index 3 # Tap the 3rd interactive element on screen +pippin tap --index 3 # Tap the 3rd interactive element on screen ``` This is a last resort but useful when labels are ambiguous or missing. diff --git a/docs/improvements/07-action-feedback.md b/docs/improvements/07-action-feedback.md index dea3453..27c067f 100644 --- a/docs/improvements/07-action-feedback.md +++ b/docs/improvements/07-action-feedback.md @@ -2,17 +2,17 @@ **Impact:** Medium — eliminates the mandatory inspect-after-every-action pattern. **Effort:** Small -**Files:** `sid/commands/interaction.py`, `sid/main.py` +**Files:** `pippin/commands/interaction.py`, `pippin/main.py` ## Problem -The typical AI agent loop with sid looks like: +The typical AI agent loop with pippin looks like: ``` -sid tap "Settings" → "Tapped at 187, 340" -sid inspect → { ... new screen ... } -sid tap "General" → "Tapped at 187, 540" -sid inspect → { ... new screen ... } +pippin tap "Settings" → "Tapped at 187, 340" +pippin inspect → { ... new screen ... } +pippin tap "General" → "Tapped at 187, 540" +pippin inspect → { ... new screen ... } ``` Every action requires a follow-up `inspect` to see what happened. That's 2x the calls needed. @@ -24,7 +24,7 @@ Every action requires a follow-up `inspect` to see what happened. That's 2x the Add a global flag that appends an `inspect` result after any interaction command: ``` -sid tap "Settings" --inspect +pippin tap "Settings" --inspect ``` Output: @@ -86,7 +86,7 @@ else: UI transitions take time. The `--inspect` should include a configurable settle delay: ``` -sid tap "Settings" --inspect --settle 0.5 # Wait 500ms before inspecting +pippin tap "Settings" --inspect --settle 0.5 # Wait 500ms before inspecting ``` Default: 300ms. This avoids capturing mid-animation states. @@ -96,7 +96,7 @@ Default: 300ms. This avoids capturing mid-animation states. Combine with the wait command for transitions: ``` -sid tap "Settings" --wait "General" --inspect +pippin tap "Settings" --wait "General" --inspect ``` This means: tap Settings, wait until "General" appears in the UI, then return the inspect result. Useful for navigation transitions where the AI knows what to expect on the next screen. diff --git a/docs/improvements/08-housekeeping.md b/docs/improvements/08-housekeeping.md index 2c5eb12..6cde6fa 100644 --- a/docs/improvements/08-housekeeping.md +++ b/docs/improvements/08-housekeeping.md @@ -8,19 +8,19 @@ ### 1. Duplicated STATE_FILE constant -`STATE_FILE = "/tmp/sid_last_bundle_id"` is defined in both: -- `sid/commands/system.py:6` -- `sid/commands/verification.py:7` +`STATE_FILE = "/tmp/pippin_last_bundle_id"` is defined in both: +- `pippin/commands/system.py:6` +- `pippin/commands/verification.py:7` -And it's also read inline in `sid/commands/vision.py:35-42`. +And it's also read inline in `pippin/commands/vision.py:35-42`. **Fix:** Move to a shared location: ```python -# sid/utils/state.py +# pippin/utils/state.py import os -STATE_FILE = "/tmp/sid_last_bundle_id" +STATE_FILE = "/tmp/pippin_last_bundle_id" def get_last_bundle_id() -> str | None: if os.path.exists(STATE_FILE): diff --git a/docs/improvements/README.md b/docs/improvements/README.md index 6c73f60..09e5b83 100644 --- a/docs/improvements/README.md +++ b/docs/improvements/README.md @@ -1,6 +1,6 @@ -# Sid Improvement Plan +# Pippin Improvement Plan -Recommendations for making Sid more effective as an AI-agent bridge to the iOS Simulator. Organized into independent workstreams that can be tackled in any order, though the suggested priority reflects impact. +Recommendations for making Pippin more effective as an AI-agent bridge to the iOS Simulator. Organized into independent workstreams that can be tackled in any order, though the suggested priority reflects impact. ## Priority Order diff --git a/sid/__init__.py b/pippin/__init__.py similarity index 100% rename from sid/__init__.py rename to pippin/__init__.py diff --git a/sid/commands/__init__.py b/pippin/commands/__init__.py similarity index 100% rename from sid/commands/__init__.py rename to pippin/commands/__init__.py diff --git a/sid/commands/doctor.py b/pippin/commands/doctor.py similarity index 97% rename from sid/commands/doctor.py rename to pippin/commands/doctor.py index 463111e..56aab94 100644 --- a/sid/commands/doctor.py +++ b/pippin/commands/doctor.py @@ -2,7 +2,7 @@ import sys import os import subprocess -from sid.utils.executor import execute_command +from pippin.utils.executor import execute_command def _install_idb(): print("\nAttempting to install idb dependencies...") @@ -41,7 +41,7 @@ def _install_idb(): return False def doctor_cmd(): - print("Checking Sid dependencies...\n") + print("Checking Pippin dependencies...\n") dependencies = { "idb": "Essential for UI inspection and advanced interactions.", @@ -112,7 +112,7 @@ def doctor_cmd(): all_passed = False if all_passed: - print("\n✨ Sid is ready to go!") + print("\n✨ Pippin is ready to go!") else: print("\n⚠️ Some dependencies are missing or misconfigured.") sys.exit(1) diff --git a/sid/commands/interaction.py b/pippin/commands/interaction.py similarity index 97% rename from sid/commands/interaction.py rename to pippin/commands/interaction.py index 1ca345c..13b49cd 100644 --- a/sid/commands/interaction.py +++ b/pippin/commands/interaction.py @@ -1,7 +1,7 @@ import sys import time -from sid.utils.executor import execute_command -from sid.utils.ui import get_ui_tree, find_element, get_center +from pippin.utils.executor import execute_command +from pippin.utils.ui import get_ui_tree, find_element, get_center def tap_cmd(query: str = None, x: int = None, y: int = None): target_x, target_y = None, None diff --git a/sid/commands/system.py b/pippin/commands/system.py similarity index 92% rename from sid/commands/system.py rename to pippin/commands/system.py index 2513966..45613b7 100644 --- a/sid/commands/system.py +++ b/pippin/commands/system.py @@ -1,9 +1,9 @@ import sys import os import shlex -from sid.utils.executor import execute_command +from pippin.utils.executor import execute_command -STATE_FILE = "/tmp/sid_last_bundle_id" +STATE_FILE = "/tmp/pippin_last_bundle_id" def _get_app_container(bundle_id): try: @@ -22,7 +22,7 @@ def launch_cmd(bundle_id: str, clean: bool = False, args: str = None, locale: st pass if not bundle_id: - print("ERR_NO_TARGET_APP: Could not determine target app. Run 'sid launch' first or provide a bundle ID.", file=sys.stderr) + print("ERR_NO_TARGET_APP: Could not determine target app. Run 'pippin launch' first or provide a bundle ID.", file=sys.stderr) return if clean: @@ -68,7 +68,7 @@ def stop_cmd(bundle_id: str = None): pass if not bundle_id: - print("ERR_NO_TARGET_APP: Could not determine target app. Run 'sid launch' first or provide a bundle ID.", file=sys.stderr) + print("ERR_NO_TARGET_APP: Could not determine target app. Run 'pippin launch' first or provide a bundle ID.", file=sys.stderr) return try: @@ -87,7 +87,7 @@ def relaunch_cmd(bundle_id: str = None, clean: bool = False, args: str = None, l pass if not bundle_id: - print("ERR_NO_TARGET_APP: Could not determine target app. Run 'sid launch' first or provide a bundle ID.", file=sys.stderr) + print("ERR_NO_TARGET_APP: Could not determine target app. Run 'pippin launch' first or provide a bundle ID.", file=sys.stderr) return stop_cmd(bundle_id) @@ -110,7 +110,7 @@ def permission_cmd(service: str, status: str): pass if not bundle_id: - print("ERR_NO_TARGET_APP: Could not determine target app. Run 'sid launch' first.", file=sys.stderr) + print("ERR_NO_TARGET_APP: Could not determine target app. Run 'pippin launch' first.", file=sys.stderr) return try: diff --git a/sid/commands/verification.py b/pippin/commands/verification.py similarity index 96% rename from sid/commands/verification.py rename to pippin/commands/verification.py index 0e1ce64..9b06d00 100644 --- a/sid/commands/verification.py +++ b/pippin/commands/verification.py @@ -1,10 +1,10 @@ import sys import os import time -from sid.utils.executor import execute_command -from sid.utils.ui import find_element +from pippin.utils.executor import execute_command +from pippin.utils.ui import find_element -STATE_FILE = "/tmp/sid_last_bundle_id" +STATE_FILE = "/tmp/pippin_last_bundle_id" def wait_cmd(query: str, timeout: float = 10.0, state: str = "visible"): """Waits for an element to reach a certain state.""" @@ -67,7 +67,7 @@ def logs_cmd(crash_report: bool = False): pass if not bundle_id: - print("ERR_NO_TARGET_APP: Could not determine target app. Run 'sid launch' first.", file=sys.stderr) + print("ERR_NO_TARGET_APP: Could not determine target app. Run 'pippin launch' first.", file=sys.stderr) return if crash_report: @@ -133,7 +133,7 @@ def tree_cmd(directory: str): pass if not bundle_id: - print("ERR_NO_TARGET_APP: Could not determine target app. Run 'sid launch' first.", file=sys.stderr) + print("ERR_NO_TARGET_APP: Could not determine target app. Run 'pippin launch' first.", file=sys.stderr) return subpath = "" diff --git a/sid/commands/vision.py b/pippin/commands/vision.py similarity index 95% rename from sid/commands/vision.py rename to pippin/commands/vision.py index e72d361..88f3762 100644 --- a/sid/commands/vision.py +++ b/pippin/commands/vision.py @@ -1,7 +1,7 @@ import json import sys -from sid.utils.executor import execute_command -from sid.utils.ui import get_ui_tree +from pippin.utils.executor import execute_command +from pippin.utils.ui import get_ui_tree def inspect_cmd(interactive_only: bool = True, depth: int = None): if depth is not None: @@ -32,7 +32,7 @@ def inspect_cmd(interactive_only: bool = True, depth: int = None): break # If we have a state file, use that as a fallback/source for bundle_id - from sid.commands.verification import STATE_FILE + from pippin.commands.verification import STATE_FILE import os if os.path.exists(STATE_FILE): try: diff --git a/sid/main.py b/pippin/main.py similarity index 94% rename from sid/main.py rename to pippin/main.py index 5847c89..1720f9b 100644 --- a/sid/main.py +++ b/pippin/main.py @@ -1,14 +1,14 @@ import argparse import sys -from sid.commands.vision import inspect_cmd, screenshot_cmd -from sid.commands.interaction import tap_cmd, type_cmd, scroll_cmd, gesture_cmd -from sid.commands.system import launch_cmd, stop_cmd, relaunch_cmd, open_cmd, permission_cmd, location_cmd, network_cmd -from sid.commands.verification import assert_cmd, logs_cmd, tree_cmd, wait_cmd -from sid.commands.doctor import doctor_cmd +from pippin.commands.vision import inspect_cmd, screenshot_cmd +from pippin.commands.interaction import tap_cmd, type_cmd, scroll_cmd, gesture_cmd +from pippin.commands.system import launch_cmd, stop_cmd, relaunch_cmd, open_cmd, permission_cmd, location_cmd, network_cmd +from pippin.commands.verification import assert_cmd, logs_cmd, tree_cmd, wait_cmd +from pippin.commands.doctor import doctor_cmd def main(): DESCRIPTION = """\ -Sid: A Token-Efficient CLI for iOS Automation +Pippin: A Token-Efficient CLI for iOS Automation Vision: inspect Inspect UI hierarchy and return a simplified JSON tree @@ -41,7 +41,7 @@ def main(): parser = argparse.ArgumentParser( description=DESCRIPTION, formatter_class=argparse.RawDescriptionHelpFormatter, - usage="sid [command] [options]", + usage="pippin [command] [options]", ) subparsers = parser.add_subparsers(dest="command", required=True, help="Available commands") diff --git a/sid/utils/__init__.py b/pippin/utils/__init__.py similarity index 100% rename from sid/utils/__init__.py rename to pippin/utils/__init__.py diff --git a/sid/utils/executor.py b/pippin/utils/executor.py similarity index 100% rename from sid/utils/executor.py rename to pippin/utils/executor.py diff --git a/sid/utils/ui.py b/pippin/utils/ui.py similarity index 98% rename from sid/utils/ui.py rename to pippin/utils/ui.py index 1034d4d..ef54b18 100644 --- a/sid/utils/ui.py +++ b/pippin/utils/ui.py @@ -1,7 +1,7 @@ import sys import json import subprocess -from sid.utils.executor import execute_command +from pippin.utils.executor import execute_command def flatten_tree(nodes): flat_list = [] diff --git a/pyproject.toml b/pyproject.toml index 281bc21..c59b1b3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [project] -name = "sid" +name = "pippin" version = "0.1.0" description = "A CLI for iOS Automation" readme = "README.md" @@ -7,7 +7,7 @@ requires-python = ">=3.9" dependencies = [] [project.scripts] -sid = "sid.main:main" +pippin = "pippin.main:main" [build-system] requires = ["hatchling"] diff --git a/tests/test_commands.py b/tests/test_commands.py index 9bda8da..eaa311c 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -5,14 +5,14 @@ from io import StringIO # We need to import the modules to patch them -import sid.commands.vision as vision -import sid.commands.interaction as interaction -import sid.commands.system as system -import sid.commands.verification as verification +import pippin.commands.vision as vision +import pippin.commands.interaction as interaction +import pippin.commands.system as system +import pippin.commands.verification as verification class TestCommands(unittest.TestCase): - @patch('sid.commands.vision.get_ui_tree') + @patch('pippin.commands.vision.get_ui_tree') @patch('os.path.exists') @patch('builtins.open', new_callable=unittest.mock.mock_open, read_data="com.dynamic.app") def test_inspect_basic(self, mock_file, mock_exists, mock_get_tree): @@ -38,8 +38,8 @@ def test_inspect_basic(self, mock_file, mock_exists, mock_get_tree): self.assertIn('"type": "Button"', output) self.assertNotIn('"type": "Window"', output) # Interactive only filters window - @patch('sid.utils.ui.get_ui_tree') # Patch where find_element looks it up - @patch('sid.commands.interaction.execute_command') + @patch('pippin.utils.ui.get_ui_tree') # Patch where find_element looks it up + @patch('pippin.commands.interaction.execute_command') def test_tap_with_query(self, mock_exec, mock_get_tree): # Mock tree mock_data = [ @@ -52,7 +52,7 @@ def test_tap_with_query(self, mock_exec, mock_get_tree): # Expect tap call with center: 10+50=60, 20+25=45 mock_exec.assert_any_call(["idb", "ui", "tap", "60.0", "45.0"]) - @patch('sid.commands.system.execute_command') + @patch('pippin.commands.system.execute_command') def test_launch(self, mock_exec): system.launch_cmd("com.test.app", clean=True, args="-flag val", locale="en_US") @@ -63,8 +63,8 @@ def test_launch(self, mock_exec): expected_launch = ["xcrun", "simctl", "launch", "booted", "com.test.app", "-AppleLanguages", "(en_US)", "-AppleLocale", "en_US", "-flag", "val"] mock_exec.assert_any_call(expected_launch) - @patch('sid.commands.verification.execute_command') - @patch('sid.utils.ui.get_ui_tree') + @patch('pippin.commands.verification.execute_command') + @patch('pippin.utils.ui.get_ui_tree') def test_assert_exists(self, mock_get_tree, mock_exec): mock_data = [ {"role": "Button", "AXIdentifier": "btn1", "AXLabel": "Login"} @@ -81,7 +81,7 @@ def test_assert_exists(self, mock_get_tree, mock_exec): output = captured_output.getvalue().strip() self.assertEqual(output, "PASS") - @patch('sid.commands.interaction.get_ui_tree') + @patch('pippin.commands.interaction.get_ui_tree') def test_scroll_dynamic_dimensions(self, mock_get_tree): # Mock a large window (e.g. iPad) mock_data = [ @@ -89,7 +89,7 @@ def test_scroll_dynamic_dimensions(self, mock_get_tree): ] mock_get_tree.return_value = mock_data - with patch('sid.commands.interaction.execute_command') as mock_exec: + with patch('pippin.commands.interaction.execute_command') as mock_exec: interaction.scroll_cmd("down") # Get the actual call arguments @@ -107,7 +107,7 @@ def test_scroll_dynamic_dimensions(self, mock_get_tree): self.assertAlmostEqual(float(cmd[5]), 512.0) self.assertAlmostEqual(float(cmd[6]), 409.8) - @patch('sid.utils.ui.get_ui_tree') + @patch('pippin.utils.ui.get_ui_tree') def test_tap_error_code(self, mock_get_tree): mock_get_tree.return_value = [] # No elements @@ -130,7 +130,7 @@ def test_logs_crash_report(self, mock_file, mock_mtime, mock_listdir, mock_exist mock_expanduser.return_value = "/Users/acrollet/Library/Logs/DiagnosticReports" # Mock STATE_FILE exists and contains bundle_id # We need to handle multiple open calls - mock_exists.side_effect = lambda p: p == "/tmp/sid_last_bundle_id" or p == "/Users/acrollet/Library/Logs/DiagnosticReports" + mock_exists.side_effect = lambda p: p == "/tmp/pippin_last_bundle_id" or p == "/Users/acrollet/Library/Logs/DiagnosticReports" # First call to open is for STATE_FILE # Second call is for the crash report @@ -153,7 +153,7 @@ def test_logs_crash_report(self, mock_file, mock_mtime, mock_listdir, mock_exist self.assertIn("CRASH_REPORT_FOUND", output) self.assertIn("Stack trace line 1", output) - @patch('sid.commands.doctor.execute_command') + @patch('pippin.commands.doctor.execute_command') @patch('shutil.which') @patch('builtins.input', return_value='n') def test_doctor_fail_no_install(self, mock_input, mock_which, mock_exec): @@ -164,7 +164,7 @@ def test_doctor_fail_no_install(self, mock_input, mock_which, mock_exec): sys.stdout = captured_stdout try: with self.assertRaises(SystemExit): - from sid.commands.doctor import doctor_cmd + from pippin.commands.doctor import doctor_cmd doctor_cmd() finally: sys.stdout = sys.__stdout__ @@ -176,10 +176,10 @@ def test_doctor_fail_no_install(self, mock_input, mock_which, mock_exec): self.assertIn("❌ idb NOT FOUND", output) self.assertIn("⚠️ Some dependencies are missing or misconfigured.", output) - @patch('sid.commands.doctor.execute_command') + @patch('pippin.commands.doctor.execute_command') @patch('shutil.which') @patch('builtins.input', return_value='y') - @patch('sid.commands.doctor._install_idb', return_value=True) + @patch('pippin.commands.doctor._install_idb', return_value=True) def test_doctor_install_success(self, mock_install, mock_input, mock_which, mock_exec): # Simulate idb missing initially, then found after install mock_which.side_effect = ["/usr/bin/xcrun", None, "/path/to/idb"] # xcrun checked first? No, idb then xcrun @@ -193,7 +193,7 @@ def test_doctor_install_success(self, mock_install, mock_input, mock_which, mock captured_stdout = StringIO() sys.stdout = captured_stdout try: - from sid.commands.doctor import doctor_cmd + from pippin.commands.doctor import doctor_cmd doctor_cmd() except SystemExit: pass @@ -204,7 +204,7 @@ def test_doctor_install_success(self, mock_install, mock_input, mock_which, mock self.assertTrue(mock_install.called) self.assertIn("✅ idb found at: /path/to/idb", output) - @patch('sid.utils.ui.get_ui_tree') + @patch('pippin.utils.ui.get_ui_tree') def test_wait_success(self, mock_get_tree): # Element found on second try mock_data = [ @@ -223,8 +223,8 @@ def test_wait_success(self, mock_get_tree): output = captured_output.getvalue() self.assertIn("PASS: Element 'btn1' is visible.", output) - @patch('sid.utils.ui.get_ui_tree') - @patch('sid.commands.interaction.execute_command') + @patch('pippin.utils.ui.get_ui_tree') + @patch('pippin.commands.interaction.execute_command') def test_tap_partial_label(self, mock_exec, mock_get_tree): # Mock tree with a long label mock_data = [ @@ -236,7 +236,7 @@ def test_tap_partial_label(self, mock_exec, mock_get_tree): interaction.tap_cmd(query="Welcome") mock_exec.assert_any_call(["idb", "ui", "tap", "50.0", "50.0"]) - @patch('sid.commands.system.execute_command') + @patch('pippin.commands.system.execute_command') @patch('os.path.exists', return_value=True) @patch('builtins.open', new_callable=unittest.mock.mock_open, read_data="com.test.app") def test_stop(self, mock_file, mock_exists, mock_exec): @@ -244,7 +244,7 @@ def test_stop(self, mock_file, mock_exists, mock_exec): # Verify terminate call mock_exec.assert_any_call(["xcrun", "simctl", "terminate", "booted", "com.test.app"]) - @patch('sid.commands.system.execute_command') + @patch('pippin.commands.system.execute_command') @patch('os.path.exists', return_value=True) @patch('builtins.open', new_callable=unittest.mock.mock_open, read_data="com.test.app") def test_relaunch(self, mock_file, mock_exists, mock_exec): diff --git a/tests/test_executor.py b/tests/test_executor.py index 1fc3dc2..9497a02 100644 --- a/tests/test_executor.py +++ b/tests/test_executor.py @@ -1,5 +1,5 @@ import unittest -from sid.utils.executor import execute_command, set_dry_run, register_mock_response, clear_mock_responses +from pippin.utils.executor import execute_command, set_dry_run, register_mock_response, clear_mock_responses import subprocess class TestExecutor(unittest.TestCase): diff --git a/uv.lock b/uv.lock index 8fd895b..28d37c6 100644 --- a/uv.lock +++ b/uv.lock @@ -2,6 +2,6 @@ version = 1 requires-python = ">=3.9" [[package]] -name = "sid" +name = "pippin" version = "0.1.0" source = { editable = "." }