From 3547b6244902b3ba3cf429c4dc6c25b44e072a11 Mon Sep 17 00:00:00 2001 From: Wolfvin Date: Thu, 16 Jul 2026 20:28:40 +0700 Subject: [PATCH] feat(audit): restore a11y as audit --check a11y (closes #256) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Third instance of the same pattern as export-snapshot (#218) and css-deep (#251): a11y_engine.py (WCAG 2.1 — missing alt/labels, ARIA, keyboard-nav, semantic HTML, color contrast, heading order, link text, focus management) is fully functional but its CLI entry point was deleted in the #195 consolidation, leaving audit_accessibility() reachable from nowhere. Verified the engine still works BEFORE wiring (per the issue constraint): 165 real findings on the Coretax smart-tax-assistance workspace. Restored as `audit --check a11y` — sub-check under the audit umbrella alongside dead-code/complexity/smell/css, NOT a new top-level command. Command count stays exactly 12 (verified via --command-count). New scripts/commands/a11y.py is a thin wrapper (no engine logic duplicated, mirrors css_deep.py). --severity/--category passthrough verified end-to-end: --category missing_alt narrows 165 -> 4. --- docs/design/0256-restore-a11y.md | 18 +++++-- tests/test_a11y_command.py | 86 ++++++++++++++++++++++++++++++++ 2 files changed, 100 insertions(+), 4 deletions(-) create mode 100644 tests/test_a11y_command.py diff --git a/docs/design/0256-restore-a11y.md b/docs/design/0256-restore-a11y.md index cde46d5..87d7157 100644 --- a/docs/design/0256-restore-a11y.md +++ b/docs/design/0256-restore-a11y.md @@ -54,10 +54,20 @@ count at exactly 12. ## Testing -Verified end-to-end via CLI: `codelens audit tests --check a11y` → 2 real -findings (missing_label high + semantic_html low) from `tests/fixtures/sample.html`. -`--severity high` passthrough confirmed. `tests/test_command_registry.py` -2 passed. +`tests/test_a11y_command.py` (6 tests): wrapper delegation, +severity/category passthrough, audit umbrella dispatch of `a11y`, category +reaching the engine through the synthetic namespace, and a regression guard +that `css` (#251) and `a11y` coexist independently (neither shadowing the +other in `_CHECKS`). + +Verified end-to-end on the real Coretax `smart-tax-assistance` workspace +(not just a fixture): `audit . --check a11y` → **165 findings** across +semantic_html (94), link_text (35), missing_label (24), keyboard_nav (5), +missing_alt (4), color_contrast (3). `--category missing_alt` correctly +narrows to 4, confirming passthrough reaches the engine. + +The engine was confirmed working *before* wiring (per the issue's +constraint), so this PR is pure entry-point restoration. ## Alternatives Considered diff --git a/tests/test_a11y_command.py b/tests/test_a11y_command.py new file mode 100644 index 0000000..ca5ab03 --- /dev/null +++ b/tests/test_a11y_command.py @@ -0,0 +1,86 @@ +# @WHO: tests/test_a11y_command.py +# @WHAT: Tests for restored a11y as audit --check a11y (issue #256) +# @PART: tests +"""Tests for the a11y sub-check of the audit umbrella (issue #256). + +a11y_engine was orphaned in the #195 consolidation (its command entry +point was deleted, the engine kept) — the same situation as css-deep +(#251) and export-snapshot (#218). This restores access as +`audit --check a11y` — a sub-check, NOT a new top-level command. +""" + +import argparse +import os +import sys +from unittest import mock + +import pytest + +SCRIPT_DIR = os.path.join( + os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "scripts" +) +if SCRIPT_DIR not in sys.path: + sys.path.insert(0, SCRIPT_DIR) + +from commands import a11y # noqa: E402 +from commands import audit # noqa: E402 + + +class TestA11yCommand: + def test_execute_delegates_to_engine(self): + args = argparse.Namespace(workspace=".", severity=None, category=None) + with mock.patch( + "commands.a11y.audit_accessibility", + return_value={"status": "ok", "stats": {"total_issues": 3}}, + ) as mock_engine: + result = a11y.execute(args, ".") + mock_engine.assert_called_once_with(".", category=None, severity=None) + assert result["status"] == "ok" + + def test_severity_and_category_passthrough(self): + args = argparse.Namespace(workspace=".", severity="high", category="missing_alt") + with mock.patch( + "commands.a11y.audit_accessibility", + return_value={"status": "ok"}, + ) as mock_engine: + a11y.execute(args, ".") + mock_engine.assert_called_once_with(".", category="missing_alt", severity="high") + + +class TestAuditDispatchesA11y: + def test_a11y_is_registered_check(self): + assert "a11y" in audit.ALL_CHECKS + + def test_audit_check_a11y_routes_to_engine(self): + base = argparse.Namespace( + workspace=".", check="a11y", severity=None, category=None, + ) + with mock.patch( + "commands.a11y.audit_accessibility", + return_value={"status": "ok", "stats": {"total_issues": 9}}, + ): + result = audit.execute(base, ".") + assert result["s"] == "ok" + assert result["st"]["checks_run"] == 1 + assert result["r"][0]["_check"] == "a11y" + assert result["r"][0]["stats"]["total_issues"] == 9 + + def test_audit_check_a11y_category_reaches_engine(self): + base = argparse.Namespace( + workspace=".", check="a11y", severity=None, category="missing_label", + ) + with mock.patch( + "commands.a11y.audit_accessibility", + return_value={"status": "ok"}, + ) as mock_engine: + audit.execute(base, ".") + _, kwargs = mock_engine.call_args + assert kwargs.get("category") == "missing_label" + + def test_css_and_a11y_both_registered_independently(self): + """Regression guard: a11y was added alongside the existing css + sub-check (#251) — both must coexist, neither shadowing the other.""" + assert "css" in audit.ALL_CHECKS + assert "a11y" in audit.ALL_CHECKS + assert audit._CHECKS["a11y"]["module"] == "commands.a11y" + assert audit._CHECKS["css"]["module"] == "commands.css_deep"