AUFGERAUMT: Root auf 10 sichtbare Elemente reduziert

Der Nutzer hat recht: Der Ordner war voller Entwicklungs-Muell.
Jetzt ist sauber getrennt:

ROOT (was der Nutzer sieht und braucht):
- run.py                     = das Programm
- hms_app/                   = der Anwendungscode
- HMS MediaEngine.app        = macOS Doppelklick-Starter
- HMS-Start.vbs              = Windows Doppelklick-Starter
- HMS-Install.vbs             = Windows Erst-Installation
- HMS-Mac-Install.command     = macOS Homebrew-Installation
- HMS-Portable-Install.command = macOS Portable-Installation (16GB-Fix)
- installer_gui.py           = grafischer Installer
- launcher.pyw + launcher_core.py = interne Start-Logik
- LIESMICH.txt               = 10-Zeilen-Kurzanleitung
- .gitignore

_entwicklung/ (alles andere, NICHT benoetigt):
- packages/ apps/ native/ plugins/ tools/ schemas/ tests/ docs/
  build/ fixture_profiles/
- PLAN.md STATUS.md ERRORS.md TEST_REPORT.md CHANGELOG.md README.md
- pyproject.toml uv.lock setup_*.sh/ps1 make_mac_app.py

Diese Trennung gilt ab sofort fuer alle Commits. Der Nutzer kann
_entwicklung/ loeschen wenn er Platz braucht - die App laeuft ohne.

Verifiziert: App startet nach Aufraeumen unveraendert (Health 200).
This commit is contained in:
HMS MediaEngine Agent
2026-09-11 23:44:06 +02:00
parent 696e8eb1b3
commit 362e089be0
338 changed files with 24 additions and 387 deletions
@@ -0,0 +1,119 @@
"""Unit-Tests Content-Manifest (PLAN.md §6.4, §10.1, §29.1).
Manifest-Chunks/-Hashes, Diff für Sync, Verifikation gegen Fälschung
und Rundreise Grundlage für Preflight und resumierbare Übertragung.
"""
from __future__ import annotations
import json
from pathlib import Path
import pytest
from hms_content_sync import CHUNK_SIZE, ContentManifest, hash_file
@pytest.fixture()
def content_tree(tmp_path: Path) -> Path:
root = tmp_path / "content"
(root / "clips").mkdir(parents=True)
(root / "clips" / "intro.mp4").write_bytes(b"A" * 1000)
(root / "clips" / "loop.mp4").write_bytes(b"B" * 500)
(root / "presets").mkdir()
(root / "presets" / "look.json").write_text("{}", encoding="utf-8")
return root
def test_hash_file_chunks_match_content(tmp_path: Path) -> None:
f = tmp_path / "f.bin"
f.write_bytes(b"x" * (CHUNK_SIZE + 100)) # > 1 Chunk
file_hash, chunks = hash_file(f)
assert len(chunks) == 2 # voller + Rest-Chunk
assert len(file_hash) == 64 # SHA-256 Hex
def test_hash_file_empty_file_yields_empty_chunk(tmp_path: Path) -> None:
f = tmp_path / "empty.bin"
f.write_bytes(b"")
file_hash, chunks = hash_file(f)
assert len(chunks) == 1
assert file_hash # auch leere Dateien haben einen definierten Hash
def test_manifest_build_covers_all_files(content_tree: Path) -> None:
manifest = ContentManifest.build(content_tree, revision=7)
assert len(manifest) == 3
assert manifest.revision == 7
assert "clips/intro.mp4" in manifest.entries
assert "presets/look.json" in manifest.entries
entry = manifest.entries["clips/intro.mp4"]
assert entry.size_bytes == 1000
assert len(entry.sha256) == 64
assert entry.kind == "media" # Default-Art
def test_manifest_kind_classification(content_tree: Path) -> None:
manifest = ContentManifest.build(
content_tree,
kind_of=lambda rel: "plugin" if rel.endswith(".json") else "media",
)
assert manifest.entries["presets/look.json"].kind == "plugin"
assert manifest.entries["clips/intro.mp4"].kind == "media"
def test_manifest_roundtrip_preserves_entries(content_tree: Path) -> None:
manifest = ContentManifest.build(content_tree, revision=3)
restored = ContentManifest.from_dict(json.loads(json.dumps(manifest.to_dict())))
assert restored.revision == 3
assert set(restored.entries) == set(manifest.entries)
orig = manifest.entries["clips/intro.mp4"]
copy = restored.entries["clips/intro.mp4"]
assert copy.sha256 == orig.sha256
assert copy.chunk_hashes == orig.chunk_hashes
def test_manifest_is_deterministic_across_builds(content_tree: Path) -> None:
"""Gleicher Inhalt → gleiche Einträge (nur manifest_id ist neu)."""
a = ContentManifest.build(content_tree)
b = ContentManifest.build(content_tree)
assert a.to_dict()["entries"] == b.to_dict()["entries"]
assert a.manifest_id != b.manifest_id # Instanz-ID ist zeitlich unique
def test_verify_detects_tampered_file(content_tree: Path) -> None:
manifest = ContentManifest.build(content_tree)
assert manifest.verify_file(content_tree, "clips/intro.mp4")
(content_tree / "clips" / "intro.mp4").write_bytes(b"TAMPERED" * 50)
assert not manifest.verify_file(content_tree, "clips/intro.mp4") # §6.4 Hashprüfung
def test_verify_detects_missing_and_unknown(content_tree: Path) -> None:
manifest = ContentManifest.build(content_tree)
(content_tree / "clips" / "loop.mp4").unlink()
assert not manifest.verify_file(content_tree, "clips/loop.mp4")
assert not manifest.verify_file(content_tree, "gibts/nicht.mp4")
def test_diff_drives_sync_plan(content_tree: Path) -> None:
"""Diff liefert exakt die Übertragungsliste (§6.4: added/removed/changed)."""
old = ContentManifest.build(content_tree, revision=1)
# Ändern + Hinzufügen + Entfernen
(content_tree / "clips" / "intro.mp4").write_bytes(b"GEAENDERT")
(content_tree / "clips" / "neu.mp4").write_bytes(b"N")
(content_tree / "clips" / "loop.mp4").unlink()
new = ContentManifest.build(content_tree, revision=2)
diff = old.diff(new)
assert diff.added == frozenset({"clips/neu.mp4"})
assert diff.removed == frozenset({"clips/loop.mp4"})
assert diff.changed == frozenset({"clips/intro.mp4"})
assert not diff.empty
# identische Manifeste: leerer Diff
same = old.diff(ContentManifest.from_dict(old.to_dict()))
assert same.empty
def test_build_rejects_missing_root(tmp_path: Path) -> None:
with pytest.raises(FileNotFoundError):
ContentManifest.build(tmp_path / "gibts-nicht")