269 lines
9.8 KiB
Python
269 lines
9.8 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
import stat
|
|
from hashlib import sha256
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
import mdpolish.artifact_store as artifact_store
|
|
from mdpolish.artifact_store import ArtifactStoreError, StoredDocument, publish_run
|
|
|
|
|
|
def stored_document(document_id: str = "paper", output: bytes | None = b"cleaned\n") -> StoredDocument:
|
|
status = "success" if output is not None else "failed"
|
|
current_sha256 = sha256(output).hexdigest() if output is not None else "2" * 64
|
|
payload = {
|
|
"schema_version": 1,
|
|
"document": {"document_id": document_id, "source_label": f"inputs/{document_id}.md"},
|
|
"status": status,
|
|
"input_sha256": "1" * 64,
|
|
"current_sha256": current_sha256,
|
|
"changes": [],
|
|
"errors": [] if status == "success" else [{"error_type": "SyntheticError"}],
|
|
"residual_proposals": [],
|
|
"output": {
|
|
"cleaned_path": "cleaned.md" if output is not None else None,
|
|
"diff_path": "changes.diff" if output is not None else None,
|
|
},
|
|
}
|
|
return StoredDocument(
|
|
document_id=document_id,
|
|
result_json=(json.dumps(payload, indent=2) + "\n").encode(),
|
|
cleaned_markdown=output,
|
|
diff=b"" if output is not None else None,
|
|
output_sha256=sha256(output).hexdigest() if output is not None else None,
|
|
)
|
|
|
|
|
|
def manifest_json(
|
|
run_date: str,
|
|
run_id: str,
|
|
documents: tuple[StoredDocument, ...],
|
|
) -> bytes:
|
|
indexes: list[dict[str, object]] = []
|
|
statuses: list[str] = []
|
|
change_count = 0
|
|
for document in documents:
|
|
report = json.loads(document.result_json)
|
|
status = report["status"]
|
|
statuses.append(status)
|
|
change_count += len(report["changes"])
|
|
base = f"documents/{document.document_id}"
|
|
indexes.append(
|
|
{
|
|
"document_id": document.document_id,
|
|
"source_label": report["document"]["source_label"],
|
|
"status": status,
|
|
"input_sha256": report["input_sha256"],
|
|
"current_sha256": report["current_sha256"],
|
|
"change_count": len(report["changes"]),
|
|
"result_path": f"{base}/result.json",
|
|
"cleaned_path": f"{base}/cleaned.md" if status == "success" else None,
|
|
"diff_path": f"{base}/changes.diff" if status == "success" else None,
|
|
}
|
|
)
|
|
failed_count = statuses.count("failed")
|
|
unstable_count = statuses.count("unstable")
|
|
overall_status = "failed" if failed_count else "unstable" if unstable_count else "success"
|
|
payload = {
|
|
"schema_version": 1,
|
|
"run": {"run_id": run_id, "run_date": run_date, "status": overall_status},
|
|
"documents": indexes,
|
|
"summary": {
|
|
"document_count": len(documents),
|
|
"success_count": statuses.count("success"),
|
|
"failed_count": failed_count,
|
|
"unstable_count": unstable_count,
|
|
"change_count": change_count,
|
|
},
|
|
}
|
|
return (json.dumps(payload, indent=2) + "\n").encode()
|
|
|
|
|
|
def test_publish_run_creates_private_date_layout_and_status_specific_files(tmp_path: Path) -> None:
|
|
artifacts_root = tmp_path / "artifacts"
|
|
documents = (stored_document("success"), stored_document("failed", None))
|
|
|
|
run_directory = publish_run(
|
|
artifacts_root=artifacts_root,
|
|
run_date="2026-08-22",
|
|
run_id="example-run",
|
|
manifest_json=manifest_json("2026-08-22", "example-run", documents),
|
|
documents=documents,
|
|
)
|
|
|
|
assert run_directory == artifacts_root / "2026-08-22" / "runs" / "example-run"
|
|
assert json.loads((run_directory / "manifest.json").read_bytes())["run"]["status"] == "failed"
|
|
assert (run_directory / "documents/success/result.json").is_file()
|
|
assert (run_directory / "documents/success/cleaned.md").read_bytes() == b"cleaned\n"
|
|
assert (run_directory / "documents/success/changes.diff").read_bytes() == b""
|
|
assert (run_directory / "documents/failed/result.json").is_file()
|
|
assert not (run_directory / "documents/failed/cleaned.md").exists()
|
|
assert not (run_directory / "documents/failed/changes.diff").exists()
|
|
|
|
for directory in (
|
|
artifacts_root,
|
|
artifacts_root / "2026-08-22",
|
|
artifacts_root / "2026-08-22/runs",
|
|
run_directory,
|
|
run_directory / "documents",
|
|
run_directory / "documents/success",
|
|
):
|
|
assert stat.S_IMODE(directory.stat().st_mode) == 0o700
|
|
for artifact_file in run_directory.rglob("*"):
|
|
if artifact_file.is_file():
|
|
assert stat.S_IMODE(artifact_file.stat().st_mode) == 0o600
|
|
|
|
|
|
def test_publish_run_rejects_existing_target_without_overwriting(tmp_path: Path) -> None:
|
|
artifacts_root = tmp_path / "artifacts"
|
|
documents = (stored_document(),)
|
|
first_manifest = manifest_json("2026-08-22", "same-run", documents)
|
|
run_directory = publish_run(
|
|
artifacts_root=artifacts_root,
|
|
run_date="2026-08-22",
|
|
run_id="same-run",
|
|
manifest_json=first_manifest,
|
|
documents=documents,
|
|
)
|
|
|
|
with pytest.raises(ArtifactStoreError, match="already exists"):
|
|
publish_run(
|
|
artifacts_root=artifacts_root,
|
|
run_date="2026-08-22",
|
|
run_id="same-run",
|
|
manifest_json=first_manifest,
|
|
documents=documents,
|
|
)
|
|
|
|
assert (run_directory / "manifest.json").read_bytes() == first_manifest
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("run_date", "run_id"),
|
|
[
|
|
("2026-8-22", "valid"),
|
|
("2026-02-30", "valid"),
|
|
("2026-08-22", "Uppercase"),
|
|
("2026-08-22", "../escape"),
|
|
("2026-08-22", "two..dots"),
|
|
],
|
|
)
|
|
def test_publish_run_rejects_unsafe_date_and_run_id(tmp_path: Path, run_date: str, run_id: str) -> None:
|
|
documents = (stored_document(),)
|
|
with pytest.raises(ArtifactStoreError):
|
|
publish_run(
|
|
artifacts_root=tmp_path / "artifacts",
|
|
run_date=run_date,
|
|
run_id=run_id,
|
|
manifest_json=manifest_json(run_date, run_id, documents),
|
|
documents=documents,
|
|
)
|
|
|
|
|
|
def test_publish_run_rejects_inconsistent_or_duplicate_document_artifacts(tmp_path: Path) -> None:
|
|
valid = stored_document()
|
|
bad_hash = StoredDocument("paper", valid.result_json, b"output", b"", "0" * 64)
|
|
with pytest.raises(ArtifactStoreError, match="output hash"):
|
|
publish_run(
|
|
artifacts_root=tmp_path / "artifacts-a",
|
|
run_date="2026-08-22",
|
|
run_id="bad-hash",
|
|
manifest_json=manifest_json("2026-08-22", "bad-hash", (bad_hash,)),
|
|
documents=(bad_hash,),
|
|
)
|
|
|
|
duplicates = (stored_document(), stored_document())
|
|
with pytest.raises(ArtifactStoreError, match="unique"):
|
|
publish_run(
|
|
artifacts_root=tmp_path / "artifacts-b",
|
|
run_date="2026-08-22",
|
|
run_id="duplicate",
|
|
manifest_json=manifest_json("2026-08-22", "duplicate", duplicates),
|
|
documents=duplicates,
|
|
)
|
|
|
|
|
|
def test_publish_run_rejects_manifest_path_or_document_mismatch(tmp_path: Path) -> None:
|
|
documents = (stored_document(),)
|
|
wrong_date = manifest_json("2026-08-21", "review", documents)
|
|
with pytest.raises(ArtifactStoreError, match="identity"):
|
|
publish_run(
|
|
artifacts_root=tmp_path / "artifacts-a",
|
|
run_date="2026-08-22",
|
|
run_id="review",
|
|
manifest_json=wrong_date,
|
|
documents=documents,
|
|
)
|
|
|
|
payload = json.loads(manifest_json("2026-08-22", "review", documents))
|
|
payload["documents"][0]["change_count"] = 99
|
|
mismatched_index = (json.dumps(payload, indent=2) + "\n").encode()
|
|
with pytest.raises(ArtifactStoreError, match="index"):
|
|
publish_run(
|
|
artifacts_root=tmp_path / "artifacts-b",
|
|
run_date="2026-08-22",
|
|
run_id="review",
|
|
manifest_json=mismatched_index,
|
|
documents=documents,
|
|
)
|
|
|
|
|
|
def test_publish_race_does_not_replace_a_new_target(
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
documents = (stored_document(),)
|
|
original_rename = artifact_store._rename_no_replace
|
|
|
|
def create_competing_target(source: Path, target: Path) -> None:
|
|
target.mkdir(mode=0o700)
|
|
(target / "keep").write_bytes(b"existing")
|
|
original_rename(source, target)
|
|
|
|
monkeypatch.setattr(artifact_store, "_rename_no_replace", create_competing_target)
|
|
with pytest.raises(ArtifactStoreError, match="already exists"):
|
|
publish_run(
|
|
artifacts_root=tmp_path / "artifacts",
|
|
run_date="2026-08-22",
|
|
run_id="raced",
|
|
manifest_json=manifest_json("2026-08-22", "raced", documents),
|
|
documents=documents,
|
|
)
|
|
|
|
target = tmp_path / "artifacts/2026-08-22/runs/raced"
|
|
assert (target / "keep").read_bytes() == b"existing"
|
|
assert not any(path.name.startswith(".raced.") for path in target.parent.iterdir())
|
|
|
|
|
|
def test_write_failure_cleans_temporary_directory_and_does_not_publish(
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
original_write = artifact_store._write_private_file
|
|
call_count = 0
|
|
|
|
def fail_second_write(path: Path, content: bytes) -> None:
|
|
nonlocal call_count
|
|
call_count += 1
|
|
if call_count == 2:
|
|
raise OSError("synthetic write failure")
|
|
original_write(path, content)
|
|
|
|
monkeypatch.setattr(artifact_store, "_write_private_file", fail_second_write)
|
|
documents = (stored_document(),)
|
|
|
|
with pytest.raises(OSError, match="synthetic"):
|
|
publish_run(
|
|
artifacts_root=tmp_path / "artifacts",
|
|
run_date="2026-08-22",
|
|
run_id="broken",
|
|
manifest_json=manifest_json("2026-08-22", "broken", documents),
|
|
documents=documents,
|
|
)
|
|
|
|
runs_directory = tmp_path / "artifacts/2026-08-22/runs"
|
|
assert list(runs_directory.iterdir()) == []
|