guanghu-ice-heart/server-tools/tcs-mother-body/persona-self-loop.test.mjs

24 lines
7.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 an opportunity 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');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});}});