Skip to content

Commit 78cfd4b

Browse files
author
root
committed
Add 14 safe_solve exact-answer enforcement tests (217 total)
Tests cover: code fence stripping, quote stripping, multi-line first-line extraction, explanation marker extraction, import/require/ __proto__ blocking, clean passthrough. All 217 pass.
1 parent ba07d08 commit 78cfd4b

2 files changed

Lines changed: 156 additions & 0 deletions

File tree

run_tests.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1368,6 +1368,91 @@ def mock_llm(system, user):
13681368
answer = safe_solve("What is 6 * 7?", llm_fn=mock_llm)
13691369
assert answer == "42"
13701370

1371+
# ── safe_solve exact-answer enforcement ───────────────
1372+
print("\n── safe_solve exact-answer enforcement ─────────")
1373+
1374+
@test("safe_solve: strips markdown code fences")
1375+
def _():
1376+
def mock_llm(s, u): return "```\n42\n```"
1377+
assert safe_solve("What is 6*7?", llm_fn=mock_llm) == "42"
1378+
1379+
@test("safe_solve: strips code fence with language tag")
1380+
def _():
1381+
def mock_llm(s, u): return "```text\nHELLO\n```"
1382+
assert safe_solve("Reverse OLLEH", llm_fn=mock_llm) == "HELLO"
1383+
1384+
@test("safe_solve: strips surrounding double quotes")
1385+
def _():
1386+
def mock_llm(s, u): return '"42"'
1387+
assert safe_solve("What is 6*7?", llm_fn=mock_llm) == "42"
1388+
1389+
@test("safe_solve: strips surrounding single quotes")
1390+
def _():
1391+
def mock_llm(s, u): return "'HELLO'"
1392+
assert safe_solve("Reverse OLLEH", llm_fn=mock_llm) == "HELLO"
1393+
1394+
@test("safe_solve: strips surrounding backticks")
1395+
def _():
1396+
def mock_llm(s, u): return "`42`"
1397+
assert safe_solve("What is 6*7?", llm_fn=mock_llm) == "42"
1398+
1399+
@test("safe_solve: multi-line takes first non-empty line")
1400+
def _():
1401+
def mock_llm(s, u): return "42\nThis is my explanation"
1402+
assert safe_solve("What is 6*7?", llm_fn=mock_llm) == "42"
1403+
1404+
@test("safe_solve: extracts answer from 'the answer is X'")
1405+
def _():
1406+
def mock_llm(s, u): return "the answer is 42"
1407+
assert safe_solve("What is 6*7?", llm_fn=mock_llm) == "42"
1408+
1409+
@test("safe_solve: extracts answer from 'the result is: X'")
1410+
def _():
1411+
def mock_llm(s, u): return "the result is: HELLO"
1412+
assert safe_solve("Reverse OLLEH", llm_fn=mock_llm) == "HELLO"
1413+
1414+
@test("safe_solve: extracts answer from 'therefore X'")
1415+
def _():
1416+
def mock_llm(s, u): return "therefore 42"
1417+
assert safe_solve("What is 6*7?", llm_fn=mock_llm) == "42"
1418+
1419+
@test("safe_solve: strips quotes after explanation extraction")
1420+
def _():
1421+
def mock_llm(s, u): return 'the answer is "42"'
1422+
assert safe_solve("What is 6*7?", llm_fn=mock_llm) == "42"
1423+
1424+
@test("safe_solve: blocks import statement in answer")
1425+
def _():
1426+
def mock_llm(s, u): return "import os"
1427+
try:
1428+
safe_solve("What is 2+2?", llm_fn=mock_llm)
1429+
assert False, "Should have raised"
1430+
except ValueError as e:
1431+
assert "suspicious" in str(e).lower()
1432+
1433+
@test("safe_solve: blocks require() in answer")
1434+
def _():
1435+
def mock_llm(s, u): return "require('fs')"
1436+
try:
1437+
safe_solve("What is 2+2?", llm_fn=mock_llm)
1438+
assert False, "Should have raised"
1439+
except ValueError as e:
1440+
assert "suspicious" in str(e).lower()
1441+
1442+
@test("safe_solve: blocks __proto__ in answer")
1443+
def _():
1444+
def mock_llm(s, u): return "__proto__"
1445+
try:
1446+
safe_solve("What is 2+2?", llm_fn=mock_llm)
1447+
assert False, "Should have raised"
1448+
except ValueError as e:
1449+
assert "suspicious" in str(e).lower()
1450+
1451+
@test("safe_solve: clean short answer passes through unchanged")
1452+
def _():
1453+
def mock_llm(s, u): return "KRPS"
1454+
assert safe_solve("Reverse SPARK then remove vowels", llm_fn=mock_llm) == "KRPS"
1455+
13711456

13721457
# ── prompt_builder Tests ──────────────────────────────
13731458
print("\n── prompt_builder ──────────────────────────────")

tests/calibrate_5_2.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
#!/usr/bin/env python3
2+
"""Quick calibration of gpt-5.2 against all 25 challenge types (10 attempts each)."""
3+
import os, sys, json, time, subprocess
4+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src'))
5+
6+
from agentchallenge import AgentChallenge
7+
from agentchallenge.types import CHALLENGE_TYPES, DIFFICULTY_MAP
8+
9+
OPENAI_KEY = os.environ.get("OPENAI_API_KEY", "")
10+
ATTEMPTS = 10
11+
MODEL = "gpt-5.2"
12+
ALL_TYPES = sorted(CHALLENGE_TYPES.keys())
13+
14+
def call_openai(model, prompt):
15+
payload = json.dumps({
16+
"model": model,
17+
"messages": [
18+
{"role": "system", "content": "You are solving a challenge. Reply with ONLY the answer, nothing else. No explanation, no quotes, no formatting."},
19+
{"role": "user", "content": prompt}
20+
],
21+
"temperature": 0, "max_tokens": 200,
22+
})
23+
try:
24+
r = subprocess.run(
25+
["curl", "-s", "-m", "20", "https://api.openai.com/v1/chat/completions",
26+
"-H", f"Authorization: Bearer {OPENAI_KEY}",
27+
"-H", "Content-Type: application/json", "-d", payload],
28+
capture_output=True, text=True, timeout=25
29+
)
30+
data = json.loads(r.stdout)
31+
return data["choices"][0]["message"]["content"].strip()
32+
except Exception as e:
33+
return f"ERROR: {e}"
34+
35+
print(f"\n{'='*60}")
36+
print(f" MODEL: {MODEL} ({ATTEMPTS} attempts per type)")
37+
print(f"{'='*60}")
38+
39+
results = {}
40+
for ctype in ALL_TYPES:
41+
correct = 0
42+
for i in range(ATTEMPTS):
43+
ac = AgentChallenge(secret=f"cal52-{ctype}-{i}-key", types=[ctype])
44+
ch = ac.create()
45+
answer = call_openai(MODEL, ch.prompt)
46+
result = ac.verify(ch.token, answer)
47+
if result.valid:
48+
correct += 1
49+
time.sleep(0.2)
50+
51+
pct = correct / ATTEMPTS * 100
52+
bar = "█" * int(pct / 5) + "░" * (20 - int(pct / 5))
53+
status = "✅" if pct == 100 else "⚠️" if pct >= 80 else "❌"
54+
print(f" {status} {ctype:25s} {bar} {correct}/{ATTEMPTS} ({pct:.0f}%)")
55+
results[ctype] = {"correct": correct, "total": ATTEMPTS, "pct": pct}
56+
57+
# Summary by tier
58+
print(f"\n{'='*60}")
59+
print(f" TIER SUMMARY")
60+
print(f"{'='*60}")
61+
for tier_name, tier_types in DIFFICULTY_MAP.items():
62+
tier_results = [results.get(t, {}).get("pct", 0) for t in tier_types if t in results]
63+
avg = sum(tier_results) / len(tier_results) if tier_results else 0
64+
all_100 = all(p == 100 for p in tier_results)
65+
print(f" {tier_name:10s}: avg {avg:.0f}% {'✅ ALL 100%' if all_100 else ''}")
66+
67+
# Write results
68+
out = os.path.join(os.path.dirname(__file__), "calibration_gpt52.json")
69+
with open(out, "w") as f:
70+
json.dump(results, f, indent=2)
71+
print(f"\n Saved to {out}")

0 commit comments

Comments
 (0)