| """Dataset regression tests — DATA-01..06.""" |
| from __future__ import annotations |
|
|
| import hashlib |
| import json |
| import os |
| from collections import Counter |
| from pathlib import Path |
|
|
| import pytest |
|
|
|
|
| |
| |
| |
|
|
| @pytest.fixture(scope="session") |
| def generated_data(): |
| base = Path("data") |
| train_path = base / "train.jsonl" |
| eval_path = base / "eval.jsonl" |
| sft_path = base / "sft_traces.jsonl" |
| for p in [train_path, eval_path, sft_path]: |
| assert p.exists(), f"Generated file missing: {p}. Run `python -m data.generate` first." |
| train = [json.loads(l) for l in train_path.open(encoding="utf-8")] |
| eval_ = [json.loads(l) for l in eval_path.open(encoding="utf-8")] |
| sft = [json.loads(l) for l in sft_path.open(encoding="utf-8")] |
| return {"train": train, "eval": eval_, "sft": sft} |
|
|
|
|
| |
| |
| |
|
|
| def test_train_count_and_eval_count(generated_data): |
| assert len(generated_data["train"]) == 1000 |
| assert len(generated_data["eval"]) == 200 |
|
|
|
|
| |
| |
| |
|
|
| def test_mix_ratios_within_tolerance(generated_data): |
| rows = generated_data["train"] |
| c = Counter(r["task_type"] for r in rows) |
| total = len(rows) |
| expected = {"niah": 0.4, "multi_needle": 0.3, "extractive": 0.2, "counting": 0.1} |
| for task_type, target in expected.items(): |
| actual = c[task_type] / total |
| assert abs(actual - target) <= 0.02, ( |
| f"{task_type}: expected ~{target:.2f}, got {actual:.3f}" |
| ) |
|
|
|
|
| |
| |
| |
|
|
| def test_trivial_floor_satisfied(generated_data): |
| rows = generated_data["train"] |
| share = sum(1 for r in rows if r["difficulty"] == "trivial") / len(rows) |
| assert share >= 0.05, f"Trivial share {share:.3f} < 0.05" |
|
|
|
|
| |
| |
| |
|
|
| def test_train_eval_disjoint(generated_data): |
| train_ids = {r["task_id"] for r in generated_data["train"]} |
| eval_ids = {r["task_id"] for r in generated_data["eval"]} |
| assert not (train_ids & eval_ids), "Train and eval task_ids overlap" |
|
|
| train_pairs = {(r["task_type"], r["seed"]) for r in generated_data["train"]} |
| eval_pairs = {(r["task_type"], r["seed"]) for r in generated_data["eval"]} |
| assert not (train_pairs & eval_pairs), "Train and eval (task_type, seed) pairs overlap" |
|
|
|
|
| |
| |
| |
|
|
| def test_schema_keys(generated_data): |
| required = {"task_id", "task_type", "difficulty", "context_length", |
| "prompt", "context", "gold_answer", "seed"} |
| for split in ("train", "eval"): |
| for i, row in enumerate(generated_data[split]): |
| assert required.issubset(row.keys()), ( |
| f"{split}[{i}] missing keys: {required - row.keys()}" |
| ) |
|
|
|
|
| |
| |
| |
|
|
| def test_no_gold_answer_verbatim_leak(generated_data): |
| """Gold answer should appear ONLY as part of the intentional needle/fact. |
| |
| We check that the number of occurrences is exactly 1 (the needle itself). |
| Multiple occurrences would indicate the filler accidentally contains the answer, |
| which would enable trivial shortcut exploits (REW-06 length/leak gate). |
| """ |
| violations = [] |
| for split in ("train", "eval"): |
| for row in generated_data[split]: |
| gold = row["gold_answer"].lower().strip() |
| if not gold: |
| continue |
| |
| count = row["context"].lower().count(gold) |
| if count > 1: |
| violations.append((row["task_id"], count)) |
| assert not violations, f"Gold answer found >1x in context for: {violations[:5]}" |
|
|
|
|
|
|
| |
| |
| |
|
|
| def test_sft_traces_chat_format(generated_data): |
| sft = generated_data["sft"] |
| assert len(sft) >= 450, f"Only {len(sft)} SFT traces (need >= 450)" |
| valid_roles = {"system", "user", "assistant", "tool"} |
| for i, trace in enumerate(sft): |
| assert "messages" in trace, f"trace[{i}] missing 'messages'" |
| messages = trace["messages"] |
| assert len(messages) >= 3, f"trace[{i}] has only {len(messages)} messages" |
| roles = {m["role"] for m in messages} |
| assert roles.issubset(valid_roles), f"trace[{i}] invalid roles: {roles}" |
| |
| has_answer = any( |
| m["role"] == "assistant" and "<answer>" in m.get("content", "") |
| for m in messages |
| ) |
| assert has_answer, f"trace[{i}] has no assistant <answer> message" |
|
|
|
|
| |
| |
| |
|
|
| @pytest.mark.slow |
| def test_generation_is_byte_deterministic(tmp_path, monkeypatch): |
| """Re-running generate_all with same seeds produces byte-identical output.""" |
| monkeypatch.delenv("ANTHROPIC_API_KEY", raising=False) |
|
|
| from data.generate import generate_all |
|
|
| generate_all(out_dir=tmp_path / "run1", seeds_path="data/seeds.json") |
| generate_all(out_dir=tmp_path / "run2", seeds_path="data/seeds.json") |
|
|
| for fname in ("train.jsonl", "eval.jsonl", "sft_traces.jsonl"): |
| h1 = hashlib.sha256((tmp_path / "run1" / fname).read_bytes()).hexdigest() |
| h2 = hashlib.sha256((tmp_path / "run2" / fname).read_bytes()).hexdigest() |
| assert h1 == h2, f"{fname}: non-deterministic output (run1 != run2)" |
|
|