File size: 6,772 Bytes
daa2fba
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
from __future__ import annotations

import ast
from pathlib import Path


REPO_ROOT = Path(__file__).resolve().parents[1]
CONTRACT = (REPO_ROOT / "BASIC_MODE_DECLUTTER_CONTRACTS.md").read_text(encoding="utf-8")
PLAN = (REPO_ROOT / "BASIC_EXPERT_REVIEW_MODE_PLAN.md").read_text(encoding="utf-8")
APP = (REPO_ROOT / "presidio_streamlit.py").read_text(encoding="utf-8")
SIDE_BY_SIDE = (REPO_ROOT / "side_by_side_review_panel_ui.py").read_text(encoding="utf-8")
SERIAL = (REPO_ROOT / "serial_review_panel_ui.py").read_text(encoding="utf-8")
DOCX_HYGIENE = (REPO_ROOT / "docx_hygiene_audit_panel_ui.py").read_text(encoding="utf-8")
THIS_TEST = (REPO_ROOT / "tests" / "test_basic_mode_declutter_contracts.py").read_text(encoding="utf-8")


def norm(text: str) -> str:
    return " ".join(text.lower().split())


def contains(text: str, term: str) -> None:
    assert norm(term) in norm(text), f"Expected to find {term!r}"


def test_declutter_contract_document_exists_and_names_next_package() -> None:
    assert "# SCRUB-WP_BASIC_MODE_DECLUTTER_CONTRACTS" in CONTRACT
    contains(CONTRACT, "SCRUB-WP_BASIC_MODE_DECLUTTER_IMPLEMENTATION")
    contains(CONTRACT, "Only start implementation after these contract tests are accepted and merged")


def test_basiscontrole_default_and_mode_state_are_protected() -> None:
    combined = CONTRACT + "\n" + PLAN + "\n" + SIDE_BY_SIDE
    for term in [
        "Basiscontrole must remain the default selected review mode",
        "Basiscontrole should become the default user-facing MVP flow",
        "Default: `Basiscontrole`",
        "solidprivacy_review_mode",
    ]:
        contains(combined, term)


def test_basiscontrole_target_surface_is_defined() -> None:
    for term in [
        "1. Voeg document of tekst toe",
        "2. Controleer resultaat",
        "3. Exporteer resultaat / Download veilig",
        "Controleweergave",
        "side-by-side review",
        "Markeringen tonen",
        "replacement count",
        "Gemiste waarde toevoegen",
        "Details aanpassen",
        "primary document downloads",
    ]:
        contains(CONTRACT, term)


def test_basiscontrole_should_not_show_full_expert_stack_as_primary() -> None:
    contains(CONTRACT, "Basiscontrole should avoid showing the full expert expander stack")
    for term in [
        "Waarom controleren?",
        "Extra controlehulpen",
        "Mogelijk extra te controleren waarden",
        "Geavanceerde details bij de vervangtabel",
        "Stap voor stap controleren",
        "Herbruikbare vervangingen",
        "Technische informatie",
        "Geavanceerde herkenningsdetails",
    ]:
        assert term in CONTRACT


def test_expertcontrole_full_controls_remain_available() -> None:
    contains(CONTRACT, "Expertcontrole preservation contract")
    for term in [
        "Waarom controleren?",
        "Gemiste waarde toevoegen",
        "Extra controlehulpen",
        "Mogelijk extra te controleren waarden",
        "Vervangtabel controleren",
        "Geavanceerde details bij de vervangtabel",
        "Stap voor stap controleren",
        "Herbruikbare vervangingen",
        "Scrub Key downloaden",
        "Audit en technische bestanden",
        "DOCX hygiene audit",
        "Technische informatie",
        "Geavanceerde herkenningsdetails",
    ]:
        contains(CONTRACT, term)


def test_mode_switch_must_not_reset_active_document_state() -> None:
    for term in [
        "Switching modes must not reset",
        "uploaded text",
        "uploaded file",
        "recognized replacements",
        "manual additions",
        "replacement decisions",
        "download outputs",
        "session state needed for the active document",
    ]:
        contains(CONTRACT, term)


def test_future_implementation_route_is_defined() -> None:
    for term in [
        "side_by_side_review_state = render_side_by_side_review_panel",
        "review_mode = side_by_side_review_state.get",
        "is_expert_review = review_mode == \"Expertcontrole\"",
        "visibility and grouping only",
    ]:
        contains(CONTRACT, term)


def test_current_source_paths_needed_for_implementation_exist() -> None:
    for term in [
        "render_side_by_side_review_panel",
        "2. Controleer resultaat",
        "Gemiste waarde toevoegen",
        "Vervangtabel controleren",
        "Document downloaden",
        "Scrub Key downloaden",
        "Audit en technische bestanden",
        "render_docx_hygiene_audit_panel",
    ]:
        contains(APP, term)
    for term in ["Basiscontrole", "Expertcontrole", "solidprivacy_review_mode", "Markeringen tonen"]:
        contains(SIDE_BY_SIDE, term)
    contains(SERIAL, "Stap voor stap controleren")
    contains(DOCX_HYGIENE, "DOCX hygiene audit")


def test_nested_expander_pattern_is_not_approved() -> None:
    for term in [
        "Do not introduce nested expanders",
        "must not put existing `st.expander(...)` blocks inside a parent `st.expander(...)`",
        "Do not introduce a parent `Meer controleopties` expander containing other expanders",
    ]:
        contains(CONTRACT, term)


def test_semantic_boundaries_are_locked() -> None:
    for term in [
        "replacement logic",
        "review table data semantics",
        "include/remember/find/replace_with meaning",
        "export content",
        "download filenames",
        "download MIME types",
        "Scrub Key JSON semantics",
        "Scrub Key warning meaning",
        "reinsert behavior",
        "recognizer logic",
        "benchmark logic",
        "DOCX/PDF parsing behavior",
        "runtime/startup behavior",
        "dependencies",
    ]:
        contains(CONTRACT, term)


def test_app_verification_checklist_is_defined_for_later_ui_change() -> None:
    for term in [
        "Basiscontrole is selected by default",
        "Basiscontrole is visibly cleaner than before",
        "Side-by-side review remains visible",
        "Gemiste waarde toevoegen",
        "Expertcontrole exposes the full detailed controls",
        "Primary document downloads remain visible",
        "DOCX hygiene audit remains available when relevant",
        "No visible export, Scrub Key or reinsert regression appears",
    ]:
        contains(CONTRACT, term)


def test_contract_tests_do_not_import_streamlit_or_runtime_app() -> None:
    tree = ast.parse(THIS_TEST)
    imported_roots: set[str] = set()
    for node in ast.walk(tree):
        if isinstance(node, ast.Import):
            imported_roots.update(alias.name.split(".")[0] for alias in node.names)
        elif isinstance(node, ast.ImportFrom) and node.module:
            imported_roots.add(node.module.split(".")[0])
    assert "streamlit" not in imported_roots
    assert "presidio_streamlit" not in imported_roots