refactor(mock): consolidate eleven per-module MockBase classes into a shared base - #4372
Merged
Conversation
Eleven production modules each define a near-identical MockBase used only to drive standalone command-line runs. Design consolidates them onto a single apps/predbat/mock_base.py, with three small subclasses for the modules that genuinely differ (axle/octopus config_root, sigenergy readonly) and an alias for KrakenMockBase. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Planning surfaced three factual errors in the spec, now fixed: - gecloud needs a subclass, not a plain re-export: it supplies ha_interface (dereferenced at gecloud.py:1037) as well as its own config_root. - solax needs a subclass to keep local_tz = timezone.utc rather than the machine's local timezone. - The split is 5 re-exports / 5 subclasses / 1 alias, not 7/3/1. Also documents a fourth behaviour fix: fox, kraken and solis build a naive midnight_utc alongside an aware now_utc, so subtracting them raises. The shared base derives midnight_utc from the aware now_utc. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds apps/predbat/mock_base.py providing the superset of the PredBat base surface that ComponentBase dereferences, with unit tests. No module uses it yet; migrations follow.
…lemetry These five need no module-specific state, so each drops its local copy in favour of a plain import. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
axle, gecloud and octopus keep their own cache roots, gecloud its mock HA interface, solax its UTC clock and sigenergy its readonly seeding. kraken aliases the shared class and drops the now-unused json import. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ssertions CSpell flagged the abbreviation dflt; spell it out to keep the workspace dictionary free of ad-hoc shorthand. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Add a contract test that binds a real ComponentBase subclass to MockBase and exercises every property/delegate method, so an AttributeError fires if a future self.base dereference in component_base.py outgrows the mock (the old attribute_superset test only compared two hardcoded lists). - Fix MockBase.set_arg to pop the key on a None value, matching userinterface.py's Fetch.set_arg semantics instead of storing None. - Add coverage asserting deye/enphase/fox/solis/teslemetry re-export the identical shared MockBase, and axle/gecloud/octopus/sigenergy/solax subclass it. - Widen MockBase.log to accept hass.py's quiet keyword. - Correct two factual errors in the design spec: the octopus.py record_status calls are on the Octopus mixin (Output provides the method), not OctopusAPI/ComponentBase; and deye's old dashboard_item printed no attributes at all, so it gains attribute printing rather than merely losing the options elision. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
Pull request overview
This PR consolidates eleven near-identical per-module MockBase CLI harness classes into a single shared apps/predbat/mock_base.py, reducing duplication and fixing drift-induced harness-only defects across vendor modules while keeping existing call sites intact.
Changes:
- Added a shared
MockBaseimplementation (apps/predbat/mock_base.py) that satisfies theComponentBase“base” contract and unifies previously divergent behaviours (e.g.,get_argsignature breadth, timezone-awaremidnight_utc, non-mutatingdashboard_item,set_arg(None)deletion). - Refactored 11 production modules to either re-export, subclass, or alias the shared
MockBase(includingKrakenMockBasealiasing and removing now-unused imports). - Added and registered a focused unit-test suite (
apps/predbat/tests/test_mock_base.py) to lock down the shared mock’s contract and module wiring, plus added design/plan docs for traceability.
Reviewed changes
Copilot reviewed 16 out of 16 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| docs/superpowers/specs/2026-07-29-shared-mock-base-design.md | Design spec documenting scope, contract, and intentional behaviour unifications. |
| docs/superpowers/plans/2026-07-29-shared-mock-base.md | Implementation plan detailing migration steps and verification approach. |
| apps/predbat/mock_base.py | New shared MockBase used by standalone CLI harnesses; implements the superset contract and fixes drift bugs. |
| apps/predbat/tests/test_mock_base.py | New tests validating MockBase contract vs ComponentBase, wiring/identity across modules, and key behavioural guarantees. |
| apps/predbat/unit_test.py | Registers the new mock_base test suite in TEST_REGISTRY. |
| apps/predbat/axle.py | Replaces local mock with a small subclass of the shared base (custom cache root). |
| apps/predbat/deye.py | Replaces local mock with a direct re-export of the shared base; removes now-unused datetime import. |
| apps/predbat/enphase.py | Replaces local mock with a direct re-export of the shared base. |
| apps/predbat/fox.py | Replaces local mock with a direct re-export of the shared base (fixes prior narrow get_arg). |
| apps/predbat/gecloud.py | Replaces local mock with a subclass of the shared base (custom cache root + mock HA interface). |
| apps/predbat/kraken.py | Replaces KrakenMockBase with an alias to the shared base and removes unused json import. |
| apps/predbat/octopus.py | Replaces local mock with a subclass of the shared base (custom cache root). |
| apps/predbat/sigenergy.py | Replaces local mock with a subclass of the shared base and preserves readonly seeding behaviour. |
| apps/predbat/solax.py | Replaces local mock with a subclass pinned to UTC (fixes prior narrow get_arg). |
| apps/predbat/solis.py | Replaces local mock with a direct re-export of the shared base (fixes prior naive midnight_utc). |
| apps/predbat/teslemetry.py | Replaces local mock with a direct re-export of the shared base. |
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
Eleven production modules each defined a near-identical
MockBaseclass — a stand-in for the PredBat base object, used only to drive each module's standalone command-line harness (python fox.py --key=...). Roughly 90% of each copy was identical; the divergence was incidental drift from copy-paste, not intent.This replaces all eleven with a single
apps/predbat/mock_base.py:deye,enphase,fox,solis,teslemetryaxle,gecloud,octopus(cache roots),gecloud(mock HA interface),sigenergy(readonly=seeding),solax(UTC clock)kraken(KrakenMockBase = MockBase)Net: 588 duplicated lines removed, replaced by a ~125-line shared module plus ~40 lines of subclasses.
Latent bugs fixed
The drift had hidden four real defects on the CLI-harness path, none of which had automated coverage:
foxandsolaxhad a narrowget_arg(key, default)— any call routed throughComponentBase.get_arg, which passesindirect/combine/attribute/index/domain/can_override/required_unitas keywords, would raiseTypeError.gecloud'sgetattr(self.base, "record_status", None)guard was always falsy — the mock had norecord_status, so cloud auth-denied was silently never reported from the CLI.fox,krakenandsolisbuilt a naivemidnight_utcfromdatetime.now()alongside a timezone-awarenow_utc, sonow_utc - midnight_utcraisedTypeError: can't subtract offset-naive and offset-aware datetimes.dashboard_itemmutated its caller's dict —attributes["options"] = "..."was applied to the caller's own dict, which was then handed toset_state_wrapperand stored corrupted.krakenandsigenergyalready avoided this with a display copy; the shared version adopts that.Two further behaviours were unified deliberately:
get_arg/set_argnow persist to and read fromself.argseverywhere (previously nine modules returned the bare default, withself.argsempty in practice, so results are unchanged), andset_arg(key, None)now deletes the key to matchuserinterface.py.Tests
New
apps/predbat/tests/test_mock_base.py— 17 tests registered inTEST_REGISTRY. Beyond the per-method coverage, two are worth calling out:test_mock_base_covers_component_base_contractbinds a realComponentBasesubclass to aMockBaseand exercises every property and delegate, so it fails withAttributeErrorif someone adds aself.base.<x>dereference the mock doesn't cover. Verified non-vacuous: deleting an attribute makes it raise.test_mock_base_reexport_identityasserts the five re-export modules expose the shared class by identity and the five subclassing modules are subclasses of it — catching a botched future edit to any of those import lines.kraken.py'simport jsonanddeye.py'sfrom datetime import datetimebecame unused and were removed (verified withruff F401).Verification
./run_all→ exit 0, "All tests passed (total time: 102.38s)"./run_pre_commit→ all 10 hooks greenNotes
Out of scope by design: the test-local
MockBaseclasses intests/(different surface, already shared where it matters) andweb_mcp.py's inline mock (unrelated surface — noget_state_wrapper/get_argat all).Separately worth knowing:
interrogateis configured withfail-under = 100inpyproject.tomlbut is not wired into.pre-commit-config.yaml, so the 100% docstring rule in CLAUDE.md has never actually been gate-enforced. This branch satisfies it regardless — flagging as a pre-existing repo issue, not something to fix here.Design and plan:
docs/superpowers/specs/2026-07-29-shared-mock-base-design.md,docs/superpowers/plans/2026-07-29-shared-mock-base.md🤖 Generated with Claude Code