@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,703 @@
|
|
|
1
|
+
"""Tests for screen-reader profile transformation and rendering.
|
|
2
|
+
|
|
3
|
+
These tests enforce the invariants:
|
|
4
|
+
1. No invented facts - only rephrases existing content
|
|
5
|
+
2. No invented commands - SAFE commands must be verbatim from input
|
|
6
|
+
3. SAFE-only remains absolute
|
|
7
|
+
4. Additive behavior - doesn't rewrite original output
|
|
8
|
+
5. Deterministic - no randomness, no network calls
|
|
9
|
+
6. No meaning in punctuation/formatting alone
|
|
10
|
+
7. No "visual navigation" references
|
|
11
|
+
8. No parentheticals as meaning carriers
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import pytest
|
|
15
|
+
|
|
16
|
+
from a11y_assist.profiles.screen_reader import (
|
|
17
|
+
MAX_NOTE_LENGTH,
|
|
18
|
+
MAX_STEP_LENGTH,
|
|
19
|
+
MAX_STEPS_DEFAULT,
|
|
20
|
+
MAX_STEPS_LOW,
|
|
21
|
+
_cap_length,
|
|
22
|
+
_expand_abbreviations,
|
|
23
|
+
_one_sentence,
|
|
24
|
+
_remove_parentheticals,
|
|
25
|
+
_remove_visual_references,
|
|
26
|
+
_replace_symbols,
|
|
27
|
+
_strip_boilerplate,
|
|
28
|
+
apply_screen_reader,
|
|
29
|
+
generate_summary,
|
|
30
|
+
normalize_safest_step,
|
|
31
|
+
normalize_step,
|
|
32
|
+
reduce_notes,
|
|
33
|
+
reduce_plan,
|
|
34
|
+
select_safe_command,
|
|
35
|
+
)
|
|
36
|
+
from a11y_assist.profiles.screen_reader_render import render_screen_reader
|
|
37
|
+
from a11y_assist.render import AssistResult
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class TestProfileInvariants:
|
|
41
|
+
"""A) Profile invariants - anchored_id, no new commands, no invented IDs."""
|
|
42
|
+
|
|
43
|
+
def test_anchored_id_preserved(self):
|
|
44
|
+
"""Output anchored_id is identical to input anchored_id."""
|
|
45
|
+
result = AssistResult(
|
|
46
|
+
anchored_id="PAY.EXPORT.SFTP.AUTH",
|
|
47
|
+
confidence="High",
|
|
48
|
+
safest_next_step="Check credentials.",
|
|
49
|
+
plan=["Step 1"],
|
|
50
|
+
next_safe_commands=["cmd --dry-run"],
|
|
51
|
+
notes=[],
|
|
52
|
+
)
|
|
53
|
+
transformed = apply_screen_reader(result)
|
|
54
|
+
assert transformed.anchored_id == result.anchored_id
|
|
55
|
+
|
|
56
|
+
def test_none_anchored_id_preserved(self):
|
|
57
|
+
"""None anchored_id stays None."""
|
|
58
|
+
result = AssistResult(
|
|
59
|
+
anchored_id=None,
|
|
60
|
+
confidence="Low",
|
|
61
|
+
safest_next_step="Try again.",
|
|
62
|
+
plan=["Step 1"],
|
|
63
|
+
next_safe_commands=[],
|
|
64
|
+
notes=[],
|
|
65
|
+
)
|
|
66
|
+
transformed = apply_screen_reader(result)
|
|
67
|
+
assert transformed.anchored_id is None
|
|
68
|
+
|
|
69
|
+
def test_no_new_commands_added(self):
|
|
70
|
+
"""No new commands appear that weren't in input."""
|
|
71
|
+
result = AssistResult(
|
|
72
|
+
anchored_id="TEST.ID",
|
|
73
|
+
confidence="High",
|
|
74
|
+
safest_next_step="Do something.",
|
|
75
|
+
plan=["Step 1"],
|
|
76
|
+
next_safe_commands=["original-cmd --dry-run"],
|
|
77
|
+
notes=[],
|
|
78
|
+
)
|
|
79
|
+
transformed = apply_screen_reader(result)
|
|
80
|
+
# At most 1 command
|
|
81
|
+
assert len(transformed.next_safe_commands) <= 1
|
|
82
|
+
# Any command must derive from original (minus $ prefix)
|
|
83
|
+
if transformed.next_safe_commands:
|
|
84
|
+
cmd = transformed.next_safe_commands[0]
|
|
85
|
+
# Should match original or be original without $ prefix
|
|
86
|
+
assert cmd in result.next_safe_commands or f"$ {cmd}" in result.next_safe_commands
|
|
87
|
+
|
|
88
|
+
def test_no_invented_ids(self):
|
|
89
|
+
"""No new IDs are invented."""
|
|
90
|
+
result = AssistResult(
|
|
91
|
+
anchored_id="ORIG.ID",
|
|
92
|
+
confidence="High",
|
|
93
|
+
safest_next_step="Check.",
|
|
94
|
+
plan=["Step 1"],
|
|
95
|
+
next_safe_commands=[],
|
|
96
|
+
notes=[],
|
|
97
|
+
)
|
|
98
|
+
transformed = apply_screen_reader(result)
|
|
99
|
+
# anchored_id must be exactly the same
|
|
100
|
+
assert transformed.anchored_id == "ORIG.ID"
|
|
101
|
+
|
|
102
|
+
def test_low_confidence_no_commands(self):
|
|
103
|
+
"""Low confidence results in no command section."""
|
|
104
|
+
result = AssistResult(
|
|
105
|
+
anchored_id=None,
|
|
106
|
+
confidence="Low",
|
|
107
|
+
safest_next_step="Try again.",
|
|
108
|
+
plan=["Step 1"],
|
|
109
|
+
next_safe_commands=["cmd --dry-run", "cmd --validate"],
|
|
110
|
+
notes=[],
|
|
111
|
+
)
|
|
112
|
+
transformed = apply_screen_reader(result)
|
|
113
|
+
assert transformed.next_safe_commands == []
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class TestAudioSpecificConstraints:
|
|
117
|
+
"""B) Audio-specific constraints - no parentheticals, no visual refs, Step N: format."""
|
|
118
|
+
|
|
119
|
+
def test_no_parentheticals_in_output(self):
|
|
120
|
+
"""Output contains no ( or ) or [ or ]."""
|
|
121
|
+
result = AssistResult(
|
|
122
|
+
anchored_id="TEST.ID",
|
|
123
|
+
confidence="High",
|
|
124
|
+
safest_next_step="Check the config (usually in /etc).",
|
|
125
|
+
plan=[
|
|
126
|
+
"First check [see docs]",
|
|
127
|
+
"Then run (optional)",
|
|
128
|
+
"Finally verify [important]",
|
|
129
|
+
],
|
|
130
|
+
next_safe_commands=["cmd --dry-run"],
|
|
131
|
+
notes=["Note with (parenthetical) content"],
|
|
132
|
+
)
|
|
133
|
+
transformed = apply_screen_reader(result)
|
|
134
|
+
output = render_screen_reader(transformed)
|
|
135
|
+
|
|
136
|
+
# Check no parenthetical characters
|
|
137
|
+
assert "(" not in output, f"Found ( in output: {output}"
|
|
138
|
+
assert ")" not in output, f"Found ) in output: {output}"
|
|
139
|
+
assert "[" not in output, f"Found [ in output: {output}"
|
|
140
|
+
assert "]" not in output, f"Found ] in output: {output}"
|
|
141
|
+
|
|
142
|
+
def test_no_visual_navigation_references(self):
|
|
143
|
+
"""Output contains no 'see above/below/left/right/arrow'."""
|
|
144
|
+
result = AssistResult(
|
|
145
|
+
anchored_id="TEST.ID",
|
|
146
|
+
confidence="High",
|
|
147
|
+
safest_next_step="See above for details.",
|
|
148
|
+
plan=[
|
|
149
|
+
"Check the error above",
|
|
150
|
+
"Look at the output below",
|
|
151
|
+
"Click the left arrow",
|
|
152
|
+
"Move right to continue",
|
|
153
|
+
],
|
|
154
|
+
next_safe_commands=[],
|
|
155
|
+
notes=["See above for more info"],
|
|
156
|
+
)
|
|
157
|
+
transformed = apply_screen_reader(result)
|
|
158
|
+
output = render_screen_reader(transformed).lower()
|
|
159
|
+
|
|
160
|
+
assert "see above" not in output
|
|
161
|
+
assert "see below" not in output
|
|
162
|
+
assert "above" not in output or "above" in output.split("step")[-1] # Allow in step numbers context
|
|
163
|
+
assert "below" not in output
|
|
164
|
+
assert "left" not in output
|
|
165
|
+
assert "right" not in output
|
|
166
|
+
assert "arrow" not in output
|
|
167
|
+
|
|
168
|
+
def test_steps_use_step_n_format(self):
|
|
169
|
+
"""Each step starts with Step N:."""
|
|
170
|
+
result = AssistResult(
|
|
171
|
+
anchored_id="TEST.ID",
|
|
172
|
+
confidence="High",
|
|
173
|
+
safest_next_step="Do something.",
|
|
174
|
+
plan=["First step", "Second step", "Third step"],
|
|
175
|
+
next_safe_commands=[],
|
|
176
|
+
notes=[],
|
|
177
|
+
)
|
|
178
|
+
transformed = apply_screen_reader(result)
|
|
179
|
+
output = render_screen_reader(transformed)
|
|
180
|
+
|
|
181
|
+
assert "Step 1:" in output
|
|
182
|
+
assert "Step 2:" in output
|
|
183
|
+
assert "Step 3:" in output
|
|
184
|
+
|
|
185
|
+
def test_steps_end_with_period(self):
|
|
186
|
+
"""Each step ends with a period."""
|
|
187
|
+
result = AssistResult(
|
|
188
|
+
anchored_id="TEST.ID",
|
|
189
|
+
confidence="High",
|
|
190
|
+
safest_next_step="Do something.",
|
|
191
|
+
plan=["First step", "Second step no period"],
|
|
192
|
+
next_safe_commands=[],
|
|
193
|
+
notes=[],
|
|
194
|
+
)
|
|
195
|
+
transformed = apply_screen_reader(result)
|
|
196
|
+
|
|
197
|
+
for step in transformed.plan:
|
|
198
|
+
assert step.endswith("."), f"Step does not end with period: {step}"
|
|
199
|
+
|
|
200
|
+
def test_steps_length_capped(self):
|
|
201
|
+
"""Steps are <= MAX_STEP_LENGTH chars (including ellipsis if truncated)."""
|
|
202
|
+
long_step = "A" * 200
|
|
203
|
+
result = AssistResult(
|
|
204
|
+
anchored_id="TEST.ID",
|
|
205
|
+
confidence="High",
|
|
206
|
+
safest_next_step="Do something.",
|
|
207
|
+
plan=[long_step],
|
|
208
|
+
next_safe_commands=[],
|
|
209
|
+
notes=[],
|
|
210
|
+
)
|
|
211
|
+
transformed = apply_screen_reader(result)
|
|
212
|
+
|
|
213
|
+
for step in transformed.plan:
|
|
214
|
+
assert len(step) <= MAX_STEP_LENGTH + 1, f"Step too long: {len(step)}"
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
class TestDeterminism:
|
|
218
|
+
"""C) Determinism tests - same input always produces same output."""
|
|
219
|
+
|
|
220
|
+
def test_same_input_same_output(self):
|
|
221
|
+
"""Same input always produces same output."""
|
|
222
|
+
result = AssistResult(
|
|
223
|
+
anchored_id="DET.TEST",
|
|
224
|
+
confidence="Medium",
|
|
225
|
+
safest_next_step="Check the logs (verbose mode).",
|
|
226
|
+
plan=[
|
|
227
|
+
"First check the config and restart",
|
|
228
|
+
"Then verify the connection",
|
|
229
|
+
"Finally update the cache",
|
|
230
|
+
"Extra step that may get dropped",
|
|
231
|
+
"Another extra step",
|
|
232
|
+
],
|
|
233
|
+
next_safe_commands=["tool --dry-run", "tool --validate"],
|
|
234
|
+
notes=["Note with (parenthetical)", "Another note"],
|
|
235
|
+
)
|
|
236
|
+
# Run transformation multiple times
|
|
237
|
+
outputs = [apply_screen_reader(result) for _ in range(10)]
|
|
238
|
+
# All outputs should be identical
|
|
239
|
+
first = outputs[0]
|
|
240
|
+
for output in outputs[1:]:
|
|
241
|
+
assert output == first
|
|
242
|
+
|
|
243
|
+
def test_render_deterministic(self):
|
|
244
|
+
"""Rendering is deterministic."""
|
|
245
|
+
result = AssistResult(
|
|
246
|
+
anchored_id="DET.RENDER",
|
|
247
|
+
confidence="High",
|
|
248
|
+
safest_next_step="Do something.",
|
|
249
|
+
plan=["Step 1", "Step 2"],
|
|
250
|
+
next_safe_commands=["cmd --dry-run"],
|
|
251
|
+
notes=["A note"],
|
|
252
|
+
)
|
|
253
|
+
outputs = [render_screen_reader(result) for _ in range(10)]
|
|
254
|
+
first = outputs[0]
|
|
255
|
+
for output in outputs[1:]:
|
|
256
|
+
assert output == first
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
class TestNormalizationHelpers:
|
|
260
|
+
"""Tests for individual normalization functions."""
|
|
261
|
+
|
|
262
|
+
def test_strip_boilerplate_run(self):
|
|
263
|
+
"""'Run:' prefix is stripped."""
|
|
264
|
+
assert _strip_boilerplate("Run: mytool") == "mytool"
|
|
265
|
+
|
|
266
|
+
def test_strip_boilerplate_rerun(self):
|
|
267
|
+
"""'Re-run:' prefix is stripped."""
|
|
268
|
+
assert _strip_boilerplate("Re-run: command") == "command"
|
|
269
|
+
|
|
270
|
+
def test_strip_boilerplate_dollar_sign(self):
|
|
271
|
+
"""'$ ' prefix is stripped."""
|
|
272
|
+
assert _strip_boilerplate("$ ls -la") == "ls -la"
|
|
273
|
+
|
|
274
|
+
def test_strip_boilerplate_next(self):
|
|
275
|
+
"""'Next:' prefix is stripped."""
|
|
276
|
+
assert _strip_boilerplate("Next: do this") == "do this"
|
|
277
|
+
|
|
278
|
+
def test_remove_parentheticals_round(self):
|
|
279
|
+
"""Round parentheses content is removed."""
|
|
280
|
+
assert _remove_parentheticals("Do thing (optional)") == "Do thing"
|
|
281
|
+
|
|
282
|
+
def test_remove_parentheticals_square(self):
|
|
283
|
+
"""Square bracket content is removed."""
|
|
284
|
+
assert _remove_parentheticals("Run [see docs]") == "Run"
|
|
285
|
+
|
|
286
|
+
def test_remove_visual_references_see_above(self):
|
|
287
|
+
"""'see above' is removed."""
|
|
288
|
+
result = _remove_visual_references("Check see above for info")
|
|
289
|
+
assert "above" not in result.lower()
|
|
290
|
+
|
|
291
|
+
def test_remove_visual_references_below(self):
|
|
292
|
+
"""'below' is removed."""
|
|
293
|
+
result = _remove_visual_references("Look at the output below")
|
|
294
|
+
assert "below" not in result.lower()
|
|
295
|
+
|
|
296
|
+
def test_remove_visual_references_arrow(self):
|
|
297
|
+
"""'arrow' is removed."""
|
|
298
|
+
result = _remove_visual_references("Click the arrow")
|
|
299
|
+
assert "arrow" not in result.lower()
|
|
300
|
+
|
|
301
|
+
def test_expand_abbreviations_cli(self):
|
|
302
|
+
"""CLI expands to 'command line'."""
|
|
303
|
+
assert "command line" in _expand_abbreviations("Use the CLI tool")
|
|
304
|
+
|
|
305
|
+
def test_expand_abbreviations_id(self):
|
|
306
|
+
"""ID expands to 'I D'."""
|
|
307
|
+
assert "I D" in _expand_abbreviations("Check the ID")
|
|
308
|
+
|
|
309
|
+
def test_expand_abbreviations_json(self):
|
|
310
|
+
"""JSON expands to 'J S O N'."""
|
|
311
|
+
assert "J S O N" in _expand_abbreviations("Parse JSON")
|
|
312
|
+
|
|
313
|
+
def test_expand_abbreviations_sftp(self):
|
|
314
|
+
"""SFTP expands to 'S F T P'."""
|
|
315
|
+
assert "S F T P" in _expand_abbreviations("Upload via SFTP")
|
|
316
|
+
|
|
317
|
+
def test_replace_symbols_arrow(self):
|
|
318
|
+
"""-> replaces with 'to'."""
|
|
319
|
+
assert " to " in _replace_symbols("A -> B")
|
|
320
|
+
|
|
321
|
+
def test_replace_symbols_fat_arrow(self):
|
|
322
|
+
"""=> replaces with 'to'."""
|
|
323
|
+
assert " to " in _replace_symbols("A => B")
|
|
324
|
+
|
|
325
|
+
def test_replace_symbols_ampersand(self):
|
|
326
|
+
"""& replaces with 'and'."""
|
|
327
|
+
assert " and " in _replace_symbols("A & B")
|
|
328
|
+
|
|
329
|
+
def test_one_sentence_semicolon(self):
|
|
330
|
+
"""Semicolon splits and keeps first."""
|
|
331
|
+
result = _one_sentence("First part; second part")
|
|
332
|
+
assert "second" not in result
|
|
333
|
+
|
|
334
|
+
def test_cap_length_adds_ellipsis(self):
|
|
335
|
+
"""Long strings are capped with ellipsis."""
|
|
336
|
+
result = _cap_length("A" * 200, 100)
|
|
337
|
+
assert len(result) == 100
|
|
338
|
+
assert result.endswith("…")
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
class TestStepNormalization:
|
|
342
|
+
"""Tests for full step normalization."""
|
|
343
|
+
|
|
344
|
+
def test_normalize_step_complete(self):
|
|
345
|
+
"""Full normalization pipeline works."""
|
|
346
|
+
step = "Run: Check the CLI (see docs) and verify -> continue"
|
|
347
|
+
result = normalize_step(step)
|
|
348
|
+
# Should not have parenthetical
|
|
349
|
+
assert "(" not in result
|
|
350
|
+
assert ")" not in result
|
|
351
|
+
# Should have expanded CLI
|
|
352
|
+
assert "command line" in result
|
|
353
|
+
# Should have replaced arrow
|
|
354
|
+
assert "->" not in result
|
|
355
|
+
# Should end with period
|
|
356
|
+
assert result.endswith(".")
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
class TestPlanReduction:
|
|
360
|
+
"""Tests for plan reduction."""
|
|
361
|
+
|
|
362
|
+
def test_high_confidence_max_5_steps(self):
|
|
363
|
+
"""High confidence allows up to 5 steps."""
|
|
364
|
+
plan = ["A", "B", "C", "D", "E", "F"]
|
|
365
|
+
reduced = reduce_plan(plan, "High")
|
|
366
|
+
assert len(reduced) == MAX_STEPS_DEFAULT
|
|
367
|
+
|
|
368
|
+
def test_low_confidence_max_3_steps(self):
|
|
369
|
+
"""Low confidence reduces to max 3 steps."""
|
|
370
|
+
plan = ["A", "B", "C", "D", "E"]
|
|
371
|
+
reduced = reduce_plan(plan, "Low")
|
|
372
|
+
assert len(reduced) == MAX_STEPS_LOW
|
|
373
|
+
|
|
374
|
+
def test_empty_plan_gets_fallback(self):
|
|
375
|
+
"""Empty plan gets a fallback step."""
|
|
376
|
+
reduced = reduce_plan([], "High")
|
|
377
|
+
assert len(reduced) == 1
|
|
378
|
+
assert "Follow" in reduced[0]
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
class TestSafeCommandSelection:
|
|
382
|
+
"""Tests for SAFE command selection."""
|
|
383
|
+
|
|
384
|
+
def test_returns_none_for_low_confidence(self):
|
|
385
|
+
"""Low confidence returns None."""
|
|
386
|
+
assert select_safe_command(["cmd"], "Low") is None
|
|
387
|
+
|
|
388
|
+
def test_returns_none_for_empty_list(self):
|
|
389
|
+
"""Empty list returns None."""
|
|
390
|
+
assert select_safe_command([], "High") is None
|
|
391
|
+
|
|
392
|
+
def test_returns_first_command_for_high(self):
|
|
393
|
+
"""High confidence returns first command."""
|
|
394
|
+
result = select_safe_command(["cmd1", "cmd2"], "High")
|
|
395
|
+
assert result == "cmd1"
|
|
396
|
+
|
|
397
|
+
def test_strips_dollar_prefix(self):
|
|
398
|
+
"""$ prefix is stripped from command."""
|
|
399
|
+
result = select_safe_command(["$ cmd --dry-run"], "High")
|
|
400
|
+
assert result == "cmd --dry-run"
|
|
401
|
+
assert not result.startswith("$")
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
class TestNotesReduction:
|
|
405
|
+
"""Tests for notes reduction."""
|
|
406
|
+
|
|
407
|
+
def test_notes_limited_to_three(self):
|
|
408
|
+
"""Notes are limited to 3 max."""
|
|
409
|
+
notes = ["Note 1", "Note 2", "Note 3", "Note 4"]
|
|
410
|
+
reduced = reduce_notes(notes)
|
|
411
|
+
assert len(reduced) == 3
|
|
412
|
+
|
|
413
|
+
def test_notes_capped_at_length(self):
|
|
414
|
+
"""Notes are capped at MAX_NOTE_LENGTH."""
|
|
415
|
+
notes = ["A" * 200]
|
|
416
|
+
reduced = reduce_notes(notes)
|
|
417
|
+
assert len(reduced[0]) <= MAX_NOTE_LENGTH
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
class TestRenderScreenReader:
|
|
421
|
+
"""Tests for screen-reader renderer."""
|
|
422
|
+
|
|
423
|
+
def test_render_includes_profile_header(self):
|
|
424
|
+
"""Output includes screen reader profile header."""
|
|
425
|
+
result = AssistResult(
|
|
426
|
+
anchored_id="TEST.ID",
|
|
427
|
+
confidence="High",
|
|
428
|
+
safest_next_step="Do the thing.",
|
|
429
|
+
plan=["Step 1"],
|
|
430
|
+
next_safe_commands=[],
|
|
431
|
+
notes=[],
|
|
432
|
+
)
|
|
433
|
+
output = render_screen_reader(result)
|
|
434
|
+
assert "ASSIST. Profile: Screen reader." in output
|
|
435
|
+
|
|
436
|
+
def test_render_includes_anchored_id_spelled(self):
|
|
437
|
+
"""Anchored ID uses 'I D' spelling."""
|
|
438
|
+
result = AssistResult(
|
|
439
|
+
anchored_id="TEST.ID",
|
|
440
|
+
confidence="High",
|
|
441
|
+
safest_next_step="Do the thing.",
|
|
442
|
+
plan=["Step 1"],
|
|
443
|
+
next_safe_commands=[],
|
|
444
|
+
notes=[],
|
|
445
|
+
)
|
|
446
|
+
output = render_screen_reader(result)
|
|
447
|
+
assert "Anchored I D:" in output
|
|
448
|
+
|
|
449
|
+
def test_render_none_id_says_none(self):
|
|
450
|
+
"""None anchored ID says 'none'."""
|
|
451
|
+
result = AssistResult(
|
|
452
|
+
anchored_id=None,
|
|
453
|
+
confidence="Low",
|
|
454
|
+
safest_next_step="Try again.",
|
|
455
|
+
plan=["Step 1"],
|
|
456
|
+
next_safe_commands=[],
|
|
457
|
+
notes=[],
|
|
458
|
+
)
|
|
459
|
+
output = render_screen_reader(result)
|
|
460
|
+
assert "Anchored I D: none." in output
|
|
461
|
+
|
|
462
|
+
def test_render_includes_summary(self):
|
|
463
|
+
"""Output includes Summary section."""
|
|
464
|
+
result = AssistResult(
|
|
465
|
+
anchored_id="TEST.ID",
|
|
466
|
+
confidence="High",
|
|
467
|
+
safest_next_step="Do the thing.",
|
|
468
|
+
plan=["Step 1"],
|
|
469
|
+
next_safe_commands=[],
|
|
470
|
+
notes=[],
|
|
471
|
+
)
|
|
472
|
+
output = render_screen_reader(result)
|
|
473
|
+
assert "Summary:" in output
|
|
474
|
+
|
|
475
|
+
def test_render_includes_safest_next_step(self):
|
|
476
|
+
"""Output includes Safest next step."""
|
|
477
|
+
result = AssistResult(
|
|
478
|
+
anchored_id="TEST.ID",
|
|
479
|
+
confidence="High",
|
|
480
|
+
safest_next_step="Check config first.",
|
|
481
|
+
plan=["Step 1"],
|
|
482
|
+
next_safe_commands=[],
|
|
483
|
+
notes=[],
|
|
484
|
+
)
|
|
485
|
+
output = render_screen_reader(result)
|
|
486
|
+
assert "Safest next step:" in output
|
|
487
|
+
|
|
488
|
+
def test_render_includes_steps_section(self):
|
|
489
|
+
"""Output includes Steps section."""
|
|
490
|
+
result = AssistResult(
|
|
491
|
+
anchored_id="TEST.ID",
|
|
492
|
+
confidence="High",
|
|
493
|
+
safest_next_step="Do the thing.",
|
|
494
|
+
plan=["First step", "Second step"],
|
|
495
|
+
next_safe_commands=[],
|
|
496
|
+
notes=[],
|
|
497
|
+
)
|
|
498
|
+
output = render_screen_reader(result)
|
|
499
|
+
assert "Steps:" in output
|
|
500
|
+
|
|
501
|
+
def test_render_includes_safe_command(self):
|
|
502
|
+
"""Output includes next safe command when present."""
|
|
503
|
+
result = AssistResult(
|
|
504
|
+
anchored_id="TEST.ID",
|
|
505
|
+
confidence="High",
|
|
506
|
+
safest_next_step="Do the thing.",
|
|
507
|
+
plan=["Step 1"],
|
|
508
|
+
next_safe_commands=["tool --dry-run"],
|
|
509
|
+
notes=[],
|
|
510
|
+
)
|
|
511
|
+
output = render_screen_reader(result)
|
|
512
|
+
assert "Next safe command:" in output
|
|
513
|
+
assert "tool --dry-run" in output
|
|
514
|
+
|
|
515
|
+
def test_render_no_safe_command_section_when_empty(self):
|
|
516
|
+
"""No safe command section when no commands."""
|
|
517
|
+
result = AssistResult(
|
|
518
|
+
anchored_id="TEST.ID",
|
|
519
|
+
confidence="High",
|
|
520
|
+
safest_next_step="Do the thing.",
|
|
521
|
+
plan=["Step 1"],
|
|
522
|
+
next_safe_commands=[],
|
|
523
|
+
notes=[],
|
|
524
|
+
)
|
|
525
|
+
output = render_screen_reader(result)
|
|
526
|
+
assert "Next safe command:" not in output
|
|
527
|
+
|
|
528
|
+
def test_render_single_note_uses_note(self):
|
|
529
|
+
"""Single note uses 'Note:' not 'Notes:'."""
|
|
530
|
+
result = AssistResult(
|
|
531
|
+
anchored_id="TEST.ID",
|
|
532
|
+
confidence="High",
|
|
533
|
+
safest_next_step="Do the thing.",
|
|
534
|
+
plan=["Step 1"],
|
|
535
|
+
next_safe_commands=[],
|
|
536
|
+
notes=["One note."],
|
|
537
|
+
)
|
|
538
|
+
output = render_screen_reader(result)
|
|
539
|
+
assert "Note:" in output
|
|
540
|
+
# Check it's not "Notes:"
|
|
541
|
+
assert output.count("Notes:") == 0
|
|
542
|
+
|
|
543
|
+
def test_render_multiple_notes_uses_notes(self):
|
|
544
|
+
"""Multiple notes use 'Notes:'."""
|
|
545
|
+
result = AssistResult(
|
|
546
|
+
anchored_id="TEST.ID",
|
|
547
|
+
confidence="High",
|
|
548
|
+
safest_next_step="Do the thing.",
|
|
549
|
+
plan=["Step 1"],
|
|
550
|
+
next_safe_commands=[],
|
|
551
|
+
notes=["Note one.", "Note two."],
|
|
552
|
+
)
|
|
553
|
+
output = render_screen_reader(result)
|
|
554
|
+
assert "Notes:" in output
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
class TestGenerateSummary:
|
|
558
|
+
"""Tests for summary generation."""
|
|
559
|
+
|
|
560
|
+
def test_summary_from_original_title(self):
|
|
561
|
+
"""Summary extracts from 'Original title:' note."""
|
|
562
|
+
result = AssistResult(
|
|
563
|
+
anchored_id="TEST.ID",
|
|
564
|
+
confidence="High",
|
|
565
|
+
safest_next_step="Do the thing.",
|
|
566
|
+
plan=["Step 1"],
|
|
567
|
+
next_safe_commands=[],
|
|
568
|
+
notes=["Original title: Payment export failed"],
|
|
569
|
+
)
|
|
570
|
+
summary = generate_summary(result)
|
|
571
|
+
assert "Payment export failed" in summary
|
|
572
|
+
|
|
573
|
+
def test_summary_high_confidence_fallback(self):
|
|
574
|
+
"""High confidence without title gets generic summary."""
|
|
575
|
+
result = AssistResult(
|
|
576
|
+
anchored_id="TEST.ID",
|
|
577
|
+
confidence="High",
|
|
578
|
+
safest_next_step="Do the thing.",
|
|
579
|
+
plan=["Step 1"],
|
|
580
|
+
next_safe_commands=[],
|
|
581
|
+
notes=[],
|
|
582
|
+
)
|
|
583
|
+
summary = generate_summary(result)
|
|
584
|
+
assert "structured error" in summary.lower()
|
|
585
|
+
|
|
586
|
+
def test_summary_low_confidence_fallback(self):
|
|
587
|
+
"""Low confidence gets appropriate summary."""
|
|
588
|
+
result = AssistResult(
|
|
589
|
+
anchored_id=None,
|
|
590
|
+
confidence="Low",
|
|
591
|
+
safest_next_step="Do the thing.",
|
|
592
|
+
plan=["Step 1"],
|
|
593
|
+
next_safe_commands=[],
|
|
594
|
+
notes=[],
|
|
595
|
+
)
|
|
596
|
+
summary = generate_summary(result)
|
|
597
|
+
assert "identifier" in summary.lower() or "error" in summary.lower()
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
class TestGoldenOutput:
|
|
601
|
+
"""C) Golden tests - exact output committed to repo, run in CI."""
|
|
602
|
+
|
|
603
|
+
def test_golden_cli_error_invariants(self):
|
|
604
|
+
"""cli.error.v0.1 JSON produces consistent screen-reader output."""
|
|
605
|
+
# Test invariants without exact string matching (which varies by normalization)
|
|
606
|
+
from a11y_assist.from_cli_error import assist_from_cli_error, load_cli_error
|
|
607
|
+
import os
|
|
608
|
+
|
|
609
|
+
fixture_path = os.path.join(
|
|
610
|
+
os.path.dirname(__file__), "fixtures", "cli_error_good.json"
|
|
611
|
+
)
|
|
612
|
+
obj = load_cli_error(fixture_path)
|
|
613
|
+
result = assist_from_cli_error(obj)
|
|
614
|
+
transformed = apply_screen_reader(result)
|
|
615
|
+
output = render_screen_reader(transformed)
|
|
616
|
+
|
|
617
|
+
# Invariants
|
|
618
|
+
assert "ASSIST. Profile: Screen reader." in output
|
|
619
|
+
assert "Anchored I D: PAY.EXPORT.SFTP.AUTH." in output
|
|
620
|
+
assert "Confidence: High." in output
|
|
621
|
+
assert "Summary:" in output
|
|
622
|
+
assert "Safest next step:" in output
|
|
623
|
+
assert "Steps:" in output
|
|
624
|
+
assert "Step 1:" in output
|
|
625
|
+
# No parentheticals
|
|
626
|
+
assert "(" not in output
|
|
627
|
+
assert ")" not in output
|
|
628
|
+
|
|
629
|
+
def test_golden_raw_with_id_invariants(self):
|
|
630
|
+
"""Raw text with ID produces consistent screen-reader output."""
|
|
631
|
+
from a11y_assist.parse_raw import parse_raw
|
|
632
|
+
import os
|
|
633
|
+
|
|
634
|
+
fixture_path = os.path.join(
|
|
635
|
+
os.path.dirname(__file__), "fixtures", "raw_good.txt"
|
|
636
|
+
)
|
|
637
|
+
with open(fixture_path) as f:
|
|
638
|
+
text = f.read()
|
|
639
|
+
|
|
640
|
+
err_id, status, blocks = parse_raw(text)
|
|
641
|
+
plan = blocks.get("Fix:", [])
|
|
642
|
+
result = AssistResult(
|
|
643
|
+
anchored_id=err_id,
|
|
644
|
+
confidence="Medium",
|
|
645
|
+
safest_next_step="Follow the tool's Fix steps.",
|
|
646
|
+
plan=plan,
|
|
647
|
+
next_safe_commands=[line for line in plan if "--dry-run" in line][:3],
|
|
648
|
+
notes=[],
|
|
649
|
+
)
|
|
650
|
+
transformed = apply_screen_reader(result)
|
|
651
|
+
output = render_screen_reader(transformed)
|
|
652
|
+
|
|
653
|
+
# Invariants
|
|
654
|
+
assert "ASSIST. Profile: Screen reader." in output
|
|
655
|
+
assert "Anchored I D: PAY.EXPORT.SFTP.AUTH." in output
|
|
656
|
+
assert "Confidence: Medium." in output
|
|
657
|
+
assert "Step 1:" in output
|
|
658
|
+
# No parentheticals
|
|
659
|
+
assert "(" not in output
|
|
660
|
+
assert ")" not in output
|
|
661
|
+
|
|
662
|
+
def test_golden_raw_no_id_invariants(self):
|
|
663
|
+
"""Raw text without ID produces consistent screen-reader output."""
|
|
664
|
+
from a11y_assist.parse_raw import parse_raw
|
|
665
|
+
import os
|
|
666
|
+
|
|
667
|
+
fixture_path = os.path.join(
|
|
668
|
+
os.path.dirname(__file__), "fixtures", "raw_no_id.txt"
|
|
669
|
+
)
|
|
670
|
+
with open(fixture_path) as f:
|
|
671
|
+
text = f.read()
|
|
672
|
+
|
|
673
|
+
err_id, status, blocks = parse_raw(text)
|
|
674
|
+
result = AssistResult(
|
|
675
|
+
anchored_id=err_id,
|
|
676
|
+
confidence="Low",
|
|
677
|
+
safest_next_step="Follow the tool's Fix steps.",
|
|
678
|
+
plan=[
|
|
679
|
+
"Re-run the command with increased verbosity/logging.",
|
|
680
|
+
"Update the tool to emit (ID: ...) and What/Why/Fix blocks.",
|
|
681
|
+
"If this is your tool, adopt cli.error.v0.1 JSON output.",
|
|
682
|
+
],
|
|
683
|
+
next_safe_commands=[],
|
|
684
|
+
notes=["No (ID: ...) found."],
|
|
685
|
+
)
|
|
686
|
+
transformed = apply_screen_reader(result)
|
|
687
|
+
output = render_screen_reader(transformed)
|
|
688
|
+
|
|
689
|
+
# Invariants
|
|
690
|
+
assert "ASSIST. Profile: Screen reader." in output
|
|
691
|
+
assert "Anchored I D: none." in output
|
|
692
|
+
assert "Confidence: Low." in output
|
|
693
|
+
# Low confidence = max 3 steps
|
|
694
|
+
assert "Step 1:" in output
|
|
695
|
+
assert "Step 2:" in output
|
|
696
|
+
assert "Step 3:" in output
|
|
697
|
+
# No Step 4 for low confidence
|
|
698
|
+
assert "Step 4:" not in output
|
|
699
|
+
# No SAFE command section for low confidence
|
|
700
|
+
assert "Next safe command:" not in output
|
|
701
|
+
# No parentheticals in output
|
|
702
|
+
assert "(" not in output
|
|
703
|
+
assert ")" not in output
|