Skip to content

Add full unit test suite, CI for GitHub Actions and GitLab, and two crash fixes (2.5, 2.7) - #19

Merged
SeanathanVT merged 3 commits into
developmentfrom
feature/unit-tests
Oct 6, 2026
Merged

SeanathanVT merged 3 commits into
developmentfrom
feature/unit-tests

Conversation

@SeanathanVT

@SeanathanVT SeanathanVT commented Oct 5, 2026 •

Copy link
Copy Markdown
Owner

Summary

Implements ROADMAP 2.5 (Unit Tests) and 2.7 (Continuous Integration). Writing the tests turned up two app.py bugs, which this PR also fixes, and a front-end bug that is logged as ROADMAP 3.18 for a follow-up.

  • 314 tests, 98.4% line+branch coverage (98.5% line-only). They run in about a second and pass on Python 3.10, 3.12 and 3.13.
  • CI on GitHub Actions and GitLab CI, running ruff and pytest with a coverage report.
  • No behavior change for normal use apart from the two fixes below.

Fixes

  • Pause and speed buttons returned a 500 when the BLE loop was gone. /pause, the steppers and the presets called asyncio.run_coroutine_threadsafe without the try/except that /start and /resume already had. With no loop running they returned a server error, and /pause had already changed session state by then. They now log the error and redirect.
  • A non-UTF-8 session_state.json crashed startup. _load_session_state() caught JSONDecodeError but not UnicodeDecodeError. It now catches ValueError, which covers both.

Tests

tests/conftest.py adds two shared fixtures:

  • app_state: resets every app.py global, points the session-state file, config.json and the database at tmp_path, and records BLE coroutines instead of scheduling them.
  • walking: a session in progress with a controllable clock, moved here from test_active_time.py.
File Covers
test_app_helpers.py Formatting, calories, settings casts/clamps, shortcut URLs, the stats/SSE payload, session-state save/load/clear (including corrupt files), config.json writes, DB helpers, and "log and continue" when storage fails
test_status_packet.py Distance/step accumulation across device counter resets, calories, speed-history filtering and cap, resume speed on auto-pause, liveness stamping
test_routes.py Every Flask route through the test client, including restore/discard, CSV export, settings save, /stats_stream and /shutdown
test_ble_async.py Wake/start/stop sequences against a fake controller, light wake, lock timeouts, stats monitor, idle watchdog, disconnect handling, auto-reconnect backoff, scan/connect, the BLE thread lifecycle, signal shutdown
test_run.py run.py's browser launch, HTTP shutdown call, and main()'s Waitress subprocess and Ctrl+C handling
test_port_check.py Adds the EACCES/other bind-error hints

The tests never touch the real config.json, walkingdad.db or session_state.json.

Verification:

  • Breaking six pieces of logic one at a time (the counter reset, the pause guard, the UTF-8 fix, the resume-speed choice, the max-speed clamp, the wake order) turned at least one test red each time.
  • The suite stays green in random order, with each file run on its own, and with -W error::RuntimeWarning.

CI

  • .github/workflows/ci.yml: ubuntu-latest, Python 3.10 (the supported floor) and 3.13. Runs on pull requests and on pushes to main/development.
  • .gitlab-ci.yml: python:3.12. Uses merge-request pipelines when an MR is open and branch pipelines otherwise, and shows the coverage percentage plus a Cobertura report in the MR.
  • Steps for both: pip install -r requirements.txt -r requirements-dev.txt, then ruff check ., then python -m pytest --cov=.. Coverage is reported but never fails the build.

Supporting changes

  • Startup handlers: the SIGINT/SIGTERM handlers and the atexit hook are now installed only when startup is enabled (WALKINGDAD_NO_STARTUP not set). Before, importing app.py in tests installed them, and Ctrl+C killed pytest two seconds later.
  • run.py: the launcher code moved into main() so it can be tested; python run.py works the same.
  • Speed buttons: decrease_speed/increase_speed now reuse _set_preset_speed, so the guard and try/except live in one place.
  • Lint: ruff==0.16.10 is pinned in requirements-dev.txt, because its default rule set changes between releases. ruff.toml ignores four rules that only flag deliberate project style: the root logger, catch-all excepts around BLE I/O, naive local datetimes, and one ValueError raised to share an except.
    • Lint fixes in app.py: sorted imports, unused global declarations removed, one nested if combined, IOError replaced by OSError, and dict literals. None change behavior.
  • Config files: pytest.ini (pythonpath = ., so bare pytest works), .coveragerc (leaves tests out of coverage), and .gitignore additions for coverage output.

Docs

  • ROADMAP:
    • 2.5 and 2.7 marked complete;
    • cross-references updated in 2.11, 2.12, 2.13 and 3.17;
    • new 3.18 Transition Hint Shows on Every Button Press. disableButtonsOnSubmit() shows each page's transition hint on every form submit, so for example clicking a speed preset flashes "Getting the belt moving…" and shifts the layout. It predates this branch; the fix is deferred to its own branch.
  • README: the "Running tests" line now covers coverage, ruff and CI.
  • CHANGELOG: [Unreleased] has Fixed and Internal entries. Released sections are unchanged.

Test plan

  • ruff check . is clean
  • python -m pytest passes on Python 3.10, 3.12 and 3.13
  • CI commands run in a fresh venv; the GitLab coverage regex matches the TOTAL line
  • actionlint passes on the GitHub workflow
  • GitHub Actions passes on this PR
  • Smoke test on real hardware (macOS): launch, Start, speed changes, Pause/Resume/End, Ctrl+C mid-session (belt stops, UI shows the shutdown notice), relaunch with no stray restore prompt

🤖 Generated with Claude Code

SeanathanVT and others added 3 commits October 5, 2026 15:12
Cover routes, BLE sequences (fake controller), status-packet math,
persistence, and run.py; 300 tests, ~98% line coverage, green on
Python 3.10/3.12/3.13. CI runs ruff and pytest with coverage on
GitHub Actions and GitLab CI.

fix: /pause and speed routes no longer return 500 when the BLE loop
is gone; a non-UTF-8 session_state.json no longer crashes startup.

Install signal and atexit handlers only when startup is enabled, move
run.py's launcher into main() for testability, and pin ruff with its
lint fixes applied.

Co-Authored-By: Claude <[email protected]>
Add tests for non-list and bad-end_time legacy history, a failed
.migrated rename, graceful shutdown with no session/controller/client,
End while paused, connect with no BLE client, discard with nothing
pending, /shutdown loop-stop edge cases, SSE double-remove on close,
and a failed broadcaster tick. Each test fails if its target branch is
broken. storage.py reaches 100%; line+branch coverage 97.3% -> 98.4%.

Update ROADMAP 2.5's "Not covered" list to match the measured gaps.

Co-Authored-By: Claude <[email protected]>
@SeanathanVT
SeanathanVT merged commit 9523f88 into development Oct 6, 2026
2 checks passed
@SeanathanVT
SeanathanVT deleted the feature/unit-tests branch October 6, 2026 19:25
@SeanathanVT SeanathanVT mentioned this pull request Oct 8, 2026
SeanathanVT added a commit that referenced this pull request Oct 8, 2026
* Add unreleased section to CHANGELOG.

* Add full unit test suite, CI for GitHub Actions and GitLab, and two crash fixes (2.5, 2.7) (#19)

* feat(tests): add full unit test suite and CI (ROADMAP 2.5, 2.7)

Cover routes, BLE sequences (fake controller), status-packet math,
persistence, and run.py; 300 tests, ~98% line coverage, green on
Python 3.10/3.12/3.13. CI runs ruff and pytest with coverage on
GitHub Actions and GitLab CI.

fix: /pause and speed routes no longer return 500 when the BLE loop
is gone; a non-UTF-8 session_state.json no longer crashes startup.

Install signal and atexit handlers only when startup is enabled, move
run.py's launcher into main() for testability, and pin ruff with its
lint fixes applied.

Co-Authored-By: Claude <[email protected]>

* docs(roadmap): add 3.18 transition hint flashing on every button press

Co-Authored-By: Claude <[email protected]>

* test: cover migration edge cases, shutdown paths, and SSE failures

Add tests for non-list and bad-end_time legacy history, a failed
.migrated rename, graceful shutdown with no session/controller/client,
End while paused, connect with no BLE client, discard with nothing
pending, /shutdown loop-stop edge cases, SSE double-remove on close,
and a failed broadcaster tick. Each test fails if its target branch is
broken. storage.py reaches 100%; line+branch coverage 97.3% -> 98.4%.

Update ROADMAP 2.5's "Not covered" list to match the measured gaps.

Co-Authored-By: Claude <[email protected]>

---------

Co-authored-by: Claude <[email protected]>

* fix(security): block cross-site state changes (ROADMAP 2.2) (#20)

* fix(security): block cross-site POST requests (ROADMAP 2.2)

Reject any non-GET request whose Origin isn't the app's own origin, or
whose Sec-Fetch-Site is cross-site or same-site, so a website open in a
browser on the network can't start the belt, change speed, clear
history, or shut the app down.

No token or sign-in: the app is LAN-only and LAN devices are trusted by
design. Same-origin forms and fetches, requests without either header
(curl, run.py's /shutdown), and all GETs are unaffected.

Co-Authored-By: Claude <[email protected]>

* fix(security): make /reconnect POST-only and harmonize docs (ROADMAP 2.2)

/reconnect changed state on GET, so the cross-site guard skipped it and
any web page could trigger a Bluetooth reconnect. Make it POST-only and
turn the connecting screen's Connect / Try Again links into form
buttons, so every state-changing route goes through the guard.

Also harmonize docs and comments with the code:
- shutdown comments: /stats_stream frames instead of /stats polling
- remove "previously / before this change" narration from comments
- README: startup connects in up to 3 attempts, database_path isn't on
  the Settings page, add apple_health_export_enabled row, note the LAN
  trust model on the host row
- ROADMAP 2.2: files and known limits; 3.4: point at
  _build_stats_payload()
- CHANGELOG: /reconnect POST-only, reconnect in the blocked actions

Co-Authored-By: Claude <[email protected]>

---------

Co-authored-by: Claude <[email protected]>

* docs(roadmap): add 3.19 phone layout

Co-Authored-By: Claude <[email protected]>

* Phone layout, Apple Health from iPhone (status, steps), and a data/ folder (#21)

* feat(ui): phone layout and screen wake lock (ROADMAP 3.19, 3.12)

Add viewport meta so phones get a real layout instead of a scaled-down
desktop page. Under 480px: full-height hero, bottom-docked presets and
Pause/End, 44px+ targets, header controls behind a menu button, history
as cards. Add a web app manifest for full-screen home-screen launch,
pressed states for buttons (Bootstrap's default went transparent), and
a native screen wake lock on the active screen.

Co-Authored-By: Claude <[email protected]>

* feat(health): on-device Log/Install buttons and auto-confirm on iOS

A phone can't scan its own screen, so on iPhone/iPad the per-session
and setup QR codes become buttons. The log link is now an x-callback-url
whose x-success opens /health_logged/<id>, marking the session logged
and clearing the prompt on every open page via SSE. Dismiss is stored
separately (health_logged: 0 pending, 1 logged, 2 dismissed). Loopback
hosts are swapped for the LAN IP so the phone can reach the callback.

Co-Authored-By: Claude <[email protected]>

* feat(health): per-session Apple Health status, retroactive log, delete

Recent Sessions shows a filled/outline heart per session when Apple
Health export is on. An Edit toggle reveals per-row Log (QR on desktop,
direct Shortcut link on iOS) and Delete (POST /delete_session/<id>,
completed sessions only). SSE health_status_changed now carries
{id, status} so other pages update the row, banner, and open QR modal.

Co-Authored-By: Claude <[email protected]>

* chore: move runtime files into data/, tool configs into pyproject.toml

Settings, the session database, crash-recovery state, and JSON-migration
backups now live in data/ (backups in data/backups/) instead of next to
the code. Files left in the app folder by older versions are moved on
first start, never overwriting an existing file. config.py reads the
legacy root config.json until it moves, and migrate_json() takes a
backup_dir.

pytest, ruff, and coverage settings merge into pyproject.toml, replacing
pytest.ini, ruff.toml, and .coveragerc; requirements-dev.txt adds
coverage[toml] so coverage reads it on Python 3.10.

Co-Authored-By: Claude <[email protected]>

* fix: review follow-ups for Apple Health status and data/ relocation

- Close the Log QR modal only on a "logged" status change; a lingering
  "dismissed" change closed a later Log for that same session.
- Dismiss sends the banner's session id instead of marking whichever
  session is newest at click time.
- /health_logged and dismiss only act on completed sessions; unknown,
  in-progress, or failed writes return 404 without broadcasting.
- Move the database with its -wal/-shm as one group so a stale file in
  data/ can't separate them.

Co-Authored-By: Claude <[email protected]>

* feat(ui): heart icon tooltips, right-aligned history numbers

Bootstrap tooltips on the Recent Sessions heart icons, shown on hover,
tap, and keyboard focus, and updated live when a session is logged
elsewhere. Duration through Avg Speed are now right-aligned like Avg
Speed already was, so spare width spreads evenly instead of collecting
before the last column.

Co-Authored-By: Claude <[email protected]>

* style(ui): monochrome heart for logged sessions

The logged heart uses the theme's text color instead of Apple Health
pink, which clashed with themed palettes, fell just under 3:1 contrast
on some light rows, and read as the app's danger red. Theme accents
were too faint on most dark-mode rows. Fill vs outline carries the
state.

Co-Authored-By: Claude <[email protected]>

* feat(health): send step count to the Apple Health Shortcut

The Shortcut logs it as a Steps sample alongside the workout.
Points the install link and the exported backup at the updated
Shortcut.

Co-Authored-By: Claude <[email protected]>

* fix(health): don't let Dismiss undo a logged session

A page that hasn't seen the log yet (or the Settings off/on
path) could downgrade a logged session to dismissed, bringing
back its Log button and inviting a duplicate Health workout.

Co-Authored-By: Claude <[email protected]>

---------

Co-authored-by: Claude <[email protected]>

* docs(roadmap): add 5.4 native HealthKit companion app

Records the Shortcuts limits found while adding steps (no indoor
flag, no End Date, no attaching samples) and that HealthKit works
on a free Apple ID with 7-day reinstalls.

Co-Authored-By: Claude <[email protected]>

* Fix transition hint flashing on every button press (ROADMAP 3.18); add AGENTS.md (#22)

* fix(ui): show transition hint only during belt sequences (ROADMAP 3.18)

Submitting any form called setTransitioning() with the page's hint id,
so presets and steppers flashed "Getting the belt moving…" and shifted
the layout, and Start/Pause/Resume/End flashed the wrong text.
disableButtonsOnSubmit() now only disables and dims the buttons, and
the hint follows the SSE belt_transitioning flag alone.

Also stop SSE ticks from re-enabling the buttons while a submit is in
flight: a page-level submitting flag keeps them disabled until the page
navigates away, and is cleared on a back/forward-cache restore.

Co-Authored-By: Claude <[email protected]>

* docs: mark ROADMAP 3.18 complete and add CHANGELOG entries

Co-Authored-By: Claude <[email protected]>

* docs: add AGENTS.md (CLAUDE.md symlink) and Shortcut rebuild note

AGENTS.md holds repo rules for coding agents: keep docs in sync with
code, git workflow (maintainer commits, PRs target development),
desktop-first design target, and supported platforms. CLAUDE.md links
to it so Claude Code reads the same file.

ROADMAP: note that Log Health Sample's Steps Value row only appears
after granting Shortcuts write access to Steps.

Co-Authored-By: Claude <[email protected]>

---------

Co-authored-by: Claude <[email protected]>

* docs(changelog): release 1.9.0

Map every completed ROADMAP item to its CHANGELOG version.

Co-Authored-By: Claude <[email protected]>

---------

Co-authored-by: Claude <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant