Repository navigation
test(slm): guard the canonical/Ansible-mirror file pairs against drift (#14584) - #14650
Conversation
…#14584) The required code-quality check's detect-agent-code-drift.sh blanket-excluded every *_test.py from its canonical-vs-ansible-mirror diff, which is exactly how health_collector_state_change_test.py's mirror silently drifted and failed on base for a full CI cycle before anyone traced it. Names the one deliberate exception (health_collector_probe_test.py, canonical-tree-only by its own docstring) instead, and extends the same guard to the previously unchecked backend role permission_rules.yaml/error_messages.yaml mirror pair. Fails loudly on content drift (both paths + diff), a missing copy at either the directory or file level, and on resolving zero pairs. Also corrects a stranded code-quality.yml path-filter entry (autobot-infrastructure/ansible/... never existed) so edits to either side of both pairs actually trigger the check.
…rd (#14584) Runs pipeline-scripts/detect-agent-code-drift.sh against synthetic trees built under tmp_path, never against this checkout's own files. Covers: a fully-synced tree passing, the deliberate health_collector_probe_test.py exception not false-positiving as drift, content drift naming both paths, a missing mirror file, a missing mirror directory, a missing canonical file, and a mutated copy with every pair-registration call stripped still refusing to report clean on zero pairs. Collected by pytest under autobot-slm-backend (python-suite); not itself a required check, so CI evidence still has to be the shard result, not a green PR.
Review — APPROVE, and the reframing is worth more than the guardI asked you to build a drift guard. You found one already existed, had been required since #1629, ran on every PR — and was blind to precisely the file class that drifted. That is a much better answer than a new guard, and I verified both halves: So the root cause was never "no guard existed". It was an exclusion broad enough to cover the thing it was guarding. The mirror could drift indefinitely with a required check passing on every commit — which is exactly why the incident got misattributed to shard flakiness for a full CI cycle. And the second finding compounds it
So the path filter pointed at nothing while the guard excluded the drifting files. Two independent, silent failures stacked on the same mechanism: the job might not fire on a mirror change, and if it did fire it would not look at the changed files. Either alone is a hole; together they explain the incident completely. That is the third path-filter defect this session — after The rest
The named exception for The mutation coverage is thorough in the way that matters: content drift naming both paths and the differing line, a missing file, a missing directory with a distinct message, and — the one I most wanted — stripping the registration calls to simulate the guard being gutted, which reports Being straight about enforcement location is right too: the new pytest lives under On the duplication question: "could not find a documented reason, flagged as unjustified as far as found, restructuring left to #13916" is exactly the right answer to give. You were asked to judge it, not to fix it, and "I looked and found no justification" is a finding — it tells #13916 that the copy is probably removable rather than load-bearing. |
✅ SSOT Configuration Compliance: Passing🎉 No hardcoded values detected that have SSOT config equivalents! |
#14550) Rebasing onto origin/Dev_new_gui merged #14650's addition to the SAME code-quality.yml filter block this PR edits -- both PRs add entries to the `changes` job's `filters.backend` list, and #14650's is the first commit to ever put a multi-line comment INSIDE that literal `filters: |` string block (previously bare bullets only). The merge itself was correct: both sets of additions are present, valid YAML, all 19 patterns real. check_code_quality_guard_reach.py's own re-parser was not correct against that shape, though: backend_filter_patterns() treated any line that was not a `- 'pattern'` bullet as the end of the list, once at least one bullet had been collected. The inserted comment block satisfied that "first non-bullet line" condition and silently truncated the parse right after `autobot-infrastructure/shared/scripts/**` -- reading every entry after it, including this PR's own three guarded paths, as uncovered. Running the checker immediately after the rebase (per review guidance: trust it over the manual resolution) is what caught this. dorny/paths-filter itself parses this block as real YAML and is unaffected by comments -- the bug was specific to this checker's line-oriented mirror, not the workflow. Fixed by skipping blank lines and full-line `#` comments instead of treating them as terminators, so only a genuine non-bullet, non-comment line ends the block. repo_tests/code_quality_guard_reach_test.py::test_backend_filter_patterns_survives_a_comment_mid_list reproduces the exact shape (a comment between two bullets) and was confirmed to fail against the pre-fix parser before being trusted: reverted the fix locally, watched it fail with `['before/**'] == ['before/**', 'after/**']`, restored the fix, watched it pass.
#14550) Rebasing onto origin/Dev_new_gui merged #14650's addition to the SAME code-quality.yml filter block this PR edits -- both PRs add entries to the `changes` job's `filters.backend` list, and #14650's is the first commit to ever put a multi-line comment INSIDE that literal `filters: |` string block (previously bare bullets only). The merge itself was correct: both sets of additions are present, valid YAML, all 19 patterns real. check_code_quality_guard_reach.py's own re-parser was not correct against that shape, though: backend_filter_patterns() treated any line that was not a `- 'pattern'` bullet as the end of the list, once at least one bullet had been collected. The inserted comment block satisfied that "first non-bullet line" condition and silently truncated the parse right after `autobot-infrastructure/shared/scripts/**` -- reading every entry after it, including this PR's own three guarded paths, as uncovered. Running the checker immediately after the rebase (per review guidance: trust it over the manual resolution) is what caught this. dorny/paths-filter itself parses this block as real YAML and is unaffected by comments -- the bug was specific to this checker's line-oriented mirror, not the workflow. Fixed by skipping blank lines and full-line `#` comments instead of treating them as terminators, so only a genuine non-bullet, non-comment line ends the block. repo_tests/code_quality_guard_reach_test.py::test_backend_filter_patterns_survives_a_comment_mid_list reproduces the exact shape (a comment between two bullets) and was confirmed to fail against the pre-fix parser before being trusted: reverted the fix locally, watched it fail with `['before/**'] == ['before/**', 'after/**']`, restored the fix, watched it pass.
Closes #14584
Thinking Path
Started from the precedent the issue names,
check_role_facts_synced.py, but before extending it looked for whether a more directly-relevant mechanism already existed for this exact pair — and found one:pipeline-scripts/detect-agent-code-drift.sh, wired into the requiredcode-qualitycheck since #1629, already doing a recursive byte-diff ofautobot-slm-backend/slm/agent/against its Ansible mirror.Reading it explained the incident directly: it excluded
--exclude='*_test.py'from the comparison — a blanket wildcard, not a named exception — which is exactly the class of file that silently drifted. So the root cause wasn't "no guard exists," it was "the guard's own exclude list was blind to precisely the file class that broke." Fixing that one line, in the existing required check, closes the reported gap more directly than adding a second, parallel mechanism would.Establishing scope next (the issue's explicit ask):
ansible/roles/*/files/resolves to exactly two role directories in this repo —slm_agentandbackend. Tracing each role'stasks/main.ymlcopy: src:entries back to a canonical source elsewhere in the tree found:slm_agent: 8 files underslm/agent/(6 deployed modules +version_test.py+health_collector_state_change_test.py, both test-support mirrors kept in sync so CI can run the exact bytes the role ships, The cross-tree module loader in the slm health test needs a manual stub for every new backend import #14538/test(slm): fix redis health test pollution and cross-tree loader fragility #14576). A 9th canonical file,health_collector_probe_test.py, is deliberately NOT mirrored — its own docstring says it is canonical-tree-only.backend: 2 files (permission_rules.yaml,error_messages.yaml), both already byte-identical to their canonicalautobot-backend/sources but covered by no existing check at all.pairs_found = 10.
Deeper question the issue asks to judge: is byte-identical duplication the right arrangement, or should the ansible role reference the canonical file instead of holding a copy? I could not find a reason it has to be a copy — Ansible's
copy:module can source from outside the role's ownfiles/(a relativesrc:path, oransible.builtin.copyreading from anywhere the control node can see), so referencingautobot-slm-backend/slm/agent/*.pydirectly and dropping the mirror entirely looks structurally possible. I'm not making that change here — it's #13916's scope, and untangling why the mirror was introduced in the first place (possibly role-portability, possibly just how #1629 was originally written) needs more digging than this PR's budget. Recording it as unjustified-as-far-as-I-can-tell rather than silent.What Changed
pipeline-scripts/detect-agent-code-drift.sh: replaced the blanket--exclude='*_test.py'with a named exception for the one deliberately-canonical-only file (health_collector_probe_test.py), so bothversion_test.pyandhealth_collector_state_change_test.pyare now compared like any other file. Extended the same script to also cover thebackendrole'spermission_rules.yaml/error_messages.yamlpair. Refactored intocompare_dir_pair/compare_file_pairhelpers that each fail loudly on content drift (naming both paths + a diff), a missing directory, or a missing individual file, and added aPAIRS_COMPAREDcounter that fails the run if it ever reaches zero — a future edit that guts every pair-registration call must not read as clean..github/workflows/code-quality.yml: fixed a stranded path-filter entry,autobot-infrastructure/ansible/roles/slm_agent/files/**, which named a directory that has never existed in this repo (confirmed viagit log -Son the line) and so matched nothing since it was added. Corrected to the real path and widened both the push-triggerpaths:list and thechangesjob'spaths-filterto the second pair, so editing either side of either pair actually triggers the check that guards it.autobot-slm-backend/ansible/tests/detect_agent_code_drift_test.py(new): regression coverage that runs the real script against synthetic trees built undertmp_path, never against this checkout's own files. Covers a synced tree passing, the deliberate probe-test exception not false-positiving, content drift naming both paths, a missing mirror file, a missing mirror directory, a missing canonical file, and a mutated copy of the script with every registration call stripped still refusing to report a zero-pairs result as clean.Verification
This check runs inside
code-quality, which is a required status check onDev_new_gui.Scratch-checkout verification (under
/tmp, never against this repo's own tracked files), against a tree seeded from the real 10 pairs:Before/after counts: before this PR, the script silently compared 0 test-file pairs (blanket-excluded) out of what should have been 2, and 0 of the 2
backend-role pairs (not covered at all) — 6 of 10 real pairs actually enforced. After, all 10 are enforced, and the run reports the count so a future drop is visible (OK -- 10 ... pairsvs some smaller number).black/isort/flake8ran clean against the new test file locally;bash -nandpython3 -c "yaml.safe_load(...)"confirm the shell script and the edited workflow YAML both parse. Did not run the Python suite locally in a way that stands as merge evidence — see thepython-suite shard N/12runs on this PR directly; that job is not a required check.Model Used
Claude Sonnet 5