159 lines
5.9 KiB
Python
159 lines
5.9 KiB
Python
"""WP-220B regression tests: deterministic, clean-room release export."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import subprocess
|
|
import sys
|
|
import tarfile
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from scripts import create_manifest, export_release
|
|
|
|
|
|
def _git(repo: Path, *args: str) -> str:
|
|
result = subprocess.run( # noqa: S603
|
|
["git", *args], # noqa: S607
|
|
cwd=repo,
|
|
capture_output=True,
|
|
text=True,
|
|
check=True,
|
|
)
|
|
return result.stdout
|
|
|
|
|
|
def _build_repo(tmp_path: Path) -> Path:
|
|
repo = tmp_path / "repo"
|
|
repo.mkdir()
|
|
_git(repo, "init", "-q", "-b", "main")
|
|
_git(repo, "config", "user.email", "test@example.invalid")
|
|
_git(repo, "config", "user.name", "WP-220 Test")
|
|
_git(repo, "config", "core.autocrlf", "false")
|
|
(repo / ".gitattributes").write_text("* text=auto eol=lf\n", encoding="utf-8")
|
|
(repo / ".gitignore").write_text("secret.local\n", encoding="utf-8")
|
|
(repo / "README.md").write_text("hello\n", encoding="utf-8")
|
|
(repo / "src").mkdir()
|
|
(repo / "src" / "app.py").write_text("print('hi')\n", encoding="utf-8")
|
|
(repo / "pyproject.toml").write_text(
|
|
'[project]\nname = "x"\nversion = "0.0.0"\ndependencies = []\n\n'
|
|
"[dependency-groups]\ndev = []\n",
|
|
encoding="utf-8",
|
|
)
|
|
(repo / "evidence").mkdir()
|
|
(repo / "evidence" / "NOTE.md").write_text("evidence\n", encoding="utf-8")
|
|
(repo / "secret.local").write_text("not tracked, must never be exported\n", encoding="utf-8")
|
|
_git(repo, "add", "-A")
|
|
_git(repo, "commit", "-q", "-m", "initial commit")
|
|
manifest = create_manifest.build_manifest("HEAD", repo)
|
|
(repo / "PACK_MANIFEST.json").write_text(
|
|
json.dumps(manifest, indent=2) + "\n", encoding="utf-8"
|
|
)
|
|
_git(repo, "add", "PACK_MANIFEST.json")
|
|
_git(repo, "commit", "-q", "-m", "record manifest")
|
|
return repo
|
|
|
|
|
|
@pytest.fixture
|
|
def repo(tmp_path: Path) -> Path:
|
|
return _build_repo(tmp_path)
|
|
|
|
|
|
def _run_export(repo: Path, output_dir: Path, *extra: str) -> subprocess.CompletedProcess[str]:
|
|
script = Path(export_release.__file__)
|
|
return subprocess.run( # noqa: S603
|
|
[sys.executable, str(script), "--root", str(repo), "--output-dir", str(output_dir), *extra],
|
|
cwd=repo,
|
|
capture_output=True,
|
|
text=True,
|
|
check=False,
|
|
)
|
|
|
|
|
|
def test_source_archive_contains_only_tracked_content(repo: Path, tmp_path: Path) -> None:
|
|
output_dir = tmp_path / "export"
|
|
result = _run_export(repo, output_dir)
|
|
assert result.returncode == 0, result.stdout + result.stderr
|
|
|
|
archive_path = output_dir / "itworx-mcp-hub-source.tar.gz"
|
|
assert archive_path.is_file()
|
|
with tarfile.open(archive_path) as archive:
|
|
names = set(archive.getnames())
|
|
assert "secret.local" not in names
|
|
assert "README.md" in names
|
|
assert "src/app.py" in names
|
|
|
|
|
|
def test_export_verifies_clean_against_manifest(repo: Path, tmp_path: Path) -> None:
|
|
output_dir = tmp_path / "export"
|
|
result = _run_export(repo, output_dir, "--verify")
|
|
assert result.returncode == 0, result.stdout + result.stderr
|
|
|
|
export_manifest = json.loads((output_dir / "EXPORT_MANIFEST.json").read_text(encoding="utf-8"))
|
|
assert export_manifest["sourceArchiveVerifiedAgainstManifest"] is True
|
|
assert export_manifest["sourceArchiveVerificationErrors"] == []
|
|
|
|
|
|
def test_export_detects_tampering_via_verify(repo: Path, tmp_path: Path) -> None:
|
|
(repo / "src" / "app.py").write_text("print('tampered')\n", encoding="utf-8")
|
|
_git(repo, "commit", "-am", "tamper without regenerating manifest")
|
|
|
|
output_dir = tmp_path / "export"
|
|
result = _run_export(repo, output_dir, "--verify")
|
|
assert result.returncode == 1
|
|
export_manifest = json.loads((output_dir / "EXPORT_MANIFEST.json").read_text(encoding="utf-8"))
|
|
assert export_manifest["sourceArchiveVerifiedAgainstManifest"] is False
|
|
assert export_manifest["sourceArchiveVerificationErrors"]
|
|
|
|
|
|
def test_checksums_file_matches_produced_artifacts(repo: Path, tmp_path: Path) -> None:
|
|
output_dir = tmp_path / "export"
|
|
_run_export(repo, output_dir, "--include-evidence")
|
|
|
|
checksums_text = (output_dir / "CHECKSUMS.sha256").read_text(encoding="utf-8")
|
|
lines = [line for line in checksums_text.splitlines() if line.strip()]
|
|
assert any(line.endswith("itworx-mcp-hub-source.tar.gz") for line in lines)
|
|
assert any(line.endswith("sbom.cdx.json") for line in lines)
|
|
|
|
import hashlib
|
|
|
|
for line in lines:
|
|
digest, name = line.split(" ", 1)
|
|
data = (output_dir / name).read_bytes()
|
|
assert hashlib.sha256(data).hexdigest() == digest
|
|
|
|
|
|
def test_history_bundle_reproduces_identical_tree(repo: Path, tmp_path: Path) -> None:
|
|
output_dir = tmp_path / "export"
|
|
_run_export(repo, output_dir, "--include-history")
|
|
|
|
bundle_path = output_dir / "itworx-mcp-hub-history.bundle"
|
|
assert bundle_path.is_file()
|
|
|
|
clone_dir = tmp_path / "clone-from-bundle"
|
|
_git(tmp_path, "clone", "-q", str(bundle_path), str(clone_dir))
|
|
original_tree = create_manifest.resolve_tree("HEAD", repo)
|
|
cloned_tree = create_manifest.resolve_tree("HEAD", clone_dir)
|
|
assert original_tree == cloned_tree
|
|
|
|
|
|
def test_sbom_is_generated_and_records_source_commit(repo: Path, tmp_path: Path) -> None:
|
|
output_dir = tmp_path / "export"
|
|
_run_export(repo, output_dir)
|
|
|
|
sbom = json.loads((output_dir / "sbom.cdx.json").read_text(encoding="utf-8"))
|
|
head_commit = create_manifest.resolve_commit("HEAD", repo)
|
|
assert sbom["metadata"]["sourceCommit"] == head_commit
|
|
|
|
|
|
def test_evidence_archive_contains_only_evidence_subtree(repo: Path, tmp_path: Path) -> None:
|
|
output_dir = tmp_path / "export"
|
|
_run_export(repo, output_dir, "--include-evidence")
|
|
|
|
evidence_archive = output_dir / "itworx-mcp-hub-evidence.tar.gz"
|
|
assert evidence_archive.is_file()
|
|
with tarfile.open(evidence_archive) as archive:
|
|
names = set(archive.getnames())
|
|
assert names == {"evidence", "evidence/NOTE.md"}
|