"""Schema-level tests: models persist, enums hold, audit helper writes.""" from __future__ import annotations from datetime import UTC, datetime import sqlalchemy as sa from sqlalchemy import select from cayceops.models import ( AuditEntry, ContentItem, DaemonRun, MailMessage, Proposal, ProposalSource, ProposalStatus, audit, ) def test_proposal_defaults(sess): p = Proposal(kind="affiliate", title="Test merchant program") sess.add(p) sess.flush() assert p.id is not None assert p.status == ProposalStatus.PROPOSED assert p.source == ProposalSource.SCANNER assert p.attempts == 0 assert p.created_at.tzinfo is not None def test_proposal_status_transitions(sess): p = Proposal(kind="content_gap", title="Guide gap: Rio") sess.add(p) sess.flush() p.status = ProposalStatus.APPROVED p.approved_by = "operator" p.approved_at = datetime.now(UTC) p.status = ProposalStatus.RUNNING p.status = ProposalStatus.DONE sess.flush() loaded = sess.scalar(select(Proposal).where(Proposal.id == p.id)) assert loaded.status == ProposalStatus.DONE assert loaded.approved_by == "operator" def test_dedupe_key_unique(sess): sess.add(Proposal(kind="affiliate", title="A", dedupe_key="k-1")) sess.flush() sess.add(Proposal(kind="affiliate", title="B", dedupe_key="k-1")) try: sess.flush() raised = False except sa.exc.IntegrityError: raised = True sess.rollback() assert raised, "duplicate dedupe_key must be rejected" def test_audit_helper_writes(sess): p = Proposal(kind="other", title="x") sess.add(p) sess.flush() audit(sess, actor="operator", action="approve", proposal_id=p.id, detail={"note": "ok"}) sess.flush() rows = list(sess.scalars(select(AuditEntry)).all()) assert len(rows) == 1 assert rows[0].actor == "operator" assert rows[0].proposal_id == p.id def test_mail_and_content_and_runs(sess): m = MailMessage( direction="in", subject="Welcome to the partner program", classification="approval", confidence=0.9, draft_status="drafted", ) c = ContentItem(theme="lightning-abroad", channel="nostr", caption="gm") r = DaemonRun(daemon="scanner", status="ok", detail={"found": 3}) sess.add_all([m, c, r]) sess.flush() assert m.id and c.id and r.id assert m.draft_status == "drafted" assert c.status == "draft" def test_audit_chain_and_verification(sess): from cayceops.models import verify_audit_chain p = Proposal(kind="other", title="chain") sess.add(p) sess.flush() for i in range(3): audit(sess, actor=f"actor-{i}", action="step", proposal_id=p.id, detail={"i": i}) sess.flush() ok, broken = verify_audit_chain(sess) assert ok and broken is None # tamper with one entry -> chain breaks at that id first = sess.query(AuditEntry).order_by(AuditEntry.id).first() first.actor = "tampered" ok, broken = verify_audit_chain(sess) assert not ok and broken == first.id def test_concurrent_writers_keep_chain_verifiable(db_engine): """Round-2 point 4: serialized writes -> no forked chains.""" import threading import cayceops.db as appdb errors: list[Exception] = [] def writer(worker: int, n: int): try: for i in range(n): with appdb.session_scope() as s: audit(s, actor=f"w{worker}", action=f"step{i}", detail={"worker": worker}) except Exception as exc: # noqa: BLE001 errors.append(exc) threads = [threading.Thread(target=writer, args=(w, 25)) for w in range(4)] for t in threads: t.start() for t in threads: t.join() assert not errors, errors s = appdb.session_factory()() total = s.query(AuditEntry).count() assert total == 100 from cayceops.models import verify_audit_chain ok, broken = verify_audit_chain(s) assert ok and broken is None