File size: 7,678 Bytes
901bf8c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""Documentation that must follow the code (the code audit of 2026-09-21,
follow-up of 2026-09-22: 'Remaining doc drift'). Offline; reads the source
tree and RUNBOOK.md.

- RUNBOOK's 'Environment variables' table lists every variable the code
  reads (os.environ / env.get / getenv in canlex/ and tools/) and every one
  the Dockerfile sets for CanLex or the Hugging Face library; it names no
  CANLEX_* variable nothing reads or sets. (28 were undocumented, among them
  CANLEX_HTTP and CANLEX_SEE_ALSO_DRAFT.)
- The refresh choreography names every ingester (irb_guidelines and conduct
  were missing) and says every ingester takes --allow-shrink.
- The call-log section names every calllog.RESULT_KEYS field (it named five
  of twelve).
- No RUNBOOK line cites a scratchpad script that is not in the repo (three
  cited the a14/a15 before/after harness).
- canlex_list_acts' 'Bilingual' line counts French LEGISLATION only (it
  counted 124 instruments -- the 80 Acts and regulations plus 44 French
  guidance documents -- and 26,829 'articles' for 20,680).
"""
import re
import unittest
from pathlib import Path

from canlex import calllog, server
from canlex.index import LegislationIndex

ROOT = Path(__file__).resolve().parent.parent
RUNBOOK = (ROOT / "RUNBOOK.md").read_text(encoding="utf-8")

# os.environ.get("X"), os.environ["X"], env.get("X") (calllog.configure's
# env = os.environ), env["X"] (sweep.py), os.getenv("X"), and a module
# constant naming the variable a read uses, ENV_X = "CANLEX_X"
# (canlex/ratelimit.py reads its two variables through ENV_PER_MIN / ENV_BURST)
_ENV_READ = re.compile(
    r"""\b(?:environ|env)(?:\.get\(|\[)\s*["']([A-Z][A-Z0-9_]+)["']"""
    r"""|\bgetenv\(\s*["']([A-Z][A-Z0-9_]+)["']"""
    r"""|^ENV_[A-Z0-9_]+\s*=\s*["']((?:CANLEX|HF|CANLII)_[A-Z0-9_]+)["']""", re.M)
_DOCKER_ENV = re.compile(r"^\s*(?:ENV\s+)?((?:CANLEX|HF|CANLII)_[A-Z0-9_]+|PORT)=", re.M)


def env_vars_read():
    """{name: [files]} for every environment variable the code reads."""
    found = {}
    for top in ("canlex", "tools"):
        for path in sorted((ROOT / top).rglob("*.py")):
            for m in _ENV_READ.finditer(path.read_text(encoding="utf-8")):
                name = m.group(1) or m.group(2) or m.group(3)
                found.setdefault(name, []).append(path.relative_to(ROOT).as_posix())
    return found


def section(heading, level="##"):
    """The RUNBOOK text under `heading` up to the next heading of its level or above."""
    start = RUNBOOK.index(heading)
    stops = [m.start() for m in re.finditer(r"^#{1,%d} " % len(level), RUNBOOK, re.M)
             if m.start() > start]
    return RUNBOOK[start:stops[0] if stops else len(RUNBOOK)]


def table_names():
    body = section("### Environment variables", "###")
    return set(re.findall(r"^\| `([A-Z][A-Z0-9_]+)` \|", body, re.M))


class EnvironmentTableTests(unittest.TestCase):
    def test_the_grep_finds_the_known_readers(self):
        found = env_vars_read()
        for name in ("CANLEX_HTTP", "CANLEX_SEE_ALSO_DRAFT", "CANLEX_LOG_DIR",
                     "CANLII_API_KEY", "PORT", "CANLEX_MCP_URL"):
            self.assertIn(name, found)
        self.assertGreaterEqual(len(found), 35)

    def test_every_variable_the_code_reads_is_in_the_table(self):
        missing = {n: f for n, f in env_vars_read().items() if n not in table_names()}
        self.assertEqual(missing, {}, "add these to RUNBOOK 'Environment variables'")

    def test_every_variable_the_dockerfile_sets_is_in_the_table(self):
        docker = set(_DOCKER_ENV.findall((ROOT / "Dockerfile").read_text(encoding="utf-8")))
        self.assertTrue({"HF_HOME", "HF_HUB_OFFLINE", "CANLEX_HTTP"} <= docker)
        self.assertEqual(docker - table_names(), set())

    def test_the_table_names_nothing_stale(self):
        docker = set(_DOCKER_ENV.findall((ROOT / "Dockerfile").read_text(encoding="utf-8")))
        stale = {n for n in table_names() if n.startswith("CANLEX_")
                 and n not in env_vars_read() and n not in docker}
        self.assertEqual(stale, set())

    def test_the_two_local_only_variables_say_so(self):
        body = section("### Environment variables", "###")
        http = next(l for l in body.splitlines() if l.startswith("| `CANLEX_HTTP`"))
        draft = next(l for l in body.splitlines() if l.startswith("| `CANLEX_SEE_ALSO_DRAFT`"))
        self.assertIn("Local use only", http)
        self.assertIn("never set it on the Space", draft)


class ChoreographyTests(unittest.TestCase):
    def ingesters(self):
        """Every canlex module whose command line takes --allow-shrink."""
        return sorted(p.stem for p in (ROOT / "canlex").glob("*.py")
                      if '"--allow-shrink" in sys.argv' in p.read_text(encoding="utf-8"))

    def test_every_ingester_is_in_the_choreography(self):
        block = section("## The refresh choreography")
        self.assertIn("irb_guidelines", self.ingesters())
        missing = [m for m in self.ingesters() if f"py -m canlex.{m}" not in block]
        self.assertEqual(missing, [])

    def test_allow_shrink_is_documented_there(self):
        self.assertIn("--allow-shrink", section("## The refresh choreography"))


class CallLogFieldTests(unittest.TestCase):
    def test_every_result_key_is_named(self):
        body = section("## Call log (2026-09-16)")
        missing = [k for k in calllog.RESULT_KEYS if f"`{k}`" not in body]
        self.assertEqual(missing, [])

    def test_the_call_log_note_is_current(self):
        # the 2026-09-21 audit's 'the live Space predates the call log' is gone
        self.assertNotIn("live Space predates the call log", RUNBOOK)
        self.assertIn("the live Space carries the call log", RUNBOOK)


class ScratchpadCitationTests(unittest.TestCase):
    def test_no_line_cites_a_scratchpad_script(self):
        cited = re.findall(r"scratchpad[^\n]{0,40}?\b\w+\.py", RUNBOOK)
        self.assertEqual(cited, [])


def _chunk(cid, act_code, lang="en", doc_type="legislation", **extra):
    c = {"id": cid, "doc_type": doc_type, "act_code": act_code, "act_short": act_code,
         "act_name": act_code, "section": "1", "marginal_note": "t", "part": "",
         "division": "", "heading": "", "text": "x", "history": "", "last_amended": "",
         "in_force": "", "status": "in force", "current_to": "2026-03-31",
         "citation": f"{act_code}, s. 1", "source_url": "https://example.test"}
    if lang != "en":
        c["lang"] = lang
    c.update(extra)
    return c


class ListActsBilingualLineTests(unittest.TestCase):
    """canlex_list_acts' coverage line (server.py; HELD: canlex_list_acts is
    REVIEWED). Before: every French chunk counted, guidance included."""

    def setUp(self):
        idx = LegislationIndex.__new__(LegislationIndex)
        idx.chunks = [
            _chunk("I-2.5-s1", "I-2.5"), _chunk("I-2.5-fr-s1", "I-2.5", "fr"),
            _chunk("I-2.5-fr-s2", "I-2.5", "fr"),
            _chunk("C-46-s1", "C-46"), _chunk("C-46-fr-s1", "C-46", "fr"),
            _chunk("dmemo-fr-1", "D-Memo", "fr", "memorandum", part="Mémorandum"),
            _chunk("enf-fr-5-1", "ENF-5", "fr", "memorandum", part="ENF 5"),
            _chunk("directive-fr-1", "d1", "fr", "directive"),
        ]
        idx.related = lambda c: {}
        server._INDEX = idx
        self.addCleanup(setattr, server, "_INDEX", None)

    def test_only_french_legislation_is_counted(self):
        out = server.canlex_list_acts()
        self.assertIn("_Bilingual: 2 instruments also carry their parallel French "
                      "text (3 articles)", out)
        self.assertNotIn("5 instruments", out)        # the count before the fix