tinyrelayfile
tinyrelay/scripts/grant-request.test.mjs/

scripts/grant-request.test.mjs raw
1import test from 'node:test';
2import assert from 'node:assert/strict';
3import fs from 'node:fs';
4import vm from 'node:vm';
5const source=fs.readFileSync('tinyclient/grant-request.js','utf8');
6const id='a'.repeat(64),base='b'.repeat(64),agent='c'.repeat(64),operator='d'.repeat(64);
7const review=()=>({request_id:id,base,agent,operator,before:{name:"Agent",scope:{kinds:[9]}},after:{scope:{kinds:[9,30617]}},unsigned:{kind:30392,pubkey:operator,created_at:100,tags:[['grant-request',id],['grant-base',base],['e',id]],content:''}});
8function setup() {
9 const published=[],buttons=[{hasAttribute:n=>n==='data-grant-approve'},{hasAttribute:()=>false}]; let Ctor;
10 class Element {constructor(){this.isConnected=true;this.attrs={'data-review':JSON.stringify(review())};}getAttribute(n){return this.attrs[n]??null;}hasAttribute(n){return n in this.attrs;}querySelectorAll(){return buttons;}addEventListener(){} }
11 const signer={getPublicKey:async()=>operator},tiny={signer:()=>signer,signing:{publish:async e=>published.push(e)},navigate:async()=>{}};
12 vm.runInNewContext(source,{tiny,HTMLElement:Element,customElements:{define:(_,c)=>Ctor=c},AbortController,JSON,Date,Error,location:{href:'https://relay.test/approvals'}});
13 const host=new Ctor();host.report=m=>host.message=m;host.connectedCallback();
14 const fresh=review();fresh.unsigned.created_at=200;fresh.before={scope:{kinds:[9]},name:"Agent"};
15 const item={id,type:'grant',state:'open',asker:agent,asked:[operator],grant:fresh};host.latest=async()=>({item});
16 return {host,tiny,signer,buttons,published,item};
17}
18test('grant approval signs exactly one fresh replacement, not a reaction',async()=>{
19 const {host,published}=setup();await host.decide(null,true);
20 assert.equal(published.length,1);assert.equal(published[0].kind,30392);assert.equal(published[0].created_at,200);assert.deepEqual(Object.keys(published[0]),['kind','created_at','tags','content']);
21 assert.equal(published[0].tags.find(t=>t[0]==='grant-base')[1],base);
22});
23test('denial signs one reaction to the agent even when grant is paused',async()=>{
24 const {host,published,item}=setup();delete item.grant;item.grant_error='paused';await host.decide(null,false);
25 assert.equal(published.length,1);assert.equal(published[0].kind,7);assert.equal(published[0].content,'-');assert.equal(published[0].tags.find(t=>t[0]==='p')[1],agent);
26});
27for(const scenario of ['base','scope','closed','actor','identity','disconnect']) test('rejects changed '+scenario+' before signing',async()=>{
28 const {host,published,item,tiny}=setup();
29 host.latest=async()=>{if(scenario==='base')item.grant.base='e'.repeat(64);if(scenario==='scope')item.grant.after.scope.kinds.push(7);if(scenario==='closed')item.state='answered';if(scenario==='actor')item.asked=['f'.repeat(64)];if(scenario==='identity')tiny.signer=()=>({getPublicKey:async()=>operator});if(scenario==='disconnect'){host.isConnected=false;host.disconnectedCallback();}return {item};};
30 await host.decide(null,true);assert.equal(published.length,0);
31});
32test('double tap cannot publish an approval and denial together',async()=>{
33 const {host,published,buttons,item}=setup();let resolve;host.latest=()=>new Promise(done=>resolve=done);
34 const pending=host.decide(null,true);await new Promise(done=>setImmediate(done));assert.ok(buttons.every(b=>b.disabled));
35 await host.decide(null,false);resolve({item});await pending;assert.equal(published.length,1);assert.equal(published[0].kind,30392);
36});

About

ownerc01503e8035f
visibilitypublic
headd730679639bf

Maintainers

  • c01503e8035f owner
  • ba1b5beed5b5 agent hermes

Clone

  • https://012.run/npub1cq2s86qrtadf36eqq3epm38wmhdynrrcwxf84eldk3f94dkv7ttsrulrz5/tinyrelay.git

Refs