import {test} from 'node:test' import assert from 'node:assert/strict' import {build} from 'esbuild' import {fileURLToPath} from 'node:url' // api.ts 依赖 Vite 环境;只隔离环境适配层,所有工作流逻辑使用实际源代码与 Vue refs。 let bundle = await build({ stdin:{contents:"import * as workflow from './src/stores/visit.ts'; export {workflow}; export {reactive} from 'vue'",resolveDir:fileURLToPath(new URL('..',import.meta.url))}, bundle:true,platform:'node',format:'esm',write:false, plugins:[{name:'isolate-vite-api',setup(builder){ builder.onResolve({filter:/services\/api$/},()=>({path:'vite-api',namespace:'test'})) builder.onLoad({filter:/.*/,namespace:'test'},()=>({contents:"export const storeId='test-store';export function api(){throw new Error('Inject test API')}",loader:'js'})) }}] }) let module = await import('data:text/javascript;base64,'+Buffer.from(bundle.outputFiles[0].text).toString('base64')) function fixture(){ assert.equal(typeof module.workflow.createVisitWorkflow,'function','visit workflow must expose isolated API dependencies') let identity='user-a', failPost=false, failRefresh=false, posted:any[]=[], current:any=null let send=async(path:string,method='GET',body?:any)=>{ if(path==='/visits/current'&&method==='DELETE'){current=null;return null} if(path==='/visits/current')return current if(path==='/visits'&&method==='POST')return current={id:'visit-'+body.tableCode,storeId:'test-store',tableCode:body.tableCode,status:'active'} if(path==='/service-requests'&&method==='POST'){posted.push({...body});if(failPost)throw new Error('ambiguous network');return {id:'task-'+posted.length,userId:identity,tableCode:current.tableCode,storeId:'test-store',status:'pending',...body}} if(failRefresh)throw new Error('refresh offline') return [] } let state=module.reactive(module.workflow.createVisitWorkflow({storeId:'test-store',identity:()=>identity,ensure:async()=>{},api:(...args:any[])=>send(args[0],args[1],args[2])})) return {state,posted,getIdentity:()=>identity,setIdentity:(v:string)=>{identity=v},setCurrent:(v:any)=>{current=v},setFailPost:(v:boolean)=>{failPost=v},setFailRefresh:(v:boolean)=>{failRefresh=v},setAPI:(fn:typeof send)=>{send=fn}} } test('模糊失败重试复用UUID;刷新失败后保留成功且禁止再次提交',{timeout:3000},async()=>{ let f=fixture(),s=f.state;await s.confirmVisit('T01');s.note='same';f.setFailPost(true) await assert.rejects(s.submit(),/ambiguous/);f.setFailPost(false);f.setFailRefresh(true) let result=await s.submit();assert.deepEqual(f.posted[0],f.posted[1]);assert.match(f.posted[0].requestId,/^[0-9a-f-]{36}$/) assert.match(result.refreshError,/offline/);assert.ok(s.submitted);await s.submit();assert.equal(f.posted.length,2) s.startNewRequest();await s.submit();assert.notEqual(f.posted[1].requestId,f.posted[2].requestId) }) test('首入申请去确认到店保留草稿;新到店会话清除旧结果与幂等上下文',{timeout:3000},async()=>{ let f=fixture(),s=f.state;s.configureRequest({kind:'print',patternId:'pattern-1'});s.note='keep draft' await s.confirmVisit('T01');assert.equal(s.kind,'print');assert.equal(s.note,'keep draft');assert.equal(s.patternId,'pattern-1') await s.submit();let id=f.posted[0].requestId;await s.leave();assert.equal(s.submitted,null) await s.confirmVisit('T02');assert.equal(s.submitted,null);await s.submit();assert.equal(f.posted.length,2);assert.equal(s.submitted.tableCode,'T02');assert.notEqual(f.posted[1].requestId,id) }) test('服务端刷新发现新的visit,也清除旧成功结果',{timeout:3000},async()=>{ let f=fixture(),s=f.state;await s.confirmVisit('T01');await s.submit() f.setCurrent({id:'new-visit',storeId:'test-store',tableCode:'T02',status:'active'});await s.refreshVisit();assert.equal(s.submitted,null);assert.equal(s.visit.tableCode,'T02') }) test('换账号清除旧结果/草稿/记录,旧账号在途读取不能回写',{timeout:3000},async()=>{ let f=fixture(),s=f.state;await s.confirmVisit('T01');s.note='private';await s.submit() let resolve:(v:any)=>void=()=>{};f.setAPI(async()=>new Promise(r=>{resolve=r})) let read=s.refreshTasks();f.setIdentity('user-b');s.configureRequest({}) assert.equal(s.submitted,null);assert.equal(s.visit,null);assert.equal(s.note,'');assert.equal(s.tasks.length,0) resolve([{id:'old-user-task'}]);await read;assert.equal(s.tasks.length,0) }) test('在途提交期间换账号不写回旧成功结果',{timeout:3000},async()=>{ let f=fixture(),s=f.state;await s.confirmVisit('T01') let resolve:(v:any)=>void=()=>{};f.setAPI(async()=>new Promise(r=>{resolve=r})) let posting=s.submit();f.setIdentity('user-b');s.configureRequest({}) resolve({id:'old-task',tableCode:'T01'});await assert.rejects(posting,/账号|会话/);assert.equal(s.submitted,null);assert.equal(s.tasks.length,0) }) test('首次登录确认桌位保留草稿;登出清除成功与旧记录',{timeout:3000},async()=>{ let f=fixture(),s=f.state;f.setIdentity('');s.configureRequest({kind:'ironing'});s.note='first draft' // initial logged-out state is the normal request → visit journey. let initial=module.reactive(module.workflow.createVisitWorkflow({storeId:'test-store',identity:f.getIdentity,ensure:async()=>f.setIdentity('user-a'),api:async(path:string,_method:string,body:any)=>({id:'first',storeId:'test-store',tableCode:body.tableCode,status:'active'})})) initial.configureRequest({kind:'ironing'});initial.note='first draft';await initial.confirmVisit('T01');assert.equal(initial.kind,'ironing');assert.equal(initial.note,'first draft') f.setIdentity('user-a');await s.confirmVisit('T01');await s.submit();f.setIdentity('');await s.refreshVisit();assert.equal(s.submitted,null);assert.equal(s.visit,null);assert.equal(s.tasks.length,0) }) test('换账号后旧到店读取不回写;未关联打印不发请求',{timeout:3000},async()=>{ let f=fixture(),s=f.state;await s.confirmVisit('T01');s.configureRequest({kind:'print'}) await assert.rejects(s.submit(),/图纸/);assert.equal(f.posted.length,0) let resolve:(v:any)=>void=()=>{};f.setAPI(async()=>new Promise(r=>{resolve=r})) let read=s.refreshVisit();f.setIdentity('user-b');s.configureRequest({}) resolve({id:'old-visit',storeId:'test-store',tableCode:'T01',status:'active'});await read assert.equal(s.visit,null);assert.equal(s.submitted,null) })