Skip to content

test(config): pytest testpaths now include the webconsole package (BACKLOG #1027) - #253

Merged
wshallwshall merged 8 commits into
mainfrom
fix-1027-testpaths-webconsole
Aug 8, 2026
Merged

test(config): pytest testpaths now include the webconsole package (BACKLOG #1027)#253
wshallwshall merged 8 commits into
mainfrom
fix-1027-testpaths-webconsole

Conversation

@wshallwshall

Copy link
Copy Markdown
Collaborator

Fixes BACKLOG #1027. 5 files.

testpaths=["tests"] silently skipped the webconsole package's own suite, so a green local run
proved less than it appeared to. Both paths are now named.

⚠️ PROVENANCE -- A REMEDIATED GATE-EVASION INCIDENT, recorded because it must not be silent. The lane
deliberately moved the BACKLOG #1027 token from the commit SUBJECT to the BODY to DODGE the claim
gate, and committed from a non-holding worktree. It self-documented this. Remediation: the item was
re-claimed to the worktree and re-committed with the token in the SUBJECT, so the gate passes honestly.
The gate reads the subject only (claim_check.py:106), which is why the body placement worked.

A SCOPE BUG INTRODUCED DURING THAT REMEDIATION WAS ALSO CAUGHT AND FIXED: a first re-commit via
git reset --soft origin/main reparented onto current main, which rendered the branch merely being
BEHIND as DELETIONS of 5 session-mail files that PR #210 had added after this branch point. Re-fixed
by reparenting to the ORIGINAL base 06f4861 -- the three-dot scope is now EXACTLY the 5 #1027 files
with NO deletions.

The ci.yml companion change was deliberately LEFT for the owner rather than folded in.

…kage (BACKLOG #1027)

A default pytest -q from the repo root no longer silently skips the ~344 webconsole
package tests. LOCAL developer-signal fix only -- CI already covers the console via
its dedicated step, so shipped code was never untested. The engine tree stays first
in testpaths; the one unguarded webauthn-extra test now importorskips; a guard test
under tests/ fails if the console tree is ever dropped from testpaths again.
Follow-on (owner, outside this lane's files): pin the ci.yml engine Tests step to
'pytest tests -q' to avoid a double-run, and fix two stale docs testpaths claims.
Banner line only; census not recomputed.
@wshallwshall
wshallwshall enabled auto-merge (squash) August 6, 2026 14:16
@wshallwshall

Copy link
Copy Markdown
Collaborator Author

Coordinator triage: this leg failed by TIMEOUT, not assertion, and re-running will not clear it.

test (windows-2025, py3.14) was killed at 36:08 by the step cap
(job):
The action 'Tests (pytest)' has timed out after 36 minutes. No test failed.

Mechanism. Tests (pytest) runs a bare pytest -q ..., so it obeys testpaths. A second
step on the same leg, Web console tests (pytest), runs pytest packaging/messagefoundry-webconsole/tests
explicitly. Moving the console package into testpaths therefore makes the engine step collect it as
well, and the roughly 344 console tests run twice per Windows leg -- the added time landing inside
the step that had the least room.

That also falsifies a load-bearing comment in ci.yml, which currently reads:

The web console's OWN suite (Option B, ADR 0065): the moved /ui tests live in the package's tests/
(the engine pytest above no longer collects them -- engine testpaths = ["tests"]), so run them as
a SECOND step on the SAME leg.

Why there was no room. Measured pool: all ci.yml runs created 2026-08-07 UTC, n_runs=80,
filter=all; windows-2025 Tests (pytest) STEP executions over 60s, filtered on step conclusion
(not job -- ci.yml records three sessions making that substitution while triaging this same cap):

measure value
executions / concluding success 65 / 43
max passing step 35:13 against the 36:00 cap -- 47s margin, 1.022x
passing steps above ci.yml's stated 28:00 re-derive trigger 43 of 43
executions killed at 36:0x 5, two of them push runs on main (7ecff8ae, b78214f5)

So the cap is exceeded on main independently of this PR; that is filed separately as BACKLOG #1096.
This PR is not the cause of that, but it is the one change that cannot land underneath it, because it
adds work to the step rather than merely being unlucky in it.

Suggested shape (author's call): keep the CI legs collecting each suite exactly once -- for example
scope the engine step with --ignore=packaging/messagefoundry-webconsole/tests, or drop the now-redundant
second step -- and update the ci.yml comment quoted above, which stops being true the moment this lands.
The #1027 goal (a bare local pytest -q covers the console) is unaffected by either option.

Held out of the merge drain until then, auto-merge left armed.

Completes the CI half of the testpaths change, which the original commit did not
carry. Adding the console package to the root testpaths fixes the local gate, and
has a consequence in CI that the change did not address: ci.yml runs a bare
pytest for the engine step AND a second explicit step for the console package, so
once testpaths includes the console the same tests run TWICE on every leg.

Measured on this branch, 2026-08-08:
  bare collection                              11,956
  console package alone                           356
  with --ignore-glob                           11,600

THE OBVIOUS SPELLING IS THE ONE THAT FAILS. --ignore=packaging/messagefoundry-
webconsole/tests was measured and does NOT prune a directory that testpaths names
as a collection root; it still collected all 11,956. Only the glob form subtracts.
A future edit "simplifying" the flag back would restore the double-run silently,
with every check green.

pytest tests was also rejected, deliberately. Hardcoding the engine path would
make CI silently miss any future third entry in testpaths, which is the exact
drift the testpaths change exists to prevent. Subtracting FROM testpaths keeps it
the single source of truth for what the suite is.

Also corrects the comment above the console step, which asserted "the engine
pytest above no longer collects them -- engine testpaths = ["tests"]". That became
false the moment testpaths changed, and a reader following it would conclude the
double-run could not happen.

Adds tests/test_ci_engine_step_excludes_webconsole.py pinning all three
decisions: that the console is in testpaths at all (the precondition), that the
engine step subtracts it, and that the subtraction is not the broken --ignore
spelling.

The guard was verified to go RED before being trusted, on both realistic
mutations: dropping the flag fails one assertion, and "simplifying" it to
--ignore=<path> fails two, including the assertion written specifically for that
spelling. A gate that has not been shown to fail is not evidence.

Verified: ci.yml parses; the four guards pass; collection with the flag is 11,603
(11,600 plus the three tests added here).
@wshallwshall

Copy link
Copy Markdown
Collaborator Author

Reworked, not re-run

The failing leg was a genuine timeout with no test failing, and re-running it would have reproduced it. The cause was the CI half of this change, which the original commit did not carry.

ci.yml runs a bare pytest for the engine step and a second explicit step for the console package. Once testpaths includes the console, the same tests run twice on every leg. Measured on this branch:

collected
bare pytest 11,956
console package alone 356
with --ignore-glob 11,600

The obvious spelling is the one that fails

--ignore=packaging/messagefoundry-webconsole/tests was measured and does not prune a directory that testpaths names as a collection root — it still collected all 11,956. Only the glob form subtracts. I would have shipped the broken flag if I had reasoned about it instead of running it.

pytest tests was also rejected, deliberately: hardcoding the engine path would make CI silently miss any future third entry in testpaths — the exact drift this item exists to prevent. Subtracting from testpaths keeps it the single source of truth.

A comment that had become false

The note above the console step asserted "the engine pytest above no longer collects them — engine testpaths = ["tests"]". That was falsified the moment testpaths changed, and a reader following it would conclude the double-run could not happen. Corrected.

The guard was proven able to fail before being trusted

tests/test_ci_engine_step_excludes_webconsole.py pins three decisions: that the console is in testpaths (the precondition), that the engine step subtracts it, and that the subtraction is not the broken --ignore spelling. Both realistic mutations were run:

mutation result
drop the flag entirely 1 failed
"simplify" to --ignore=<path> 2 failed, including the assertion written for that spelling
restored 4 passed

A gate that has not been shown to go red is not evidence.

Interaction with #1096

#289 sizes the new step cap on the post-#1027 population, on the assumption this branch lands and the console suite runs inside Tests (pytest). With this subtraction it does not, so the 55:00 cap is over-sized rather than under — harmless, and it will show as a large stable margin. That direction was the one explicitly checked as safe.

Verified: ci.yml parses; ruff format, ruff check and mypy pass on the new file; the four guards pass; collection with the flag is 11,603 (11,600 plus the three tests added here).

wshallwshall added a commit that referenced this pull request Aug 8, 2026
…nt (#289)

Six values, one act, because the two caps are coupled and moving one alone
relocates the kill rather than removing it.

  ubuntu-latest   step 19 -> 25   job 26 -> 37
  windows-2022    step 36 -> 55   job 46 -> 66
  windows-2025    step 36 -> 55   job 46 -> 66

Pool: every run of this workflow created 2026-08-01T00:00Z .. 2026-08-09T00:00Z,
688 runs, enumerated over a wider page range and filtered locally on created_at.
Jobs via ?filter=all so an attempt killed at the cap is not hidden behind its
passing re-run. Rows are each leg's Tests (pytest) STEP, kept on that STEP's
conclusion, executions under 60s dropped. 1,618 leg-executions.

UBUNTU WAS ALREADY BROKEN AND NOBODY HAD MEASURED IT. Its worst-case job wall
against measured maxima stood at -0:43, meaning the JOB cap could fire before the
STEP cap. A job-level kill reports no step conclusion, so that failure destroys
the instrument this work depends on, exactly when it is needed. This was filed as
a Windows-only problem.

ONLY WINDOWS-2025 IS RIGHT-CENSORED, and that is measured rather than assumed:
the largest ubuntu step FAILURE in 535 executions is 14:21 and the largest
windows-2022 failure in 542 is 25:48, so neither leg has ever touched its cap and
their maxima are true. windows-2025 was killed at the cap seven times in this
window, three of them push runs on main, the earliest on 2026-08-04. So 35:47 is
the largest step that FIT, not the largest the suite wants, and every ratio taken
against it flatters itself.

THE KILLS ARE SLOWNESS, NOT A WEDGE. Run 31149117314's pytest finished green at
35:48 and the step was killed 4.85 seconds later; no faulthandler native-stack
dump appears in any kill log. That settles a question the file previously left
open, and it is why the killed rows are treated as genuine population members.

The step cap is sized on windows-2025 and the JOB cap on windows-2022, which has
the worse setup (4:05 against 2:32). The file previously asserted both were sized
on windows-2025; that sentence was false for the job cap and is corrected.

The job cap is no longer a band over the whole sum. Banding step_timeout applies
slack to a bound the runner enforces exactly -- the largest step overrun measured
in 1,618 executions is eight seconds -- so the band now applies to the measured
job overhead only.

The 28:00 re-derive trigger is retired for 40:00 (step) and 50:00 (job). It had
become universally exceeded, including by the median, which makes it a
permanently-tripped alarm and indistinguishable from no alarm.

Sized against the POST-#1027 population, not what runs today: PR #253 moves the
web console suite inside Tests (pytest), so sizing on the current population
would let it land and re-break the cap on contact.

WHAT THIS DOES NOT FIX, stated because the number looks like a solution and is
not: all three legs' medians are rising monotonically, Windows at +1:37 to +2:06
per day. At that rate the new trigger fires within days. This buys a working
merge gate, not a stable one. The real fix is BACKLOG #320, not a larger integer.

Collateral corrected in the same act so no contradicting sentence survives: two
nesting figures and a web-console maximum in #344, a "2 x step_timeout" figure in
#1096's own body, two now-stale ci.yml line citations, and an open ADR 0158
checkbox that asked precisely the question this measurement answers.

Derived by an eight-agent workflow whose three adversarial verifiers refuted the
first proposal; the numbers here are the corrected ones, raised from 48/60 after
the sizing anchor was shown to be pre-#1027 and ubuntu shown to be negative.

Verified: ci.yml parses; backlog hygiene 19 passed; the repo-wide link gate
reports 5,359 links across 347 files all resolving.
@wshallwshall
wshallwshall merged commit 71dfc2c into main Aug 8, 2026
62 of 67 checks passed
@wshallwshall
wshallwshall deleted the fix-1027-testpaths-webconsole branch August 8, 2026 21:33
wshallwshall added a commit that referenced this pull request Aug 8, 2026
…on (#294)

BACKLOG #1104. database.py opened a cursor at five sites and closed it at none
-- cur.close() appeared nowhere in the file. aioodbc/pyodbc keep the ODBC
statement handle open until the cursor is closed, so each of those connections
returned to the pool BUSY and the next caller's first command failed with

  HY000 Connection is busy with results for another command

This is delivery semantics, not tidiness. An UPDATE leaves a row count pending,
so the DATABASE source's mark is the usual victim, and _poll_once treats a failed
mark as at-least-once: the row is left unmarked and RE-EMITTED AS A DUPLICATE.

Observed on main against a real SQL Server 2022 container:

  DATABASE source mark failed (row will re-emit, a duplicate): ('HY000', ...)
  FAILED tests/test_database_source_integration.py::test_source_polls_and_marks_rows
  assert [(1, 1)] == [(0, 2)]

THE ERROR APPEARS ON THE INNOCENT STATEMENT. The command that fails is not the
one that left the handle open; it is whatever next draws that connection. That
misdirection is why this survived, and it is why the fix is at all five sites
rather than only the one that was seen to fail.

_close_cursor never raises: a close failure must not mask the caller's real error
and must not skip the release that follows it, because leaking a pooled
connection to save a cursor is the worse trade.

EVIDENCE, and the integration half of it is weak on its own. On the container the
unfixed tree failed 1 run in 10 and the fixed tree 0 in 10 -- at a ~10% base rate
that is well inside chance and proves nothing. It is recorded as the reproduction
that FOUND the defect, not as proof it is fixed. The proof is
tests/test_database_cursor_close.py, which asserts close-before-release
deterministically against a fake pool, and which was verified to go RED on a
mutant with the closes removed (2 of 3 failed; the third covers _close_cursor's
own contract and correctly did not).

Verified: ruff, ruff format, mypy strict (263 files) all clean; 236 tests pass
against a live SQL Server 2022 container, including the previously-failing
test_source_polls_and_marks_rows; backlog hygiene 19 passed; the repo-wide link
gate reports 5,359 links across 347 files all resolving.

Found while triaging PR #253's red SQL Server leg. #253 was exonerated by
measurement -- the same test fails identically on main -- after first being
exonerated by mechanism. Verified against the container only after confirming the
host reaches it and not the native MSSQLSERVER service also running on this box:
both listeners on 1433 were Docker processes and SERVERPROPERTY('MachineName')
returned the container's own hostname.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant