Files
ITWorx_MCP_Hub-Public/tests/test_wp220_manifest.py
T

446 lines
19 KiB
Python

"""WP-220A regression tests: Git-tracked-content-only release manifest.
These tests build small, throwaway Git repositories under ``tmp_path`` and
exercise scripts/create_manifest.py and scripts/validate_pack.py against
them directly (via their ``repo_root``-parameterized functions) — never
against this actual repository, so nothing here can corrupt
PACK_MANIFEST.json or require a clean working tree to run.
"""
from __future__ import annotations
import hashlib
import subprocess
from pathlib import Path
import pytest
from scripts import create_manifest, validate_pack
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 _init_repo(repo: Path) -> None:
repo.mkdir(parents=True, exist_ok=True)
# Pin the initial branch name explicitly: the local/global
# init.defaultBranch config is not guaranteed to be "main" (this
# workstation defaults to "master"), and the merge-commit regression
# tests below assume a branch literally named "main".
_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")
def _commit_all(repo: Path, message: str) -> str:
_git(repo, "add", "-A")
_git(repo, "commit", "-q", "-m", message)
return _git(repo, "rev-parse", "HEAD").strip()
@pytest.fixture
def repo(tmp_path: Path) -> Path:
path = tmp_path / "repo"
_init_repo(path)
(path / "README.md").write_text("hello\n", encoding="utf-8")
(path / "src").mkdir()
(path / "src" / "app.py").write_text("print('hi')\n", encoding="utf-8")
_commit_all(path, "initial commit")
return path
def test_clean_git_content_passes(repo: Path) -> None:
manifest = create_manifest.build_manifest("HEAD", repo)
(repo / "PACK_MANIFEST.json").write_text(
__import__("json").dumps(manifest, indent=2) + "\n", encoding="utf-8"
)
errors: list[str] = []
warnings: list[str] = []
validate_pack.validate_manifest(errors, warnings, repo_root=repo)
assert errors == []
def test_ignored_and_untracked_files_cannot_enter_manifest(tmp_path: Path) -> None:
path = tmp_path / "repo"
_init_repo(path)
(path / ".gitignore").write_text("*.env\nsecret.local\n", encoding="utf-8")
(path / "app.py").write_text("print(1)\n", encoding="utf-8")
_commit_all(path, "initial commit")
# Present on disk, but ignored/untracked — must never appear.
(path / "secret.env").write_text("TOKEN=abc123\n", encoding="utf-8")
(path / "secret.local").write_text("also secret\n", encoding="utf-8")
(path / "scratch.tmp").write_text("untracked, not even ignored\n", encoding="utf-8")
manifest = create_manifest.build_manifest("HEAD", path)
paths = {entry["path"] for entry in manifest["files"]}
assert paths == {".gitignore", "app.py"}
assert "secret.env" not in paths
assert "secret.local" not in paths
assert "scratch.tmp" not in paths
def test_manifest_excludes_itself_deterministically(repo: Path) -> None:
manifest = create_manifest.build_manifest("HEAD", repo)
paths = {entry["path"] for entry in manifest["files"]}
assert "PACK_MANIFEST.json" not in paths
assert manifest["selfExcluded"] == "PACK_MANIFEST.json"
def test_crlf_vs_lf_checkout_does_not_alter_canonical_checksums(tmp_path: Path) -> None:
"""Same commit, checked out under different core.autocrlf, must hash identically.
This requires an explicit `.gitattributes` `eol` override — without one,
`git archive` output (which build_manifest hashes) inherits whichever
machine's local core.autocrlf ran the archive command, which is exactly
the ambiguity `.gitattributes` exists to remove. This mirrors the real
repository, which pins `eol=lf` for `*.py` explicitly (see
test_ignored_and_untracked_files_cannot_enter_manifest's repo fixture,
and the real .gitattributes, for the same pattern).
The fixture's `.gitattributes` must also govern *itself*, not just
`*.py` — without a rule covering `.gitattributes`, Git's default
`text=auto` heuristic classifies it as text but leaves its own eol
conversion to `core.autocrlf`, so `git archive` output for the
`.gitattributes` blob itself differs between the two checkouts (17
bytes LF vs 18 bytes CRLF) even though `script.py` matches correctly.
Reproduced on Linux (fails there; passed on Windows only because the
fixture's own bytes happened to be written LF-only both times). The
real repository's `.gitattributes` already has this covered via its
top-level `* text=auto eol=lf` catch-all (see `.gitattributes:17`) plus
an explicit `*.gitattributes text eol=lf` rule (`.gitattributes:36`) —
this was a test-fixture gap, not a real defect in the manifest
algorithm or the real repository's attributes.
"""
origin = tmp_path / "origin"
_init_repo(origin)
(origin / ".gitattributes").write_text(
"* text=auto eol=lf\n.gitattributes text eol=lf\n*.py text eol=lf\n", encoding="utf-8"
)
(origin / "script.py").write_bytes(b"import sys\nprint(sys.argv)\n")
commit = _commit_all(origin, "add script")
lf_checkout = tmp_path / "lf-checkout"
_git(tmp_path, "clone", "-q", str(origin), str(lf_checkout))
_git(lf_checkout, "config", "core.autocrlf", "false")
_git(lf_checkout, "checkout", "-q", "-f", "HEAD")
crlf_checkout = tmp_path / "crlf-checkout"
_git(tmp_path, "clone", "-q", str(origin), str(crlf_checkout))
_git(crlf_checkout, "config", "core.autocrlf", "true")
_git(crlf_checkout, "checkout", "-q", "-f", "HEAD")
# An explicit `eol=lf` attribute overrides core.autocrlf entirely, for
# both the working-tree checkout and `git archive` output — so, unlike
# test_without_gitattributes_archive_output_is_not_platform_independent,
# both checkouts below produce identical LF bytes despite the differing
# local core.autocrlf config. That is the property .gitattributes exists
# to guarantee.
lf_bytes = (lf_checkout / "script.py").read_bytes()
crlf_bytes = (crlf_checkout / "script.py").read_bytes()
assert b"\r\n" not in lf_bytes
assert b"\r\n" not in crlf_bytes
assert lf_bytes == crlf_bytes
lf_manifest = create_manifest.build_manifest(commit, lf_checkout)
crlf_manifest = create_manifest.build_manifest(commit, crlf_checkout)
def checksum(manifest: dict, path: str) -> str:
return next(entry["sha256"] for entry in manifest["files"] if entry["path"] == path)
assert checksum(lf_manifest, "script.py") == checksum(crlf_manifest, "script.py")
assert lf_manifest["files"] == crlf_manifest["files"]
assert lf_manifest["totalBytes"] == crlf_manifest["totalBytes"]
def test_without_gitattributes_archive_output_is_not_platform_independent(tmp_path: Path) -> None:
"""Documents *why* .gitattributes is mandatory, not cosmetic.
Without an explicit `eol` attribute, `git archive` output for a
plain-text file inherits whichever machine's local core.autocrlf ran the
archive command — so two clones with different core.autocrlf legitimately
produce different manifests for the identical commit. This is exactly
the ambiguity the real repository's `.gitattributes` (`* text=auto
eol=lf` plus explicit per-extension rules) removes.
"""
origin = tmp_path / "origin"
_init_repo(origin)
(origin / "script.py").write_bytes(b"import sys\nprint(sys.argv)\n")
commit = _commit_all(origin, "add script, no .gitattributes")
lf_checkout = tmp_path / "lf-checkout"
_git(tmp_path, "clone", "-q", str(origin), str(lf_checkout))
_git(lf_checkout, "config", "core.autocrlf", "false")
crlf_checkout = tmp_path / "crlf-checkout"
_git(tmp_path, "clone", "-q", str(origin), str(crlf_checkout))
_git(crlf_checkout, "config", "core.autocrlf", "true")
lf_manifest = create_manifest.build_manifest(commit, lf_checkout)
crlf_manifest = create_manifest.build_manifest(commit, crlf_checkout)
def checksum(manifest: dict, path: str) -> str:
return next(entry["sha256"] for entry in manifest["files"] if entry["path"] == path)
assert checksum(lf_manifest, "script.py") != checksum(crlf_manifest, "script.py")
def test_explicit_eol_crlf_override_is_honoured_and_verifiable(tmp_path: Path) -> None:
"""Reproduces the scripts/bootstrap.ps1 case: a file intentionally always
CRLF via an explicit `.gitattributes` override, verified against a real
`git archive` extraction with no Git present in the extraction directory.
"""
origin = tmp_path / "origin"
_init_repo(origin)
(origin / ".gitattributes").write_text("*.ps1 text eol=crlf\n", encoding="utf-8")
(origin / "bootstrap.ps1").write_bytes(b"Write-Host 'hello'\r\n")
_commit_all(origin, "add windows script")
manifest = create_manifest.build_manifest("HEAD", origin)
entry = next(e for e in manifest["files"] if e["path"] == "bootstrap.ps1")
(origin / "PACK_MANIFEST.json").write_text(
__import__("json").dumps(manifest, indent=2) + "\n", encoding="utf-8"
)
archive_dir = tmp_path / "archive-extraction"
archive_dir.mkdir()
tar_path = tmp_path / "export.tar"
with tar_path.open("wb") as handle:
subprocess.run( # noqa: S603
["git", "archive", "--format=tar", "HEAD"], # noqa: S607
cwd=origin,
stdout=handle,
check=True,
)
import tarfile
with tarfile.open(tar_path) as archive:
archive.extractall(archive_dir, filter="data") # noqa: S202
extracted_bytes = (archive_dir / "bootstrap.ps1").read_bytes()
assert b"\r\n" in extracted_bytes
assert hashlib.sha256(extracted_bytes).hexdigest() == entry["sha256"]
tree_errors = validate_pack.validate_manifest_against_directory(
archive_dir, manifest_root=origin
)
assert tree_errors == []
def test_missing_tracked_file_fails_validation(repo: Path) -> None:
manifest = create_manifest.build_manifest("HEAD", repo)
(repo / "PACK_MANIFEST.json").write_text(
__import__("json").dumps(manifest, indent=2) + "\n", encoding="utf-8"
)
(repo / "src" / "app.py").unlink()
errors: list[str] = []
validate_pack.validate_manifest(errors, [], repo_root=repo)
assert errors
assert any("uncommitted changes" in e and "src/app.py" in e for e in errors)
def test_modified_tracked_file_fails_validation(repo: Path) -> None:
manifest = create_manifest.build_manifest("HEAD", repo)
(repo / "PACK_MANIFEST.json").write_text(
__import__("json").dumps(manifest, indent=2) + "\n", encoding="utf-8"
)
(repo / "src" / "app.py").write_text("print('tampered')\n", encoding="utf-8")
errors: list[str] = []
validate_pack.validate_manifest(errors, [], repo_root=repo)
assert errors
assert any("uncommitted changes" in e and "src/app.py" in e for e in errors)
def test_manifest_cannot_silently_validate_stale_data(repo: Path) -> None:
manifest = create_manifest.build_manifest("HEAD", repo)
(repo / "PACK_MANIFEST.json").write_text(
__import__("json").dumps(manifest, indent=2) + "\n", encoding="utf-8"
)
stale_checksum = next(e["sha256"] for e in manifest["files"] if e["path"] == "src/app.py")
# A brand new commit lands (e.g. a merge) without regenerating the manifest.
(repo / "src" / "app.py").write_text("print('new behaviour')\n", encoding="utf-8")
_commit_all(repo, "unrelated change, manifest not regenerated")
errors: list[str] = []
validate_pack.validate_manifest(errors, [], repo_root=repo)
# The manifest still describes the pre-change checksum, and that
# checksum no longer matches HEAD's actual content — it is never
# silently accepted as still valid.
assert any("checksum mismatch" in e and "src/app.py" in e for e in errors)
current_checksum = create_manifest.build_manifest("HEAD", repo)
new_checksum = next(e["sha256"] for e in current_checksum["files"] if e["path"] == "src/app.py")
assert new_checksum != stale_checksum
def test_tampered_checksum_is_caught_even_on_a_clean_tree(repo: Path) -> None:
manifest = create_manifest.build_manifest("HEAD", repo)
for entry in manifest["files"]:
if entry["path"] == "src/app.py":
entry["sha256"] = "0" * 64
(repo / "PACK_MANIFEST.json").write_text(
__import__("json").dumps(manifest, indent=2) + "\n", encoding="utf-8"
)
errors: list[str] = []
validate_pack.validate_manifest(errors, [], repo_root=repo)
assert any("checksum mismatch" in e and "src/app.py" in e for e in errors)
def test_manifest_omitting_a_tracked_file_fails(repo: Path) -> None:
manifest = create_manifest.build_manifest("HEAD", repo)
manifest["files"] = [e for e in manifest["files"] if e["path"] != "src/app.py"]
manifest["fileCount"] = len(manifest["files"])
(repo / "PACK_MANIFEST.json").write_text(
__import__("json").dumps(manifest, indent=2) + "\n", encoding="utf-8"
)
errors: list[str] = []
validate_pack.validate_manifest(errors, [], repo_root=repo)
assert any("omits Git-tracked files" in e and "src/app.py" in e for e in errors)
def test_directory_verification_matches_git_archive_extraction(repo: Path, tmp_path: Path) -> None:
"""A `git archive` extraction, verified with no Git present in that directory."""
manifest = create_manifest.build_manifest("HEAD", repo)
(repo / "PACK_MANIFEST.json").write_text(
__import__("json").dumps(manifest, indent=2) + "\n", encoding="utf-8"
)
archive_dir = tmp_path / "archive-extraction"
archive_dir.mkdir()
tar_path = tmp_path / "export.tar"
with tar_path.open("wb") as handle:
subprocess.run( # noqa: S603
["git", "archive", "--format=tar", "HEAD"], # noqa: S607
cwd=repo,
stdout=handle,
check=True,
)
import tarfile
with tarfile.open(tar_path) as archive:
archive.extractall(archive_dir, filter="data") # noqa: S202
tree_errors = validate_pack.validate_manifest_against_directory(archive_dir, manifest_root=repo)
assert tree_errors == []
def test_directory_verification_tolerates_manifest_file_committed_and_present(
repo: Path, tmp_path: Path
) -> None:
"""PACK_MANIFEST.json is itself typically tracked, so `git archive` includes
it even though the manifest's own file list always excludes it (see
selfExcluded). Its presence in the verified directory must not be
reported as "absent from the manifest".
"""
manifest = create_manifest.build_manifest("HEAD", repo)
(repo / "PACK_MANIFEST.json").write_text(
__import__("json").dumps(manifest, indent=2) + "\n", encoding="utf-8"
)
_git(repo, "add", "PACK_MANIFEST.json")
_git(repo, "commit", "-q", "-m", "record manifest")
archive_dir = tmp_path / "archive-with-manifest"
archive_dir.mkdir()
tar_path = tmp_path / "export-with-manifest.tar"
with tar_path.open("wb") as handle:
subprocess.run( # noqa: S603
["git", "archive", "--format=tar", "HEAD"], # noqa: S607
cwd=repo,
stdout=handle,
check=True,
)
import tarfile
with tarfile.open(tar_path) as archive:
archive.extractall(archive_dir, filter="data") # noqa: S202
assert (archive_dir / "PACK_MANIFEST.json").is_file()
tree_errors = validate_pack.validate_manifest_against_directory(archive_dir, manifest_root=repo)
assert tree_errors == []
def test_directory_verification_detects_tampering_outside_git(repo: Path, tmp_path: Path) -> None:
manifest = create_manifest.build_manifest("HEAD", repo)
(repo / "PACK_MANIFEST.json").write_text(
__import__("json").dumps(manifest, indent=2) + "\n", encoding="utf-8"
)
export_dir = tmp_path / "export"
export_dir.mkdir()
(export_dir / "README.md").write_text("hello\n", encoding="utf-8")
(export_dir / "src").mkdir()
(export_dir / "src" / "app.py").write_text("print('tampered outside repo')\n", encoding="utf-8")
tree_errors = validate_pack.validate_manifest_against_directory(export_dir, manifest_root=repo)
assert any("checksum mismatch" in e for e in tree_errors)
def test_content_changing_merge_is_correctly_stale(repo: Path) -> None:
"""A merge that DOES change tracked content must still be flagged stale."""
manifest = create_manifest.build_manifest("HEAD", repo)
(repo / "PACK_MANIFEST.json").write_text(
__import__("json").dumps(manifest, indent=2) + "\n", encoding="utf-8"
)
_git(repo, "add", "PACK_MANIFEST.json")
_git(repo, "commit", "-q", "-m", "record manifest")
_git(repo, "checkout", "-q", "-b", "feature")
(repo / "docs.md").write_text("unrelated new file\n", encoding="utf-8")
_commit_all(repo, "feature work")
_git(repo, "checkout", "-q", "main")
_git(repo, "merge", "-q", "--no-ff", "-m", "merge feature", "feature")
errors: list[str] = []
validate_pack.validate_manifest(errors, [], repo_root=repo)
assert errors # docs.md is new tracked content -> correctly stale
def test_no_op_merge_commit_does_not_stale_the_manifest(repo: Path) -> None:
"""Reproduces the exact WP-200 incident (commit 95329ca) as a regression test.
A PR merge that produces a brand new commit SHA but changes no tracked
file content (e.g. an already-up-to-date branch merged with --no-ff)
must NOT force a manifest-regeneration commit — only a merge that
actually changes tracked content should be treated as stale.
"""
manifest = create_manifest.build_manifest("HEAD", repo)
(repo / "PACK_MANIFEST.json").write_text(
__import__("json").dumps(manifest, indent=2) + "\n", encoding="utf-8"
)
_git(repo, "add", "PACK_MANIFEST.json")
_git(repo, "commit", "-q", "-m", "record manifest")
_git(repo, "checkout", "-q", "-b", "empty-branch")
# An empty commit: a new SHA, but identical tree to its parent.
_git(repo, "commit", "-q", "--allow-empty", "-m", "empty commit, no content change")
_git(repo, "checkout", "-q", "main")
_git(repo, "merge", "-q", "--no-ff", "-m", "no-op merge", "empty-branch")
errors: list[str] = []
validate_pack.validate_manifest(errors, [], repo_root=repo)
assert errors == []
def test_regeneration_is_deterministic_across_repeated_runs(repo: Path) -> None:
first = create_manifest.build_manifest("HEAD", repo)
second = create_manifest.build_manifest("HEAD", repo)
assert first["files"] == second["files"]
assert first["fileCount"] == second["fileCount"]
assert first["totalBytes"] == second["totalBytes"]
assert first["source"] == second["source"]
# generatedAt is a wall-clock timestamp and is deliberately excluded
# from the determinism guarantee — everything else must match exactly.