Nevar pievienot vairāk kā 25 tēmas Tēmai ir jāsākas ar burtu vai ciparu, tā var saturēt domu zīmes ('-') un var būt līdz 35 simboliem gara.
 
 
 
 

81 rinda
10 KiB

  1. import fs from 'node:fs';import crypto from 'node:crypto';import assert from 'node:assert/strict';
  2. import {chromium,expect} from '@playwright/test';import {login,call} from './role-permissions-live.mjs';
  3. const report='reports/virtual-html-full-closure-20260912';fs.mkdirSync(report,{recursive:true});
  4. const admin=await login('root'),checks=[],errors=[];
  5. const api=async(endpoint,session=admin,body,method)=>{const r=await call(endpoint,session?.accessToken,body,method);assert.equal(r.status,200,endpoint+': '+r.message);return r.data};
  6. const suffix=Date.now().toString(36),password='Wf!'+crypto.randomBytes(8).toString('hex');
  7. const roles=await api('auth/v1/roles?size=100'),role=(roles.records||roles).find(r=>r.code==='student');
  8. const user=await api('auth/v1/users',admin,{username:'wf_'+suffix,displayName:'HTML编排验收-'+suffix,departmentId:'5',roleIds:[role.id],defaultRoleId:role.id,enabled:true,password});
  9. const student=await api('auth/v1/auth/login',null,{userId:(user.user||user).id,roleCode:'student',password,rememberMe:false});
  10. const source=await api('tran/v1/content/projects/419'),content=structuredClone(source.currentVersion.content);
  11. content.steps.forEach(step=>{step.mode='measure';step.interaction='rule-check';step.targetNodeId=step.targetId;step.action={...step.action,kind:'rule-check',mode:'measure',targetId:step.targetId,requiresPlacement:false};});
  12. const edge=(to,rule='')=>({id:crypto.randomUUID(),to,rule}),node=(id,type,expression,transitions,extra={})=>({id,type,title:id,config:{expression},resolution:{expression,timeoutSeconds:2},transitions,...extra});
  13. content.executionPlan={schema:'unreal-tran.training-execution-plan/v1',entryNodeId:'start',nodes:[
  14. node('start','start','',[edge('fault')]),node('fault','fault-inject','PUMP',[edge('condition')]),
  15. node('condition','condition','fault.PUMP',[edge('wait','true'),edge('s1','false')]),
  16. node('s1','step','',[edge('s2')],{stepId:content.steps[0].id}),
  17. node('wait','event-wait','DEVICE_READY',[edge('camera')]),
  18. node('camera','camera','wide',[edge('audio')]),node('audio','audio','/audio/sfx/ui.wav',[edge('safety')]),
  19. node('safety','safety','event.DEVICE_READY',[edge('s2','true'),edge('s1','false')]),
  20. node('s2','step','',[edge('clear')],{stepId:content.steps[1].id}),
  21. node('clear','fault-clear','PUMP',[edge('score')]),node('score','score','5',[edge('end')]),node('end','end','',[])]};
  22. const created=await api('tran/v1/content/projects',admin,{type:'TRAINING',name:'HTML服务端编排-'+suffix,categoryCode:'MAINTENANCE',trainingMode:'VIRTUAL',content,dependencies:source.currentVersion.dependencies,assets:[]});
  23. await api(`tran/v1/content/projects/${created.project.id}/publish`,admin,{version:created.project.version});
  24. const project=(await api(`tran/v1/content/projects/${created.project.id}`)).project;
  25. const task=await api('tran/v1/teaching/assignments',admin,{code:'HTML-WF-'+suffix,name:'HTML编排等待与分支-'+suffix,description:'服务端故障、条件分支和外部事件验收',channel:'VIRTUAL',assignmentKind:'TRAINING',executionMode:'PRACTICE',audienceType:'ASSIGNED',collaborationMode:'INDIVIDUAL',digitalHumanAllowed:true,contentProjectId:project.id,contentVersionId:project.publishedVersionId,members:[{userId:(user.user||user).id,memberType:'LEARNER',teamCode:'NEUTRAL',positionCode:'LEARNER',sortOrder:0}]});
  26. const published=await api(`tran/v1/teaching/assignments/${task.id}/publish`,admin,{version:task.version});
  27. const run=await api(`tran/v1/teaching/assignments/${task.id}/accept`,student,{assignmentVersion:published.version,teamCode:'NEUTRAL',positionCode:'LEARNER'});
  28. fs.writeFileSync(report+'/workflow-fixture.json',JSON.stringify({userId:(user.user||user).id,projectId:project.id,assignmentId:task.id,runId:run.id},null,2));
  29. const browser=await chromium.launch({channel:'msedge',headless:true,args:['--enable-unsafe-swiftshader']});
  30. const context=await browser.newContext({viewport:{width:1920,height:1080}});await context.addInitScript(s=>{sessionStorage.setItem('unreal-tran:web:access-token:v1',s.accessToken);sessionStorage.setItem('unreal-tran:web:refresh-token:v1',s.refreshToken)},student);
  31. const page=await context.newPage();page.on('pageerror',e=>errors.push(e.message));
  32. const pass=(name,details)=>{checks.push({name,pass:true,details});console.log('PASS',name)};
  33. const endpoint=`tran/v1/teaching/runs/${run.id}`,diagnostic=()=>page.evaluate(()=>globalThis.__trainingSceneDiagnostics?.());
  34. const mounted=async()=>{await expect.poll(async()=>Boolean((await diagnostic())?.publishedContentMounted),{timeout:90000}).toBe(true)};
  35. try{
  36. await page.goto(`http://127.0.0.1:6180/teaching/virtual-training/runs/${run.id}`);await mounted();
  37. let state=await api(endpoint+'/workflow',student);assert.equal(state.waiting,true);assert.equal(state.execution,undefined);
  38. await expect(page.getByRole('button',{name:'编排状态与事件'})).toHaveCount(0);
  39. const privateState=await api(endpoint+'/workflow');assert.equal(privateState.execution.faults.PUMP,true);
  40. const before=await api(endpoint,student);
  41. await call(endpoint+'/events',student.accessToken,{type:'workflow.state',title:'spoof',summary:'spoof',source:'WORKFLOW',visibility:'PUBLIC',publicPayload:{cursor:'end',ended:true}});
  42. assert.equal((await api(endpoint+'/workflow',student)).waiting,true,'Public event cannot replace private workflow journal');
  43. const denied=await call(endpoint+'/workflow/signals',student.accessToken,{version:before.version,commandId:crypto.randomUUID(),signalCode:'DEVICE_READY'});assert.equal(denied.status,403);
  44. const premature=await call(endpoint+'/submit',student.accessToken,{version:before.version,commandId:crypto.randomUUID(),summary:'premature'});assert.notEqual(premature.status,200);
  45. await page.getByRole('button',{name:'刷新流程',exact:true}).click();await mounted();
  46. await expect.poll(async()=>(await api(endpoint+'/workflow',student)).timedOut,{timeout:12000}).toBe(true);
  47. await page.reload();await mounted();assert.equal((await api(endpoint+'/workflow',student)).waiting,true);
  48. await page.screenshot({path:report+'/03-workflow-waiting.png',fullPage:true});
  49. pass('故障真值与编排游标在服务端;超时仍等待,刷新恢复;学员不能注入事件或提前交卷');
  50. const managerContext=await browser.newContext({viewport:{width:1920,height:1080}});await managerContext.addInitScript(s=>{sessionStorage.setItem('unreal-tran:web:access-token:v1',s.accessToken);sessionStorage.setItem('unreal-tran:web:refresh-token:v1',s.refreshToken)},admin);
  51. const manager=await managerContext.newPage();await manager.goto(`http://127.0.0.1:6180/teaching/virtual-training/runs/${run.id}`);
  52. await manager.getByRole('button',{name:'编排状态与事件'}).click();await manager.getByPlaceholder('与事件等待节点一致,例如 DEVICE_READY').fill('DEVICE_READY');
  53. const signalRequest=manager.waitForRequest(request=>request.url().endsWith('/workflow/signals')&&request.method()==='POST');
  54. await manager.getByRole('button',{name:'确认事件并继续编排'}).click();
  55. const signalBody=(await signalRequest).postDataJSON();
  56. await expect.poll(async()=>(await api(endpoint+'/workflow',student)).waiting).toBe(false);
  57. const signalVersion=(await api(endpoint)).version;
  58. const replayed=await api(endpoint+'/workflow/signals',admin,signalBody);assert.equal(replayed.version,signalVersion);assert.equal((await api(endpoint)).version,signalVersion);
  59. await manager.screenshot({path:report+'/04-teacher-workflow-event.png',fullPage:true});
  60. const facts=await api(endpoint+'/steps',student);assert.equal(facts[0].statusCode,'SKIPPED');assert.equal(facts[1].statusCode,'IN_PROGRESS');
  61. await page.getByRole('button',{name:'刷新流程',exact:true}).click();await mounted();
  62. await expect(page.locator('[data-step-index="0"]')).toContainText('分支跳过');
  63. assert.deepEqual((await api(endpoint+'/workflow',student)).effects.map(e=>e.type),['camera','audio']);
  64. await expect.poll(async()=>Math.abs((await diagnostic()).framing.distance-15.5),{timeout:6000}).toBeLessThan(.1);
  65. const panel=page.locator('.training-server-preparation');if(!(await panel.evaluate(p=>p.open)))await panel.locator('summary').click();
  66. await panel.locator('[data-tool="pressure-gauge"]').check();for(const checkbox of await panel.locator('[data-safety]').all())await checkbox.check();
  67. await panel.locator('textarea').fill('编排分支验收:压力归零。');await panel.locator('[data-save-preparation]').click();await expect(panel.locator('[data-save-status]')).toHaveText('已保存');await panel.locator('summary').click();
  68. await page.waitForTimeout(1500);await page.locator('[data-action="toggle-issue-highlight"]').click();await page.waitForTimeout(500);
  69. const d=await diagnostic(),rect=await page.locator('canvas.training-canvas').boundingBox();assert.ok(d.framing.targetOnScreen?.insideCanvas);
  70. await page.mouse.click(rect.x+d.framing.targetOnScreen.localX,rect.y+d.framing.targetOnScreen.localY+38);
  71. await expect.poll(async()=>(await api(endpoint,student)).progressPercent,{timeout:20000}).toBe(100);
  72. const ended=await api(endpoint+'/workflow');assert.equal(ended.ended,true);assert.deepEqual(ended.execution.faults,{});assert.equal(Number(ended.execution.suggestedScore),5);
  73. await page.locator('[data-action="next"]').click();await expect.poll(async()=>(await api(endpoint,student)).status,{timeout:20000}).toBe('SUBMITTED');
  74. await page.screenshot({path:report+'/05-workflow-submitted.png',fullPage:true});
  75. pass('教员页面确认事件、服务端选择分支、明确跳过第一步、真实操作第二步、清除故障与提交');
  76. const submitted=await api(endpoint);await api(endpoint+'/review',admin,{version:submitted.version,commandId:crypto.randomUUID(),score:91,passed:true,feedback:'编排路径与实际操作已核验'});
  77. const records=await api(`tran/v1/teaching/analytics/records?keyword=${encodeURIComponent(task.name)}&assignmentKind=TRAINING&page=1&size=20`);assert.ok(records.records.some(r=>String(r.runId||r.id)===run.id));
  78. pass('编排训练交卷后由教员评分,真实统计包含本次运行',{runId:run.id,score:91});assert.equal(errors.length,0,errors.join('\n'));
  79. }catch(error){checks.push({name:'编排闭环实测',pass:false,error:error.message});console.error(error.message);process.exitCode=1;await page.screenshot({path:report+'/workflow-failure.png',fullPage:true}).catch(()=>{})}
  80. finally{fs.writeFileSync(report+'/workflow-checks.json',JSON.stringify({checks,errors},null,2));await browser.close()}