File size: 9,645 Bytes
071ba6b | 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 | """Phase 0 env verification suite β ENV-01..03, ENV-07, ENV-08.
ROADMAP Success Criteria literals:
- `pytest tests/test_env.py::test_gold_answer_never_in_observation` passes (ENV-08)
- All 4 termination reasons appear as named tests covering ENV-07 (a/b/c/d)
- End-to-end HTTP roundtrip via FathomEnvClient proves ENV-01..03
"""
from __future__ import annotations
import time
import pytest
from fastapi.testclient import TestClient
from env.client import FathomEnvClient
from env.models import FathomAction, FathomObservation, FathomStepResult, TerminationReason
# βββ ENV-01: scaffold + HTTP roundtrip ββββββββββββββββββββββββββββββββββββββββ
def test_healthz_returns_ok(fresh_test_client: TestClient) -> None:
r = fresh_test_client.get("/healthz")
assert r.status_code == 200
assert r.json() == {"status": "ok"}
def test_reset_returns_observation_no_gold_field(fresh_test_client: TestClient) -> None:
r = fresh_test_client.post("/reset", json={"seed": 0, "difficulty": "easy"})
assert r.status_code == 200, r.text
obs_json = r.json()
assert "gold_answer" not in obs_json, f"ENV-08 violation: gold_answer key in observation JSON: {obs_json}"
assert "task_type" not in obs_json
obs = FathomObservation.model_validate(obs_json)
assert obs.tokens_remaining == 100_000
assert obs.depth_max == 2
assert obs.turns_left == 20
def test_step_roundtrip_print_1_plus_1(fresh_test_client: TestClient) -> None:
fresh_test_client.post("/reset", json={"seed": 0, "difficulty": "easy"})
r = fresh_test_client.post("/step", json={"tool_name": "repl", "code": "print(1 + 1)"})
assert r.status_code == 200
body = r.json()
assert "gold_answer" not in body.get("observation", {})
sr = FathomStepResult.model_validate(body)
assert "2" in sr.observation.stdout
assert sr.done is False
# βββ ENV-08: gold-answer sealing (ROADMAP Success Criterion #3 literal) βββββββ
def test_gold_answer_never_in_observation(fresh_test_client: TestClient, fixture_rows: list[dict]) -> None:
"""For every fixture row, after reset() + several steps, no observation dict contains
gold_answer or task_type as a key.
This is the LITERAL test that ROADMAP Phase 0 Exit Gate Success Criterion #3 names.
Key-presence is what's tested β the gold VALUE appearing inside context_preview is
NOT a violation (the context legitimately contains it).
"""
difficulties = ["easy", "medium", "hard"]
for diff in difficulties:
r = fresh_test_client.post("/reset", json={"seed": 0, "difficulty": diff})
assert r.status_code == 200
obs = r.json()
assert "gold_answer" not in obs, f"diff={diff}: gold_answer leaked on reset: keys={list(obs.keys())}"
assert "task_type" not in obs, f"diff={diff}: task_type leaked on reset: keys={list(obs.keys())}"
for code in ("x = 1", "y = x + 2", "z = y * 3"):
r2 = fresh_test_client.post("/step", json={"tool_name": "repl", "code": code})
assert r2.status_code == 200
body = r2.json()
assert "gold_answer" not in body.get("observation", {}), f"step leak on diff={diff}, code={code}"
assert "task_type" not in body.get("observation", {})
def test_state_endpoint_sanitized(fresh_test_client: TestClient) -> None:
"""ENV-08: /state returns a whitelist dict β no gold_answer, task_type, or raw context."""
fresh_test_client.post("/reset", json={"seed": 0, "difficulty": "easy"})
r = fresh_test_client.get("/state")
assert r.status_code == 200
s = r.json()
assert "gold_answer" not in s
assert "task_type" not in s
assert "context" not in s
assert "episode_id" in s
assert "step_count" in s
assert "tokens_used_total" in s
# βββ ENV-07: 4 termination reasons ββββββββββββββββββββββββββββββββββββββββββββ
def test_termination_answer(fresh_test_client: TestClient) -> None:
"""ENV-07a β <answer>...</answer> emits done=True with reason=answer."""
fresh_test_client.post("/reset", json={"seed": 0, "difficulty": "easy"})
r = fresh_test_client.post("/step", json={"tool_name": "repl", "code": "<answer>42</answer>"})
sr = FathomStepResult.model_validate(r.json())
assert sr.done is True
assert sr.info.get("termination_reason") == TerminationReason.ANSWER.value
assert sr.observation.answer_emitted is True
assert sr.observation.return_val == "42"
def test_termination_max_steps(fresh_test_client: TestClient) -> None:
"""ENV-07b β exceeding max_steps terminates with reason=max_steps."""
fresh_test_client.post("/reset", json={"seed": 0, "difficulty": "easy", "max_steps": 2})
r1 = fresh_test_client.post("/step", json={"tool_name": "repl", "code": "a = 1"})
sr1 = FathomStepResult.model_validate(r1.json())
assert sr1.done is False
r2 = fresh_test_client.post("/step", json={"tool_name": "repl", "code": "b = 2"})
sr2 = FathomStepResult.model_validate(r2.json())
assert sr2.done is True
assert sr2.info.get("termination_reason") == TerminationReason.MAX_STEPS.value
def test_termination_max_tokens(fresh_test_client: TestClient) -> None:
"""ENV-07c β exceeding max_tokens terminates with reason=max_tokens."""
fresh_test_client.post("/reset", json={"seed": 0, "difficulty": "easy", "max_tokens": 2})
r = fresh_test_client.post(
"/step",
json={"tool_name": "repl", "code": "print('this is a long string designed to exceed two tokens easily')"},
)
sr = FathomStepResult.model_validate(r.json())
assert sr.done is True, f"Expected done=True on max_tokens, got {sr}"
assert sr.info.get("termination_reason") == TerminationReason.MAX_TOKENS.value
def test_termination_walltime(fresh_test_client: TestClient) -> None:
"""ENV-07d β walltime budget exceeded terminates with reason=walltime."""
fresh_test_client.post("/reset", json={"seed": 0, "difficulty": "easy", "walltime_budget_s": 0.3})
time.sleep(0.5)
r = fresh_test_client.post("/step", json={"tool_name": "repl", "code": "x = 1"})
sr = FathomStepResult.model_validate(r.json())
assert sr.done is True, f"Expected walltime termination, got {sr}"
assert sr.info.get("termination_reason") == TerminationReason.WALLTIME.value
# βββ ENV-06: malformed code β structured error, done=False ββββββββββββββββββββ
def test_malformed_code_returns_error_observation_done_false(fresh_test_client: TestClient) -> None:
fresh_test_client.post("/reset", json={"seed": 0, "difficulty": "easy"})
r = fresh_test_client.post("/step", json={"tool_name": "repl", "code": "1 / 0"})
sr = FathomStepResult.model_validate(r.json())
assert sr.done is False, "ENV-06: division by zero should NOT terminate the episode"
combined = sr.observation.stderr + (sr.observation.return_val or "")
assert "ZeroDivisionError" in combined
def test_malformed_syntax_returns_error_observation(fresh_test_client: TestClient) -> None:
fresh_test_client.post("/reset", json={"seed": 0, "difficulty": "easy"})
r = fresh_test_client.post("/step", json={"tool_name": "repl", "code": "def (:"})
sr = FathomStepResult.model_validate(r.json())
assert sr.done is False
assert sr.observation.stderr, "syntax error should surface in stderr"
# βββ ENV-03 / D-10: difficulty mapping ββββββββββββββββββββββββββββββββββββββββ
def test_difficulty_mapping_medium_loads_multi_needle_row(fresh_test_client: TestClient) -> None:
fresh_test_client.post("/reset", json={"seed": 0, "difficulty": "medium"})
r = fresh_test_client.get("/state")
assert r.status_code == 200
s = r.json()
assert s["task_id"] == "fixture-0001-multi-needle", f"D-10 violation: got {s['task_id']}"
def test_difficulty_mapping_hard_loads_counting_row(fresh_test_client: TestClient) -> None:
fresh_test_client.post("/reset", json={"seed": 0, "difficulty": "hard"})
r = fresh_test_client.get("/state")
s = r.json()
assert s["task_id"] == "fixture-0002-counting"
def test_reset_by_task_id(fresh_test_client: TestClient) -> None:
fresh_test_client.post("/reset", json={"seed": 0, "task_id": "fixture-0002-counting"})
r = fresh_test_client.get("/state")
s = r.json()
assert s["task_id"] == "fixture-0002-counting"
# βββ ENV-01: live HTTP roundtrip via FathomEnvClient ββββββββββββββββββββββββββ
def test_live_uvicorn_roundtrip(live_uvicorn_server: str) -> None:
"""End-to-end HTTP roundtrip matching ROADMAP Success Criterion #1.
Spawns uvicorn in a subprocess, uses FathomEnvClient over the real HTTP wire,
verifies reset() + step(print(1+1)) + state() all succeed with no gold leak.
"""
with FathomEnvClient(base_url=live_uvicorn_server) as client:
assert client.healthz() == {"status": "ok"}
obs = client.reset(seed=0, difficulty="easy")
assert isinstance(obs, FathomObservation)
sr = client.step(FathomAction(tool_name="repl", code="print(1+1)"))
assert isinstance(sr, FathomStepResult)
assert "2" in sr.observation.stdout
assert sr.done is False
s = client.state()
assert "gold_answer" not in s
assert "task_type" not in s
assert s.get("step_count") == 1
|