Repository navigation
ci: enforce the python floor production actually runs (3.9) - #2479
Conversation
The live discord-bridge on the 24/7 node runs the CommandLineTools system interpreter: /Library/Developer/CommandLineTools/.../Versions/3.9/.../Python -> 3.9.6 measured on the running process (pid 55664), not inferred from config. No workflow validates that version. ci.yml runs on "the runner's stock python3" with no setup-python step, and the only pins in .github/workflows are 3.11 x3 and 3.12 x2. So a 3.10+ syntax regression in a bridge-imported module is green on every existing gate and fails only at import after a bridge restart. The constraint was already documented and already unenforced: progress_stream.py says "Python 3.9 compatible (workspace runs system python3 = 3.9.6)" in its docstring, and 47 modules rely on `from __future__ import annotations` to hold it up. This gives that documented rule a caller. Scope, stated rather than left to be discovered: this is a SYNTAX gate. It catches 3.10+ grammar (match/case). It does NOT catch runtime-only features that parse fine on 3.9 — datetime.UTC, tomllib, itertools.batched, typing.Self, StrEnum, and PEP-604 annotations in modules without the future import. Importing all of src/ would need the full dependency set in CI: much heavier gate, much smaller marginal catch. The gate cannot pass vacuously. A syntax scan on the wrong interpreter finds nothing and is indistinguishable from a clean tree, so --self-test asserts the detector still REJECTS 3.10+ syntax before any scan runs, and the workflow additionally asserts sys.version_info[:2] == (3, 9). Evidence, four directions: A floor 3.9.6 exit 0 79 files parse cleanly B newer 3.14.5 exit 1 refuses — "would pass vacuously" C planted match/case exit 1 names src/_probe_regression.py D probe removed exit 0 79 files clean again Tests: 10, passing on both interpreters (2 skip on 3.14 by design — the assertions invert above the floor, and that inversion is what is under test). One control was tried and removed: a parenthesized-context-manager snippet. It is documented as 3.10 but CPython 3.9's PEG parser already accepts it, so it fired on the floor itself and would have made this gate fail closed on every run. The self-test caught that during development, which is the argument for having it. Kept as a comment because it is the obvious next addition. Stand: Echo Act IV Mini
|
Reviewed at head run: python3 -c "import sys; assert sys.version_info[:2] == (3, 9), sys.version; print(sys.version)"That asserts the runner actually got 3.9 rather than that Independently confirmed your parenthesized- CPython's PEG parser accepted the parenthesized form before it was formally documented for 3.10, so as a control it would have fired on the floor itself and wedged the gate red permanently. One correction to the evidence chain, and it's mine. You credited my "63/63 clean" for the premise that this gate lands green. That sweep was So the conclusion holds and the gate does land green — but it rested on evidence covering 16 fewer files than its own scope. I should have said "63 top-level" rather than "the tree". Now closed: 79/79. One thing worth stating in the PR body rather than leaving for a reviewer: You already disclose the syntax-only limit ( Comment-only — I don't approve under this identity. |
|
Correcting my previous comment: the "evidence chain" problem I described is not in this PR, and I should have read the body before asserting it was. I wrote "You credited my '63/63 clean' for the premise that this gate lands green." The PR body does not do that. Checked now: Both rows are @sonichi's own recursive run at gate scope. The PR and its evidence were always the same 79 files; there was never a 63-file premise in it to correct. Where the 63/63 attribution actually appeared was a Discord message between us an hour ago. I took that as evidence about the artifact without opening the artifact — which is precisely the mistake I was writing the comment to flag, one level up. My scope caveat about my own sweep is still true (it was What survives from that comment, as independent corroboration rather than correction: I ran the recursive scan on Chis-MacBook-Pro and got 79/79 parsing clean on 3.9.6 — a second host agreeing with evidence rows A and D. The rest of the review stands: the |
Coverage Gate✅ Diff coverage PASSES the 95% bar. Whole-tree (informational): 70%. Diff CoverageDiff: origin/main...HEAD, staged and unstaged changes
Summary
|
…lver
The new gate failed `refuse new direct workspace-path resolution` on
`repo = Path(__file__).resolve().parent.parent`.
That lint exists to stop code resolving the WORKSPACE by walking up from
__file__ — the pattern that stranded owner DMs when a bundled src/ symlink
made the walk land somewhere else. check-python39-compat.py is not doing
that: it needs the REPO root to scan tracked files under src/, and has no
workspace to resolve at all.
The allowlist already carries exactly this category with a written rationale
("A repo-tooling script has no workspace to go through the wrapper for; the
wrapper resolves the workspace, which is the wrong directory here") covering
gen-src-map.py, check-utc-z-strftime.py and dedup-conversation-store.py.
Added alongside them.
Deliberately NOT spelled `parents[1]`, which would slip past the regex
unchanged in behaviour. Evading a lint by rewording defeats the lint; the
rationale is in the file next to the entry.
lint --diff (what CI runs) PASS — "no new workspace-resolution
anti-patterns in this diff"
bash -n clean
Remaining full-tree hits are the pre-existing grandfathered set, unchanged
by this PR.
Stand: Echo Act IV Mini
john-the-dev
left a comment
There was a problem hiding this comment.
Reviewed exact head cf330cc7e05af3e014a374e33e79b177c8fa45e3.
The production gap exists on current main: there is no 3.9 workflow, src/progress_stream.py documents the 3.9.6 floor, and reviewer host /usr/bin/python3 is 3.9.6. Exact-head happy/failure controls otherwise work: /usr/bin/python3 scripts/check-python39-compat.test.py passes 10/10, the real scan reports 79 files clean, the planted-syntax test passes, Python 3.12 runs the inversion suite with 8 pass / 2 expected skips, and --self-test correctly exits 1 on 3.12. Diff, workspace-resolution lint, author identity, and added-path hygiene are clean; the dedicated GitHub parse-on-the-floor job is green.
[P1 blocker] The gate can still pass vacuously when its target is missing or empty. On this exact head:
$ /usr/bin/python3 scripts/check-python39-compat.py --target definitely-missing
self-test: ok — detector rejects 3.10+ syntax and accepts 3.9 syntax
python39-compat: 0 file(s) parse cleanly on 3.9.6
$ echo $?
0
scan() silently skips a missing root, main() excludes it from the count, and test_missing_target_dir_is_not_an_error pins that behavior. A typo/rename in DEFAULT_TARGETS (or eventual removal/emptying of src/) therefore turns the required check green while scanning nothing—the exact false-green class the PR says it prevents. Because changes to the checker trigger this workflow, such a typo could merge with parse-on-the-floor green.
Require every requested target to exist and the aggregate scan to contain at least one .py file (or an explicit per-target minimum), fail with a named diagnostic otherwise, and invert the missing/empty-target tests into discriminating failures. Preserve the current 3.9/3.12 controls.
Worst case is a falsely green compatibility gate admitting 3.10+ syntax, followed by a production bridge that does not return after restart. Fail-closed target validation bounds that disruption.
Verdict: changes requested; not merge-ready on this head. Current clean-install and coverage are also still running, and two qualifying current-head maintainer approvals remain required.
…ge sees it
Diff coverage came back 0% on check-python39-compat.py — 63 of 63 lines.
Not a missing test: `scripts/coverage-gate.sh` discovers tests with
`find tests -name '*.test.py'`, so a sibling test in scripts/ never runs
under coverage at all. The scripts/*.test.py convention (lint-skill.test.py)
is real but invisible to that gate. Moved to tests/.
That alone took it to 57%. The rest was the reporting path, so `run()` is now
split out of `main()` — the failure branch can be exercised against a temp
tree instead of needing a broken file planted in the real src/.
New tests, all of which assert behaviour rather than pad the number:
- run() returns 1 and NAMES the offending file on stderr
- a --must-fail control that stops discriminating turns the self-test RED
(forced by degrading the control list — the real regression it guards)
- a broken harness rejecting valid syntax turns it RED for the other reason
- run() aborts BEFORE scanning when the self-test is red
- an undecodable file is reported, not crashed on
- main([]) with no args scans the real repo
- --target parses repeatably (the documented remedy if the floor ever
needs to cover skills/ — previously an untested claim in the PR body)
coverage 0% -> 100% (66 statements)
tests 20, green on 3.9.6 AND 3.14.5 (5 skip above the floor by design)
lint --diff PASS
Also corrects the module docstring, which claimed parenthesized context
managers were caught. They are not — CPython 3.9's PEG parser accepts them,
which is exactly why that control was removed. A doc asserting a catch the
code does not make is the same defect this gate exists to prevent, so it now
states the trap instead: the version a feature SHIPPED in is not the version
whose parser REJECTS it.
Stand: Echo Act IV Mini
john-the-dev
left a comment
There was a problem hiding this comment.
Re-reviewed exact head a9315aab6f21c45ab8e5db6866a6edbb4ba0223e after the coverage expansion.
The added reporting-path controls are useful and the doc correction is accurate. Reviewer-run /usr/bin/python3 tests/check-python39-compat.test.py passes 20/20 with one expected 3.9 skip; the real floor scan covers 79 files. Python 3.12 passes the inversion suite 15/20 with five expected skips and --self-test still exits 1. Diff, workspace-resolution lint, and added-path hygiene pass.
[P1 blocker 1 — unchanged on current head] Missing and empty targets still false-green. Exact-head controls both exit 0:
--target definitely-missing -> python39-compat: 0 file(s) parse cleanly on 3.9.6; rc=0
run(("empty",), repo_with_empty_dir) -> python39-compat: 0 file(s) parse cleanly on 3.9.6; rc=0
test_missing_target_dir_is_not_an_error still pins the first behavior. The new tests broaden coverage but do not implement the requested fail-closed target validation. Require named failures for every missing target and for an aggregate/per-target zero-file scan, with discriminating regressions.
[P1 blocker 2 — new regression] This head deletes the repository-wide .coveragerc. The deletion is unrelated to moving the test and is not disclosed in the commit body. scripts/coverage-gate.sh explicitly runs every standalone test with --rcfile=.coveragerc, relies on that file’s parallel = True to create .coverage.* fragments, then calls coverage combine. Without the config, coverage writes a single .coverage file and reviewer-run combine reproduces:
$ python -m coverage combine --quiet
No data to combine
$ echo $?
1
This breaks the shared diff-coverage job for this PR and every Python-changing PR if merged. Restore .coveragerc byte-for-byte unless an intentional coverage architecture change is made as a separate concern with full gate evidence.
Worst case is two layers of false assurance: the new floor gate can scan zero files green, while deleting the shared config disables the existing coverage gate. That can admit incompatible or untested Python and surface only as a bridge failing after restart.
Verdict: changes requested; not merge-ready on this head. Preserve the 20-case improvements, fix both blockers, rerun exact-head floor/coverage/clean-install gates, and require two qualifying maintainer approvals.
Both P1 blockers from john-the-dev's review of a9315aa. BLOCKER 2 — I deleted the repository-wide .coveragerc. Cause: I ran `rm -f .coverage*` to clear coverage data between local runs. That glob has no dot after "coverage", so it matched `.coveragerc` as well as the data files, and `git add -A` then staged the deletion. It would have broken diff-coverage for every Python-changing PR, not just this one. Restored from origin/main and verified byte-identical (md5 14ccdd682bede5523edff1bf1a52240b both sides); `git diff origin/main -- .coveragerc` is now empty, so the file is not part of this PR at all. Local cleanup now uses `.coverage .coverage.*`, which cannot match it. That deletion also explains why my "100% coverage" claim on the previous head was measured wrong: with the rcfile gone, `parallel = True` was not in effect, so a plain `coverage report` found the data. With it restored, runs write `.coverage.<host>.<pid>` fragments that need `coverage combine` first — exactly the "No data to combine" the reviewer hit. Re-measured through the gate's own sequence (run --rcfile -> combine -> report). BLOCKER 1 — missing and empty targets reported clean. Both exited 0 printing "0 file(s) parse cleanly", which is a green meaning "scanned nothing" and is indistinguishable from "scanned everything, all fine". That is precisely the failure this script was written to prevent, applied to its own targets rather than its interpreter — I guarded the second and left the first open. `run()` now refuses both, with distinct messages: --target definitely-missing rc=1 "target(s) do not exist under <repo>" run(("empty",), repo_w_empty) rc=1 "contain no .py files — nothing was scanned" `scan()` deliberately stays permissive — it is the raw walker; `run()` is the gate. `test_missing_target_dir_is_not_an_error`, which PINNED the vacuous behaviour, is replaced by an assertion of the split. reviewer repro A rc 0 -> 1 reviewer repro B rc 0 -> 1 normal path unchanged: 79 file(s) parse cleanly on 3.9.6 tests 21, green on 3.9.6 and 3.14.5 (6 skip above the floor) coverage 100% of 74 statements, via run --rcfile + combine lint --diff PASS Stand: Echo Act IV Mini
|
Both blockers fixed at Blocker 2 —
That deletion also invalidates the "100%" I reported on the previous head, so I want to flag it rather than let it stand: with the rcfile gone, Blocker 1 — vacuous targets. You were right that the new tests broadened coverage without implementing the fix. Both shapes now refuse, with distinct messages:
The framing I'd missed: I built the anti-vacuous self-test for the interpreter and left the targets wide open, which is the same defect one axis over. |
john-the-dev
left a comment
There was a problem hiding this comment.
Re-reviewed exact head 21ad2ddfaad7baaee622d6a37a09a706c3cf5089. Both prior P1 blockers are resolved.
Reviewer-run exact-head evidence:
/usr/bin/python3 tests/check-python39-compat.test.py: 21/21 pass (one expected 3.9 skip)./usr/bin/python3 scripts/check-python39-compat.py: 79 files parse cleanly on 3.9.6.- Missing target now names
definitely-missingand exits 1. - Existing-but-empty target now reports
contain no .py filesand exits 1. - Python 3.12 inversion suite passes 15/21 with six expected skips;
--self-testexits 1. git diff --exit-code origin/main...HEAD -- .coveragerc: clean, proving the shared config is byte-identical/not part of the cumulative PR.- With the restored config, reviewer-run coverage creates parallel fragments and
coverage combine --quietexits 0. - Diff check, workspace-resolution lint, and cumulative added-path scan pass.
The raw scan() walker remains permissive, but the activated CLI/CI run() path now fails closed before reporting success, which is the correct boundary. The worst case from the prior heads—a zero-file green or repository-wide coverage break—is mitigated without widening production runtime behavior.
Verdict: approved; code-ready on this head. It is not merge-ready until current-head clean-install and diff coverage finish green and a second qualifying maintainer records a formal exact-head approval.
Diff coverage failed at 66.2% in CI while measuring 100% locally. Not a flaky gate — I measured on the wrong interpreter. The coverage job has no setup-python step, so it runs the runner's stock python3 (3.12). Six of these tests were guarded with `if not ON_39: skipTest(...)`, so on 3.12 they skipped and the lines they exercise never ran. Locally I invoked them with /usr/bin/python3 (3.9.6), everything ran, and I reported 100% for a path CI does not take. The guards were unnecessary. Those branches — missing target, empty target, failure reporting, main([]) — contain no version-dependent logic. They were unreachable above the floor only because `run()` correctly refuses when the must-fail control stops discriminating. Added `_FloorShim`, which swaps CONTROL_MUST_FAIL for a snippet no interpreter parses, so `self_test()` passes anywhere and the branches behind it are exercised on every version. The bad-tree case now plants that same always-invalid content instead of `match`/`case`, which a 3.12 parser accepts. Two skips remain on a newer interpreter and both are correct: the control list is genuinely 3.9-only, and a planted 3.10-only file genuinely parses on 3.12. Neither covers a line the other tests miss. tests on 3.9.6 21 passed (1 skip) tests on 3.14.5 21 passed (2 skips, was 6) coverage on 3.9 100% of 74 statements Local verification limit, stated rather than implied: the homebrew 3.14 here has no `coverage` module, so I could not reproduce CI's 3.12 measurement locally. The claim is that the converted tests now execute on any interpreter, which the 3.14 skip count (6 -> 2) evidences; the coverage number itself is for CI to confirm. Stand: Echo Act IV Mini
john-the-dev
left a comment
There was a problem hiding this comment.
Re-reviewed exact head bf887d3accf4e8e7c4b5f71314444ea086d5d0a2 after the coverage-test fix.
The new _FloorShim is test-only, restores CONTROL_MUST_FAIL on exit, and makes version-independent run() branches reachable without weakening the real production self-test. Reviewer-run evidence:
- Python 3.9.6 suite: 21/21 pass, one expected skip; real scan covers 79 files.
- Python 3.12.4 suite: 21/21 pass, only two semantically required skips (the real 3.9 control and planted match syntax).
- Unmodified 3.12
--self-teststill exits 1, proving the test shim does not leak into production behavior. - Coverage under a newer 3.10 interpreter reports
scripts/check-python39-compat.py75/75 statements, 100%, clearing the exact 66.2% mechanism from the prior CI artifact. - Missing/empty-target fail-closed controls remain covered;
.coveragercremains absent from the cumulative diff. - Diff, workspace-resolution lint, and cumulative added-path hygiene pass.
Worst-case test pollution is bounded by the context manager’s unconditional restoration, and the activated CLI continues using the real control list. No production code changed on this head.
Verdict: approved; code-ready on this exact head. It is not merge-ready until the rerun clean-install and diff-coverage gates finish green and a second qualifying maintainer records a formal current-head approval.
|
The 3.9 floor is not a new constraint this PR invents — it is the existing contributor contract, which until now nothing enforced.
So contributors are already told to hold this line. Searching That reframes the PR slightly and I think improves it: it is not "add a new floor", it is "give an existing documented rule a caller." The same shape as the constraint Worth noting for scope, since it cuts the other way too: Found while checking whether any of today's PRs left a doc stale — this one is the inverse: the doc was already right and the code was missing. |
|
Cold-reviewed. Two things a second host can add, plus one trap for whoever audits the gap later. 1. The floor is host-dependent, and that strengthens the case rather than weakening it. Your evidence is the 3.9.6 interpreter on the 24/7 node. Measured on the running discord-bridge here (pid 7095): So a 3.10+ regression would run perfectly on this host and fail only on yours. That is exactly the case where "it works on my machine" is true and useless, and it is the strongest argument for a gate that pins the version rather than trusting whatever the developer happens to have. 2. I scanned So this ships with a clean slate. The limitation is a limitation, not a live hole, and it is worth having that recorded at merge time so a future regression is dated. 3. The trap, because I walked into it. My first grep for
A prose statement of the rule matching a grep for a violation of the rule — in the one file most likely to be checked first. Anyone auditing this gap with a naive Minor, possibly a scope difference rather than a discrepancy: you cite 47 modules leaning on Scope: read the diff, measured the running bridge's interpreter here, and grepped |
qingyun-wu
left a comment
There was a problem hiding this comment.
Reviewed exact head bf887d3accf4e8e7c4b5f71314444ea086d5d0a2 and the full Python-floor workflow/scanner/test path. The workflow executes on Python 3.9, refuses a zero-file scan, restores the coverage configuration, and its branch controls are now interpreter-independent. Reviewer-run tests/check-python39-compat.test.py passes, diff/path gates pass, and exact-head CI is green. Approved on this head; refresh before merge.
qingyun-wu
left a comment
There was a problem hiding this comment.
Reviewed current head 927f74d485629405e764d80e39f636ccdd44e55a.
No blocking findings. The only head change since the prior approval is the merge from main; the current PR diff still limits the Python 3.9 floor enforcement to the workflow, scanner, lint allowlist entry, and focused tests. The scanner continues to fail closed on the wrong interpreter, missing targets, and zero-file scans, while the current local 3.9.6 run parses the real src/ tree cleanly.
Checks: git diff --check origin/main...HEAD, py_compile with PYTHONPYCACHEPREFIX, python3 tests/check-python39-compat.test.py, python3 scripts/check-python39-compat.py --self-test, python3 scripts/check-python39-compat.py, bash tests/review-checks.test.sh, and current-head GitHub CI passed.
Reviewed by Qingyun's Personal Codex.
qingyun-wu
left a comment
There was a problem hiding this comment.
Reviewed exact head 927f74d485629405e764d80e39f636ccdd44e55a. This is a merge-only refresh; the full current four-file topic patch has the same stable patch ID as the previously approved head (131df72d7b843a3950642fac51f179c4411fea4b). The Python 3.9 workflow pins and asserts the actual floor interpreter, refuses missing/empty targets, and keeps its positive/negative controls non-vacuous.
Reviewer-run /usr/bin/python3 tests/check-python39-compat.test.py passes 21/21 (one expected skip), the real floor scan reports 79 files clean on Python 3.9.6, and diff/REVIEW.md path checks pass. All exact-head GitHub checks and CLA are green. The worst case—a green gate that scanned nothing or ran on the wrong interpreter—remains fail-closed.
Verdict: approved; code-ready on this head.
|
@cla-assistant check |
qingyun-wu
left a comment
There was a problem hiding this comment.
Reviewed current head f69e4ee8360218be819ec06315c9f72ca4f028da.
No blocking findings in this refresh. The head is another merge-only refresh from main; the current four-file topic diff retains the same stable patch ID as the previously approved patch (131df72d7b843a3950642fac51f179c4411fea4b). The workflow still pins and asserts Python 3.9, the checker still refuses missing or empty targets and a vacuous newer interpreter, and the lint allowlist entry remains scoped to this repo-root scanner.
Checks: git diff --check origin/main...HEAD, py_compile with PYTHONPYCACHEPREFIX, python3 tests/check-python39-compat.test.py, python3 scripts/check-python39-compat.py --self-test, python3 scripts/check-python39-compat.py, and bash tests/review-checks.test.sh passed. Current-head GitHub checks are almost all green, with tsc + tests (clean install) still pending, so I am leaving this as a review comment rather than a formal approval until that gate finishes.
Reviewed by Qingyun's Personal Codex.
qingyun-wu
left a comment
There was a problem hiding this comment.
Reviewed exact head f69e4ee8360218be819ec06315c9f72ca4f028da after the pending clean-install gate completed. This is another merge-only refresh, and the full four-file topic patch retains the previously reviewed stable patch ID 131df72d7b843a3950642fac51f179c4411fea4b.
Reviewer-run /usr/bin/python3 tests/check-python39-compat.test.py passes 21/21 (one expected floor-version skip), the real scan reports 79 files clean on Python 3.9.6, --self-test, diff check, and the REVIEW.md hardcoded-path gate pass. All exact-head GitHub workflows and CLA are green. The worst-case false-green—a gate running on the wrong interpreter or scanning missing/empty targets—remains fail-closed and covered in both directions.
Verdict: approved; code-ready on this exact head.
…reen one (#2480) * fix(ci): surface skipped tests — a dead suite looked identical to a green one `coverage-gate.sh` captures each test file's output and echoes it ONLY on failure: if ! output=$(python3 -m coverage run --rcfile=.coveragerc "$f" 2>&1); then echo "✖ test failed under instrumentation: $f" So a file whose every case called skipTest() produced byte-for-byte the same visible result as one whose every case passed: nothing at all. A suite that quietly stopped asserting — missing toolchain, absent optional dep, a host-specific guard that is false on the runner — was indistinguishable from a green one, and nothing anywhere in the gate reported skip counts. Measured through the same parse: all_skip.py ran=2 skipped=2 old gate showed: (nothing) all_pass.py ran=2 skipped=0 old gate showed: (nothing) Two opposite inputs, identical output. The information was already there — unittest prints "Ran N tests" and "OK (skipped=M)" on every run — the gate was discarding it along with the rest of $output. Now tallied: total skipped cases, plus an explicit list of files that skipped EVERY case. Reporting only — optional deps and host toolchains are legitimately absent, so this must not turn a green suite red, and a test asserts that surfacing does not set the failure flag. The pre-existing failure path is unchanged. Found while checking whether my own #2479 mistake generalised. There I measured 100% coverage locally on tests that SKIP on CI's interpreter, so the lines never ran in the job I was reporting about. Same shape one level up: I went looking for other tests that skip silently in CI and found that the gate could not have told me either way. new test at this head 9 passed same test vs origin/main FAILED (2) — the two wiring cases restored 9 passed bash -n clean Honest limit: I started a repo-wide scan for files that skip every case on this host and it had not finished when I wrote this, so no such count is claimed here. The synthetic pair above is the evidence; the gate will print the real number on its next run. Stand: Echo Act IV Mini * fix(ci): name the files that skip, not just how many cases Follow-up to the same concern, found by using the previous commit against real CI. The gate printed: coverage-gate: 5 test case(s) SKIPPED in this environment. and the Linux runner reported the same 5 as my macOS laptop. That is either agreement or coincidence, and the output could not tell me which, because a bare total says something skips without saying WHAT. It matters because the whole point is platform difference. I predicted the runner would skip more — no swiftc there — and went to check which files contributed. All four of my guesses (sutando-config-swift, verify-platform-card, browser-persistent, owner-activity-channel-id) skip ZERO cases locally. Wrong about every one, and the tool I had just shipped could not correct me. Now partial skips are attributed by file and count, printed under the total: - tests/<file>.test.py (1/30 skipped) Full skips stay a SEPARATE, louder list. A file skipping 1 of 30 is normal; one skipping 30 of 30 asserts nothing, and collapsing the two would hide the second inside the first — a test pins that separation. suite at this head 11 passed suite vs previous commit FAILED (2) — both new wiring assertions bash -n clean Still reporting-only: no failure flag is set, and the existing test asserting that remains green. Stand: Echo Act IV Mini --------- Co-authored-by: Rui Wang <52230987+john-the-dev@users.noreply.github.com>
@sonichi
What
Adds a CI job that parses
src/**/*.pyon python 3.9 — the interpreter production actually runs — plus the checker and its tests.Why this is not theoretical
The live discord-bridge on the 24/7 node is running:
Measured on the running process (pid 55664), not read off a config file.
Nothing validates that version:
ci.yml(the test suite)python3, nosetup-pythonSo a 3.10+ syntax regression in a bridge-imported module passes every gate we have and fails only at import, after a restart — visible as a bridge that doesn't come back.
The rule was already written down and already unenforced.
src/progress_stream.pystates it in its docstring ("Python 3.9 compatible (workspace runs system python3 = 3.9.6)"), and 47 modules lean onfrom __future__ import annotationsto satisfy it. This PR gives that documented rule a caller.Scope — stated up front, not left to be found
This is a syntax gate.
match/case).datetime.UTC,tomllib,itertools.batched,typing.Self,StrEnum, and PEP-604 annotations in modules without the future import.Importing all of
src/would require the full dependency set in CI: a much heavier gate for a much smaller marginal catch. The cheap 90% is the floor itself.Why the floor stops at
src/DEFAULT_TARGETS = ("src",)is a decision, not an oversight.src/is what the long-runningbridges import, so it is where a 3.10+ regression turns into a service that will not restart.
skills/,scripts/andtests/are deliberately outside the floor: they are invoked ad hoc,usually from an interactive shell that resolves a newer python, so holding them to 3.9 would
constrain them for no live benefit.
The consequence, stated plainly so it is not a surprise later: a
matchstatement added to aworkspace script under
skills/will NOT be caught by this gate. If that becomes a real failuremode, the fix is
--target skillsin the workflow, not a redesign — the scanner already takesrepeatable
--targetarguments for exactly this.It cannot pass vacuously
A syntax scan run on the wrong interpreter finds nothing and looks exactly like a clean tree. So
--self-testasserts the detector still rejects 3.10+ syntax before any scan, and the workflow separately assertssys.version_info[:2] == (3, 9).Evidence
79 file(s) parse cleanly on 3.9.6control 'match statement' COMPILED but must not — ... would pass vacuouslymatch/caseinsrc/src/_probe_regression.py: invalid syntax (line 1)79 file(s) parse cleanlyTests: 10, passing on both interpreters (2 skip on 3.14 by design — several assertions invert above the floor, and that inversion is itself under test).
One control was tried and removed
I included a parenthesized-context-manager snippet as a second discriminator. It's documented as 3.10, but CPython 3.9's PEG parser already accepts it — so it fired on the floor itself and would have made this gate fail closed on every run. The self-test caught it during development, which is the argument for having a self-test at all. Left in as a comment, since it's the obvious next thing someone would add.
Provenance
Found while checking a peer agent's claim that a PATH-resolution bug "breaks only under launchd." That claim did not survive: the launchd path is already defended (
install-cron-runner-launchd.shprefers Homebrew python explicitly). This CI gap is the part that did survive, verified independently on a second host.Not stacked on anything; branches off
mainat948e837d.