mirror of
https://github.com/conorluddy/ios-simulator-skill.git
synced 2026-09-14 20:07:12 +08:00
c6c0ef8105
* feat: HangBuster session mode for hang_watcher (#77) Evolves hang_watcher.py from a passive `tail -f`-style log stream into an agent-native session recorder with progressive disclosure. Default --stop output is ~80-120 tokens (vs ~3-5k for v1 raw streaming) with explicit drill paths to per-cluster detail. Architecture: pure-function filter pipeline in common/hang_pipeline.py (parse → normalise → threshold → bucket → cluster → aggregate → rank → format). Session storage in common/hang_sessions.py with its own dir layout (~/.ios-simulator-skill/sessions/<id>/{meta.json, events.jsonl, summary.json}); 4-hex session-id suffix avoids same-second collisions. New CLI surface (HangBuster): - --start: detached worker via Popen(start_new_session=True); worker writes its own pid into meta.json (no pidfile race), opens events.jsonl line- buffered, applies normalise+threshold+bucket inline, select-based readline timeout with stream_ended sentinel. - --stop / --get-details [--cluster N | --raw] / --list-sessions / --clear-sessions [--older-than DURATION] / --diff A B - --budget-tokens picks the densest level (L0/L1/L2) that fits via char/4 estimator; --terse forces L0. - --auto-sample soft-imports main_thread_sampler (#62) with debounce (one sample per cluster per session); graceful no-op if missing. - --diff guards against fingerprint_version mismatch and emits a verdict line (regression / improvement / drift / no change) for CI. Backward compat: HangWatcher class + --watch / --since unchanged. Stateless parse helpers extracted to common/hang_pipeline.py; HangWatcher delegates. New env vars (all via env_int): IOS_SIM_HANG_MIN_MS (default 250), IOS_SIM_HANG_SESSION_TTL_HOURS (24), IOS_SIM_HANG_DEFAULT_TOP_N (3), IOS_SIM_HANG_BUDGET_TOKENS (unset). Tests: scaffolds pytest infrastructure (pyproject.toml + tests/conftest.py + pythonpath config) since the repo had none. 88 tests across pipeline (51), sessions (23), token budgets (8), diff (6). Token-budget contract asserted: 50-event session emits ≤200 tokens at L1, ≤2000 tokens at L2/L3. Docs: SKILL.md and README updated with HangBuster section + env vars. Closes #77 * docs: update CLAUDE.md with HangBuster + new shared utilities - Add hang_watcher.py + common/hang_pipeline.py + common/hang_sessions.py to the project structure tree - Add hang_watcher.py to Testing & Analysis category (now 6 scripts) - Document hang_pipeline.py + hang_sessions.py under Shared Utilities - Update Key Statistics: 7 shared utilities (was 6), reference 88 tests * test: manual smoke script for HangBuster lifecycle Walks through help / empty session / real session / drill (L2 + L3) / budget collapse / JSON output / diff / list+clear / legacy --watch in one script. Skips simulator-dependent steps when none is booted. Verified on macOS 26 with a booted iPhone simulator: 21 checks pass. Usage: tests/manual/hangbuster_smoke.sh # use booted simulator tests/manual/hangbuster_smoke.sh "iPhone 15" # boots a device first IOS_SIM_HANG_MIN_MS=100 tests/manual/hangbuster_smoke.sh * fix: address PR #78 review — blockers, atomicity, and code hygiene Blockers: - hang_watcher.py: `--top N` ternary was inverted; `--top 5` silently returned all clusters. Drop the ternary and always slice. - pyproject.toml: add "." to pytest pythonpath so bare `pytest tests/` resolves `tests.fixtures.*` imports (previously only worked via `python -m pytest`). Atomicity / boundary fixes: - hang_sessions.py: fsync before os.replace in _write_meta and _write_summary — atomic rename does not imply durability. - hang_sessions.py: new SessionStore.persist_worker_counters() public method; HangBuster.run_worker now calls it instead of reaching into the private _write_meta. The new method re-reads meta so a concurrent stop() that already set status=stopped is not clobbered to running. - hang_sessions.py: parse JSON for stream_ended sentinel detection instead of substring match (avoids false-positives on any message containing the literal 'stream_ended'). Code hygiene: - hang_pipeline.py: cluster_to_json / event_to_jsonl drop redundant Severity.value overwrites — asdict() leaves StrEnum members in place and json.dumps serialises them as strings (StrEnum is a str subclass). - hang_pipeline.py: distinct CRITICAL icon (‼) so format_l1 no longer uses the same ⚠ for both WARN and CRITICAL. - hang_watcher.py: delete duplicated HangWatcher._resolve_predicate method; route the two call sites to the module-level function that the worker already uses. Verification: - ruff check, black --check: clean - pytest tests/ and python -m pytest tests/: 88 passed * chore: simplify marketplace manifest - Move description to top-level field (was nested under metadata) - Drop pinned version from plugin.json so the git commit SHA becomes the version, giving users updates on every push
77 lines
2.5 KiB
TOML
77 lines
2.5 KiB
TOML
[project]
|
|
name = "ios-simulator-skill"
|
|
version = "1.5.0"
|
|
description = "Build, test, and automate iOS apps with accessibility-driven navigation"
|
|
readme = "README.md"
|
|
requires-python = ">=3.12"
|
|
license = {file = "LICENSE.md"}
|
|
|
|
[tool.black]
|
|
line-length = 100
|
|
target-version = ["py312"]
|
|
include = '\.pyi?$'
|
|
extend-exclude = '''
|
|
/(
|
|
\.git
|
|
| \.ruff_cache
|
|
| \.venv
|
|
| build
|
|
| dist
|
|
)/
|
|
'''
|
|
|
|
[tool.ruff]
|
|
line-length = 100
|
|
target-version = "py312"
|
|
# Only check skill code, not root-level dev scripts
|
|
include = ["ios-simulator-skill/skills/ios-simulator-skill/scripts/**/*.py"]
|
|
|
|
[tool.ruff.lint]
|
|
# Enable ALL recommended rules (strict mode)
|
|
select = [
|
|
"E", # pycodestyle errors
|
|
"W", # pycodestyle warnings
|
|
"F", # pyflakes
|
|
"I", # isort
|
|
"N", # pep8-naming
|
|
"UP", # pyupgrade
|
|
"B", # flake8-bugbear
|
|
"C4", # flake8-comprehensions
|
|
"SIM", # flake8-simplify
|
|
"RET", # flake8-return
|
|
"ARG", # flake8-unused-arguments
|
|
"PTH", # flake8-use-pathlib
|
|
"PL", # pylint
|
|
"RUF", # ruff-specific rules
|
|
]
|
|
|
|
# Strict mode with pragmatic exceptions for CLI tools
|
|
ignore = [
|
|
"PLR0913", # Too many arguments - common in CLI tools
|
|
"PLR2004", # Magic values - acceptable for coordinates/constants
|
|
"PLR0912", # Too many branches - acceptable for CLI scripts
|
|
"PLR0915", # Too many statements - acceptable for main() functions
|
|
"PLR0911", # Too many return statements
|
|
"PLC0415", # Import outside top-level - lazy imports are intentional
|
|
"PTH123", # Use Path.open() - open() is fine for simple cases
|
|
"PTH119", # Use Path - os.path is fine for simple cases
|
|
"E501", # Line too long - Black handles this, some long lines OK
|
|
"E722", # Bare except - acceptable for top-level error handling
|
|
"RUF012", # Mutable class attributes - acceptable for constants
|
|
"RUF001", # Ambiguous characters - intentional use of emoji
|
|
"RUF005", # Iterable unpacking - list concatenation is readable
|
|
"ARG001", # Unused function arguments - required by signal handlers
|
|
"SIM102", # Nested if statements - more readable in some cases
|
|
"PLW2901", # Redefined loop variable - acceptable in some contexts
|
|
"F401", # Unused import - may be used in __init__.py exports
|
|
"N806", # Non-lowercase variable - acceptable for constants
|
|
]
|
|
|
|
[tool.ruff.lint.isort]
|
|
known-first-party = ["xcode"]
|
|
|
|
[tool.pytest.ini_options]
|
|
testpaths = ["tests"]
|
|
pythonpath = [".", "ios-simulator-skill/skills/ios-simulator-skill/scripts"]
|
|
addopts = "-ra"
|