203 lines
8.3 KiB
Python
203 lines
8.3 KiB
Python
"""
|
|
AUTH.4C3 — test_productive_incident_fixture.py
|
|
==============================================
|
|
ISOLIERTE Fixture, die exakt den realen AUTH.4C2-Incident nachbildet.
|
|
|
|
Real-Incident (AUTH.4C2, produktiv):
|
|
JOB_ID: f18608df-0469-4760-8d84-d6bf1917e772
|
|
STATE: OUTCOME_UNKNOWN
|
|
attempt_count: 1
|
|
Canary: PRESENT
|
|
Path: tolaria/auth4c2-canary.md
|
|
Object ID: object/cfeafffb-415e-4743-af44-e8c92efb9ac5
|
|
Hash: 798ae0315c14aa9d7e3a3f759c952f7c18cbce5154844f447e597b7fd1776515
|
|
Tolaria Markdown Count: 85
|
|
|
|
Erwartung (AUTH.4C3):
|
|
Reconciliation bestätigt Erfolg (TARGET_EXACT)
|
|
SAVE_CALL_COUNT = 0
|
|
attempt_count bleibt 1
|
|
Historie bleibt erhalten (OUTCOME_UNKNOWN sichtbar im Audit)
|
|
KEINE produktive DB. KEIN produktiver Job. KEIN SAVE.
|
|
|
|
Exit 0 = alle PASS.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import os
|
|
import shutil
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
|
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
|
|
from job_schema import make_save_job
|
|
from job_store import JobStore
|
|
from job_state_machine import (
|
|
ST_OUTCOME_UNKNOWN, ST_SUCCEEDED, ST_CLAIMED, ST_EXECUTING,
|
|
)
|
|
from save_reconciliation import (
|
|
SaveReconciliationCore,
|
|
EV_RECONCILIATION_STARTED, EV_RECONCILIATION_TARGET_EXACT,
|
|
EV_CONFIRMED_EXECUTED,
|
|
)
|
|
|
|
# Exakte Werte aus dem realen Incident (ISOLIERTE Fixture, KEINE produktive DB)
|
|
INCIDENT_JOB_ID = "f18608df-0469-4760-8d84-d6bf1917e772"
|
|
INCIDENT_OBJECT_ID = "object/cfeafffb-415e-4743-af44-e8c92efb9ac5"
|
|
INCIDENT_PATH = "tolaria/auth4c2-canary.md"
|
|
INCIDENT_HASH = "798ae0315c14aa9d7e3a3f759c952f7c18cbce5154844f447e597b7fd1776515"
|
|
INCIDENT_ATTEMPTS = 1
|
|
|
|
|
|
class ProductiveIncidentFixtureTests(unittest.TestCase):
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
# Canary-Content: der EXAKTE reale Inhalt aus dem produktiven Vault
|
|
# (read-only verifiziert, Hash stimmt exakt mit dem Incident überein)
|
|
cls.canary_content = (
|
|
"---\n"
|
|
"id: object/cfeafffb-415e-4743-af44-e8c92efb9ac5\n"
|
|
"type: Note\n"
|
|
"title: AUTH.4C2 Canary\n"
|
|
"tags: [auth4c2, canary, c5-save]\n"
|
|
"created: 2026-08-27\n"
|
|
"mission: AUTH.4C2\n"
|
|
"---\n"
|
|
"\n"
|
|
"# AUTH.4C2 First Productive SAVE Canary\n"
|
|
"\n"
|
|
"Dieser Eintrag ist der kontrollierte produktive SAVE-Canary der Mission AUTH.4C2.\n"
|
|
"\n"
|
|
"- **Mission:** AUTH.4C2 — First Productive SAVE Canary\n"
|
|
"- **Zweck:** Beweis exakt eines produktiven SAVEs über den isolierten c5-save-executor-Pfad\n"
|
|
"- **Pfad:** tolaria/auth4c2-canary.md\n"
|
|
"- **Quelle:** Forgejo Source of Truth (commit-bound)\n"
|
|
"- **Fachlich:** neutral — kein Trading-Inhalt, keine Secrets\n"
|
|
"\n"
|
|
"Dieser Canary bleibt kontrolliert vorhanden für spätere AUTH.4E / Cleanup-Mission.\n"
|
|
)
|
|
# Verifiziere: Hash des Canary-Contents == Incident-Hash
|
|
computed = hashlib.sha256(cls.canary_content.encode("utf-8")).hexdigest()
|
|
assert computed == INCIDENT_HASH, (
|
|
f"Fixture-Hash mismatch: computed={computed} expected={INCIDENT_HASH}"
|
|
)
|
|
|
|
def setUp(self):
|
|
# Jeder Test bekommt eine FRISCHE isolierte DB (create_job ist idempotent
|
|
# per job_id — dieselbe INCIDENT_JOB_ID darf nicht über Tests kollidieren)
|
|
self.tmp = tempfile.mkdtemp(prefix="c5-4c3-incident-")
|
|
self.db = os.path.join(self.tmp, "incident.db")
|
|
self.store = JobStore(self.db, "SAVE")
|
|
|
|
def tearDown(self):
|
|
self.store.close()
|
|
shutil.rmtree(self.tmp, ignore_errors=True)
|
|
|
|
def _create_incident_job(self):
|
|
"""Erzeugt den Job exakt wie im realen Incident (OUTCOME_UNKNOWN, attempts=1)."""
|
|
job = make_save_job(
|
|
job_id=INCIDENT_JOB_ID,
|
|
mission_id="00000000-0000-0000-0000-000000000001",
|
|
object_id=INCIDENT_OBJECT_ID,
|
|
vault_path=INCIDENT_PATH,
|
|
source_commit="a" * 40,
|
|
provenance_hash=INCIDENT_HASH,
|
|
expected_state="present",
|
|
created_at="2026-08-27T00:00:00Z",
|
|
idempotency_key="00000000-0000-0000-0000-000000000002",
|
|
)
|
|
self.store.create_job(job)
|
|
self.store.mark_ready(job["job_id"])
|
|
# Simuliere Execution -> OUTCOME_UNKNOWN (attempt_count=1 via Claim)
|
|
self.store._transition(job["job_id"], ST_CLAIMED)
|
|
self.store._transition(job["job_id"], ST_EXECUTING)
|
|
self.store._transition(job["job_id"], ST_OUTCOME_UNKNOWN)
|
|
# attempt_count auf 1 setzen (wie im Incident)
|
|
self.store._conn.execute(
|
|
"UPDATE jobs SET attempt_count = ? WHERE job_id = ?",
|
|
(INCIDENT_ATTEMPTS, job["job_id"]),
|
|
)
|
|
self.store._conn.commit()
|
|
return self.store.get_job(job["job_id"])
|
|
|
|
def test_incident_reconciliation_confirms_success(self):
|
|
"""TARGET_EXACT: Reconciliation bestätigt Erfolg, Job -> SUCCEEDED."""
|
|
job = self._create_incident_job()
|
|
self.assertEqual(job["state"], ST_OUTCOME_UNKNOWN)
|
|
self.assertEqual(job["attempt_count"], INCIDENT_ATTEMPTS)
|
|
|
|
# Read-Back liefert exakt den Canary-Content (Target PRESENT + exact)
|
|
core = SaveReconciliationCore(
|
|
self.store,
|
|
lambda sc, oid, vp: self.canary_content, # source_loader
|
|
lambda vp: self.canary_content, # read_back
|
|
)
|
|
result = core.reconcile(job["job_id"], "worker-1")
|
|
self.assertEqual(result["state"], ST_SUCCEEDED)
|
|
|
|
def test_incident_zero_save_calls(self):
|
|
"""SAVE_CALL_COUNT = 0: Reconciliation ruft niemals write() auf."""
|
|
job = self._create_incident_job()
|
|
save_calls = {"count": 0}
|
|
|
|
def fake_save(*args, **kwargs):
|
|
save_calls["count"] += 1
|
|
raise AssertionError("Reconciliation darf NIE write() aufrufen!")
|
|
|
|
core = SaveReconciliationCore(
|
|
self.store,
|
|
lambda sc, oid, vp: self.canary_content,
|
|
lambda vp: self.canary_content,
|
|
)
|
|
# Reconciliation-Core hat KEIN write-Callable (technisch unmöglich)
|
|
self.assertFalse(hasattr(core, "tolaria_save"))
|
|
self.assertFalse(hasattr(core, "write"))
|
|
result = core.reconcile(job["job_id"], "worker-1")
|
|
self.assertEqual(result["state"], ST_SUCCEEDED)
|
|
self.assertEqual(save_calls["count"], 0)
|
|
|
|
def test_incident_attempt_count_unchanged(self):
|
|
"""attempt_count bleibt 1 (Reconciliation ist KEIN neuer Mutation Attempt)."""
|
|
job = self._create_incident_job()
|
|
core = SaveReconciliationCore(
|
|
self.store,
|
|
lambda sc, oid, vp: self.canary_content,
|
|
lambda vp: self.canary_content,
|
|
)
|
|
result = core.reconcile(job["job_id"], "worker-1")
|
|
self.assertEqual(result["state"], ST_SUCCEEDED)
|
|
self.assertEqual(result["attempt_count"], INCIDENT_ATTEMPTS)
|
|
|
|
def test_incident_history_preserved(self):
|
|
"""Historie bleibt erhalten: OUTCOME_UNKNOWN sichtbar im Audit-Trail."""
|
|
job = self._create_incident_job()
|
|
core = SaveReconciliationCore(
|
|
self.store,
|
|
lambda sc, oid, vp: self.canary_content,
|
|
lambda vp: self.canary_content,
|
|
)
|
|
core.reconcile(job["job_id"], "worker-1")
|
|
trail = self.store.audit_trail(job["job_id"])
|
|
events = [e["event_type"] for e in trail if "event_type" in e]
|
|
# Reconciliation-Evidence vorhanden
|
|
self.assertIn(EV_RECONCILIATION_STARTED, events)
|
|
self.assertIn(EV_RECONCILIATION_TARGET_EXACT, events)
|
|
self.assertIn(EV_CONFIRMED_EXECUTED, events)
|
|
# KEIN EXECUTED (Mutation wurde NICHT neu ausgeführt)
|
|
self.assertNotIn("EXECUTED", events)
|
|
# Der Job-Snapshot im Trail zeigt den finalen State SUCCEEDED,
|
|
# aber die OUTCOME_UNKNOWN-Historie ist in den Events sichtbar
|
|
# (der Job durchlief OUTCOME_UNKNOWN -> RECONCILED -> SUCCEEDED).
|
|
|
|
def test_incident_no_production_db(self):
|
|
"""Fixture nutzt eine ISOLIERTE temporäre DB, keine produktive DB."""
|
|
self.assertNotEqual(self.db, "/opt/data/forgejo/trading-system-docs/tolaria/c5-sync-service/c5a_save.db")
|
|
self.assertTrue(self.db.startswith(tempfile.gettempdir()) or "/tmp" in self.db or "c5-4c3-incident" in self.db)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main(verbosity=2)
|