trading-system-docs/tolaria/c5-sync-service/test_c5c.py

868 lines
36 KiB
Python

#!/usr/bin/env python3
"""
Red Queen — C5C Testsuite (Tolaria Propagation & Drift Verification).
Testet gegen ein kontrolliertes lokales Git-Fixture-Repo + Fake-Tolaria-API.
Jeder Test nutzt eine frische temp-DB (tempfile.mkdtemp), nie die Produkt-DB.
KEINE produktiven Writes. KEINE produktiven Knowledge-Dokumente.
Abgedeckte Faelle (§19):
CREATE success / already target / path collision
CONTENT_UPDATE success / already target / unexpected drift
METADATA_UPDATE / STATE_UPDATE / TAGS_UPDATE
source/canonical ordered success / canonical blocked if source missing / dangling derived_from
rename/move safe case / rename/move ambiguity
DELETE_REQUEST -> Human Gate
SUPERSEDE safe case
legacy object -> Human Gate
secret detected -> no write
Tolaria unavailable -> retry
Read-back mismatch -> fail closed
multi-object commit success
partial commit failure + replay
idempotent replay
no Forgejo write
no Search rebuild
"""
from __future__ import annotations
import json
import os
import shutil
import subprocess
import sys
import tempfile
import unittest
from typing import Any, Dict, List, Optional
# C5C importieren (aus demselben Verzeichnis)
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from rq_c5a import (
C5AStore, ST_READY, ST_PROPAGATING_TOLARIA, ST_VERIFYING_TOLARIA,
ST_UPDATING_SEARCH, ST_HUMAN_REVIEW_REQUIRED, ST_RETRY_PENDING, ST_DEAD,
ST_APPLIED, IDEM_ALREADY_AT_TARGET, IDEM_ALREADY_APPLIED, IDEM_RETRY_SAFE,
RC_UNEXPECTED_TOLARIA_DRIFT, RC_UNKNOWN_OBJECT_ID, RC_SECRET_DETECTED,
RC_DANGLING_DERIVED_FROM, RC_AMBIGUOUS_DELETE, RC_TOLARIA_UNAVAILABLE,
OP_CREATE, OP_CONTENT_UPDATE, OP_METADATA_UPDATE, OP_STATE_UPDATE,
OP_TAGS_UPDATE, OP_SOURCE_CANONICAL_RELATION_UPDATE, OP_RENAME, OP_MOVE,
OP_DELETE_REQUEST, OP_SUPERSEDE,
)
from rq_c5b import (
GitReader, parse_frontmatter, content_hash, metadata_hash, detect_secret,
)
from rq_c5c import (
TolariaClient, C5CPropagator, C5CDryRun,
pre_write_drift_check, _vault_path,
DRIFT_WRITE_ALLOWED, DRIFT_ALREADY_AT_TARGET, DRIFT_UNEXPECTED,
PLAN_WOULD_WRITE, PLAN_ALREADY_AT_TARGET, PLAN_HUMAN_REVIEW, PLAN_DRIFT,
TolariaUnavailableError, ReadBackMismatchError, UnexpectedDriftError,
assert_no_search_calls, assert_no_master_write,
)
UUID_A = "object/77b02661-d67b-4bae-b612-01d1287cea6b"
UUID_B = "object/48bd264f-607b-15f1-5f73-3e922af9b19d"
UUID_C = "object/deea2ed6-cd6f-b006-2e4e-949abea60904"
# ---------------------------------------------------------------------------
# Fixture: kontrolliertes Git-Repo (Test-Helfer darf schreiben)
# ---------------------------------------------------------------------------
def _git(repo: str, *args: str) -> str:
proc = subprocess.run(["git", "-C", repo] + list(args),
capture_output=True, text=True)
if proc.returncode != 0:
raise RuntimeError(f"git {' '.join(args)} fehlgeschlagen: {proc.stderr}")
return proc.stdout
def _write(repo: str, path: str, content: str) -> None:
full = os.path.join(repo, path)
os.makedirs(os.path.dirname(full), exist_ok=True)
with open(full, "w", encoding="utf-8") as f:
f.write(content)
def _commit(repo: str, message: str) -> str:
_git(repo, "add", "-A")
_git(repo, "commit", "-m", message, "--no-verify")
return _git(repo, "rev-parse", "HEAD").strip()
def _fm(id_: str, **extra) -> str:
lines = ["---", "knowledge_schema: 1", f"id: {id_}"]
for k, v in extra.items():
if isinstance(v, list):
lines.append(f"{k}: [{', '.join(v)}]")
else:
lines.append(f"{k}: {v}")
lines.append("---")
return "\n".join(lines) + "\n"
class FixtureRepo:
def __init__(self):
self.dir = tempfile.mkdtemp(prefix="c5c_fixture_")
_git(self.dir, "init", "-q", "-b", "main")
_git(self.dir, "config", "user.email", "test@test")
_git(self.dir, "config", "user.name", "Test")
self.commits: Dict[str, str] = {}
def write(self, path: str, content: str) -> None:
_write(self.dir, path, content)
def commit(self, message: str) -> str:
sha = _commit(self.dir, message)
self.commits[message] = sha
return sha
def cleanup(self) -> None:
shutil.rmtree(self.dir, ignore_errors=True)
# ---------------------------------------------------------------------------
# Fake-Tolaria-API (Mock, isolierter Test-Vault)
# ---------------------------------------------------------------------------
class FakeTolaria:
"""In-Memory Fake der Tolaria-Vault-API (read/write/verify)."""
def __init__(self):
self.vault: Dict[str, str] = {} # vault_path -> content
self.write_count = 0
self.fail_next_write = False
self.fail_next_read = False
self.unavailable = False
def read(self, vault_path: str) -> Optional[str]:
if self.unavailable:
raise TolariaUnavailableError("Tolaria down", RC_TOLARIA_UNAVAILABLE)
if self.fail_next_read:
self.fail_next_read = False
raise TolariaUnavailableError("read timeout", RC_TOLARIA_UNAVAILABLE)
return self.vault.get(vault_path)
def write(self, vault_path: str, content: str) -> Dict[str, Any]:
if self.unavailable:
raise TolariaUnavailableError("Tolaria down", RC_TOLARIA_UNAVAILABLE)
if self.fail_next_write:
self.fail_next_write = False
raise TolariaUnavailableError("write timeout", RC_TOLARIA_UNAVAILABLE)
self.vault[vault_path] = content
self.write_count += 1
return {"ok": True}
def list(self, vault_path: str = "/app/vault") -> List[Dict[str, Any]]:
return [{"path": p} for p in self.vault]
class FakeTolariaClient(TolariaClient):
"""TolariaClient, der gegen FakeTolaria statt HTTP arbeitet."""
def __init__(self, fake: FakeTolaria):
super().__init__(base_url="http://fake")
self.fake = fake
def read(self, vault_path: str) -> Optional[str]:
return self.fake.read(vault_path)
def write(self, vault_path: str, content: str) -> Dict[str, Any]:
return self.fake.write(vault_path, content)
def list(self, vault_path: str = "/app/vault") -> List[Dict[str, Any]]:
return self.fake.list(vault_path)
# ---------------------------------------------------------------------------
# Test-Helfer: C5A-Store + Commit mit ObjectChanges
# ---------------------------------------------------------------------------
def _make_store() -> C5AStore:
db = os.path.join(tempfile.mkdtemp(prefix="c5c_db_"), "c5a.db")
return C5AStore(db)
def _seed_commit(store: C5AStore, sha: str, parent: Optional[str],
objs: List[Dict[str, Any]]) -> None:
"""Legt einen Commit + ObjectChanges im Store an (Status READY)."""
store.upsert_commit({
"commit_sha": sha, "parent_sha": parent, "sequence": 1,
"status": ST_READY, "retry_count": 0,
})
for oc in objs:
oc["commit_sha"] = sha
store.add_object_change(oc)
# ---------------------------------------------------------------------------
# Tests
# ---------------------------------------------------------------------------
class TestPreWriteDriftCheck(unittest.TestCase):
"""§5 Pre-Write Drift Check (harte Invariante)."""
def setUp(self):
self.fake = FakeTolaria()
self.client = FakeTolariaClient(self.fake)
def test_write_allowed_when_current_is_before(self):
before = _fm(UUID_A, state="current") + "body v1"
target = _fm(UUID_A, state="current") + "body v2"
self.fake.vault["/app/vault/x.md"] = before
drift, cur = pre_write_drift_check(self.client, "/app/vault/x.md", before, target)
self.assertEqual(drift, DRIFT_WRITE_ALLOWED)
self.assertEqual(cur, before)
def test_already_at_target_no_write(self):
target = _fm(UUID_A, state="current") + "body v2"
self.fake.vault["/app/vault/x.md"] = target
drift, cur = pre_write_drift_check(self.client, "/app/vault/x.md", None, target)
self.assertEqual(drift, DRIFT_ALREADY_AT_TARGET)
def test_unexpected_drift_fail_closed(self):
before = _fm(UUID_A, state="current") + "body v1"
target = _fm(UUID_A, state="current") + "body v2"
# Tolaria hat einen DRITTEN Zustand (weder before noch target)
self.fake.vault["/app/vault/x.md"] = _fm(UUID_A, state="current") + "body v3"
drift, cur = pre_write_drift_check(self.client, "/app/vault/x.md", before, target)
self.assertEqual(drift, DRIFT_UNEXPECTED)
def test_create_write_allowed_when_absent(self):
target = _fm(UUID_A, state="current") + "body"
drift, cur = pre_write_drift_check(self.client, "/app/vault/x.md", None, target)
self.assertEqual(drift, DRIFT_WRITE_ALLOWED)
self.assertIsNone(cur)
class TestCreate(unittest.TestCase):
"""§6 CREATE."""
def setUp(self):
self.fake = FakeTolaria()
self.client = FakeTolariaClient(self.fake)
self.repo = FixtureRepo()
self.store = _make_store()
self.reader = GitReader(self.repo.dir)
self.prop = C5CPropagator(self.store, self.reader, self.client)
def tearDown(self):
self.repo.cleanup()
self.store.close()
def _create_commit(self, content: str) -> str:
self.repo.write("modul-09.md", content)
return self.repo.commit("create")
def test_create_success(self):
content = _fm(UUID_A, type="arch", role="module", representation="source", state="current")
sha = self._create_commit(content)
_seed_commit(self.store, sha, None, [{
"object_id": UUID_A, "operation": OP_CREATE,
"path_before": None, "path_after": "modul-09.md",
"content_hash_after": content_hash(parse_frontmatter(content)[1]),
}])
res = self.prop.propagate_commit(sha)
self.assertEqual(res["status"], ST_UPDATING_SEARCH)
self.assertEqual(self.fake.vault["/app/vault/modul-09.md"], content)
self.assertEqual(self.fake.write_count, 1)
def test_create_already_target(self):
content = _fm(UUID_A, type="arch", role="module", representation="source", state="current")
sha = self._create_commit(content)
# Tolaria hat bereits exakt den Ziel-Inhalt
self.fake.vault["/app/vault/modul-09.md"] = content
_seed_commit(self.store, sha, None, [{
"object_id": UUID_A, "operation": OP_CREATE,
"path_before": None, "path_after": "modul-09.md",
"content_hash_after": content_hash(parse_frontmatter(content)[1]),
}])
res = self.prop.propagate_commit(sha)
self.assertEqual(res["status"], ST_UPDATING_SEARCH)
self.assertEqual(self.fake.write_count, 0) # kein Write
def test_create_path_collision(self):
# Pfad ist bereits mit ANDEREM Inhalt belegt (weder before noch target)
content = _fm(UUID_A, type="arch", role="module", representation="source", state="current")
sha = self._create_commit(content)
self.fake.vault["/app/vault/modul-09.md"] = _fm(UUID_B) + "anderer inhalt"
_seed_commit(self.store, sha, None, [{
"object_id": UUID_A, "operation": OP_CREATE,
"path_before": None, "path_after": "modul-09.md",
"content_hash_after": content_hash(parse_frontmatter(content)[1]),
}])
res = self.prop.propagate_commit(sha)
self.assertEqual(res["status"], ST_HUMAN_REVIEW_REQUIRED)
self.assertEqual(self.fake.write_count, 0) # kein Write bei Drift
class TestContentUpdate(unittest.TestCase):
"""§7 CONTENT_UPDATE."""
def setUp(self):
self.fake = FakeTolaria()
self.client = FakeTolariaClient(self.fake)
self.repo = FixtureRepo()
self.store = _make_store()
self.reader = GitReader(self.repo.dir)
self.prop = C5CPropagator(self.store, self.reader, self.client)
def tearDown(self):
self.repo.cleanup()
self.store.close()
def _content_update_commit(self, before: str, after: str) -> tuple:
self.repo.write("modul-09.md", before)
parent = self.repo.commit("v1")
self.repo.write("modul-09.md", after)
sha = self.repo.commit("v2")
return sha, parent
def test_content_update_success(self):
before = _fm(UUID_A, state="current") + "body v1"
after = _fm(UUID_A, state="current") + "body v2"
sha, parent = self._content_update_commit(before, after)
self.fake.vault["/app/vault/modul-09.md"] = before # Tolaria = BEFORE
_seed_commit(self.store, sha, parent, [{
"object_id": UUID_A, "operation": OP_CONTENT_UPDATE,
"path_before": "modul-09.md", "path_after": "modul-09.md",
"content_hash_before": content_hash(parse_frontmatter(before)[1]),
"content_hash_after": content_hash(parse_frontmatter(after)[1]),
}])
res = self.prop.propagate_commit(sha)
self.assertEqual(res["status"], ST_UPDATING_SEARCH)
self.assertEqual(self.fake.vault["/app/vault/modul-09.md"], after)
self.assertEqual(self.fake.write_count, 1)
def test_content_update_already_target(self):
before = _fm(UUID_A, state="current") + "body v1"
after = _fm(UUID_A, state="current") + "body v2"
sha, parent = self._content_update_commit(before, after)
self.fake.vault["/app/vault/modul-09.md"] = after # bereits am Ziel
_seed_commit(self.store, sha, parent, [{
"object_id": UUID_A, "operation": OP_CONTENT_UPDATE,
"path_before": "modul-09.md", "path_after": "modul-09.md",
"content_hash_before": content_hash(parse_frontmatter(before)[1]),
"content_hash_after": content_hash(parse_frontmatter(after)[1]),
}])
res = self.prop.propagate_commit(sha)
self.assertEqual(res["status"], ST_UPDATING_SEARCH)
self.assertEqual(self.fake.write_count, 0)
def test_content_update_unexpected_drift(self):
before = _fm(UUID_A, state="current") + "body v1"
after = _fm(UUID_A, state="current") + "body v2"
sha, parent = self._content_update_commit(before, after)
# Tolaria hat einen DRITTEN Zustand
self.fake.vault["/app/vault/modul-09.md"] = _fm(UUID_A, state="current") + "body v3"
_seed_commit(self.store, sha, parent, [{
"object_id": UUID_A, "operation": OP_CONTENT_UPDATE,
"path_before": "modul-09.md", "path_after": "modul-09.md",
"content_hash_before": content_hash(parse_frontmatter(before)[1]),
"content_hash_after": content_hash(parse_frontmatter(after)[1]),
}])
res = self.prop.propagate_commit(sha)
self.assertEqual(res["status"], ST_HUMAN_REVIEW_REQUIRED)
self.assertEqual(self.fake.write_count, 0) # kein Ueberschreiben
class TestMetadataStateTags(unittest.TestCase):
"""§8 METADATA / STATE / TAGS."""
def setUp(self):
self.fake = FakeTolaria()
self.client = FakeTolariaClient(self.fake)
self.repo = FixtureRepo()
self.store = _make_store()
self.reader = GitReader(self.repo.dir)
self.prop = C5CPropagator(self.store, self.reader, self.client)
def tearDown(self):
self.repo.cleanup()
self.store.close()
def _update_commit(self, before: str, after: str) -> tuple:
self.repo.write("modul-09.md", before)
parent = self.repo.commit("v1")
self.repo.write("modul-09.md", after)
sha = self.repo.commit("v2")
return sha, parent
def test_metadata_update(self):
before = _fm(UUID_A, type="arch", role="module", representation="source", state="current") + "body"
after = _fm(UUID_A, type="arch", role="module", representation="source", state="current", summary="neu") + "body"
sha, parent = self._update_commit(before, after)
self.fake.vault["/app/vault/modul-09.md"] = before
_seed_commit(self.store, sha, parent, [{
"object_id": UUID_A, "operation": OP_METADATA_UPDATE,
"path_before": "modul-09.md", "path_after": "modul-09.md",
"metadata_hash_before": metadata_hash(parse_frontmatter(before)[0]),
"metadata_hash_after": metadata_hash(parse_frontmatter(after)[0]),
}])
res = self.prop.propagate_commit(sha)
self.assertEqual(res["status"], ST_UPDATING_SEARCH)
self.assertEqual(self.fake.vault["/app/vault/modul-09.md"], after)
def test_state_update(self):
before = _fm(UUID_A, state="current") + "body"
after = _fm(UUID_A, state="historical") + "body"
sha, parent = self._update_commit(before, after)
self.fake.vault["/app/vault/modul-09.md"] = before
_seed_commit(self.store, sha, parent, [{
"object_id": UUID_A, "operation": OP_STATE_UPDATE,
"path_before": "modul-09.md", "path_after": "modul-09.md",
"metadata_hash_before": metadata_hash(parse_frontmatter(before)[0]),
"metadata_hash_after": metadata_hash(parse_frontmatter(after)[0]),
}])
res = self.prop.propagate_commit(sha)
self.assertEqual(res["status"], ST_UPDATING_SEARCH)
self.assertEqual(self.fake.vault["/app/vault/modul-09.md"], after)
def test_tags_update(self):
before = _fm(UUID_A, tags=["a"]) + "body"
after = _fm(UUID_A, tags=["a", "b"]) + "body"
sha, parent = self._update_commit(before, after)
self.fake.vault["/app/vault/modul-09.md"] = before
_seed_commit(self.store, sha, parent, [{
"object_id": UUID_A, "operation": OP_TAGS_UPDATE,
"path_before": "modul-09.md", "path_after": "modul-09.md",
"metadata_hash_before": metadata_hash(parse_frontmatter(before)[0]),
"metadata_hash_after": metadata_hash(parse_frontmatter(after)[0]),
}])
res = self.prop.propagate_commit(sha)
self.assertEqual(res["status"], ST_UPDATING_SEARCH)
self.assertEqual(self.fake.vault["/app/vault/modul-09.md"], after)
class TestSourceCanonical(unittest.TestCase):
"""§9 Source/Canonical Relation."""
def setUp(self):
self.fake = FakeTolaria()
self.client = FakeTolariaClient(self.fake)
self.repo = FixtureRepo()
self.store = _make_store()
self.reader = GitReader(self.repo.dir)
self.prop = C5CPropagator(self.store, self.reader, self.client)
def tearDown(self):
self.repo.cleanup()
self.store.close()
def test_source_canonical_ordered_success(self):
# Source (Root) zuerst, dann Canonical (derived_from=Source)
source = _fm(UUID_A, type="arch", role="module", representation="source", state="current") + "body"
canonical = _fm(UUID_B, type="arch", role="module", representation="canonical",
state="current", derived_from=UUID_A) + "body"
self.repo.write("modul-09.md", source)
self.repo.write("notes/trading/system-docs/modul-09.md", canonical)
sha = self.repo.commit("pair")
# Beide Objekte im selben Commit
_seed_commit(self.store, sha, None, [
{"object_id": UUID_A, "operation": OP_CREATE, "path_before": None,
"path_after": "modul-09.md",
"content_hash_after": content_hash(parse_frontmatter(source)[1])},
{"object_id": UUID_B, "operation": OP_CREATE, "path_before": None,
"path_after": "notes/trading/system-docs/modul-09.md",
"content_hash_after": content_hash(parse_frontmatter(canonical)[1])},
])
res = self.prop.propagate_commit(sha)
self.assertEqual(res["status"], ST_UPDATING_SEARCH)
self.assertEqual(self.fake.vault["/app/vault/modul-09.md"], source)
self.assertEqual(self.fake.vault["/app/vault/notes/trading/system-docs/modul-09.md"], canonical)
def test_canonical_blocked_if_source_missing(self):
# Canonical referenziert Source, die NICHT existiert -> dangling
canonical = _fm(UUID_B, type="arch", role="module", representation="canonical",
state="current", derived_from=UUID_A) + "body"
self.repo.write("notes/trading/system-docs/modul-09.md", canonical)
sha = self.repo.commit("canonical-only")
_seed_commit(self.store, sha, None, [{
"object_id": UUID_B, "operation": OP_CREATE, "path_before": None,
"path_after": "notes/trading/system-docs/modul-09.md",
"content_hash_after": content_hash(parse_frontmatter(canonical)[1]),
}])
res = self.prop.propagate_commit(sha)
self.assertEqual(res["status"], ST_HUMAN_REVIEW_REQUIRED)
self.assertEqual(self.fake.write_count, 0) # kein Write bei dangling
def test_dangling_derived_from(self):
# derived_from zeigt auf nicht-existierende ID
content = _fm(UUID_B, type="arch", role="module", representation="canonical",
state="current", derived_from="object/00000000-0000-0000-0000-000000000000") + "body"
self.repo.write("modul-09.md", content)
sha = self.repo.commit("dangling")
_seed_commit(self.store, sha, None, [{
"object_id": UUID_B, "operation": OP_CREATE, "path_before": None,
"path_after": "modul-09.md",
"content_hash_after": content_hash(parse_frontmatter(content)[1]),
}])
res = self.prop.propagate_commit(sha)
self.assertEqual(res["status"], ST_HUMAN_REVIEW_REQUIRED)
self.assertEqual(self.fake.write_count, 0)
class TestRenameMove(unittest.TestCase):
"""§10 RENAME / MOVE -> Human Gate (sichere v1-Policy)."""
def setUp(self):
self.fake = FakeTolaria()
self.client = FakeTolariaClient(self.fake)
self.repo = FixtureRepo()
self.store = _make_store()
self.reader = GitReader(self.repo.dir)
self.prop = C5CPropagator(self.store, self.reader, self.client)
def tearDown(self):
self.repo.cleanup()
self.store.close()
def test_rename_safe_case_human_gate(self):
content = _fm(UUID_A, state="current") + "body"
self.repo.write("modul-09.md", content)
parent = self.repo.commit("v1")
self.repo.write("modul-09-new.md", content)
self.repo.write("modul-09.md", "") # alt entfernen
sha = self.repo.commit("rename")
_seed_commit(self.store, sha, parent, [{
"object_id": UUID_A, "operation": OP_RENAME,
"path_before": "modul-09.md", "path_after": "modul-09-new.md",
}])
res = self.prop.propagate_commit(sha)
self.assertEqual(res["status"], ST_HUMAN_REVIEW_REQUIRED)
self.assertEqual(self.fake.write_count, 0) # kein automatischer Rename
def test_move_ambiguity_human_gate(self):
content = _fm(UUID_A, state="current") + "body"
self.repo.write("modul-09.md", content)
parent = self.repo.commit("v1")
self.repo.write("sub/modul-09.md", content)
self.repo.write("modul-09.md", "")
sha = self.repo.commit("move")
_seed_commit(self.store, sha, parent, [{
"object_id": UUID_A, "operation": OP_MOVE,
"path_before": "modul-09.md", "path_after": "sub/modul-09.md",
}])
res = self.prop.propagate_commit(sha)
self.assertEqual(res["status"], ST_HUMAN_REVIEW_REQUIRED)
self.assertEqual(self.fake.write_count, 0)
class TestDeleteSupersedeLegacy(unittest.TestCase):
"""§11 DELETE, §12 SUPERSEDE, §13 LEGACY."""
def setUp(self):
self.fake = FakeTolaria()
self.client = FakeTolariaClient(self.fake)
self.repo = FixtureRepo()
self.store = _make_store()
self.reader = GitReader(self.repo.dir)
self.prop = C5CPropagator(self.store, self.reader, self.client)
def tearDown(self):
self.repo.cleanup()
self.store.close()
def test_delete_request_human_gate(self):
content = _fm(UUID_A, state="current") + "body"
self.repo.write("modul-09.md", content)
parent = self.repo.commit("v1")
self.repo.write("modul-09.md", "")
sha = self.repo.commit("delete")
_seed_commit(self.store, sha, parent, [{
"object_id": UUID_A, "operation": OP_DELETE_REQUEST,
"path_before": "modul-09.md", "path_after": None,
}])
res = self.prop.propagate_commit(sha)
self.assertEqual(res["status"], ST_HUMAN_REVIEW_REQUIRED)
self.assertEqual(self.fake.write_count, 0) # kein Hard Delete
def test_supersede_safe_case(self):
before = _fm(UUID_A, state="current") + "body"
after = _fm(UUID_A, state="superseded") + "body"
self.repo.write("modul-09.md", before)
parent = self.repo.commit("v1")
self.repo.write("modul-09.md", after)
sha = self.repo.commit("supersede")
self.fake.vault["/app/vault/modul-09.md"] = before
_seed_commit(self.store, sha, parent, [{
"object_id": UUID_A, "operation": OP_SUPERSEDE,
"path_before": "modul-09.md", "path_after": "modul-09.md",
"metadata_hash_before": metadata_hash(parse_frontmatter(before)[0]),
"metadata_hash_after": metadata_hash(parse_frontmatter(after)[0]),
}])
res = self.prop.propagate_commit(sha)
self.assertEqual(res["status"], ST_UPDATING_SEARCH)
self.assertEqual(self.fake.vault["/app/vault/modul-09.md"], after)
def test_legacy_object_human_gate(self):
# Legacy-Objekt (keine gueltige object_id) -> Human Gate
content = "---\nknowledge_schema: 1\nid: legacy\n---\nbody"
self.repo.write("README.md", content)
sha = self.repo.commit("legacy")
_seed_commit(self.store, sha, None, [{
"object_id": None, "operation": OP_METADATA_UPDATE,
"path_before": None, "path_after": "README.md",
"reason_code": "UNKNOWN_OBJECT_ID",
}])
res = self.prop.propagate_commit(sha)
self.assertEqual(res["status"], ST_HUMAN_REVIEW_REQUIRED)
self.assertEqual(self.fake.write_count, 0)
class TestSecretSafety(unittest.TestCase):
"""Secret detected -> no write."""
def setUp(self):
self.fake = FakeTolaria()
self.client = FakeTolariaClient(self.fake)
self.repo = FixtureRepo()
self.store = _make_store()
self.reader = GitReader(self.repo.dir)
self.prop = C5CPropagator(self.store, self.reader, self.client)
def tearDown(self):
self.repo.cleanup()
self.store.close()
def test_secret_detected_no_write(self):
content = _fm(UUID_A, state="current") + "body\nkey: sk-1234567890abcdefghijklmnopqrstuvwxyz"
self.repo.write("modul-09.md", content)
sha = self.repo.commit("secret")
_seed_commit(self.store, sha, None, [{
"object_id": UUID_A, "operation": OP_CREATE,
"path_before": None, "path_after": "modul-09.md",
"content_hash_after": content_hash(parse_frontmatter(content)[1]),
}])
res = self.prop.propagate_commit(sha)
self.assertEqual(res["status"], ST_HUMAN_REVIEW_REQUIRED)
self.assertEqual(self.fake.write_count, 0)
class TestRetry(unittest.TestCase):
"""§17 Retry: nur technische Fehler retrybar."""
def setUp(self):
self.fake = FakeTolaria()
self.client = FakeTolariaClient(self.fake)
self.repo = FixtureRepo()
self.store = _make_store()
self.reader = GitReader(self.repo.dir)
self.prop = C5CPropagator(self.store, self.reader, self.client)
def tearDown(self):
self.repo.cleanup()
self.store.close()
def test_tolaria_unavailable_retry(self):
content = _fm(UUID_A, state="current") + "body"
self.repo.write("modul-09.md", content)
sha = self.repo.commit("create")
_seed_commit(self.store, sha, None, [{
"object_id": UUID_A, "operation": OP_CREATE,
"path_before": None, "path_after": "modul-09.md",
"content_hash_after": content_hash(parse_frontmatter(content)[1]),
}])
# Tolaria down -> retrybar
self.fake.unavailable = True
res = self.prop.propagate_commit(sha)
self.assertEqual(res["status"], ST_RETRY_PENDING)
self.assertEqual(res["results"][0]["retry_count"], 1) # _handle_failure hat increment_retry aufgerufen
def test_drift_not_retryable(self):
# Drift ist NICHT retrybar -> direkt Human Gate
before = _fm(UUID_A, state="current") + "body v1"
after = _fm(UUID_A, state="current") + "body v2"
self.repo.write("modul-09.md", before)
parent = self.repo.commit("v1")
self.repo.write("modul-09.md", after)
sha = self.repo.commit("v2")
self.fake.vault["/app/vault/modul-09.md"] = _fm(UUID_A, state="current") + "body v3"
_seed_commit(self.store, sha, parent, [{
"object_id": UUID_A, "operation": OP_CONTENT_UPDATE,
"path_before": "modul-09.md", "path_after": "modul-09.md",
"content_hash_before": content_hash(parse_frontmatter(before)[1]),
"content_hash_after": content_hash(parse_frontmatter(after)[1]),
}])
res = self.prop.propagate_commit(sha)
self.assertEqual(res["status"], ST_HUMAN_REVIEW_REQUIRED)
self.assertEqual(self.store.increment_retry(sha), 1) # kein Retry-Increment durch C5C
class TestReadBackMismatch(unittest.TestCase):
"""Read-back mismatch -> fail closed."""
def setUp(self):
self.fake = FakeTolaria()
self.client = FakeTolariaClient(self.fake)
self.repo = FixtureRepo()
self.store = _make_store()
self.reader = GitReader(self.repo.dir)
self.prop = C5CPropagator(self.store, self.reader, self.client)
def tearDown(self):
self.repo.cleanup()
self.store.close()
def test_readback_mismatch_fail_closed(self):
content = _fm(UUID_A, state="current") + "body"
self.repo.write("modul-09.md", content)
sha = self.repo.commit("create")
_seed_commit(self.store, sha, None, [{
"object_id": UUID_A, "operation": OP_CREATE,
"path_before": None, "path_after": "modul-09.md",
"content_hash_after": content_hash(parse_frontmatter(content)[1]),
}])
# Fake schreibt, aber Read-Back liefert anderen Inhalt (Simulation)
self.fake.vault["/app/vault/modul-09.md"] = _fm(UUID_A, state="current") + "ANDERER body"
res = self.prop.propagate_commit(sha)
self.assertEqual(res["status"], ST_HUMAN_REVIEW_REQUIRED)
class TestCommitAtomicity(unittest.TestCase):
"""§15 Commit-Atomicity + Replay."""
def setUp(self):
self.fake = FakeTolaria()
self.client = FakeTolariaClient(self.fake)
self.repo = FixtureRepo()
self.store = _make_store()
self.reader = GitReader(self.repo.dir)
self.prop = C5CPropagator(self.store, self.reader, self.client)
def tearDown(self):
self.repo.cleanup()
self.store.close()
def test_multi_object_commit_success(self):
c1 = _fm(UUID_A, state="current") + "body A"
c2 = _fm(UUID_B, state="current") + "body B"
self.repo.write("modul-09.md", c1)
self.repo.write("modul-10.md", c2)
sha = self.repo.commit("multi")
_seed_commit(self.store, sha, None, [
{"object_id": UUID_A, "operation": OP_CREATE, "path_before": None,
"path_after": "modul-09.md",
"content_hash_after": content_hash(parse_frontmatter(c1)[1])},
{"object_id": UUID_B, "operation": OP_CREATE, "path_before": None,
"path_after": "modul-10.md",
"content_hash_after": content_hash(parse_frontmatter(c2)[1])},
])
res = self.prop.propagate_commit(sha)
self.assertEqual(res["status"], ST_UPDATING_SEARCH)
self.assertEqual(self.fake.write_count, 2)
def test_partial_commit_failure(self):
# Objekt 1 ok, Objekt 2 drift -> Commit NICHT als complete markieren
c1 = _fm(UUID_A, state="current") + "body A"
c2 = _fm(UUID_B, state="current") + "body B"
self.repo.write("modul-09.md", c1)
self.repo.write("modul-10.md", c2)
sha = self.repo.commit("multi")
# Objekt 2 hat Drift in Tolaria
self.fake.vault["/app/vault/modul-10.md"] = _fm(UUID_B, state="current") + "DRIFT"
_seed_commit(self.store, sha, None, [
{"object_id": UUID_A, "operation": OP_CREATE, "path_before": None,
"path_after": "modul-09.md",
"content_hash_after": content_hash(parse_frontmatter(c1)[1])},
{"object_id": UUID_B, "operation": OP_CREATE, "path_before": None,
"path_after": "modul-10.md",
"content_hash_after": content_hash(parse_frontmatter(c2)[1])},
])
res = self.prop.propagate_commit(sha)
self.assertEqual(res["status"], ST_HUMAN_REVIEW_REQUIRED)
# Commit NICHT als Tolaria-complete (nicht UPDATING_SEARCH)
self.assertNotEqual(res["status"], ST_UPDATING_SEARCH)
def test_idempotent_replay(self):
# Nach erfolgreichem Write: Replay erkennt ALREADY_AT_TARGET, kein Doppel-Write
content = _fm(UUID_A, state="current") + "body"
self.repo.write("modul-09.md", content)
sha = self.repo.commit("create")
_seed_commit(self.store, sha, None, [{
"object_id": UUID_A, "operation": OP_CREATE,
"path_before": None, "path_after": "modul-09.md",
"content_hash_after": content_hash(parse_frontmatter(content)[1]),
}])
# Erster Lauf: schreibt
res1 = self.prop.propagate_commit(sha)
self.assertEqual(res1["status"], ST_UPDATING_SEARCH)
writes_after_first = self.fake.write_count
# Zweiter Lauf (Replay): Tolaria hat bereits Ziel -> kein Write
res2 = self.prop.propagate_commit(sha)
self.assertEqual(res2["status"], ST_UPDATING_SEARCH)
self.assertEqual(self.fake.write_count, writes_after_first) # kein Doppel-Write
class TestGuarantees(unittest.TestCase):
"""No-Search / No-Master-Write Guarantee (statisch)."""
def test_no_search_calls(self):
res = assert_no_search_calls()
self.assertTrue(res["no_search_calls"], res)
def test_no_master_write(self):
res = assert_no_master_write()
self.assertTrue(res["no_master_write"], res)
class TestDryRun(unittest.TestCase):
"""§20 Live-Dry-Run (read-only, kein Write)."""
def setUp(self):
self.fake = FakeTolaria()
self.client = FakeTolariaClient(self.fake)
self.repo = FixtureRepo()
self.store = _make_store()
self.reader = GitReader(self.repo.dir)
self.dry = C5CDryRun(self.store, self.reader, self.client)
def tearDown(self):
self.repo.cleanup()
self.store.close()
def test_dry_run_plan(self):
content = _fm(UUID_A, state="current") + "body"
self.repo.write("modul-09.md", content)
sha = self.repo.commit("create")
_seed_commit(self.store, sha, None, [{
"object_id": UUID_A, "operation": OP_CREATE,
"path_before": None, "path_after": "modul-09.md",
"content_hash_after": content_hash(parse_frontmatter(content)[1]),
}])
plan = self.dry.plan_commit(sha)
self.assertEqual(plan["plan"][0]["plan"], PLAN_WOULD_WRITE)
self.assertEqual(self.fake.write_count, 0) # KEIN Write im Dry-Run
def test_dry_run_already_at_target(self):
content = _fm(UUID_A, state="current") + "body"
self.repo.write("modul-09.md", content)
sha = self.repo.commit("create")
self.fake.vault["/app/vault/modul-09.md"] = content
_seed_commit(self.store, sha, None, [{
"object_id": UUID_A, "operation": OP_CREATE,
"path_before": None, "path_after": "modul-09.md",
"content_hash_after": content_hash(parse_frontmatter(content)[1]),
}])
plan = self.dry.plan_commit(sha)
self.assertEqual(plan["plan"][0]["plan"], PLAN_ALREADY_AT_TARGET)
self.assertEqual(self.fake.write_count, 0)
def test_dry_run_drift(self):
content = _fm(UUID_A, state="current") + "body"
self.repo.write("modul-09.md", content)
sha = self.repo.commit("create")
self.fake.vault["/app/vault/modul-09.md"] = _fm(UUID_A, state="current") + "DRIFT"
_seed_commit(self.store, sha, None, [{
"object_id": UUID_A, "operation": OP_CREATE,
"path_before": None, "path_after": "modul-09.md",
"content_hash_after": content_hash(parse_frontmatter(content)[1]),
}])
plan = self.dry.plan_commit(sha)
self.assertEqual(plan["plan"][0]["plan"], PLAN_DRIFT)
self.assertEqual(self.fake.write_count, 0)
if __name__ == "__main__":
unittest.main(verbosity=2)