Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -454,8 +454,21 @@ jobs:
python-version: "3.13"
cache: pip

# The same two files every other Python job installs, not a hand-picked
# subset: mypy resolves imports from the environment, and with
# --ignore-missing-imports anything absent silently becomes Any. That
# cuts both ways. Missing `pytest` makes every `@pytest.fixture` and
# `@pytest.mark.parametrize` untyped, so an annotated test reports
# `untyped-decorator` here while type-checking clean in the local .venv
# -- and no amount of annotating closes it. Missing `fastapi` does the
# same to `@router.get`. Missing `numpy` goes the other way and MASKS
# real errors: pwl_window_dp.py reports 7 errors with numpy installed
# and 3 without, so a genuine mistake would pass the merge gate and
# fail only on the maintainer's machine.
- name: Install tools
run: pip install black ruff mypy
run: |
pip install -r backend/requirements.txt
pip install -r requirements-dev.txt

- name: Black formatting
run: black --check . --exclude="/(build|\.venv|node_modules)/"
Expand Down
22 changes: 16 additions & 6 deletions backend/tests/test_agent_permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,9 @@ def rules() -> dict[str, list[str]]:


@pytest.mark.parametrize("command", RUNS_UNATTENDED)
def test_implement_issue_runs_without_prompting(command, rules):
def test_implement_issue_runs_without_prompting(
command: str, rules: dict[str, list[str]]
) -> None:
verdict = decide(command, rules)
assert verdict == "allow", (
f"{command!r} resolves to {verdict!r}. implement-issue runs this "
Expand All @@ -153,7 +155,9 @@ def test_implement_issue_runs_without_prompting(command, rules):


@pytest.mark.parametrize("command", NEEDS_APPROVAL)
def test_maintainer_decisions_still_ask(command, rules):
def test_maintainer_decisions_still_ask(
command: str, rules: dict[str, list[str]]
) -> None:
verdict = decide(command, rules)
assert verdict == "ask", (
f"{command!r} resolves to {verdict!r}, expected 'ask'. This action is "
Expand All @@ -162,13 +166,17 @@ def test_maintainer_decisions_still_ask(command, rules):


@pytest.mark.parametrize("command", FORBIDDEN)
def test_prohibited_commands_are_denied(command, rules):
def test_prohibited_commands_are_denied(
command: str, rules: dict[str, list[str]]
) -> None:
verdict = decide(command, rules)
assert verdict == "deny", f"{command!r} resolves to {verdict!r}, expected 'deny'."


@pytest.mark.parametrize("command", MUST_NOT_BE_DENIED)
def test_non_destructive_forms_keep_an_escape_hatch(command, rules):
def test_non_destructive_forms_keep_an_escape_hatch(
command: str, rules: dict[str, list[str]]
) -> None:
verdict = decide(command, rules)
assert verdict != "deny", (
f"{command!r} is denied. `deny` never prompts, so this removes the "
Expand All @@ -177,7 +185,7 @@ def test_non_destructive_forms_keep_an_escape_hatch(command, rules):
)


def test_gh_api_writes_ask_in_either_flag_position(rules):
def test_gh_api_writes_ask_in_either_flag_position(rules: dict[str, list[str]]) -> None:
"""`gh api` reads must pass; writes must ask regardless of argument order.

`Bash(gh api * -X *)` alone does not match `gh api -X POST <endpoint>` --
Expand All @@ -198,7 +206,9 @@ def test_gh_api_writes_ask_in_either_flag_position(rules):
assert decide(write, rules) == "ask", f"{write!r} must ask"


def test_no_client_rule_duplicates_a_server_ruleset(rules):
def test_no_client_rule_duplicates_a_server_ruleset(
rules: dict[str, list[str]],
) -> None:
"""Ref protection is the server's job; duplicating it is pure friction.

`main`, `beta` and tags are protected by GitHub rulesets with empty bypass
Expand Down
3 changes: 2 additions & 1 deletion core/bess/simulation/vpp_simulator.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
where hand-mirrored copies of the same logic were the whole bug class.
"""

from collections.abc import Callable
from dataclasses import dataclass, field

from core.bess.dp_battery_algorithm import (
Expand Down Expand Up @@ -387,7 +388,7 @@ def simulate_vpp_commands(


def _simulate(
command_at,
command_at: Callable[[int, float], VppCommand],
n_periods: int,
solar_production: list[float],
home_consumption: list[float],
Expand Down
8 changes: 6 additions & 2 deletions core/bess/tests/unit/test_solax_modbus_growatt_vpp.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,9 @@ def test_idle_enables_battery_first_hold(self, controller):
assert power_pct == 1
assert enabled is True

def test_idle_at_reserve_floor_releases_remote_control(self, controller):
def test_idle_at_reserve_floor_releases_remote_control(
self, controller: SolaxModbusGrowattController
) -> None:
"""#592: at the reserve floor the battery-first hold has nothing left
to protect, but keeping remote control enabled re-asserts a command
every period so the inverter (and its BMS) never sleeps.
Expand All @@ -150,7 +152,9 @@ def test_idle_at_reserve_floor_releases_remote_control(self, controller):
assert power_pct == 0
assert enabled is False

def test_idle_above_reserve_floor_still_holds_battery_first(self, controller):
def test_idle_above_reserve_floor_still_holds_battery_first(
self, controller: SolaxModbusGrowattController
) -> None:
"""#592 must not weaken #466: above the floor there IS energy being
held back for a later peak, so the battery-first hold stays."""
power_pct, enabled = controller._intent_to_vpp(
Expand Down
33 changes: 20 additions & 13 deletions core/bess/tests/unit/test_vpp_idle_at_reserve_floor.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"""

from types import SimpleNamespace
from typing import Any, cast

from core.bess.battery_system_manager import BatterySystemManager
from core.bess.dp_schedule import DPSchedule
Expand Down Expand Up @@ -51,17 +52,23 @@ def _make_vpp_bsm(
},
)
intents = ["IDLE"] * 96
bsm._inverter_controller.strategic_intents = intents
bsm._inverter_controller.current_schedule = SimpleNamespace(actions=[0.0] * 96)
inverter_controller = bsm._inverter_controller
assert inverter_controller is not None
inverter_controller.strategic_intents = intents
# A duck-typed stand-in: only `.actions` is read on this path.
inverter_controller.current_schedule = cast(
DPSchedule, SimpleNamespace(actions=[0.0] * 96)
)
return bsm, controller


def _last_vpp_command(controller: MockHomeAssistantController) -> dict:
return controller.calls["growatt_vpp_periods"][-1]
def _last_vpp_command(controller: MockHomeAssistantController) -> dict[str, Any]:
command: dict[str, Any] = controller.calls["growatt_vpp_periods"][-1]
return command


class TestIdleAtReserveFloorReleasesControl:
def test_idle_at_the_floor_releases_the_inverter(self):
def test_idle_at_the_floor_releases_the_inverter(self) -> None:
"""At min SoC the written command must release remote control, so the
inverter reverts to its own self-use and stops being commanded."""
bsm, controller = _make_vpp_bsm(soc=10.0)
Expand All @@ -75,7 +82,7 @@ def test_idle_at_the_floor_releases_the_inverter(self):
assert command["power_pct"] == 0
assert command["remote_control_enabled"] is False

def test_idle_above_the_floor_still_holds_battery_first(self):
def test_idle_above_the_floor_still_holds_battery_first(self) -> None:
"""#466 must survive #592: with energy still banked for the morning
peak, IDLE holds battery_first exactly as before."""
bsm, controller = _make_vpp_bsm(soc=50.0)
Expand All @@ -86,7 +93,7 @@ def test_idle_above_the_floor_still_holds_battery_first(self):
assert command["power_pct"] == 1
assert command["remote_control_enabled"] is True

def test_released_control_stops_re_asserting_every_period(self):
def test_released_control_stops_re_asserting_every_period(self) -> None:
"""The actual mechanism behind "the BMS never sleeps": with remote
control enabled `_apply_period_vpp` rewrites every period to refresh
the inverter's fallback timer (#404). Once released there is nothing
Expand All @@ -101,7 +108,7 @@ def test_released_control_stops_re_asserting_every_period(self):
"period -- re-asserting is what kept the BMS awake"
)

def test_unreadable_soc_holds_rather_than_releasing(self):
def test_unreadable_soc_holds_rather_than_releasing(self) -> None:
"""`get_battery_soc()` is typed `float | None`, so a transient
unavailable/unknown HA sensor must not decide this.

Expand All @@ -122,7 +129,7 @@ def test_unreadable_soc_holds_rather_than_releasing(self):
assert command["power_pct"] == 1
assert command["remote_control_enabled"] is True

def test_out_of_range_soc_holds_rather_than_releasing(self):
def test_out_of_range_soc_holds_rather_than_releasing(self) -> None:
"""Same branch, the other invalid shape a sensor can report. Mirrors
the existing `0 <= soc <= 100` validation in
`_get_current_battery_soc()` rather than inventing a second rule."""
Expand All @@ -135,7 +142,7 @@ def test_out_of_range_soc_holds_rather_than_releasing(self):
assert command["power_pct"] == 1
assert command["remote_control_enabled"] is True

def test_hold_still_re_asserts_every_period_above_the_floor(self):
def test_hold_still_re_asserts_every_period_above_the_floor(self) -> None:
"""Guard rail on the test above: the every-period refresh is correct
and must be preserved wherever remote control is genuinely active,
otherwise the fallback timer would lapse mid-hold (#404)."""
Expand Down Expand Up @@ -191,19 +198,19 @@ class TestDisplayAgreesWithWhatIsWritten:
exactly the periods production releases.
"""

def test_predicted_idle_at_the_floor_displays_the_release(self):
def test_predicted_idle_at_the_floor_displays_the_release(self) -> None:
controller = _vpp_controller_with_plan(soe=5.0) # == min_soe_kwh
fields = controller.get_period_settings(PERIOD)
assert fields["vpp_power_pct"] == 0
assert fields["vpp_remote_control"] is False

def test_predicted_idle_above_the_floor_displays_the_hold(self):
def test_predicted_idle_above_the_floor_displays_the_hold(self) -> None:
controller = _vpp_controller_with_plan(soe=25.0)
fields = controller.get_period_settings(PERIOD)
assert fields["vpp_power_pct"] == 1
assert fields["vpp_remote_control"] is True

def test_the_crossing_period_still_displays_the_hold(self):
def test_the_crossing_period_still_displays_the_hold(self) -> None:
"""The uniform-trajectory fixtures above cannot see an off-by-one:
with every index equal, `soe[period]` and `soe[period - 1]` agree.

Expand Down
8 changes: 4 additions & 4 deletions core/bess/tests/unit/test_vpp_simulator_branches.py
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ class TestIdleAtReserveFloor:
re-pinning the v10.0.2 VPP baseline.
"""

def test_release_matches_the_hold_when_solar_is_in_surplus(self):
def test_release_matches_the_hold_when_solar_is_in_surplus(self) -> None:
"""Both absorb the surplus: the hold via `0.0` -> `_state_transition`'s
IDLE branch, the released command via its own `deficit <= 0` return.

Expand All @@ -259,7 +259,7 @@ def test_release_matches_the_hold_when_solar_is_in_surplus(self):
"the grid, which is why #592 releases control instead"
)

def test_release_matches_the_hold_when_load_exceeds_solar(self):
def test_release_matches_the_hold_when_load_exceeds_solar(self) -> None:
"""No headroom at the floor, so the released command cannot discharge:
`available == 0` makes `delivered` 0 and the branch returns None (a
hold), exactly like battery_first. This is what makes releasing safe
Expand Down Expand Up @@ -292,7 +292,7 @@ def test_release_matches_the_hold_when_load_exceeds_solar(self):
home_consumption=2.0,
) == pytest.approx(at_floor), "neither command may move SoE at the floor"

def test_release_would_discharge_above_the_floor(self):
def test_release_would_discharge_above_the_floor(self) -> None:
"""The guard rail on the test above: releasing control is only
flow-neutral *at* the floor. One kWh above it the released command
drains the battery to cover load -- which is exactly #466's defect,
Expand Down Expand Up @@ -325,7 +325,7 @@ def test_mismatched_plan_lengths_raise(self):
with pytest.raises(ValueError, match="inconsistent"):
derive_vpp_commands(["IDLE"], [0.0, 0.0], s, soe)

def test_short_soe_trajectory_raises(self):
def test_short_soe_trajectory_raises(self) -> None:
"""Same reasoning for the SoE trajectory #592 added: one entry short
and the last periods would be derived against the wrong floor state,
silently, rather than failing at the input."""
Expand Down
Loading