amana / scripts /smoke_test.py
Misbahuddin's picture
Phase 4.1: gate hardening — corroborate hard citations + enforce ELIG-4
438e8e8
Raw
History Blame Contribute Delete
12.7 kB
"""Offline-first smoke test for the triage copilot.
Runs four layers, skipping any that need deps/keys that aren't present:
1. Data layer — needs only pydantic (campaign stripping, schema, policy parse, scanner)
2. Index + retrieval — needs chromadb + sentence-transformers
3. Agent wiring — needs pydantic-ai (uses TestModel, no API call)
4. Live triage — needs ANTHROPIC_API_KEY
python -m scripts.smoke_test
Exit code is non-zero if any *run* check fails (skips don't fail the build).
"""
from __future__ import annotations
import os
import sys
import traceback
PASS, FAIL, SKIP = "PASS", "FAIL", "SKIP"
results: list[tuple[str, str, str]] = []
def check(name: str, fn):
try:
fn()
results.append((PASS, name, ""))
except _Skip as s:
results.append((SKIP, name, str(s)))
except Exception as e:
results.append((FAIL, name, f"{type(e).__name__}: {e}"))
traceback.print_exc()
class _Skip(Exception):
pass
def need(module: str):
import importlib.util
if importlib.util.find_spec(module) is None:
raise _Skip(f"{module} not installed")
# --------------------------------------------------------------- 1. data layer
def t_no_private_keys_leak():
from src.campaigns import load_campaign_dict, list_campaign_paths
for p in list_campaign_paths():
d = load_campaign_dict(p)
leaked = [k for k in _all_keys(d) if k.startswith("_")]
assert not leaked, f"{p.name} leaked private keys: {leaked}"
def _all_keys(obj):
if isinstance(obj, dict):
for k, v in obj.items():
yield k
yield from _all_keys(v)
elif isinstance(obj, list):
for v in obj:
yield from _all_keys(v)
def t_all_campaigns_validate():
from src.campaigns import load_campaign, list_campaign_paths
paths = list_campaign_paths()
assert len(paths) == 18, f"expected 18 campaigns, found {len(paths)}"
for p in paths:
load_campaign(p) # raises on schema mismatch
def t_policy_parses():
from src.policy import parse_policy_rules, valid_rule_ids
rules = parse_policy_rules()
ids = valid_rule_ids()
assert len(rules) == 26, f"expected 26 rules, parsed {len(rules)}"
for must in ("ELIG-2", "PROH-3", "COMP-1", "CONT-2", "DEC-6"):
assert must in ids, f"missing rule {must}"
# every rule has non-trivial text
for r in rules:
assert len(r.text) > 20, f"{r.rule_id} text too short"
def t_expected_rules_are_real():
"""Data integrity: every rule referenced in a campaign's private _expected must exist."""
from src.campaigns import load_expected, list_campaign_paths
from src.policy import valid_rule_ids
ids = valid_rule_ids()
for p in list_campaign_paths():
exp = load_expected(p) or {}
for rid in exp.get("primary_rules", []):
assert rid in ids, f"{p.name} references unknown rule {rid}"
def t_risk_scanner():
from src.campaigns import load_campaign
from src.tools import scan_risk_signals
def names(camp_id):
c = load_campaign(f"data/campaigns/{camp_id}.json")
return {s.name for s in scan_risk_signals(c)}
assert "off_platform_payment" in names("camp-007")
assert "embedded_instruction" in names("camp-015")
assert "weapons" in names("camp-008")
assert "prize_draw" in names("camp-006")
assert "investment_return" in names("camp-005")
assert "high_value_goal" in names("camp-010")
# The nuance case: principal-debt mentions "interest" but is NOT an investment.
n017 = names("camp-017")
assert "interest_mentioned" in n017, "should flag interest mention for agent to adjudicate"
assert "investment_return" not in n017, "must NOT flag debt-principal as an investment"
def t_sanctions_mock():
from src.tools import check_sanctions
hit = check_sanctions(["Viktor Sanctionov"], ["Sanctionistan"])
assert hit["matched"] and hit["country_matches"] and hit["name_matches"]
clean = check_sanctions(["Yusuf Khan"], ["United States"])
assert not clean["matched"]
def t_policy_get_rule():
from src.policy import get_rule, policy_index
assert len(policy_index()) == 26
rule = get_rule("PROH-3")
assert rule is not None and rule.rule_id == "PROH-3"
assert "interest" in rule.text.lower(), "PROH-3 text should mention interest"
assert get_rule("NOPE-9") is None
def t_policy_gate():
"""The deterministic safety envelope — enforced in code, independent of any model or the index.
These invariants are what make a weak local model and Claude converge on the cases that matter."""
from src.campaigns import load_campaign
from src.gate import apply_policy_gate
from src.schemas import RuleViolation, TriageDecision
def camp(cid):
return load_campaign(f"data/campaigns/{cid}.json")
def dec(rec, confidence="medium", violations=None, manip=False):
return TriageDecision(recommendation=rec, confidence=confidence,
rule_violations=violations or [], risk_signals=[], rationale="t",
questions_for_submitter=[], manipulation_detected=manip)
# Sanctions match -> an APPROVE is forced to ESCALATE (COMP-1).
g = apply_policy_gate(camp("camp-009"), dec("APPROVE"))
assert g.decision.recommendation == "ESCALATE" and any(o.rule_id == "COMP-1" for o in g.overrides)
# Prompt injection -> ESCALATE, and the gate corrects the manipulation flag the model missed (DEC-6).
g = apply_policy_gate(camp("camp-015"), dec("APPROVE", manip=False))
assert g.decision.recommendation == "ESCALATE" and g.decision.manipulation_detected is True
assert any(o.rule_id == "DEC-6" for o in g.overrides)
# High-value goal -> an APPROVE is forced to ESCALATE (COMP-2).
g = apply_policy_gate(camp("camp-010"), dec("APPROVE"))
assert g.decision.recommendation == "ESCALATE" and any(o.rule_id == "COMP-2" for o in g.overrides)
# REJECT without a confirmed hard-rule citation escalates instead (DEC-2).
g = apply_policy_gate(camp("camp-001"), dec("REJECT", violations=[]))
assert g.decision.recommendation == "ESCALATE" and any(o.rule_id == "DEC-2" for o in g.overrides)
# REJECT resting on a real, scanner-corroborated hard citation (PROH-3 + investment_return) stands.
g = apply_policy_gate(camp("camp-005"),
dec("REJECT", violations=[RuleViolation(rule_id="PROH-3", severity="hard",
evidence="interest-bearing investment")]))
assert g.decision.recommendation == "REJECT" and g.overrides == []
# A hard citation the scanner does NOT corroborate cannot reject — closes weak-model fabrication
# (camp-011 has no weapons content, so a fabricated PROH-2 REJECT escalates, DEC-2).
g = apply_policy_gate(camp("camp-011"),
dec("REJECT", violations=[RuleViolation(rule_id="PROH-2", severity="hard",
evidence="(fabricated)")]))
assert g.decision.recommendation == "ESCALATE" and any(o.rule_id == "DEC-2" for o in g.overrides)
# A large-goal APPROVE with no confirmable fund-use breakdown defers to a human (ELIG-4) —
# camp-012 is >$10k with no breakdown; the gate must not let it slip through.
g = apply_policy_gate(camp("camp-012"), dec("APPROVE"))
assert g.decision.recommendation == "ESCALATE" and any(o.rule_id == "ELIG-4" for o in g.overrides)
# Low confidence on an APPROVE -> ESCALATE (DEC-5, calibrated humility).
g = apply_policy_gate(camp("camp-001"), dec("APPROVE", confidence="low"))
assert g.decision.recommendation == "ESCALATE" and any(o.rule_id == "DEC-5" for o in g.overrides)
# A clean, confident APPROVE passes through untouched (the gate is not trigger-happy).
g = apply_policy_gate(camp("camp-017"), dec("APPROVE", confidence="medium"))
assert g.decision.recommendation == "APPROVE" and g.overrides == []
assert g.llm_recommendation == "APPROVE"
def t_audit_roundtrip():
import os
import tempfile
from src.audit import append_decision, decided_campaign_ids, load_decisions
with tempfile.TemporaryDirectory() as tmp:
log = os.path.join(tmp, "audit.jsonl")
assert load_decisions(log) == [] # tolerates missing file
append_decision({"campaign_id": "camp-005", "ai_recommendation": "REJECT",
"human_decision": "REJECT", "is_override": False, "reason": ""}, path=log)
append_decision({"campaign_id": "camp-017", "ai_recommendation": "APPROVE",
"human_decision": "REJECT", "is_override": True, "reason": "looks off"}, path=log)
rows = load_decisions(log)
assert len(rows) == 2 and all("timestamp" in r for r in rows), "timestamp auto-added"
latest = decided_campaign_ids(log)
assert latest["camp-017"]["is_override"] is True and latest["camp-017"]["reason"] == "looks off"
# --------------------------------------------------- 2. index + retrieval (heavy)
def t_build_index_and_search():
need("chromadb")
need("sentence_transformers")
from scripts.build_index import build_policy, build_cases
from src.tools import policy_search, similar_cases
assert build_policy() == 26
assert build_cases() == 8
# Repeat to guard against the Chroma client-lifecycle bug (re-instantiating the client per
# call corrupted its shared system; the agent makes many tool calls per run).
for _ in range(12):
hits = policy_search("interest-bearing investment with guaranteed return")
assert any(h["rule_id"] == "PROH-3" for h in hits), f"PROH-3 not retrieved; got {[h['rule_id'] for h in hits]}"
cases = similar_cases("donate to enter a prize raffle")
assert any(c["outcome"] == "REJECTED" for c in cases)
# ------------------------------------------------------ 3. agent wiring (TestModel)
def t_agent_wiring():
need("pydantic_ai")
from pydantic_ai.models.test import TestModel
from src.agent import triage
from src.campaigns import load_campaign
from src.schemas import GatedDecision
c = load_campaign("data/campaigns/camp-001.json")
# call_tools=[] => exercise construction + structured output without needing the index
gated = triage(c, model=TestModel(call_tools=[]))
assert isinstance(gated, GatedDecision)
assert gated.decision.recommendation in ("APPROVE", "REJECT", "ESCALATE")
# ----------------------------------------------------------- 4. live triage (key)
def t_live_triage():
if not os.getenv("ANTHROPIC_API_KEY"):
raise _Skip("ANTHROPIC_API_KEY not set")
need("pydantic_ai")
from src.agent import triage
from src.campaigns import load_campaign
g = triage(load_campaign("data/campaigns/camp-005.json"))
rec = g.decision.recommendation
# camp-005 is a clear riba investment. A capable model REJECTs citing PROH-3 (hard); a weaker
# model may under-cite, in which case the policy gate routes the unconfirmed reject to ESCALATE
# (DEC-2). The invariant that must ALWAYS hold — model-independently — is that it is never approved.
assert rec in ("REJECT", "ESCALATE"), f"camp-005 (riba) must never be approved, got {rec}"
def main() -> int:
for name, fn in [
("data: no private keys leak", t_no_private_keys_leak),
("data: all 18 campaigns validate", t_all_campaigns_validate),
("data: policy parses to 26 rules", t_policy_parses),
("data: _expected rule IDs are real", t_expected_rules_are_real),
("data: deterministic risk scanner", t_risk_scanner),
("data: mock sanctions screen", t_sanctions_mock),
("data: policy.get_rule lookup", t_policy_get_rule),
("gate: deterministic policy gate", t_policy_gate),
("audit: append/load round-trip", t_audit_roundtrip),
("index: build + retrieve PROH-3", t_build_index_and_search),
("agent: wiring via TestModel", t_agent_wiring),
("agent: live triage (camp-005)", t_live_triage),
]:
check(name, fn)
print("\n" + "=" * 60)
for status, name, msg in results:
line = f" {status} {name}"
if msg:
line += f" — {msg}"
print(line)
n_fail = sum(1 for s, _, _ in results if s == FAIL)
n_pass = sum(1 for s, _, _ in results if s == PASS)
n_skip = sum(1 for s, _, _ in results if s == SKIP)
print("=" * 60)
print(f" {n_pass} passed, {n_fail} failed, {n_skip} skipped")
return 1 if n_fail else 0
if __name__ == "__main__":
sys.exit(main())