testsAndMisc/NEXT_SESSION_PROMPT.md
Krzysztof kuhy Rudnicki 4fafa0bc62
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
Rewrite the brief around what this session proved wrong
Three of the old brief's own instructions did not survive contact:

  - the config.sh fingerprint recipe CANNOT WORK. Four policy fields are
    frozenset, so json.dumps(default=str) serialises them in per-process
    iteration order; three runs gave three digests. Replaced with the repo's
    own deterministic exporter (baseline 83e05e82) AND a diff of the sourced
    environment, because the hash alone passed clean through both bugs hit
    this session.
  - extract_shell_functions.py does NOT reliably place its source line. It
    got the position wrong three ways: before SCRIPT_DIR existed, before
    STATE_DIR (every path then expanded against an empty prefix), and on a
    nested re-source instead of the top-level one.
  - "100% line coverage, always" now has a measured exception, and stating
    it as an absolute is actively dangerous. daemon_apps.sh has five
    uncovered `done <file` lines whose loops call pm; a reader who pipes
    them -- as this session correctly did in curfew_net.sh, where the body
    is pure -- reintroduces a measured 1-of-54 bug in the code that decides
    which apps get disabled at home. It is now a PROHIBITION at the top of
    the file, with the line numbers and mapfile as the only alternative.

Also new: the kcov artifact taxonomy with an explicit safe/unsafe column,
the mutation tooling and the rule that an equivalent mutation means dead
code to delete rather than a test to write, and the two hazards
phone_focus_mode now carries (config.sh at exactly 250 with fifteen
loader-pinned variables enforced only by a comment; deploy.sh's two
hardcoded lists now living in two files).

Two numbers were checked rather than recalled, and both were wrong in the
draft: 120 mutations across six specs, not 127; and TWO copies of the
Haversine, not three -- daemon_location.sh's calc_distance IS focus_daemon's
original, moved, which I had double-counted in an earlier commit message.

Running two mutation specs concurrently leaves a mutation applied and reads
as a survivor; that cost a false 16/17 here and is now written down.

295 -> 250 lines. The old brief counted itself among the over-cap files and
was 45 lines past the gate that never checks .md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
2026-08-17 18:15:18 +02:00

251 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Next session: §3 restructures, then §4's deployed-copy trap
> **Paste this whole file into a fresh Claude session opened in `~/testsAndMisc`.**
> It is self-contained. Do not go looking for the previous session's context.
Over-cap: **15** (12 shell, 2 kotlin, 1 dart). `phone_focus_mode/` is **done** —
no file there is over the cap. Working tree clean, `main` in sync.
Two standing decisions from the user, already made — do **not** re-litigate:
1. **100% line coverage on what you extract** — with the one measured
exception in §0.1, which is a prohibition, not a target to chase.
2. **Split blind where there is no device.** Do not deploy to the phone, do
not ask to. Prove splits with tests, hashes and real runs instead.
---
## 0. Read this first — three rules that cost real time to learn
### 0.1 NEVER pipe a `while read` loop whose body calls `pm`/`adb`/`iptables`
These lines are **deliberately uncovered** and must stay that way:
| file | lines | why |
| --------------------------------- | --------------------- | ---------------------- |
| `phone_focus_mode/daemon_apps.sh` | 35, 60, 110, 130, 158 | trailing `done < file` |
Each is the redirect of a loop whose body calls `pm`. Piping the file in
instead lets `pm` inherit and drain the loop's stdin, so it processes **one**
package instead of all of them — measured in this repo as 1 of 54. kcov
cannot attribute a trailing `done < file`, so those five lines read as
uncovered forever; that is the price and it is paid knowingly. They are
pinned by three mutations in `meta/scripts/fixtures/mutations/daemon_libs.json`
that redirect them from `/dev/null` and fail the suite.
If you ever must remove such a redirect line, use `mapfile -t arr < file`
then `for x in "${arr[@]}"` — **never a pipe** — and only after confirming
nothing in the body reads stdin. Piping _is_ correct where the body is pure
(`curfew_net.sh` did it safely); the body is what decides.
### 0.2 kcov artifact taxonomy
Four constructs are instrumented but never reported, so they hold a file
below 100% no matter how well tested:
| construct | safe to restructure? |
| ------------------------------- | ------------------------------------------- |
| multi-line array/list literal | yes — put it on one line |
| `{ ... } >file` group | yes — redirect per statement, or one printf |
| multi-line `mapfile < <(...)` | yes — put the substitution on one line |
| embedded multi-line `awk '...'` | **no** — collapsing it destroys readability |
| trailing `done < file` | **only if the body is stdin-pure** (§0.1) |
Before restructuring, prove the lines actually execute with a mutation. Two
files are knowingly below 100% for this reason and both are documented:
`daemon_location.sh` 65.00% (the awk Haversine) and `daemon_apps.sh` 93.42%.
### 0.3 An equivalent mutation means dead code, not a missing test
`meta/scripts/mutate_shell.py <spec>` runs one of the six specs in
`meta/scripts/fixtures/mutations/` — 120 mutations, all killed.
A **survivor** is either a missing assertion or _equivalent code_. Twice this
session it was the latter, and both times the right fix was deleting the dead
branch: an unreachable `-f` guard in `hosts_mount.sh`, and a redundant
comment filter in `ctl_usage.sh`. Never resolve a survivor by weakening the
mutation or adding a test that cannot fail.
A **no-op** (the `find` string matched nothing) counts as a failure — a stale
spec entry silently stops testing. Run specs **one at a time**: two runs at
once edit the same subjects and leave a mutation applied, which reads as a
survivor and dirties the tree.
## 1. §3 — the three restructures (start here)
Verbatim moves cannot get these under the cap, so `verify_shell_split.sh`
does not apply. Each needs a real test at 100%.
- **`install_plagiarism_tools.sh` (534, only 3 tiny functions)** — ~500 lines
of top-level code. Wrap coherent stages in functions first, then move them.
Most mechanical of the three; do it first. `verify_shell_split.sh` only
hashes _function bodies_, so it is blind to top-level code — read
`git diff` on the entry script before committing.
- **`libre_translate.sh` (488, 18 funcs)** — the user chose the approach:
move the whole config-globals cluster into **one lib that owns both the
writes and the reads**. Do not try relocating `parse_args` alone; four
attempts failed.
- **`diagnose_pacman_hook_stall.sh` (493, 15 funcs)** — `run_one` writes
`LAST_ELAPSED`, `main` reads it. Emits **SC2153** (`PACMAN_BIN` vs
`PACMAN_PID`) once split: a real finding to resolve, never to suppress.
## 2. §4 — last, and only after §3
**`install_leechblock.sh` (485)** and **`block_compulsive_opening.sh` (705)**
are copied to `/usr/local/…`, and `pacman_wrapper.sh:831` prefers the
deployed copy on **every pacman invocation**. Split them naively and every
`pacman -S` on this machine breaks. Teach the installer to deploy the
directory first, in its own commit, re-baseline, then split.
You cannot run `sudo pacman -S` from the Bash tool — it deadlocks on
`db.lck`. Hand the user a `! sudo pacman -S <pkg>` line plus the expected
output.
## 3. Out of scope until the user says otherwise
`setup_midnight_shutdown.sh` (1734), `check_and_enable_services.sh` (1301),
`generate_study_materials.sh` (1017), `pacman_wrapper.sh` (929),
`setup_night_lockdown.sh` (918), `hosts/install.sh` (912),
`setup_hosts_guard.sh` (576), `EnforcementRunner.kt` (564),
`DevicePolicyBridge.kt` (415), `status_page_state.dart` (307).
`pacman_wrapper.sh` carries the same live-deployment trap as §2. The Kotlin
and Dart files need a different verification stack — `focus_owner` gradle
needs `JAVA_HOME=/usr/lib/jvm/java-21-openjdk` and `--rerun-tasks`; a plain
`gradlew test` reports `UP-TO-DATE` and proves nothing.
## Tooling
**`meta/scripts/check_file_length.sh --all`** — the 250-line gate. Run
**before every commit**: a split that fixes one file while pushing its new
library over the cap is a net zero, and that happened three times.
**`meta/scripts/shell_coverage.sh <test> <subject> [min]`** — kcov wrapper,
100% minimum. Hand it the test script **directly**; `bash script.sh`
instruments the bash binary and silently reports 0/0.
**`meta/scripts/mutate_shell.py <spec>`** — see §0.3.
**`meta/scripts/extract_shell_functions.py`** — moves functions brace-by-
brace. **Never slice by line range**: it also cut through a multi-line
quoted string in `config.sh` and left an unterminated quote, which the policy
loader's regex tolerated while the shell could no longer source the file.
**Do not trust where it puts the source line.** It got the position wrong
three times: before `SCRIPT_DIR` existed, before `STATE_DIR` (so every path
expanded against an empty prefix), and on a _nested_ re-source rather than
the top-level one. Always `grep -c` the anchor and check the placement.
**`meta/scripts/verify_shell_split.sh <pre-split-rev> <old> <new>...`** — for
a **partial** split list the old path among the new paths too, or it reports
a false `DIFFERENCE`.
## Rules that will bite you
- **No suppressions, ever.** Every time this came up, the seam or the code
was wrong. A comment line starting with the word "shellcheck" is parsed as
a directive (SC1073) — reword it.
- **A file must not assign a global it never reads** (SC2034). The hook runs
`shellcheck` with **no `-x`**, so each file stands alone. Constants travel
with their readers; a global genuinely written on both sides of a seam
stays in the entry script (`NET_BUILT`, `CURRENT_MODE`, `NEEDS_GPS_FETCH`).
- **New sourced libs need a shebang AND the executable bit.** The hook reads
the **git index**: stage with `git add --chmod=+x`, and note that a plain
`git add` afterwards resets it. This cost three failed commits.
- `pre-commit run --files <changed>` before committing. **`prettier` and
`ci-mirror` run on pre-push.** `npx prettier --write` any `.md` you touch.
- **Every commit touching code needs evidence** in
`docs/superpowers/evidence/<slug>-<date>.json`. Staging **≥4 code files
also needs** a fresh `docs/superpowers/contracts/*.json`. Put the
**measured** coverage number in it, not a rounded-up one.
- Work directly on `main`. `git stash` and branch creation are blocked; use
`git worktree add --detach` for a clean baseline.
- **Do not wire the file-length hook into pre-commit.** It lands last, once
`check_file_length.sh --all` exits 0. 15 files are still over.
- Cap pytest memory:
`systemd-run --user --scope -p MemoryMax=2G -p MemorySwapMax=0`.
- Watch `jscpd` (fails above 2%). Measure in a **clean HEAD worktree** —
the working tree reads ~2.5% because of vendored `.venv`, but HEAD is
1.57%. Per-file test harnesses are near-identical by nature; drive repeated
shapes through a table rather than repeating the block.
## `phone_focus_mode` — what changed, and its two live hazards
Nine commits took it from 8 over-cap files to zero. `config.sh` 571→250,
`focus_ctl.sh` 1091→171+8, `deploy.sh` 875→241+6, plus monitor,
`phone_backup.sh`, `curfew_enforcer.sh`, `hosts_enforcer.sh` and
`focus_daemon.sh`. 515 assertions across ten suites, from ~60 before.
**Hazard 1 — `config.sh` is at EXACTLY 250 with no headroom.** 184 of those
lines are the fifteen variables `python_pkg/focus_policy/loader.py` reads by
regex **over `config.sh`'s own text**. Move one into a `config_*.sh` sibling
and the loader does not error — it silently yields an empty set, which for
`WHITELIST` reads as "hide everything". The list is in `config_paths.sh`'s
header and enforced only by that comment.
Verify **both** of these before and after touching it:
```bash
python3 -m python_pkg.focus_policy --config phone_focus_mode/config.sh | sha256sum
# 83e05e82dd1683e1dff1c79a96fec0a4a56aa73295604c681ae355b40a590ba3
```
plus a diff of the **sourced environment** (79 variables) against the
pre-change commit. The hash alone is not enough: it passed clean through
both bugs hit this session — a mangled quote and a `STATE_DIR` ordering
error. The previous brief's recipe (`asdict` + `json.dumps(default=str)`,
baseline `1624750c…`) **cannot work**: four policy fields are `frozenset`, so
`default=str` serialises them in per-process iteration order.
**Hazard 2 — `deploy.sh`'s two hardcoded file lists are now in two files.**
The push list is in `deploy_phases.sh`, the cp list in `deploy_install.sh`. A
new phone-side sibling must go in **both**, or it is staged and never lands,
and whatever sources it fails to start with no obvious cause. Check with
`comm` in both directions; `config_secrets.sh` is deliberately asymmetric.
## Known pre-existing state — not yours, do not fix silently
- `bash phone_focus_mode/lib/tests/test_magisk_service.sh` **hangs**.
Confirmed identical at HEAD before this session's work; unrelated to it.
- `bash linux_configuration/tests/test_security_hardening.sh` exits 1 with
`❌ FAIL: Compulsive block wrappers installed`. Belongs to §2.
- `bucket_catch/packages/frontend` has 4 eslint errors, documented in
`docs/superpowers/evidence/bucket-catch-eslint-2026-08-17.json`.
- **Eight splits are UNVERIFIED ON DEVICE**: `tether_enforcer`, `dns_enforcer`
(both pre-existing), plus `curfew_enforcer`, `hosts_enforcer`,
`focus_daemon`, `config.sh`, `focus_ctl` and `deploy.sh`. One real deploy
validates all of them at once — but `deploy.sh` is itself the untested one,
so watch it closely and keep an adb push of the directory as the fallback.
- **Two copies of the Haversine** exist: `daemon_location.sh:calc_distance`
(the original, moved) and `ctl_daemon.sh:cmd_status`'s inline awk. Both are
tested against known city distances, but not against each other.
`ctl_is_curfew_now` likewise duplicates `daemon_location.sh`'s
`is_curfew_now`; those two **are** pinned by an agreement test.
- The eight `ctl_*.sh` libraries are at **41.79–92.98%**, and unlike the two
files in §0.2 most of that is genuinely untested status-reporting code, not
a measurement artifact. Closing it means a device fixture per enforcer.
## Testing notes specific to this repo
- `phone_focus_mode`'s shell tests are **not** in CI — `shell-tests.yml` uses
an explicit list covering `linux_configuration/tests/` only. Run by hand:
```bash
for t in test_dns_iptables test_monitor test_tether_enforcer test_curfew_net \
test_hosts_libs test_daemon_libs test_backup_capture test_ctl_libs \
test_adb_common test_adb_trusted; do
printf '%-22s %s\n' "$t" "$(bash phone_focus_mode/lib/tests/$t.sh | tail -1)"
done
# expect: 24 / 82 / 17 / 41 / 69 / 108 / 22 / 136 / 10 / 6
```
- A test that passes proves nothing on its own. `test_dns_enforcer.sh` was
deleted last session because its six assertions targeted three functions
that never existed. Check the **count**, the coverage, and the mutations.
- Test files are subject to the 250-line cap too. Split them into a harness
plus sourced case files, keeping **one** entry point so a single coverage
command still measures every subject.
- `set -e` kills a suite silently. Guard any subject call whose nonzero exit
is not part of its contract, and never seed `$$` as a pid a subcommand will
`kill` — the suite signals itself and dies mid-run with no error.