test: close the r4 mediums — structural tuple scan and reuse-key inputs (#42)

M1: the AC5 scanner parses the (field, code, message) literal tuple in
validation.py structurally instead of naming the two known codes — a
third tuple entry with an unregistered code now fails the registry test
(verified with an injected invalid_ghost_entity_mutant_probe), and a
tuple whose string count is not a multiple of three refuses instead of
guessing.
M2: the backend reuse key now includes its direct job inputs introduced
by this issue — scripts/backend-coverage-baseline.txt (the threshold the
comparison step reads), requirements_test.txt (the pip source) and
pyproject.toml (ruff/mypy config) — verified: the key changes when the
baseline changes and is restored byte-for-byte with the file.

User-Visible: no
Issue: #42
This commit is contained in:
Codex
2026-08-30 21:34:32 +03:00
parent 26d45cf405
commit 9f50778df5
2 changed files with 20 additions and 2 deletions
+7 -1
View File
@@ -49,7 +49,13 @@ export const HARNESS = {
|| /^demo\/benchmark_(glow|large_house)\.mjs$/.test(rel),
},
backend: {
roots: ['tests_backend', 'custom_components', 'pytest.ini'],
// #42: порог покрытия и пины зависимостей — прямые входы job
// (`head -1 baseline` в шаге сравнения; `pip install -r`): их изменение
// без правок тестов обязано сбрасывать реюз, иначе baseline-bump молча
// пройдёт по старому зелёному маркеру.
roots: ['tests_backend', 'custom_components', 'pytest.ini',
'scripts/backend-coverage-baseline.txt', 'requirements_test.txt',
'pyproject.toml'],
// Внутри custom_components/** значим только Python: собранный фронтенд
// лежит там же и меняется от любой сборки, а backend его не исполняет.
keep: (rel) => !rel.startsWith('custom_components/') || rel.endsWith('.py'),
+13 -1
View File
@@ -48,7 +48,19 @@ def _emitted_codes() -> tuple[set[str], set[str]]:
r'(?:source_error|attribute_error)\s*=\s*"([a-z0-9_]+)"\s*if[^\n]*\\\s*\n\s*else\s*"([a-z0-9_]+)"',
validation):
fixed |= set(pair)
fixed |= set(re.findall(r'"(invalid_(?:light|toggle)_entity)"', validation))
# The literal-tuple path (field, code, message) is parsed STRUCTURALLY,
# not by naming the two known codes: a third tuple entry with a brand-new
# code must land in `fixed` (and so fail the registry test if the code is
# unregistered). Fail-closed: a tuple block whose string count is not a
# multiple of three means the shape changed — refuse instead of guessing.
for block in re.findall(
r'for field, code, message in \(\n(.*?)\n\s*\):', validation, re.S):
strings = re.findall(r'"([^"]*)"', block)
if not strings or len(strings) % 3:
raise AssertionError(
"unrecognized (field, code, message) tuple shape in validation.py: "
f"{len(strings)} string literals — update the AC5 scanner")
fixed |= set(strings[1::3])
families: set[str] = set()
families |= {match + "_" if not match.endswith("_") else match