mirror of
https://github.com/block/buzz.git
synced 2026-10-29 21:41:33 +00:00
164 lines
13 KiB
TypeScript
164 lines
13 KiB
TypeScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { mkdtempSync, realpathSync, rmSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
import { setTimeout as delay } from 'node:timers/promises';
|
|
import { registerHooks } from 'node:module';
|
|
const stopFault = { active:false };
|
|
Object.assign(globalThis,{__beehivePrivateMoveStopFault:stopFault});
|
|
registerHooks({load(url,context,next){
|
|
const result=next(url,context);
|
|
if (!url.endsWith('/src/owned.ts')) return result;
|
|
const source=String(result.source), marker='async stop() {';
|
|
if(!source.includes(marker)) throw Error('Owned Stop test seam changed');
|
|
return {...result,source:source.replace(marker,marker+" if ((globalThis as any).__beehivePrivateMoveStopFault.active) throw Error('Synthetic owned Stop failure');")};
|
|
}});
|
|
const {registrationFixture}=await import('./registration-fixture.ts');
|
|
import { managementClient } from '../src/intents.ts';
|
|
import { message, newKey, publicKey, type Message } from '../src/protocol.ts';
|
|
import { signSuccessor, verifySuccessors, privateExchange } from '../src/private-handoff.ts';
|
|
import { createGenesis } from '../src/assignment.ts';
|
|
import { hash, type Assignment, type Grant } from '../src/handoff.ts';
|
|
async function until(fn:()=>boolean) { for(let n=0;n<400;n++) { if(fn()) return; await delay(20); } throw Error('Private Move evidence missing'); }
|
|
|
|
test('private two-host owned-process transfer, exact replay and reverse signed successor',async t=>{
|
|
const home=realpathSync(mkdtempSync(join(tmpdir(),'beehive-pairing-cli-private-move-')));
|
|
const f=await registrationFixture(home,()=>{},true,true).catch(error=>{console.error(error);throw error;});
|
|
const inventory=new Map<string,Message>();
|
|
const c=managementClient(join(home,'client'),f.url,f.ownerSecret,m=>{if(m.type==='inventory')inventory.set(m.host,m);},()=>{},{catalog:{version:1,owner:publicKey(f.ownerSecret),relay:f.url,registrations:[]}});
|
|
t.after(async()=>{c.close();await f.close();rmSync(home,{recursive:true,force:true});});
|
|
await c.ready; await until(()=>inventory.size===2);
|
|
const source=()=>inventory.get(f.host)!, dest=()=>inventory.get(f.destination!.host)!;
|
|
const send=async(m:Message)=>{c.submit(m);await until(()=>c.status().some(s=>s.request.id===m.id&&s.state!=='pending'));return c.status().find(s=>s.request.id===m.id)!;};
|
|
assert.equal((await send(message('start',f.host,f.agent,source().revision))).state,'completed');
|
|
await until(()=>source().body.phase==='running');
|
|
const original=source().body.actualRun;
|
|
const prior=message('save',dest().host,f.agent,dest().revision,dest().body.selectedNext as Record<string,unknown>);
|
|
assert.equal((await send(prior)).state,'completed');await until(()=>dest().revision>prior.revision);
|
|
const duplicate=message('move',f.host,f.agent,source().revision,{target:dest().host,targetRevision:dest().revision,selection:dest().body.selectedNext});duplicate.id=prior.id;
|
|
const other=managementClient(join(home,'other-controller'),f.url,f.ownerSecret,()=>{},()=>{},{catalog:{version:1,owner:publicKey(f.ownerSecret),relay:f.url,registrations:[]}});
|
|
t.after(()=>other.close());await other.ready;await delay(100);other.submit(duplicate);
|
|
await until(()=>other.status().some(o=>o.request.id===duplicate.id&&o.state==='failed'));
|
|
assert.deepEqual(source().body.actualRun,original,'cross-controller destination ID conflict cannot stop source');
|
|
|
|
const move=message('move',f.host,f.agent,source().revision,{target:dest().host,targetRevision:dest().revision,selection:dest().body.selectedNext});
|
|
assert.equal((await send(move)).state,'completed');
|
|
await until(()=>dest().body.phase==='running'&&source().body.phase==='stopped');
|
|
assert.equal(source().body.actualRun,null);assert.notDeepEqual(dest().body.actualRun,original);
|
|
assert.equal(dest().body.assignedHost,dest().host);
|
|
const chain=dest().body.assignmentChain as Grant[];assert.equal(chain.length,1);assert.ok(chain[0]!.proof);
|
|
const before=JSON.stringify(dest().body.actualRun);
|
|
c.reconcile();await delay(250);assert.equal(JSON.stringify(dest().body.actualRun),before);
|
|
assert.throws(()=>c.submit(move),/already prepared/);
|
|
assert.equal((await send(message('start',f.host,f.agent,source().revision))).state,'failed');
|
|
const reverse=message('move',dest().host,f.agent,dest().revision,{target:f.host,targetRevision:source().revision,selection:source().body.selectedNext});
|
|
assert.equal((await send(reverse)).state,'completed');
|
|
await until(()=>source().body.phase==='running'&&dest().body.phase==='stopped');
|
|
assert.equal((source().body.assignmentChain as Grant[]).length,2);
|
|
assert.equal((await send(message('stop',f.host,f.agent,source().revision))).state,'completed');
|
|
assert.equal(c.status().find(s=>s.request.id===move.id)!.state,'completed','later Stop/Move cannot rewrite historical completion');
|
|
c.close();
|
|
const reopened=managementClient(join(home,'client'),f.url,f.ownerSecret,()=>{},()=>{},{catalog:{version:1,owner:publicKey(f.ownerSecret),relay:f.url,registrations:[]}});
|
|
t.after(()=>reopened.close());await reopened.ready;
|
|
assert.equal(reopened.status().find(s=>s.request.id===move.id)!.state,'completed','durable destination receipt survives controller reopen');
|
|
assert.ok(f.relay.independentWraps>0);assert.deepEqual(f.errors,[]);
|
|
});
|
|
|
|
test('unseen successors require exact source/relay signature and authenticated pair',()=>{
|
|
const secret=newKey(),source=publicKey(secret),owner=publicKey(newKey()),agent=publicKey(newKey()),target=publicKey(newKey());
|
|
const genesis=createGenesis(owner,agent,source), current:Assignment={genesis,assignedHost:source};
|
|
const selection={model:'fixture',workspace:'/tmp',profile:'default'};
|
|
const op=message('move',source,agent,0,{target,targetRevision:0,selection});
|
|
const unsigned:Grant={root:hash(genesis),predecessor:hash(genesis),source,target,agent,operation:op,prepared:'token',selection,targetRevision:0,sourceRun:null};
|
|
const grant=signSuccessor(unsigned,secret,'wss://fixture.invalid');
|
|
const next:Assignment={genesis,assignedHost:target,chain:[grant]};
|
|
verifySuccessors(current,next,'wss://fixture.invalid');
|
|
assert.throws(()=>verifySuccessors(current,{...next,chain:[unsigned]},'wss://fixture.invalid'),/Unsigned/);
|
|
assert.throws(()=>verifySuccessors(current,next,'wss://other.invalid'),/invalid successor/);
|
|
assert.throws(()=>verifySuccessors(current,{...next,chain:[{...grant,sourceRun:'forged'}]},'wss://fixture.invalid'),/invalid successor/);
|
|
const m=message('grant',target,agent,0,{assignment:next});
|
|
assert.equal(privateExchange({sender:source,message:m},owner,target),m);
|
|
assert.throws(()=>privateExchange({sender:target,message:m},owner,target),/authority/);
|
|
assert.throws(()=>privateExchange({sender:source,message:m},publicKey(newKey()),target),/authority/);
|
|
});
|
|
|
|
test('private destination failure and stale CAS preserve source; cancelled late preparation cannot transfer; lost grant recovers once',async t=>{
|
|
const home=realpathSync(mkdtempSync(join(tmpdir(),'beehive-pairing-cli-private-move-faults-')));
|
|
const f=await registrationFixture(home,()=>{},true,true);
|
|
const inventory=new Map<string,Message>();
|
|
const c=managementClient(join(home,'client'),f.url,f.ownerSecret,m=>{if(m.type==='inventory')inventory.set(m.host,m);},()=>{},{catalog:{version:1,owner:publicKey(f.ownerSecret),relay:f.url,registrations:[]}});
|
|
t.after(async()=>{c.close();await f.close();rmSync(home,{recursive:true,force:true});});
|
|
await c.ready;await until(()=>inventory.size===2);
|
|
const source=()=>inventory.get(f.host)!,dest=()=>inventory.get(f.destination!.host)!;
|
|
const outcome=async(m:Message)=>{c.submit(m);await until(()=>c.status().some(s=>s.request.id===m.id&&s.state!=='pending'));return c.status().find(s=>s.request.id===m.id)!;};
|
|
await outcome(message('start',f.host,f.agent,source().revision));await until(()=>source().body.phase==='running');
|
|
const actual=source().body.actualRun;
|
|
const move=()=>message('move',f.host,f.agent,source().revision,{target:dest().host,targetRevision:dest().revision,selection:dest().body.selectedNext});
|
|
const stale=move();stale.body.targetRevision=100;
|
|
assert.equal((await outcome(stale)).state,'failed');assert.deepEqual(source().body.actualRun,actual);
|
|
const {credentialReference}=await import('../src/credential-store.ts');
|
|
const key=credentialReference('agent',f.agent),secret=f.destination!.credentials.read(key)!;
|
|
f.destination!.credentials.remove(key);
|
|
assert.equal((await outcome(move())).state,'failed');assert.deepEqual(source().body.actualRun,actual);
|
|
f.destination!.credentials.create(key,secret);
|
|
f.setHoldPrepared(true);const cancelled=move();c.submit(cancelled);await until(()=>f.heldPrepared>0);
|
|
await outcome(message('stop',f.host,f.agent,source().revision));f.releasePrepared();await until(()=>source().body.phase==='stopped');await delay(100);
|
|
assert.equal(source().body.assignedHost,f.host);assert.equal(dest().body.phase,'stopped');
|
|
assert.equal(c.status().find(s=>s.request.id===cancelled.id)!.state,'failed');
|
|
await until(()=>dest().revision>0);
|
|
f.setDropGrant(true);const recover=move();c.submit(recover);
|
|
await until(()=>source().body.assignedHost===dest().host);
|
|
assert.equal(dest().body.phase,'stopped','lost grant does not manufacture destination authority');
|
|
assert.equal(c.status().find(s=>s.request.id===recover.id)!.state,'pending');
|
|
f.setDropGrant(false);c.reconcile();await until(()=>dest().body.phase==='running');
|
|
const run=dest().body.actualRun;c.reconcile();await delay(200);assert.deepEqual(dest().body.actualRun,run);
|
|
f.setDropGrant(true);
|
|
const reverse=message('move',dest().host,f.agent,dest().revision,{target:source().host,targetRevision:source().revision,selection:source().body.selectedNext});
|
|
c.submit(reverse);await until(()=>dest().body.assignedHost===source().host);
|
|
f.credentials.remove(key);f.setDropGrant(false);c.reconcile();
|
|
await until(()=>c.status().find(s=>s.request.id===reverse.id)?.state==='failed');
|
|
await until(()=>source().body.assignedHost===source().host&&source().body.phase==='stopped');
|
|
assert.equal(source().body.actualRun,null);assert.equal(dest().body.actualRun,null);
|
|
f.credentials.create(key,secret);
|
|
assert.equal((await outcome(message('start',source().host,f.agent,source().revision))).state,'completed');
|
|
|
|
});
|
|
|
|
test('relay-listed agent Move uses typed submission and observes destination before completion',async t=>{
|
|
const home=realpathSync(mkdtempSync(join(tmpdir(),'beehive-pairing-cli-private-move-manager-')));
|
|
let snapshot: import('../src/manager-controller.ts').ManagerSnapshot;
|
|
const f=await registrationFixture(home,s=>{snapshot=s;},true,true);
|
|
t.after(async()=>{await f.close();rmSync(home,{recursive:true,force:true});});
|
|
let id=0;const request=(action:string,values?:Record<string,string>,target?:string,revision?:number)=>f.controller.request({id:++id,action,values,target,revision});
|
|
await request('signin');const row=()=>snapshot!.agents.find(a=>a.id===f.agent)!;
|
|
await until(()=>!!row()?.target&&!!row()?.destinations?.length);
|
|
f.setDropGrant(true);
|
|
const d=row().destinations![0]!;
|
|
const r=await request('move',{destination:d.target,destinationRevision:String(d.revision)},row().target,row().revision);
|
|
assert.equal(r.state,'submitted',snapshot!.status);if(r.state!=='submitted')return;
|
|
await until(()=>snapshot!.diagnostics!.some(o=>o.id===`operation:${r.operationId}`&&o.label.includes('Pending')));
|
|
f.setDropGrant(false);await request('reconcile');
|
|
await until(()=>row().label.includes('running')&&row().target===d.target);
|
|
await until(()=>snapshot!.diagnostics!.some(o=>o.id===`operation:${r.operationId}`&&o.detail.includes('Move complete; destination running')));
|
|
assert.ok(!JSON.stringify(snapshot!).includes(f.nsec));
|
|
});
|
|
|
|
test('failed owned source Stop retains quarantine, actual run and source authority; explicit repaired Stop recovers without destination execution',async t=>{
|
|
const home=realpathSync(mkdtempSync(join(tmpdir(),'beehive-pairing-cli-private-move-stop-')));
|
|
const f=await registrationFixture(home,()=>{},true,true),inventory=new Map<string,Message>();
|
|
const c=managementClient(join(home,'client'),f.url,f.ownerSecret,m=>{if(m.type==='inventory')inventory.set(m.host,m);},()=>{},{catalog:{version:1,owner:publicKey(f.ownerSecret),relay:f.url,registrations:[]}});
|
|
t.after(async()=>{stopFault.active=false;c.close();await f.close();rmSync(home,{recursive:true,force:true});});
|
|
await c.ready;await until(()=>inventory.size===2);
|
|
const source=()=>inventory.get(f.host)!,dest=()=>inventory.get(f.destination!.host)!;
|
|
const outcome=async(m:Message)=>{c.submit(m);await until(()=>c.status().some(s=>s.request.id===m.id&&s.state!=='pending'));return c.status().find(s=>s.request.id===m.id)!;};
|
|
await outcome(message('start',f.host,f.agent,source().revision));await until(()=>source().body.phase==='running');
|
|
const actual=source().body.actualRun;stopFault.active=true;
|
|
const move=message('move',f.host,f.agent,source().revision,{target:dest().host,targetRevision:dest().revision,selection:dest().body.selectedNext});
|
|
assert.equal((await outcome(move)).state,'failed');
|
|
await until(()=>source().body.phase==='quarantined');
|
|
assert.deepEqual(source().body.actualRun,actual);assert.equal(source().body.assignedHost,f.host);assert.equal(dest().body.phase,'stopped');
|
|
stopFault.active=false;assert.equal((await outcome(message('stop',f.host,f.agent,source().revision))).state,'completed');
|
|
await until(()=>source().body.phase==='stopped');assert.equal(source().body.actualRun,null);assert.equal(dest().body.actualRun,null);
|
|
});
|