File size: 12,863 Bytes
4be6a52
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
"""Release staging checks on synthetic development fixtures; no held-out games."""

import copy
import importlib.util
import os
import stat
import zipfile
from pathlib import Path
from types import SimpleNamespace

import pytest

from stackcraft.evaluation import paired_report
from stackcraft.players import RandomPlayer
from stackcraft.tournament import run_episode


@pytest.fixture
def script():
    path = Path(__file__).resolve().parents[1] / "scripts/build_release.py"
    spec = importlib.util.spec_from_file_location("stackcraft_test_release", path)
    assert spec is not None and spec.loader is not None
    module = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(module)
    return module


@pytest.mark.parametrize("path", ["/tmp/private", "../private", "nested/../../private", ""])
def test_evidence_paths_cannot_escape_selection_directory(script, path):
    with pytest.raises(ValueError, match="evidence paths"):
        script.safe_relative(path)


def test_file_copy_rejects_symlinks_and_preserves_exact_bytes(script, tmp_path):
    source = tmp_path / "original"
    source.write_bytes(b"\x00checkpoint unchanged\xff")
    destination = tmp_path / "model/checkpoint/weights"
    script.copy_file(source, destination)
    assert script.sha256(source) == script.sha256(destination)
    link = tmp_path / "link"
    link.symlink_to(source)
    with pytest.raises(ValueError, match="not links"):
        script.copy_file(link, tmp_path / "copied-link")


def test_demo_allowlist_excludes_planning_credentials_and_arbitrary_data(script, tmp_path):
    for relative in (
        "src/stackcraft/server.py",
        "src/stackcraft/web/app.js",
        "src/stackcraft/web/baseline-demo.json",
        "src/stackcraft/web/secret.json",
        "src/stackcraft/web/.env",
        "plan.md",
        "record.md",
        "runs/raw.json",
    ):
        path = tmp_path / relative
        path.parent.mkdir(parents=True, exist_ok=True)
        path.write_text("test")
    paths = {str(path) for path in script.code_files(tmp_path, demo=True)}
    assert {
        "src/stackcraft/server.py",
        "src/stackcraft/web/app.js",
        "Dockerfile",
        ".dockerignore",
    } <= paths
    assert "src/stackcraft/web/baseline-demo.json" in paths
    assert not paths & {
        "plan.md",
        "record.md",
        "runs/raw.json",
        "src/stackcraft/web/secret.json",
        "src/stackcraft/web/.env",
    }


def evaluation_fixture(script, tmp_path, monkeypatch):
    # Explicitly reduce only this test's validator pool to development seeds.
    # No policy is run on any reserved study seed.
    seeds = (7, 8)
    monkeypatch.setattr(script, "FINAL_TEST_SEEDS", seeds)
    checkpoint = tmp_path / "checkpoint"
    checkpoint.mkdir()
    for name, content in (
        ("training_config.json", '{"mode":"head"}'),
        ("joint_head.safetensors", "test bytes, not model weights"),
        ("reference.json", "{}"),
    ):
        (checkpoint / name).write_text(content)
    evaluator = script.load_helper("evaluate_clef.py")
    exporter = script.load_helper("export_demo.py")
    calls = []
    evaluator.validate_selection = lambda args, hashes: calls.append((args, hashes))
    hashes = evaluator.checkpoint_hashes(checkpoint)
    evaluation = tmp_path / "evaluation"
    evaluation.mkdir()
    selection = tmp_path / "selection.json"
    selection.write_text('{"test_fixture":true}')
    prototypes = {seed: run_episode(RandomPlayer(), seed, 200) for seed in seeds}
    report = {
        "mode": "tournament",
        "final_test": True,
        "seeds": list(seeds),
        "max_pieces": 200,
        "checkpoint_sha256": hashes,
        "selection": script.json_file(selection),
        "players": {},
        "episodes": [],
    }
    for name in script.PLAYERS:
        metadata = {"revision": f"test-{name}", "runtime_config": {}, "load_seconds": [0.0]}
        report["players"][name] = metadata
        script.write_json(evaluation / name / "player.json", metadata)
        for seed in seeds:
            episode = copy.deepcopy(prototypes[seed])
            episode["player_id"] = name
            episode["player"] = {
                "name": name,
                "revision": metadata["revision"],
                "runtime_config": {},
            }
            report["episodes"].append(episode)
            script.write_json(evaluation / name / f"seed-{seed}.json", episode)
    for key, (trained, base) in script.COMPARISONS.items():
        report[key] = paired_report(report["episodes"], trained_id=trained, base_id=base)
    script.write_json(evaluation / "report.json", report)
    request = {
        "mode": "tournament",
        "final_test": True,
        "seeds": list(seeds),
        "max_pieces": 200,
        "checkpoint_sha256": hashes,
        "selection_sha256": script.sha256(selection),
        "players": list(script.PLAYERS),
        "max_length": 4096,
    }
    script.write_json(evaluation / "request.json", request)
    return evaluation, selection, checkpoint, {"evaluate": evaluator, "export": exporter}, calls


def test_complete_release_evidence_recomputes_replays_and_pairings(script, tmp_path, monkeypatch):
    evaluation, selection, checkpoint, helpers, calls = evaluation_fixture(
        script, tmp_path, monkeypatch
    )
    report, hashes, paths = script.validate_evaluation(evaluation, selection, checkpoint, helpers)
    assert len(report["episodes"]) == 10
    assert len(paths) == 17  # report/request + 5 metadata + 10 episodes.
    assert calls[0][0].checkpoint == checkpoint
    assert hashes == helpers["evaluate"].checkpoint_hashes(checkpoint)


@pytest.mark.parametrize("tampering", ["missing-pair", "outcome", "metric", "checkpoint"])
def test_release_rejects_incomplete_or_inconsistent_evidence(
    script, tmp_path, monkeypatch, tampering
):
    evaluation, selection, checkpoint, helpers, _ = evaluation_fixture(
        script, tmp_path, monkeypatch
    )
    report_path = evaluation / "report.json"
    report = script.json_file(report_path)
    if tampering == "missing-pair":
        report["episodes"].pop()
    elif tampering == "outcome":
        report["episodes"][0]["outcome"]["score"] = 999
        episode = report["episodes"][0]
        script.write_json(
            evaluation / episode["player_id"] / f"seed-{episode['seed']}.json", episode
        )
    elif tampering == "metric":
        report["trained_vs_base"]["paired_trained_minus_base"]["lines"]["mean_difference"] = 999
    else:
        (checkpoint / "joint_head.safetensors").write_text("changed")
    script.write_json(report_path, report)
    with pytest.raises(ValueError):
        script.validate_evaluation(evaluation, selection, checkpoint, helpers)


def test_model_card_uses_verified_results_and_rejects_other_pending_text(
    script, tmp_path, monkeypatch
):
    evaluation, _, _, _, _ = evaluation_fixture(script, tmp_path, monkeypatch)
    template = (
        "**Release preparation: full-study results and selected checkpoint are pending.**\n"
        "**Insert verified held-out results here before publication.**"
    )
    card = script.render_model_card(
        template, script.json_file(evaluation / "report.json"), {"selected_key": "epoch-01"}
    )
    assert "epoch-01" in card
    assert "| base-fp32 |" in card
    assert "paired 95% bootstrap interval" in card
    for first, second in script.COMPARISONS.values():
        assert f"| {first} − {second} |" in card
    assert "Lines mean (median)" in card
    assert "Median ms | p95 ms | Invalid decisions" in card
    assert "not adjusted for multiple comparisons" in card
    assert "pending" not in card.lower()
    with pytest.raises(ValueError, match="pending"):
        script.render_model_card(
            template + "\nAnother pending result",
            script.json_file(evaluation / "report.json"),
            {"selected_key": "epoch-01"},
        )


def test_existing_release_is_never_overwritten(script, tmp_path):
    with pytest.raises(ValueError, match="already exists"):
        script.build_release(SimpleNamespace(output=tmp_path))


def evidence_fixture(model):
    content = {
        "evidence/evaluation/report.json": b'{"episodes": []}\n' * 100,
        "evidence/evaluation/base/seed-30000.json": b"exact original bytes\x00\xff\n",
        "evidence/selection/epoch-02/report.json": b"{}\n",
        "evidence/selection/selection.json": b'{"selected_key":"epoch-02"}\n',
    }
    for name, value in content.items():
        path = model / name
        path.parent.mkdir(parents=True, exist_ok=True)
        path.write_bytes(value)
    return content


def test_evidence_zip_extracts_exact_original_paths_and_bytes(script, tmp_path):
    model = tmp_path / "model"
    content = evidence_fixture(model)
    manifest = script.pack_evidence(model)
    assert (model / "evidence").is_dir()  # Only build_release owns staged-copy deletion.
    assert manifest == script.json_file(model / "evidence-files.json")
    assert set(manifest["files"]) == set(content)
    assert manifest["archive_sha256"] == script.sha256(model / "evidence.zip")
    with zipfile.ZipFile(model / "evidence.zip") as archive:
        assert archive.namelist() == sorted(content)
        for info in archive.infolist():
            assert info.date_time == (1980, 1, 1, 0, 0, 0)
            assert info.compress_type == zipfile.ZIP_DEFLATED
            assert stat.S_IFMT(info.external_attr >> 16) == stat.S_IFREG
        archive.extractall(tmp_path / "extracted-model")
    for name, value in content.items():
        extracted = tmp_path / "extracted-model" / name
        assert extracted.read_bytes() == value
        assert script.sha256(extracted) == manifest["files"][name]["sha256"]
        assert extracted.stat().st_size == manifest["files"][name]["bytes"]
    with pytest.raises(ValueError, match="already exists"):
        script.pack_evidence(model)


def test_archive_is_deterministic_despite_original_mtime_and_permissions(script, tmp_path):
    first, second = tmp_path / "first", tmp_path / "second"
    evidence_fixture(first)
    content = evidence_fixture(second)
    for name in content:
        os.utime(second / name, (1234567890, 1234567890))
        (second / name).chmod(0o600)
    script.pack_evidence(first)
    script.pack_evidence(second)
    assert (first / "evidence.zip").read_bytes() == (second / "evidence.zip").read_bytes()
    assert (first / "evidence-files.json").read_bytes() == (
        second / "evidence-files.json"
    ).read_bytes()


@pytest.mark.parametrize(
    "name",
    [
        "../escape",
        "/absolute",
        "evidence/../escape",
        "evidence//x",
        "evidence/x\\y",
        "evidence/C:x",
        "other/file",
    ],
)
def test_archive_rejects_unsafe_or_wrong_root_entry_names(script, tmp_path, name):
    with pytest.raises(ValueError, match="canonical"):
        script.verify_evidence_archive(tmp_path / "not-opened.zip", {name: {}})


def test_archive_rejects_different_bytes_and_duplicate_entries(script, tmp_path):
    model = tmp_path / "model"
    evidence_fixture(model)
    manifest = script.pack_evidence(model)
    expected = copy.deepcopy(manifest["files"])
    expected["evidence/evaluation/report.json"]["sha256"] = "0" * 64
    with pytest.raises(ValueError, match="SHA256"):
        script.verify_evidence_archive(model / "evidence.zip", expected)
    with zipfile.ZipFile(model / "evidence.zip", "a") as archive:
        with pytest.warns(UserWarning, match="Duplicate name"):
            archive.writestr("evidence/selection/selection.json", b"duplicate")
    with pytest.raises(ValueError, match="exact raw evidence inventory"):
        script.verify_evidence_archive(model / "evidence.zip", manifest["files"])


def test_archive_refuses_symlinks_and_leaves_raw_evidence(script, tmp_path):
    model = tmp_path / "model"
    evidence_fixture(model)
    (model / "evidence/linked").symlink_to(model / "evidence/selection")
    with pytest.raises(ValueError, match="symlinks"):
        script.pack_evidence(model)
    assert (model / "evidence/selection/selection.json").is_file()


def test_evidence_changed_during_packing_is_not_removed(script, tmp_path, monkeypatch):
    model = tmp_path / "model"
    evidence_fixture(model)
    original_verify = script.verify_evidence_archive

    def verify_then_change(archive, expected):
        original_verify(archive, expected)
        (model / "evidence/selection/selection.json").write_bytes(b"changed source")

    monkeypatch.setattr(script, "verify_evidence_archive", verify_then_change)
    with pytest.raises(ValueError, match="changed while packing"):
        script.pack_evidence(model)
    assert (model / "evidence/selection/selection.json").read_bytes() == b"changed source"
    assert not (model / "evidence-files.json").exists()