TEST-26 (absorbs TEST-25). Test files only; no app/ change.
What
Checks and prints that raised on a missing value (empty list indexed, next() exhausted, :.0f on None, a dict key the code under test omitted) killed the rest of their file, so a regression showed up as one unnamed traceback and every check below it went ungraded (MUTATION.md, FCST-06).
goodwe_controller/named_absence.py (new, shared): nth(), first(), field(), given() return the value or an Absent stand-in that never raises (formats as <ABSENT ...>, compares False both ways, is falsy). The checks that consume one add present(x) / len(x) == 1, because not x, not in and all() are true of an absence.
Every site in the brief, plus the ones the new scan and a sweep found in 17 files (dict keys on published frames, attribute lists like .frames[-1]/.writes[-1], source-pin next() chains, config values that may be None, two / slew_limit_w() divisions).
test_named_absence.py (new, suite 31 → 32 files): scans every test_*.py for unguarded filtered/accumulator-list indexing (including lists returned by a function in the same file), next() without a default and numeric formats on maybe-None names. It checks that it flags a planted site of each kind and ignores the guarded spellings. It prints what it cannot see, counted per form (dict keys 1041, lists it cannot size 481, ...).
test_price.py 16a: the _T17_CAPS is _T17_CAPS tautology now reads the file's two assemble() calls and grades that they share caps/feed/markup by name and differ only in taxes.
test_control.py: the coverage table is re-measured, with the exact form of each mutation, on runs graded to the end. Rows marked * were short before because the file died. The stale-bytecode note now names both ways a figure under-reports.
main.py circ latch restore → tripped=False, (−38 B)
128/231, 4 named, traceback
231/231, 6 named
test_watchdog.py:2056
main.py N=1 arms unit_sign_cfg (−26 B)
193/231, 23 named, traceback
231/231, 30 named
test_clamp.py:325
clamp.py escalation + re-assert → if False (−37 B)
36/166, 1 named, traceback
166/166, 3 named
test_clamp.py:526
main.py clamp log row if → if False: (−24 B)
52/166, 1 named, traceback
166/166, 3 named
test_freshness.py:452 (+1244-6, 1558-9, 1599)
freshness.py self.stale = True → False (+1 B)
19/113, 7 named, traceback
113/113, 52 named
test_tariff.py:263 (+318, 335-6)
p1.py if stop < end: → <= (+1 B)
8/121, 5 named, traceback
121/121, 44 named
test_peakguard.py:1459-61
main.py peakguard.decide( → getattr(peakguard, "decide")( (+12 B)
93/315, 0 named, traceback
315/315, 6 named
test_peakguard.py:4816
main.py arbitrage.decide( → getattr(…) (+12 B)
277/315, 0 named, traceback
315/315, 1 named
test_peakguard.py:2495/2510
floor-limited status row dropped (+45 B)
174/315, 2 named, traceback
315/315, 4 named
test_peakguard.py:5956
peak-guard idle/engaged row dropped (+6 B)
295/315, 2 named, traceback
315/315, 3 named
test_p1.py:1323-4
mqtt.py "p1_age" → "p1_agE" (+0 B, fresh copy)
229/242, 1 named, traceback
242/242, 4 named
test_inverter.py:997
main.py decision = compute( → compute.__call__( (+9 B)
62/147, 1 named, traceback
147/147, 2 named
test_control.py:994
slew_limit_w() → return abs(float(tuning.slew_w)) (−9 B)
91/99, 5 named, ZeroDivisionError
99/99, 9 named
¹ 98/99 because the if _bad: check is skipped when _bad is empty. That is by design and the run still reaches the end of the file.
The scan bites: putting back {started[0]} at test_control.py:521 turns test_named_absence.py red with test_control.py:521 index started[0].
What this does not cover
The scan only reads syntax within one file. It does not see dict keys, lists returned by other modules or methods, formats on values from another module's function (e.g. short_by()), divisions, or attribute access on None. Part 3 prints these with their counts on every run.
int(), range() and list indexing still raise on an Absent, so those sites are guarded locally.
A missing config.yaml key now gives a named precondition FAIL first. The Absent it produces can still raise later, once it is inside app/ code.
MUTATION.md habit 20 still says test_price_history.py's FCST-04 section 1 read is unguarded. This PR does not edit MUTATION.md.
Clean suite: 32/32 PASS with checks.yml's exact command, purged, :ro.
TEST-26 (absorbs TEST-25). Test files only; no `app/` change.
## What
Checks and prints that **raised** on a missing value (empty list indexed, `next()` exhausted, `:.0f` on None, a dict key the code under test omitted) killed the rest of their file, so a regression showed up as one unnamed traceback and every check below it went ungraded (MUTATION.md, FCST-06).
- **`goodwe_controller/named_absence.py`** (new, shared): `nth()`, `first()`, `field()`, `given()` return the value or an `Absent` stand-in that never raises (formats as `<ABSENT ...>`, compares False both ways, is falsy). The checks that consume one add `present(x)` / `len(x) == 1`, because `not x`, `not in` and `all()` are true of an absence.
- **Every site in the brief**, plus the ones the new scan and a sweep found in 17 files (dict keys on published frames, attribute lists like `.frames[-1]`/`.writes[-1]`, source-pin `next()` chains, config values that may be None, two `/ slew_limit_w()` divisions).
- **`test_named_absence.py`** (new, **suite 31 → 32 files**): scans every `test_*.py` for unguarded filtered/accumulator-list indexing (including lists returned by a function in the same file), `next()` without a default and numeric formats on maybe-None names. It checks that it flags a planted site of each kind and ignores the guarded spellings. It prints what it cannot see, counted per form (dict keys 1041, lists it cannot size 481, ...).
- **`test_price.py` 16a**: the `_T17_CAPS is _T17_CAPS` tautology now reads the file's two `assemble()` calls and grades that they share caps/feed/markup by name and differ only in `taxes`.
- **`test_control.py`**: the coverage table is re-measured, with the exact form of each mutation, on runs graded to the end. Rows marked `*` were short before because the file died. The stale-bytecode note now names both ways a figure under-reports.
## Mutations: before (a45ed06) → after
Each mutation was run on a fresh `cp -a`, with `__pycache__` purged, in `goodwe-addon-checks:1` with the tree mounted `:ro`. "Graded" = ok + FAIL lines.
| site | mutation | before | after |
|---|---|---|---|
| test_pv.py:351 | main.py `if "pv" in optional_keys(self.o):` → `… and self.pv_w is not None:` (+26 B) | 10/69, **0 named**, traceback (whole 31-file suite: 0 named) | 69/69, **5 named** |
| test_control.py:555 | control.py freeze EXIT `if` → `if False` (−96 B) | 58/99, 4 named, traceback | 98/99¹, 11 named |
| test_control.py:523 | control.py `if frozen:` → `if False:` (−1 B) | 55/99, 5 named, traceback | 98/99¹, 14 named |
| test_watchdog.py:1105 | main.py circ latch restore → `tripped=False,` (−38 B) | 128/231, 4 named, traceback | 231/231, 6 named |
| test_watchdog.py:2056 | main.py N=1 arms `unit_sign_cfg` (−26 B) | 193/231, 23 named, traceback | 231/231, 30 named |
| test_clamp.py:325 | clamp.py escalation + re-assert → `if False` (−37 B) | 36/166, 1 named, traceback | 166/166, 3 named |
| test_clamp.py:526 | main.py clamp log row `if` → `if False:` (−24 B) | 52/166, 1 named, traceback | 166/166, 3 named |
| test_freshness.py:452 (+1244-6, 1558-9, 1599) | freshness.py `self.stale = True` → `False` (+1 B) | 19/113, 7 named, traceback | 113/113, 52 named |
| test_tariff.py:263 (+318, 335-6) | p1.py `if stop < end:` → `<=` (+1 B) | 8/121, 5 named, traceback | 121/121, 44 named |
| test_peakguard.py:1459-61 | main.py `peakguard.decide(` → `getattr(peakguard, "decide")(` (+12 B) | 93/315, 0 named, traceback | 315/315, 6 named |
| test_peakguard.py:4816 | main.py `arbitrage.decide(` → `getattr(…)` (+12 B) | 277/315, 0 named, traceback | 315/315, 1 named |
| test_peakguard.py:2495/2510 | floor-limited status row dropped (+45 B) | 174/315, 2 named, traceback | 315/315, 4 named |
| test_peakguard.py:5956 | peak-guard idle/engaged row dropped (+6 B) | 295/315, 2 named, traceback | 315/315, 3 named |
| test_p1.py:1323-4 | mqtt.py `"p1_age"` → `"p1_agE"` (**+0 B**, fresh copy) | 229/242, 1 named, traceback | 242/242, 4 named |
| test_inverter.py:997 | main.py `decision = compute(` → `compute.__call__(` (+9 B) | 62/147, 1 named, traceback | 147/147, 2 named |
| test_control.py:994 | `slew_limit_w()` → `return abs(float(tuning.slew_w))` (−9 B) | 91/99, 5 named, ZeroDivisionError | 99/99, 9 named |
¹ 98/99 because the `if _bad:` check is skipped when `_bad` is empty. That is by design and the run still reaches the end of the file.
The scan bites: putting back `{started[0]}` at test_control.py:521 turns `test_named_absence.py` red with `test_control.py:521 index started[0]`.
## What this does not cover
- The scan only reads syntax within one file. It does not see dict keys, lists returned by other modules or methods, formats on values from another module's function (e.g. `short_by()`), divisions, or attribute access on None. Part 3 prints these with their counts on every run.
- `int()`, `range()` and list indexing still raise on an `Absent`, so those sites are guarded locally.
- A missing config.yaml key now gives a named precondition FAIL first. The `Absent` it produces can still raise later, once it is inside app/ code.
- MUTATION.md habit 20 still says `test_price_history.py`'s FCST-04 section 1 read is unguarded. This PR does not edit MUTATION.md.
Clean suite: 32/32 PASS with checks.yml's exact command, purged, `:ro`.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01BwRLw7eKYj2byVdwB6kbjU
Checks and prints that indexed an empty list, exhausted next(), or formatted
None killed the rest of their file (FCST-06). Read them through the new
named_absence.py (nth/first/field/given/present) and conjoin a presence term.
- every site in the brief, plus the family the new scan and a sweep found
- test_named_absence.py: scans every test_*.py for unguarded filtered-list
indexing, next() without a default and numeric formats on maybe-None
names; proven to bite; prints what it cannot see, counted
- test_price.py 16a: the `_T17_CAPS is _T17_CAPS` tautology now grades the
two assemble() calls' arguments
- test_control.py: coverage table re-measured on runs graded to the end
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BwRLw7eKYj2byVdwB6kbjU
Review (TEST-26) - no blocking findings. Evidence below; four non-blocking notes need an answer here, as a reply or a follow-up card.
Combined trees (goodwe-addon-checks:1, --network none, source :ro, __pycache__ purged):
(a) this PR merged onto release/1.0 @ 6e65272: 34/34 PASS. That includes test_control_task.py (#121), and the guard scans all 34 files with 0 found.
(b) (a) plus #120 @ f4f7221: 35/35 PASS. The guard also scans test_tool_inputs.py and flags nothing.
(c) #120 has since merged at b06f1fa. This PR merged onto release/1.0 @ 3e38baf: 35/35 PASS.
CI-10: run 835 (attempt 1, PRNUM 122, BASEREF release/1.0, head 770988c) is green, but it graded against base fa3d237, not the current 3e38baf, and ran 33 files. I'm reopening the PR to get a run graded against the current base before merging.
Absent never turns a FAIL into a PASS: I AST-scanned every check() whose condition reads an accessor result and uses not / not in / != / is not / all(). I hand-read 47 sites across 10 files. Every reachable one either has a presence term or starts with a term that is False on an Absent. I set an indexed list to [] in 6 places. Each gave a named FAIL, and each file was graded to the end with the same total count as the baseline:
watchdog _pre38: 4 FAIL / 231
clamp notes_x: 2 / 166, including the not in + present() check
pv _pv_cfg: 2 / 69
freshness _stale57: 2 / 113, including the not in + present() check
peakguard rows25: 3 / 315
control pinned: 1 / 99
The guard bites: I put back _pv_cfg[0] (test_pv:477) and a bare next(...) (test_packshare:351). Both were flagged, and the file went red. The part 3 skip table prints its counts (1046 / 492 / 476 / 435 / 258 / 216 / 103).
Coverage table: I re-ran saturated_now = False. It gives 27 FAIL of 98 graded, matching the row.
No weakened assertions: every changed condition is equal or stricter. The added len(x)==1 terms make checks stricter, and the T17 identity check (_T17_CAPS is _T17_CAPS, which was always true) is replaced by a real check.
Non-blocking:
Guard scope. The PR says the guard flags "any unguarded constant index". It only flags indexes on lists it can see are filtered, or were started as []. Everything else goes into the census (492 name-lists). That is honestly printed, but the PR text / docstring should say "filtered list".
test_pv.py:897.pv_msg(_m_full) != pv_msg(...) has no presence term. It is unreachable in practice, because the same line's .frames[-1] would raise first. Add present(_m_full), or leave it and note why.
test_peakguard: config constants now flow into the code under test as Absent. Two checks can then pass: not near(...) at :422 and not any(near(...)) at :6055. The named precondition (present(CAP_W, ...)) still turns the file red. This matches the old None behaviour, so it is a follow-up at most.
test_price _AMAIN.next(..., None) inside derived() swaps derived's own _Raised naming for a None that fails later. It is cosmetic, but the scan could exempt derived(lambda: ...) bodies instead.
Length: fine. The helper docstring is 11 lines, and the coverage table got shorter. No bloat to flag.
**Review (TEST-26) - no blocking findings.** Evidence below; four non-blocking notes need an answer here, as a reply or a follow-up card.
**Combined trees** (goodwe-addon-checks:1, `--network none`, source `:ro`, `__pycache__` purged):
- (a) this PR merged onto release/1.0 @ 6e65272: **34/34 PASS**. That includes `test_control_task.py` (#121), and the guard scans all 34 files with 0 found.
- (b) (a) plus #120 @ f4f7221: **35/35 PASS**. The guard also scans `test_tool_inputs.py` and flags nothing.
- (c) #120 has since merged at b06f1fa. This PR merged onto release/1.0 @ 3e38baf: **35/35 PASS**.
**CI-10:** run 835 (attempt 1, PRNUM 122, BASEREF release/1.0, head 770988c) is green, but it graded against base **fa3d237**, not the current 3e38baf, and ran 33 files. I'm reopening the PR to get a run graded against the current base before merging.
**Absent never turns a FAIL into a PASS:** I AST-scanned every `check()` whose condition reads an accessor result and uses `not` / `not in` / `!=` / `is not` / `all()`. I hand-read 47 sites across 10 files. Every reachable one either has a presence term or starts with a term that is False on an Absent. I set an indexed list to `[]` in 6 places. Each gave a named FAIL, and each file was graded to the end with the same total count as the baseline:
- watchdog `_pre38`: 4 FAIL / 231
- clamp `notes_x`: 2 / 166, including the `not in` + `present()` check
- pv `_pv_cfg`: 2 / 69
- freshness `_stale57`: 2 / 113, including the `not in` + `present()` check
- peakguard `rows25`: 3 / 315
- control `pinned`: 1 / 99
**The guard bites:** I put back `_pv_cfg[0]` (test_pv:477) and a bare `next(...)` (test_packshare:351). Both were flagged, and the file went red. The part 3 skip table prints its counts (1046 / 492 / 476 / 435 / 258 / 216 / 103).
**Coverage table:** I re-ran `saturated_now = False`. It gives 27 FAIL of 98 graded, matching the row.
**No weakened assertions:** every changed condition is equal or stricter. The added `len(x)==1` terms make checks stricter, and the T17 identity check (`_T17_CAPS is _T17_CAPS`, which was always true) is replaced by a real check.
Non-blocking:
1. **Guard scope.** The PR says the guard flags "any unguarded constant index". It only flags indexes on lists it can see are filtered, or were started as `[]`. Everything else goes into the census (492 name-lists). That is honestly printed, but the PR text / docstring should say "filtered list".
2. **test_pv.py:897.** `pv_msg(_m_full) != pv_msg(...)` has no presence term. It is unreachable in practice, because the same line's `.frames[-1]` would raise first. Add `present(_m_full)`, or leave it and note why.
3. **test_peakguard: config constants now flow into the code under test as Absent.** Two checks can then pass: `not near(...)` at :422 and `not any(near(...))` at :6055. The named precondition (`present(CAP_W, ...)`) still turns the file red. This matches the old None behaviour, so it is a follow-up at most.
4. **test_price `_AMAIN`.** `next(..., None)` inside `derived()` swaps derived's own `_Raised` naming for a None that fails later. It is cosmetic, but the scan could exempt `derived(lambda: ...)` bodies instead.
Length: fine. The helper docstring is 11 lines, and the coverage table got shorter. No bloat to flag.
CI-10: run 839 is green. Attempt 1, PRNUM 122, BASEREF release/1.0, base 3e38baf (the current tip, which includes #120 and #121), head 770988c, GRADED: refs/pull/122/head merged into release/1.0, 35/35 files. Merging with head_commit_id pinned. The four non-blocking notes above move to a follow-up card, TEST-27, unless answered here.
CI-10: run 839 is green. Attempt 1, PRNUM 122, BASEREF release/1.0, base 3e38baf (the current tip, which includes #120 and #121), head 770988c, `GRADED: refs/pull/122/head merged into release/1.0`, 35/35 files. Merging with head_commit_id pinned. The four non-blocking notes above move to a follow-up card, TEST-27, unless answered here.
admin
merged commit 43b274035f into release/1.02026-09-25 00:57:21 +02:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
TEST-26 (absorbs TEST-25). Test files only; no
app/change.What
Checks and prints that raised on a missing value (empty list indexed,
next()exhausted,:.0fon None, a dict key the code under test omitted) killed the rest of their file, so a regression showed up as one unnamed traceback and every check below it went ungraded (MUTATION.md, FCST-06).goodwe_controller/named_absence.py(new, shared):nth(),first(),field(),given()return the value or anAbsentstand-in that never raises (formats as<ABSENT ...>, compares False both ways, is falsy). The checks that consume one addpresent(x)/len(x) == 1, becausenot x,not inandall()are true of an absence..frames[-1]/.writes[-1], source-pinnext()chains, config values that may be None, two/ slew_limit_w()divisions).test_named_absence.py(new, suite 31 → 32 files): scans everytest_*.pyfor unguarded filtered/accumulator-list indexing (including lists returned by a function in the same file),next()without a default and numeric formats on maybe-None names. It checks that it flags a planted site of each kind and ignores the guarded spellings. It prints what it cannot see, counted per form (dict keys 1041, lists it cannot size 481, ...).test_price.py16a: the_T17_CAPS is _T17_CAPStautology now reads the file's twoassemble()calls and grades that they share caps/feed/markup by name and differ only intaxes.test_control.py: the coverage table is re-measured, with the exact form of each mutation, on runs graded to the end. Rows marked*were short before because the file died. The stale-bytecode note now names both ways a figure under-reports.Mutations: before (
a45ed06) → afterEach mutation was run on a fresh
cp -a, with__pycache__purged, ingoodwe-addon-checks:1with the tree mounted:ro. "Graded" = ok + FAIL lines.if "pv" in optional_keys(self.o):→… and self.pv_w is not None:(+26 B)if→if False(−96 B)if frozen:→if False:(−1 B)tripped=False,(−38 B)unit_sign_cfg(−26 B)if False(−37 B)if→if False:(−24 B)self.stale = True→False(+1 B)if stop < end:→<=(+1 B)peakguard.decide(→getattr(peakguard, "decide")((+12 B)arbitrage.decide(→getattr(…)(+12 B)"p1_age"→"p1_agE"(+0 B, fresh copy)decision = compute(→compute.__call__((+9 B)slew_limit_w()→return abs(float(tuning.slew_w))(−9 B)¹ 98/99 because the
if _bad:check is skipped when_badis empty. That is by design and the run still reaches the end of the file.The scan bites: putting back
{started[0]}at test_control.py:521 turnstest_named_absence.pyred withtest_control.py:521 index started[0].What this does not cover
short_by()), divisions, or attribute access on None. Part 3 prints these with their counts on every run.int(),range()and list indexing still raise on anAbsent, so those sites are guarded locally.Absentit produces can still raise later, once it is inside app/ code.test_price_history.py's FCST-04 section 1 read is unguarded. This PR does not edit MUTATION.md.Clean suite: 32/32 PASS with checks.yml's exact command, purged,
:ro.🤖 Generated with Claude Code
https://claude.ai/code/session_01BwRLw7eKYj2byVdwB6kbjU
Review (TEST-26) - no blocking findings. Evidence below; four non-blocking notes need an answer here, as a reply or a follow-up card.
Combined trees (goodwe-addon-checks:1,
--network none, source:ro,__pycache__purged):6e65272: 34/34 PASS. That includestest_control_task.py(#121), and the guard scans all 34 files with 0 found.f4f7221: 35/35 PASS. The guard also scanstest_tool_inputs.pyand flags nothing.b06f1fa. This PR merged onto release/1.0 @3e38baf: 35/35 PASS.CI-10: run 835 (attempt 1, PRNUM 122, BASEREF release/1.0, head
770988c) is green, but it graded against basefa3d237, not the current3e38baf, and ran 33 files. I'm reopening the PR to get a run graded against the current base before merging.Absent never turns a FAIL into a PASS: I AST-scanned every
check()whose condition reads an accessor result and usesnot/not in/!=/is not/all(). I hand-read 47 sites across 10 files. Every reachable one either has a presence term or starts with a term that is False on an Absent. I set an indexed list to[]in 6 places. Each gave a named FAIL, and each file was graded to the end with the same total count as the baseline:_pre38: 4 FAIL / 231notes_x: 2 / 166, including thenot in+present()check_pv_cfg: 2 / 69_stale57: 2 / 113, including thenot in+present()checkrows25: 3 / 315pinned: 1 / 99The guard bites: I put back
_pv_cfg[0](test_pv:477) and a barenext(...)(test_packshare:351). Both were flagged, and the file went red. The part 3 skip table prints its counts (1046 / 492 / 476 / 435 / 258 / 216 / 103).Coverage table: I re-ran
saturated_now = False. It gives 27 FAIL of 98 graded, matching the row.No weakened assertions: every changed condition is equal or stricter. The added
len(x)==1terms make checks stricter, and the T17 identity check (_T17_CAPS is _T17_CAPS, which was always true) is replaced by a real check.Non-blocking:
[]. Everything else goes into the census (492 name-lists). That is honestly printed, but the PR text / docstring should say "filtered list".pv_msg(_m_full) != pv_msg(...)has no presence term. It is unreachable in practice, because the same line's.frames[-1]would raise first. Addpresent(_m_full), or leave it and note why.not near(...)at :422 andnot any(near(...))at :6055. The named precondition (present(CAP_W, ...)) still turns the file red. This matches the old None behaviour, so it is a follow-up at most._AMAIN.next(..., None)insidederived()swaps derived's own_Raisednaming for a None that fails later. It is cosmetic, but the scan could exemptderived(lambda: ...)bodies instead.Length: fine. The helper docstring is 11 lines, and the coverage table got shorter. No bloat to flag.
CI-10: run 839 is green. Attempt 1, PRNUM 122, BASEREF release/1.0, base
3e38baf(the current tip, which includes #120 and #121), head770988c,GRADED: refs/pull/122/head merged into release/1.0, 35/35 files. Merging with head_commit_id pinned. The four non-blocking notes above move to a follow-up card, TEST-27, unless answered here.