@mcptoolshop/accessibility-suite 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +63 -0
- package/LICENSE +21 -0
- package/README.md +37 -0
- package/docs/prov-spec/.github/workflows/ci.yml +68 -0
- package/docs/prov-spec/CHANGELOG.md +69 -0
- package/docs/prov-spec/CODE_OF_CONDUCT.md +129 -0
- package/docs/prov-spec/CONFORMANCE_LEVELS.md +223 -0
- package/docs/prov-spec/CONTRIBUTING.md +145 -0
- package/docs/prov-spec/IMPLEMENTER_CHECKLIST.md +137 -0
- package/docs/prov-spec/LICENSE +21 -0
- package/docs/prov-spec/PRESS_RELEASE.md +74 -0
- package/docs/prov-spec/README.md +182 -0
- package/docs/prov-spec/SETUP.md +135 -0
- package/docs/prov-spec/WHY.md +86 -0
- package/docs/prov-spec/examples/artifact.example.json +14 -0
- package/docs/prov-spec/examples/artifact.ref.example.json +9 -0
- package/docs/prov-spec/examples/evidence.example.json +6 -0
- package/docs/prov-spec/examples/mcp.envelope.example.json +97 -0
- package/docs/prov-spec/examples/mcp.request.example.json +28 -0
- package/docs/prov-spec/examples/prov.record.example.json +35 -0
- package/docs/prov-spec/interop/PROOF_NODE_ENGINE.md +114 -0
- package/docs/prov-spec/spec/MCP_COMPATIBILITY.md +241 -0
- package/docs/prov-spec/spec/PROV_METHODS_CATALOG.md +142 -0
- package/docs/prov-spec/spec/PROV_METHODS_SPEC.md +397 -0
- package/docs/prov-spec/spec/methods.json +213 -0
- package/docs/prov-spec/spec/schemas/artifact.ref.schema.v0.1.json +58 -0
- package/docs/prov-spec/spec/schemas/artifact.schema.v0.1.json +61 -0
- package/docs/prov-spec/spec/schemas/assist.request.schema.v0.1.json +52 -0
- package/docs/prov-spec/spec/schemas/assist.response.schema.v0.1.json +70 -0
- package/docs/prov-spec/spec/schemas/cli.error.schema.v0.1.json +78 -0
- package/docs/prov-spec/spec/schemas/evidence.schema.v0.1.json +37 -0
- package/docs/prov-spec/spec/schemas/mcp.envelope.schema.v0.1.json +141 -0
- package/docs/prov-spec/spec/schemas/mcp.request.schema.v0.1.json +79 -0
- package/docs/prov-spec/spec/schemas/methods.schema.json +93 -0
- package/docs/prov-spec/spec/schemas/prov-capabilities.schema.json +122 -0
- package/docs/prov-spec/spec/schemas/prov.record.schema.v0.1.json +133 -0
- package/docs/prov-spec/spec/vectors/adapter.wrap.envelope_v0_1/expected.json +4 -0
- package/docs/prov-spec/spec/vectors/adapter.wrap.envelope_v0_1/input.json +1 -0
- package/docs/prov-spec/spec/vectors/adapter.wrap.envelope_v0_1/negative/double_wrapped.json +14 -0
- package/docs/prov-spec/spec/vectors/adapter.wrap.envelope_v0_1/negative/wrong_schema_version.json +11 -0
- package/docs/prov-spec/spec/vectors/engine.extract.evidence.json_pointer/expected.json +24 -0
- package/docs/prov-spec/spec/vectors/engine.extract.evidence.json_pointer/input.json +8 -0
- package/docs/prov-spec/spec/vectors/integrity.digest.sha256/expected.json +7 -0
- package/docs/prov-spec/spec/vectors/integrity.digest.sha256/input.json +1 -0
- package/docs/prov-spec/spec/vectors/integrity.digest.sha256/negative/non_hex_chars.json +16 -0
- package/docs/prov-spec/spec/vectors/integrity.digest.sha256/negative/uppercase_hex.json +16 -0
- package/docs/prov-spec/spec/vectors/integrity.digest.sha256/negative/wrong_length.json +16 -0
- package/docs/prov-spec/spec/vectors/method_id_syntax/negative/hyphen_separator.json +8 -0
- package/docs/prov-spec/spec/vectors/method_id_syntax/negative/reserved_namespace.json +8 -0
- package/docs/prov-spec/spec/vectors/method_id_syntax/negative/starts_with_digit.json +8 -0
- package/docs/prov-spec/spec/vectors/method_id_syntax/negative/uppercase.json +8 -0
- package/docs/prov-spec/spec/vectors/method_id_syntax/positive/valid_ids.json +18 -0
- package/docs/prov-spec/tools/python/prov_validator.py +428 -0
- package/examples/a11y-demo-site/.github/workflows/a11y-artifacts.yml +81 -0
- package/examples/a11y-demo-site/.github/workflows/a11y.yml +34 -0
- package/examples/a11y-demo-site/CODE_OF_CONDUCT.md +129 -0
- package/examples/a11y-demo-site/CONTRIBUTING.md +83 -0
- package/examples/a11y-demo-site/LICENSE +21 -0
- package/examples/a11y-demo-site/README.md +155 -0
- package/examples/a11y-demo-site/html/contact.html +15 -0
- package/examples/a11y-demo-site/html/index.html +20 -0
- package/examples/a11y-demo-site/scripts/a11y.sh +20 -0
- package/package.json +26 -0
- package/src/a11y-assist/.github/workflows/publish.yml +52 -0
- package/src/a11y-assist/.github/workflows/test.yml +30 -0
- package/src/a11y-assist/A11Y_ASSIST_TEST_COVERAGE_REQUIREMENTS.md +104 -0
- package/src/a11y-assist/CODE_OF_CONDUCT.md +129 -0
- package/src/a11y-assist/CONTRIBUTING.md +98 -0
- package/src/a11y-assist/ENGINE.md +363 -0
- package/src/a11y-assist/LICENSE +21 -0
- package/src/a11y-assist/PRESS_RELEASE.md +71 -0
- package/src/a11y-assist/QUICKSTART.md +101 -0
- package/src/a11y-assist/README.md +192 -0
- package/src/a11y-assist/RELEASE_NOTES.md +319 -0
- package/src/a11y-assist/a11y_assist/__init__.py +3 -0
- package/src/a11y-assist/a11y_assist/cli.py +599 -0
- package/src/a11y-assist/a11y_assist/from_cli_error.py +149 -0
- package/src/a11y-assist/a11y_assist/guard.py +444 -0
- package/src/a11y-assist/a11y_assist/ingest.py +407 -0
- package/src/a11y-assist/a11y_assist/methods.py +137 -0
- package/src/a11y-assist/a11y_assist/parse_raw.py +71 -0
- package/src/a11y-assist/a11y_assist/profiles/__init__.py +29 -0
- package/src/a11y-assist/a11y_assist/profiles/cognitive_load.py +245 -0
- package/src/a11y-assist/a11y_assist/profiles/cognitive_load_render.py +86 -0
- package/src/a11y-assist/a11y_assist/profiles/dyslexia.py +144 -0
- package/src/a11y-assist/a11y_assist/profiles/dyslexia_render.py +77 -0
- package/src/a11y-assist/a11y_assist/profiles/plain_language.py +119 -0
- package/src/a11y-assist/a11y_assist/profiles/plain_language_render.py +66 -0
- package/src/a11y-assist/a11y_assist/profiles/screen_reader.py +348 -0
- package/src/a11y-assist/a11y_assist/profiles/screen_reader_render.py +89 -0
- package/src/a11y-assist/a11y_assist/render.py +95 -0
- package/src/a11y-assist/a11y_assist/schemas/assist.request.schema.v0.1.json +52 -0
- package/src/a11y-assist/a11y_assist/schemas/assist.response.schema.v0.1.json +70 -0
- package/src/a11y-assist/a11y_assist/schemas/cli.error.schema.v0.1.json +78 -0
- package/src/a11y-assist/a11y_assist/storage.py +31 -0
- package/src/a11y-assist/pyproject.toml +60 -0
- package/src/a11y-assist/tests/__init__.py +1 -0
- package/src/a11y-assist/tests/fixtures/base_inputs/cli_error_high.json +18 -0
- package/src/a11y-assist/tests/fixtures/base_inputs/cli_error_medium.json +16 -0
- package/src/a11y-assist/tests/fixtures/base_inputs/raw_text_low.txt +3 -0
- package/src/a11y-assist/tests/fixtures/cli_error_good.json +9 -0
- package/src/a11y-assist/tests/fixtures/cli_error_missing_id.json +7 -0
- package/src/a11y-assist/tests/fixtures/cli_error_string_format.json +7 -0
- package/src/a11y-assist/tests/fixtures/expected/cognitive_load_high.txt +20 -0
- package/src/a11y-assist/tests/fixtures/expected/dyslexia_high.txt +20 -0
- package/src/a11y-assist/tests/fixtures/expected/lowvision_high.txt +18 -0
- package/src/a11y-assist/tests/fixtures/expected/plain_language_high.txt +14 -0
- package/src/a11y-assist/tests/fixtures/expected/screen_reader_high.txt +19 -0
- package/src/a11y-assist/tests/fixtures/golden_screen_reader_cli_error.txt +16 -0
- package/src/a11y-assist/tests/fixtures/golden_screen_reader_raw_no_id.txt +14 -0
- package/src/a11y-assist/tests/fixtures/golden_screen_reader_raw_with_id.txt +14 -0
- package/src/a11y-assist/tests/fixtures/raw_good.txt +11 -0
- package/src/a11y-assist/tests/fixtures/raw_no_id.txt +2 -0
- package/src/a11y-assist/tests/test_cognitive_load.py +469 -0
- package/src/a11y-assist/tests/test_dyslexia.py +337 -0
- package/src/a11y-assist/tests/test_explain.py +74 -0
- package/src/a11y-assist/tests/test_golden.py +127 -0
- package/src/a11y-assist/tests/test_guard.py +819 -0
- package/src/a11y-assist/tests/test_guard_integration.py +457 -0
- package/src/a11y-assist/tests/test_ingest.py +311 -0
- package/src/a11y-assist/tests/test_methods_metadata.py +236 -0
- package/src/a11y-assist/tests/test_plain_language.py +348 -0
- package/src/a11y-assist/tests/test_render.py +117 -0
- package/src/a11y-assist/tests/test_screen_reader.py +703 -0
- package/src/a11y-assist/tests/test_storage_last.py +61 -0
- package/src/a11y-assist/tests/test_triage.py +86 -0
- package/src/a11y-ci/.github/workflows/ci.yml +43 -0
- package/src/a11y-ci/.github/workflows/test.yml +30 -0
- package/src/a11y-ci/A11Y_CI_TEST_COVERAGE_REQUIREMENTS.md +94 -0
- package/src/a11y-ci/CODE_OF_CONDUCT.md +129 -0
- package/src/a11y-ci/CONTRIBUTING.md +142 -0
- package/src/a11y-ci/LICENSE +21 -0
- package/src/a11y-ci/README.md +105 -0
- package/src/a11y-ci/a11y_ci/__init__.py +3 -0
- package/src/a11y-ci/a11y_ci/allowlist.py +83 -0
- package/src/a11y-ci/a11y_ci/cli.py +145 -0
- package/src/a11y-ci/a11y_ci/gate.py +131 -0
- package/src/a11y-ci/a11y_ci/render.py +48 -0
- package/src/a11y-ci/a11y_ci/schemas/allowlist.schema.json +24 -0
- package/src/a11y-ci/a11y_ci/scorecard.py +99 -0
- package/src/a11y-ci/npm/package.json +35 -0
- package/src/a11y-ci/pyproject.toml +64 -0
- package/src/a11y-ci/tests/__init__.py +1 -0
- package/src/a11y-ci/tests/fixtures/allowlist_expired.json +10 -0
- package/src/a11y-ci/tests/fixtures/allowlist_ok.json +10 -0
- package/src/a11y-ci/tests/fixtures/baseline_ok.json +7 -0
- package/src/a11y-ci/tests/fixtures/current_fail.json +6 -0
- package/src/a11y-ci/tests/fixtures/current_ok.json +6 -0
- package/src/a11y-ci/tests/fixtures/current_regresses.json +7 -0
- package/src/a11y-ci/tests/test_gate.py +134 -0
- package/src/a11y-evidence-engine/.github/workflows/ci.yml +53 -0
- package/src/a11y-evidence-engine/CODE_OF_CONDUCT.md +129 -0
- package/src/a11y-evidence-engine/CONTRIBUTING.md +128 -0
- package/src/a11y-evidence-engine/LICENSE +21 -0
- package/src/a11y-evidence-engine/README.md +71 -0
- package/src/a11y-evidence-engine/bin/a11y-engine.js +11 -0
- package/src/a11y-evidence-engine/fixtures/bad/button-no-name.html +30 -0
- package/src/a11y-evidence-engine/fixtures/bad/img-missing-alt.html +19 -0
- package/src/a11y-evidence-engine/fixtures/bad/input-missing-label.html +26 -0
- package/src/a11y-evidence-engine/fixtures/bad/missing-lang.html +11 -0
- package/src/a11y-evidence-engine/fixtures/good/index.html +29 -0
- package/src/a11y-evidence-engine/package-lock.json +109 -0
- package/src/a11y-evidence-engine/package.json +45 -0
- package/src/a11y-evidence-engine/src/cli.js +74 -0
- package/src/a11y-evidence-engine/src/evidence/canonicalize.js +52 -0
- package/src/a11y-evidence-engine/src/evidence/json_pointer.js +34 -0
- package/src/a11y-evidence-engine/src/evidence/prov_emit.js +153 -0
- package/src/a11y-evidence-engine/src/fswalk.js +56 -0
- package/src/a11y-evidence-engine/src/html_parse.js +117 -0
- package/src/a11y-evidence-engine/src/ids.js +53 -0
- package/src/a11y-evidence-engine/src/rules/document_missing_lang.js +50 -0
- package/src/a11y-evidence-engine/src/rules/form_control_missing_label.js +105 -0
- package/src/a11y-evidence-engine/src/rules/img_missing_alt.js +77 -0
- package/src/a11y-evidence-engine/src/rules/index.js +37 -0
- package/src/a11y-evidence-engine/src/rules/interactive_missing_name.js +129 -0
- package/src/a11y-evidence-engine/src/scan.js +128 -0
- package/src/a11y-evidence-engine/test/scan.test.js +149 -0
- package/src/a11y-evidence-engine/test/vectors.test.js +200 -0
- package/src/a11y-lint/.github/workflows/ci.yml +46 -0
- package/src/a11y-lint/.github/workflows/test.yml +34 -0
- package/src/a11y-lint/CODE_OF_CONDUCT.md +129 -0
- package/src/a11y-lint/CONTRIBUTING.md +70 -0
- package/src/a11y-lint/GOVERNANCE.md +57 -0
- package/src/a11y-lint/LICENSE +21 -0
- package/src/a11y-lint/PRESS_RELEASE.md +50 -0
- package/src/a11y-lint/README.md +276 -0
- package/src/a11y-lint/RELEASE_NOTES.md +57 -0
- package/src/a11y-lint/RELEASING.md +57 -0
- package/src/a11y-lint/a11y_lint/__init__.py +64 -0
- package/src/a11y-lint/a11y_lint/cli.py +319 -0
- package/src/a11y-lint/a11y_lint/errors.py +252 -0
- package/src/a11y-lint/a11y_lint/render.py +293 -0
- package/src/a11y-lint/a11y_lint/report_md.py +289 -0
- package/src/a11y-lint/a11y_lint/scan_cli_text.py +434 -0
- package/src/a11y-lint/a11y_lint/schemas/cli.error.schema.v0.1.json +83 -0
- package/src/a11y-lint/a11y_lint/scorecard.py +244 -0
- package/src/a11y-lint/a11y_lint/validate.py +225 -0
- package/src/a11y-lint/pyproject.toml +75 -0
- package/src/a11y-lint/tests/__init__.py +1 -0
- package/src/a11y-lint/tests/test_cli.py +200 -0
- package/src/a11y-lint/tests/test_errors.py +188 -0
- package/src/a11y-lint/tests/test_render.py +202 -0
- package/src/a11y-lint/tests/test_report_md.py +188 -0
- package/src/a11y-lint/tests/test_scan_cli_text.py +290 -0
- package/src/a11y-lint/tests/test_scorecard.py +195 -0
- package/src/a11y-lint/tests/test_validate.py +257 -0
- package/src/a11y-mcp-tools/.github/workflows/ci.yml +53 -0
- package/src/a11y-mcp-tools/CODE_OF_CONDUCT.md +129 -0
- package/src/a11y-mcp-tools/CONTRIBUTING.md +136 -0
- package/src/a11y-mcp-tools/LICENSE +21 -0
- package/src/a11y-mcp-tools/PROV_METHODS_CATALOG.md +104 -0
- package/src/a11y-mcp-tools/README.md +168 -0
- package/src/a11y-mcp-tools/bin/cli.js +452 -0
- package/src/a11y-mcp-tools/bin/server.js +244 -0
- package/src/a11y-mcp-tools/fixtures/requests/a11y.diagnose.ok.json +27 -0
- package/src/a11y-mcp-tools/fixtures/requests/a11y.evidence.ok.json +25 -0
- package/src/a11y-mcp-tools/fixtures/responses/a11y.diagnose.ok.json +139 -0
- package/src/a11y-mcp-tools/fixtures/responses/a11y.diagnose.provenance_fail.json +13 -0
- package/src/a11y-mcp-tools/fixtures/responses/a11y.evidence.ok.json +88 -0
- package/src/a11y-mcp-tools/package-lock.json +189 -0
- package/src/a11y-mcp-tools/package.json +49 -0
- package/src/a11y-mcp-tools/src/envelope.js +197 -0
- package/src/a11y-mcp-tools/src/index.js +9 -0
- package/src/a11y-mcp-tools/src/schemas/artifact.js +85 -0
- package/src/a11y-mcp-tools/src/schemas/diagnosis.schema.v0.1.json +137 -0
- package/src/a11y-mcp-tools/src/schemas/envelope.schema.v0.1.json +108 -0
- package/src/a11y-mcp-tools/src/schemas/evidence.bundle.schema.v0.1.json +129 -0
- package/src/a11y-mcp-tools/src/schemas/evidence.js +97 -0
- package/src/a11y-mcp-tools/src/schemas/index.js +11 -0
- package/src/a11y-mcp-tools/src/schemas/provenance.js +140 -0
- package/src/a11y-mcp-tools/src/schemas/tools/a11y.diagnose.request.schema.v0.1.json +77 -0
- package/src/a11y-mcp-tools/src/schemas/tools/a11y.diagnose.response.schema.v0.1.json +50 -0
- package/src/a11y-mcp-tools/src/schemas/tools/a11y.evidence.request.schema.v0.1.json +120 -0
- package/src/a11y-mcp-tools/src/schemas/tools/a11y.evidence.response.schema.v0.1.json +50 -0
- package/src/a11y-mcp-tools/src/tools/diagnose.js +597 -0
- package/src/a11y-mcp-tools/src/tools/evidence.js +481 -0
- package/src/a11y-mcp-tools/src/tools/index.js +10 -0
- package/src/a11y-mcp-tools/test/contract.test.mjs +154 -0
- package/src/a11y-mcp-tools/test/diagnose.test.js +485 -0
- package/src/a11y-mcp-tools/test/evidence.test.js +183 -0
- package/src/a11y-mcp-tools/test/schema.test.js +327 -0
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
"""Tests for the ingest command."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import shutil
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
from a11y_assist.ingest import (
|
|
12
|
+
IngestError,
|
|
13
|
+
build_advisories,
|
|
14
|
+
canonicalize,
|
|
15
|
+
group_by_file,
|
|
16
|
+
group_by_rule,
|
|
17
|
+
ingest,
|
|
18
|
+
load_findings,
|
|
19
|
+
verify_provenance,
|
|
20
|
+
write_advisories,
|
|
21
|
+
write_ingest_summary,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@pytest.fixture
|
|
26
|
+
def sample_findings(tmp_path: Path) -> Path:
|
|
27
|
+
"""Create a sample findings.json for testing."""
|
|
28
|
+
findings = {
|
|
29
|
+
"engine": "a11y-evidence-engine",
|
|
30
|
+
"version": "0.1.0",
|
|
31
|
+
"target": {"path": "./test-html"},
|
|
32
|
+
"summary": {"files_scanned": 2, "errors": 3, "warnings": 1, "info": 0},
|
|
33
|
+
"findings": [
|
|
34
|
+
{
|
|
35
|
+
"finding_id": "finding-0001",
|
|
36
|
+
"rule_id": "html.img.missing_alt",
|
|
37
|
+
"severity": "error",
|
|
38
|
+
"confidence": 0.98,
|
|
39
|
+
"message": "Image element is missing alt text.",
|
|
40
|
+
"location": {"file": "index.html", "json_pointer": "/nodes/5"},
|
|
41
|
+
"evidence_ref": {
|
|
42
|
+
"record": "provenance/finding-0001/record.json",
|
|
43
|
+
"digest": "provenance/finding-0001/digest.json",
|
|
44
|
+
"envelope": "provenance/finding-0001/envelope.json",
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"finding_id": "finding-0002",
|
|
49
|
+
"rule_id": "html.img.missing_alt",
|
|
50
|
+
"severity": "error",
|
|
51
|
+
"confidence": 0.98,
|
|
52
|
+
"message": "Image element is missing alt text.",
|
|
53
|
+
"location": {"file": "index.html", "json_pointer": "/nodes/8"},
|
|
54
|
+
"evidence_ref": {
|
|
55
|
+
"record": "provenance/finding-0002/record.json",
|
|
56
|
+
"digest": "provenance/finding-0002/digest.json",
|
|
57
|
+
"envelope": "provenance/finding-0002/envelope.json",
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"finding_id": "finding-0003",
|
|
62
|
+
"rule_id": "html.form_control.missing_label",
|
|
63
|
+
"severity": "error",
|
|
64
|
+
"confidence": 0.95,
|
|
65
|
+
"message": "Form control is missing an associated label.",
|
|
66
|
+
"location": {"file": "form.html", "json_pointer": "/nodes/3"},
|
|
67
|
+
"evidence_ref": {
|
|
68
|
+
"record": "provenance/finding-0003/record.json",
|
|
69
|
+
"digest": "provenance/finding-0003/digest.json",
|
|
70
|
+
"envelope": "provenance/finding-0003/envelope.json",
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
"finding_id": "finding-0004",
|
|
75
|
+
"rule_id": "html.document.missing_lang",
|
|
76
|
+
"severity": "warning",
|
|
77
|
+
"confidence": 1.0,
|
|
78
|
+
"message": "Document is missing lang attribute.",
|
|
79
|
+
"location": {"file": "form.html", "json_pointer": "/nodes/0"},
|
|
80
|
+
"evidence_ref": {
|
|
81
|
+
"record": "provenance/finding-0004/record.json",
|
|
82
|
+
"digest": "provenance/finding-0004/digest.json",
|
|
83
|
+
"envelope": "provenance/finding-0004/envelope.json",
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
findings_path = tmp_path / "findings.json"
|
|
90
|
+
findings_path.write_text(json.dumps(findings, indent=2))
|
|
91
|
+
return findings_path
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@pytest.fixture
|
|
95
|
+
def findings_with_provenance(tmp_path: Path, sample_findings: Path) -> Path:
|
|
96
|
+
"""Create findings with valid provenance bundles."""
|
|
97
|
+
# Create provenance directories
|
|
98
|
+
for i in range(1, 5):
|
|
99
|
+
prov_dir = tmp_path / f"provenance/finding-000{i}"
|
|
100
|
+
prov_dir.mkdir(parents=True, exist_ok=True)
|
|
101
|
+
|
|
102
|
+
# Evidence content
|
|
103
|
+
evidence = {
|
|
104
|
+
"document_ref": "test.html",
|
|
105
|
+
"pointer": f"/nodes/{i}",
|
|
106
|
+
"evidence": {"tagName": "img", "attrs": {}},
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
# Compute digest
|
|
110
|
+
canonical = canonicalize(evidence)
|
|
111
|
+
import hashlib
|
|
112
|
+
|
|
113
|
+
digest_value = hashlib.sha256(canonical.encode("utf-8")).hexdigest()
|
|
114
|
+
|
|
115
|
+
# Write record.json
|
|
116
|
+
record = {
|
|
117
|
+
"prov.record.v0.1": {
|
|
118
|
+
"method_id": "engine.extract.evidence.json_pointer",
|
|
119
|
+
"timestamp": "2026-01-26T00:00:00Z",
|
|
120
|
+
"inputs": [],
|
|
121
|
+
"outputs": [{"artifact.v0.1": {"name": "evidence", "content": evidence}}],
|
|
122
|
+
"agent": {"name": "test", "version": "1.0"},
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
(prov_dir / "record.json").write_text(json.dumps(record))
|
|
126
|
+
|
|
127
|
+
# Write digest.json
|
|
128
|
+
digest = {
|
|
129
|
+
"prov.record.v0.1": {
|
|
130
|
+
"method_id": "integrity.digest.sha256",
|
|
131
|
+
"timestamp": "2026-01-26T00:00:00Z",
|
|
132
|
+
"inputs": [],
|
|
133
|
+
"outputs": [
|
|
134
|
+
{
|
|
135
|
+
"artifact.v0.1": {
|
|
136
|
+
"name": "digest",
|
|
137
|
+
"digest": {"algorithm": "sha256", "value": digest_value},
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
],
|
|
141
|
+
"agent": {"name": "test", "version": "1.0"},
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
(prov_dir / "digest.json").write_text(json.dumps(digest))
|
|
145
|
+
|
|
146
|
+
# Write envelope.json
|
|
147
|
+
envelope = {"mcp.envelope.v0.1": {"result": {}, "provenance": {}}}
|
|
148
|
+
(prov_dir / "envelope.json").write_text(json.dumps(envelope))
|
|
149
|
+
|
|
150
|
+
return sample_findings
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class TestLoadFindings:
|
|
154
|
+
def test_load_valid_findings(self, sample_findings: Path):
|
|
155
|
+
data = load_findings(sample_findings)
|
|
156
|
+
assert data["engine"] == "a11y-evidence-engine"
|
|
157
|
+
assert len(data["findings"]) == 4
|
|
158
|
+
|
|
159
|
+
def test_load_missing_file(self, tmp_path: Path):
|
|
160
|
+
with pytest.raises(IngestError, match="not found"):
|
|
161
|
+
load_findings(tmp_path / "nonexistent.json")
|
|
162
|
+
|
|
163
|
+
def test_load_invalid_json(self, tmp_path: Path):
|
|
164
|
+
bad_file = tmp_path / "bad.json"
|
|
165
|
+
bad_file.write_text("not json")
|
|
166
|
+
with pytest.raises(IngestError, match="Invalid JSON"):
|
|
167
|
+
load_findings(bad_file)
|
|
168
|
+
|
|
169
|
+
def test_load_missing_required_fields(self, tmp_path: Path):
|
|
170
|
+
incomplete = tmp_path / "incomplete.json"
|
|
171
|
+
incomplete.write_text('{"engine": "test"}')
|
|
172
|
+
with pytest.raises(IngestError, match="Missing required"):
|
|
173
|
+
load_findings(incomplete)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
class TestCanonicalize:
|
|
177
|
+
def test_sorted_keys(self):
|
|
178
|
+
assert canonicalize({"z": 1, "a": 2}) == '{"a":2,"z":1}'
|
|
179
|
+
|
|
180
|
+
def test_nested_objects(self):
|
|
181
|
+
obj = {"b": {"d": 1, "c": 2}, "a": 1}
|
|
182
|
+
assert canonicalize(obj) == '{"a":1,"b":{"c":2,"d":1}}'
|
|
183
|
+
|
|
184
|
+
def test_arrays_preserve_order(self):
|
|
185
|
+
assert canonicalize([3, 1, 2]) == "[3,1,2]"
|
|
186
|
+
|
|
187
|
+
def test_null_and_booleans(self):
|
|
188
|
+
assert canonicalize(None) == "null"
|
|
189
|
+
assert canonicalize(True) == "true"
|
|
190
|
+
assert canonicalize(False) == "false"
|
|
191
|
+
|
|
192
|
+
def test_strings_escaped(self):
|
|
193
|
+
assert canonicalize('hello "world"') == '"hello \\"world\\""'
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
class TestGroupByRule:
|
|
197
|
+
def test_groups_correctly(self, sample_findings: Path):
|
|
198
|
+
data = load_findings(sample_findings)
|
|
199
|
+
grouped = group_by_rule(data["findings"])
|
|
200
|
+
|
|
201
|
+
assert len(grouped) == 3
|
|
202
|
+
# Should be sorted by count descending
|
|
203
|
+
assert grouped[0]["rule_id"] == "html.img.missing_alt"
|
|
204
|
+
assert grouped[0]["count"] == 2
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
class TestGroupByFile:
|
|
208
|
+
def test_groups_correctly(self, sample_findings: Path):
|
|
209
|
+
data = load_findings(sample_findings)
|
|
210
|
+
grouped = group_by_file(data["findings"])
|
|
211
|
+
|
|
212
|
+
assert len(grouped) == 2
|
|
213
|
+
# index.html has 2 errors, form.html has 1 error + 1 warning
|
|
214
|
+
assert grouped[0]["file"] == "index.html"
|
|
215
|
+
assert grouped[0]["errors"] == 2
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
class TestBuildAdvisories:
|
|
219
|
+
def test_builds_advisories(self, sample_findings: Path):
|
|
220
|
+
data = load_findings(sample_findings)
|
|
221
|
+
advisories = build_advisories(data["findings"])
|
|
222
|
+
|
|
223
|
+
assert len(advisories) == 3
|
|
224
|
+
# First advisory should be for most common rule
|
|
225
|
+
assert advisories[0]["rule_id"] == "html.img.missing_alt"
|
|
226
|
+
assert len(advisories[0]["instances"]) == 2
|
|
227
|
+
assert advisories[0]["title"] == "Add alt text to images"
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
class TestIngest:
|
|
231
|
+
def test_basic_ingest(self, sample_findings: Path):
|
|
232
|
+
result = ingest(sample_findings)
|
|
233
|
+
|
|
234
|
+
assert result.source_engine == "a11y-evidence-engine"
|
|
235
|
+
assert result.source_version == "0.1.0"
|
|
236
|
+
assert len(result.findings) == 4
|
|
237
|
+
assert len(result.by_rule) == 3
|
|
238
|
+
|
|
239
|
+
def test_filter_by_severity(self, sample_findings: Path):
|
|
240
|
+
result = ingest(sample_findings, min_severity="error")
|
|
241
|
+
|
|
242
|
+
# Should exclude the warning
|
|
243
|
+
assert len(result.findings) == 3
|
|
244
|
+
|
|
245
|
+
def test_provenance_verification_missing_files(self, sample_findings: Path):
|
|
246
|
+
result = ingest(sample_findings, verify_provenance_flag=True)
|
|
247
|
+
|
|
248
|
+
# Should have errors because provenance files don't exist
|
|
249
|
+
assert not result.provenance_verified
|
|
250
|
+
assert len(result.provenance_errors) > 0
|
|
251
|
+
|
|
252
|
+
def test_provenance_verification_success(self, findings_with_provenance: Path):
|
|
253
|
+
result = ingest(findings_with_provenance, verify_provenance_flag=True)
|
|
254
|
+
|
|
255
|
+
assert result.provenance_verified
|
|
256
|
+
assert len(result.provenance_errors) == 0
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
class TestWriteOutputs:
|
|
260
|
+
def test_write_summary(self, sample_findings: Path, tmp_path: Path):
|
|
261
|
+
result = ingest(sample_findings)
|
|
262
|
+
out_path = tmp_path / "output" / "ingest-summary.json"
|
|
263
|
+
|
|
264
|
+
write_ingest_summary(result, out_path)
|
|
265
|
+
|
|
266
|
+
assert out_path.exists()
|
|
267
|
+
data = json.loads(out_path.read_text())
|
|
268
|
+
assert data["source_engine"] == "a11y-evidence-engine"
|
|
269
|
+
assert "by_rule" in data
|
|
270
|
+
|
|
271
|
+
def test_write_advisories(self, sample_findings: Path, tmp_path: Path):
|
|
272
|
+
result = ingest(sample_findings)
|
|
273
|
+
out_path = tmp_path / "output" / "advisories.json"
|
|
274
|
+
|
|
275
|
+
write_advisories(result, out_path)
|
|
276
|
+
|
|
277
|
+
assert out_path.exists()
|
|
278
|
+
data = json.loads(out_path.read_text())
|
|
279
|
+
assert data["schema"] == "a11y-assist/advisories@v0.1"
|
|
280
|
+
assert len(data["advisories"]) == 3
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
class TestVerifyProvenance:
|
|
284
|
+
def test_missing_evidence_ref(self, tmp_path: Path):
|
|
285
|
+
finding = {"finding_id": "test-001"}
|
|
286
|
+
success, error = verify_provenance(finding, tmp_path)
|
|
287
|
+
|
|
288
|
+
assert not success
|
|
289
|
+
assert "Missing evidence_ref" in error
|
|
290
|
+
|
|
291
|
+
def test_missing_file(self, tmp_path: Path):
|
|
292
|
+
finding = {
|
|
293
|
+
"finding_id": "test-001",
|
|
294
|
+
"evidence_ref": {
|
|
295
|
+
"record": "provenance/record.json",
|
|
296
|
+
"digest": "provenance/digest.json",
|
|
297
|
+
"envelope": "provenance/envelope.json",
|
|
298
|
+
},
|
|
299
|
+
}
|
|
300
|
+
success, error = verify_provenance(finding, tmp_path)
|
|
301
|
+
|
|
302
|
+
assert not success
|
|
303
|
+
assert "not found" in error
|
|
304
|
+
|
|
305
|
+
def test_valid_provenance(self, findings_with_provenance: Path):
|
|
306
|
+
data = load_findings(findings_with_provenance)
|
|
307
|
+
base_dir = findings_with_provenance.parent
|
|
308
|
+
|
|
309
|
+
for finding in data["findings"]:
|
|
310
|
+
success, error = verify_provenance(finding, base_dir)
|
|
311
|
+
assert success, f"Failed for {finding['finding_id']}: {error}"
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
"""Tests for methods metadata (audit-only fields).
|
|
2
|
+
|
|
3
|
+
These tests verify:
|
|
4
|
+
1. Metadata is populated deterministically
|
|
5
|
+
2. Metadata does not affect rendering output
|
|
6
|
+
3. Evidence anchors are correct
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
import pytest
|
|
12
|
+
|
|
13
|
+
from a11y_assist.from_cli_error import assist_from_cli_error, load_cli_error
|
|
14
|
+
from a11y_assist.methods import (
|
|
15
|
+
METHOD_GUARD_VALIDATE,
|
|
16
|
+
METHOD_NORMALIZE_CLI_ERROR,
|
|
17
|
+
METHOD_NORMALIZE_RAW_TEXT,
|
|
18
|
+
METHOD_PROFILE_COGNITIVE_LOAD,
|
|
19
|
+
METHOD_PROFILE_DYSLEXIA,
|
|
20
|
+
METHOD_PROFILE_LOWVISION,
|
|
21
|
+
METHOD_PROFILE_PLAIN_LANGUAGE,
|
|
22
|
+
METHOD_PROFILE_SCREEN_READER,
|
|
23
|
+
with_method,
|
|
24
|
+
with_methods,
|
|
25
|
+
)
|
|
26
|
+
from a11y_assist.profiles import (
|
|
27
|
+
apply_cognitive_load,
|
|
28
|
+
apply_screen_reader,
|
|
29
|
+
render_cognitive_load,
|
|
30
|
+
)
|
|
31
|
+
from a11y_assist.render import AssistResult, Evidence, render_assist
|
|
32
|
+
|
|
33
|
+
FIXTURES_DIR = Path(__file__).parent / "fixtures"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class TestCliErrorMetadata:
|
|
37
|
+
"""Tests for metadata from cli.error.v0.1 path."""
|
|
38
|
+
|
|
39
|
+
@pytest.fixture
|
|
40
|
+
def result(self):
|
|
41
|
+
"""Load high-confidence fixture and get AssistResult."""
|
|
42
|
+
obj = load_cli_error(str(FIXTURES_DIR / "base_inputs" / "cli_error_high.json"))
|
|
43
|
+
return assist_from_cli_error(obj)
|
|
44
|
+
|
|
45
|
+
def test_methods_applied_contains_normalize(self, result):
|
|
46
|
+
"""Result should include normalization method."""
|
|
47
|
+
assert METHOD_NORMALIZE_CLI_ERROR in result.methods_applied
|
|
48
|
+
|
|
49
|
+
def test_evidence_for_safest_next_step(self, result):
|
|
50
|
+
"""Evidence should include safest_next_step source."""
|
|
51
|
+
fields = [e.field for e in result.evidence]
|
|
52
|
+
assert "safest_next_step" in fields
|
|
53
|
+
|
|
54
|
+
# Find the evidence for safest_next_step
|
|
55
|
+
for e in result.evidence:
|
|
56
|
+
if e.field == "safest_next_step":
|
|
57
|
+
assert e.source.startswith("cli.error.")
|
|
58
|
+
break
|
|
59
|
+
|
|
60
|
+
def test_evidence_for_plan_steps(self, result):
|
|
61
|
+
"""Evidence should include plan step sources."""
|
|
62
|
+
plan_evidence = [e for e in result.evidence if e.field.startswith("plan[")]
|
|
63
|
+
# Should have evidence for each plan step
|
|
64
|
+
assert len(plan_evidence) == len(result.plan)
|
|
65
|
+
|
|
66
|
+
# Each should reference cli.error.fix
|
|
67
|
+
for e in plan_evidence:
|
|
68
|
+
assert "cli.error.fix" in e.source
|
|
69
|
+
|
|
70
|
+
def test_evidence_for_safe_commands(self, result):
|
|
71
|
+
"""Evidence should include safe command sources."""
|
|
72
|
+
if result.next_safe_commands:
|
|
73
|
+
cmd_evidence = [
|
|
74
|
+
e for e in result.evidence if e.field.startswith("next_safe_commands[")
|
|
75
|
+
]
|
|
76
|
+
assert len(cmd_evidence) == len(result.next_safe_commands)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class TestMetadataDoesNotAffectRendering:
|
|
80
|
+
"""Verify that metadata doesn't change rendered output."""
|
|
81
|
+
|
|
82
|
+
@pytest.fixture
|
|
83
|
+
def result(self):
|
|
84
|
+
"""Load fixture and get AssistResult."""
|
|
85
|
+
obj = load_cli_error(str(FIXTURES_DIR / "base_inputs" / "cli_error_high.json"))
|
|
86
|
+
return assist_from_cli_error(obj)
|
|
87
|
+
|
|
88
|
+
def test_lowvision_render_unchanged_with_metadata(self, result):
|
|
89
|
+
"""Lowvision render should be same with or without metadata."""
|
|
90
|
+
# Render with metadata
|
|
91
|
+
output_with = render_assist(result)
|
|
92
|
+
|
|
93
|
+
# Create same result without metadata
|
|
94
|
+
result_without = AssistResult(
|
|
95
|
+
anchored_id=result.anchored_id,
|
|
96
|
+
confidence=result.confidence,
|
|
97
|
+
safest_next_step=result.safest_next_step,
|
|
98
|
+
plan=result.plan,
|
|
99
|
+
next_safe_commands=result.next_safe_commands,
|
|
100
|
+
notes=result.notes,
|
|
101
|
+
methods_applied=(),
|
|
102
|
+
evidence=(),
|
|
103
|
+
)
|
|
104
|
+
output_without = render_assist(result_without)
|
|
105
|
+
|
|
106
|
+
assert output_with == output_without
|
|
107
|
+
|
|
108
|
+
def test_cognitive_load_render_unchanged_with_metadata(self, result):
|
|
109
|
+
"""Cognitive load render should be same with or without metadata."""
|
|
110
|
+
transformed = apply_cognitive_load(result)
|
|
111
|
+
output_with = render_cognitive_load(transformed)
|
|
112
|
+
|
|
113
|
+
# Same transform but clear metadata
|
|
114
|
+
transformed_clean = AssistResult(
|
|
115
|
+
anchored_id=transformed.anchored_id,
|
|
116
|
+
confidence=transformed.confidence,
|
|
117
|
+
safest_next_step=transformed.safest_next_step,
|
|
118
|
+
plan=transformed.plan,
|
|
119
|
+
next_safe_commands=transformed.next_safe_commands,
|
|
120
|
+
notes=transformed.notes,
|
|
121
|
+
methods_applied=(),
|
|
122
|
+
evidence=(),
|
|
123
|
+
)
|
|
124
|
+
output_without = render_cognitive_load(transformed_clean)
|
|
125
|
+
|
|
126
|
+
assert output_with == output_without
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class TestMetadataDeterminism:
|
|
130
|
+
"""Verify metadata is deterministic."""
|
|
131
|
+
|
|
132
|
+
def test_same_input_same_methods(self):
|
|
133
|
+
"""Same input should produce same methods_applied."""
|
|
134
|
+
obj = load_cli_error(str(FIXTURES_DIR / "base_inputs" / "cli_error_high.json"))
|
|
135
|
+
|
|
136
|
+
results = []
|
|
137
|
+
for _ in range(3):
|
|
138
|
+
result = assist_from_cli_error(obj)
|
|
139
|
+
results.append(result.methods_applied)
|
|
140
|
+
|
|
141
|
+
# All should be identical
|
|
142
|
+
assert all(r == results[0] for r in results)
|
|
143
|
+
|
|
144
|
+
def test_same_input_same_evidence(self):
|
|
145
|
+
"""Same input should produce same evidence."""
|
|
146
|
+
obj = load_cli_error(str(FIXTURES_DIR / "base_inputs" / "cli_error_high.json"))
|
|
147
|
+
|
|
148
|
+
results = []
|
|
149
|
+
for _ in range(3):
|
|
150
|
+
result = assist_from_cli_error(obj)
|
|
151
|
+
results.append(result.evidence)
|
|
152
|
+
|
|
153
|
+
# All should be identical
|
|
154
|
+
assert all(r == results[0] for r in results)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class TestWithMethodsHelpers:
|
|
158
|
+
"""Tests for the methods.py helper functions."""
|
|
159
|
+
|
|
160
|
+
@pytest.fixture
|
|
161
|
+
def base_result(self):
|
|
162
|
+
"""Create a minimal AssistResult."""
|
|
163
|
+
return AssistResult(
|
|
164
|
+
anchored_id="TEST.001",
|
|
165
|
+
confidence="High",
|
|
166
|
+
safest_next_step="Do something.",
|
|
167
|
+
plan=["Step 1", "Step 2"],
|
|
168
|
+
next_safe_commands=[],
|
|
169
|
+
notes=[],
|
|
170
|
+
methods_applied=(),
|
|
171
|
+
evidence=(),
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
def test_with_method_adds_single(self, base_result):
|
|
175
|
+
"""with_method should add a single method ID."""
|
|
176
|
+
updated = with_method(base_result, "test.method")
|
|
177
|
+
assert "test.method" in updated.methods_applied
|
|
178
|
+
|
|
179
|
+
def test_with_method_preserves_existing(self, base_result):
|
|
180
|
+
"""with_method should preserve existing methods."""
|
|
181
|
+
updated = with_method(base_result, "first.method")
|
|
182
|
+
updated = with_method(updated, "second.method")
|
|
183
|
+
assert "first.method" in updated.methods_applied
|
|
184
|
+
assert "second.method" in updated.methods_applied
|
|
185
|
+
|
|
186
|
+
def test_with_method_deduplicates(self, base_result):
|
|
187
|
+
"""with_method should not add duplicates."""
|
|
188
|
+
updated = with_method(base_result, "test.method")
|
|
189
|
+
updated = with_method(updated, "test.method")
|
|
190
|
+
assert updated.methods_applied.count("test.method") == 1
|
|
191
|
+
|
|
192
|
+
def test_with_methods_adds_multiple(self, base_result):
|
|
193
|
+
"""with_methods should add multiple method IDs."""
|
|
194
|
+
updated = with_methods(base_result, ["method.a", "method.b", "method.c"])
|
|
195
|
+
assert "method.a" in updated.methods_applied
|
|
196
|
+
assert "method.b" in updated.methods_applied
|
|
197
|
+
assert "method.c" in updated.methods_applied
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
class TestMethodIDConstants:
|
|
201
|
+
"""Verify method ID constants are stable and well-formed."""
|
|
202
|
+
|
|
203
|
+
def test_normalize_methods_exist(self):
|
|
204
|
+
"""Normalization method IDs should exist."""
|
|
205
|
+
assert METHOD_NORMALIZE_CLI_ERROR == "engine.normalize.from_cli_error_v0_1"
|
|
206
|
+
assert METHOD_NORMALIZE_RAW_TEXT == "engine.normalize.from_raw_text"
|
|
207
|
+
|
|
208
|
+
def test_profile_methods_exist(self):
|
|
209
|
+
"""Profile method IDs should exist."""
|
|
210
|
+
assert METHOD_PROFILE_LOWVISION == "profile.lowvision.apply"
|
|
211
|
+
assert METHOD_PROFILE_COGNITIVE_LOAD == "profile.cognitive_load.apply"
|
|
212
|
+
assert METHOD_PROFILE_SCREEN_READER == "profile.screen_reader.apply"
|
|
213
|
+
assert METHOD_PROFILE_DYSLEXIA == "profile.dyslexia.apply"
|
|
214
|
+
assert METHOD_PROFILE_PLAIN_LANGUAGE == "profile.plain_language.apply"
|
|
215
|
+
|
|
216
|
+
def test_guard_methods_exist(self):
|
|
217
|
+
"""Guard method IDs should exist."""
|
|
218
|
+
assert METHOD_GUARD_VALIDATE == "guard.validate_profile_transform"
|
|
219
|
+
|
|
220
|
+
def test_method_ids_are_dotted_namespace(self):
|
|
221
|
+
"""All method IDs should follow dotted namespace convention."""
|
|
222
|
+
all_methods = [
|
|
223
|
+
METHOD_NORMALIZE_CLI_ERROR,
|
|
224
|
+
METHOD_NORMALIZE_RAW_TEXT,
|
|
225
|
+
METHOD_PROFILE_LOWVISION,
|
|
226
|
+
METHOD_PROFILE_COGNITIVE_LOAD,
|
|
227
|
+
METHOD_PROFILE_SCREEN_READER,
|
|
228
|
+
METHOD_PROFILE_DYSLEXIA,
|
|
229
|
+
METHOD_PROFILE_PLAIN_LANGUAGE,
|
|
230
|
+
METHOD_GUARD_VALIDATE,
|
|
231
|
+
]
|
|
232
|
+
for method in all_methods:
|
|
233
|
+
# Should have at least one dot (namespace.name)
|
|
234
|
+
assert "." in method, f"{method} should be dotted"
|
|
235
|
+
# Should not have uppercase
|
|
236
|
+
assert method == method.lower(), f"{method} should be lowercase"
|