@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,469 @@
|
|
|
1
|
+
"""Tests for cognitive-load 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
|
+
"""
|
|
10
|
+
|
|
11
|
+
import pytest
|
|
12
|
+
|
|
13
|
+
from a11y_assist.profiles.cognitive_load import (
|
|
14
|
+
MAX_STEP_LENGTH,
|
|
15
|
+
_cap_length,
|
|
16
|
+
_reduce_conjunctions,
|
|
17
|
+
_remove_parentheticals,
|
|
18
|
+
_strip_boilerplate,
|
|
19
|
+
_to_imperative,
|
|
20
|
+
apply_cognitive_load,
|
|
21
|
+
normalize_safest_step,
|
|
22
|
+
normalize_step,
|
|
23
|
+
reduce_notes,
|
|
24
|
+
reduce_plan,
|
|
25
|
+
select_safe_command,
|
|
26
|
+
)
|
|
27
|
+
from a11y_assist.profiles.cognitive_load_render import (
|
|
28
|
+
STEP_LABELS,
|
|
29
|
+
render_cognitive_load,
|
|
30
|
+
)
|
|
31
|
+
from a11y_assist.render import AssistResult
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class TestNoInvention:
|
|
35
|
+
"""7.1 No invention test: output contains only input-derived content."""
|
|
36
|
+
|
|
37
|
+
def test_no_extra_plan_steps_added(self):
|
|
38
|
+
"""Transformation never adds plan steps beyond input."""
|
|
39
|
+
result = AssistResult(
|
|
40
|
+
anchored_id="TEST.ID",
|
|
41
|
+
confidence="High",
|
|
42
|
+
safest_next_step="Check the config file.",
|
|
43
|
+
plan=["Step A", "Step B"],
|
|
44
|
+
next_safe_commands=["cmd --dry-run"],
|
|
45
|
+
notes=["Note 1"],
|
|
46
|
+
)
|
|
47
|
+
transformed = apply_cognitive_load(result)
|
|
48
|
+
# Should have at most input plan steps (2), never more
|
|
49
|
+
assert len(transformed.plan) <= 2
|
|
50
|
+
|
|
51
|
+
def test_no_extra_commands_added(self):
|
|
52
|
+
"""Transformation never adds commands beyond input."""
|
|
53
|
+
result = AssistResult(
|
|
54
|
+
anchored_id="TEST.ID",
|
|
55
|
+
confidence="High",
|
|
56
|
+
safest_next_step="Do something.",
|
|
57
|
+
plan=["Step 1"],
|
|
58
|
+
next_safe_commands=["cmd1 --dry-run", "cmd2 --validate"],
|
|
59
|
+
notes=[],
|
|
60
|
+
)
|
|
61
|
+
transformed = apply_cognitive_load(result)
|
|
62
|
+
# At most 1 command for cognitive-load profile
|
|
63
|
+
assert len(transformed.next_safe_commands) <= 1
|
|
64
|
+
# And it must be from original set
|
|
65
|
+
if transformed.next_safe_commands:
|
|
66
|
+
assert transformed.next_safe_commands[0] in result.next_safe_commands
|
|
67
|
+
|
|
68
|
+
def test_safe_commands_are_verbatim(self):
|
|
69
|
+
"""SAFE commands must be exactly as provided in input."""
|
|
70
|
+
original_cmd = "mytool --dry-run --verbose"
|
|
71
|
+
result = AssistResult(
|
|
72
|
+
anchored_id=None,
|
|
73
|
+
confidence="High",
|
|
74
|
+
safest_next_step="Run the check.",
|
|
75
|
+
plan=["Step 1"],
|
|
76
|
+
next_safe_commands=[original_cmd],
|
|
77
|
+
notes=[],
|
|
78
|
+
)
|
|
79
|
+
transformed = apply_cognitive_load(result)
|
|
80
|
+
# Command must be EXACTLY as provided - no modification
|
|
81
|
+
assert transformed.next_safe_commands[0] == original_cmd
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class TestPlanCappedAtThree:
|
|
85
|
+
"""7.2 Plan capped at 3 steps."""
|
|
86
|
+
|
|
87
|
+
def test_plan_max_three_steps(self):
|
|
88
|
+
"""Plan never exceeds 3 steps."""
|
|
89
|
+
result = AssistResult(
|
|
90
|
+
anchored_id=None,
|
|
91
|
+
confidence="High",
|
|
92
|
+
safest_next_step="Start here.",
|
|
93
|
+
plan=["Step 1", "Step 2", "Step 3", "Step 4", "Step 5"],
|
|
94
|
+
next_safe_commands=[],
|
|
95
|
+
notes=[],
|
|
96
|
+
)
|
|
97
|
+
transformed = apply_cognitive_load(result)
|
|
98
|
+
assert len(transformed.plan) <= 3
|
|
99
|
+
|
|
100
|
+
def test_reduce_plan_preserves_order(self):
|
|
101
|
+
"""First 3 steps are preserved in order."""
|
|
102
|
+
plan = ["A", "B", "C", "D", "E"]
|
|
103
|
+
reduced = reduce_plan(plan)
|
|
104
|
+
assert reduced == ["A.", "B.", "C."] # After normalization adds periods
|
|
105
|
+
|
|
106
|
+
def test_empty_plan_gets_fallback(self):
|
|
107
|
+
"""Empty plan gets a reasonable fallback."""
|
|
108
|
+
reduced = reduce_plan([])
|
|
109
|
+
assert len(reduced) == 1
|
|
110
|
+
assert "Follow" in reduced[0]
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class TestSafeOmittedOnLowConfidence:
|
|
114
|
+
"""7.3 SAFE command omitted on Low confidence."""
|
|
115
|
+
|
|
116
|
+
def test_low_confidence_no_safe_commands(self):
|
|
117
|
+
"""Low confidence results in empty SAFE commands."""
|
|
118
|
+
result = AssistResult(
|
|
119
|
+
anchored_id=None,
|
|
120
|
+
confidence="Low",
|
|
121
|
+
safest_next_step="Check things.",
|
|
122
|
+
plan=["Step 1"],
|
|
123
|
+
next_safe_commands=["cmd --dry-run", "cmd --validate"],
|
|
124
|
+
notes=[],
|
|
125
|
+
)
|
|
126
|
+
transformed = apply_cognitive_load(result)
|
|
127
|
+
assert transformed.next_safe_commands == []
|
|
128
|
+
|
|
129
|
+
def test_medium_confidence_keeps_one_command(self):
|
|
130
|
+
"""Medium confidence keeps one SAFE command."""
|
|
131
|
+
result = AssistResult(
|
|
132
|
+
anchored_id=None,
|
|
133
|
+
confidence="Medium",
|
|
134
|
+
safest_next_step="Check things.",
|
|
135
|
+
plan=["Step 1"],
|
|
136
|
+
next_safe_commands=["cmd --dry-run", "cmd --validate"],
|
|
137
|
+
notes=[],
|
|
138
|
+
)
|
|
139
|
+
transformed = apply_cognitive_load(result)
|
|
140
|
+
assert len(transformed.next_safe_commands) == 1
|
|
141
|
+
|
|
142
|
+
def test_high_confidence_keeps_one_command(self):
|
|
143
|
+
"""High confidence keeps one SAFE command."""
|
|
144
|
+
result = AssistResult(
|
|
145
|
+
anchored_id=None,
|
|
146
|
+
confidence="High",
|
|
147
|
+
safest_next_step="Check things.",
|
|
148
|
+
plan=["Step 1"],
|
|
149
|
+
next_safe_commands=["cmd --dry-run", "cmd --validate"],
|
|
150
|
+
notes=[],
|
|
151
|
+
)
|
|
152
|
+
transformed = apply_cognitive_load(result)
|
|
153
|
+
assert len(transformed.next_safe_commands) == 1
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class TestNoParentheticals:
|
|
157
|
+
"""7.4 Parentheticals are removed."""
|
|
158
|
+
|
|
159
|
+
def test_remove_round_parentheticals(self):
|
|
160
|
+
"""Round parentheses content is removed."""
|
|
161
|
+
assert _remove_parentheticals("Do thing (optional)") == "Do thing"
|
|
162
|
+
|
|
163
|
+
def test_remove_square_brackets(self):
|
|
164
|
+
"""Square bracket content is removed."""
|
|
165
|
+
assert _remove_parentheticals("Run [see docs]") == "Run"
|
|
166
|
+
|
|
167
|
+
def test_multiple_parentheticals_removed(self):
|
|
168
|
+
"""Multiple parentheticals are all removed."""
|
|
169
|
+
result = _remove_parentheticals("A (note) B [info] C")
|
|
170
|
+
assert "note" not in result
|
|
171
|
+
assert "info" not in result
|
|
172
|
+
|
|
173
|
+
def test_step_normalization_removes_parentheticals(self):
|
|
174
|
+
"""Full step normalization removes parentheticals."""
|
|
175
|
+
result = normalize_step("Check the config (usually in /etc)")
|
|
176
|
+
assert "(usually in /etc)" not in result
|
|
177
|
+
|
|
178
|
+
def test_safest_step_removes_parentheticals(self):
|
|
179
|
+
"""Safest step normalization removes parentheticals."""
|
|
180
|
+
result = normalize_safest_step("Start here (see manual for details)")
|
|
181
|
+
assert "(see manual for details)" not in result
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class TestOneSentencePerStep:
|
|
185
|
+
"""7.5 One sentence per step - conjunctions reduced."""
|
|
186
|
+
|
|
187
|
+
def test_and_then_becomes_period_then(self):
|
|
188
|
+
"""'and then' becomes '. Then'."""
|
|
189
|
+
result = _reduce_conjunctions("Do A and then do B")
|
|
190
|
+
# Should keep only first sentence
|
|
191
|
+
assert "Do A." == result or result.startswith("Do A")
|
|
192
|
+
|
|
193
|
+
def test_and_splits_to_first_sentence(self):
|
|
194
|
+
"""'and' in middle splits and keeps first part."""
|
|
195
|
+
result = _reduce_conjunctions("Check logs and restart service")
|
|
196
|
+
assert "Check logs." == result
|
|
197
|
+
|
|
198
|
+
def test_but_splits_to_first_sentence(self):
|
|
199
|
+
"""'but' in middle splits and keeps first part."""
|
|
200
|
+
result = _reduce_conjunctions("Try this but be careful")
|
|
201
|
+
assert "Try this." == result
|
|
202
|
+
|
|
203
|
+
def test_full_step_is_one_sentence(self):
|
|
204
|
+
"""Full normalization results in one sentence."""
|
|
205
|
+
result = normalize_step("First check the logs and then restart the service")
|
|
206
|
+
# Should not contain 'and then'
|
|
207
|
+
assert " and then " not in result
|
|
208
|
+
# Should be one sentence (possibly truncated)
|
|
209
|
+
assert result.count(". ") == 0 or result.endswith(".")
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
class TestLengthLimits:
|
|
213
|
+
"""7.6 Length limits are enforced."""
|
|
214
|
+
|
|
215
|
+
def test_step_capped_at_90_chars(self):
|
|
216
|
+
"""Steps are capped at MAX_STEP_LENGTH (90) chars."""
|
|
217
|
+
long_step = "A" * 150
|
|
218
|
+
result = normalize_step(long_step)
|
|
219
|
+
assert len(result) <= MAX_STEP_LENGTH
|
|
220
|
+
|
|
221
|
+
def test_cap_length_adds_ellipsis(self):
|
|
222
|
+
"""Truncation adds ellipsis."""
|
|
223
|
+
result = _cap_length("A" * 100, 50)
|
|
224
|
+
assert len(result) == 50
|
|
225
|
+
assert result.endswith("…")
|
|
226
|
+
|
|
227
|
+
def test_notes_capped_at_100_chars(self):
|
|
228
|
+
"""Notes are capped at 100 chars."""
|
|
229
|
+
notes = ["A" * 150]
|
|
230
|
+
reduced = reduce_notes(notes)
|
|
231
|
+
assert len(reduced[0]) <= 100
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
class TestNormalizationHelpers:
|
|
235
|
+
"""Tests for individual normalization functions."""
|
|
236
|
+
|
|
237
|
+
def test_strip_boilerplate_run(self):
|
|
238
|
+
"""'Run:' prefix is stripped."""
|
|
239
|
+
assert _strip_boilerplate("Run: mytool") == "mytool"
|
|
240
|
+
|
|
241
|
+
def test_strip_boilerplate_rerun(self):
|
|
242
|
+
"""'Re-run:' prefix is stripped."""
|
|
243
|
+
assert _strip_boilerplate("Re-run: command") == "command"
|
|
244
|
+
|
|
245
|
+
def test_strip_boilerplate_dollar_sign(self):
|
|
246
|
+
"""'$ ' prefix is stripped."""
|
|
247
|
+
assert _strip_boilerplate("$ ls -la") == "ls -la"
|
|
248
|
+
|
|
249
|
+
def test_strip_boilerplate_greater_than(self):
|
|
250
|
+
"""'> ' prefix is stripped."""
|
|
251
|
+
assert _strip_boilerplate("> npm install") == "npm install"
|
|
252
|
+
|
|
253
|
+
def test_to_imperative_you_should(self):
|
|
254
|
+
"""'You should' becomes 'Do '."""
|
|
255
|
+
assert _to_imperative("You should check the logs").startswith("Do ")
|
|
256
|
+
|
|
257
|
+
def test_to_imperative_please(self):
|
|
258
|
+
"""'Please' becomes 'Do '."""
|
|
259
|
+
assert _to_imperative("Please restart").startswith("Do ")
|
|
260
|
+
|
|
261
|
+
def test_to_imperative_consider(self):
|
|
262
|
+
"""'Consider' becomes 'Try '."""
|
|
263
|
+
assert _to_imperative("Consider updating").startswith("Try ")
|
|
264
|
+
|
|
265
|
+
def test_to_imperative_it_may_help(self):
|
|
266
|
+
"""'It may help to' becomes 'Try '."""
|
|
267
|
+
assert _to_imperative("It may help to restart").startswith("Try ")
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
class TestRenderCognitiveLoad:
|
|
271
|
+
"""Tests for cognitive-load renderer."""
|
|
272
|
+
|
|
273
|
+
def test_render_includes_cognitive_load_header(self):
|
|
274
|
+
"""Output includes ASSIST (Cognitive Load) header."""
|
|
275
|
+
result = AssistResult(
|
|
276
|
+
anchored_id="TEST.ID",
|
|
277
|
+
confidence="High",
|
|
278
|
+
safest_next_step="Do the thing.",
|
|
279
|
+
plan=["Step 1", "Step 2"],
|
|
280
|
+
next_safe_commands=[],
|
|
281
|
+
notes=[],
|
|
282
|
+
)
|
|
283
|
+
output = render_cognitive_load(result)
|
|
284
|
+
assert "ASSIST (Cognitive Load):" in output
|
|
285
|
+
|
|
286
|
+
def test_render_includes_goal_line(self):
|
|
287
|
+
"""Output includes fixed Goal line."""
|
|
288
|
+
result = AssistResult(
|
|
289
|
+
anchored_id=None,
|
|
290
|
+
confidence="High",
|
|
291
|
+
safest_next_step="Start here.",
|
|
292
|
+
plan=["Step 1"],
|
|
293
|
+
next_safe_commands=[],
|
|
294
|
+
notes=[],
|
|
295
|
+
)
|
|
296
|
+
output = render_cognitive_load(result)
|
|
297
|
+
assert "Goal: Get back to a known-good state." in output
|
|
298
|
+
|
|
299
|
+
def test_render_uses_first_next_last_labels(self):
|
|
300
|
+
"""Plan uses First/Next/Last labels instead of numbers."""
|
|
301
|
+
result = AssistResult(
|
|
302
|
+
anchored_id=None,
|
|
303
|
+
confidence="High",
|
|
304
|
+
safest_next_step="Start here.",
|
|
305
|
+
plan=["Step A", "Step B", "Step C"],
|
|
306
|
+
next_safe_commands=[],
|
|
307
|
+
notes=[],
|
|
308
|
+
)
|
|
309
|
+
output = render_cognitive_load(result)
|
|
310
|
+
assert "First:" in output
|
|
311
|
+
assert "Next:" in output
|
|
312
|
+
assert "Last:" in output
|
|
313
|
+
# Should NOT have numbered steps
|
|
314
|
+
assert "1)" not in output
|
|
315
|
+
|
|
316
|
+
def test_render_single_step_uses_first(self):
|
|
317
|
+
"""Single step plan uses First label."""
|
|
318
|
+
result = AssistResult(
|
|
319
|
+
anchored_id=None,
|
|
320
|
+
confidence="High",
|
|
321
|
+
safest_next_step="Start here.",
|
|
322
|
+
plan=["Only step"],
|
|
323
|
+
next_safe_commands=[],
|
|
324
|
+
notes=[],
|
|
325
|
+
)
|
|
326
|
+
output = render_cognitive_load(result)
|
|
327
|
+
assert "First:" in output
|
|
328
|
+
|
|
329
|
+
def test_render_two_steps_uses_first_next(self):
|
|
330
|
+
"""Two step plan uses First and Next labels."""
|
|
331
|
+
result = AssistResult(
|
|
332
|
+
anchored_id=None,
|
|
333
|
+
confidence="High",
|
|
334
|
+
safest_next_step="Start here.",
|
|
335
|
+
plan=["Step A", "Step B"],
|
|
336
|
+
next_safe_commands=[],
|
|
337
|
+
notes=[],
|
|
338
|
+
)
|
|
339
|
+
output = render_cognitive_load(result)
|
|
340
|
+
assert "First:" in output
|
|
341
|
+
assert "Next:" in output
|
|
342
|
+
# Last only appears for 3 steps
|
|
343
|
+
assert output.count("Last:") == 0
|
|
344
|
+
|
|
345
|
+
def test_render_includes_safest_next_step(self):
|
|
346
|
+
"""Output includes safest next step."""
|
|
347
|
+
result = AssistResult(
|
|
348
|
+
anchored_id=None,
|
|
349
|
+
confidence="High",
|
|
350
|
+
safest_next_step="Check the config file first.",
|
|
351
|
+
plan=["Step 1"],
|
|
352
|
+
next_safe_commands=[],
|
|
353
|
+
notes=[],
|
|
354
|
+
)
|
|
355
|
+
output = render_cognitive_load(result)
|
|
356
|
+
assert "Safest next step:" in output
|
|
357
|
+
assert "Check the config file first" in output
|
|
358
|
+
|
|
359
|
+
def test_render_includes_safe_command_when_high_confidence(self):
|
|
360
|
+
"""SAFE command shown for High confidence."""
|
|
361
|
+
result = AssistResult(
|
|
362
|
+
anchored_id=None,
|
|
363
|
+
confidence="High",
|
|
364
|
+
safest_next_step="Run the check.",
|
|
365
|
+
plan=["Step 1"],
|
|
366
|
+
next_safe_commands=["tool --dry-run"],
|
|
367
|
+
notes=[],
|
|
368
|
+
)
|
|
369
|
+
output = render_cognitive_load(result)
|
|
370
|
+
assert "Next (SAFE):" in output
|
|
371
|
+
assert "tool --dry-run" in output
|
|
372
|
+
|
|
373
|
+
def test_render_no_safe_section_when_empty(self):
|
|
374
|
+
"""No SAFE section when no commands."""
|
|
375
|
+
result = AssistResult(
|
|
376
|
+
anchored_id=None,
|
|
377
|
+
confidence="High",
|
|
378
|
+
safest_next_step="Run the check.",
|
|
379
|
+
plan=["Step 1"],
|
|
380
|
+
next_safe_commands=[],
|
|
381
|
+
notes=[],
|
|
382
|
+
)
|
|
383
|
+
output = render_cognitive_load(result)
|
|
384
|
+
assert "Next (SAFE):" not in output
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
class TestDeterminism:
|
|
388
|
+
"""Tests to ensure deterministic behavior."""
|
|
389
|
+
|
|
390
|
+
def test_same_input_same_output(self):
|
|
391
|
+
"""Same input always produces same output."""
|
|
392
|
+
result = AssistResult(
|
|
393
|
+
anchored_id="DET.TEST",
|
|
394
|
+
confidence="Medium",
|
|
395
|
+
safest_next_step="Check the logs (verbose mode).",
|
|
396
|
+
plan=[
|
|
397
|
+
"First check the config and restart",
|
|
398
|
+
"Then verify the connection",
|
|
399
|
+
"Finally update the cache",
|
|
400
|
+
"Extra step that gets dropped",
|
|
401
|
+
],
|
|
402
|
+
next_safe_commands=["tool --dry-run", "tool --validate"],
|
|
403
|
+
notes=["Note with (parenthetical)", "Another note"],
|
|
404
|
+
)
|
|
405
|
+
# Run transformation multiple times
|
|
406
|
+
outputs = [apply_cognitive_load(result) for _ in range(10)]
|
|
407
|
+
# All outputs should be identical
|
|
408
|
+
first = outputs[0]
|
|
409
|
+
for output in outputs[1:]:
|
|
410
|
+
assert output == first
|
|
411
|
+
|
|
412
|
+
def test_render_deterministic(self):
|
|
413
|
+
"""Rendering is deterministic."""
|
|
414
|
+
result = AssistResult(
|
|
415
|
+
anchored_id="DET.RENDER",
|
|
416
|
+
confidence="High",
|
|
417
|
+
safest_next_step="Do something.",
|
|
418
|
+
plan=["Step 1", "Step 2"],
|
|
419
|
+
next_safe_commands=["cmd --dry-run"],
|
|
420
|
+
notes=["A note"],
|
|
421
|
+
)
|
|
422
|
+
outputs = [render_cognitive_load(result) for _ in range(10)]
|
|
423
|
+
first = outputs[0]
|
|
424
|
+
for output in outputs[1:]:
|
|
425
|
+
assert output == first
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
class TestSelectSafeCommand:
|
|
429
|
+
"""Tests for select_safe_command function."""
|
|
430
|
+
|
|
431
|
+
def test_returns_none_for_low(self):
|
|
432
|
+
"""Returns None for Low confidence."""
|
|
433
|
+
assert select_safe_command(["cmd"], "Low") is None
|
|
434
|
+
|
|
435
|
+
def test_returns_none_for_empty_list(self):
|
|
436
|
+
"""Returns None for empty command list."""
|
|
437
|
+
assert select_safe_command([], "High") is None
|
|
438
|
+
|
|
439
|
+
def test_returns_first_command_for_medium(self):
|
|
440
|
+
"""Returns first command for Medium confidence."""
|
|
441
|
+
result = select_safe_command(["cmd1", "cmd2"], "Medium")
|
|
442
|
+
assert result == "cmd1"
|
|
443
|
+
|
|
444
|
+
def test_returns_first_command_for_high(self):
|
|
445
|
+
"""Returns first command for High confidence."""
|
|
446
|
+
result = select_safe_command(["cmd1", "cmd2"], "High")
|
|
447
|
+
assert result == "cmd1"
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
class TestNotesReduction:
|
|
451
|
+
"""Tests for notes reduction."""
|
|
452
|
+
|
|
453
|
+
def test_notes_limited_to_two(self):
|
|
454
|
+
"""Notes are limited to 2 max."""
|
|
455
|
+
notes = ["Note 1", "Note 2", "Note 3", "Note 4"]
|
|
456
|
+
reduced = reduce_notes(notes)
|
|
457
|
+
assert len(reduced) == 2
|
|
458
|
+
|
|
459
|
+
def test_empty_notes_stays_empty(self):
|
|
460
|
+
"""Empty notes list stays empty."""
|
|
461
|
+
assert reduce_notes([]) == []
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
class TestStepLabels:
|
|
465
|
+
"""Tests for STEP_LABELS constant."""
|
|
466
|
+
|
|
467
|
+
def test_labels_are_first_next_last(self):
|
|
468
|
+
"""Labels are First, Next, Last."""
|
|
469
|
+
assert STEP_LABELS == ["First", "Next", "Last"]
|