81 lines
6.0 KiB
Python
81 lines
6.0 KiB
Python
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
|