mirror of
https://github.com/conorluddy/ios-simulator-skill.git
synced 2026-09-14 20:07:12 +08:00
feat/xcode27-docs-and-dependency-floor
3 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
00afc436d6 |
test: harden HangBuster coverage — crashed-worker, fixture, verdicts
Closes #82. Three coverage gaps from the PR #78 review, now closed. The crashed-worker recovery path had no test coverage. Adds: - test_mark_crashed_sets_status — direct call after create - test_mark_crashed_after_claim_overrides_running — overrides running state - test_mark_crashed_silent_on_missing_session — no-op on dead session ID - test_load_summary_returns_none_when_missing — crashed session has no summary.json The 50-event fixture in tests/fixtures/sample_session.py only ever produced CRITICAL events (all duration ≥ 600 ms). L1 hit ~36% of its 200-token budget; L2 only ~8% of 2000. Token-budget tests trivially passed. Hardened make_events(): - 12 clusters spanning all 4 Severity tiers (MINOR/WARN/CRITICAL/FROZEN) - One hot cluster at 18 events (near-max count); others spread across cold tail - Two processes (MyApp + Helper) → process_distribution populates - First-cluster events packed tightly → detect_temporal_bursts populates - 7s gap inserted mid-window → detect_quiet_periods populates - Long symbol names → per-line formatter stress After hardening: L0=28/30 (93%), L1=91/200 (46%), L2=298/2000 (15%). L1/L2 are naturally sparse at default top_n=3; the real win is that all four L2 aggregate branches (Severity / Bursts / Quiet periods / Processes) now actually render. Adds qualitative assertions in test_token_budgets.py: - test_fixture_covers_all_severity_tiers — guard against fixture regression - test_fixture_has_at_least_ten_clusters - test_l2_renders_all_aggregate_branches — every L2 branch must populate - test_l1_floor_is_meaningful — floor at 60 tokens - test_l2_floor_renders_full_summary — floor at 200 tokens diff_sessions() produces 5 verdict strings; only 3 were tested. format_diff was only rendered for 'no change' and version mismatch — the new/resolved/drift render paths were never asserted. Adds 6 new in tests/test_diff.py: - test_diff_verdict_regression_new_minor - test_diff_verdict_improvement_resolved - test_diff_verdict_drift_mixed_signs (the bidirectional drift case) - test_format_diff_renders_new_block - test_format_diff_renders_resolved_block - test_format_diff_renders_drift_block Small helper _summary_with_clusters() builds SessionSummary instances with controlled per-cluster max_duration_ms — keeps verdict tests isolated from the event pipeline. test_diff_detects_resolved_cluster filtered by an old symbol name from the soft fixture ([ImageDecoder decode:]). Updated to use the new equivalent. - ruff check: All checks passed - black --check (scripts): clean - python -m pytest tests/: 103 passed (was 88) - pytest tests/: 103 passed |
||
|
|
1f98d4c51c |
fix: handle zero-duration clusters in diff_sessions()
Closes #79. The divide-by-zero guard in diff_sessions() used a bare `continue`, silently dropping clusters whose A-side max_duration_ms was 0. A real regression where a previously-silent cluster started hanging (0 → N ms) was lost from the diff entirely. Replace with explicit 4-way branching: - both zero → stable - A=0, B>0 → drift, delta_pct = +inf (treated as worsened) - A>0, B=0 → drift, delta_pct = -100.0 (fully improved) - both nonzero → existing percentage logic, unchanged format_diff() renders the inf delta as `new` instead of `+inf%` for readability. Tests: 5 new in tests/test_diff.py covering each branch + a regression guard that the standard nonzero path still uses the drift threshold, plus format_diff render assertion for the inf case. Verification: ruff/black clean; 93 passed (88 → 93). |
||
|
|
c6c0ef8105 |
feat: HangBuster — session-scoped recorder with progressive disclosure (#77) (#78)
* 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 |