File size: 11,611 Bytes
b277c46
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46bb905
b277c46
 
 
 
 
 
9d1e3f2
 
254ec06
b277c46
 
 
 
 
 
d109222
9dadaf7
b277c46
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d109222
b277c46
 
 
 
 
 
 
 
 
 
 
d109222
b277c46
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""Tests Sprint 97 β€” B.6 : politique de modules contribuΓ©s.

Couvre :

1. ``ModuleManifest`` : as_dict, champs.
2. ``validate_manifest`` :
   - manifest valide β†’ tous les checks passent
   - champ manquant β†’ check fail
   - input/output_types vides β†’ check fail
3. ``audit_module`` :
   - module + manifest valide β†’ passed=True
   - classe ne hΓ©rite pas de BaseModule β†’ fail
   - I/O ne correspondent pas β†’ fail
   - process absent β†’ fail
   - case-insensitive sur les types
4. Vue HTML :
   - empty
   - rendu complet
   - badge βœ“ / βœ—
   - anti-injection
   - FR + EN
5. Documentation : prΓ©sente.
6. ComplΓ©tude i18n FR/EN.
"""

from __future__ import annotations

import json
from pathlib import Path

from picarones.evaluation.metrics.module_policy import (
    AuditCheck,
    AuditResult,
    ModuleManifest,
    audit_module,
    validate_manifest,
)
from picarones.domain.artifacts import ArtifactType
from picarones.domain.module_protocol import BaseModule
from picarones.reports_v2.html.renderers.module_audit import (
    build_module_audit_html,
)


def _load_labels(lang: str) -> dict:
    p = (
        Path(__file__).parent.parent.parent
        / "picarones" / "reports_v2" / "i18n" / f"{lang}.json"
    )
    return json.loads(p.read_text(encoding="utf-8"))


def _ok_manifest(**overrides) -> ModuleManifest:
    base = {
        "name": "my-mod", "version": "1.0.0",
        "author": "alice", "license": "MIT",
        "description": "test module",
        "input_types": ["text"], "output_types": ["text"],
    }
    base.update(overrides)
    return ModuleManifest(**base)


class _MockTextModule(BaseModule):
    name = "mock-text"
    input_types = (ArtifactType.TEXT,)
    output_types = (ArtifactType.TEXT,)
    execution_mode = "cpu"

    def process(self, inputs):
        return inputs


# ──────────────────────────────────────────────────────────────────────────
# 1. ModuleManifest
# ──────────────────────────────────────────────────────────────────────────


class TestManifest:
    def test_as_dict(self) -> None:
        m = _ok_manifest()
        d = m.as_dict()
        assert d["name"] == "my-mod"
        assert d["input_types"] == ["text"]
        assert d["citation"] is None

    def test_optional_fields(self) -> None:
        m = _ok_manifest(citation="Foo 2025", homepage="https://example")
        d = m.as_dict()
        assert d["citation"] == "Foo 2025"
        assert d["homepage"] == "https://example"


# ──────────────────────────────────────────────────────────────────────────
# 2. validate_manifest
# ──────────────────────────────────────────────────────────────────────────


class TestValidate:
    def test_full_manifest_passes(self) -> None:
        checks = validate_manifest(_ok_manifest())
        assert all(c.passed for c in checks)

    def test_missing_field_fails(self) -> None:
        checks = validate_manifest(_ok_manifest(license=""))
        assert any(
            (c.name == "manifest.license" and not c.passed)
            for c in checks
        )

    def test_empty_input_types_fails(self) -> None:
        checks = validate_manifest(_ok_manifest(input_types=[]))
        assert any(
            (c.name == "manifest.input_types" and not c.passed)
            for c in checks
        )

    def test_empty_output_types_fails(self) -> None:
        checks = validate_manifest(_ok_manifest(output_types=[]))
        assert any(
            (c.name == "manifest.output_types" and not c.passed)
            for c in checks
        )


# ──────────────────────────────────────────────────────────────────────────
# 3. audit_module
# ──────────────────────────────────────────────────────────────────────────


class TestAuditModule:
    def test_valid_module_passes(self) -> None:
        result = audit_module(_MockTextModule, _ok_manifest())
        assert result.passed
        assert result.n_failed == 0

    def test_non_basemodule_fails(self) -> None:
        class NotABaseModule:
            input_types = (ArtifactType.TEXT,)
            output_types = (ArtifactType.TEXT,)
            def process(self, inputs):
                return inputs
        result = audit_module(NotABaseModule, _ok_manifest())
        assert not result.passed
        assert any(
            c.name == "module.inherits_base_module" and not c.passed
            for c in result.checks
        )

    def test_io_mismatch_fails(self) -> None:
        # Manifest dit ALTO mais module dit TEXT
        manifest = _ok_manifest(output_types=["alto"])
        result = audit_module(_MockTextModule, manifest)
        assert not result.passed
        assert any(
            c.name == "module.output_types_match_manifest" and not c.passed
            for c in result.checks
        )

    def test_case_insensitive_types(self) -> None:
        # Manifest en majuscules, module en lowercase
        manifest = _ok_manifest(
            input_types=["TEXT"], output_types=["TEXT"],
        )
        result = audit_module(_MockTextModule, manifest)
        assert result.passed

    def test_accepts_instance_or_class(self) -> None:
        result_class = audit_module(_MockTextModule, _ok_manifest())
        result_instance = audit_module(_MockTextModule(), _ok_manifest())
        assert result_class.passed == result_instance.passed

    def test_audit_result_dict(self) -> None:
        result = audit_module(_MockTextModule, _ok_manifest())
        d = result.as_dict()
        assert d["module_name"] == "my-mod"
        assert d["passed"] is True
        assert d["n_passed"] >= 5
        assert isinstance(d["checks"], list)


# ──────────────────────────────────────────────────────────────────────────
# 4. Vue HTML
# ──────────────────────────────────────────────────────────────────────────


def _audit_entry(manifest: ModuleManifest, passed: bool = True,
                 n_failed: int = 0) -> dict:
    audit = AuditResult(
        module_name=manifest.name,
        passed=passed,
        checks=[
            AuditCheck("manifest.name", True),
            AuditCheck("manifest.version", True),
            AuditCheck("module.inherits_base_module", passed),
        ],
    )
    return {
        "manifest": manifest.as_dict(),
        "audit": audit.as_dict(),
    }


class TestRender:
    def test_empty_returns_empty(self) -> None:
        assert build_module_audit_html(None) == ""
        assert build_module_audit_html([]) == ""

    def test_renders_table(self) -> None:
        entry = _audit_entry(_ok_manifest(citation="Foo 2025"))
        html = build_module_audit_html([entry], _load_labels("fr"))
        assert "<table" in html
        assert "my-mod" in html
        assert "1.0.0" in html
        assert "Foo 2025" in html
        # Badge βœ“
        assert "βœ“" in html

    def test_failed_audit_shows_cross(self) -> None:
        manifest = _ok_manifest()
        entry = _audit_entry(manifest, passed=False, n_failed=2)
        # Patch n_failed
        entry["audit"]["n_failed"] = 2
        html = build_module_audit_html([entry], _load_labels("fr"))
        assert "βœ—" in html
        assert "2" in html  # nombre d'Γ©checs

    def test_anti_injection_name(self) -> None:
        manifest = _ok_manifest(name="<script>alert(1)</script>")
        entry = _audit_entry(manifest)
        html = build_module_audit_html([entry], _load_labels("fr"))
        assert "<script>alert" not in html
        assert "&lt;script&gt;" in html

    def test_anti_injection_homepage(self) -> None:
        manifest = _ok_manifest(homepage="<svg/>")
        entry = _audit_entry(manifest)
        html = build_module_audit_html([entry], _load_labels("fr"))
        assert "<svg/>" not in html
        assert "&lt;svg" in html

    def test_anti_injection_citation(self) -> None:
        manifest = _ok_manifest(citation="<img src=x onerror=alert>")
        entry = _audit_entry(manifest)
        html = build_module_audit_html([entry], _load_labels("fr"))
        assert "<img src" not in html
        assert "&lt;img" in html

    def test_renders_in_english(self) -> None:
        entry = _audit_entry(_ok_manifest())
        html = build_module_audit_html([entry], _load_labels("en"))
        assert "Audited modules" in html


# ──────────────────────────────────────────────────────────────────────────
# 5. Documentation
# ──────────────────────────────────────────────────────────────────────────


class TestDocumentation:
    def test_docs_present(self) -> None:
        path = (
            Path(__file__).parent.parent.parent
            / "docs" / "developer" / "module-policy.md"
        )
        assert path.exists()
        text = path.read_text(encoding="utf-8")
        # Doit mentionner les concepts clΓ©s
        assert "ModuleManifest" in text
        assert "BaseModule" in text
        assert "audit_module" in text

    def test_docs_lists_required_fields(self) -> None:
        path = (
            Path(__file__).parent.parent.parent
            / "docs" / "developer" / "module-policy.md"
        )
        text = path.read_text(encoding="utf-8")
        for key in ("name", "version", "author", "license", "description"):
            assert f"`{key}`" in text, f"champ manquant dans la doc : {key}"


# ──────────────────────────────────────────────────────────────────────────
# 6. ComplΓ©tude i18n
# ──────────────────────────────────────────────────────────────────────────


_KEYS = {
    "audit_title", "audit_note", "audit_pass", "audit_fail",
    "audit_module", "audit_status", "audit_version", "audit_author",
    "audit_license", "audit_io", "audit_citation", "audit_homepage",
}


class TestI18n:
    def test_fr(self) -> None:
        d = _load_labels("fr")
        assert not _KEYS - d.keys()

    def test_en(self) -> None:
        d = _load_labels("en")
        assert not _KEYS - d.keys()