File size: 9,435 Bytes
1a12965
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""Tests for the case_type-driven verification test-case generation feature.

Covers: registry determinism, correct rendering/ground-truth for every one
of the 12 case types, companion-box behavior, and backward compatibility
with the existing (non-case-type) generation path.
"""

from __future__ import annotations

from src.config.loader import load_config
from src.generator import GeneratorContext
from src.verification.case_types import CASE_TABLE
from src.verification.registry import SignerIdentityRegistry


def _make_context(tmp_path, case_type: "str | None" = None, seed: int = 4242):
    overrides = [f"seed={seed}", f"paths.output_dir={tmp_path.as_posix()}", "generation.count=1"]
    if case_type is not None:
        overrides.append(f"quality.case_type={case_type}")
    config = load_config(overrides=overrides)
    return GeneratorContext(config)


# --------------------------------------------------------------------- #
# Registry
# --------------------------------------------------------------------- #

def test_registry_authorized_signer_is_deterministic(tmp_path):
    ctx1 = _make_context(tmp_path)
    ctx2 = _make_context(tmp_path)

    for template_id, template in ctx1.template_loader.discover().items():
        for box in template.annotation.pages[0].signature_boxes:
            a = ctx1.identity_registry.authorized_signer_group(template_id, box.id)
            b = ctx2.identity_registry.authorized_signer_group(template_id, box.id)
            assert a == b


def test_registry_display_name_is_stable_across_instances(tmp_path):
    ctx1 = _make_context(tmp_path)
    ctx2 = _make_context(tmp_path)
    group = ctx1.identity_registry._signer_groups[0]
    assert ctx1.identity_registry.display_name(group) == ctx2.identity_registry.display_name(group)


def test_registry_foreign_authorized_picks_a_different_template(tmp_path):
    ctx = _make_context(tmp_path)
    group, foreign_template_id, foreign_box_id = ctx.identity_registry.foreign_authorized("bank_doc_01")
    assert foreign_template_id != "bank_doc_01"
    assert group == ctx.identity_registry.authorized_signer_group(foreign_template_id, foreign_box_id)


# --------------------------------------------------------------------- #
# Per-case-type ground truth
# --------------------------------------------------------------------- #

def test_all_case_types_produce_matching_verdict_and_reject_code(tmp_path):
    for code, spec in CASE_TABLE.items():
        ctx = _make_context(tmp_path, case_type=code, seed=99)
        metadata, _, report = ctx.run_sample(1)
        v = metadata["verification"]

        assert v["case_type"] == code
        assert v["verdict"] == spec.verdict, f"{code}: verdict mismatch"
        assert v["reject_code"] == spec.reject_code, f"{code}: reject_code mismatch"
        assert report is not None and report.passed, (
            f"{code}: validation failed: {[i.message for i in report.errors]}"
        )


def test_pass_cases_use_the_authorized_signer(tmp_path):
    for code in ("1a", "1b", "1d"):
        ctx = _make_context(tmp_path, case_type=code, seed=17)
        metadata, _, _ = ctx.run_sample(1)
        v = metadata["verification"]
        assert v["actual_signer_group"] == v["expected_signer_group"]
        assert v["actual_source_type"] == "genuine"
        assert v["companion"] is None


def test_multi_signer_pass_case_fills_companion_correctly(tmp_path):
    ctx = _make_context(tmp_path, case_type="1c", seed=17)
    metadata, _, _ = ctx.run_sample(1)
    v = metadata["verification"]
    assert v["actual_signer_group"] == v["expected_signer_group"]
    assert v["companion"] is not None
    assert v["companion"]["filled"] is True
    assert v["companion"]["matches_expected"] is True


def test_blank_cases_have_no_actual_signer(tmp_path):
    for code in ("2a", "5a"):
        ctx = _make_context(tmp_path, case_type=code, seed=5)
        metadata, _, _ = ctx.run_sample(1)
        v = metadata["verification"]
        assert v["actual_signer_group"] is None
        assert v["actual_source_type"] == "none"
        assert metadata["signature_source"]["signature_id"] is None

    ctx_5a = _make_context(tmp_path, case_type="5a", seed=5)
    metadata_5a, _, _ = ctx_5a.run_sample(1)
    assert metadata_5a["verification"]["companion"]["filled"] is True

    ctx_2a = _make_context(tmp_path, case_type="2a", seed=5)
    metadata_2a, _, _ = ctx_2a.run_sample(1)
    assert metadata_2a["verification"]["companion"] is None


def test_identity_mismatch_cases_use_a_different_signer(tmp_path):
    for code in ("3a", "5b"):
        ctx = _make_context(tmp_path, case_type=code, seed=23)
        metadata, _, _ = ctx.run_sample(1)
        v = metadata["verification"]
        assert v["actual_signer_group"] != v["expected_signer_group"]
        assert v["actual_source_type"] == "genuine"


def test_skilled_forgery_case_uses_forged_sample_of_the_authorized_signer(tmp_path):
    ctx = _make_context(tmp_path, case_type="3b", seed=23)
    metadata, _, _ = ctx.run_sample(1)
    v = metadata["verification"]
    assert v["actual_signer_group"] == v["expected_signer_group"]
    assert v["actual_source_type"] == "forged"


def test_role_mismatch_cases_use_a_legitimately_registered_signer(tmp_path):
    ctx = _make_context(tmp_path, case_type="4a", seed=31)
    metadata, _, _ = ctx.run_sample(1)
    v = metadata["verification"]
    # The target box got the *companion* box's own authorized signer.
    assert v["actual_signer_group"] == v["companion"]["signer_group"]
    assert v["actual_signer_group"] != v["expected_signer_group"]

    ctx_4b = _make_context(tmp_path, case_type="4b", seed=31)
    metadata_4b, _, _ = ctx_4b.run_sample(1)
    v4b = metadata_4b["verification"]
    assert v4b["actual_signer_group"] != v4b["expected_signer_group"]


def test_printed_text_case_has_no_biometric_identity(tmp_path):
    ctx = _make_context(tmp_path, case_type="2b", seed=11)
    metadata, _, _ = ctx.run_sample(1)
    v = metadata["verification"]
    assert v["actual_signer_group"] is None
    assert v["actual_source_type"] == "printed"


# --------------------------------------------------------------------- #
# Backward compatibility
# --------------------------------------------------------------------- #

def test_unset_case_type_is_unaffected(tmp_path):
    ctx = _make_context(tmp_path, case_type=None, seed=1)
    metadata, _, report = ctx.run_sample(1)
    v = metadata["verification"]
    assert v == {
        "case_type": None, "verdict": None, "reject_code": None,
        "target_box_id": None, "expected_signer_group": None, "expected_signer_script": None,
        "actual_signer_group": None, "actual_source_type": None, "actual_signer_script": None,
        "companion": None,
    }
    assert report is not None and report.passed


# --------------------------------------------------------------------- #
# Multi-script signer pool (Hindi/Bengali/Latin named identities)
# --------------------------------------------------------------------- #

def test_display_name_passes_through_real_names_but_generates_for_anonymous_groups(tmp_path):
    ctx = _make_context(tmp_path)
    assert ctx.identity_registry.display_name("Priya Sharma") == "Priya Sharma"
    generated = ctx.identity_registry.display_name("signer_0014")
    assert generated != "signer_0014"
    assert " " in generated  # looks like "First Last"


def test_named_signers_are_part_of_the_authorized_pool(tmp_path):
    ctx = _make_context(tmp_path)
    groups = set(ctx.signature_dataset.signer_groups())
    assert {"Priya Sharma", "Raj Malhotra", "Vikram Singh", "Suman Das", "Anika Roy", "Carol Wong", "Henry Ooi"} <= groups


def test_named_signer_flows_through_to_metadata_with_correct_script(tmp_path):
    """Find a sample that lands on a named, scripted signer and check it renders cleanly."""
    ctx = _make_context(tmp_path, case_type="1a", seed=1)
    for i in range(1, 200):
        metadata, _, report = ctx.run_sample(i)
        v = metadata["verification"]
        if v["expected_signer_script"] != "latin":
            assert metadata["signer"]["name"] == v["expected_signer_group"]
            assert v["actual_signer_script"] == v["expected_signer_script"]
            assert report is not None and report.passed
            return
    raise AssertionError("No sample in range landed on a non-latin-script signer; widen the search range.")


def test_forged_mode_degrades_gracefully_for_signers_without_forgeries(tmp_path):
    """Named signers have no CEDAR-style forged counterpart; sampling 'forged' for them
    should fall back to their genuine records rather than crash (SignatureDataset already
    logs a warning for this -- this just confirms the caller-facing behavior)."""
    ctx = _make_context(tmp_path)
    records = ctx.signature_dataset.records_for_group("Priya Sharma", source_filter="forged")
    assert len(records) > 0
    assert all(r.signer_group == "Priya Sharma" for r in records)


def test_unrecognized_case_type_label_falls_back_to_generic_path(tmp_path):
    """A free-text case_type not in CASE_TABLE behaves exactly like today's plain-label usage."""
    ctx = _make_context(tmp_path, case_type="my-custom-label", seed=1)
    metadata, _, report = ctx.run_sample(1)
    assert metadata["verification"]["case_type"] is None
    assert metadata["quality"]["case_type"] == "my-custom-label"
    assert report is not None and report.passed