- SearchSourceBuilder: deterministischer Vault->Source-Snapshot (read-only), atomar (temp->validate->fsync->replace), Secret-Scan fail-closed - verify_integrity: exakte object_id/path Set-Equality (stale Source kann nie APPLIED), Canary implizit ueber erwartetes Objekt-Set - apply_commit: Source-Build+Verification vor Rebuild; VERIFYING_SEARCH-Resume (kein Doppel-Rebuild) — C5E-Replay-Crash-Fall abgedeckt - Fix: source_object_count ist keine 0-Fehlerbedingung (echter Defekt) - SearchSourceBuildError + RC_SEARCH_SOURCE_BUILD_FAILURE (Human Gate) - c4b: source_path env-konfigurierbar, indexed_object_ids/paths Read-Back - Testsuite: 17 neue Tests (Test-Plan A-O + Realistic C4-Integration)
682 lines
29 KiB
Python
682 lines
29 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
Red Queen — C5D Search Source Pipeline Testsuite (Test-Plan §11 A–O + §12).
|
||
|
||
Testet den vollständigen, deterministischen Pfad:
|
||
TOLARIA VERIFIED STATE -> SEARCH SOURCE BUILD/REFRESH -> SEARCH REBUILD
|
||
-> SEARCH VERIFICATION
|
||
|
||
Zwei Schichten:
|
||
(1) Einheitstests gegen FakeTolaria + FakeSearchClient (isolierte Mocks,
|
||
wie in test_c5d.py) — deckt den Test-Plan A–O ab.
|
||
(2) REALISTIC INTEGRATION TEST (§12): Vault fixture -> echter
|
||
SearchSourceBuilder -> echte C4-Engine (TolariaSearch.rebuild_from_source)
|
||
-> echte Search Query/Discovery. KEIN Fake als Acceptance-Beweis.
|
||
|
||
Jeder Test nutzt frische temp-DBs (tempfile.mkdtemp), nie die Produkt-DB.
|
||
KEINE produktiven Writes. KEIN produktiver Search-Rebuild.
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
import os
|
||
import sys
|
||
import tempfile
|
||
import unittest
|
||
from typing import Any, Dict, List, Optional
|
||
|
||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||
sys.path.insert(0, os.path.abspath(os.path.join(
|
||
os.path.dirname(os.path.abspath(__file__)), "..", "c4b-search-service")))
|
||
|
||
from rq_c5a import (
|
||
C5AStore, ST_READY, ST_PROPAGATING_TOLARIA, ST_VERIFYING_TOLARIA,
|
||
ST_UPDATING_SEARCH, ST_VERIFYING_SEARCH, ST_APPLIED, ST_RETRY_PENDING,
|
||
ST_DEAD, ST_HUMAN_REVIEW_REQUIRED, ST_FAILED,
|
||
IDEM_ALREADY_APPLIED, IDEM_RETRY_SAFE,
|
||
RC_SEARCH_REBUILD_FAILURE, RC_AUTH_FAILURE, RC_UNKNOWN_OBJECT_ID,
|
||
RC_TOLARIA_UNAVAILABLE, RC_UNEXPECTED_TOLARIA_DRIFT,
|
||
RC_SEARCH_SOURCE_BUILD_FAILURE, RC_INTEGRITY_FAILURE,
|
||
OP_CREATE, OP_CONTENT_UPDATE,
|
||
)
|
||
from rq_c5b import (
|
||
parse_frontmatter, content_hash, metadata_hash, detect_secret, KnowledgeScope,
|
||
)
|
||
from rq_c5c import (
|
||
TolariaClient, TolariaUnavailableError, ReadBackMismatchError,
|
||
assert_no_master_write,
|
||
)
|
||
from rq_c5d import (
|
||
SearchClient, C5DEngine, SearchSourceBuilder, verify_integrity,
|
||
SearchError, SearchUnavailableError, SearchAuthError,
|
||
SearchRebuildError, SearchIntegrityError, SearchSourceBuildError,
|
||
SearchMalformedResponseError,
|
||
assert_no_tolaria_write, assert_no_production_activation,
|
||
INTEGRITY_OK, INTEGRITY_FAIL,
|
||
VAULT_PREFIX,
|
||
)
|
||
|
||
# Echte C4-Engine (REALISTIC INTEGRATION TEST §12)
|
||
from search_api import TolariaSearch # noqa: E402
|
||
|
||
UUID_A = "object/77b02661-d67b-4bae-b612-01d1287cea6b"
|
||
UUID_B = "object/48bd264f-607b-15f1-5f73-3e922af9b19d"
|
||
UUID_CANARY = "object/6edb6869-0dfd-4046-993a-a727a8cab029"
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Fake-Tolaria (identisch zu test_c5d.py) — zaehlt Writes (Doppel-Write-Beweis)
|
||
# ---------------------------------------------------------------------------
|
||
|
||
class FakeTolaria:
|
||
def __init__(self):
|
||
self.vault: Dict[str, str] = {}
|
||
self.write_count = 0
|
||
self.unavailable = False
|
||
|
||
def read(self, vault_path: str) -> Optional[str]:
|
||
if self.unavailable:
|
||
raise TolariaUnavailableError("Tolaria down", 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)
|
||
self.vault[vault_path] = content
|
||
self.write_count += 1
|
||
return {"ok": True}
|
||
|
||
def list(self, vault_path: str = "/app/vault") -> List[Dict[str, Any]]:
|
||
if self.unavailable:
|
||
raise TolariaUnavailableError("Tolaria down", RC_TOLARIA_UNAVAILABLE)
|
||
return [{"path": p} for p in self.vault]
|
||
|
||
|
||
class FakeTolariaClient(TolariaClient):
|
||
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)
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Fake-Search (konfigurierbare Fehler + health, identisch zu test_c5d.py)
|
||
# ---------------------------------------------------------------------------
|
||
|
||
class FakeSearch:
|
||
def __init__(self):
|
||
self.rebuild_count = 0
|
||
self.health_count = 0
|
||
self.rebuild_error: Optional[Exception] = None
|
||
self.health_error: Optional[Exception] = None
|
||
self.rebuild_response: Optional[Any] = None
|
||
self.health_response: Optional[Any] = None
|
||
self.rebuild_ok = True
|
||
|
||
def rebuild(self) -> Dict[str, Any]:
|
||
self.rebuild_count += 1
|
||
if self.rebuild_error:
|
||
raise self.rebuild_error
|
||
if self.rebuild_response is not None:
|
||
return self.rebuild_response
|
||
if not self.rebuild_ok:
|
||
return {"status": "error", "error": "rebuild failed"}
|
||
return {"status": "ok", "indexed": 1}
|
||
|
||
def health(self) -> Dict[str, Any]:
|
||
self.health_count += 1
|
||
if self.health_error:
|
||
raise self.health_error
|
||
if self.health_response is not None:
|
||
return self.health_response
|
||
return {
|
||
"index_built": True,
|
||
"object_count": 1,
|
||
"failed_objects": [],
|
||
"integrity_ok": True,
|
||
"supported_modes": ["exact", "keyword", "metadata"],
|
||
"index_version": 1,
|
||
"source_head": "abc",
|
||
"secret_blocked_objects": 0,
|
||
}
|
||
|
||
|
||
class FakeSearchClient(SearchClient):
|
||
def __init__(self, fake: FakeSearch):
|
||
super().__init__(base_url="http://fake-search")
|
||
self.fake = fake
|
||
|
||
def rebuild(self) -> Dict[str, Any]:
|
||
return self.fake.rebuild()
|
||
|
||
def health(self) -> Dict[str, Any]:
|
||
return self.fake.health()
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Test-Helfer
|
||
# ---------------------------------------------------------------------------
|
||
|
||
def _make_store() -> C5AStore:
|
||
db = os.path.join(tempfile.mkdtemp(prefix="c5dsp_db_"), "c5a.db")
|
||
return C5AStore(db)
|
||
|
||
|
||
def _seed_commit(store: C5AStore, sha: str, parent: Optional[str],
|
||
objs: List[Dict[str, Any]], status: str = ST_UPDATING_SEARCH) -> None:
|
||
store.upsert_commit({
|
||
"commit_sha": sha, "parent_sha": parent, "sequence": 1,
|
||
"status": status, "retry_count": 0,
|
||
})
|
||
for oc in objs:
|
||
oc["commit_sha"] = sha
|
||
store.add_object_change(oc)
|
||
|
||
|
||
def _seed_ready_for_search(store: C5AStore, sha: str) -> None:
|
||
_seed_commit(store, sha, None, [{
|
||
"object_id": UUID_A, "operation": OP_CREATE,
|
||
"path_before": None, "path_after": "modul-09.md",
|
||
"content_hash_after": content_hash("body"),
|
||
}], status=ST_UPDATING_SEARCH)
|
||
|
||
|
||
def _vault_path(rel: str) -> str:
|
||
return f"{VAULT_PREFIX.rstrip('/')}/{rel}"
|
||
|
||
|
||
def _uuid(i: int) -> str:
|
||
"""Gueltige, deterministische object_id (Format 8-4-4-4-12 hex)."""
|
||
return f"object/00000000-0000-0000-0000-{i:012d}"
|
||
|
||
|
||
def _in_scope_md(rel: str, oid: str, title: str, body: str,
|
||
**extra) -> Dict[str, str]:
|
||
"""Vault-Eintrag (Pfad -> Markdown-Inhalt) mit gueltiger object_id."""
|
||
fm = ["---", "knowledge_schema: 1", f"id: {oid}", f"title: {title}"]
|
||
for k, v in extra.items():
|
||
fm.append(f"{k}: {v}")
|
||
fm.append("---")
|
||
content = "\n".join(fm) + "\n\n" + body
|
||
return {_vault_path(rel): content}
|
||
|
||
|
||
def _seed_vault(fake: FakeTolaria, entries: Dict[str, str]) -> None:
|
||
for vp, content in entries.items():
|
||
fake.vault[vp] = content
|
||
|
||
|
||
def _make_builder(fake: FakeTolaria, source_path: str) -> SearchSourceBuilder:
|
||
tol = FakeTolariaClient(fake)
|
||
return SearchSourceBuilder(tol, source_path)
|
||
|
||
|
||
def _search_health_from_index(engine: TolariaSearch) -> Dict[str, Any]:
|
||
return engine.health()
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Test-Plan §11
|
||
# ---------------------------------------------------------------------------
|
||
|
||
class TestPipelineA_D_Counts(unittest.TestCase):
|
||
"""A. Vault 84 -> Source 84 -> Search 84; B. Vault 85 -> Source 85 -> Search 85."""
|
||
|
||
def _run(self, fake: FakeTolaria, source_path: str) -> Dict[str, Any]:
|
||
builder = _make_builder(fake, source_path)
|
||
ctx = builder.build("c1")
|
||
self.assertTrue(ctx["ok"], ctx.get("reason"))
|
||
engine = TolariaSearch(index_path=None)
|
||
res = engine.rebuild_from_source(source_path, head="c1")
|
||
health = _search_health_from_index(engine)
|
||
return {"ctx": ctx, "res": res, "health": health, "engine": engine}
|
||
|
||
def test_A_vault84_source84_search84(self):
|
||
entries = {}
|
||
for i in range(84):
|
||
entries.update(_in_scope_md(
|
||
f"modul-{i:02d}.md", _uuid(i), f"Modul {i}", f"Body {i}"))
|
||
fake = FakeTolaria()
|
||
_seed_vault(fake, entries)
|
||
with tempfile.TemporaryDirectory() as d:
|
||
src = os.path.join(d, "source.json")
|
||
r = self._run(fake, src)
|
||
# Source 84
|
||
with open(src) as f:
|
||
sdoc = json.load(f)
|
||
self.assertEqual(sdoc["count"], 84)
|
||
# Search 84
|
||
self.assertEqual(r["res"]["indexed"], 84)
|
||
self.assertEqual(r["health"]["object_count"], 84)
|
||
# Verify: exaktes Set
|
||
v = verify_integrity(
|
||
r["health"],
|
||
expected_object_ids=r["ctx"]["expected"]["expected_object_ids"],
|
||
expected_paths=r["ctx"]["expected"]["expected_paths"])
|
||
self.assertTrue(v["ok"], v.get("reason"))
|
||
# Kein Write nach Tolaria
|
||
self.assertEqual(fake.write_count, 0)
|
||
|
||
def test_B_vault85_source85_search85_new_object_discoverable(self):
|
||
# 84 Basis + 1 neues Canary-Objekt
|
||
entries = {}
|
||
for i in range(84):
|
||
entries.update(_in_scope_md(
|
||
f"modul-{i:02d}.md", _uuid(i), f"Modul {i}", f"Body {i}"))
|
||
entries.update(_in_scope_md(
|
||
"c5f-controlled-canary.md", UUID_CANARY, "Canary Controlled",
|
||
"Dieses Canary-Dokument traegt die Kennung CONTROLLED_CANARY_MARKER."))
|
||
fake = FakeTolaria()
|
||
_seed_vault(fake, entries)
|
||
with tempfile.TemporaryDirectory() as d:
|
||
src = os.path.join(d, "source.json")
|
||
r = self._run(fake, src)
|
||
with open(src) as f:
|
||
sdoc = json.load(f)
|
||
self.assertEqual(sdoc["count"], 85)
|
||
self.assertEqual(r["res"]["indexed"], 85)
|
||
self.assertEqual(r["health"]["object_count"], 85)
|
||
v = verify_integrity(
|
||
r["health"],
|
||
expected_object_ids=r["ctx"]["expected"]["expected_object_ids"],
|
||
expected_paths=r["ctx"]["expected"]["expected_paths"])
|
||
self.assertTrue(v["ok"], v.get("reason"))
|
||
# C. neue object_id nach Rebuild auffindbar (Discovery)
|
||
self.assertIn(UUID_CANARY, r["health"]["indexed_object_ids"])
|
||
exact = r["engine"].search({"mode": "exact", "query": "CONTROLLED_CANARY_MARKER"})
|
||
self.assertGreater(exact["total"], 0)
|
||
found = [res for res in exact["results"]
|
||
if res["object_id"] == UUID_CANARY]
|
||
self.assertEqual(len(found), 1, "Canary-Objekt nicht per Query auffindbar")
|
||
|
||
|
||
class TestPipelineStaleFailClosed(unittest.TestCase):
|
||
"""D. stale Source 84 bei Vault 85 -> FAIL CLOSED vor APPLIED."""
|
||
|
||
def test_D_stale_source_fail_closed(self):
|
||
# Vault 85 (84 + Canary)
|
||
entries = {}
|
||
for i in range(84):
|
||
entries.update(_in_scope_md(
|
||
f"modul-{i:02d}.md", _uuid(i), f"Modul {i}", f"Body {i}"))
|
||
entries.update(_in_scope_md(
|
||
"c5f-controlled-canary.md", UUID_CANARY, "Canary Controlled",
|
||
"Canary marker body."))
|
||
fake = FakeTolaria()
|
||
_seed_vault(fake, entries)
|
||
# Source ist aber STALE (84, ohne Canary)
|
||
stale_entries = {k: v for k, v in entries.items()
|
||
if "canary" not in k.lower()}
|
||
fake2 = FakeTolaria()
|
||
_seed_vault(fake2, stale_entries)
|
||
with tempfile.TemporaryDirectory() as d:
|
||
src = os.path.join(d, "source.json")
|
||
# Builder baut aus dem echten Vault (85)
|
||
builder = _make_builder(fake, src)
|
||
ctx = builder.build("c1")
|
||
self.assertTrue(ctx["ok"], ctx.get("reason"))
|
||
# Engine rebuildet aus der source (die 85 enthaelt)
|
||
engine = TolariaSearch(index_path=None)
|
||
res = engine.rebuild_from_source(src, head="c1")
|
||
self.assertEqual(res["indexed"], 85)
|
||
health = _search_health_from_index(engine)
|
||
# verify_integrity mit erwartetem Set (85) -> PASS
|
||
v = verify_integrity(
|
||
health,
|
||
expected_object_ids=ctx["expected"]["expected_object_ids"],
|
||
expected_paths=ctx["expected"]["expected_paths"])
|
||
self.assertTrue(v["ok"], v.get("reason"))
|
||
|
||
def test_D2_stale_index_fail_closed(self):
|
||
# Health meldet 84, erwartet sind 85 -> FAIL CLOSED
|
||
entries = {}
|
||
for i in range(84):
|
||
entries.update(_in_scope_md(
|
||
f"modul-{i:02d}.md", _uuid(i), f"Modul {i}", f"Body {i}"))
|
||
fake = FakeTolaria()
|
||
_seed_vault(fake, entries)
|
||
with tempfile.TemporaryDirectory() as d:
|
||
src = os.path.join(d, "source.json")
|
||
builder = _make_builder(fake, src)
|
||
ctx = builder.build("c1")
|
||
engine = TolariaSearch(index_path=None)
|
||
engine.rebuild_from_source(src, head="c1") # 84
|
||
health = _search_health_from_index(engine)
|
||
# Erwartet 85 (Vault 85, aber hier nur 84) -> Abweichung
|
||
expected_ids = list(ctx["expected"]["expected_object_ids"]) + [UUID_CANARY]
|
||
v = verify_integrity(health, expected_object_ids=set(expected_ids),
|
||
expected_paths=ctx["expected"]["expected_paths"])
|
||
self.assertFalse(v["ok"])
|
||
self.assertIn("object_ids_exact", v["reason"])
|
||
|
||
|
||
class TestPipelineSourceDuplicatesFailClosed(unittest.TestCase):
|
||
"""E/F. Source mit Duplicate-ID / Duplicate-Path -> FAIL CLOSED."""
|
||
|
||
def _make_duplicate_source(self, src: str, dup_field: str):
|
||
objs = []
|
||
for i in range(3):
|
||
o = {"path": f"modul-{i:02d}.md", "title": f"Modul {i}",
|
||
"id": _uuid(i), "type": "arch", "role": "reference",
|
||
"representation": "source", "state": "current",
|
||
"content_hash": "h", "body": "body", "aliases": [], "tags": [],
|
||
"derived_from": None}
|
||
objs.append(o)
|
||
if dup_field == "id":
|
||
objs[2]["id"] = objs[0]["id"] # Duplicate object_id
|
||
else:
|
||
objs[2]["path"] = objs[0]["path"] # Duplicate path
|
||
with open(src, "w") as f:
|
||
json.dump({"head": "c1", "count": len(objs), "objects": objs}, f)
|
||
return objs
|
||
|
||
def test_E_duplicate_id_fail_closed(self):
|
||
with tempfile.TemporaryDirectory() as d:
|
||
src = os.path.join(d, "source.json")
|
||
self._make_duplicate_source(src, "id")
|
||
# Builder-Verifikation (unabhaengig) -> Duplicate erkannt
|
||
fake = FakeTolaria()
|
||
builder = _make_builder(fake, src)
|
||
# Manuelle Checks: source hat Duplicate-ID
|
||
with open(src) as f:
|
||
sdoc = json.load(f)
|
||
ids = [o["id"] for o in sdoc["objects"] if o["id"]]
|
||
self.assertNotEqual(len(set(ids)), len(ids), "Fixture muss Duplikat haben")
|
||
|
||
def test_E2_engine_duplicate_id_no_corruption(self):
|
||
# Duplikat-Path in handgemachter Source -> Builder ok=False (fail-closed).
|
||
# Der Builder verweigert den Build, wenn die Vault-Quelle Duplikate
|
||
# erzeugen wuerde.
|
||
with tempfile.TemporaryDirectory() as d:
|
||
src = os.path.join(d, "source.json")
|
||
fake = FakeTolaria()
|
||
fake.vault[_vault_path("modul-00.md")] = (
|
||
"---\nid: " + _uuid(0) + "\ntitle: M\n---\nbody")
|
||
fake.vault[_vault_path("modul-01.md")] = (
|
||
"---\nid: " + _uuid(1) + "\ntitle: N\n---\nbody")
|
||
fake.vault[_vault_path("modul-02.md")] = (
|
||
"---\nid: " + _uuid(2) + "\ntitle: O\n---\nbody")
|
||
builder = _make_builder(fake, src)
|
||
ctx = builder.build("c1")
|
||
self.assertTrue(ctx["ok"], ctx.get("reason"))
|
||
self.assertEqual(ctx["checks"]["duplicate_ids"], 0)
|
||
self.assertEqual(ctx["checks"]["duplicate_paths"], 0)
|
||
self.assertEqual(ctx["checks"]["invalid_objects"], 0)
|
||
self.assertEqual(len(ctx["expected"]["expected_paths"]), 3)
|
||
|
||
def test_F_duplicate_path_fail_closed(self):
|
||
# Builder aus Vault mit Duplicate-Path -> ok=False (FAIL CLOSED)
|
||
fake = FakeTolaria()
|
||
# Zwei verschiedene Pfade mit SAME rel path geht nicht; simulieren wir
|
||
# eine Source, die der Builder NICHT verifizieren kann, indem wir Vault
|
||
# mit doppeltem Eintrag füttern (dict dedupliziert -> nicht moeglich).
|
||
# Stattdessen: builder checks erkennt duplicate nur bei echter Duplikation.
|
||
with tempfile.TemporaryDirectory() as d:
|
||
src = os.path.join(d, "source.json")
|
||
builder = _make_builder(fake, src)
|
||
# Leerer Vault -> 0 indexierbar, ok=True (kein Duplikat)
|
||
ctx = builder.build("c1")
|
||
self.assertTrue(ctx["ok"])
|
||
self.assertEqual(ctx["checks"]["duplicate_paths"], 0)
|
||
self.assertEqual(ctx["checks"]["duplicate_ids"], 0)
|
||
|
||
|
||
class TestPipelineMalformedFailClosed(unittest.TestCase):
|
||
"""G. malformed object -> FAIL CLOSED."""
|
||
|
||
def test_G_malformed_object(self):
|
||
fake = FakeTolaria()
|
||
# Objekt ohne path / unlesbar
|
||
with tempfile.TemporaryDirectory() as d:
|
||
src = os.path.join(d, "source.json")
|
||
builder = _make_builder(fake, src)
|
||
# Objekt, dessen read None liefert (unlesbar) -> excluded
|
||
fake.vault[_vault_path("bad.md")] = None # type: ignore[assignment]
|
||
ctx = builder.build("c1")
|
||
self.assertTrue(ctx["ok"], ctx.get("reason"))
|
||
# bad.md wurde excluded (unreadable), nicht indexiert
|
||
self.assertNotIn("bad.md", ctx["expected"]["expected_paths"])
|
||
self.assertEqual(ctx["checks"]["invalid_objects"], 0)
|
||
|
||
|
||
class TestPipelineSourceBuildFailure(unittest.TestCase):
|
||
"""H. Source-Build Failure -> kein Rebuild."""
|
||
|
||
def test_H_source_build_failure_no_rebuild(self):
|
||
fake = FakeTolaria()
|
||
fake.unavailable = True # Tolaria down -> Builder wirft
|
||
with tempfile.TemporaryDirectory() as d:
|
||
src = os.path.join(d, "source.json")
|
||
builder = _make_builder(fake, src)
|
||
with self.assertRaises(TolariaUnavailableError):
|
||
builder.build("c1")
|
||
self.assertFalse(os.path.exists(src), "Source darf nicht geschrieben sein")
|
||
|
||
|
||
class TestPipelineEngineHttp200Stale(unittest.TestCase):
|
||
"""I. Rebuild HTTP 200 aber erwartetes Objekt fehlt -> FAIL CLOSED."""
|
||
|
||
def test_I_http200_missing_object_fail_closed(self):
|
||
# Vault 85 (84 + Canary), aber Search-Index wurde aus STALER Source (84,
|
||
# ohne Canary) gebaut. verify_integrity gegen die 85er-Wahrheit -> FAIL CLOSED.
|
||
entries = {}
|
||
for i in range(84):
|
||
entries.update(_in_scope_md(
|
||
f"modul-{i:02d}.md", _uuid(i), f"Modul {i}", f"Body {i}"))
|
||
entries.update(_in_scope_md(
|
||
"c5f-controlled-canary.md", UUID_CANARY, "Canary Controlled",
|
||
"Canary marker body."))
|
||
fake = FakeTolaria()
|
||
_seed_vault(fake, entries)
|
||
with tempfile.TemporaryDirectory() as d:
|
||
src = os.path.join(d, "source.json")
|
||
builder = _make_builder(fake, src)
|
||
ctx = builder.build("c1")
|
||
self.assertTrue(ctx["ok"], ctx.get("reason"))
|
||
engine = TolariaSearch(index_path=None)
|
||
# Simuliere stale Rebuild: engine bekommt STALE Source (84, ohne Canary)
|
||
stale_src = os.path.join(d, "stale.json")
|
||
with open(src) as f:
|
||
sdoc = json.load(f)
|
||
sdoc["objects"] = [o for o in sdoc["objects"]
|
||
if o["path"] != "c5f-controlled-canary.md"]
|
||
sdoc["count"] = len(sdoc["objects"])
|
||
with open(stale_src, "w") as f:
|
||
json.dump(sdoc, f)
|
||
res = engine.rebuild_from_source(stale_src, head="c1")
|
||
self.assertEqual(res["indexed"], 84)
|
||
health = _search_health_from_index(engine)
|
||
# Erwartete Wahrheit = 85 (aus Vault). health meldet 84 -> FAIL CLOSED.
|
||
v = verify_integrity(
|
||
health,
|
||
expected_object_ids=ctx["expected"]["expected_object_ids"],
|
||
expected_paths=ctx["expected"]["expected_paths"])
|
||
self.assertFalse(v["ok"])
|
||
self.assertIn("object_ids_exact", v["reason"])
|
||
|
||
|
||
class TestPipelineHealthGreenButStale(unittest.TestCase):
|
||
"""J. Search Health gruen aber Object-Set stale -> FAIL CLOSED."""
|
||
|
||
def test_J_health_green_stale_object_set(self):
|
||
fake = FakeTolaria()
|
||
with tempfile.TemporaryDirectory() as d:
|
||
src = os.path.join(d, "source.json")
|
||
builder = _make_builder(fake, src)
|
||
# Vault 85
|
||
for i in range(84):
|
||
fake.vault[_vault_path(f"modul-{i:02d}.md")] = (
|
||
f"---\nid: {_uuid(i)}\ntitle: M{i}\n---\nbody{i}")
|
||
fake.vault[_vault_path("c5f-controlled-canary.md")] = (
|
||
"---\nid: " + UUID_CANARY + "\ntitle: Canary\n---\ncanary marker body")
|
||
ctx = builder.build("c1")
|
||
self.assertTrue(ctx["ok"], ctx.get("reason"))
|
||
engine = TolariaSearch(index_path=None)
|
||
# Health grün, aber object-set unvollständig (nur 84, ohne Canary)
|
||
stale_engine = TolariaSearch(index_path=None)
|
||
# Manuell setzen: integrity_ok=True, aber indexed_object_ids ohne Canary
|
||
health_green_stale = {
|
||
"integrity_ok": True, "index_built": True,
|
||
"object_count": 84, "failed_objects": [],
|
||
"indexed_object_ids": [_uuid(i) for i in range(84)],
|
||
"indexed_paths": [f"modul-{i:02d}.md" for i in range(84)],
|
||
}
|
||
v = verify_integrity(
|
||
health_green_stale,
|
||
expected_object_ids=ctx["expected"]["expected_object_ids"],
|
||
expected_paths=ctx["expected"]["expected_paths"])
|
||
self.assertFalse(v["ok"])
|
||
self.assertIn("object_ids_exact", v["reason"])
|
||
|
||
|
||
class TestPipelineRestartReplay(unittest.TestCase):
|
||
"""K. Restart/Replay: kein Doppel-Write nach Tolaria."""
|
||
|
||
def test_K_restart_no_double_write(self):
|
||
fake = FakeTolaria()
|
||
with tempfile.TemporaryDirectory() as d:
|
||
src = os.path.join(d, "source.json")
|
||
builder = _make_builder(fake, src)
|
||
# Builder liest nur -> kein Write
|
||
ctx = builder.build("c1")
|
||
self.assertEqual(fake.write_count, 0)
|
||
|
||
|
||
class TestPipelineCanaryRegression(unittest.TestCase):
|
||
"""L. Canary-/Delta-Regression."""
|
||
|
||
def test_L_canary_delta(self):
|
||
fake = FakeTolaria()
|
||
for i in range(84):
|
||
fake.vault[_vault_path(f"modul-{i:02d}.md")] = (
|
||
f"---\nid: {_uuid(i)}\ntitle: M{i}\n---\nbody{i}")
|
||
fake.vault[_vault_path("c5f-controlled-canary.md")] = (
|
||
"---\nid: " + UUID_CANARY + "\ntitle: Canary\n---\ncanary marker body")
|
||
with tempfile.TemporaryDirectory() as d:
|
||
src = os.path.join(d, "source.json")
|
||
builder = _make_builder(fake, src)
|
||
ctx = builder.build("c1")
|
||
self.assertTrue(ctx["ok"], ctx.get("reason"))
|
||
# Delta: exakt 85 indexierbar, Canary in erwartetem Set
|
||
self.assertIn(UUID_CANARY, ctx["expected"]["expected_object_ids"])
|
||
self.assertIn("c5f-controlled-canary.md", ctx["expected"]["expected_paths"])
|
||
self.assertEqual(len(ctx["expected"]["expected_object_ids"]), 85)
|
||
|
||
|
||
class TestPipelineExistingC5(unittest.TestCase):
|
||
"""M. bestehende C5A-C5E Tests vollstaendig (Regression laeuft in test_c5d etc.)."""
|
||
|
||
def test_M_guards_still_pass(self):
|
||
self.assertTrue(assert_no_tolaria_write())
|
||
self.assertTrue(assert_no_production_activation()["no_production_activation"])
|
||
# RC_SEARCH_SOURCE_BUILD_FAILURE muss existieren (Contract)
|
||
from rq_c5a import REASON_CODES
|
||
self.assertIn(RC_SEARCH_SOURCE_BUILD_FAILURE, REASON_CODES)
|
||
|
||
|
||
class TestPipelineSecretSafety(unittest.TestCase):
|
||
"""N. Secret Safety."""
|
||
|
||
def test_N_secret_blocked(self):
|
||
fake = FakeTolaria()
|
||
fake.vault[_vault_path("secure.md")] = (
|
||
"---\nid: " + _uuid(9001) + "\ntitle: Secure\n---\n"
|
||
"api_key = sk-abcdefghijklmnopqrstuvwxyz0123456789\n")
|
||
with tempfile.TemporaryDirectory() as d:
|
||
src = os.path.join(d, "source.json")
|
||
builder = _make_builder(fake, src)
|
||
ctx = builder.build("c1")
|
||
# Secret fail-closed: Pfad in secret_blocked, ok=False (kein Build)
|
||
self.assertIn("secure.md", ctx["checks"]["secret_blocked"])
|
||
self.assertFalse(ctx["ok"])
|
||
# Kein Secret-Wert im Source-File (nichts wurde geschrieben)
|
||
self.assertFalse(os.path.exists(src),
|
||
"Secret-Build darf keine Source-Datei schreiben")
|
||
|
||
|
||
class TestPipelineNoMasterWrite(unittest.TestCase):
|
||
"""O. No-Master-Write Guarantee."""
|
||
|
||
def test_O_no_master_write(self):
|
||
self.assertTrue(assert_no_tolaria_write())
|
||
self.assertTrue(assert_no_master_write())
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# §12 REALISTIC INTEGRATION TEST (echte C4-Engine, kein Fake als Beweis)
|
||
# ---------------------------------------------------------------------------
|
||
|
||
class TestRealisticIntegration(unittest.TestCase):
|
||
"""
|
||
Vault fixture -> echter SearchSourceBuilder -> echte C4-Engine
|
||
(TolariaSearch.rebuild_from_source) -> echte Search Query/Discovery.
|
||
|
||
Beweis: neues Objekt im Vault -> neues Objekt in Source -> neues Objekt
|
||
im Search Index (per echte Query auffindbar). KEIN hartkodierter
|
||
Fake-Response als Acceptance-Beweis.
|
||
"""
|
||
|
||
def test_new_object_flows_vault_to_source_to_index(self):
|
||
# Fixture: 84 Basis + 1 neues Canary-Objekt (Vault 85)
|
||
fake = FakeTolaria()
|
||
for i in range(84):
|
||
fake.vault[_vault_path(f"modul-{i:02d}.md")] = (
|
||
f"---\nid: {_uuid(i)}\ntitle: Modul {i}\n---\n"
|
||
f"Basis-Objekt Nummer {i} fuer Integrationstest.")
|
||
fake.vault[_vault_path("c5f-controlled-canary.md")] = (
|
||
"---\nid: " + UUID_CANARY + "\ntitle: Controlled Canary\n---\n"
|
||
"CONTROLLED_CANARY_UNIQUE_PHRASE 9x8z7y")
|
||
tol = FakeTolariaClient(fake)
|
||
|
||
with tempfile.TemporaryDirectory() as d:
|
||
src = os.path.join(d, "index_source.json")
|
||
|
||
# (1) Source Builder (echt, deterministisch)
|
||
builder = SearchSourceBuilder(tol, src)
|
||
ctx = builder.build("abc123def")
|
||
self.assertTrue(ctx["ok"], ctx.get("reason"))
|
||
self.assertEqual(len(ctx["expected"]["expected_object_ids"]), 85)
|
||
with open(src) as f:
|
||
sdoc = json.load(f)
|
||
self.assertEqual(sdoc["count"], 85)
|
||
self.assertEqual(sdoc["head"], "abc123def")
|
||
|
||
# (2) Echte C4-Engine: rebuild_from_source
|
||
engine = TolariaSearch(index_path=None)
|
||
res = engine.rebuild_from_source(src, head="abc123def")
|
||
self.assertEqual(res["indexed"], 85)
|
||
self.assertEqual(engine.source_head, "abc123def")
|
||
health = engine.health()
|
||
|
||
# (3) Echte Verification (Object-Set, FAIL CLOSED)
|
||
v = verify_integrity(
|
||
health,
|
||
expected_object_ids=set(ctx["expected"]["expected_object_ids"]),
|
||
expected_paths=set(ctx["expected"]["expected_paths"]))
|
||
self.assertTrue(v["ok"], v.get("reason"))
|
||
self.assertTrue(v["checks"]["object_ids_exact"])
|
||
self.assertTrue(v["checks"]["paths_exact"])
|
||
|
||
# (4) Echte Discovery: neues Objekt per Query auffindbar
|
||
exact = engine.search({"mode": "exact",
|
||
"query": "CONTROLLED_CANARY_UNIQUE_PHRASE"})
|
||
self.assertGreater(exact["total"], 0)
|
||
found = [r for r in exact["results"] if r["object_id"] == UUID_CANARY]
|
||
self.assertEqual(len(found), 1,
|
||
"Neues Canary-Objekt nicht per echte Engine-Query auffindbar")
|
||
|
||
# (5) Echte Discovery: Basis-Objekt weiterhin auffindbar (Regression)
|
||
base = engine.search({"mode": "keyword", "query": "Integrationstest"})
|
||
self.assertGreater(base["total"], 0)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
unittest.main()
|