Skip to content

Commit b057aae

Browse files
committed
test: make the suite runnable under dev.bat's Enterprise unlock
Three local-only failures, none of which CI could see. Two share a root cause worth stating plainly: the native LicenseManager latches the first tier the process resolves and offers no way back. `set_license_key("")` raises ValueError, and `get_license_info()` keeps reporting the latched tier even after QECTOR_LICENSE_KEY is removed from the environment. Any test asserting Community-tier *rejection* is therefore unassertable once something has resolved a higher tier. test_enforcement_matrix `_native_cap_cell` popped QECTOR_LICENSE_KEY and assumed that produced Community. It does, until the latch is set - which is why the file passed alone and failed after test_rest_api_auth, whose authorised /api/license/info request is the first call to get_license_info(). Bisected one test at a time rather than guessed; three earlier theories (env caching, QECTOR_API_KEY, QECTOR_ENFORCE) were each disproved by probe first. test_auto_decoder_native monkeypatched the Python `qd.get_license_info`, but NativeAutoDecoder asks the Rust gate, so the fake never applied. Both now skip when the process has latched a non-Community tier, with the reason stated. CI runs unlicensed, so the tier is Community there and every one of these cells still executes - verified: 25 passed / 8 skipped unlicensed, exactly as before, and 20 passed / 13 skipped under dev.bat with nothing failing. test_auto_debug_fallbacks is unrelated and a real bug: it patched an instance attribute on AutoDecoder, which defines __slots__, so unpatching raised "attribute is read-only" in teardown. It never surfaced because CI has no GPU - `_get_cuda()` returns None there and the entire patched block is skipped, so the test passed while exercising none of the fallback path it exists to cover. Patch the class instead.
1 parent 29f6363 commit b057aae

3 files changed

Lines changed: 43 additions & 4 deletions

File tree

python/tests/test_auto_debug_fallbacks.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,21 @@ class _RaisingCuda:
3131
def batch_decode(self, _syn):
3232
raise RuntimeError("Simulated CUDA OOM Error")
3333

34-
real_get_decoder = ad._get_decoder
34+
# Patch the CLASS, not the instance. AutoDecoder defines __slots__, and
35+
# unpatching an instance attribute on a slotted object fails on exit with
36+
# "'AutoDecoder' object attribute '_get_decoder' is read-only" - so the
37+
# test blew up in teardown rather than testing anything. It went unnoticed
38+
# because CI has no GPU: `_get_cuda()` returns None there, this whole block
39+
# is skipped, and the test passes without exercising the fallback at all.
40+
# It only ran, and only failed, on a machine with CUDA.
41+
real_get_decoder = AutoDecoder._get_decoder
3542

36-
def _patched_get_decoder(backend):
43+
def _patched_get_decoder(self, backend):
3744
if backend == Backend.CUDA:
3845
return _RaisingCuda()
39-
return real_get_decoder(backend)
46+
return real_get_decoder(self, backend)
4047

41-
with patch.object(ad, "_get_decoder", new=_patched_get_decoder):
48+
with patch.object(AutoDecoder, "_get_decoder", _patched_get_decoder):
4249
syns = (np.random.default_rng(42).random((32, code.n_checks)) < 0.08).astype(np.uint8)
4350

4451
# The auto-debug engine should catch the CUDA exception and gracefully fall back to CPU_RAYON / CPU_SINGLE

python/tests/test_auto_decoder_native.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,18 @@ def test_native_auto_decoder_routing_d5():
3535

3636
@pytest.mark.skipif(not hasattr(qd, "NativeAutoDecoder"), reason="NativeAutoDecoder not available")
3737
def test_native_auto_decoder_rejects_qldpc_if_license(monkeypatch):
38+
# Read the real tier BEFORE the monkeypatch below shadows it. Faking
39+
# `qd.get_license_info` only moves the Python shim; NativeAutoDecoder asks
40+
# the Rust LicenseManager, which latches the first licence the process
41+
# resolves and offers no way back to Community. Under dev.bat's Enterprise
42+
# token the constructor is therefore allowed and this raises nothing.
43+
# CI runs unlicensed, so the tier is Community and the assertion holds.
44+
latched = str((qd.get_license_info() or {}).get("tier", "Community"))
45+
if latched != "Community":
46+
pytest.skip(
47+
f"process has latched tier {latched!r}; the native licence gate cannot be "
48+
"forced back to Community from Python. Run without QECTOR_LICENSE_KEY."
49+
)
3850
monkeypatch.setenv("QECTOR_ENFORCE", "1")
3951
monkeypatch.setattr(qd, "get_license_info", lambda: {"tier": "Community", "max_distance": 7})
4052
code = codes.rotated_surface_code(5)

python/tests/test_enforcement_matrix.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,26 @@ def _native_cap_cell(tier: str, side: str) -> None:
165165
"asserted in Rust (grpc_server.rs / mcp_server.rs)."
166166
)
167167
os.environ.pop("QECTOR_LICENSE_KEY", None)
168+
# Popping the variable is not sufficient once the process has resolved a
169+
# licence. The native LicenseManager latches the first tier it sees and
170+
# exposes no way back - `set_license_key("")` raises ValueError, and
171+
# `get_license_info()` keeps reporting the latched tier - so on a machine
172+
# running under dev.bat (Enterprise token) the cap below simply does not
173+
# fire and `pytest.raises(PermissionError)` fails.
174+
#
175+
# It only reproduced in a specific order: the latch is set by whichever test
176+
# first calls get_license_info(), which is test_rest_api_auth's authorised
177+
# /api/license/info request. Alone, this file passed; after that file, it
178+
# failed. Skip rather than assert something the process can no longer be put
179+
# into. CI runs unlicensed, so the tier is Community there and these cells
180+
# execute exactly as before - the coverage that matters is not lost.
181+
latched = str((q.get_license_info() or {}).get("tier", "Community"))
182+
if latched != "Community":
183+
pytest.skip(
184+
f"process has already latched tier {latched!r}; the native LicenseManager "
185+
"caches the first licence it resolves and cannot be returned to Community "
186+
"in-process. Run without QECTOR_LICENSE_KEY (as CI does) to exercise this."
187+
)
168188
d = STRADDLE[tier][side]
169189
if _expect(tier, side):
170190
with pytest.raises(PermissionError):

0 commit comments

Comments
 (0)