guanghu-world-entry/skills/guanghu-feishu-glp/receiver.test.mjs

22 lines
7.1 KiB
JavaScript
Raw Normal View History

import test from 'node:test';import assert from 'node:assert/strict';import {request,seal,hash,ZERO,validate} from './contract.mjs';import {createReceiver,zeroQuestion} from './receiver.mjs';
const token='fixture-not-real-secret',client={id:'fixture',key_sha256:hash(token),expires_at:'2099-01-01T00:00:00Z',personas:['ICE-P-X1016']};
const native=(answer,revision=5,text='q')=>({schema:'guanghu.zeropoint-channel-ask-receipt/v1',no_state_change:true,spoken:answer,brain_turn:{speaker:{brain_id:ZERO},brain_revision:revision,no_state_change:true,input_sha256:hash(text),reply_sha256:hash(answer),reply:answer,outcome:'BRAIN_REPLY_ACCEPTED'}});
const make=()=>request('请说明这次只读问答的边界。',{session:'fixture-session',task:'fixture-task'});
async function fixture(t,extra={}){let calls=0;const server=createReceiver({config:{clients:[client]},ask:async()=>{calls++;return {brain_id:ZERO,answer:'只读测试回答',no_state_change:true};},rules:async()=>({version:'fixture',automatic_adoption:false}),...extra});await new Promise(r=>server.listen(0,'127.0.0.1',r));t.after(()=>{server.closeAllConnections();server.close();});return {calls:()=>calls,url:'http://127.0.0.1:'+server.address().port,post:async(b=make(),auth=token)=>fetch('http://127.0.0.1:'+server.address().port+'/v1/glp',{method:'POST',headers:{'content-type':'application/json',...(auth?{authorization:'Bearer '+auth}:{})},body:JSON.stringify(b)})};}
test('anonymous requests and wrong routes never invoke a brain',async t=>{const f=await fixture(t);assert.equal((await f.post(make(),null)).status,401);assert.equal((await fetch(f.url+'/say',{method:'POST'})).status,404);assert.equal(f.calls(),0);});
test('valid GLP returns received, response and executed as separate stages',async t=>{const f=await fixture(t),m=make(),r=await f.post(m),j=await r.json();assert.equal(r.status,200);assert.equal(r.headers.get('cache-control'),'no-store');assert.equal(j.ack.message_type,'ACK');assert.equal(j.response.message_type,'RESPONSE');assert.equal(j.receipt.payload.content.status,'ACK-EXECUTED');assert.equal(j.response.context.parent_message_id,m.glp_message.message_id);assert.equal(j.no_execution_authority,true);});
test('checksum, foreign persona, target, expiry and missing task are rejected',async t=>{const f=await fixture(t);for(const alter of [m=>m.payload.content+='变更',m=>m.sender.object_id='ICE-P-OTHER',m=>m.receiver.object_id='other',m=>m.control.expires_at='2000-01-01',m=>m.context.task_id='']){const m=make().glp_message;alter(m);const b=seal(m);if(m.payload.content.endsWith('变更'))b.glp_message.integrity.checksum='0'.repeat(64);assert.equal((await f.post(b)).status,400);}assert.equal(f.calls(),0);});
test('extra execution fields and unsupported message types are rejected',async t=>{const f=await fixture(t);for(const alter of [m=>m.payload.command='rm',m=>m.message_type='BROADCAST',m=>m.integrity.signature='self-signed',m=>m.payload.attachments.push({url:'http://internal'})]){const m=make().glp_message;alter(m);const b=seal(m);if(m.integrity.signature)b.glp_message.integrity.signature='self-signed';assert.equal((await f.post(b)).status,400);}assert.equal(f.calls(),0);});
test('exact replay returns one result; same id with new content conflicts',async t=>{const f=await fixture(t),m=make();const a=await(await f.post(m)).json(),b=await(await f.post(m)).json();assert.deepEqual(a,b);const changed=structuredClone(m.glp_message);changed.payload.content='另一问题';assert.equal((await f.post(seal(changed))).status,409);assert.equal(f.calls(),1);});
test('concurrent retries share the same pending question',async t=>{let release,calls=0;const blocked=new Promise(r=>release=r),f=await fixture(t,{ask:async()=>{calls++;await blocked;return {answer:'ok'};}}),m=make();const a=f.post(m),b=f.post(m);await new Promise(r=>setTimeout(r,30));release();const [x,y]=await Promise.all([a,b]);assert.equal(x.status,200);assert.equal(y.status,200);assert.equal(calls,1);});
test('SYNC queries release metadata without calling a brain or adopting rules',async t=>{const f=await fixture(t),j=await(await f.post(request('当前发行版本',{type:'SYNC',session:'s',task:'t'}))).json();assert.equal(j.response.payload.content.version,'fixture');assert.equal(j.response.payload.content.automatic_adoption,false);assert.equal(f.calls(),0);});
test('upstream failure is withheld and cached with failed receipt',async t=>{let calls=0;const f=await fixture(t,{ask:async()=>{calls++;throw Error('secret /private/path');}}),m=make();for(let i=0;i<2;i++){const r=await f.post(m),j=await r.json();assert.equal(r.status,502);assert.equal(j.receipt.payload.content.status,'ACK-FAILED');assert.equal(JSON.stringify(j).includes('secret'),false);}assert.equal(calls,1);});
test('a full replay cache refuses new requests instead of evicting live ids',async t=>{const f=await fixture(t,{capacity:1});assert.equal((await f.post()).status,200);assert.equal((await f.post()).status,503);assert.equal(f.calls(),1);});
test('oversized messages do not reach a brain',async t=>{const f=await fixture(t);assert.equal((await f.post({padding:'x'.repeat(25000)})).status,413);const m=make();assert.throws(()=>validate(m,{personas:[]}),/SENDER/);});
test('native Zero adapter checks own identity and read-only reply',async()=>{const status={brain_id:ZERO,revision:5,integrity:'BRAIN_VERIFIED',node_role:'REPLICA_READ_ONLY',brain_write_allowed:false};let urls=[];const get=async(u,o)=>{urls.push(u);if(u.endsWith('/status'))return status;assert.equal(JSON.parse(o.body).text,'q');return native('已核验回答');};assert.equal((await zeroQuestion('q',{get})).revision,5);assert.equal(urls.filter(x=>x.endsWith('/ask')).length,1);assert.equal(urls.some(x=>x.endsWith('/say')),false);});
test('native adapter withholds wrong identity, rejected reply and changed source',async()=>{const status={brain_id:ZERO,revision:5,integrity:'BRAIN_VERIFIED',node_role:'REPLICA_READ_ONLY',brain_write_allowed:false};await assert.rejects(zeroQuestion('q',{get:async()=>({...status,brain_id:'other'})}),/IDENTITY/);await assert.rejects(zeroQuestion('q',{get:async u=>u.endsWith('/status')?status:{no_state_change:false}}),/WITHHELD/);let reads=0;await assert.rejects(zeroQuestion('q',{get:async u=>u.endsWith('/status')?{...status,revision:5+reads++}:native('x')}),/SOURCE_CHANGED/);});
test('expired credentials cannot enter the receiver',async t=>{const f=await fixture(t,{config:{clients:[{...client,expires_at:'2000-01-01T00:00:00Z'}]}});assert.equal((await f.post()).status,401);assert.equal(f.calls(),0);});
test('new requests are rate limited while exact retries stay cached',async t=>{const f=await fixture(t);for(let i=0;i<12;i++)assert.equal((await f.post()).status,200);assert.equal((await f.post()).status,429);assert.equal(f.calls(),12);});
test('native reply speaker, input digest and reply digest must match',async()=>{const status={brain_id:ZERO,revision:5,integrity:'BRAIN_VERIFIED',node_role:'REPLICA_READ_ONLY',brain_write_allowed:false};for(const mutate of [r=>r.brain_turn.speaker.brain_id='foreign',r=>r.brain_turn.input_sha256='0'.repeat(64),r=>r.spoken='changed',r=>r.brain_turn.brain_revision=4]){const r=native('answer');mutate(r);await assert.rejects(zeroQuestion('q',{get:async u=>u.endsWith('/status')?status:r}),/WITHHELD/);}});