core/tests/test_holdout_encryption.py
Shay 257fd4503d
feat(evals): ADR-0105 — sealed holdout encryption via age (#108)
* feat(evals): add pyrage dependency

* feat(evals): add sealed holdout path resolution

* feat(evals): implement sealed holdout decryption

* feat(evals): add sealed holdout CLI

* test(evals): add sealed holdout encryption tests

* docs(decisions): add ADR-0105 sealed holdout encryption

* feat(evals): route holdout split through sealed decryptor

* docs(decisions): add ADR-0105 index entry

* chore: restore project description

* fix(evals): use pyrage Identity.from_str and pin curriculum SHA

- holdout_runner: pyrage exposes Identity.from_str, not from_file; parse
  identity file by line and pass list[Identity] into decrypt(). Restores
  PR 108's sealed-holdout test suite to green.
- verify_lane_shas: realign curriculum_loop_closure pin with the actual
  deterministic runner output (carryover from PR 107).
2026-05-22 10:09:43 -07:00

94 lines
2.7 KiB
Python

from __future__ import annotations
import os
from pathlib import Path
import pytest
from pyrage import encrypt
from pyrage import x25519
from evals.holdout_runner import HOLDOUT_KEY_ENV
from evals.holdout_runner import _decrypt_holdout
@pytest.fixture()
def plaintext_cases() -> str:
return '{"prompt":"alpha","expected":"beta"}\n'
@pytest.fixture()
def identity_pair() -> tuple[x25519.Identity, x25519.Recipient]:
identity = x25519.Identity.generate()
return identity, identity.to_public()
def test_round_trip_decrypt(tmp_path: Path, plaintext_cases: str, identity_pair) -> None:
identity, recipient = identity_pair
identity_path = tmp_path / "identity.txt"
identity_path.write_text(str(identity))
encrypted_path = tmp_path / "cases.jsonl.age"
encrypted_path.write_bytes(
encrypt(plaintext_cases.encode("utf-8"), [recipient])
)
os.environ[HOLDOUT_KEY_ENV] = str(identity_path)
try:
cases = _decrypt_holdout(encrypted_path)
finally:
os.environ.pop(HOLDOUT_KEY_ENV, None)
assert len(cases) == 1
assert cases[0]["prompt"] == "alpha"
def test_reject_wrong_identity(tmp_path: Path, plaintext_cases: str) -> None:
good_identity = x25519.Identity.generate()
wrong_identity = x25519.Identity.generate()
wrong_identity_path = tmp_path / "wrong_identity.txt"
wrong_identity_path.write_text(str(wrong_identity))
encrypted_path = tmp_path / "cases.jsonl.age"
encrypted_path.write_bytes(
encrypt(
plaintext_cases.encode("utf-8"),
[good_identity.to_public()],
)
)
os.environ[HOLDOUT_KEY_ENV] = str(wrong_identity_path)
try:
with pytest.raises(RuntimeError):
_decrypt_holdout(encrypted_path)
finally:
os.environ.pop(HOLDOUT_KEY_ENV, None)
def test_dev_mode_plaintext_fallback(tmp_path: Path, plaintext_cases: str) -> None:
plaintext_path = tmp_path / "cases_plaintext.jsonl"
plaintext_path.write_text(plaintext_cases)
encrypted_path = tmp_path / "cases.jsonl.age"
os.environ.pop(HOLDOUT_KEY_ENV, None)
cases = _decrypt_holdout(encrypted_path)
assert len(cases) == 1
assert cases[0]["expected"] == "beta"
def test_key_set_disables_plaintext_fallback(tmp_path: Path, plaintext_cases: str) -> None:
plaintext_path = tmp_path / "cases_plaintext.jsonl"
plaintext_path.write_text(plaintext_cases)
missing_identity = tmp_path / "missing_identity.txt"
encrypted_path = tmp_path / "cases.jsonl.age"
os.environ[HOLDOUT_KEY_ENV] = str(missing_identity)
try:
with pytest.raises(FileNotFoundError):
_decrypt_holdout(encrypted_path)
finally:
os.environ.pop(HOLDOUT_KEY_ENV, None)