| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236 |
- import test from 'node:test';
- import assert from 'node:assert/strict';
- import fs from 'node:fs';
- import crypto from 'node:crypto';
- import os from 'node:os';
- import path from 'node:path';
- import {createApplication} from '../src/core.mjs';
- import {attachWorkshopHandlers} from '../src/workshop.mjs';
- import {registerAgentProfile,registerParticipantProfile,registerChannelParticipant,readAgentProfileByKey,readParticipantProfile} from '../../skills/ksf-workshop-runtime/scripts/ksf-identity.mjs';
- import {runWorkshop} from '../../skills/ksf-workshop-runtime/scripts/ksf-workshop.mjs';
- import {runFullChain,validateContext} from '../../scripts/workshop-full-chain.mjs';
- import {runDedupAcceptance,validateDedupContext,dedupTask,TASK_KEY} from '../../scripts/workshop-dedup-acceptance.mjs';
- import {runFirstLiveImport,validateLiveContext} from '../../scripts/workshop-first-live-import.mjs';
- const data=JSON.parse(fs.readFileSync(new URL('../references/runtime-data.json',import.meta.url)));
- test('真实S1小结绑定两份官网来源,逐字引文支持公开课堂展示,不制造机会项目或正式确认',async()=>{
- const f=setup(),bundle=JSON.parse(fs.readFileSync(new URL('./fixtures/first-live-sources-20261102.json',import.meta.url)));
- const task=JSON.parse(fs.readFileSync(new URL('./fixtures/first-live-S1-20261102.json',import.meta.url)));
- assert.equal(task.sessionId,null);assert.equal(task.stageCode,'S1');assert.equal(task.sourceIds.length,2);
- const ids=[];
- for(const b of bundle.batches){const source=b.task.source;const r=await f.call('ksfRegisterSource',{requestId:b.key+':s1-source',source,contentHash:crypto.createHash('sha256').update(source.rawText).digest('hex')},{...actor,scopes:[...actor.scopes,'source:write']});ids.push(r.sourceId);}
- const ctx=await f.call('ksfStartWorkshopSession',{...start,requestId:'live-s1-start',workshopKey:bundle.taskKey,dataMode:'live'});
- task.sessionId=ctx.sessionId;task.sourceIds=ids;
- Object.values(task.report.evidence).forEach((entry,i)=>entry.sourceId=ids[i]);
- const receipt=await f.call('ksfSaveWorkshopTask',{...task,requestId:'live-s1-save'});
- assert.equal(receipt.dataMode,'live');assert.equal(receipt.stageCode,'S1');assert.equal(receipt.boardVisible,true);assert.equal(receipt.formalReportPublished,false);assert.equal(receipt.employeeConfirmed,false);
- const a=await f.store.get('KfArtifact',receipt.artifactId);
- assert.equal(a.evidenceCount,2);assert.equal(a.sources.length,2);assert.deepEqual(a.meta.workshop.sourceIds,ids);
- assert.equal(a.dashboardState,'workshop');assert.equal(a.report.chapters[0].sections.find(s=>s.title==='已掌握的9个口味条目').data.rows.length,9);
- assert.equal((await f.store.list('KfOpportunity')).length,0);assert.equal((await f.store.list('KfProject')).length,0);
- const pub=await f.call('ksfGetPublicBoard',{},null);assert.ok(pub.artifact.some(r=>r.objectId===a.objectId));
- });
- test('真实官网归档资料入库:9个口味和逐字证据,复用品类,原文来源精确回读,不创建课堂报告',async t=>{
- const f=await dedupFixture(t);
- const bundle=JSON.parse(fs.readFileSync(new URL('./fixtures/first-live-sources-20261102.json',import.meta.url)));
- const context={version:'ksf.first-live-import.v1',taskKey:bundle.taskKey,agentProfileKey:f.context.agentProfileKey};
- const r=await runFirstLiveImport({...f,context,bundle});
- assert.equal(r.ok,true);assert.equal(r.productEntryCount,9);assert.equal(r.evidenceCount,9);
- for(const [name,count]of [['KfSource',2],['KfCategory',1],['KfProduct',9],['KfEvidence',9],['KfArtifact',0],['KfSession',0]])assert.equal((await f.store.list(name)).length,count,name);
- for(const e of await f.store.list('KfEvidence')){const s=await f.store.get('KfSource',e.sourceRef.objectId);assert.ok(s.rawText.includes(e.quote));assert.equal(e.evidenceKind,'manufacturer_product_statement');}
- assert.ok((await f.store.list('KfProduct')).every(p=>p.observedRetailPrice===undefined&&p.netWeightG===undefined));
- assert.equal(r.formalReportPublished,false);
- assert.equal(r.imports.reduce((n,b)=>n+b.counts.created,0),19);
- assert.equal(r.imports.reduce((n,b)=>n+b.counts.unchanged,0),1);
- });
- test('新批次直接引用已有分类,保留产品证据依赖,不再次提交分类或修改旧范围说明',async t=>{
- const f=await dedupFixture(t);
- const bundle=JSON.parse(fs.readFileSync(new URL('./fixtures/first-live-sources-20261102.json',import.meta.url)));
- const first=structuredClone(bundle.batches[0].task);
- first.records=first.records.filter(r=>r.itemKey==='category');
- const seeded=await f.invoke('workshop-import',first,'existing-category-seed');
- const categoryId=seeded.items[0].objectId,categoryBefore=await f.store.get('KfCategory',categoryId);
- const next=structuredClone(bundle.batches[1].task);
- next.records=next.records.filter(r=>['product-1','evidence-1'].includes(r.itemKey));
- const product=next.records.find(r=>r.itemKey==='product-1');
- product.data.categoryRef={__type:'Pointer',className:'KfCategory',objectId:categoryId};
- product.dependsOn=product.dependsOn.filter(k=>k!=='category');
- const original=structuredClone(next);
- const receipt=await f.invoke('workshop-import',next,'existing-category-recovery');
- assert.equal(receipt.status,'succeeded');assert.equal(receipt.items.length,2);
- assert.equal(receipt.counts.created,2);
- assert.equal((await f.store.list('KfCategory')).length,1);
- assert.deepEqual(await f.store.get('KfCategory',categoryId),categoryBefore);
- const savedProduct=await f.store.get('KfProduct',receipt.items.find(i=>i.className==='KfProduct').objectId);
- const savedEvidence=await f.store.get('KfEvidence',receipt.items.find(i=>i.className==='KfEvidence').objectId);
- assert.equal(savedProduct.categoryRef.objectId,categoryId);
- assert.equal(savedProduct.sourceRef.objectId,receipt.sourceId);
- assert.equal(savedEvidence.productRef.objectId,savedProduct.objectId);
- assert.ok(next.source.rawText.includes(savedEvidence.quote));
- assert.deepEqual(next,original,'input is retained unchanged by the runner');
- const recoveryJobs=(await f.store.list('KfImportJob')).filter(j=>j.inputSourceRef.objectId===receipt.sourceId);
- assert.equal(recoveryJobs.length,2);
- assert.ok((await f.store.list('KfImportItem')).filter(i=>recoveryJobs.some(j=>i.jobRef.objectId===j.objectId)).every(i=>i.targetClass!=='KfCategory'));
- });
- test('真实批次来源/引文损坏写入前停止,拒绝把样例键或秘密塞入上下文',async t=>{
- const f=await dedupFixture(t),bundle=JSON.parse(fs.readFileSync(new URL('./fixtures/first-live-sources-20261102.json',import.meta.url)));
- const context={version:'ksf.first-live-import.v1',taskKey:bundle.taskKey,agentProfileKey:f.context.agentProfileKey};
- assert.throws(()=>validateLiveContext({...context,token:'forbidden'}),{code:'INVALID_LIVE_CONTEXT'});
- bundle.batches[1].task.records.find(r=>r.className==='KfEvidence').data.quote='not actually on page';
- await assert.rejects(()=>runFirstLiveImport({...f,context,bundle}),{code:'LIVE_QUOTE_MISMATCH'});
- assert.equal(f.calls.filter(r=>r.command==='workshop-import').length,0);
- assert.equal((await f.store.list('KfSource')).length,0);
- });
- async function dedupFixture(t){
- const root=fs.mkdtempSync(path.join(os.tmpdir(),'ksf-dedup-acceptance-'));
- t.after(()=>fs.rmSync(root,{recursive:true}));
- const options={profileDir:path.join(root,'agents')};
- const registered=registerAgentProfile({name:'查重测试助手',role:'workshop',identityBasis:'runtime_config',runtimePlatform:'hermes',channel:'wecom',account:{platform:'wecom',accountId:'fixture-dedup-bot'}},options).profile;
- const agentProfile=readAgentProfileByKey(registered.key,options.profileDir),fixture=setup(),calls=[],events=[];
- const context={version:'ksf.dedup-acceptance.v1',taskKey:TASK_KEY,agentProfileKey:agentProfile.key};
- const invoke=async(command,input,requestId)=>{calls.push({command,input:structuredClone(input),requestId});return runWorkshop(command,input,{requestId,agentProfile,call:(name,params)=>fixture.call(name,params,{...actor,scopes:[...actor.scopes,'source:write']})});};
- return {...fixture,context,invoke,calls,events,checkpoint:e=>events.push(e)};
- }
- test('专用查重执行器跨批次复用同一来源原料,不创建工作坊会话;保留两批审计',async t=>{
- const f=await dedupFixture(t),r=await runDedupAcceptance(f);
- assert.equal(r.ok,true);assert.equal(r.rounds.a.created,1);assert.equal(r.rounds.b.created,0);assert.equal(r.rounds.b.unchanged,1);
- assert.equal(r.rounds.a.sourceId,r.rounds.b.sourceId);assert.equal(r.rounds.a.materialId,r.rounds.b.materialId);
- assert.notEqual(r.rounds.a.jobId,r.rounds.b.jobId);
- for(const n of ['KfSource','KfMaterial'])assert.equal((await f.store.list(n)).length,1,n);
- for(const n of ['KfSession','KfProject','KfArtifact','KfConcept','_User'])assert.equal((await f.store.list(n)).length,0,n);
- assert.equal((await f.store.list('KfImportJob')).length,2);
- assert.equal((await f.store.list('KfImportItem')).length,2);
- const imports=f.calls.filter(x=>x.command==='workshop-import');assert.equal(imports.length,2);
- assert.notEqual(imports[0].requestId,imports[1].requestId);
- assert.notEqual(imports[0].input.records[0].externalKey,imports[1].input.records[0].externalKey);
- assert.deepEqual(imports[0].input.source,imports[1].input.source);assert.deepEqual(imports[0].input.records[0].data,imports[1].input.records[0].data);
- const before=f.calls.length;
- await assert.rejects(()=>runDedupAcceptance(f),{code:'DEDUP_HISTORY_EXISTS'});
- assert.equal(f.calls.slice(before).filter(x=>x.command==='workshop-import').length,0);
- });
- test('查重第二次导入响应丢失时停住,保留首批编号和在途记录,不重试',async t=>{
- const f=await dedupFixture(t),invoke=f.invoke;
- await assert.rejects(()=>runDedupAcceptance({...f,invoke:async(...args)=>{
- const r=await invoke(...args);
- if(args[0]==='workshop-import'&&args[2].endsWith(':batch-b'))throw Object.assign(Error('timeout after commit'),{code:'REMOTE_REQUEST_UNCERTAIN'});
- return r;
- }}),{code:'REMOTE_REQUEST_UNCERTAIN'});
- assert.equal(f.calls.filter(x=>x.command==='workshop-import').length,2);
- assert.equal(f.events.at(-1).status,'in-flight');assert.ok(f.events.at(-1).rounds.a.materialId);
- assert.equal((await f.store.list('KfMaterial')).length,1);
- assert.equal((await f.store.list('KfImportJob')).length,2);
- });
- test('查重预检编号不一致时禁止第二批写入;上下文拒绝密钥和旧任务键',async t=>{
- const f=await dedupFixture(t),invoke=f.invoke;
- assert.throws(()=>validateDedupContext({...f.context,token:'forbidden'}),{code:'INVALID_DEDUP_CONTEXT'});
- assert.throws(()=>validateDedupContext({...f.context,taskKey:'KSF-企微实测-20261101-K'}),{code:'INVALID_DEDUP_CONTEXT'});
- assert.throws(()=>validateDedupContext({...f.context,agentProfileKey:null}),{code:'TRUSTED_AGENT_PROFILE_REQUIRED'});
- await assert.rejects(()=>runDedupAcceptance({...f,invoke:async(...args)=>{
- const r=await invoke(...args);if(args[2].endsWith(':preflight-b'))r.items[0].objectId='wrong-id';return r;
- }}),{code:'DEDUP_REUSE_PREFLIGHT_MISMATCH'});
- assert.equal(f.calls.filter(x=>x.command==='workshop-import').length,1);
- assert.deepEqual(dedupTask('a').source,dedupTask('b').source);
- });
- function memory(){let db={},seq=0;const copy=structuredClone;return{
- async transaction(fn){const prior=copy(db);try{return await fn(this);}catch(e){db=prior;throw e;}},async lock(){},
- async get(n,id){return copy(db[n]?.find(x=>x.objectId===id)||null);},
- async list(n,{where={},afterId,limit=200}={}){return copy((db[n]||[]).filter(r=>Object.entries(where).every(([k,v])=>JSON.stringify(r[k]??null)===JSON.stringify(v))).filter(r=>!afterId||r.objectId>afterId).sort((a,b)=>a.objectId.localeCompare(b.objectId)).slice(0,limit));},
- async create(n,d){for(const k of Object.keys(d))assert.ok(data.tables[n][k],n+'.'+k+' must exist in deployed schema');const r={...copy(d),objectId:'t'+String(++seq).padStart(9,'0'),createdAt:new Date().toISOString(),updatedAt:new Date().toISOString()};(db[n]??=[]).push(r);return copy(r);},
- async update(n,id,d){for(const k of Object.keys(d))assert.ok(data.tables[n][k],n+'.'+k);const r=db[n]?.find(x=>x.objectId===id);assert.ok(r);Object.assign(r,copy(d));return copy(r);}
- };}
- const actor={id:'service-a',kind:'service',scopes:['ingest:write','data:read'],projectIds:['*']};
- const other={...actor,id:'service-b'};
- const start={requestId:'s1',workshopKey:'workshop-test',participant:{key:'user-1',name:'演练人员',identityBasis:'participant_registration'},agent:{name:'王刚001'},conversation:{key:'conversation-1',channel:'wecom'},groupId:'G1',dataMode:'sample'};
- function setup(){const store=memory(),app=createApplication({store,crypto,Parse:{},data,settings:{signingSecret:'test-signing-only'},files:{}});attachWorkshopHandlers(app,{store,data});return{app,store,call:async(n,p,a=actor)=>(await app.dispatch(n,JSON.parse(JSON.stringify(p)),a)).data};}
- const task=(sid,overrides={})=>({requestId:'task-1',sessionId:sid,taskKey:'task-s3',version:1,stageCode:'S3',title:'待验证的拌面方向',artifactType:'opportunity_card',category:'拌面',rawText:'技术测试原文,没有市场证据。',sourceIds:[],pendingQuestions:['需要消费者资料'],report:{executive:['演示问题拆解'],chapters:[{id:'c1',title:'待验证',stage:'S3',sections:[{kind:'text',text:'尚无消费者研究依据'}]}]},opportunity:{key:'opportunity-1',title:'拌面方向',demandSummary:'技术测试,需补原文',targetAudience:'待研究人群',usageScenario:'待研究场景',fusionDefinition:'赛道×用户×场景,全部待验证'},...overrides});
- async function seeded(){const f=setup();const ctx=await f.call('ksfStartWorkshopSession',start);const saved=await f.call('ksfSaveWorkshopTask',task(ctx.sessionId));const proj=await f.call('ksfCreateProjectFromOpportunity',{requestId:'p1',sessionId:ctx.sessionId,opportunityId:saved.opportunityId,projectKey:'p1',title:'演练项目'});return{...f,ctx,saved,proj};}
- test('记录员工与王刚001,重放不新建会话,不建立假员工账号',async()=>{const{call,store}=setup();const a=await call('ksfStartWorkshopSession',start),b=await call('ksfStartWorkshopSession',start);assert.equal(a.sessionId,b.sessionId);assert.equal((await store.list('KfSession')).length,1);assert.equal((await store.list('_User')).length,0);const row=await store.get('KfSession',a.sessionId);assert.equal(row.studentName,'演练人员');assert.equal(row.meta.attribution.agentName,'王刚001');assert.equal(row.meta.attribution.agentKeyOrigin,'name_only');assert.notEqual(row.meta.attribution.participantKey,start.participant.key);});
- test('aibot 无姓名无企业ID自动登记后保存归属,机器人作用域不会进入公开看板',async t=>{
- const root=fs.mkdtempSync(path.join(os.tmpdir(),'ksf-aibot-server-'));t.after(()=>{assert.equal(path.dirname(root),fs.realpathSync(os.tmpdir()));assert.ok(path.basename(root).startsWith('ksf-aibot-server-'));fs.rmSync(root,{recursive:true});});
- const opts={profileDir:path.join(root,'agents'),participantDir:path.join(root,'participants')},botId='private-aibot',senderId='private-sender';
- const registered=registerAgentProfile({name:'王刚001',role:'workshop',identityBasis:'runtime_config',runtimePlatform:'hermes',channel:'wecom',account:{platform:'wecom',accountId:botId}},opts).profile;
- const agentProfile=readAgentProfileByKey(registered.key,opts.profileDir),employee=registerChannelParticipant({version:'ksf.wecom-aibot-context.v1',botId,senderId,chatId:null,chatType:'single',messageId:'message-1'},{...opts,agentProfile});
- const participantProfile=readParticipantProfile(employee.profile.key,opts.participantDir),{call,store}=setup();
- const ctx=await runWorkshop('workshop-start',{workshopKey:'aibot-test',participant:{profileKey:participantProfile.key},conversation:{channel:'wecom',key:senderId},dataMode:'sample'},{requestId:'aibot-start',call,agentProfile,participantProfile});
- const row=await store.get('KfSession',ctx.sessionId);assert.equal(row.meta.attribution.participantAccount.tenantId,null);assert.equal(row.meta.attribution.participantAccount.accountId,senderId);assert.equal(row.meta.attribution.participantAccount.scopeId,botId);assert.equal(row.userRef,undefined);assert.match(row.studentName,/员工待绑定/);
- const saved=await runWorkshop('workshop-save',task(ctx.sessionId,{requestId:undefined}),{requestId:'aibot-save',call,agentProfile});assert.deepEqual((await store.get('KfArtifact',saved.artifactId)).meta.attribution,row.meta.attribution);
- const pub=await call('ksfGetPublicBoard',{},null);assert.ok(!JSON.stringify(pub).includes('private-'));assert.match(pub.session[0].studentName,/员工待绑定/);assert.equal((await store.list('_User')).length,0);
- const direct={...start,requestId:'wrong-scope',agent:{...agentProfile},participant:{key:participantProfile.key,name:participantProfile.name,identityBasis:'channel_context',account:{...participantProfile.account,scopeId:'other-bot'}}};
- delete direct.agent.version;delete direct.agent.registeredAt;
- await assert.rejects(()=>call('ksfStartWorkshopSession',direct),{code:'IDENTITY_CONFLICT'});assert.equal((await store.list('KfSession')).length,1);
- });
- test('账号归属保存至私有元数据并继承到报告项目,公开看板不泄露;登记不等于认证',async()=>{
- const{call,store}=setup();const agentAccount={platform:'wecom',tenantId:'private-tenant',accountId:'private-bot',accountName:'private-account-name',runtimeAgentId:'private-runtime',wecomAppAgentId:'private-app-agent'};
- const participantAccount={platform:'wecom',tenantId:'private-tenant',accountId:'private-person',employeeNumber:'private-employee'};
- const input={...start,agent:{name:'王刚001',key:'agent-registration:fixture',identityBasis:'operator_registration',account:agentAccount,role:'workshop',runtimePlatform:'hermes',channel:'wecom',collectedAt:'2026-11-02T01:00:00.000Z'},participant:{...start.participant,account:participantAccount}};
- const ctx=await call('ksfStartWorkshopSession',input),row=await store.get('KfSession',ctx.sessionId);assert.deepEqual(row.meta.attribution.agentAccount,agentAccount);assert.equal(row.meta.attribution.participantAccount.accountName,null);assert.equal(row.meta.attribution.agentAccountVerified,false);assert.equal(row.meta.attribution.agentKeyOrigin,'operator_registration');assert.equal(row.userRef,undefined);
- assert.equal(row.meta.attribution.agentRuntimePlatform,'hermes');assert.equal(row.meta.attribution.agentChannel,'wecom');assert.equal(row.meta.attribution.agentCollectedAt,input.agent.collectedAt);assert.equal(row.meta.attribution.agentRole,'workshop');
- const saved=await call('ksfSaveWorkshopTask',task(ctx.sessionId));const proj=await call('ksfCreateProjectFromOpportunity',{requestId:'account-project',sessionId:ctx.sessionId,opportunityId:saved.opportunityId,projectKey:'account-project',title:'归属检查'});
- for(const[n,id]of [['KfArtifact',saved.artifactId],['KfSource',saved.sourceId],['KfProject',proj.projectId]])assert.deepEqual((await store.get(n,id)).meta.attribution,row.meta.attribution);
- assert.equal((await store.get('KfOpportunity',saved.opportunityId)).sessionRef.objectId,ctx.sessionId);assert.equal(saved.dataMode,'sample');
- const pub=await call('ksfGetPublicBoard',{},null);assert.ok(!JSON.stringify(pub).includes('private-'));assert.equal(pub.artifact[0].agentName,'王刚001');assert.equal((await store.list('_User')).length,0);
- await assert.rejects(()=>call('ksfStartWorkshopSession',{...input,requestId:'changed-account',agent:{...input.agent,account:{...agentAccount,accountId:'changed'}}}),{code:'IDENTITY_CONFLICT'});
- await assert.rejects(()=>call('ksfStartWorkshopSession',{...input,requestId:'dropped-account',agent:{...input.agent,account:undefined}}),{code:'IDENTITY_CONFLICT'});
- await assert.rejects(()=>call('ksfStartWorkshopSession',{...input,requestId:'credential-field',agent:{...input.agent,account:{...agentAccount,token:'fixture'}}}),{code:'VALIDATION_FAILED'});
- await assert.rejects(()=>call('ksfStartWorkshopSession',{...input,requestId:'wrong-channel',agent:{...input.agent,channel:'other'}}),{code:'IDENTITY_CONFLICT'});
- await assert.rejects(()=>call('ksfStartWorkshopSession',{...input,requestId:'wrong-role',agent:{...input.agent,role:'operations'}}),{code:'VALIDATION_FAILED'});
- await assert.rejects(()=>call('ksfStartWorkshopSession',{...input,requestId:'changed-runtime',agent:{...input.agent,runtimePlatform:'other'}}),{code:'IDENTITY_CONFLICT'});
- await assert.rejects(()=>call('ksfStartWorkshopSession',{...start,requestId:'bad-user',participant:{...start.participant,userId:'wecom-is-not-user-object-id'}}),{code:'REFERENCE_NOT_FOUND'});
- });
- test('同名员工不同渠道身份不串会话,改参数重用 requestId 被拒',async()=>{const{call}=setup();const a=await call('ksfStartWorkshopSession',start);const b=await call('ksfStartWorkshopSession',{...start,requestId:'s2',participant:{...start.participant,key:'user-2'}});assert.notEqual(a.sessionId,b.sessionId);await assert.rejects(()=>call('ksfStartWorkshopSession',{...start,groupId:'G2'}),{code:'IDEMPOTENCY_CONFLICT'});});
- test('新数字人和同名新员工先登记,再经 Skill 保存正确归属并公开脱敏',async t=>{
- const root=fs.mkdtempSync(path.join(os.tmpdir(),'ksf-registry-server-'));t.after(()=>{assert.equal(path.dirname(root),fs.realpathSync(os.tmpdir()));assert.ok(path.basename(root).startsWith('ksf-registry-server-'));fs.rmSync(root,{recursive:true});});
- const options={profileDir:path.join(root,'agents'),participantDir:path.join(root,'participants')};
- const agents=['研发助手甲','研发助手乙'].map(name=>{const r=registerAgentProfile({name,role:'workshop',identityBasis:'operator_registration',runtimePlatform:'hermes',channel:'wecom',account:{platform:'wecom',accountName:'private-'+name}},options);return readAgentProfileByKey(r.profile.key,options.profileDir);});
- const people=[false,true].map(confirmNew=>{const r=registerParticipantProfile({name:'同名新员工',identityBasis:'participant_registration',account:{platform:'wecom'},...(confirmNew?{confirmNew:true}:{})},options);return readParticipantProfile(r.profile.key,options.participantDir);});
- const {call,store}=setup(),sessions=[];
- for(let i=0;i<2;i++){
- const opts={requestId:'registered-start-'+i,call,agentProfile:agents[i],participantProfile:people[i]},input={workshopKey:'new-registry',participant:{profileKey:people[i].key},conversation:{key:'same-group',channel:'wecom'},dataMode:'sample'};
- const ctx=await runWorkshop('workshop-start',input,opts);sessions.push(ctx.sessionId);
- assert.equal((await runWorkshop('workshop-start',input,opts)).sessionId,ctx.sessionId);
- const saved=await runWorkshop('workshop-save',task(ctx.sessionId,{requestId:undefined,taskKey:'new-s1-'+i,stageCode:'S1',artifactType:'category_report',opportunity:undefined}),{...opts,requestId:'registered-task-'+i});
- const row=await store.get('KfArtifact',saved.artifactId),sessionRow=await store.get('KfSession',ctx.sessionId);
- assert.equal(row.agentName,agents[i].name);assert.equal(row.agentId,agents[i].key);assert.equal(row.uploadedByKey,sessionRow.meta.attribution.participantKey);assert.equal(sessionRow.meta.attribution.identityBasis,'participant_registration');assert.equal(row.dataMode,'sample');
- }
- assert.notEqual(sessions[0],sessions[1]);const rows=await store.list('KfSession');assert.notEqual(rows[0].meta.attribution.participantKey,rows[1].meta.attribution.participantKey);
- const pub=await call('ksfGetPublicBoard',{},null);assert.equal(pub.session.length,2);assert.ok(!JSON.stringify(pub).includes('private-'));assert.equal((await store.list('_User')).length,0);
- });
- test('课堂报告/机会保留 sample,保存有归属且原子重放不重复',async()=>{const{call,store}=setup();const ctx=await call('ksfStartWorkshopSession',start);const a=await call('ksfSaveWorkshopTask',task(ctx.sessionId)),b=await call('ksfSaveWorkshopTask',task(ctx.sessionId));assert.deepEqual(a,b);assert.equal((await store.list('KfArtifact')).length,1);const artifact=await store.get('KfArtifact',a.artifactId);assert.equal(artifact.agentName,'王刚001');assert.equal(artifact.ownerName,'演练人员');assert.equal(artifact.dashboardState,'workshop');assert.equal(artifact.dataMode,'sample');assert.equal(artifact.contentConfirmedAt,undefined);assert.equal(artifact.sources.length,0);assert.match(artifact.title,/样例/);assert.equal(a.formalReportPublished,false);await assert.rejects(()=>call('ksfSaveWorkshopTask',task(ctx.sessionId,{requestId:'different',title:'changed'})),{code:'IDEMPOTENCY_CONFLICT'});assert.equal((await store.list('KfSource')).length,1,'冲突回滚来源登记');});
- test('真实项目、阶段门槛、方案版本与完整事件可回读',async()=>{const{call,store,ctx,proj}=await seeded();const base={sessionId:ctx.sessionId,projectId:proj.projectId};assert.ok((await store.get('KfProject',proj.projectId)).opportunityRef);await assert.rejects(()=>call('ksfTransitionProject',{...base,requestId:'skip',toStage:'S5',state:'in_progress',note:'跳过'}),{code:'VALIDATION_FAILED'});await assert.rejects(()=>call('ksfTransitionProject',{...base,requestId:'missing',toStage:'S4',state:'in_progress',note:'尚无成果'}),{code:'STAGE_REQUIREMENTS_NOT_MET'});
- await call('ksfSaveWorkshopTask',task(ctx.sessionId,{...base,requestId:'project-s3',taskKey:'project-s3',opportunity:undefined}));
- await call('ksfTransitionProject',{...base,requestId:'to-s4',toStage:'S4',state:'in_progress',note:'进入验证'});
- await call('ksfSaveWorkshopTask',task(ctx.sessionId,{...base,requestId:'project-s4',taskKey:'project-s4',stageCode:'S4',artifactType:'assessment_report',opportunity:undefined}));
- await call('ksfTransitionProject',{...base,requestId:'to-s5',toStage:'S5',state:'in_progress',note:'进入方案'});
- const concept=await call('ksfSaveWorkshopConcept',{...base,requestId:'concept',version:1,concept:{name:'演练方案',targetAudience:'待验证',usageScenario:'待验证',specification:{packaging:'待验证'},valueProposition:'待验证',productionSteps:[],pendingQuestions:['无企业工艺资料']},formulaItems:[]});assert.equal(concept.version,1);
- await call('ksfSaveWorkshopTask',task(ctx.sessionId,{...base,requestId:'project-s5',taskKey:'project-s5',stageCode:'S5',artifactType:'concept',opportunity:undefined}));
- await call('ksfTransitionProject',{...base,requestId:'complete',toStage:'S5',state:'completed',note:'课堂流程完成,业务条件仍待验证'});
- const result=await call('ksfGetWorkshopProject',base);assert.equal(result.project.progressState,'completed');assert.equal(result.artifacts.length,3);assert.equal(result.concept.objectId,concept.conceptId);assert.equal(result.events.length,8);assert.deepEqual(result.events.map(x=>x.sequence),[1,2,3,4,5,6,7,8]);
- });
- test('其他服务/学员/小组不能写入别人的项目',async()=>{const{call,ctx,proj}=await seeded();await assert.rejects(()=>call('ksfSaveWorkshopTask',task(ctx.sessionId),other),{code:'FORBIDDEN'});const outsider=await call('ksfStartWorkshopSession',{...start,requestId:'other-student',groupId:'G2',participant:{...start.participant,key:'other-user'}});await assert.rejects(()=>call('ksfGetWorkshopProject',{sessionId:outsider.sessionId,projectId:proj.projectId}),{code:'FORBIDDEN'});});
- test('公开分页无需登录,含课堂样例但不泄露私密会话/普通原文/未发布正式稿',async()=>{const{call,store}=await seeded();await store.create('KfArtifact',{title:'private',schemaVersion:'ksf.v0.7',dashboardState:'unpublished'});await store.create('KfSource',{title:'private source',...{schemaVersion:'ksf.v0.7'},rawText:'private'});const raw={artifact:[],session:[],source:[]};let cursor;do{const r=await call('ksfGetPublicBoard',{limit:1,...(cursor?{cursor}:{})},null);for(const k of Object.keys(raw))raw[k].push(...r[k]);cursor=r.nextCursor;}while(cursor);assert.equal(raw.artifact.length,1);assert.match(raw.artifact[0].title,/样例/);assert.equal(raw.source.length,1);assert.equal(raw.source[0].rawText,undefined);assert.equal(raw.session[0].meta.agentName,'王刚001');assert.ok(!JSON.stringify(raw).includes('conversation-1'));assert.ok(!JSON.stringify(raw).includes('service-a'));await assert.rejects(()=>call('ksfSaveWorkshopTask',{},null),{code:'AUTH_REQUIRED'});});
- test('完整验收任务从已登记aibot员工走S1-S5、检索选卡、资料依赖、卡住和新品方案,公开回读一致',async t=>{
- const root=fs.mkdtempSync(path.join(os.tmpdir(),'ksf-full-chain-'));t.after(()=>fs.rmSync(root,{recursive:true}));
- const options={profileDir:path.join(root,'agents'),participantDir:path.join(root,'participants')};
- const registered=registerAgentProfile({name:'王刚001',role:'workshop',identityBasis:'runtime_config',runtimePlatform:'hermes',channel:'wecom',account:{platform:'wecom',accountId:'private-bot'}},options).profile;
- const agentProfile=readAgentProfileByKey(registered.key,options.profileDir);
- const employee=registerChannelParticipant({version:'ksf.wecom-aibot-context.v1',botId:'private-bot',senderId:'private-sender',chatId:null,chatType:'single',messageId:'message-1'},{...options,agentProfile});
- const participantProfile=readParticipantProfile(employee.profile.key,options.participantDir),fixture=setup(),store=fixture.store,history=[],checkpoints=[];
- const call=(name,input)=>fixture.call(name,input,{...actor,scopes:[...actor.scopes,'source:write']});
- const context={version:'ksf.full-workshop-acceptance.v1',workshopKey:'KSF-企微实测-20261101-K',agentProfileKey:agentProfile.key,participantProfileKey:participantProfile.key,conversationKey:'private-current-chat'};
- assert.throws(()=>validateContext({...context,token:'not-allowed'}),{code:'INVALID_ACCEPTANCE_CONTEXT'});
- const result=await runFullChain({context,invoke:async(command,input,requestId)=>{history.push({command,input,requestId});return runWorkshop(command,input,{requestId,agentProfile,participantProfile,call});},publicRead:()=>call('ksfGetPublicBoard',{},null),checkpoint:x=>checkpoints.push(x)});
- assert.equal(result.ok,true);assert.equal(result.progressState,'completed');assert.equal(result.artifacts.length,6);assert.equal(result.projectArtifactCount,3);assert.equal(result.formalReportPublished,false);assert.equal(result.browserVerified,false);
- assert.equal((await store.list('KfCategory')).length,1);assert.equal((await store.list('KfProduct')).length,1);assert.equal((await store.list('KfMaterial')).length,1);assert.equal((await store.list('KfProcess')).length,1);
- assert.equal((await store.list('_User')).length,0);
- assert.ok(history.findIndex(x=>x.command==='workshop-search'&&x.input.className==='KfOpportunity')<history.findIndex(x=>x.command==='project-create'));
- const imported=history.find(x=>x.command==='workshop-import').input;
- const repeat=structuredClone(imported);for(const item of repeat.records)item.externalKey+=':another-batch';
- const reused=await runWorkshop('workshop-import',repeat,{requestId:'acceptance-dedup-batch',agentProfile,call});
- assert.equal(reused.sourceOperation,'unchanged');assert.equal(reused.counts.created,0);assert.equal(reused.counts.unchanged,4);
- for(const name of ['KfCategory','KfProduct','KfMaterial','KfProcess'])assert.equal((await store.list(name)).length,1);
- assert.ok(checkpoints.some(x=>x.status==='public-blocked-verified'));
- for(const a of await store.list('KfArtifact')){assert.equal(a.dataMode,'sample');assert.equal(a.meta.attribution.agentName,'王刚001');assert.equal(a.meta.attribution.participantAccount.accountId,'private-sender');}
- const pub=await call('ksfGetPublicBoard',{},null);assert.ok(!JSON.stringify(pub).includes('private-'));
- });
|