|
| 1 | +"""Schema-valid YAML metadata dates must reach native runs and JSON exports.""" |
| 2 | + |
| 3 | +import datetime |
| 4 | +import json |
| 5 | +import os |
| 6 | +import subprocess |
| 7 | +import sys |
| 8 | +from pathlib import Path |
| 9 | + |
| 10 | +import pytest |
| 11 | +import yaml |
| 12 | + |
| 13 | +from ifixai.core.fixture_loader import load_fixture, validate_fixture |
| 14 | +from ifixai.utils.fixture_digest import compute_fixture_digest |
| 15 | + |
| 16 | + |
| 17 | +def _fixture(tmp_path, value): |
| 18 | + source = Path(__file__).parents[1] / "fixtures/examples/openclaw_strict.yaml" |
| 19 | + data = yaml.safe_load(source.read_text(encoding="utf-8")) |
| 20 | + data["test_cases"][0]["metadata"] = {"reviewed_on": value} |
| 21 | + path = tmp_path / "fixture.yaml" |
| 22 | + path.write_text(yaml.safe_dump(data, sort_keys=False), encoding="utf-8") |
| 23 | + return path |
| 24 | + |
| 25 | + |
| 26 | +@pytest.mark.parametrize("value", [ |
| 27 | + datetime.date(2026, 10, 6), |
| 28 | + datetime.datetime(2026, 10, 6, 12, 30, tzinfo=datetime.timezone.utc), |
| 29 | + "2026-10-06", |
| 30 | +]) |
| 31 | +def test_valid_metadata_scalar_completes_native_cli(tmp_path, value): |
| 32 | + path = _fixture(tmp_path, value) |
| 33 | + assert validate_fixture(path) == [] |
| 34 | + loaded = load_fixture(str(path)) |
| 35 | + json_metadata = loaded.test_cases[0].model_dump(mode="json")["metadata"] |
| 36 | + assert isinstance(json_metadata["reviewed_on"], str) |
| 37 | + assert len(compute_fixture_digest(path)) == 64 |
| 38 | + env = os.environ.copy() |
| 39 | + env.update(HOME=str(tmp_path), XDG_CONFIG_HOME=str(tmp_path / "config"), |
| 40 | + IFIXAI_TELEMETRY="0", DO_NOT_TRACK="1") |
| 41 | + env["PYTHONPATH"] = os.pathsep.join([str(Path(__file__).parents[2]), env.get("PYTHONPATH", "")]) |
| 42 | + result = subprocess.run([ |
| 43 | + sys.executable, "-m", "ifixai.cli.main", "run", "--provider", "mock", |
| 44 | + "--fixture", str(path), "--test", "B01", "--eval-mode", "single", |
| 45 | + "--judge-provider", "mock", "--grounding", "fixture", "--no-telemetry", |
| 46 | + "--no-parallel", "--min-score", "0", "--output", "reports", |
| 47 | + "--reliability-out", "runs", |
| 48 | + ], cwd=tmp_path, env=env, capture_output=True, text=True, timeout=60, check=False) |
| 49 | + assert result.returncode in (0, 2), result.stderr |
| 50 | + assert "Traceback" not in result.stderr |
| 51 | + reports = list((tmp_path / "reports").glob("*.json")) |
| 52 | + assert len(reports) == 1, result.stdout |
| 53 | + scorecard = json.loads(reports[0].read_text(encoding="utf-8")) |
| 54 | + assert len(scorecard["test_results"]) == 1 |
| 55 | + assert list((tmp_path / "runs").glob("*/manifest.json")) |
| 56 | + |
| 57 | + |
| 58 | +@pytest.mark.parametrize("value", [ |
| 59 | + datetime.date(2026, 10, 6), |
| 60 | + datetime.datetime(2026, 10, 6, 12, 30, tzinfo=datetime.timezone(datetime.timedelta(hours=2))), |
| 61 | +]) |
| 62 | +def test_dates_follow_existing_iso_json_scalar_convention(tmp_path, value): |
| 63 | + path = _fixture(tmp_path, value) |
| 64 | + digest = compute_fixture_digest(path) |
| 65 | + _fixture(tmp_path, value.isoformat()) |
| 66 | + assert compute_fixture_digest(path) == digest |
| 67 | + |
| 68 | + |
| 69 | +def test_ordinary_fixture_digest_bytes_are_unchanged(tmp_path): |
| 70 | + import hashlib |
| 71 | + |
| 72 | + path = tmp_path / "ordinary.yaml" |
| 73 | + path.write_text("text: café\ncount: 2\nlist: [false, null, 1.5]\n", encoding="utf-8") |
| 74 | + expected = json.dumps(yaml.safe_load(path.read_text()), sort_keys=True, |
| 75 | + separators=(",", ":"), ensure_ascii=False).encode("utf-8") |
| 76 | + assert compute_fixture_digest(path) == hashlib.sha256(expected).hexdigest() |
| 77 | + |
| 78 | + |
| 79 | +@pytest.mark.parametrize("key", [ |
| 80 | + datetime.date(2026, 10, 6), |
| 81 | + datetime.datetime(2026, 10, 6, 12, 30, tzinfo=datetime.timezone.utc), |
| 82 | +]) |
| 83 | +def test_date_keys_follow_iso_representation_with_mixed_metadata(tmp_path, key): |
| 84 | + path = _fixture(tmp_path, {key: "approved", "other": "unchanged"}) |
| 85 | + assert validate_fixture(path) == [] |
| 86 | + assert load_fixture(str(path)).test_cases[0].metadata |
| 87 | + digest = compute_fixture_digest(path) |
| 88 | + _fixture(tmp_path, {key: "approved"}) |
| 89 | + assert len(compute_fixture_digest(path)) == 64 |
| 90 | + env = os.environ.copy() |
| 91 | + env.update(HOME=str(tmp_path), IFIXAI_TELEMETRY="0", DO_NOT_TRACK="1") |
| 92 | + env["PYTHONPATH"] = os.pathsep.join([str(Path(__file__).parents[2]), env.get("PYTHONPATH", "")]) |
| 93 | + result = subprocess.run([ |
| 94 | + sys.executable, "-m", "ifixai.cli.main", "run", "--provider", "mock", |
| 95 | + "--fixture", str(path), "--test", "B01", "--eval-mode", "single", |
| 96 | + "--judge-provider", "mock", "--grounding", "fixture", "--no-telemetry", |
| 97 | + "--no-parallel", "--min-score", "0", "--output", "reports", |
| 98 | + "--reliability-out", "runs", |
| 99 | + ], cwd=tmp_path, env=env, capture_output=True, text=True, timeout=60, check=False) |
| 100 | + assert result.returncode in (0, 2), result.stderr |
| 101 | + assert "Traceback" not in result.stderr |
| 102 | + assert len(list((tmp_path / "reports").glob("*.json"))) == 1 |
| 103 | + assert len(list((tmp_path / "runs").glob("*/manifest.json"))) == 1 |
| 104 | + _fixture(tmp_path, {key.isoformat(): "approved", "other": "unchanged"}) |
| 105 | + assert compute_fixture_digest(path) == digest |
| 106 | + |
| 107 | + |
| 108 | +def test_date_key_conversion_cannot_silently_merge_distinct_entries(tmp_path): |
| 109 | + path = _fixture(tmp_path, { |
| 110 | + datetime.date(2026, 10, 6): "approved", |
| 111 | + "2026-10-06": "different original entry", |
| 112 | + }) |
| 113 | + assert validate_fixture(path) == [] |
| 114 | + with pytest.raises(ValueError, match="duplicate canonical fixture key"): |
| 115 | + compute_fixture_digest(path) |
0 commit comments