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 {registerAgentProfile,readAgentProfileByKey,checkAgentProfile,registerParticipantProfile,registerChannelParticipant,readParticipantProfile,checkParticipantProfile,findParticipantProfiles,listIdentityProfiles,adoptLegacyAgentProfile,bindOwnAgent,bindRegisteredParticipant,redactIdentity,executeIdentity} from '../skills/ksf-workshop-runtime/scripts/ksf-identity.mjs'; import {runWorkshop} from '../skills/ksf-workshop-runtime/scripts/ksf-workshop.mjs'; import {registerBridgeIdentity} from '../skills/ksf-workshop-runtime/scripts/ksf-identity-bridge.mjs'; const agentInput={name:'新数字人',role:'workshop',identityBasis:'operator_registration',runtimePlatform:'hermes',channel:'wecom',account:{platform:'wecom',tenantId:null,accountId:null,accountName:null,runtimeAgentId:null,wecomAppAgentId:null}}; const employeeInput={name:'新员工',identityBasis:'participant_registration',account:{platform:'wecom',tenantId:null,accountId:null,accountName:null,employeeNumber:null}}; function directory(t){const dir=fs.mkdtempSync(path.join(os.tmpdir(),'ksf-identity-test-'));t.after(()=>{assert.equal(path.dirname(dir),fs.realpathSync(os.tmpdir()));assert.ok(path.basename(dir).startsWith('ksf-identity-test-'));fs.rmSync(dir,{recursive:true});});return dir;} const dirs=root=>({profileDir:path.join(root,'agents'),participantDir:path.join(root,'participants')}); test('新 Agent 自助登记生成通用稳定 UUID、同资料重用,密钥字段拒绝',t=>{ const root=directory(t),opts=dirs(root),a=registerAgentProfile(agentInput,opts),before=fs.readFileSync(path.join(opts.profileDir,a.profile.key.split(':')[1]+'.json')); assert.match(a.profile.key,/^agent-registration:/);assert.equal(a.changed,true);assert.equal(registerAgentProfile({...agentInput,reuseKey:a.profile.key},opts).changed,false);assert.equal(registerAgentProfile(agentInput,opts).needsReview,true);assert.equal(readAgentProfileByKey(a.profile.key,opts.profileDir).name,'新数字人'); assert.equal(checkAgentProfile(a.profile.key,{...agentInput,collectedAt:'2026-11-02T02:00:00.000Z'},opts).matches,true); assert.deepEqual(fs.readFileSync(path.join(opts.profileDir,a.profile.key.split(':')[1]+'.json')),before); const duplicate=registerAgentProfile({...agentInput,name:'新数字人',account:{...agentInput.account,accountName:'其他账号'}},opts);assert.equal(duplicate.needsReview,true);assert.equal(duplicate.candidates.length,1); assert.throws(()=>registerAgentProfile({...agentInput,account:{...agentInput.account,token:'fixture'}},opts),{code:'INVALID_IDENTITY_FIELDS'}); const separate=registerAgentProfile({...agentInput,confirmNew:true},opts);assert.equal(separate.changed,true);assert.notEqual(separate.profile.key,a.profile.key); }); test('真实 Agent ID 生成可校验的账号摘要;稳定 ID 冲突拒绝',t=>{ const opts=dirs(directory(t)),input={...agentInput,identityBasis:'runtime_config',account:{...agentInput.account,accountId:'fixture-agent-account'}}; const p=registerAgentProfile(input,opts).profile;assert.match(p.key,/^agent-account:[a-f0-9]{64}$/);assert.equal(readAgentProfileByKey(p.key,opts.profileDir).key,p.key); assert.throws(()=>registerAgentProfile({...input,name:'changed'},opts),{code:'AGENT_PROFILE_CONFLICT'}); assert.throws(()=>checkAgentProfile(p.key,{...input,runtimePlatform:'changed'},opts),{code:'AGENT_PROFILE_CONFLICT'}); }); test('可信来源必须有稳定 ID,篡改 UUID 档案的来源不能冒充运行配置',t=>{ const opts=dirs(directory(t)); for(const identityBasis of ['runtime_config','channel_context'])assert.throws(()=>registerAgentProfile({...agentInput,identityBasis},opts),{code:'RUNTIME_AGENT_ID_REQUIRED'}); for(const identityBasis of ['channel_context','existing_record'])assert.throws(()=>registerParticipantProfile({...employeeInput,identityBasis},opts),{code:'STABLE_EMPLOYEE_ID_REQUIRED'}); assert.throws(()=>registerParticipantProfile({...employeeInput,identityBasis:'channel_context',account:{...employeeInput.account,accountId:'user-without-corp'}},opts),{code:'STABLE_EMPLOYEE_ID_REQUIRED'}); const agent=registerAgentProfile(agentInput,opts).profile,file=path.join(opts.profileDir,agent.key.split(':')[1]+'.json'),row=JSON.parse(fs.readFileSync(file,'utf8')); row.identityBasis='runtime_config';row.account.runtimeAgentId='actual-looking-id';fs.writeFileSync(file,JSON.stringify(row)); assert.throws(()=>readAgentProfileByKey(agent.key,opts.profileDir),{code:'INVALID_IDENTITY_FILE'}); const employee=registerParticipantProfile(employeeInput,opts).profile,employeeFile=path.join(opts.participantDir,employee.key.split(':')[1]+'.json'),person=JSON.parse(fs.readFileSync(employeeFile,'utf8')); person.identityBasis='channel_context';person.account.tenantId='test-corp';person.account.accountId='test-user';fs.writeFileSync(employeeFile,JSON.stringify(person)); assert.throws(()=>readParticipantProfile(employee.key,opts.participantDir),{code:'INVALID_IDENTITY_FILE'}); }); test('任意数量同角色数字人可共存,旧档案迁入不覆盖原件,CLI仅输出摘要',t=>{ const root=directory(t),opts=dirs(root),first=registerAgentProfile(agentInput,opts).profile; const second=registerAgentProfile({...agentInput,name:'另一个产品数字人'},opts).profile;assert.notEqual(first.key,second.key); const legacy=path.join(root,'agent-self-workshop.json'),original=fs.readFileSync(path.join(opts.profileDir,first.key.split(':')[1]+'.json'));fs.writeFileSync(legacy,original,{mode:0o600}); const next={profileDir:path.join(root,'migrated')};assert.equal(adoptLegacyAgentProfile(legacy,next).changed,true);assert.equal(adoptLegacyAgentProfile(legacy,next).changed,false);assert.deepEqual(fs.readFileSync(legacy),original); const input=path.join(root,'summary.json');fs.writeFileSync(input,JSON.stringify({...employeeInput,account:{...employeeInput.account,accountName:'private-account-name'}})); const out=executeIdentity(['register-employee','--input',input],opts);assert.equal(out.databaseWrites,0);assert.equal(out.networkRequests,0);assert.equal(out.accountAuthenticated,false);assert.ok(!JSON.stringify(out).includes('private-account-name')); const status=executeIdentity(['status'],opts);assert.equal(status.agents.length,2);assert.equal(status.participants.length,1); assert.throws(()=>registerParticipantProfile({...employeeInput,reuseKey:out.profile.key,confirmNew:true},opts),{code:'INVALID_IDENTITY_VALUE'}); assert.throws(()=>readAgentProfileByKey('agent-registration:../../invalid',opts.profileDir),{code:'INVALID_AGENT_KEY'}); }); test('新员工先登记;稳定账号自动复用,无账号同名需人工确认',t=>{ const opts=dirs(directory(t)),first=registerParticipantProfile(employeeInput,opts);assert.equal(first.changed,true);assert.match(first.profile.key,/^participant-registration:/); const candidates=findParticipantProfiles('新员工',opts);assert.equal(candidates.length,1);assert.ok(!JSON.stringify(candidates).includes('accountId')); const pending=registerParticipantProfile(employeeInput,opts);assert.equal(pending.needsReview,true);assert.equal(pending.candidates[0].key,first.profile.key); const reused=registerParticipantProfile({...employeeInput,reuseKey:first.profile.key},opts);assert.equal(reused.changed,false);assert.equal(reused.profile.key,first.profile.key); const other=registerParticipantProfile({...employeeInput,confirmNew:true},opts);assert.notEqual(other.profile.key,first.profile.key); const stable={...employeeInput,name:'有账号员工',identityBasis:'channel_context',account:{...employeeInput.account,tenantId:'corp-test',accountId:'wecom-user-7'}}; const a=registerParticipantProfile(stable,opts),b=registerParticipantProfile(stable,opts);assert.equal(a.profile.key,b.profile.key);assert.equal(b.changed,false); assert.throws(()=>registerParticipantProfile({...stable,name:'不同显示名'},opts),{code:'PARTICIPANT_PROFILE_CONFLICT'}); assert.equal(checkParticipantProfile(first.profile.key,employeeInput,opts).matches,true); }); test('workshop-start 必须引用登记的员工和 Agent 档案,并注入字段快照',async t=>{ const opts=dirs(directory(t)),agentRow=registerAgentProfile(agentInput,opts).profile,employeeRow=registerParticipantProfile(employeeInput,opts).profile; const agent=readAgentProfileByKey(agentRow.key,opts.profileDir),participant=readParticipantProfile(employeeRow.key,opts.participantDir),calls=[]; const call=async(name,params)=>{calls.push({name,params});return name==='ksfGetIngestContract'?{workshop:{identitySchema:'ksf.agent-self.v1'}}:{sessionId:'session'};}; await assert.rejects(()=>runWorkshop('workshop-start',{participant:{profileKey:participant.key}},{requestId:'missing-agent',participantProfile:participant,call}),{code:'AGENT_PROFILE_REQUIRED'}); await assert.rejects(()=>runWorkshop('workshop-start',{}, {requestId:'missing-employee',agentProfile:agent,call}),{code:'PARTICIPANT_PROFILE_REQUIRED'});assert.equal(calls.length,0); const input={workshopKey:'w',participant:{profileKey:participant.key},conversation:{key:'conversation-1',channel:'wecom'},groupId:'',dataMode:'sample'}; await runWorkshop('workshop-start',input,{requestId:'start',agentProfile:agent,participantProfile:participant,call}); const start=calls.find(x=>x.name==='ksfStartWorkshopSession').params;assert.deepEqual(start.agent,bindOwnAgent({},agent).agent);assert.deepEqual(start.participant,bindRegisteredParticipant({profileKey:participant.key},participant));assert.equal(start.dataMode,'sample'); calls.length=0;await assert.rejects(()=>runWorkshop('workshop-start',{...input,agent:{name:'别的数字人'}},{requestId:'fake',agentProfile:agent,participantProfile:participant,call}),{code:'AGENT_IDENTITY_MISMATCH'});assert.equal(calls.length,0); await assert.rejects(()=>runWorkshop('workshop-start',input,{requestId:'ops',agentProfile:{...agent,role:'operations'},participantProfile:participant,call}),{code:'AGENT_ROLE_MISMATCH'});assert.equal(calls.length,0); }); test('旧服务不支持身份版本时会话写入前停止',async t=>{ const opts=dirs(directory(t)),agentRow=registerAgentProfile(agentInput,opts).profile,employeeRow=registerParticipantProfile(employeeInput,opts).profile,calls=[]; const agent=readAgentProfileByKey(agentRow.key,opts.profileDir),employee=readParticipantProfile(employeeRow.key,opts.participantDir); await assert.rejects(()=>runWorkshop('workshop-start',{participant:{profileKey:employee.key}},{requestId:'old-service',agentProfile:agent,participantProfile:employee,call:async name=>{calls.push(name);return{workshop:{version:'old'}};}}),{code:'IDENTITY_CAPABILITY_UNAVAILABLE'});assert.deepEqual(calls,['ksfGetIngestContract']); await assert.rejects(()=>runWorkshop('workshop-start',{},{requestId:'no-agent',participantProfile:employee,call:async()=>{throw Error('must not call');}}),{code:'AGENT_PROFILE_REQUIRED'}); }); test('只读回读脱敏账号与会话键,公开producer不携带账号值',async t=>{ const opts=dirs(directory(t)),agentRow=registerAgentProfile({...agentInput,account:{...agentInput.account,accountId:'private-account'}},opts).profile,agent=readAgentProfileByKey(agentRow.key,opts.profileDir); const result=await runWorkshop('workshop-read',{className:'KfArtifact',objectId:'artifact'},{requestId:'read',call:async(n,params)=>{assert.equal(n,'ksfGetRecord');assert.equal(params.objectId,'artifact');return{record:{title:'报告',agentName:agent.name,userRef:{objectId:'private-user'},meta:{ownerId:'private-service',attribution:{agentName:agent.name,agentAccount:{accountId:'private-account'},participantAccount:{accountId:'private-person'},conversationKey:'private-chat'}}}};}}); assert.equal(result.record.title,'报告');assert.ok(!JSON.stringify(result).includes('private-'));assert.deepEqual(redactIdentity({producer:{agent:{name:agent.name,key:agent.key,identityBasis:agent.identityBasis,account:{accountId:'private-account'}}}}).producer.agent,{name:agent.name,key:agent.key,identityBasis:agent.identityBasis}); assert.equal(listIdentityProfiles(opts).agents.length,1); }); const channelContext={version:'ksf.wecom-aibot-context.v1',botId:'private-bot-one',senderId:'private-sender-one',chatType:'single',chatId:null,messageId:'message-one'}; function channelAgent(opts,botId=channelContext.botId){const a=registerAgentProfile({...agentInput,name:botId==='private-bot-one'?'王刚001':'运维001',identityBasis:'runtime_config',account:{platform:'wecom',accountId:botId}},opts).profile;return readAgentProfileByKey(a.key,opts.profileDir);} test('aibot 无企业ID或姓名也能按发送者自动登记,单聊换群不改变员工键',t=>{ const opts=dirs(directory(t)),agentProfile=channelAgent(opts); const first=registerChannelParticipant(channelContext,{...opts,agentProfile});assert.equal(first.changed,true);assert.equal(first.needsNameBinding,true);assert.match(first.profile.name,/^员工待绑定·/); const row=readParticipantProfile(first.profile.key,opts.participantDir);assert.equal(row.account.tenantId,null);assert.equal(row.userId,null);assert.equal(row.account.accountId,channelContext.senderId);assert.equal(row.account.scopeId,channelContext.botId);assert.equal(row.account.identifierType,'wecom_aibot_sender'); const filename=path.join(opts.participantDir,row.key.split(':')[1]+'.json'),before=fs.readFileSync(filename); const again=registerChannelParticipant({...channelContext,chatType:'group',chatId:'private-group',messageId:'message-two'},{...opts,agentProfile});assert.equal(again.changed,false);assert.equal(again.profile.key,first.profile.key);assert.deepEqual(fs.readFileSync(filename),before); assert.equal(first.channelContextOriginVerified,false);assert.ok(!JSON.stringify(first).includes('private-')); }); test('同一群里不同发送者分开;跨机器人标识不擅自合并',t=>{ const opts=dirs(directory(t)),agentProfile=channelAgent(opts),a=registerChannelParticipant({...channelContext,chatType:'group',chatId:'private-group'},{...opts,agentProfile}); const b=registerChannelParticipant({...channelContext,senderId:'private-sender-two',chatType:'group',chatId:'private-group'},{...opts,agentProfile});assert.notEqual(a.profile.key,b.profile.key); const other=channelAgent(opts,'private-bot-two'),c=registerChannelParticipant({...channelContext,botId:'private-bot-two'},{...opts,agentProfile:other});assert.notEqual(a.profile.key,c.profile.key);assert.equal(listIdentityProfiles(opts).participants.length,3); }); test('aibot 缺发送者/机器人/群会话ID或机器人不匹配在登记前停止',t=>{ const opts=dirs(directory(t)),agentProfile=channelAgent(opts); for(const bad of [{...channelContext,senderId:''},{...channelContext,botId:''},{...channelContext,chatType:'group',chatId:null},{...channelContext,displayName:'猜出的姓名'},{...channelContext,rawText:'不得从正文提取身份'}])assert.throws(()=>registerChannelParticipant(bad,{...opts,agentProfile})); assert.throws(()=>registerChannelParticipant({...channelContext,botId:'different-bot'},{...opts,agentProfile}),{code:'CHANNEL_AGENT_MISMATCH'}); assert.equal(listIdentityProfiles(opts).participants.length,0); }); test('aibot 旧文件 CLI 已关闭;旧合同与错机器人阻止会话',async t=>{ const root=directory(t),opts=dirs(root),agentProfile=channelAgent(opts),file=path.join(root,'inbound.json');fs.writeFileSync(file,JSON.stringify(channelContext),{mode:0o600}); for(const argv of [[],['--context',file],['--agent-profile',agentProfile.key,'--context',file],['--message-id','made-up']])assert.throws(()=>executeIdentity(['register-channel-employee',...argv],opts),{code:'TRUSTED_IDENTITY_TOOL_REQUIRED'}); assert.equal(listIdentityProfiles(opts).participants.length,0); const result=registerBridgeIdentity({version:'ksf.current-employee-registration.v1',agentProfileKey:agentProfile.key,identity:channelContext},opts);assert.ok(!JSON.stringify(result).includes('private-'));assert.equal(result.databaseWrites,0);assert.equal(result.accountAuthenticated,false); const participantProfile=readParticipantProfile(result.profile.key,opts.participantDir),task={participant:{profileKey:participantProfile.key},conversation:{key:'private-chat',channel:'wecom'}},calls=[]; const options={requestId:'channel-start',agentProfile,participantProfile,call:async name=>{calls.push(name);return{workshop:{identitySchema:'ksf.agent-self.v1'}};}}; await assert.rejects(()=>runWorkshop('workshop-start',task,options),{code:'CHANNEL_IDENTITY_CAPABILITY_UNAVAILABLE'});assert.deepEqual(calls,['ksfGetIngestContract']); const call=async name=>{calls.push(name);return{workshop:{identitySchema:'ksf.agent-self.v1',channelIdentitySchema:'ksf.wecom-aibot-sender.v1'}};}; calls.length=0;await assert.rejects(()=>runWorkshop('workshop-start',task,{...options,call,agentProfile:channelAgent(opts,'private-bot-two')}),{code:'CHANNEL_AGENT_MISMATCH'});assert.deepEqual(calls,['ksfGetIngestContract']); assert.deepEqual(redactIdentity({scopeId:'private-bot',botId:'private-bot',senderId:'private-user',name:'显示标签'}),{name:'显示标签'}); }); test('通用员工 JSON 不能替代通道工具;内部登记复用规范键且拒绝错实例与运维角色',t=>{ const root=directory(t),opts=dirs(root),agentProfile=channelAgent(opts),input=path.join(root,'input.json'); for(const value of [ {...employeeInput,identityBasis:'channel_context',account:{platform:'wecom',tenantId:'corp',accountId:'sender'}}, {...employeeInput,account:{platform:'wecom',identifierType:'wecom_aibot_sender',scopeId:'bot',accountId:'sender'}} ]){fs.writeFileSync(input,JSON.stringify(value));assert.throws(()=>executeIdentity(['register-employee','--input',input],opts),{code:'TRUSTED_IDENTITY_TOOL_REQUIRED'});} assert.equal(listIdentityProfiles(opts).participants.length,0); const request={version:'ksf.current-employee-registration.v1',agentProfileKey:agentProfile.key,identity:channelContext}; const a=registerBridgeIdentity(request,opts),b=registerBridgeIdentity({...request,identity:{...channelContext,messageId:'another-message',chatType:'group',chatId:'group'}},opts); assert.equal(a.profile.key,b.profile.key);assert.equal(a.changed,true);assert.equal(b.changed,false); assert.throws(()=>registerBridgeIdentity({...request,messageId:'extra-field'},opts),{code:'INVALID_BRIDGE_REQUEST'}); assert.throws(()=>registerBridgeIdentity({...request,identity:{...channelContext,botId:'other-bot'}},opts),{code:'CHANNEL_AGENT_MISMATCH'}); const ops=registerAgentProfile({...agentInput,name:'运维001',role:'operations',account:{platform:'wecom',accountId:'operations-bot'}},opts).profile; assert.throws(()=>registerBridgeIdentity({...request,agentProfileKey:ops.key},opts),{code:'AGENT_ROLE_MISMATCH'}); assert.equal(listIdentityProfiles(opts).participants.length,1); }); test('上传实名与待同步回执均保留;成功保存不因实名状态再次调用',async t=>{ const opts=dirs(directory(t)),agentProfile=channelAgent(opts); const verified={employee:{key:'employee:'+'a'.repeat(64),name:'测试员工',departmentNames:['测试产品部'],source:'wecom_directory',nameVerified:true,verifiedAt:'2026-11-03T00:00:00.000Z'},employeeIdentity:{automatic:true,status:'verified'}}; const pending={employee:{key:'channel-employee:'+'b'.repeat(64),name:null,source:'wecom_sender',nameVerified:false},employeeIdentity:{automatic:true,status:'pending',reason:'WECOM_DIRECTORY_NOT_CONFIGURED'}}; for(const receipt of [verified,pending,{}]){ const calls=[]; const result=await runWorkshop('workshop-save',{sessionId:'original-session',taskKey:'original-task',version:1},{requestId:'original-save',agentProfile,call:async(name,params)=>{ calls.push({name,params}); if(name==='ksfGetRecord')return{record:{meta:{attribution:{agentKey:agentProfile.key,agentName:agentProfile.name,agentRuntimePlatform:agentProfile.runtimePlatform,agentChannel:agentProfile.channel}}}}; assert.equal(name,'ksfSaveWorkshopTask');assert.equal(params.requestId,'original-save'); assert.equal(params.sessionId,'original-session');assert.equal(params.taskKey,'original-task'); return{artifactId:'saved-artifact',...receipt,meta:{attribution:{participantAccount:{accountId:'private-sender'},agentAccount:{accountId:'private-bot'}}}}; }}); assert.equal(result.artifactId,'saved-artifact');assert.deepEqual(result.employee,receipt.employee);assert.deepEqual(result.employeeIdentity,receipt.employeeIdentity); assert.deepEqual(calls.map(x=>x.name),['ksfGetRecord','ksfSaveWorkshopTask']);assert.ok(!JSON.stringify(result).includes('private-')); } }); test('通讯录查询失败透传并停止,Skill运行库不换键重试保存',async t=>{ const opts=dirs(directory(t)),agentProfile=channelAgent(opts),calls=[]; await assert.rejects(()=>runWorkshop('workshop-save',{sessionId:'original-session',taskKey:'original-task'},{requestId:'original-save',agentProfile,call:async(name,params)=>{ calls.push({name,params}); if(name==='ksfGetRecord')return{record:{meta:{attribution:{agentKey:agentProfile.key,agentName:agentProfile.name,agentRuntimePlatform:agentProfile.runtimePlatform,agentChannel:agentProfile.channel}}}}; throw Object.assign(Error('fixture failure'),{code:'WECOM_SENDER_MAPPING_REQUIRED'}); }}),{code:'WECOM_SENDER_MAPPING_REQUIRED'}); assert.deepEqual(calls.map(x=>x.name),['ksfGetRecord','ksfSaveWorkshopTask']);assert.equal(calls[1].params.requestId,'original-save'); });