Add pytest suite (stacked on #150) - #151
Draft
shauneccles wants to merge 16 commits into
Draft
Conversation
Replace the hand-rolled GLinetRouter data layer (manual async_track_time_interval timer, ad-hoc dispatcher signals, per-entity async_update, should_poll=True) with a single DataUpdateCoordinator. - coordinator.py: GLinetUpdateCoordinator owns the gli4py client and device identity and produces a GLinetData snapshot each SCAN_INTERVAL. _async_update_data raises UpdateFailed when the router is unreachable and ConfigEntryAuthFailed on token failure, so entities go unavailable / reauth triggers idiomatically. Ports async_init/renew_token/_update_platform and the update_* fetch bodies. - models.py: shared data classes (DeviceInterfaceType, WifiInterface, WireGuardClient, ClientDevInfo) moved out of router.py. - const.py: SCAN_INTERVAL moved here. - All platforms become CoordinatorEntity reading coordinator.data; drop should_poll and async_update. Switch toggles call the API then async_request_refresh(). Device trackers discover new clients via a coordinator listener (replaces signal_device_new/update dispatchers). - sensor.py also folds in the uptime stabilisation (PR HarvsG#148) and the list-comprehension setup filter (PR HarvsG#149), now driven by the coordinator. - __init__.py uses async_config_entry_first_refresh; HA owns the poll timer so the previous reload timer leak is gone. router.py is removed. Verified with py_compile + ruff; not yet runtime-tested against live HA. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Review follow-ups on the coordinator conversion: - update_tailscale_state guards the unwrapped tailscale_configured() call so a transient API error can't abort the whole refresh or escape uncaught. - GLinetDevice overrides available -> True so trackers keep reporting home/away across a failed poll instead of going unavailable (CoordinatorEntity would otherwise tie availability to last_update_success, a presence regression). - Drop the misleading "immutable" claim from the GLinetData docstring. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Owner
|
Very excited for this PR. I know it's still in draft but tests are failing - probably because it's waiting on #150 |
The coordinator declared a read-only `name` property, but DataUpdateCoordinator.__init__ assigns `self.name = name`, which raised `AttributeError: property 'name' has no setter` and aborted entry setup entirely. Rename the device-display property to `device_name` (its only consumer is `device_info`), freeing `self.name` for the base class. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Fail the whole refresh (UpdateFailed) when any call hits a transport error this cycle, instead of publishing a snapshot with stale data - Rebuild the wifi/wireguard collections fresh each poll so a removed interface/client no longer lingers in the snapshot - Drop the dead update_options() path (the update listener was never registered) - Add a GlinetConfigEntry type alias for typed entry.runtime_data - Remove the section-banner comment clutter (review feedback) and add a TODO to lean on ConfigEntryAuthFailed/UpdateFailed in future Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…port - RebootButton is now a CoordinatorEntity so it tracks availability with the coordinator - Import ScannerEntity from homeassistant.components.device_tracker (the .config_entry path is deprecated) - Type entry.runtime_data via the GlinetConfigEntry alias across __init__ and all platforms Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
shauneccles
force-pushed
the
test/coordinator-suite
branch
from
June 26, 2026 04:40
38b1cb4 to
63df810
Compare
The coordinator raises ConfigEntryAuthFailed when the stored token is rejected, which drives Home Assistant's reauth flow - but there was no async_step_reauth, so every auth failure dead-ended with an UnknownStep error in the event loop. Add a minimal, standard async_step_reauth/async_step_reauth_confirm that re-prompts for the password and refreshes the stored token in place, with the matching strings/translations. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
mypy strict reported `no-any-return` in three entity properties because `self.coordinator` was inferred as Any (the homeassistant stubs don't propagate the CoordinatorEntity generic to the attribute) and `dt_util.utcnow()` is untyped. - Bind the concrete coordinator type on the switch base so `self.coordinator.data` is GLinetData, not Any. - Pin the uptime sensor's `now` to datetime before subtracting. - Correct GLinetData.wireguard_clients to dict[int, ...] (it is keyed by the integer peer_id at runtime) - the previous dict[str, ...] only type-checked because the coordinator access was Any. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
shauneccles
force-pushed
the
test/coordinator-suite
branch
from
June 26, 2026 04:52
63df810 to
b0f87b8
Compare
The old `list(DeviceInterfaceType)[dev_info["type"]]` lookup crashed with an
IndexError on Wi-Fi 7 / MLO clients (newer routers report higher type codes),
and the duplicate UNKNOWN2 enum alias silently shifted every index past 8.
Resolve the interface from the router's self-describing `iface` string first
("2.4G", "5G", "6G", "MLO", "cable", guest variants), falling back to a
bounds-checked integer `type` map. Resolving from the string survives firmware
that renumbers the integer codes; the fallback never raises and unknown
interfaces become UNKNOWN (so the device is still tracked) and are logged once
with model/firmware context for adding support over time.
Supersedes HarvsG#145, whose fix targets router.py (removed by this refactor); folds
in the iface-string angle raised in that PR's review.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Load averages of exactly 0 made the sensor go unavailable: the old `... and la[i]` short-circuits on the falsy 0. Use an explicit conditional so a genuine 0.0 is reported (HarvsG#152). - Treat buffer/cache memory as available when computing memory usage, matching how Linux reports free memory, and expose memory_buff_cache / memory_available / memory_used attributes (HarvsG#111). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Scaffold the integration's first automated tests using pytest-homeassistant-custom-component, with the GLinet API client mocked by fixtures captured (and sanitised) from a live MT6000. - tests/fixtures/*.json: real router responses with MACs/IPs/SSIDs/names and keys scrubbed (see scripts/capture_fixtures.py, a local git-ignored helper). WireGuard is absent on the reference router, so those two fixtures are hand-authored to the known shape. - conftest.py: enable_custom_integrations, a MockConfigEntry, a mock GLinet client wired to the fixtures, and an init_integration fixture. - test_init: setup/unload, ConfigEntryNotReady -> retry when unreachable, reauth on token auth failure. - test_coordinator: GLinetData snapshot is populated; identity from router_info; UpdateFailed marks the refresh unsuccessful. - test_sensor: system sensor values; uptime stays flat across polls within tolerance (regression for the original flapping) and re-anchors on reboot; unavailable-value filter. - test_switch / test_device_tracker / test_button / test_config_flow: state from coordinator.data, API calls + refresh on toggle, dynamic tracker discovery, reboot, and the user config flow (success/cannot_connect/ invalid_auth). - pyproject: add a `test` dependency group (pthcc pinned to HA 2026.2.0) and pytest config (asyncio auto, pythonpath). - .github/workflows/pytest.yml: run the suite on Linux via pip. The HA test stack can't build on the Windows dev box, so the suite is verified in CI, not locally; fixtures and lint were validated here. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Run the suite via `uv run --no-project --with ...` so CI is pure-uv (matching the other workflows), needs no project build, and leaves uv.lock untouched. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Reframe the auth-failure test to assert setup aborts (SETUP_ERROR); the reauth-flow assertion is removed because the config flow has no async_step_reauth yet (noted as a follow-up). - Move CONF_CONSIDER_HOME to the entry options (where the coordinator reads it) instead of data. - Add test_unnamed_client_excluded so the device-tracker name filter is actually exercised. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rebasing onto the current HarvsG#150 (which now adds the config-flow reauth flow and fixes its strict-typing debt) lets this branch carry only the test + tooling changes: - test_init: the auth-failure test asserts that ConfigEntryAuthFailed starts the reauth flow (now implemented in HarvsG#150) rather than dead-ending. - test_device_tracker: ScannerEntity trackers are disabled by default, so assert discovery against the entity registry and presence via the coordinator snapshot rather than live states. - Tooling so `uv run` and the pre-commit hooks resolve and pass: align homeassistant-stubs (dev) to the test group's HA pin, mypy explicit_package_bases + scope to source, pylint disables reconciled with ruff's isort, and drop the stale /Scripts/ .gitignore rule so scripts/ is tracked. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Restructure the fixtures from a flat MT6000-only set into per-profile
directories, each with a profile.json manifest (model, firmware, capability
flags, expected counts). conftest discovers every profile and parametrizes the
suite over it, so each test runs against every model/firmware and a new
captured profile drops in with no code changes.
- tests/fixtures/<profile>/ - mt6000 (the real sanitised capture) plus derived
edge profiles: no-WireGuard, no-Tailscale, a different model (MT3000), and a
WiFi7/MLO client that reproduces a real interface-index crash.
- build_mock_api coerces omitted endpoints to the real client's return type
([]/{}/False), so feature-absent profiles exercise the real absent-feature
paths instead of truthy mocks.
- test_snapshots.py snapshots every platform's entity registry + states per
profile via snapshot_platform (HA convention), with the clock frozen so the
uptime sensor's derived boot time is deterministic. The behavioural tests keep
only what snapshots can't express (uptime stability, sensor filtering, tracker
discovery, toggle-calls-API, lifecycle) and read expectations from the
manifest; feature-specific switch tests are gated on profile capabilities.
- A dedicated xfail(strict) regression documents the WiFi7/MLO crash.
Scripts (now committed):
- scripts/capture_fixtures.py - capture + deterministically sanitise a live
router into a profile (MACs/IPs/SSIDs/hostnames/secrets), covered by
tests/test_capture_fixtures.py so it can't leak PII into a committed fixture.
- scripts/synthesize_profiles.py - regenerate the derived edge profiles.
Keep prettier off tests/fixtures and tests/snapshots: those are generated by the
capture/synthesize scripts and syrupy, and own their formatting.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- pytest workflow: report coverage (pytest-cov) and document why it stays a single job (profiles are parametrized in-process) and omits --snapshot-update (so committed snapshot drift fails CI). - scripts/test and scripts/capture-fixtures: thin wrappers over the pinned ephemeral uv environment, matching CI. - CONTRIBUTING.md: the profile model, running tests, capturing a new router, snapshots, and the HA-version-bump procedure. README gains a short Testing pointer; tick the now-done "add tests" and reauth TODOs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Covers the bugfixes ported into HarvsG#150: - tests/test_interface_type.py exercises the resolver directly: iface-string resolution (incl. guest/MLO), integer-code fallback, and that out-of-range / non-numeric / missing codes resolve to UNKNOWN without raising. - The fixtures' sanitised `iface` placeholders are restored to realistic, type- consistent values ("2.4G"/"5G"/"cable") so the iface path is actually exercised; capture_fixtures.py already preserves iface for real captures. - wifi7_mlo_client is now a normal matrix profile (an MLO client with iface "MLO" and an out-of-range integer type). The dedicated regression test drops its xfail and asserts setup loads and the client is labelled MLO. - test_load_average_zero_is_reported guards the falsy-0 fix; the memory change is captured by the regenerated per-profile snapshots. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
shauneccles
force-pushed
the
test/coordinator-suite
branch
from
June 26, 2026 05:31
b0f87b8 to
227592e
Compare
29 tasks
shauneccles
added a commit
to shauneccles/ha-glinet4-integration
that referenced
this pull request
Jun 26, 2026
`dev` is the rolling fork line (folds in HarvsG#150 + HarvsG#151) for rapid development against a live MT6000; HarvsG#150/HarvsG#151 stay clean for upstream. Point dev/test/CI and the script wrappers at our gli4py fork's `dev` branch so the integration and the library iterate together (issue #12). Not for HACS distribution, so a git ref is fine - no PyPI fork package needed. The fork is still version 0.0.17b3 (matches manifest.json); bump the manifest pin in lockstep if the fork's version changes. scripts/sync-upstream keeps `master` a clean mirror of upstream for tidy PRs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Open
8 tasks
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.
Summary
Adds the integration's automated test suite for the DataUpdateCoordinator
conversion in #150, rebased onto the current #150 and rebuilt around a
dynamic, profile-driven model so any router model/firmware can be tested.
Draft for comment, stacked on #150. Once #150 merges to
master, GitHubrecomputes the merge-base and this diff collapses to the tests + tooling.
What's here
tests/fixtures/<profile>/each hold aprofile.jsonmanifest (model, firmware, capability flags, expected counts) plus the captured
endpoint responses.
conftestdiscovers every profile and parametrizes thewhole suite over it — each test runs once per profile, and a new captured
profile drops in with no code changes. Ships
mt6000(real, sanitisedMT6000 capture) plus derived edge profiles: no-WireGuard, no-Tailscale, a
different model (MT3000), and a WiFi7/MLO client.
tests/test_snapshots.pysnapshots every platform'sentity registry + states per profile via HA's
snapshot_platform, clock frozenfor a deterministic uptime timestamp. Behavioural tests keep only what
snapshots can't express (uptime stability, sensor filtering, tracker discovery,
toggle-calls-API, lifecycle, reauth) and read expectations from the manifest.
scripts/capture_fixtures.pycaptures + sanitisesa live router (MACs/IPs/SSIDs/hostnames/secrets) into a new profile, guarded by
tests/test_capture_fixtures.pyso it can't leak PII.scripts/synthesize_profiles.pyregenerates the derived profiles.
scripts/test/scripts/capture-fixtureswrappers; coverage in CI;
CONTRIBUTING.mddocuments the workflow.uv run/pre-commit resolve and pass: devhomeassistant-stubsaligned to the test group's HA pin, mypy
explicit_package_bases+ sourcescope, pylint reconciled with ruff's isort,
/Scripts/.gitignorerule dropped.Relationship to #150
The config-flow reauth flow and the strict-typing fixes it surfaced now live
in #150 (where the
ConfigEntryAuthFailedpath and the entity typing originate);this branch is rebased on top, so its tests assert that behaviour against the base.
Bugfix coverage
Beyond the coordinator conversion, the suite now covers the bugfixes ported into
#150:
tests/test_interface_type.pyfor the iface/integer interface resolver,a
wifi7_mlo_clientmatrix profile (an MLO client that used to crash setup),test_load_average_zero_is_reportedfor the falsy-0 fix, and the memorybuffers/cache change via the per-profile snapshots. Fixture
ifaceplaceholderswere restored to realistic values so the resolver's string path is exercised.
Note
The
wifi7_mlo_clientprofile previously documented a real bug(an unbounded
DeviceInterfaceTypeindex crashing on WiFi7/MLO clients); thatfix now lives in #150 and the profile asserts it resolves cleanly.
🤖 Generated with Claude Code