Commit Graph
10 Commits
Author SHA1 Message Date
adminandClaude Opus 5.5 6e3399322a CI-12: CI grades test_freeze.py against the real hwsim at a pinned admin/goodwe
infra-pin / fresh (pull_request) Failing after 1s
checks / unit (pull_request) Failing after 2s
- .gitea/infra-pin: the one place the admin/goodwe commit is pinned.
- checks.yml: checks out admin/goodwe at the pin as the sibling test_freeze.py
  looks for; any failure fails the job. Tests run with GOODWE_REQUIRE_HWSIM=1,
  and `CI: ` lines are echoed on PASS so the log shows which transport ran.
- test_freeze.py: GOODWE_REQUIRE_HWSIM refuses the stub; a local stub run
  prints a STUB USED line.
- infra-pin.yml: a separate, non-required check that goes red naming both
  SHAs when admin/goodwe master has moved past the pin.
- README: when and how to bump the pin.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 01:57:04 +02:00
glenn schrooyenandClaude Opus 5 8f9c2cad57 CI-9 follow-up: the skip direction, written the right way round
checks / unit (pull_request) Successful in 8m37s
`every subsequent pull request matching it is skipped too` is the inverse of
the behaviour. A pull request that MATCHES a `branches:` filter still RUNS;
the one the filter EXCLUDES is what Gitea sits out, reports `skipped`, and the
rollup launders into `success`.

The error arrived with the generalisation in the previous commit: `33e17db`
said "every subsequent pull request is skipped too", which was unqualified and
true for the `[main]` case it described. Naming the wider hazard made a
qualifier necessary, and the wrong one went in - contradicting the paragraph
directly below it, which describes `[release/1.0]` correctly as leaving every
day-to-day PR running and green.

Corrected, and the direction is now stated explicitly with the failed draft
recorded, because this file already carries one instance of the same shape:
"That was wrong, it contradicted the checkout step's own note a few lines
below, and the error was not cosmetic."

Guard re-verified on the edited bytes: passes here, still refuses
`branches: [release/1.0]` added to the `on:` block. Comment-only, 0
non-comment lines changed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BwRLw7eKYj2byVdwB6kbjU
2026-09-24 14:27:19 +02:00
glenn schrooyenandClaude Opus 5 628279a68a CI-9 follow-up: name the filter that actually gets written, not the one that looks wrong
checks / unit (pull_request) Successful in 7m58s
Three sentences in the blind-spot clause went stale between writing and review.

`no pull request in 99 has ever targeted master` is no longer true: SEC-2 made
`release/1.0` -> `master` the mandated release path and protected `master`
with the same required context, and a PR against that base has run for real.
The census it came from also erases its own evidence - PR #102 was itself
opened against `master` before retargeting, which the /pulls listing no longer
shows, exactly as the caveat 25 lines above this clause warns.

The clause also named `branches: [main]` as the hazard. That is the version
that looks wrong. The one someone would actually write is
`branches: [release/1.0]` - it reads as tightening onto the protected branch,
every day-to-day PR keeps running green, and the only thing it switches off is
the release PR whose content reaches an installed system.

And the prescribed fix named only `release/1.0`'s status_check_contexts. Since
SEC-2 `master` carries the same rule, so a reader following it literally would
half-close the hole and believe it shut.

Guard re-verified against the edited bytes: passes on this file (whose new
prose quotes `branches: [release/1.0]` in a comment) and still refuses a real
filter added to the `on:` block. Comment-only: 0 non-comment lines changed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BwRLw7eKYj2byVdwB6kbjU
2026-09-23 20:18:50 +02:00
glenn schrooyenandClaude Opus 5 33e17dba0e CI-9 follow-up: the summary said "mechanically", the blind spots say otherwise
checks / unit (pull_request) Successful in 7m57s
The residual-risk paragraph at the top of checks.yml claims the `guard the
trigger` step "keeps it that way mechanically rather than by asking the next
editor to read this comment". For blind spots 1 and 3 that is not true: the
guard lives inside the workflow a filter makes Gitea skip, so for those cases
the defence IS the comment.

That paragraph is the preamble someone reads at the exact moment they are
editing the trigger, so a summary running ahead of its detail is worse here
than elsewhere. It now points at the blind spots and states spot 3 plainly:
a filter naming a branch nothing targets is itself skipped, lands green and
untested, and then skips every subsequent pull request - the gate off for
everything, silently.

Flagged by the reviewer on PR #95 (issuecomment-1424) as non-blocking.

This PR is also the first real test of CI-9's fix on release/1.0 itself. Run
806 was a workflow_dispatch and posts no commit status at all, so it verified
content but never the gate. This one has to post `checks / unit
(pull_request)` as success rather than skipped.

Verified before push: the guard passes against this edited file (whose new
prose quotes `branches: [main]` and `master`) and still refuses a genuine
filter added to the `on:` block.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BwRLw7eKYj2byVdwB6kbjU
2026-09-19 15:53:01 +02:00
adminandClaude Opus 5 ca8fd01819 CI-9: correct the guard comment's grading claim, and name the third blind spot
checks / unit (pull_request) Successful in 7m54s
Documentation only - no change to the guard, the trigger, or any step.

The guard's comment block contradicted itself. It said the pull request that
reintroduces a filter "is still graded by the unfiltered workflow which is on
the base today", while the checkout step's note a few lines below says grading
uses the HEAD's workflow. The head's is correct, and the review measured it on
PR #101: head carrying a filter that excludes the base, base carrying the
unfiltered workflow -> no run, context posted `skipped`, rollup `success`, gate
cleared (409). The wrong version made the step sound like an unconditional
ratchet. What is actually true is narrower: it bites whenever the reintroduced
filter still names the pull request's own base, which is the ordinary case.

That correction exposes a third blind spot, worse than the two already listed.
A reintroduced `branches:` filter that simply does not name the base escapes
the guard entirely, because the pull request carrying it is itself skipped. It
needs no craft - `branches: [main]` from a template, or a tidy-up to `[master]`,
which nothing in this repository's history has ever targeted - and it lands as a
skipped, laundered, untested merge into release/1.0. After that every subsequent
pull request is skipped as well: the gate off completely, not merely the stacked
case leaking.

It is documented, not fixed, and deliberately so. No in-repo guard can close it,
because any such guard lives inside the workflow that gets skipped. Closing it
needs a second permanently unfiltered workflow whose context is added to
release/1.0's `status_check_contexts` - a forge change, the owner's decision,
being put to them separately, and explicitly not a blocker on CI-9.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BwRLw7eKYj2byVdwB6kbjU
2026-09-19 14:44:16 +02:00
adminandClaude Opus 5 d2ad080e45 CI-9: guard the trigger mechanically, and re-derive two figures I made up
checks / unit (pull_request) Successful in 7m55s
Review findings on PR #95.

1. Two figures in the header were written under the word "measured" and were
   not measured. "of the 50 pull requests in this repository's history, 49..."
   came from a /pulls call I did not notice was truncated at Gitea's 50-item
   page cap; and "#93 was stacked on TEL-10 five days ago" was invented - #93
   was created 2026-09-19T10:05:00 and merged 11:05:26, the same morning, under
   three hours before CI-9 was opened. Both re-derived from a paged API query
   and now stated with their denominator: 93 pull requests predate this ticket
   (#1..#93, excluding CI-9's own #94..#98), 92 against release/1.0, exactly one
   stacked (#2, TEL-01 -> SAFETY-04). The 92 is flagged as an upper bound rather
   than a census, because the API reports a pull request's CURRENT base and so
   hides any that was opened stacked and later retargeted - which is #93 itself.
   The ratio, and so the cost argument, is unchanged.

2. The fix rested on an invariant - checks.yml has no event filter - that was
   defended only by a comment. Prose has already failed in this exact file:
   9c1a23328a shipped "NO `paths:` FILTER, AND ITS ABSENCE IS LOAD-BEARING" and
   `branches: [release/1.0, master]` in the same commit, eighteen lines apart,
   and that survived 20 days and 84 merges. Adds a step that reads the `on:`
   block of the MERGED checks.yml and fails if it has regained a `branches:`,
   `paths:`, `tags:`, `types:` or any `-ignore` variant. This is not the card's
   option 3 - it does not try to detect its own skip, which is impossible; it
   bites on the pull request that reintroduces a filter, which the unfiltered
   workflow still grades. Two blind spots are named in the step's comment, both
   needing a protection-rule change that is the owner's call.

Also softens "uncloseable from here", which the review judged overstated:
Gitea's acceptance of a `skipped` context is unreachable from this repository,
but the consequence is substantially closeable, and the guard is what closes it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BwRLw7eKYj2byVdwB6kbjU
2026-09-19 14:25:22 +02:00
adminandClaude Opus 5 074157c4c2 CI-9: drop the pull_request branches filter, because a skipped check clears the gate
checks / unit (pull_request) Successful in 7m57s
`branches: [release/1.0, master]` meant a stacked pull request - one based on
another feature branch - never matched. Gitea does not stay silent about a
workflow it sits out: it posts the required context itself with state
`skipped`, the combined-status rollup reports that as `success`, and the merge
gate accepts it. Retargeting does not repair it, because the skip is a commit
status on the head sha and so survives the base change.

Measured on Gitea 1.27.3 with throwaway PR #94 against a branch carrying a copy
of release/1.0's protection rule:

  only status is `skipped`   ->  POST .../merge  409 head out of date  (CLEARED)
  no status at all           ->  POST .../merge  405 not all required  (refused)
  status `pending`           ->  POST .../merge  405 not all required  (refused)
  status `success`           ->  POST .../merge  409 head out of date  (CLEARED)

The 409s come from a deliberately wrong head_commit_id in the probe, which
Gitea checks only after branch protection - so a 409 means the request had
already cleared the gate, and nothing was ever merged. `skipped` is the only
state that leaks.

Fixing this at the gate is not possible. Gitea branch protection can require
that a context is PRESENT, never that it is SUCCESS: its whole status-check
vocabulary is `enable_status_check` and `status_check_contexts`, per this
server's own /swagger.v1.json. So the fix has to be upstream - never emit a
skip - which means no event filter.

Cost: every pull request now runs the full ~8 minute suite, including the
intermediate steps of a stack. 49 of the 50 pull requests in this repository's
history were opened straight against release/1.0 and would have run anyway.

Also corrects two things CI-01's header stated as fact. A filter miss is
fail-OPEN, not the fail-closed pending-forever it described; and it is a
skip-ci keyword, not a filter miss, that actually withholds the context and
leaves it pending.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BwRLw7eKYj2byVdwB6kbjU
2026-09-19 13:58:58 +02:00
adminandClaude Opus 5 1e4bad8aa7 CI-02: grade the merge, and stop a cancelled run reading as a test failure
checks / unit (pull_request) Successful in 3m44s
Two things a green from `checks / unit (pull_request)` was claiming without
having observed. Both were measured on a deliberately conflicted throwaway pull
request (#29, now closed and its branch deleted) rather than read off the
workflow, because the Actions log endpoints 404 on this Gitea and a step's own
conclusion is the only channel out of a job.

CASE 1, AND THE TICKET'S MECHANISM IS WRONG IN A WAY THAT MAKES IT WORSE.
The card says a conflicted pull request loses its merge ref, so that

    git fetch -q --depth 1 origin "$GITHUB_REF" \
      || git fetch -q --depth 1 origin "$GITHUB_SHA"

falls through to the second half and quietly grades the bare branch head. The
symptom is real - PR #14 reported success while `mergeable: false` - but that
is not how. Asking one question per step on the conflicted probe (runs 558,
559):

    GITHUB_REF == refs/pull/<n>/head          P01 passed
    GITHUB_REF != refs/pull/<n>/merge         P02 failed
    GITHUB_SHA == the pull request head sha   P03 passed
    fetching GITHUB_REF succeeds              P04 passed  <- even conflicted
    ...and it lands on the bare head          P05 passed
    refs/pull/<n>/merge does not exist        P06 failed

`git ls-remote origin 'refs/pull/*'` on this server advertises only `/head`, for
open and closed pull requests alike. There are no merge refs here at all. The
first fetch has therefore always succeeded and the fallback has never fired
once; both halves name the same commit anyway. So the gate did not lose merged
content on conflicted pull requests - it has never had merged content, on any
pull request in this repository's history. Green has meant "this branch passed
on its own", and with `block_on_outdated_branch` off on release/1.0 the base has
usually moved underneath it.

Reproduced end to end on one pull request, same conflict, gate swapped:

    run 567  the ORIGINAL gate   -> success, "Successful in 3m38s"
    run 566  the gate below      -> failure in 2s, merge step red,
                                    both test steps `skipped`

The checkout now fetches the base tip and the pull request head, merges them,
and refuses if the merge does not apply - as its own NAMED step, because
/actions/runs/<id>/jobs carries a per-step conclusion and that is what lets a
reader tell "nothing was tested" from "the tests spoke". `--depth 1` is gone: a
merge needs a merge base, and a full fetch of this repository measured 6.8 MB
and 1.1 s. The fallback survives only on the workflow_dispatch path, which has
no pull request to merge and now announces which bare ref it took.

CASE 2, AND `cancel-in-progress` IS NOT THE KNOB.
Runs 531 and 533 on PR #14 were cancelled at 0s, each at the exact second the
next run on that pull request was created:

    531  created 17:08:19 `Waiting to run`, cancelled 17:10:44 = 532's creation
    533  created 17:45:11 `Waiting to run`, cancelled 18:11:22 = 534's creation

Both have `started_at` at the Unix epoch. A cancelled run posts `failure` on the
required context, described `Canceled after 0s`; branch protection compares
state, not description, so "nothing ran" and "your tests are broken" arrive as
the same red wanting opposite responses. Three settings were measured, each with
one run holding the single runner slot and two further pushes behind it:

    no concurrency block at all   runs 561 and 562 both queued, both executed
    group, cancel-in-progress:false
                                  run 564 sat `Blocked by required conditions`
                                  and was cancelled at 0s the second 565 existed

A shared group cancels a run that has not started regardless of that flag -
GitHub documents it and Gitea 1.27 implements it. Having no group is the only
configuration measured to leave a queued run alive, so the block is gone and the
measurement is written above it. The cost is stated in the file: a developer who
pushes four times occupies the shared runner four times. That is CI-01's own
trade written the other way round - a delayed verdict, never a destroyed one -
and a destroyed verdict is worse here because it is indistinguishable from a
real failure. A sha-keyed group was considered and rejected: the only
cancellations it still permits are same-sha ones, whose red lands on a commit
that IS still the head.

WHAT IS NOT ESTABLISHED, SAID PLAINLY. Nothing here makes a cancelled run
distinguishable at the gate BY STATE; that is Gitea posting the status, not this
workflow, and no edit to this file can change it. What ships removes the cause
instead of relabelling the symptom. And a commit that never had a run carries an
empty `/statuses/<sha>` list, while a queued one carries `pending / Waiting to
run` within seconds and a concurrency-held one `pending / Blocked by required
conditions` - those were already distinguishable at the API. `/actions/tasks`,
which is what was polled when this was first reported, lists only tasks that
started and cannot answer the question at all.

test_ci_gate.py grades the gate by RUNNING it. It extracts the checkout and
merge steps out of checks.yml and executes the shipped commands against two
throwaway local repositories it builds - one whose pull request merges, one
whose does not - and pins that the old one-liner exits 0 on the unmergeable one
while the new steps refuse and say nothing was tested. Part 1 pins the two
tokens that assemble `checks / unit (pull_request)`: renaming `name:` or the
`unit:` job key would leave every open and future pull request waiting forever
on a context that never arrives, with no error anywhere in that sequence, and
admin override blocked. Seven mutations of checks.yml were each confirmed red,
including both renames and a restored `--depth 1`. .gitea/ci/Dockerfile gains
`git` for it; the fixtures are local paths and no network is touched.

17/17 green in the checks image.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-01 03:42:54 +02:00
glenn schrooyenandClaude Opus 5 7b4877d56d TEST-15: purge the bytecode cache before a mutation run, not after
checks / unit (pull_request) Successful in 3m35s
FCST-03's developer reverted `self.provisional` to `here.provisional`, re-ran,
and reported the mutation as a survivor. It was not one. CPython validates a
cached `.pyc` on (source size, source mtime truncated to whole seconds), and
that edit moved neither number - same length, same second - so the interpreter
loaded the bytecode it already had. Disassembly settled it at the time: the
source read `here`, the running code loaded `self`. A mutation run had graded
code that was not there, with no error and exit status 0.

This does not make the results already on the record wrong. It makes them
UNVERIFIED, and that distinction is the whole card - so nothing here re-runs a
campaign and nothing here quietly assumes the old numbers hold.

REPRODUCED, THEN CLOSED. test_bytecode.py builds a real package whose two
return values are the same length, imports it in a real subprocess, rewrites it
inside the same second (retrying until both writes land in one whole second -
milliseconds, no sleep) and reads back which value ran. Part 1 shows the
interpreter running SELF off a file that says HERE.

AND THE TWO OBVIOUS MITIGATIONS DO NOT WORK. Measured, not read off
documentation: `python3 -B` and PYTHONDONTWRITEBYTECODE=1 are write-side only.
Neither stops an EXISTING stale .pyc being read, and a mutation run always has
one, because the previous run wrote it. Part 2 grades both still running the old
code. The advice in test_control.py's coverage audit led with `-B`; it is
corrected here - the purge is the half that works, and `-B` earns its place by
keeping THIS run from leaving a cache for the NEXT edit to inherit.

NO SLEEP ANYWHERE. One-second mtime resolution is the mechanism, not the enemy.
Waiting it out would slow every run down and would still only cover the edit
somebody remembered to slow down.

THE HARNESS. tools/mutation_run.sh purges, prints a sha256/byte fingerprint of
every app/*.py it is about to grade - so a mutation report can say WHICH source
produced it, which is the reviewer's addition to this card - runs each suite
under `python3 -B`, refuses a tree with no test_*.py (CI-01's rule, for CI-01's
reason), and fails if a cache appears during the run. It takes the tree as an
argument so the infra repo's replay harnesses can call it without being moved.

WHERE IT IS SAID (AC 2). MUTATION.md carries the mechanism, the measurement,
the reporting rules and the survey, and every one of the 16 suites now points at
it from its docstring. That list is DERIVED FROM THE TREE by test_bytecode.py
part 5, so a new suite that joins this directory without the pointer is red -
the same failure shape CI-01's file glob exists to prevent.

CI WAS ALREADY IMMUNE, AND IS DESCRIBED THAT WAY (AC 4). Verified rather than
reasoned: with the checkout bind mounted `-v "$WS:/w:ro"` the interpreter writes
no __pycache__ into the tree at all, and it fails SILENTLY - no error, exit
status 0 - while the same probe on a writable mount produces one. Each job also
rm -rf's its workspace and clones afresh, so nothing crosses between jobs on the
shared runner. PYTHONDONTWRITEBYTECODE=1 is added as belt and braces only: the
`:ro` is one character wide and the next person debugging a write will be
tempted by it. Part 6 grades both ON THE DOCKER COMMAND LINE rather than on the
file - the first draft searched the whole workflow and stayed green after the
flag was deleted, because the comment explaining it still named it.

THE SHIPPED ADD-ON IS NOT AFFECTED and the question is closed:
goodwe_controller/Dockerfile has set PYTHONDONTWRITEBYTECODE=1 since it was
written. There is no deploy-side finding.

THE SURVEY (AC 3), in MUTATION.md, scoped BY THE PROPERTY. The defect is
one-directional - a mutation that never took leaves the suite green, so stale
bytecode manufactures SURVIVORS and cannot manufacture a red - which puts every
"all red, no survivors" claim in the window out of scope, along with mutations
executed in-process (SAFETY-07's B1/B2/B3, test_peakguard parts 7 and 10, where
no source file is edited) and everything run after the purge discipline landed
on 2026-08-31 (SAFETY-24, SAFETY-26, OPT-04, FCST-03's re-run). What is left is
the recorded SURVIVORS from source-edit runs before that date. Two were re-run
here under the harness and both still stand: TARIFF-01's record_block() ignoring
its policy argument, and SAFETY-03's abs() on the clamp limit - 15 suites green
each, on the tree as it stood. The rest are labelled still-unverified rather
than assumed or re-run; each is one harness run to close, and the moment to
spend it is when somebody next touches that module.

OUT OF SCOPE AND RECORDED: the infra repo's sim/ replay harnesses import the
add-on and write bytecode into both trees (goodwe/sim/__pycache__ exists today),
so SAFETY-02's headline replay carries the same exposure. mutation_run.sh takes
a tree argument for exactly that, but the change belongs in that repo and some
of those harnesses want the rig.

RED AND GREEN. Every new check was broken and watched fail: the purge commented
out of the harness turns part 4 red (and its non-vacuity probe already proves
the fixture would mis-grade without it); one docstring pointer removed turns
part 5 red naming the file; `:ro` and the `-e` flag removed from the workflow
turn part 6 red. Full suite green, 16 files, in the CI image on a read-only
mount - the shape the runner uses.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-01 02:07:09 +02:00
glenn schrooyenandClaude Opus 5 9c1a23328a CI-01: this repository's first automated check
checks / unit (pull_request) Successful in 2m39s
Before this commit `admin/goodwe-addon` had no `.gitea/workflows/` directory at
all. Eight runnable test files - `test_loop.py` alone drives the real
`run_control()` for 93 s of wall clock - and not one of them had ever been run
by a machine. Every merge here was a human reading a diff, and the product
owner's requirement that a green CI gate a merge could not be met on a
repository with no CI to be green.

One job, `unit`, with no `paths:` filter, so it reports on every PR and can
therefore be a REQUIRED status check: Gitea leaves a required context it never
receives pending forever, which is the failure mode on the other side of this
ticket. Concurrency is scoped per PR from the start rather than to a bare group
name - the bare name is what let the infra repo's run 521 be superseded while
still pending, so its four jobs concluded `cancelled` with `started_at` at the
Unix epoch and its PR merged on no evidence at all.

The tests run in `.gitea/ci/Dockerfile`: `alpine:3` plus `py3-aiohttp` and
`py3-paho-mqtt` from apk, matching goodwe_controller/Dockerfile exactly, because
pip would install glibc wheels of a library the shipped image gets from Alpine.
The runner itself cannot host them - bare python3, no aiohttp, no pip - and
mutating a shared long-lived runner with `apk add` is invisible to anyone
reading this repo.

Two things measured while building it and recorded in the file rather than
learned again: the checkout must sit two levels below the mount root or
test_freeze.py's `find_hwsim()` dies on `IndexError: 1` before its first
assertion, and test_freeze's hwsim section is silently absent here because
admin/goodwe is private - so its PASS is a weaker claim than it looks, and the
file says so.

Verified green on release/1.0 content before this workflow was written: 8 of 8
files pass, ~4 minutes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015VXjA3LWccN1rJsk3okyPC
2026-08-30 15:46:30 +02:00