File size: 13,791 Bytes
827e786
0cd97fa
 
827e786
 
 
 
 
 
 
 
 
 
 
0cd97fa
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
205fd04
0cd97fa
 
 
 
 
 
 
 
205fd04
0cd97fa
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
827e786
 
 
 
 
 
 
 
 
 
 
dfedb9d
 
827e786
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
173ca53
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
827e786
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0cd97fa
 
 
827e786
 
 
 
173ca53
827e786
 
 
 
 
 
 
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
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
import sys
import re
import json
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
import app


def _assert(cond: bool, msg: str) -> None:
    if not cond:
        raise AssertionError(msg)


def _css_block(selector: str) -> str:
    pat = re.compile(rf"{re.escape(selector)}\s*\{{([^}}]+)\}}", re.DOTALL)
    m = pat.search(app.css)
    _assert(m is not None, f"missing CSS selector block: {selector}")
    return m.group(1)


def _css_prop(block: str, prop: str) -> str:
    pat = re.compile(rf"{re.escape(prop)}\s*:\s*([^;]+);", re.IGNORECASE)
    m = pat.search(block)
    _assert(m is not None, f"missing CSS property '{prop}' in block: {block}")
    return m.group(1).strip().lower()


def test_toggle_color_contract_matches_legend() -> None:
    legend_bg_by_origin = {
        "rewrite": _css_prop(_css_block(".source-legend .chip.rewrite"), "background"),
        "selection": _css_prop(_css_block(".source-legend .chip.selection"), "background"),
        "structural": _css_prop(_css_block(".source-legend .chip.structural"), "background"),
        "classifier": _css_prop(_css_block(".source-legend .chip.classifier"), "background"),
        "implied": _css_prop(_css_block(".source-legend .chip.implied"), "background"),
        "user": _css_prop(_css_block(".source-legend .chip.user"), "background"),
    }

    on_bg2_by_origin = {
        "rewrite": _css_prop(_css_block('.lego-tags label[data-psq-preselected="1"][data-psq-origin="rewrite"] span'), "--on-bg2"),
        "selection": _css_prop(_css_block('.lego-tags label[data-psq-preselected="1"][data-psq-origin="selection"] span'), "--on-bg2"),
        "structural": _css_prop(_css_block('.lego-tags label[data-psq-preselected="1"][data-psq-origin="structural"] span'), "--on-bg2"),
        "classifier": _css_prop(_css_block('.lego-tags label[data-psq-preselected="1"][data-psq-origin="classifier"] span'), "--on-bg2"),
        "implied": _css_prop(_css_block('.lego-tags label[data-psq-preselected="1"][data-psq-origin="implied"] span'), "--on-bg2"),
        "user": _css_prop(_css_block('.lego-tags label[data-psq-preselected="0"] span'), "--on-bg2"),
    }

    for origin, legend_bg in legend_bg_by_origin.items():
        _assert(
            on_bg2_by_origin[origin] == legend_bg,
            f"legend/toggle color mismatch for '{origin}': toggle={on_bg2_by_origin[origin]} legend={legend_bg}",
        )


def test_client_js_stamps_source_color_attributes() -> None:
    js = app.client_js
    has_origin_attr = ("data-psq-origin" in js) or ("dataset.psqOrigin" in js)
    has_preselected_attr = ("data-psq-preselected" in js) or ("dataset.psqPreselected" in js)
    _assert(has_origin_attr, "client_js does not stamp per-tag origin attributes on checkbox labels")
    _assert(has_preselected_attr, "client_js does not stamp per-tag preselected attributes on checkbox labels")


def test_tooltip_payload_includes_row_meta_for_color_mapping() -> None:
    row_defs = [
        {
            "name": "r1",
            "label": "R1",
            "tags": ["solo", "female"],
            "tag_meta": {
                "solo": {"origin": "rewrite", "preselected": True},
                "female": {"origin": "selection", "preselected": False},
            },
        }
    ]
    raw = app._build_tooltip_payload(row_defs, max_rows=app.display_max_rows_default)
    payload = json.loads(raw)
    _assert("meta_rows" in payload, "tooltip payload missing meta_rows")
    _assert(isinstance(payload["meta_rows"], list), "tooltip payload meta_rows must be a list")
    _assert(len(payload["meta_rows"]) == 1, "meta_rows should align with visible row count")
    _assert(isinstance(payload["meta_rows"][0], list), "meta_rows row must be a list")
    _assert(len(payload["meta_rows"][0]) == 2, "meta_rows tag metadata count must align with tags")

    first = payload["meta_rows"][0][0]
    second = payload["meta_rows"][0][1]
    _assert(first.get("origin") == "rewrite", "first tag origin metadata mismatch")
    _assert(first.get("preselected") is True, "first tag preselected metadata mismatch")
    _assert(second.get("origin") == "selection", "second tag origin metadata mismatch")
    _assert(second.get("preselected") is False, "second tag preselected metadata mismatch")


def test_prompt_uses_visible_rows_only() -> None:
    # If selected state contains stale hidden tags, prompt should still reflect visible-row selections only.
    row_defs = [
        {"name": "r1", "label": "R1", "tags": ["solo", "female"], "tag_meta": {}},
        {"name": "r2", "label": "R2", "tags": ["cub"], "tag_meta": {}},
    ]
    payload = app._build_ui_payload(
        console_text="x",
        row_defs=row_defs,
        selected_tags=["solo", "rosalina_(mario)"],
    )
    prompt_text = payload[4]["value"]
    selected_state = payload[5]
    _assert("rosalina \\(mario\\)" not in prompt_text, "stale hidden tag leaked into prompt")
    _assert("solo" in prompt_text, "visible selected tag missing from prompt")
    _assert("rosalina_(mario)" not in selected_state, "stale hidden tag leaked into selected state")


def test_row_deduping() -> None:
    row_defs = [
        {
            "name": "other_retrieved",
            "label": "Other (Retrieved)",
            "tags": ["cub", "expressions", "invalid_tag", "cub", "expressions"],
            "tag_meta": {},
        }
    ]
    prompt_text, row_values_state, _, checkbox_updates = app._build_row_component_updates(
        row_defs=row_defs,
        selected_tags=["cub", "expressions"],
        max_rows=app.display_max_rows_default,
    )
    _assert(prompt_text == "cub, expressions", "prompt should be deduped and ordered from row")
    _assert(row_values_state[0] == ["cub", "expressions"], "row selected values should be deduped")
    first_choices = checkbox_updates[0]["choices"]
    first_values = [v for _, v in first_choices]
    _assert(first_values == ["cub", "expressions", "invalid_tag"], "row choices should be deduped")


def test_rebuild_ignores_stale_selected_state() -> None:
    row_defs = [
        {"name": "selected_other", "label": "Selected (Other)", "tags": ["solo", "female", "anthro"], "tag_meta": {}},
        {"name": "other_retrieved", "label": "Other (Retrieved)", "tags": ["cub", "expressions"], "tag_meta": {}},
    ]
    # Simulate UI state where user has deselected anthro, but stale selected state still contains it.
    selected_state = ["solo", "female", "anthro", "cub"]
    row_values_state = [["solo", "female"], ["cub"]]
    out = app._rebuild_rows_from_selected(
        selected_state,
        row_defs,
        row_values_state,
        app.display_top_groups_default,
        app.display_top_tags_per_group_default,
        app.display_rank_top_k_default,
    )
    prompt = out[1]
    selected_after = out[2]
    _assert("anthro" not in selected_after, "rebuild should not resurrect stale deselected tags")
    _assert("anthro" not in prompt, "prompt should not include stale deselected tags")
    _assert("solo" in prompt and "female" in prompt and "cub" in prompt, "rebuild should retain current row selections")


def test_toggle_then_rebuild_does_not_resurrect_removed_tag() -> None:
    row_defs = [
        {"name": "selected_other", "label": "Selected (Other)", "tags": ["solo", "anthro", "female"], "tag_meta": {}},
        {"name": "other_retrieved", "label": "Other (Retrieved)", "tags": ["cub", "expressions"], "tag_meta": {}},
    ]
    selected_state = ["solo", "anthro", "female", "cub"]
    row_values_state = [["solo", "anthro", "female"], ["cub"]]

    # User unchecks anthro in row 0.
    toggle_out = app._on_toggle_row(
        0,
        ["solo", "female"],
        selected_state,
        False,
        row_defs,
        row_values_state,
        app.display_max_rows_default,
    )
    selected_after_toggle = toggle_out[0]
    row_values_after_toggle = toggle_out[3]
    _assert("anthro" not in selected_after_toggle, "toggle should remove anthro from selected state")

    # Rebuild from current row values must preserve the user-toggle result.
    rebuild_out = app._rebuild_rows_from_selected(
        selected_after_toggle,
        row_defs,
        row_values_after_toggle,
        app.display_top_groups_default,
        app.display_top_tags_per_group_default,
        app.display_rank_top_k_default,
    )
    prompt_after_rebuild = rebuild_out[1]
    selected_after_rebuild = rebuild_out[2]
    _assert("anthro" not in selected_after_rebuild, "rebuild should not resurrect deselected anthro")
    _assert("anthro" not in prompt_after_rebuild, "prompt should not contain deselected anthro after rebuild")
    _assert("solo" in prompt_after_rebuild and "female" in prompt_after_rebuild, "kept selections should remain")
    _assert("cub" in prompt_after_rebuild, "other retrieved selection should remain")


def test_rebuild_preserves_user_toggled_provenance() -> None:
    row_defs = [
        {
            "name": "demo_row",
            "label": "Demo Row",
            "tags": ["base_tag", "manual_tag"],
            "tag_meta": {
                "base_tag": {"origin": "selection", "preselected": True},
                "manual_tag": {"origin": "selection", "preselected": False},
            },
        }
    ]
    selected_state = ["base_tag", "manual_tag"]
    row_values_state = [["base_tag", "manual_tag"]]
    out = app._rebuild_rows_from_selected(
        selected_state,
        row_defs,
        row_values_state,
        app.display_top_groups_default,
        app.display_top_tags_per_group_default,
        app.display_rank_top_k_default,
    )
    rebuilt_rows = out[6]
    manual_meta = None
    for row in rebuilt_rows:
        tags = row.get("tags", []) if isinstance(row, dict) else []
        if "manual_tag" in tags:
            row_meta = row.get("tag_meta", {}) if isinstance(row.get("tag_meta", {}), dict) else {}
            manual_meta = row_meta.get("manual_tag")
            if manual_meta:
                break
    _assert(manual_meta is not None, "manual_tag metadata missing after rebuild")
    _assert(manual_meta.get("origin") == "user", "rebuild should preserve manual_tag origin as user")
    _assert(manual_meta.get("preselected") is False, "rebuild should preserve manual_tag preselected=False")


def test_toggle_does_not_cross_activate_unrelated_row_tag() -> None:
    row_defs = [
        {"name": "organization", "label": "Organization", "tags": ["pinup", "close-up"], "tag_meta": {}},
        {"name": "color_markings", "label": "Color Markings", "tags": ["shoulder_markings", "black_markings"], "tag_meta": {}},
    ]
    selected_state = []
    row_values_state = [[], []]

    # User enables close-up in organization row.
    out = app._on_toggle_row(
        0,
        ["close-up"],
        selected_state,
        False,
        row_defs,
        row_values_state,
        app.display_max_rows_default,
    )
    selected_after = out[0]
    row_values_after = out[3]
    _assert("close-up" in selected_after, "close-up should be selected")
    _assert("shoulder_markings" not in selected_after, "unrelated row tag should not be auto-selected")
    _assert(row_values_after[0] == ["close-up"], "organization row values should include close-up only")
    _assert(row_values_after[1] == [], "color markings row should remain unselected")


def test_shared_tag_mirrors_without_unrelated_cross_toggle() -> None:
    row_defs = [
        {"name": "objects_props", "label": "Objects Props", "tags": ["holding_face", "holding_clothing"], "tag_meta": {}},
        {"name": "expression_detail", "label": "Expression Detail", "tags": ["open_mouth", "closed_smile"], "tag_meta": {}},
        {"name": "pose_action_detail", "label": "Pose Action Detail", "tags": ["holding_face", "walking"], "tag_meta": {}},
    ]
    selected_state = []
    row_values_state = [[], [], []]

    # Enable open_mouth; should not affect holding_face rows.
    out1 = app._on_toggle_row(
        1,
        ["open_mouth"],
        selected_state,
        False,
        row_defs,
        row_values_state,
        app.display_max_rows_default,
    )
    sel1 = out1[0]
    vals1 = out1[3]
    _assert("open_mouth" in sel1, "open_mouth should be selected")
    _assert("holding_face" not in sel1, "holding_face must remain unselected")
    _assert(vals1[0] == [], "objects props row should remain unselected")
    _assert(vals1[1] == ["open_mouth"], "expression row should select open_mouth")
    _assert(vals1[2] == [], "pose row should remain unselected")

    # Enable holding_face in objects row; should mirror only to pose row, not expression row.
    out2 = app._on_toggle_row(
        0,
        ["holding_face"],
        sel1,
        True,
        row_defs,
        vals1,
        app.display_max_rows_default,
    )
    sel2 = out2[0]
    vals2 = out2[3]
    _assert("holding_face" in sel2 and "open_mouth" in sel2, "both explicitly selected tags should be present")
    _assert(vals2[0] == ["holding_face"], "objects row should select holding_face")
    _assert(vals2[1] == ["open_mouth"], "expression row should keep open_mouth only")
    _assert(vals2[2] == ["holding_face"], "pose row should mirror holding_face")


def main() -> None:
    test_toggle_color_contract_matches_legend()
    test_client_js_stamps_source_color_attributes()
    test_tooltip_payload_includes_row_meta_for_color_mapping()
    test_prompt_uses_visible_rows_only()
    test_row_deduping()
    test_rebuild_ignores_stale_selected_state()
    test_toggle_then_rebuild_does_not_resurrect_removed_tag()
    test_rebuild_preserves_user_toggled_provenance()
    test_toggle_does_not_cross_activate_unrelated_row_tag()
    test_shared_tag_mirrors_without_unrelated_cross_toggle()
    print("ui state smoke: ok")


if __name__ == "__main__":
    main()