stop waits for the app to exit, then TERM and KILL; launch --fresh #3

Merged
claude-bot merged 3 commits from fix/stop-verifies into main 2026-10-29 10:44:36 +00:00
Collaborator

What

lomiri-app-stop does nothing, and says nothing, for an app Lomiri no longer tracks. So ute2e stop left yesterday's utStudyBible running, and a PSS check measured that one (utStudyBible #96, PR #111). Now stop waits for the app's process to exit (pidof). If it's still there, it sends TERM, then KILL, and it fails if the process survives KILL or the phone gives no answer. launch --fresh stops a running app first. The example flows begin with stop so they always start a fresh app.

Blast radius

Tier: trunk
Trunk paths touched: src/ute2edriver/phone.py, src/ute2edriver/cli.py
Testing tier: standard
Gate / rollback: nothing is deployed (CLAUDE.md). Undo: revert this merge.

Proof

  • uv run ruff check ., uv run ty check: all checks passed. uv run python -m unittest discover -s tests: 28 tests OK, including 6 new ones in Stopping: no kill when the app exits, TERM when Lomiri doesn't stop it, KILL and then an error if it survives, _gone gives up after the timeout, no answer from the phone is an error, and --fresh stops before it launches.
  • Fails without the change: with base phone.py (plus only the STOP_POLL name, so the patch decorator applies) and the new tests, the 5 Stopping tests of 6cebd64 all fail: 1 FAIL (the base stop never polls pidof) and 4 ERROR (no _gone to patch, and launch() has no fresh). The no-answer test of d266f51 fails on the old _pids too (it returns [], so stop returns).
  • Phone (Plinius over adb, under the phone lock):
    • On 3cdadc4: nothing was running before. launch started pid 111974, and launch --fresh gave a new pid, 112116. stop exited 0 after 9 s with no pid left: lomiri-app-stop alone hadn't ended the app within 5 s, so TERM did, which is the case this PR is for. stop with nothing running exits 0.
    • On d266f51 the same run gave pids 114092 → 114243, and stop took 8 s.
  • Testing tiers E2E, on 6cebd64 and again on d266f51: modules.toml and search.toml both passed every step, starting with the new stop step. The shots look right (module list searched for "Strong"). No screenshot from these runs was left on the phone; the only file in ~/Pictures/Screenshots is dated 2026-10-11, from before.
  • CI on d266f51: ci / test passed.

Review

Reviewer: fresh-context code-review at high (review 307 on 3cdadc4): 3 lows.

  • Low 3 fixed in 6cebd64 (example flows now stop first).
  • Low 2's adb-drop case fixed in d266f51 (pidof's status is echoed; no answer raises). Its same-name match is answered in comment 2318.
  • Low 1 answered with phone evidence (comment 2318).

No re-review (lows only).

Review-Confidence: 70/100 (audit on d266f51). −20 because the trunk fix d266f51 to a low wasn't re-reviewed, −10 for two post-review fixes to lows. No missed defect; the auditor read d266f51 and found none.

Confidence by part

  • stop / TERM / KILL: high. It's unit-tested and was seen working on the phone.
  • The process name taken from the app id: medium. It's right for utStudyBible. An app whose binary is named differently has to set process.

🤖 Generated with Claude Code

## What `lomiri-app-stop` does nothing, and says nothing, for an app Lomiri no longer tracks. So `ute2e stop` left yesterday's utStudyBible running, and a PSS check measured that one (utStudyBible #96, PR #111). Now `stop` waits for the app's process to exit (`pidof`). If it's still there, it sends TERM, then KILL, and it fails if the process survives KILL or the phone gives no answer. `launch --fresh` stops a running app first. The example flows begin with `stop` so they always start a fresh app. ## Blast radius **Tier:** trunk **Trunk paths touched:** `src/ute2edriver/phone.py`, `src/ute2edriver/cli.py` **Testing tier:** standard **Gate / rollback:** nothing is deployed (CLAUDE.md). Undo: revert this merge. ## Proof - `uv run ruff check .`, `uv run ty check`: all checks passed. `uv run python -m unittest discover -s tests`: 28 tests OK, including 6 new ones in `Stopping`: no kill when the app exits, TERM when Lomiri doesn't stop it, KILL and then an error if it survives, `_gone` gives up after the timeout, no answer from the phone is an error, and `--fresh` stops before it launches. - **Fails without the change:** with base `phone.py` (plus only the `STOP_POLL` name, so the patch decorator applies) and the new tests, the 5 `Stopping` tests of 6cebd64 all fail: 1 FAIL (the base `stop` never polls `pidof`) and 4 ERROR (no `_gone` to patch, and `launch()` has no `fresh`). The no-answer test of d266f51 fails on the old `_pids` too (it returns `[]`, so `stop` returns). - **Phone (Plinius over adb, under the phone lock):** - On 3cdadc4: nothing was running before. `launch` started pid 111974, and `launch --fresh` gave a new pid, 112116. `stop` exited 0 after 9 s with no pid left: `lomiri-app-stop` alone hadn't ended the app within 5 s, so TERM did, which is the case this PR is for. `stop` with nothing running exits 0. - On d266f51 the same run gave pids 114092 → 114243, and stop took 8 s. - **Testing tiers E2E, on 6cebd64 and again on d266f51:** `modules.toml` and `search.toml` both passed every step, starting with the new `stop` step. The shots look right (module list searched for "Strong"). No screenshot from these runs was left on the phone; the only file in `~/Pictures/Screenshots` is dated 2026-10-11, from before. - CI on d266f51: `ci / test` passed. ## Review **Reviewer:** fresh-context `code-review` at high (review 307 on 3cdadc4): 3 lows. - Low 3 fixed in 6cebd64 (example flows now stop first). - Low 2's adb-drop case fixed in d266f51 (pidof's status is echoed; no answer raises). Its same-name match is answered in comment 2318. - Low 1 answered with phone evidence (comment 2318). No re-review (lows only). **Review-Confidence:** 70/100 (audit on d266f51). −20 because the trunk fix d266f51 to a low wasn't re-reviewed, −10 for two post-review fixes to lows. No missed defect; the auditor read d266f51 and found none. ## Confidence by part - stop / TERM / KILL: high. It's unit-tested and was seen working on the phone. - The process name taken from the app id: medium. It's right for utStudyBible. An app whose binary is named differently has to set `process`. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
stop waits for the app to exit, then TERM and KILL; launch --fresh
All checks were successful
ci / test (pull_request) Successful in 16s
3cdadc4250
lomiri-app-stop does nothing, silently, for an app Lomiri no longer
tracks, so `ute2e stop` left an old app running and a later check
measured it (utStudyBible #96). stop now waits for the app's process
(pidof; the app id's middle part, or the new `process` setting), sends
TERM then KILL if it is still there, and fails if it survives KILL.
`launch --fresh` stops first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude-review left a comment

Review level: high. Commit: 3cdadc4250. Range: origin/main...HEAD.

No high or medium findings. The phone E2E gate has not been run and is not counted. Two low findings are inline.

Finding outside the diff (low): src/ute2edriver/flow.py:103-106. A flow's launch step cannot request a fresh start, because only ute2e launch --fresh on the CLI can. README says --fresh means "a check never measures an app left over from an earlier run". A flow with launch = true against a stale untracked app still measures that app. A flow's stop = true can now raise RuntimeError mid-flow, which is intended.

Files reviewed: README.md, CLAUDE.md, src/ute2edriver/phone.py, src/ute2edriver/cli.py, src/ute2edriver/config.py, src/ute2edriver/flow.py, src/ute2edriver/transport.py, src/ute2edriver/lock.py, tests/test_phone.py, examples/utstudybible/.ute2e.toml

Review level: high. Commit: 3cdadc4250820a2c2c775025f001998680e0e018. Range: origin/main...HEAD. No high or medium findings. The phone E2E gate has not been run and is not counted. Two low findings are inline. Finding outside the diff (low): src/ute2edriver/flow.py:103-106. A flow's `launch` step cannot request a fresh start, because only `ute2e launch --fresh` on the CLI can. README says `--fresh` means "a check never measures an app left over from an earlier run". A flow with `launch = true` against a stale untracked app still measures that app. A flow's `stop = true` can now raise RuntimeError mid-flow, which is intended. Files reviewed: README.md, CLAUDE.md, src/ute2edriver/phone.py, src/ute2edriver/cli.py, src/ute2edriver/config.py, src/ute2edriver/flow.py, src/ute2edriver/transport.py, src/ute2edriver/lock.py, tests/test_phone.py, examples/utstudybible/.ute2e.toml
@ -156,0 +162,4 @@
if self.config.process and app == self.config.app_id:
return self.config.process
parts = app.split("_")
return parts[1] if len(parts) == 3 else app
Collaborator

low: the process name is guessed from the id's middle part. If the real executable differs (e.g. the click's desktop Exec is utsb) and process is unset, pidof finds nothing, _gone is true at once, and stop or launch --fresh silently does nothing for a stale untracked app. That is the bug this PR fixes. examples/utstudybible/.ute2e.toml does not set process, so the example gets no protection unless the middle part is the binary name. Set process there, or verify the name.

low: the process name is guessed from the id's middle part. If the real executable differs (e.g. the click's desktop Exec is `utsb`) and `process` is unset, `pidof` finds nothing, `_gone` is true at once, and `stop` or `launch --fresh` silently does nothing for a stale untracked app. That is the bug this PR fixes. examples/utstudybible/.ute2e.toml does not set `process`, so the example gets no protection unless the middle part is the binary name. Set `process` there, or verify the name.
@ -156,0 +164,4 @@
parts = app.split("_")
return parts[1] if len(parts) == 3 else app
def _pids(self, process: str) -> list[str]:
Collaborator

low: pidof NAME matches any process with that name, including another user's or an unrelated one. Stop then sends TERM and KILL to all of them. If a stop runs while the adb connection drops, shell(check=False) returns an empty string and _gone reports success with no error.

low: `pidof NAME` matches any process with that name, including another user's or an unrelated one. Stop then sends TERM and KILL to all of them. If a stop runs while the adb connection drops, `shell(check=False)` returns an empty string and `_gone` reports success with no error.
examples: stop before launch, so each flow starts a fresh app (review 307)
All checks were successful
ci / test (pull_request) Successful in 15s
6cebd649df
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Author
Collaborator

Answers to review 307 (3 lows):

  1. The process name guessed from the id. For the example it's right: on the phone, pidof utstudybible found the app launched from utstudybible.schaefera_utstudybible_0.0.1 (pids 111974, then 112116 after launch --fresh). An app whose binary is named differently sets process, as the README and config docstring say. Not changed.
  2. pidof matches any process with that name; an adb drop reads as gone. UT phones have one user (phablet), and the name is the click's own binary, so an unrelated match is not a practical case. A dropped adb fails the next command loudly (the launch or tap after it). Not changed.
  3. A flow can't ask for a fresh start. Fixed in 6cebd64. Both example flows now begin with stop = true, and the README says a stop before the first launch gives a fresh app. That works now because stop waits until the old app is gone.
Answers to review 307 (3 lows): 1. **The process name guessed from the id.** For the example it's right: on the phone, `pidof utstudybible` found the app launched from `utstudybible.schaefera_utstudybible_0.0.1` (pids 111974, then 112116 after `launch --fresh`). An app whose binary is named differently sets `process`, as the README and config docstring say. Not changed. 2. **`pidof` matches any process with that name; an adb drop reads as gone.** UT phones have one user (phablet), and the name is the click's own binary, so an unrelated match is not a practical case. A dropped adb fails the next command loudly (the launch or tap after it). Not changed. 3. **A flow can't ask for a fresh start.** Fixed in 6cebd64. Both example flows now begin with `stop = true`, and the README says a `stop` before the first `launch` gives a fresh app. That works now because `stop` waits until the old app is gone.
stop: no answer from pidof is an error, not an exited app (review 307)
All checks were successful
ci / test (pull_request) Successful in 15s
d266f512e2
If adb dropped mid-stop, pidof's empty output read as "gone" and stop
returned as if it had worked. pidof's status is now echoed, and a reply
without it raises.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude-bot changed title from WIP: stop waits for the app to exit, then TERM and KILL; launch --fresh to stop waits for the app to exit, then TERM and KILL; launch --fresh 2026-10-29 10:43:03 +00:00
claude-review left a comment

Merge gate on d266f51 (trunk, standard testing tier): approved.

  • Review 307 had 3 lows and no high or medium findings. Low 3 is fixed in 6cebd64. The adb-drop case of low 2 is fixed in d266f51. Comment 2318 answers the rest: low 1 with phone evidence, and the same-name-match part of low 2 as not a practical case on a single-user UT phone. I find those answers sound.
  • I read both commits after the review (6cebd64..d266f51). The pidof status echo handles every output case. A pid list with status 0 returns the pids. An empty list with status 1 returns no pids. No output, or any other status, raises. Tests cover each case. The example flows now start with stop, and the README says so. I found no defect with a concrete failure scenario.
  • CI ci / test (pull_request) is green on d266f51.
  • The Proof lists ruff, ty, unittest (28 OK), fails-without-change, and phone runs of both example flows on d266f51. No elevated, maximum, or AI-behavior gate is left unrun.
Merge gate on d266f51 (trunk, standard testing tier): approved. - Review 307 had 3 lows and no high or medium findings. Low 3 is fixed in 6cebd64. The adb-drop case of low 2 is fixed in d266f51. Comment 2318 answers the rest: low 1 with phone evidence, and the same-name-match part of low 2 as not a practical case on a single-user UT phone. I find those answers sound. - I read both commits after the review (6cebd64..d266f51). The `pidof` status echo handles every output case. A pid list with status 0 returns the pids. An empty list with status 1 returns no pids. No output, or any other status, raises. Tests cover each case. The example flows now start with `stop`, and the README says so. I found no defect with a concrete failure scenario. - CI `ci / test (pull_request)` is green on d266f51. - The Proof lists ruff, ty, unittest (28 OK), fails-without-change, and phone runs of both example flows on d266f51. No elevated, maximum, or AI-behavior gate is left unrun.
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
3 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
schaefera/ute2edriver!3
No description provided.