From 13aaa221ad2cf6d2c3f744b564371eb6c0bea984 Mon Sep 17 00:00:00 2001 From: golem Date: Mon, 5 Oct 2026 14:18:43 -0600 Subject: [PATCH 1/9] test: smoke-test script for installed artifacts (catches the 0.2.2 import crash) Co-Authored-By: Claude Opus 5.5 (1M context) --- scripts/smoke_import.py | 54 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 scripts/smoke_import.py diff --git a/scripts/smoke_import.py b/scripts/smoke_import.py new file mode 100644 index 0000000..caa7ce4 --- /dev/null +++ b/scripts/smoke_import.py @@ -0,0 +1,54 @@ +"""Smoke-test an *installed* app_skellington (wheel, sdist or index release). + +Run it from outside the source tree, so the installed copy is the one imported: + + cd / && python /path/to/scripts/smoke_import.py [EXPECTED_VERSION] +""" + +import importlib.metadata +import os +import pathlib +import subprocess +import sys + + +def fail(msg): + sys.exit("smoke FAILED: " + msg) + + +def main(argv): + expected = argv[1] if len(argv) > 1 else None + + import app_skellington + + location = pathlib.Path(app_skellington.__file__).resolve() + if "site-packages" not in location.parts: + fail("imported the source tree, not an installed copy: %s" % location) + + from app_skellington import ( # noqa: F401 (the public API must import) + ApplicationContainer, + CommandTree, + Config, + LoggingLayer, + NoCommandSpecified, + ServiceNotFound, + SubMenu, + ) + + version = importlib.metadata.version("app_skellington") + if version.startswith("0.0"): + fail("setuptools_scm found no tags (shallow clone?): %s" % version) + if expected is not None and version != expected: + fail("expected version %s, got %s" % (expected, version)) + + # _bootstrap.py takes a different code path when APPSKELLINGTON_DEBUG is set. + env = dict(os.environ, APPSKELLINGTON_DEBUG="1") + proc = subprocess.run([sys.executable, "-c", "import app_skellington"], env=env) + if proc.returncode != 0: + fail("import failed with APPSKELLINGTON_DEBUG=1") + + print("smoke ok:", version, location) + + +if __name__ == "__main__": + main(sys.argv) From c361b286c95b1cb3f7e5c421dafd462e9aaa15ea Mon Sep 17 00:00:00 2001 From: golem Date: Mon, 5 Oct 2026 14:20:10 -0600 Subject: [PATCH 2/9] =?UTF-8?q?ci:=20Gitea=20checks=20=E2=80=94=20ruff,=20?= =?UTF-8?q?CPython=203.8=E2=80=933.14=20matrix,=20wheel+sdist=20smoke=20te?= =?UTF-8?q?sts,=20ci-ok=20aggregate?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 (1M context) --- .gitea/workflows/ci.yaml | 88 ++++++++++++++++++++++++++++ CLAUDE.md | 9 +++ README.md | 2 + pyproject.toml | 11 +++- tests/test_ci_workflow.py | 118 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 226 insertions(+), 2 deletions(-) create mode 100644 .gitea/workflows/ci.yaml create mode 100644 tests/test_ci_workflow.py diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml new file mode 100644 index 0000000..f25bf11 --- /dev/null +++ b/.gitea/workflows/ci.yaml @@ -0,0 +1,88 @@ +name: CI +on: + pull_request: + types: [opened, synchronize, reopened, edited] # edited: un-WIP starts CI + push: + branches: [develop, main] + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + # Cancel superseded PR runs. Never cancel a push run. + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +jobs: + lint: + if: >- + github.event_name != 'pull_request' || ( + !startsWith(github.event.pull_request.title, 'WIP:') && + !startsWith(github.event.pull_request.title, '[WIP]') + ) + runs-on: ubuntu-latest + container: python:3.14 + env: + UV_PYTHON_DOWNLOADS: never + steps: + - name: Install node (Gitea actions are node programs) + run: apt-get update && apt-get install -y --no-install-recommends nodejs + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - run: git config --global --add safe.directory "$PWD" + - run: pip install uv + - run: uv sync --locked + - run: uv run ruff check + - run: uv run ruff format --check + + test: + if: >- + github.event_name != 'pull_request' || ( + !startsWith(github.event.pull_request.title, 'WIP:') && + !startsWith(github.event.pull_request.title, '[WIP]') + ) + runs-on: ubuntu-latest + container: python:${{ matrix.python }} + strategy: + fail-fast: false + matrix: + python: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] + env: + UV_PYTHON: ${{ matrix.python }} + UV_PYTHON_DOWNLOADS: never + steps: + - name: Install node (Gitea actions are node programs) + run: apt-get update && apt-get install -y --no-install-recommends nodejs + - uses: actions/checkout@v4 + with: + fetch-depth: 0 # setuptools_scm needs tags + - run: git config --global --add safe.directory "$PWD" + - run: pip install uv + - run: uv sync --locked + - run: uv run pytest + - name: Build, install and smoke-test the wheel and the sdist + run: | + rm -rf dist && uv build + for dist in dist/*.whl dist/*.tar.gz; do + rm -rf /tmp/smoke && uv venv /tmp/smoke + uv pip install --python /tmp/smoke "$dist" + (cd / && /tmp/smoke/bin/python "$GITHUB_WORKSPACE/scripts/smoke_import.py") + done + + ci-ok: + needs: [lint, test] + # always(): a plain needs: aggregate would be *skipped* when a gate fails, + # and Gitea reports skipped as success. The WIP gate stays. + if: >- + always() && ( + github.event_name != 'pull_request' || ( + !startsWith(github.event.pull_request.title, 'WIP:') && + !startsWith(github.event.pull_request.title, '[WIP]') + ) + ) + runs-on: ubuntu-latest + steps: + - name: Require every gate to have succeeded + run: | + echo "lint=${{ needs.lint.result }} test=${{ needs.test.result }}" + test "${{ needs.lint.result }}" = success + test "${{ needs.test.result }}" = success diff --git a/CLAUDE.md b/CLAUDE.md index ef17e0d..9332627 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -30,6 +30,15 @@ uv lock # after any dependency change; commit uv.lock `requires-python` is `>=3.8`, and the claim should match what CI actually tests. When an end-of-life Python version starts failing, drop it: raise `requires-python` and remove its CI lane. Don't add compat shims, version branches or contorted code just to keep it working. Clean code matters more than supporting EOL runtimes. Users on old Pythons keep the older releases on PyPI. +## CI + +`.gitea/workflows/ci.yaml` runs on PRs and on pushes to `develop`/`main`. PRs titled `WIP:` are skipped; removing the prefix starts the run. +- `lint`: ruff. +- `test`: a matrix over CPython 3.8–3.14 in `python:` containers. Each lane runs the locked suite, then builds the wheel and sdist, installs each into a clean venv, and runs `scripts/smoke_import.py` from outside the source tree. +- `ci-ok`: the only required check (`CI / ci-ok (pull_request)`). It uses `if: always()` and fails unless every gate succeeded, because Gitea reports a skipped job as success. + +`tests/test_ci_workflow.py` pins these invariants. To add or drop a Python version, change all four together: the matrix, the classifiers, `scripts/verify_index_release.sh`'s list, and `NEWEST_MINOR` (or `requires-python`). + ## Architecture Package layout: `_bootstrap.py`, `_util.py`, `cfg.py`, `log.py`, `cli.py` and `app_container.py`. `__init__.py` re-exports the public names explicitly. diff --git a/README.md b/README.md index 3d43b6a..3150ee4 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,8 @@ Application framework for Python, features include: - Colored logging (provided through colorlog) - Linux supported. Windows and macOS are goals, but untested. +**Tested:** Linux, CPython 3.8–3.14 (Gitea CI on every PR). Windows and macOS: goals, untested. + Principles: - Lend to creating beautiful, easy to read and understand code in the application. - Minimize coupling of applications to this framework. diff --git a/pyproject.toml b/pyproject.toml index 9bb1998..97e80d4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,8 +20,15 @@ authors = [ keywords = ["cli", "logging", "application"] classifiers = [ "Programming Language :: Python :: 3", - "License :: OSI Approved :: MIT No Attribution License (MIT-0)", - "Operating System :: OS Independent" + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", + "Operating System :: POSIX :: Linux", + "License :: OSI Approved :: MIT No Attribution License (MIT-0)" ] diff --git a/tests/test_ci_workflow.py b/tests/test_ci_workflow.py new file mode 100644 index 0000000..710b1dd --- /dev/null +++ b/tests/test_ci_workflow.py @@ -0,0 +1,118 @@ +"""Pins the CI workflow's invariants (spec: docs/superpowers/specs/ +2026-10-04-modernization-design.md). CI drift shows up as a red test here.""" + +import pathlib +import re + +import pytest +import yaml + +ROOT = pathlib.Path(__file__).resolve().parent.parent +WORKFLOW = ROOT / ".gitea" / "workflows" / "ci.yaml" +VERIFY_SCRIPT = ROOT / "scripts" / "verify_index_release.sh" + +# Newest CPython the project claims. To add a version, bump this, the +# matrix, the classifiers and the verify script's list together. +NEWEST_MINOR = 14 + +WIP_CLAUSES = ( + "!startsWith(github.event.pull_request.title, 'WIP:')", + "!startsWith(github.event.pull_request.title, '[WIP]')", +) + + +def load(): + return yaml.safe_load(WORKFLOW.read_text()) + + +def triggers(wf): + # PyYAML follows YAML 1.1, where the bare key `on` parses as boolean True. + return wf["on"] if "on" in wf else wf[True] + + +def expected_versions(): + text = (ROOT / "pyproject.toml").read_text() + floor = int(re.search(r'requires-python\s*=\s*">=3\.(\d+)"', text).group(1)) + return ["3.%d" % minor for minor in range(floor, NEWEST_MINOR + 1)] + + +def flat(text): + return " ".join(text.split()) + + +def test_triggers(): + on = triggers(load()) + assert set(on["pull_request"]["types"]) == { + "opened", + "synchronize", + "reopened", + "edited", + } + assert on["push"]["branches"] == ["develop", "main"] + assert "workflow_dispatch" in on + + +def test_superseded_pr_runs_cancel_but_push_runs_never_do(): + assert ( + load()["concurrency"]["cancel-in-progress"] + == "${{ github.event_name == 'pull_request' }}" + ) + + +def test_every_job_is_wip_gated(): + for name, job in load()["jobs"].items(): + cond = flat(job.get("if", "")) + for clause in WIP_CLAUSES: + assert clause in cond, "%s lacks WIP gate clause %s" % (name, clause) + + +def test_matrix_matches_requires_python(): + test = load()["jobs"]["test"] + assert test["strategy"]["matrix"]["python"] == expected_versions() + assert test["strategy"]["fail-fast"] is False + + +def test_classifiers_match_matrix(): + text = (ROOT / "pyproject.toml").read_text() + claimed = re.findall(r'"Programming Language :: Python :: (3\.\d+)"', text) + assert claimed == expected_versions() + + +@pytest.mark.skipif(not VERIFY_SCRIPT.exists(), reason="added with the release task") +def test_verify_script_covers_matrix(): + match = re.search(r"^pythons=\(([^)]*)\)", VERIFY_SCRIPT.read_text(), re.M) + assert match.group(1).split() == expected_versions() + + +def test_checkout_fetches_full_history_for_setuptools_scm(): + for name, job in load()["jobs"].items(): + for step in job.get("steps", []): + if step.get("uses", "").startswith("actions/checkout"): + assert step.get("with", {}).get("fetch-depth") == 0, name + + +def test_lint_job_runs_ruff(): + runs = flat(" ".join(s.get("run", "") for s in load()["jobs"]["lint"]["steps"])) + assert "uv run ruff check" in runs + assert "uv run ruff format --check" in runs + + +def test_test_lanes_run_locked_suite_and_smoke_test_both_artifacts(): + runs = flat(" ".join(s.get("run", "") for s in load()["jobs"]["test"]["steps"])) + assert "uv sync --locked" in runs + assert "uv run pytest" in runs + assert "uv build" in runs + assert "dist/*.whl dist/*.tar.gz" in runs + assert "scripts/smoke_import.py" in runs + + +def test_ci_ok_fails_unless_every_gate_succeeded(): + jobs = load()["jobs"] + ci_ok = jobs["ci-ok"] + # Gitea reports a skipped job as success, so ci-ok must run always() + # and check each result explicitly. + assert set(ci_ok["needs"]) == set(jobs) - {"ci-ok"} + assert flat(ci_ok["if"]).startswith("always() &&") + runs = " ".join(s.get("run", "") for s in ci_ok["steps"]) + for needed in ci_ok["needs"]: + assert 'test "${{ needs.%s.result }}" = success' % needed in runs From d79045d9aa30818f3b4187b1b63dc01b17fdcd67 Mon Sep 17 00:00:00 2001 From: golem Date: Mon, 5 Oct 2026 14:22:04 -0600 Subject: [PATCH 3/9] canary: unused import must turn lint and ci-ok red (reverted next) Co-Authored-By: Claude Opus 5.5 (1M context) --- app_skellington/_util.py | 1 + 1 file changed, 1 insertion(+) diff --git a/app_skellington/_util.py b/app_skellington/_util.py index 9222e53..d43402d 100644 --- a/app_skellington/_util.py +++ b/app_skellington/_util.py @@ -1,4 +1,5 @@ from __future__ import print_function +import json import inspect import os From 2e02acbbc19b16c53c23ef3470f92c60ef7b60d9 Mon Sep 17 00:00:00 2001 From: golem Date: Mon, 5 Oct 2026 14:23:59 -0600 Subject: [PATCH 4/9] Revert canary: unused import (lint canary proved lint + ci-ok go red, run 2682) Co-Authored-By: Claude Opus 5.5 (1M context) --- app_skellington/_util.py | 1 - 1 file changed, 1 deletion(-) diff --git a/app_skellington/_util.py b/app_skellington/_util.py index d43402d..9222e53 100644 --- a/app_skellington/_util.py +++ b/app_skellington/_util.py @@ -1,5 +1,4 @@ from __future__ import print_function -import json import inspect import os From db78a21e5ce7f3463d401981b35f5c5ab34617e3 Mon Sep 17 00:00:00 2001 From: golem Date: Mon, 5 Oct 2026 14:24:06 -0600 Subject: [PATCH 5/9] canary: failing test must turn every test lane and ci-ok red (reverted next) Co-Authored-By: Claude Opus 5.5 (1M context) --- tests/cfg/test_cfg.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/cfg/test_cfg.py b/tests/cfg/test_cfg.py index ee2074b..605ea94 100644 --- a/tests/cfg/test_cfg.py +++ b/tests/cfg/test_cfg.py @@ -90,3 +90,7 @@ class TestConfig_e2e: "expecting default from config.spec (didnt get)" ) assert cfg["app"]["sub_option"] == "def_sub", "expecting default for sub option" + + +def test_canary_must_fail(): + assert False From 6bd717f231ad4e830b685951803d5b8c3b98a84f Mon Sep 17 00:00:00 2001 From: golem Date: Mon, 5 Oct 2026 14:25:35 -0600 Subject: [PATCH 6/9] Revert canary: failing test (test canary proved every lane + ci-ok go red, run 2683) Co-Authored-By: Claude Opus 5.5 (1M context) --- tests/cfg/test_cfg.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/cfg/test_cfg.py b/tests/cfg/test_cfg.py index 605ea94..ee2074b 100644 --- a/tests/cfg/test_cfg.py +++ b/tests/cfg/test_cfg.py @@ -90,7 +90,3 @@ class TestConfig_e2e: "expecting default from config.spec (didnt get)" ) assert cfg["app"]["sub_option"] == "def_sub", "expecting default for sub option" - - -def test_canary_must_fail(): - assert False From 509be4c6bcccc62b0c53a70d6bc7a18e79605fa1 Mon Sep 17 00:00:00 2001 From: golem Date: Mon, 5 Oct 2026 14:30:41 -0600 Subject: [PATCH 7/9] fix(ci): smoke guard catches tagless builds; DEBUG import must hit the install; pin more workflow invariants Review findings on PR #13: - release-branch-semver gives 0.1.0.devN without tags, so the 0.0 guard never fired; check_version() now requires >= 0.2 (unit-tested) - the APPSKELLINGTON_DEBUG child import could resolve the source tree from the cwd and false-pass; it now reports its location and must match - test_ci_workflow: no continue-on-error, matrix python drives container and UV_PYTHON, WIP gate compared whole (mutation-tested) Co-Authored-By: Claude Opus 5.5 (1M context) --- scripts/smoke_import.py | 47 +++++++++++++++++++++++++++++++------- tests/test_ci_workflow.py | 21 +++++++++++++++-- tests/test_smoke_import.py | 35 ++++++++++++++++++++++++++++ 3 files changed, 93 insertions(+), 10 deletions(-) create mode 100644 tests/test_smoke_import.py diff --git a/scripts/smoke_import.py b/scripts/smoke_import.py index caa7ce4..c5ab3d9 100644 --- a/scripts/smoke_import.py +++ b/scripts/smoke_import.py @@ -1,21 +1,40 @@ """Smoke-test an *installed* app_skellington (wheel, sdist or index release). -Run it from outside the source tree, so the installed copy is the one imported: +Run it from outside the source tree: cd / && python /path/to/scripts/smoke_import.py [EXPECTED_VERSION] + +The script's own directory (scripts/) heads sys.path, so the main import +can't see the source tree. But the APPSKELLINGTON_DEBUG child process puts +its cwd first, which is why the cwd must be outside the repo. The child's +import location is checked too, so a wrong cwd fails instead of passing. """ import importlib.metadata import os import pathlib +import re import subprocess import sys +# Any real release is >= 0.2. With no tags reachable (a shallow clone), +# setuptools_scm's release-branch-semver scheme yields 0.1.0.devN, and other +# schemes yield 0.0.x. +MIN_RELEASE = (0, 2) + def fail(msg): sys.exit("smoke FAILED: " + msg) +def check_version(version, expected): + major_minor = tuple(int(n) for n in re.match(r"(\d+)\.(\d+)", version).groups()) + if major_minor < MIN_RELEASE: + fail("setuptools_scm found no tags (shallow clone?): %s" % version) + if expected is not None and version != expected: + fail("expected version %s, got %s" % (expected, version)) + + def main(argv): expected = argv[1] if len(argv) > 1 else None @@ -35,19 +54,31 @@ def main(argv): SubMenu, ) - version = importlib.metadata.version("app_skellington") - if version.startswith("0.0"): - fail("setuptools_scm found no tags (shallow clone?): %s" % version) - if expected is not None and version != expected: - fail("expected version %s, got %s" % (expected, version)) + check_version(importlib.metadata.version("app_skellington"), expected) # _bootstrap.py takes a different code path when APPSKELLINGTON_DEBUG is set. env = dict(os.environ, APPSKELLINGTON_DEBUG="1") - proc = subprocess.run([sys.executable, "-c", "import app_skellington"], env=env) + proc = subprocess.run( + [ + sys.executable, + "-c", + "import app_skellington, pathlib;" + "print(pathlib.Path(app_skellington.__file__).resolve())", + ], + env=env, + stdout=subprocess.PIPE, + universal_newlines=True, + ) if proc.returncode != 0: fail("import failed with APPSKELLINGTON_DEBUG=1") + child_location = proc.stdout.strip().splitlines()[-1] + if child_location != str(location): + fail( + "APPSKELLINGTON_DEBUG import resolved %s, not the installed %s" + " (run from outside the source tree)" % (child_location, location) + ) - print("smoke ok:", version, location) + print("smoke ok:", importlib.metadata.version("app_skellington"), location) if __name__ == "__main__": diff --git a/tests/test_ci_workflow.py b/tests/test_ci_workflow.py index 710b1dd..b086eec 100644 --- a/tests/test_ci_workflow.py +++ b/tests/test_ci_workflow.py @@ -60,10 +60,27 @@ def test_superseded_pr_runs_cancel_but_push_runs_never_do(): def test_every_job_is_wip_gated(): + # Compared whole, not by substring: `&&` turned into `||` must fail. + gate = "github.event_name != 'pull_request' || ( %s && %s )" % WIP_CLAUSES for name, job in load()["jobs"].items(): cond = flat(job.get("if", "")) - for clause in WIP_CLAUSES: - assert clause in cond, "%s lacks WIP gate clause %s" % (name, clause) + assert cond in (gate, "always() && ( %s )" % gate), name + + +def test_no_gate_can_be_softened_by_continue_on_error(): + # continue-on-error makes needs..result read success, so ci-ok + # would go green over a failure. + for name, job in load()["jobs"].items(): + assert "continue-on-error" not in job, name + for step in job.get("steps", []): + assert "continue-on-error" not in step, name + + +def test_each_lane_really_runs_its_matrix_python(): + test = load()["jobs"]["test"] + assert test["container"] == "python:${{ matrix.python }}" + assert test["env"]["UV_PYTHON"] == "${{ matrix.python }}" + assert test["env"]["UV_PYTHON_DOWNLOADS"] == "never" def test_matrix_matches_requires_python(): diff --git a/tests/test_smoke_import.py b/tests/test_smoke_import.py new file mode 100644 index 0000000..2535bc5 --- /dev/null +++ b/tests/test_smoke_import.py @@ -0,0 +1,35 @@ +"""Unit tests for scripts/smoke_import.py's version checks.""" + +import importlib.util +import pathlib + +import pytest + +SCRIPT = pathlib.Path(__file__).resolve().parent.parent / "scripts" / "smoke_import.py" +spec = importlib.util.spec_from_file_location("smoke_import", SCRIPT) +smoke_import = importlib.util.module_from_spec(spec) +spec.loader.exec_module(smoke_import) + + +@pytest.mark.parametrize( + "version", + [ + "0.0.0", + "0.0.1.dev3+gabc1234", + # release-branch-semver with no tags reachable (shallow clone): + "0.1.0.dev1+g6bd717f23", + ], +) +def test_tagless_versions_are_rejected(version): + with pytest.raises(SystemExit, match="no tags"): + smoke_import.check_version(version, expected=None) + + +@pytest.mark.parametrize("version", ["0.2.3", "0.3.0.dev23+gc361b286c", "1.0.0"]) +def test_tag_derived_versions_pass(version): + smoke_import.check_version(version, expected=None) + + +def test_expected_version_must_match_exactly(): + with pytest.raises(SystemExit, match="expected version 0.2.3"): + smoke_import.check_version("0.2.3.dev1", expected="0.2.3") From ee8dfe0bccd41ed1614a366b42b0a09313f770c1 Mon Sep 17 00:00:00 2001 From: golem Date: Mon, 5 Oct 2026 14:30:49 -0600 Subject: [PATCH 8/9] canary: fail only the 3.14 lane; ci-ok must still go red (reverted next) Co-Authored-By: Claude Opus 5.5 (1M context) --- tests/cfg/test_cfg.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/cfg/test_cfg.py b/tests/cfg/test_cfg.py index ee2074b..e92ec1d 100644 --- a/tests/cfg/test_cfg.py +++ b/tests/cfg/test_cfg.py @@ -90,3 +90,9 @@ class TestConfig_e2e: "expecting default from config.spec (didnt get)" ) assert cfg["app"]["sub_option"] == "def_sub", "expecting default for sub option" + + +def test_canary_fails_only_on_newest_lane(): + import sys + + assert sys.version_info < (3, 14) From 41f91ce2b6c195701ba26a364b4dee2493b6fa2d Mon Sep 17 00:00:00 2001 From: golem Date: Mon, 5 Oct 2026 15:32:34 -0600 Subject: [PATCH 9/9] Revert canary: 3.14-only failure (proved one red lane turns ci-ok red, run 2685) Co-Authored-By: Claude Opus 5.5 (1M context) --- tests/cfg/test_cfg.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/tests/cfg/test_cfg.py b/tests/cfg/test_cfg.py index e92ec1d..ee2074b 100644 --- a/tests/cfg/test_cfg.py +++ b/tests/cfg/test_cfg.py @@ -90,9 +90,3 @@ class TestConfig_e2e: "expecting default from config.spec (didnt get)" ) assert cfg["app"]["sub_option"] == "def_sub", "expecting default for sub option" - - -def test_canary_fails_only_on_newest_lane(): - import sys - - assert sys.version_info < (3, 14)