| from __future__ import annotations |
|
|
| import gzip |
| import json |
| from datetime import datetime, timezone |
| from pathlib import Path |
|
|
| import pytest |
|
|
| from ctx.core.wiki import artifact_promotion |
|
|
|
|
| def test_promote_staged_artifact_validates_replaces_and_records_metadata( |
| tmp_path: Path, |
| ) -> None: |
| target = tmp_path / "artifact.txt" |
| staged = tmp_path / "candidate.txt" |
| target.write_bytes(b"old\n") |
| staged.write_bytes(b"new\n") |
| validated: list[Path] = [] |
|
|
| result = artifact_promotion.promote_staged_artifact( |
| staged, |
| target, |
| validate=lambda path: validated.append(path), |
| now=datetime(2026, 5, 4, tzinfo=timezone.utc), |
| ) |
|
|
| metadata = json.loads(result.metadata_path.read_text(encoding="utf-8")) |
| assert validated == [staged] |
| assert target.read_bytes() == b"new\n" |
| assert not staged.exists() |
| assert metadata["status"] == "promoted" |
| assert metadata["previous"]["exists"] is True |
| assert metadata["previous"]["size"] == 4 |
| assert metadata["candidate"]["size"] == 4 |
| assert metadata["current"]["sha256"] == metadata["candidate"]["sha256"] |
| assert metadata["last_good"] == metadata["previous"] |
| assert metadata["rollback"]["available"] is True |
| assert metadata["rollback"]["sha256"] == metadata["previous"]["sha256"] |
|
|
|
|
| def test_promote_staged_artifact_validation_failure_preserves_target( |
| tmp_path: Path, |
| ) -> None: |
| target = tmp_path / "artifact.txt" |
| staged = tmp_path / "candidate.txt" |
| target.write_bytes(b"old\n") |
| staged.write_bytes(b"new\n") |
|
|
| def fail_validation(_path: Path) -> None: |
| raise ValueError("candidate failed validation") |
|
|
| with pytest.raises(ValueError, match="candidate failed validation"): |
| artifact_promotion.promote_staged_artifact( |
| staged, |
| target, |
| validate=fail_validation, |
| ) |
|
|
| assert target.read_bytes() == b"old\n" |
| assert staged.exists() |
| assert not target.with_name("artifact.txt.promotion.json").exists() |
|
|
| malformed_json = tmp_path / "broken.json" |
| malformed_json.write_text("{", encoding="utf-8") |
| with pytest.raises(ValueError, match="invalid JSON artifact"): |
| artifact_promotion.validate_json_artifact(malformed_json) |
|
|
| truncated_gzip = tmp_path / "broken.json.gz" |
| truncated_gzip.write_bytes(b"\x1f\x8b") |
| with pytest.raises(ValueError, match="invalid gzip JSON artifact"): |
| artifact_promotion.validate_gzip_json_artifact(truncated_gzip) |
|
|
| valid_gzip = tmp_path / "catalog.json.gz" |
| with gzip.open(valid_gzip, "wt", encoding="utf-8") as fh: |
| json.dump({"skills": []}, fh) |
| artifact_promotion.validate_gzip_json_artifact(valid_gzip, required_keys=("skills",)) |
|
|
|
|
| def test_promote_staged_artifact_replace_failure_preserves_target_and_last_good( |
| tmp_path: Path, |
| monkeypatch: pytest.MonkeyPatch, |
| ) -> None: |
| target = tmp_path / "artifact.txt" |
| staged = tmp_path / "candidate.txt" |
| target.write_bytes(b"old\n") |
| staged.write_bytes(b"new\n") |
|
|
| def locked_replace(_src: Path, _dst: Path) -> None: |
| raise PermissionError("locked") |
|
|
| monkeypatch.setattr(artifact_promotion, "_replace_with_retry", locked_replace) |
|
|
| with pytest.raises(PermissionError, match="locked"): |
| artifact_promotion.promote_staged_artifact(staged, target) |
|
|
| metadata = json.loads( |
| target.with_name("artifact.txt.promotion.json").read_text(encoding="utf-8") |
| ) |
| assert target.read_bytes() == b"old\n" |
| assert staged.exists() |
| assert metadata["status"] == "staged" |
| assert metadata["previous"]["sha256"] != metadata["candidate"]["sha256"] |
| assert metadata["last_good"] == metadata["previous"] |
| assert metadata["rollback"]["available"] is True |
|
|
|
|
| def test_promote_staged_artifact_recovers_after_post_replace_crash( |
| tmp_path: Path, |
| ) -> None: |
| target = tmp_path / "artifact.txt" |
| staged = tmp_path / "candidate.txt" |
| metadata_path = target.with_name("artifact.txt.promotion.json") |
| target.write_bytes(b"new\n") |
| candidate = artifact_promotion._snapshot(target) |
| previous = { |
| "path": str(target), |
| "exists": True, |
| "size": 4, |
| "sha256": "old-sha", |
| "mtime_ns": 1, |
| } |
| metadata_path.write_text( |
| json.dumps({ |
| "schema_version": 1, |
| "status": "staged", |
| "target": str(target), |
| "started_at": "2026-05-04T00:00:00+00:00", |
| "previous": previous, |
| "candidate": candidate, |
| }), |
| encoding="utf-8", |
| ) |
|
|
| result = artifact_promotion.promote_staged_artifact(staged, target) |
|
|
| metadata = json.loads(metadata_path.read_text(encoding="utf-8")) |
| assert result.current["sha256"] == candidate["sha256"] |
| assert metadata["status"] == "promoted" |
| assert metadata["previous"] == previous |
| assert metadata["candidate"]["sha256"] == candidate["sha256"] |
| assert metadata["current"]["sha256"] == candidate["sha256"] |
|
|