fix(ci): run tests/architecture/ guards on every PR, and fix the four they were hiding - #4469
Open
dieterolson wants to merge 5 commits into
Open
fix(ci): run tests/architecture/ guards on every PR, and fix the four they were hiding#4469dieterolson wants to merge 5 commits into
dieterolson wants to merge 5 commits into
Conversation
…ture guards `tests/architecture/test_sidekick_external_imports_3316.py:: test_production_code_uses_shared_python_imports` has been failing against main with 18 violations. The bare `sidekick.`, `theme.` and `ai.` spellings route through the deprecated compat shims in `src/<pkg>/__init__.py`, which `shared/python/import_aliases.py` documents as legacy-only for the #3316 deprecation window. Six modules under `src/shared/python/sidekick/` were missed by that codemod — `session_store.py` imported `from sidekick.persistence.schema` two lines above `from shared.python.contracts`, and `window.py` used the bare `ai.` spelling 20 lines from an existing `shared.python.theme.` one. The rest of the package already uses the intended idiom (388 relative imports; canonical `shared.python.*` for cross-package), so these were stragglers, not intent. Intra-package imports become relative; cross-package ones take the canonical `shared.python.*` spelling. This also makes the package importable without `src/` on `sys.path` — relevant to the standalone wheel. `apply_tokens()` in `preferences.py` imports `theme.sidekick_tokens`, which does not exist in the repo under either spelling; the method is unreachable (no callers, no tests) and was already broken on main. The spelling is canonicalized for consistency and the defect documented in place rather than silently "fixed" — tracked separately. Two further guards were red against main for the same invisibility reason: - `test_gh1696_god_modules.py` pinned `pressure_drop_calculator` paths under `src/shared/python/upstream_drift_tools/`, which the rename to `sidekick` reduced to a lone `__init__.py`. All three guarded invariants still hold at the canonical path (160 lines <= 200, no inline classes, `_legacy.py` present), so this is pure path rot. - `_shared_python_roots()` was called from `_root_matches()` once per import node in every `src/**/*.py`, re-running an ~8 ms directory scan each time. The guard took 66.9 s against the repo-wide 60 s pytest timeout, so it could not have passed even with zero violations. Caching drops it to 9.3 s. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The `tests` job runs changed test files plus a pinned `core_tests` set. Architecture guards assert properties of the tree as a whole, so the file a guard protects is almost never the guard's own file — under changed-file selection they only ran when someone happened to edit the guard itself. That is not a theoretical hole. Four guards across two files were failing against main and nobody saw it, because the last thing to enlist one was a cosmetic ruff reformat on PR #4446 — which worked around the red by restoring main's bytes so the file dropped back out of the changed set, re-hiding the failure rather than fixing it. Replace the single `test_layer_boundaries.py` pin with the whole `tests/architecture/` directory so guards added later are covered the day they land rather than waiting for someone to remember this list. pytest deduplicates a directory against a file inside it, so the three architecture entries in the `codex/tools-3316-import-canonicalization` branch block stay harmless. The existing `empty_core` guard (issue #3324) covers the new entry: it collects 64 tests, so a future directory rename cannot silently make this vacuous. Cost is ~57 s serial on top of the existing core set; the prerequisite fixes for all four failures are in the parent commit, so main does not go red. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
dieterolson
enabled auto-merge (squash)
August 14, 2026 10:01
…#4474) `apply_tokens()` imported `shared.python.theme.sidekick_tokens` for `COLOR_TOKEN_MAP` and `DEFAULT_SIDEKICK_TOKENS`. That module has never existed in Tools under that spelling or the former bare `theme.` one, and neither name is defined anywhere in `src/`, so every call raised `ModuleNotFoundError`. The method had no callers and no tests here, and none in UpstreamDrift or Gasification_Model. The preceding commit canonicalized the import spelling and documented the defect in a `.. warning::` block rather than silently "fixing" it, leaving the disposition to be tracked separately. This is that follow-up. Removed rather than reimplemented, for three reasons: - It is a duplicate, not a capability. The exact algorithm already exists and is importable as `sidekick_tokens_from_theme()` in UpstreamDrift's `src/shared/python/theme/sidekick_tokens.py` — the module `apply_tokens()` was written against, which was never synchronized into Tools despite that file's own "canonical changes must be made in the Tools repository" header. Deleting the broken copy removes a DRY violation. - The obvious reimplementation target does not fit the documented contract. `sidekick.ui.design_tokens.get_token_dict()` returns flat `@color_*` / `@spacing_*` / `@radius_*` QSS placeholders selected by theme *name*; the docstring specifies the `sidekick.color.*` namespace mapped from a caller-supplied `theme_colors` dict. Rebuilding on it would keep the name while changing the meaning, with no consumer to validate against. - Theme-token mapping is not a preferences concern. The method never touched `self`, and `sidekick/ui/design_tokens.py` already owns this. Not a downstream break: a name that cannot be called without raising is not in use. Removal turns `ModuleNotFoundError` into `AttributeError`. UpstreamDrift's tracked shadow copy at `src/shared/python/sidekick/standalone/preferences.py` is unaffected and still resolves the import locally; `vendor/ud-tools` picks this up on the next bump. The `tests/sidekick_api_baseline.json` entry was edited in place instead of regenerated via `--regenerate-api-baseline`. Regeneration is not safe here: `tests/test_sidekick_public_api_stability.py` is already red against this branch's base with 43 unrelated signature drifts across `api/`, `process_calculators/`, `selected_tab_panel.py`, `tab_context_menu.py` and `ui/tools_sidebar/`, which a regeneration would silently bless, and its `json.dump` serializer also reorders every top-level key (~9.3k lines of churn). That staleness is pre-existing and left untouched. Verified: `tests/unit/sidekick/test_standalone_public_api_baseline.py` (10 passed, `standalone/preferences.py` included), `tests/unit/sidekick/test_standalone_runtime.py` (7 passed), `tests/architecture/` (64 passed), and `ruff check` / `ruff format --check` at the CI-pinned 0.14.10. Co-authored-by: codex-scheduled <codex-scheduled@users.noreply.github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
`apply_tokens()` was a symptom. The cause is that nothing in the toolchain checks a first-party import names a module that exists. Probing each gate with a deliberately bogus import: repo mypy, CI's delta-mypy, and ruff all pass; only `ignore_missing_imports = False` fails it. That one line in mypy.ini (plus `--ignore-missing-imports` in CI) is the sole suppressor — `--follow-imports=skip` is not. Ruff never resolves imports, and F401 stays quiet because the imported names *are* used. Two more instances were live on main, both silent rather than loud: - `signal_toolkit.polynomial_generator` imported `shared.python .logging_config` inside `try/except ImportError`. The module is at `shared.python.logging_pkg.logging_config`, so the package had been falling back to bare logging forever. - `model_generation.humanoid` and `model_generation.mesh` each wrapped all their re-exports in ONE `try/except ImportError: pass`. A single missing module aborted the whole block, so every name was dead — verified 34 of 34 and 14 of 14 missing at runtime while `__all__` advertised them. Both files carried `# mypy: ignore-errors`, guaranteeing nobody would find out. Every name exists; only the submodule paths were wrong (`.appearance`, `.builder`, `.segments`, `.urdf`, `.mesh.lod`, `.mesh.mesh_inertia` never existed). Repointed at the defining modules and both pragmas removed. The new guard resolves by filesystem path walk against the roots this repo declares, deliberately not `importlib.util.find_spec`: find_spec reports failure when a *parent* package raises on import, depends on ambient sys.path, and executes package side effects during a static check. Naive find_spec resolution reported 63 production violations, 40 of them phantom sub-app path artifacts; the path walk reports the 10 real ones. Its one exemption is `shared.python.launcher_embed`, which is NOT a defect: UpstreamDrift provides it, and Tools' migrated `data_explorer` embed adapter registers itself only when that host is present. The allowlist is split into `_HOST_PROVIDED` and an empty `_KNOWN_UNRESOLVED` ratchet so the two never blur, and two further tests fail if an entry stops being imported or starts resolving locally — otherwise an exemption silently becomes permanent. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Same blind spot as tests/architecture/, one layer up: these tests assert on .github/workflows/*.yml, so editing a workflow never enlists them. Three had been failing against main since `quality-gate` moved to `ubuntu-24.04` and correctly left the self-hosted tool-cache steps behind — the tests still demanded them by job name. The fix is not to delete the assertions but to key them on the mechanism rather than a hard-coded job list, so a future migration cannot rot them the same way: - Persistent-tool-cache and cache-clean/restore contracts now apply to the jobs that opt into the shared fleet tool cache, and additionally assert such a job IS self-hosted (a hosted runner is ephemeral and needs no such step) and that the cleaner's version argument tracks that job's own setup-python request rather than a duplicated literal. `tests` is pinned as required so the opt-in cannot silently disappear. - `quality-gate`'s pip-isolation contract asserts the isolation actually in force — a dedicated venv under $RUNNER_TEMP plus PYTHONNOUSERSITE — instead of per-step PIP_CACHE_DIR pinning that an ephemeral runner made redundant. Verified by mutation: renaming the tool-cache step still fails the guard. Scoping to self-hosted jobs generally was tried first and over-reached — rust-quality-gate provisions Python without the tool-cache dance, so that formulation asserted more than CI promises. Whole directory again, and cheap: 130 tests in ~2 s with no Python provisioning. Fixes are in the parent commits, so main does not go red. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
tests/architecture/test_sidekick_external_imports_3316.py::test_production_code_uses_shared_python_importshas been failing againstmainand nobody noticed, because thetestsjob runs changed test files plus a pinnedcore_testsset. Architecture guards assert properties of the tree as a whole, so the file a guard protects is almost never the guard's own file — under changed-file selection they only ran when someone happened to edit the guard itself.That is not theoretical. It blocked #4446, which was a cosmetic
ruff formatreformat that enlisted the file; #4446 worked around it by restoringmain's bytes so the file dropped back out of the changed set — re-hiding the failure rather than fixing it. Sweeping the whole directory turned up four failing guards across two files, not one.Verdict on the sidekick imports: they were wrong
src/shared/python/import_aliases.pydocuments the baresidekick./theme./ai.spellings as legacy-only for the #3316 deprecation window; they resolve through the compat shims insrc/<pkg>/__init__.py. The package already uses the intended idiom nearly everywhere — 388 relative intra-package imports, and canonicalshared.python.*for cross-package. The 18 violations were stragglers the codemod missed, and two files prove it internally:standalone/session_store.pyimportedfrom sidekick.persistence.schematwo lines abovefrom shared.python.contracts.standalone/window.py:241used bareai.twenty lines from an existingshared.python.theme.import.So: intra-package → relative, cross-package → canonical
shared.python.*. This also makes the package importable withoutsrc/onsys.path, which matters for the standalone wheel.Two further defects the sweep exposed
The guard could not have passed even at zero violations.
_shared_python_roots()was called from_root_matches()once per import node in everysrc/**/*.py, re-running an ~8 ms directory scan each time — 66.9 s against the repo-wide 60 s pytest timeout. Cached, it is 9.3 s.test_gh1696_god_modules.pywas path-rotted. Itspressure_drop_calculatorconstants pointed undersrc/shared/python/upstream_drift_tools/, which the rename tosidekickreduced to a lone__init__.py. All three guarded invariants still hold at the canonical path (160 lines ≤ 200, no inline classes,_legacy.pypresent), so this is purely stale paths.Closing the hole
Replaced the single
test_layer_boundaries.pypin with the wholetests/architecture/directory, so guards added later are covered the day they land instead of waiting for someone to remember this list. pytest deduplicates a directory against a file inside it, so the three architecture entries in thecodex/tools-3316-import-canonicalizationbranch block stay harmless. Theempty_coreguard (#3324) covers the new entry — it collects 64 tests, so a future directory rename cannot silently make it vacuous.Ordering is deliberate: the four fixes are in the parent commit, the always-on switch in the child, so
mainnever goes red.Verification
tests/architecture/(whole directory,-n 0)_duplicate_import_violations()empty_corecollect simulation on new entryfind_specruff check+ruff format --check(320 files)mypyon all 8 changed filesprettier --checkSPEC.md + workflowcoverage_policy.jsontracked_packagesPre-existing failures confirmed unrelated by re-running them against a pristine
origin/maintree in the same worktree: 6 intests/unit/sidekick/test_standalone_*+test_standalone_wheel.py(local venv is missingplatformdirs, which is declared inpyproject.toml), and 3 intests/ops/test_ci_standard_web_dependencies.py. Identical failures before and after.One thing deliberately not fixed
StandalonePreferences.apply_tokens()importstheme.sidekick_tokens, which does not exist anywhere in the repo under either spelling — neitherCOLOR_TOKEN_MAPnorDEFAULT_SIDEKICK_TOKENSis defined insrc/. The method has no callers and no tests, and was already dead onmain; both spellings raiseModuleNotFoundErroridentically, so this PR is behaviour-neutral there. Choosing between deleting it (it is on the public API surface, so it needs a baseline regen plus downstream coordination) and reimplementing it against the liveui/design_tokens.get_token_dictis a product call, so the defect is documented in place rather than silently "fixed", and tracked separately.Note
--timeout=60means any always-on guard has to stay comfortably under a minute. The directory is at ~57s serial for the whole set, with the slowest single test at 9.3s, so there is headroom — but it is worth knowing that whole-tree AST guards are the natural place for this to bite again.🤖 Generated with Claude Code
Update — root cause fixed, not just the symptom
The original PR made the
tests/architecture/guards always-on. Follow-up work in this branch fixes the reason those guards were needed and closes the same hole one layer up.Why dead imports survive here
Probing each gate with a deliberately bogus first-party import:
mypy.ini)--ignore-missing-imports --follow-imports=skip)ruff checkignore_missing_imports = Falseerror: Cannot find implementation or library stub … [import-not-found]ignore_missing_imports = Trueis the sole suppressor —--follow-imports=skipdoes not hide it. Ruff never resolves imports, and F401 stays quiet because the names are used. All six fleet repos carry this setting, so the blindness is fleet-wide even though the changed-file selection blind spot is Tools-only (every other repo runspytest tests/wholesale).Two more live defects, both silent
signal_toolkit.polynomial_generatorimportedshared.python.logging_configinsidetry/except ImportError. The real module isshared.python.logging_pkg.logging_config, so it had been falling back to bare logging forever. That module name exists in no fleet repo.model_generation.humanoidandmodel_generation.mesheach wrapped all their re-exports in onetry/except ImportError: pass. A single missing module aborted the whole block. Verified: 34 of 34 and 14 of 14 exported names were dead at runtime while__all__advertised them —from …model_generation.humanoid import BodyParametersraisedImportError. Both files carried# mypy: ignore-errors.Every name existed; only the submodule paths were wrong (
.appearance,.builder,.segments,.urdf,.mesh.lod,.mesh.mesh_inertianever existed). Repointed at the defining modules, both pragmas removed, both facades now 34/34 and 14/14 live.The guard
tests/architecture/test_import_resolvability.pywalks everyImport/ImportFrominsrc/and resolves first-party targets by filesystem path walk against declared roots — deliberately notimportlib.util.find_spec, which reports failure when a parent package raises on import, depends on ambientsys.path, and executes package side effects during a static check. Naive find_spec resolution reported 63 production violations, ~40 of them phantom sub-app path artifacts; the path walk reported the 10 real ones.Its one exemption is not a defect:
shared.python.launcher_embedis UpstreamDrift's embeddable-tool contract, and Tools' migrateddata_exploreradapter registers itself only when that host is present — a genuine cross-repo soft dependency._HOST_PROVIDEDis kept separate from an empty_KNOWN_UNRESOLVEDratchet so the two never blur, and two further tests fail if an entry stops being imported or starts resolving locally, so an exemption can't quietly become permanent.Verified by negative control: reintroducing
shared.python.theme.sidekick_tokens(the import that started all this) fails the guard with file:line; removing it passes.tests/ops/— the same blind spot one layer upThose tests assert on
.github/workflows/*.yml, so editing a workflow never enlists them. Three had been red sincequality-gatemoved toubuntu-24.04and correctly left the self-hosted tool-cache steps behind — the tests still demanded them by job name.Retargeted at the mechanism rather than a job list, so a future migration can't rot them again: the tool-cache contracts now key on jobs that opt into the shared fleet cache, additionally assert such a job is self-hosted, and require the cleaner's version argument to track that job's own
setup-pythonrequest instead of a duplicated literal.quality-gate's pip-isolation contract now asserts the isolation actually in force ($RUNNER_TEMP/ci-venv+PYTHONNOUSERSITE). Scoping to self-hosted jobs generally was tried first and over-reached —rust-quality-gateprovisions Python without the tool-cache dance.Verified by mutation: renaming the tool-cache step still fails the guard. Then added to
core_tests— 130 tests in ~2 s, no Python provisioning.Verification
tests/architecture/+tests/ops/+model_generation+signal_toolkitempty_corecollect simulation,tests/ops/ruff check/ruff format --check/prettierOne pre-existing
no-any-returninmodel_generation/inertia/calculator.py:120is now inherited because this PR touches that file; fixed with the repo's established# type: ignore[no-any-return]idiom and confirmed live under--warn-unused-ignores.Fleet
Scanned every fleet repo's
origin/main. UpstreamDrift has the identical cluster — it owns its ownhumanoid_character_builderandlogging_pkgtrees with the same missing submodules — filed as UpstreamDrift#8641. The patch is explicitly not transplantable: the trees have drifted (URDFGeneratorConfigandPrimitiveMeshGeneratorlive in different modules there), which the issue calls out.Gasification_Model's raw count is inflated by its Tools symlink — it has no own
shared/python/sidekick/tree, so those imports are shared-library-provided rather than broken. A guard there needs the host-provided concept first.Not done here
Narrowing
ignore_missing_importsto per-module overrides is the durable fix, but it can't be flipped in one PR: legacy baresidekick./theme./ai.spellings resolve only through the runtime alias finder and are invisible to mypy, so flipping today floods errors. It should follow the #3316 canonicalisation — at which point it also makes that deprecation statically enforceable.