Port the post-v0.228.0 development train - #70
Merged
Merged
Conversation
Ports litclock-dev afa2d6b4..fde50fc6 — the seven PRs merged 2026-09-19/20 (litclock-dev#872-#878). The boundary was established by CONTENT, not by the previous port commit's stated range: public carries litclock-dev#865's blocked-sha and none of litclock-dev#868/#870/#871, and the file sizes agree (update.sh 2542 vs 2739, state.sh 317 vs 416, quote_corpus.py 249 vs 368). Gated on the bench QA pass of 2026-09-20 (`dev-20260920-512c291`, a fresh flash on a Pi Zero 2 W): 13 checks, no blocking findings, every member of this train exercised on hardware. What owners get (one line in the CHANGELOG, because that is all that is visible from outside): - gift mode and the app's Factory reset now wipe AND LOCK the default shell history files, as clone prep already did (litclock-dev#868). The lock is a root-owned empty directory at the path: `history -c` reaches only the script's own shell, and the console or SSH shell the operator ran it from writes its history back on exit, during the power-off. first-boot.sh removes both directories on the not-yet-set-up path, so the recipient's first login gets an ordinary history file. Shipping silently, by design: - the quote corpus is resolved per active language from the registry (litclock-dev#870). Latent while English is the sole active language: the pre-rendered PNGs stay English, so a second language needs on-device text rendering before it can be activated. - the updater's runtime-render self-test, shipped INERT (litclock-dev#871 Stage A). On a marker-bearing device an applied release now asks whether the device can actually paint a quote from text and records the verdict in /var/lib/litclock/runtime-render-selftest.json; it changes no setting, and a failure is not an update failure. First field measurement: 3.8s on a Pi Zero 2 W during an update, 3.4-3.7s idle — whole-process wall time, not render time. Also: the LKG recorder no longer claims bootcheck/revert is unshipped (litclock-dev#877), the WiFi retry tests stub the IP-geo resolver that was making a live network call (litclock-dev#876), and CLAUDE.md gains the 2026-09-20 bench results plus the rewritten litclock-dev#867 raw-journal check (the old one passed vacuously). Port mechanics: - 48 bare `#NNN` refs and 14 `dev#NNN` shorthands requalified to `litclock-dev#NNN` on ported lines only, validated by this repo's own tests/test_issue_ref_namespace.py rather than by a hand regex. - Hand-merged where public has diverged: README (its own structure and headings), docs/script-reference.md (public's `#resetting` anchor), scripts/lib/state.sh (public's already-qualified env.sh writer-lock header), and the two comment conflicts in update.sh and status.py. - CONTRIBUTING.md edited in place to preserve its CRLF line endings. - Scrubbed on the way over: the `~/archives/litclock-qa` harness path, which does not exist for a reader of this repo. ruff clean, shellcheck clean, pytest 4638 passed / 66 skipped, vitest 209 passed.
Ports litclock-dev 7c2063ea..9c8ddc81. Both defects were carried over by the
train in the parent commit, not introduced by it; both were found by the
gstack /review of this branch.
- tests/test_wifi_retry_flow.py: the IP-geo stub litclock-dev#876 added covered
ONE class of three. Measured with a getaddrinfo probe on this checkout: 24
DNS lookups of ip-api.com across 8 tests. The resolver's success path calls
set_system_timezone(), which shells out to sudo, and ENV_FILE is
monkeypatched by these tests while that call is not sandboxed by anything —
so on a machine where the installer has run, `pytest tests/` could change the
system timezone. The fixture is module-scoped and autouse now, so a class
inherits it rather than appearing on a list of the classes somebody
remembered. Probe on this branch after the port: 0 attempts, 70 passed.
- scripts/update.sh: ${EPOCHREALTIME/[.,]/}, not a literal dot. The separator
follows LC_NUMERIC, so under a comma locale the strip matched nothing and the
comma parsed as bash arithmetic's comma operator. Three measured outcomes,
all wrong; the dangerous one is silent (rc 0, nothing on stderr) and IS
recorded, beside "result":"passed", in the file litclock-dev#871 Stage B is
meant to gate on. Production always got C from systemd; the exposure is a
maintainer running the script by hand from a non-English desktop, which
README's manual-update section tells owners they may do.
The seven tests lift the shipped t0=/dur_ms=/duration= assignments out of
_runtime_render_selftest and execute them verbatim with EPOCHREALTIME injected,
rather than a copy — a copy proves nothing about the script, and neither did
counting occurrences near it (an indirect-expansion mutant satisfied the count
and reintroduced the bug). Five mutants verified red against THIS checkout's
update.sh, including the full revert.
No CHANGELOG entry: nothing here is visible to an owner.
ruff clean, shellcheck clean, pytest 4645 passed / 66 skipped, vitest 209.
…dings of this repo's own
Ports litclock-dev 9c8ddc81..7438f611, plus two fixes that exist only here.
From dev:
- `${EPOCHREALTIME//[^0-9]/}` — strip every non-digit, not just `[.,]`. glibc
gives fa_IR and ps_AF U+066B (٫), and measured, `[.,]` leaves it in place for
rc 0, duration=0.0, `invalid arithmetic operator` on stderr, and a wrong
value jq accepts and records. Same hazard class, rarer locale family.
- The test that holds it stopped asserting source text. Four review rounds each
broke the previous round's rule with a shape it did not model (a decoy in a
comment, an indirect expansion, a `local`-prefixed assignment, and finally
`printf -v duration '0.0'`, which is not an assignment at all). So a second
class EXECUTES the shipped `_runtime_render_selftest` with its helpers
stubbed and a painter that sleeps a known time, and asserts on the value
`_runtime_selftest_record_write` is handed, with a second painter as the
control. Verified against THIS checkout: printf -v and eval red, the full
revert red, and narrowing back to `[.,]` red.
- The hazard test now uses a fraction whose wrong output `jq tonumber`
ACCEPTS, so it exercises the silent-and-recorded shape the fix exists for;
the earlier default produced `0.-3`, which jq refuses, i.e. the loud band.
Found in this repo, by the review of this branch at its final HEAD:
- `tests/test_wifi_retry_flow.py:161` carried `litclock-dev litclock-dev#876` —
the requalifier prefixed a `#876` on a line that already said `litclock-dev`
with a space. The only occurrence in the tree; this is the dev#767 doubled-
qualifier class, so it is worth catching rather than leaving.
- `PUBLIC_NUMBER_CEILING` was still 67, two ports behind: #68 and #69 landed
the v0.227.0 and v0.228.0 trains and this lands as #70. Audited 68..70 across
all tracked files — ZERO bare `#N` in that range, so nothing in the tree
changes meaning — and raised to 70 with that audit recorded, which is the
deliberate act the module docstring asks for.
Also removed a stray empty `0755/` directory in the working tree, a `mkdir -m`
typo from 2026-09-13.
ruff clean, shellcheck clean, pytest 4648 passed / 66 skipped, vitest 209.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Ports the litclock-dev train merged 2026-09-19/20 (litclock-dev#872-#878), boundary
afa2d6b4..fde50fc6.Gate
Bench QA pass of 2026-09-20 on a fresh flash of
dev-20260920-512c291(Pi Zero 2 W): 13 checks, no blocking findings, every member of this train exercised on real hardware — the setup-key rotation arms, the shell-history wipe on the PWA factory reset, the registry corpus resolution, and the Stage A self-test.What owners get
One line in the CHANGELOG, because it is the only thing visible from outside:
history -creaches only the script's own shell; the console or SSH shell the operator ran it from writes its history back on exit, during the power-off. The lock is a root-owned empty directory at the path, which fails that write with EISDIR;first-boot.shremoves both on the not-yet-set-up path, so the recipient's first login gets an ordinary history file.Shipping silently, by design
languages.jsonregistry for the active language;LITCLOCK_CORPUS_CSVstill overrides, and the PNG lookup readsimage_corpus_path()on purpose — the corpus the images were baked from. Latent while English is the sole active language: the pre-rendered images stay English, so a second language needs on-device text rendering before it can be activated./var/lib/litclock/runtime-render-selftest.json. It changes no setting, it renders into a throwaway directory so the live frame is untouched, weather is forced off, and a failed self-test is not an update failure. First field measurement: 3.8s on a Pi Zero 2 W during an update, 3.4-3.7s idle — whole-process wall time (interpreter start + PIL import + render), not render time, and not comparable to frame settle.Plus: the LKG recorder stops claiming bootcheck/revert is unshipped (litclock-dev#877), the WiFi retry tests stub an IP-geo resolver that was making a live network call (litclock-dev#876), and CLAUDE.md gains the 2026-09-20 bench results and the rewritten litclock-dev#867 raw-journal check (the old one passed vacuously).
Port mechanics
#NNNand 14dev#NNNrequalified tolitclock-dev#NNN, on ported lines only, validated by this repo's owntests/test_issue_ref_namespace.py.docs/script-reference.md(the#resettinganchor),scripts/lib/state.sh(the already-qualified writer-lock header), and two comment conflicts.CONTRIBUTING.mdedited in place to preserve CRLF.Review findings, fixed on dev and ported in
/reviewon this branch found two defects the train carried over from dev. Both were fixed on dev first (litclock-dev#879, #880, #882) and ported here, which is why this branch has three commits.getaddrinfoprobe: 24 DNS lookups of ip-api.com across 8 tests. The resolver's success path callsset_system_timezone(), which shells out tosudo, and it is not sandboxed by anything these tests patch — so on a machine where the installer has run,pytest tests/could change the system timezone. The fixture is module-scoped now. Probe after the fix: 0.${EPOCHREALTIME/./}recorded a silently wrongduration_sunder a non-dotLC_NUMERIC. Now${EPOCHREALTIME//[^0-9]/}— every non-digit, because the separator is not limited to a dot and a comma (glibc givesfa_IRandps_AFU+066B). Production always gotCfrom systemd; the exposure is a maintainer runningupdate.shby hand from a non-English desktop, which the README tells owners they may do.Four review rounds each broke the previous round's test with a shape it did not model, so the test that holds this now executes the shipped function with stubs and a known-duration painter and asserts on the value the record writer receives, rather than asserting anything about source text.
Two findings belong to this repo alone and are fixed here: a doubled
litclock-dev litclock-dev#876the requalifier created, andPUBLIC_NUMBER_CEILINGraised 67 → 70 (two ports behind), with the 68–70 audit recorded.Hardware QA
The train was gated by the full bench pass of 2026-09-20 (fresh flash of
dev-20260920-512c291, 13 checks, no blocking findings). The only device-facing code added after that pass is the two timing expansions above, so they got their own targeted check on the bench today — the Pi's bash is 5.2.15 against 5.2.21 on the dev box, which is why it needed hardware:LITCLOCK_RUNTIME_RENDERstillfalse, record file untouched.No OTA run: the self-test is inert this release,
update.shhas noset -e, and a failed self-test is explicitly not an update failure, so a broken expansion can mis-record a number but cannot stop an update.Verification
ruff check .clean, shellcheck (exact CI invocation) clean, pytest 4648 passed / 66 skipped, vitest 209 passed.