"""Tests for the DPO example config and sample data. These tests lock the example config + data to a working state so users who follow README steps never hit a broken example. Values are asserted in logical groups so a deliberate example change surfaces in one focused test rather than a 22-line wall. """ from __future__ import annotations import json from pathlib import Path from soup_cli.config.loader import load_config from soup_cli.config.schema import SoupConfig from soup_cli.data.formats import detect_format, format_to_messages from soup_cli.data.validator import validate_and_stats EXAMPLES_DIR = Path(__file__).resolve().parent.parent / "examples" DPO_CONFIG_PATH = EXAMPLES_DIR / "configs" / "dpo_example.yaml" DPO_DATA_PATH = EXAMPLES_DIR / "data" / "dpo_sample.jsonl" def test_dpo_example_config_loads(): """dpo_example.yaml should load and validate without errors.""" cfg = load_config(DPO_CONFIG_PATH) assert isinstance(cfg, SoupConfig) def test_dpo_example_task_and_data(): """Core task + data wiring: what matters for DPO to run at all.""" cfg = load_config(DPO_CONFIG_PATH) assert cfg.task == "dpo" assert cfg.data.format == "dpo" assert cfg.data.train == "examples/data/dpo_sample.jsonl" assert cfg.base == "meta-llama/Llama-3.1-8B-Instruct" def test_dpo_example_training_hyperparams(): """Training hyperparameters match the example's advertised config.""" cfg = load_config(DPO_CONFIG_PATH) assert cfg.training.epochs == 3 assert cfg.training.lr == 5e-6 assert cfg.training.dpo_beta == 0.1 assert cfg.training.quantization == "4bit" assert cfg.training.batch_size == 4 assert cfg.training.gradient_accumulation_steps == 4 def test_dpo_example_lora_config(): """LoRA config matches the example's advertised rank/alpha.""" cfg = load_config(DPO_CONFIG_PATH) assert cfg.training.lora.r == 16 assert cfg.training.lora.alpha == 32 assert cfg.training.lora.target_modules == "auto" def test_dpo_sample_data_exists(): """dpo_sample.jsonl should exist and have the issue-#4 minimum of 5+ pairs.""" assert DPO_DATA_PATH.exists() data = _load_jsonl(DPO_DATA_PATH) assert len(data) >= 5, "Expected at least 5 preference pairs" def test_dpo_sample_data_format_detected(): """Auto-detect should identify dpo_sample.jsonl as DPO format.""" data = _load_jsonl(DPO_DATA_PATH) detected = detect_format(data) assert detected == "dpo" def test_dpo_sample_data_has_required_keys(): """Every row in dpo_sample.jsonl should have prompt, chosen, rejected.""" data = _load_jsonl(DPO_DATA_PATH) for i, row in enumerate(data): assert "prompt" in row, f"Row {i} missing 'prompt'" assert "chosen" in row, f"Row {i} missing 'chosen'" assert "rejected" in row, f"Row {i} missing 'rejected'" def test_dpo_sample_data_validates_clean(): """dpo_sample.jsonl should pass validation with zero issues.""" data = _load_jsonl(DPO_DATA_PATH) stats = validate_and_stats(data, expected_format="dpo") assert stats["valid_rows"] == stats["total"] assert stats["duplicates"] == 0 assert stats["issues"] == [] def test_dpo_sample_data_converts(): """Every row should convert successfully via format_to_messages.""" data = _load_jsonl(DPO_DATA_PATH) for i, row in enumerate(data): result = format_to_messages(row, "dpo") assert result is not None, f"Row {i} failed conversion" assert "prompt" in result assert "chosen" in result assert "rejected" in result def _load_jsonl(path: Path) -> list[dict]: """Load a JSONL file into a list of dicts.""" with open(path, encoding="utf-8") as fh: return [json.loads(line) for line in fh if line.strip()]