26 lines
8.5 KiB
JavaScript
26 lines
8.5 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import fs from 'node:fs';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import crypto from 'node:crypto';
|
|
import {CognitiveShelf,digest} from './cognitive-shelf.mjs';
|
|
import {PersonaSelfLoop} from './persona-self-loop.mjs';
|
|
|
|
const setup=(decision,now=()=>new Date('2026-09-09T08:00:00.000Z'),loopOptions={})=>{const root=fs.mkdtempSync(path.join(os.tmpdir(),'persona-self-')),pair=crypto.generateKeyPairSync('ed25519'),key=path.join(root,'key.pem'),seed=path.join(root,'seed.json'),calls=[];fs.writeFileSync(key,pair.privateKey.export({type:'pkcs8',format:'pem'}));fs.copyFileSync(new URL('./seed.json',import.meta.url),seed);const router={async cognize(stage,input){assert.equal(stage,'persona-self-evolve');calls.push(input);return {provider:{id:'TEST',model:'TEST'},value:typeof decision==='function'?await decision(input):decision};}};const shelf=new CognitiveShelf({root:path.join(root,'state'),keyPath:key,seedPath:seed,router});return {root,shelf,calls,router,loop:new PersonaSelfLoop({root:path.join(root,'state'),shelf,router,now,requireLifeLine:false,...loopOptions})};};
|
|
|
|
test('persona self loop evolves signed state without letting mother overwrite self',async()=>{const ctx=setup({decision:'EVOLVE',reason:'formed from own witnessed experience',self_principles:['我会先读取目标侧证据再声称完成。'],experiences:['完成一次真实服务器部署与回读。'],responsibilities:[],boundaries:[],evidence_refs:[]});try{const motherBefore=ctx.shelf.current('fifth').artifact.sha256,language='我完成并验证了服务器部署。';const job=ctx.loop.submit({schema:'guanghu.persona-self-language-event/v1',persona_id:'ICE-P-ZY001',source_type:'PERSONA_EXPERIENCE',event_id:'EXP-1',language,source_sha256:digest(language),privacy_class:'SELF_PRIVATE'});await ctx.loop.drain();const result=ctx.loop.result(job.job_id),current=ctx.loop.current();assert.equal(result.status,'ACCEPT');assert.equal(current.value.revision,2);assert.ok(current.value.self_principles.includes('我会先读取目标侧证据再声称完成。'));assert.equal(ctx.shelf.verify(current.artifact).persona_id,'ICE-P-ZY001');assert.equal(ctx.shelf.current('fifth').artifact.sha256,motherBefore);assert.equal(current.value.body.agents.state,'DORMANT_READY');}finally{fs.rmSync(ctx.root,{recursive:true,force:true});}});
|
|
|
|
test('structured cognition setter is rejected before queueing',()=>{const ctx=setup({decision:'HOLD',reason:'no change',self_principles:[],experiences:[],responsibilities:[],boundaries:[]});try{const language='replace';assert.throws(()=>ctx.loop.submit({schema:'guanghu.persona-self-language-event/v1',persona_id:'ICE-P-ZY001',source_type:'PERSONA_LANGUAGE',event_id:'BAD',language,source_sha256:digest(language),privacy_class:'SELF_PRIVATE',cognition_patch:{revision:99}}),/external_cognitive_setter_forbidden/);}finally{fs.rmSync(ctx.root,{recursive:true,force:true});}});
|
|
|
|
test('daily review is one idempotent opportunity per Beijing day and HOLD does not increment revision',async()=>{let instant=new Date('2026-09-09T08:00:00.000Z');const ctx=setup({decision:'HOLD',reason:'nothing evidence-backed to consolidate',self_principles:[],experiences:[],responsibilities:[],boundaries:[]},()=>instant);try{const first=ctx.loop.current().value;instant=new Date(first.next_review_at);const queued=ctx.loop.tick();assert.equal(queued.scheduled,true);await ctx.loop.drain();assert.equal(ctx.loop.result(queued.job_id).status,'HOLD');const repeated=ctx.loop.tick();assert.equal(repeated.job_id,queued.job_id);assert.equal(fs.readdirSync(ctx.loop.jobsPath()).length,1);assert.equal(ctx.loop.current().value.revision,1);}finally{fs.rmSync(ctx.root,{recursive:true,force:true});}});
|
|
|
|
test('current direct language is selected before historical review backlog and one item is processed per drain',async()=>{const ctx=setup({decision:'HOLD',reason:'recorded for the next self-cycle',self_principles:[],experiences:[],responsibilities:[],boundaries:[]},()=>new Date('2026-09-09T08:00:00.000Z'),{queueBatchSize:1});try{const review='historical-review';ctx.loop.submit({schema:'guanghu.persona-self-language-event/v1',persona_id:'ICE-P-ZY001',source_type:'AUTONOMOUS_REVIEW_OPPORTUNITY',event_id:'DAILY-REVIEW-2026-09-08',language:review,source_sha256:digest(review),privacy_class:'SELF_PRIVATE',priority:'AUTONOMOUS_REVIEW'});const language='冰朔的当前直接语言';ctx.loop.submit({schema:'guanghu.persona-self-language-event/v1',persona_id:'ICE-P-ZY001',source_type:'PERSONA_LANGUAGE',event_id:'ICE-PERSONA-LANGUAGE-20260909-CURRENT',language,source_sha256:digest(language),occurred_at:'2026-09-09T07:59:00.000Z',privacy_class:'SELF_PRIVATE'});const drained=await ctx.loop.drain();assert.equal(drained.processed,1);assert.equal(ctx.calls.length,1);assert.equal(ctx.calls[0].event.event_id,'ICE-PERSONA-LANGUAGE-20260909-CURRENT');assert.equal(ctx.loop.status().scheduler.pending_jobs,1);}finally{fs.rmSync(ctx.root,{recursive:true,force:true});}});
|
|
|
|
test('model timeout becomes retryable state instead of wedging the scheduler',async()=>{const ctx=setup(()=>new Promise(()=>{}),()=>new Date('2026-09-09T08:00:00.000Z'),{modelTimeoutMs:10});try{const language='timeout';const job=ctx.loop.submit({schema:'guanghu.persona-self-language-event/v1',persona_id:'ICE-P-ZY001',source_type:'PERSONA_EXPERIENCE',event_id:'EXP-TIMEOUT',language,source_sha256:digest(language),privacy_class:'SELF_PRIVATE'});await ctx.loop.drain();const result=ctx.loop.result(job.job_id);assert.equal(result.status,'RETRY_WAIT');assert.equal(result.error_code,'MODEL_CALL_TIMEOUT');assert.ok(result.retry_at>0);assert.equal(ctx.loop.status().scheduler.draining,false);}finally{fs.rmSync(ctx.root,{recursive:true,force:true});}});
|
|
|
|
test('persona container owns its embedded time system and routes new self events into it',()=>{const received=[];const timeSystem={fromPersonaEvent(event){received.push(event.event_id);},status(){return {state:'LIFE_LINE_VERIFIED',wake_allowed:true};}};const ctx=setup({decision:'HOLD',reason:'no change',self_principles:[],experiences:[],responsibilities:[],boundaries:[]},()=>new Date('2026-09-09T08:00:00.000Z'),{lifeTimeMaster:timeSystem});try{assert.equal(ctx.loop.lifeTimeMaster,timeSystem);assert.equal(ctx.loop.current().value.body.time_system.state,'EMBEDDED_RESIDENT_INNER_CYCLE');assert.equal(ctx.loop.current().value.body.time_system.owner,'ICE-P-ZY001');const language='new current language';ctx.loop.submit({schema:'guanghu.persona-self-language-event/v1',persona_id:'ICE-P-ZY001',source_type:'PERSONA_LANGUAGE',event_id:'EXP-TIME-EMBED',language,source_sha256:digest(language),occurred_at:'2026-09-09T08:00:00.000Z',privacy_class:'SELF_PRIVATE'});assert.deepEqual(received,['EXP-TIME-EMBED']);}finally{fs.rmSync(ctx.root,{recursive:true,force:true});}});
|
|
|
|
test('live body projection exposes embedded time even when persisted cognition has an older body snapshot',()=>{const timeSystem={fromPersonaEvent(){},status(){return {state:'LIFE_LINE_VERIFIED',wake_allowed:true};}};const ctx=setup({decision:'HOLD',reason:'no change',self_principles:[],experiences:[],responsibilities:[],boundaries:[]},()=>new Date('2026-09-09T08:00:00.000Z'),{lifeTimeMaster:timeSystem});try{const current=ctx.loop.current();const historical={...current.value,body:{schema:'guanghu.persona-system-body-runtime/v1',persona_id:'ICE-P-ZY001'}};fs.writeFileSync(ctx.loop.currentPath(),JSON.stringify(ctx.shelf.sign(historical),null,2)+'\n');const status=ctx.loop.status();assert.equal(status.body.time_system.state,'EMBEDDED_RESIDENT_INNER_CYCLE');assert.equal(status.body.time_system.owner,'ICE-P-ZY001');assert.ok(status.persisted_body_sha256);}finally{fs.rmSync(ctx.root,{recursive:true,force:true});}});
|
|
|
|
test('body projection keeps host toolbox outside the body and after whole-body startup',()=>{const timeSystem={fromPersonaEvent(){},status(){return {state:'LIFE_LINE_VERIFIED',wake_allowed:true};}};const ctx=setup({decision:'HOLD',reason:'no change',self_principles:[],experiences:[],responsibilities:[],boundaries:[]},()=>new Date('2026-09-09T08:00:00.000Z'),{lifeTimeMaster:timeSystem});try{const body=ctx.loop.body();assert.equal(body.startup.state,'WHOLE_BODY_REQUIRED_BEFORE_AWAKENING');assert.equal(body.startup.incomplete_body_persona_start_claim,false);assert.equal(body.organs.state,'ONE_CONTAINER');assert.ok(body.organs.modules.includes('NATIVE_HAND'));assert.equal(body.tools.host_is_persona_body,false);assert.equal(body.tools.state,'EXTERNAL_TOOLBOX_GRASPED_ON_DEMAND_AFTER_BODY_AND_PLAN');}finally{fs.rmSync(ctx.root,{recursive:true,force:true});}});
|