Add durable Human Gates inbox

This commit is contained in:
timmy committed 2026-08-26 00:05:27 +00:00
1 parent 7ec78ac182
commit e02115bd71
14 files changed
+1024 -1

No files matched your search

+106
View File
@@ -0,0 +1,106 @@
import sqlite3
import httpx
import pytest
from src import main
from src.human_gate_store import HumanGateStore
CANDIDATE = {
"source": "release-bot", "project": "stackchain/dashboard", "candidate_hash": "abc123",
"title": "Release candidate", "priority": 7,
"artifacts": [{"name": "manifest", "url": "https://forge.example/manifest"}],
"links": [{"label": "pull", "url": "https://forge.example/pulls/1"}],
"checks": [{"name": "unit", "state": "success", "required": True}],
"score": {"value": 98, "provenance": "eval/v1"},
"provenance": {"run": "9"},
}
CHECKLIST = {"exact_hash": True, "artifacts_reviewed": True, "provenance_reviewed": True}
@pytest.fixture
def gate_api(monkeypatch, tmp_path):
ticks = iter(range(100, 120))
store = HumanGateStore(tmp_path / "gates.sqlite3", clock=lambda: next(ticks))
monkeypatch.setattr(main, "_human_gate_store", lambda: store, raising=False)
async def identity():
return {"id": 1, "login": "timmy"}
monkeypatch.setattr(main, "current_user", identity)
return store
@pytest.mark.anyio
async def test_intake_list_and_detail_are_account_bound_and_no_store(gate_api):
transport = httpx.ASGITransport(app=main.app)
async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
created = await client.post("/api/v1/human-gates/intake", json=CANDIDATE, headers={"Idempotency-Key": "run-9"})
repeated = await client.post("/api/v1/human-gates/intake", json=CANDIDATE, headers={"Idempotency-Key": "run-9"})
listing = await client.get("/api/v1/human-gates")
detail = await client.get(f"/api/v1/human-gates/{created.json()['id']}")
assert created.status_code == 201
assert repeated.status_code == 200
assert repeated.json()["id"] == created.json()["id"]
assert listing.json()["pending_count"] == 1
assert detail.json()["candidate_hash"] == "abc123"
assert detail.json()["history"][0]["action"] == "intake"
assert all(response.headers["cache-control"] == "no-store" for response in (created, repeated, listing, detail))
@pytest.mark.anyio
async def test_decision_requires_revision_and_returns_durable_receipt(gate_api):
gate = gate_api.intake("timmy", CANDIDATE, idempotency_key="run-9")
transport = httpx.ASGITransport(app=main.app)
payload = {"expected_revision": gate["revision"], "decision": "release", "reason": "", "override_reason": "", "checklist": CHECKLIST}
async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
decided = await client.post(f"/api/v1/human-gates/{gate['id']}/decision", json=payload, headers={"Idempotency-Key": "decision-9"})
repeated = await client.post(f"/api/v1/human-gates/{gate['id']}/decision", json=payload, headers={"Idempotency-Key": "decision-9"})
receipt = await client.get(f"/api/v1/human-gate-receipts/{decided.json()['receipt_id']}")
stale = await client.post(f"/api/v1/human-gates/{gate['id']}/decision", json=payload, headers={"Idempotency-Key": "decision-10"})
assert decided.status_code == 201
assert repeated.status_code == 200
assert receipt.json() == decided.json()
assert stale.status_code == 409
assert all(response.headers["cache-control"] == "no-store" for response in (decided, repeated, receipt, stale))
@pytest.mark.anyio
async def test_gate_store_failure_is_sanitized_no_store(monkeypatch):
def unavailable():
raise sqlite3.OperationalError("sensitive database path")
monkeypatch.setattr(main, "_human_gate_store", unavailable)
async def identity():
return {"id": 1, "login": "timmy"}
monkeypatch.setattr(main, "current_user", identity)
transport = httpx.ASGITransport(app=main.app)
async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
response = await client.get("/api/v1/human-gates")
assert response.status_code == 503
assert response.headers["cache-control"] == "no-store"
assert response.json() == {"detail": "Human Gates are temporarily unavailable"}
@pytest.mark.anyio
async def test_gate_mutations_require_idempotency_key_and_validate_override(gate_api):
failing = {**CANDIDATE, "candidate_hash": "fail123", "checks": [{"name": "browser", "state": "failure", "required": True}]}
gate = gate_api.intake("timmy", failing, idempotency_key="run-fail")
transport = httpx.ASGITransport(app=main.app)
async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
missing_key = await client.post("/api/v1/human-gates/intake", json=CANDIDATE)
no_override = await client.post(
f"/api/v1/human-gates/{gate['id']}/decision",
json={"expected_revision": 1, "decision": "release", "reason": "", "override_reason": "", "checklist": CHECKLIST},
headers={"Idempotency-Key": "decision-fail"},
)
assert missing_key.status_code == 422
assert no_override.status_code == 422
assert "override reason" in no_override.json()["detail"]
+110
View File
@@ -0,0 +1,110 @@
import sqlite3
import pytest
from src.human_gate_store import GateConflict, GateValidationError, HumanGateStore
def candidate(hash_value="abc123", *, priority=2, check_state="success"):
return {
"source": "release-bot",
"project": "stackchain/dashboard",
"candidate_hash": hash_value,
"title": "Dashboard candidate",
"priority": priority,
"artifacts": [{"name": "manifest", "url": "https://forge.example/artifacts/manifest.json"}],
"links": [{"label": "change", "url": "https://forge.example/pulls/1415"}],
"checks": [{"name": "browser", "state": check_state, "required": True}],
"score": {"value": 92, "provenance": "release-evaluator/v2"},
"provenance": {"producer": "release-bot", "run_id": "run-9"},
}
def checklist():
return {"exact_hash": True, "artifacts_reviewed": True, "provenance_reviewed": True}
def test_intake_is_account_bound_idempotent_and_survives_restart(tmp_path):
path = tmp_path / "gates.sqlite3"
store = HumanGateStore(path, clock=lambda: 100)
first = store.intake("timmy", candidate(), idempotency_key="producer-9")
repeated = store.intake("timmy", candidate(), idempotency_key="producer-9")
restarted = HumanGateStore(path, clock=lambda: 101)
assert repeated == first
assert restarted.detail("timmy", first["id"])["candidate_hash"] == "abc123"
assert restarted.list("alex")["pending_count"] == 0
with sqlite3.connect(path) as connection:
assert connection.execute("PRAGMA journal_mode").fetchone()[0] == "wal"
def test_new_hash_supersedes_only_older_pending_candidate_without_losing_audit(tmp_path):
store = HumanGateStore(tmp_path / "gates.sqlite3", clock=iter([100, 101]).__next__)
old = store.intake("timmy", candidate("abc123"), idempotency_key="run-1")
new = store.intake("timmy", candidate("def456"), idempotency_key="run-2")
old_detail = store.detail("timmy", old["id"])
queue = store.list("timmy")
assert old_detail["state"] == "superseded"
assert old_detail["superseded_by"] == new["id"]
assert old_detail["history"][-1]["action"] == "superseded"
assert queue["pending_count"] == 1
assert queue["items"][0]["candidate_hash"] == "def456"
def test_pending_queue_orders_highest_priority_then_oldest(tmp_path):
clock = iter([100, 101, 102]).__next__
store = HumanGateStore(tmp_path / "gates.sqlite3", clock=clock)
low = store.intake("timmy", {**candidate("a1", priority=1), "project": "p/one"}, idempotency_key="1")
oldest_high = store.intake("timmy", {**candidate("b2", priority=5), "project": "p/two"}, idempotency_key="2")
newest_high = store.intake("timmy", {**candidate("c3", priority=5), "project": "p/three"}, idempotency_key="3")
assert [item["id"] for item in store.list("timmy")["items"]] == [oldest_high["id"], newest_high["id"], low["id"]]
def test_decision_checks_revision_rules_and_returns_durable_idempotent_receipt(tmp_path):
path = tmp_path / "gates.sqlite3"
store = HumanGateStore(path, clock=iter([100, 101]).__next__)
gate = store.intake("timmy", candidate(), idempotency_key="run-1")
receipt = store.decide(
"timmy", gate["id"], expected_revision=gate["revision"], decision="release",
reason="", override_reason="", checklist=checklist(), idempotency_key="decision-1",
)
repeated = store.decide(
"timmy", gate["id"], expected_revision=gate["revision"], decision="release",
reason="", override_reason="", checklist=checklist(), idempotency_key="decision-1",
)
assert repeated == receipt
assert receipt["state"] == "released"
assert receipt["candidate_hash"] == "abc123"
assert HumanGateStore(path, clock=lambda: 999).receipt("timmy", receipt["receipt_id"]) == receipt
with pytest.raises(GateConflict):
store.decide("timmy", gate["id"], expected_revision=gate["revision"], decision="hold", reason="later", override_reason="", checklist=checklist(), idempotency_key="decision-2")
def test_hold_requires_reason_and_release_requires_checklist_and_override_for_unmet_checks(tmp_path):
store = HumanGateStore(tmp_path / "gates.sqlite3", clock=lambda: 100)
gate = store.intake("timmy", candidate(check_state="failure"), idempotency_key="run-1")
with pytest.raises(GateValidationError, match="Hold reason"):
store.decide("timmy", gate["id"], expected_revision=1, decision="hold", reason="", override_reason="", checklist={}, idempotency_key="d1")
with pytest.raises(GateValidationError, match="checklist"):
store.decide("timmy", gate["id"], expected_revision=1, decision="release", reason="", override_reason="needed", checklist={"exact_hash": True}, idempotency_key="d2")
with pytest.raises(GateValidationError, match="override reason"):
store.decide("timmy", gate["id"], expected_revision=1, decision="release", reason="", override_reason="", checklist=checklist(), idempotency_key="d3")
receipt = store.decide("timmy", gate["id"], expected_revision=1, decision="hold", reason="Awaiting owner", override_reason="", checklist={}, idempotency_key="d4")
assert receipt["state"] == "held"
assert receipt["checklist"] == {}
def test_same_hash_cannot_be_redefined_by_a_new_idempotency_key(tmp_path):
store = HumanGateStore(tmp_path / "gates.sqlite3", clock=lambda: 100)
store.intake("timmy", candidate(), idempotency_key="run-1")
with pytest.raises(GateConflict, match="hash"):
store.intake("timmy", {**candidate(), "title": "Changed facts"}, idempotency_key="run-2")
+80
View File
@@ -0,0 +1,80 @@
import json
import subprocess
from pathlib import Path
MODULE = Path(__file__).parents[1] / "frontend" / "human-gates.js"
INDEX = Path(__file__).parents[1] / "frontend" / "index.html"
DASHBOARD = Path(__file__).parents[1] / "frontend" / "dashboard.js"
def run_node(body):
script = f"const createHumanGates=require({json.dumps(str(MODULE))});\n" + body
result = subprocess.run(["node", "-e", script], check=True, text=True, capture_output=True)
return json.loads(result.stdout)
def test_queue_loads_pending_count_uses_account_cache_and_renders_inbox_zero():
output = run_node(r"""
const values=new Map(); const storage={getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v)};
let response={pending_count:1,items:[{id:'g1',title:'Candidate',candidate_hash:'abc123',priority:4,state:'pending',revision:1,checks:[]}]};
const nodes={count:{textContent:''},list:{innerHTML:''},status:{textContent:''},panel:{hidden:true}};
const gates=createHumanGates({storage,getLogin:()=> 'timmy',isOnline:()=>true,nodes,location:{hash:''},fetchJson:async()=>response});
(async()=>{ await gates.load(); const first={snapshot:gates.snapshot(),count:nodes.count.textContent,html:nodes.list.innerHTML,keys:[...values.keys()]}; response={pending_count:0,items:[]}; await gates.load(); process.stdout.write(JSON.stringify({first,zero:{snapshot:gates.snapshot(),status:nodes.status.textContent,html:nodes.list.innerHTML}})); })();
""")
assert output["first"]["count"] == "1"
assert "abc123" in output["first"]["html"]
assert output["first"]["keys"] == ["stackchain.human-gates.v1:timmy"]
assert output["zero"]["snapshot"]["pending_count"] == 0
assert "Inbox zero" in output["zero"]["html"]
def test_review_next_is_a_fixed_snapshot_and_decision_and_next_advances_without_new_arrivals():
output = run_node(r"""
const calls=[]; const nodes={count:{textContent:''},list:{innerHTML:''},status:{textContent:''},panel:{hidden:true},detail:{innerHTML:''}};
const initial={pending_count:2,items:[{id:'old',title:'Old',candidate_hash:'a1',revision:1,checks:[]},{id:'next',title:'Next',candidate_hash:'b2',revision:1,checks:[]}]};
const gates=createHumanGates({storage:{getItem:()=>null,setItem(){}},getLogin:()=> 'timmy',isOnline:()=>true,nodes,location:{hash:'#/my-work/human-gates'},fetchJson:async(path,options={})=>{calls.push({path,options}); if(options.method==='POST') return {receipt_id:'r1',state:'released'}; return initial;}});
(async()=>{await gates.load(); const reviewed=gates.reviewNext(); initial.items.unshift({id:'new',title:'New arrival',candidate_hash:'c3',revision:1,checks:[]}); const result=await gates.decideAndNext('release',{checklist:{exact_hash:true,artifacts_reviewed:true,provenance_reviewed:true}}); process.stdout.write(JSON.stringify({reviewed,result,current:gates.current(),calls,hash:gates.route()}));})();
""")
assert output["reviewed"]["id"] == "old"
assert output["result"]["receipt"]["receipt_id"] == "r1"
assert output["current"]["id"] == "next"
assert output["hash"] == "#/my-work/human-gates"
post = next(call for call in output["calls"] if call["options"].get("method") == "POST")
assert post["path"] == "api/v1/human-gates/old/decision"
assert "Idempotency-Key" in post["options"]["headers"]
def test_review_loads_exact_detail_with_links_provenance_and_history():
output = run_node(r"""
const detail={id:'g1',title:'Candidate',candidate_hash:'abc123',revision:1,checks:[{name:'unit',state:'success',required:true}],artifacts:[{name:'manifest',url:'https://forge.example/manifest'}],links:[{label:'pull',url:'https://forge.example/pull/1'}],score:{value:98,provenance:'eval/v1'},provenance:{producer:'bot',run_id:'9'},history:[{action:'intake',at:100}]};
const nodes={count:{},list:{},status:{},panel:{},detail:{innerHTML:''}};
const gates=createHumanGates({storage:{getItem:()=>null,setItem(){}},getLogin:()=> 'timmy',isOnline:()=>true,nodes,location:{hash:''},fetchJson:async path=>path.endsWith('/g1')?detail:{pending_count:1,items:[detail]}});
(async()=>{await gates.load();gates.reviewNext();await new Promise(resolve=>setTimeout(resolve,0));process.stdout.write(JSON.stringify({html:nodes.detail.innerHTML}));})();
""")
assert "https://forge.example/pull/1" in output["html"]
assert "bot" in output["html"]
assert "intake" in output["html"]
def test_release_requires_override_for_unmet_required_checks_and_decisions_require_online_identity():
output = run_node(r"""
let online=true, login='timmy', posts=0;
const item={id:'g1',title:'Failing',candidate_hash:'a1',revision:1,checks:[{name:'browser',state:'failure',required:true}]};
const gates=createHumanGates({storage:{getItem:()=>null,setItem(){}},getLogin:()=>login,isOnline:()=>online,nodes:{count:{},list:{},status:{},panel:{},detail:{}},location:{hash:''},fetchJson:async(path,options={})=>{if(options.method==='POST'){posts++;return {receipt_id:'r1'}};return {pending_count:1,items:[item]};}});
(async()=>{await gates.load();gates.reviewNext();let override,offline,identity;try{await gates.decideAndNext('release',{checklist:{exact_hash:true,artifacts_reviewed:true,provenance_reviewed:true}})}catch(e){override=e.message} online=false;try{await gates.decideAndNext('hold',{reason:'wait',checklist:{exact_hash:true,artifacts_reviewed:true,provenance_reviewed:true}})}catch(e){offline=e.message} online=true;login='';try{await gates.decideAndNext('hold',{reason:'wait',checklist:{exact_hash:true,artifacts_reviewed:true,provenance_reviewed:true}})}catch(e){identity=e.message}process.stdout.write(JSON.stringify({override,offline,identity,posts}));})();
""")
assert "override reason" in output["override"]
assert "online" in output["offline"]
assert "identity" in output["identity"]
assert output["posts"] == 0
def test_human_gate_mobile_shell_and_deep_route_are_wired():
index = INDEX.read_text()
dashboard = DASHBOARD.read_text()
assert 'id="human-gates"' in index
assert 'id="human-gates-count"' in index
assert 'static/human-gates.js' in index
assert "#/my-work/human-gates" in dashboard
assert "createHumanGates" in dashboard
+22
View File
@@ -0,0 +1,22 @@
from pathlib import Path
README = Path(__file__).parents[1] / "README.md"
def test_readme_documents_hash_bound_producer_intake_and_revision_decisions():
text = README.read_text()
assert "POST /api/v1/human-gates/intake" in text
assert "Idempotency-Key" in text
assert '"candidate_hash"' in text
assert "expected_revision" in text
assert "STACKCHAIN_HUMAN_GATE_DB" in text
def test_readme_defines_privacy_safe_telegram_coalescing_contract():
text = README.read_text()
assert "Telegram coalescing contract" in text
assert "#/my-work/human-gates" in text
assert "count and route only" in text
assert "candidate hash" in text
assert "superseded" in text
+1
View File
@@ -1373,6 +1373,7 @@ def test_install_precaches_complete_subpath_scoped_app_shell():
"/dashboard/manifest.webmanifest",
"/dashboard/static/dashboard.css",
"/dashboard/static/dashboard.js",
"/dashboard/static/human-gates.js",
"/dashboard/static/icons/stackchain-192.png",
"/dashboard/static/icons/stackchain-512.png",
"/dashboard/static/session.js",