chore(管理端): 初始化工程仓库
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
import { test } from 'node:test'
|
||||
import { strict as assert } from 'node:assert'
|
||||
import { unwrap, nextStatus } from '../src/contract.ts'
|
||||
test('HTTP 200 envelopes still reject unauthenticated and forbidden operations',()=>{assert.equal(unwrap({code:200,msg:'ok',data:3}),3); for(let code of [401,403,409]) assert.throws(()=>unwrap({code,msg:'拒绝',data:null}), /拒绝/)})
|
||||
test('task actions follow pending accepted completed sequence',()=>{assert.equal(nextStatus('pending'),'accepted');assert.equal(nextStatus('accepted'),'completed');assert.equal(nextStatus('completed'),null);assert.equal(nextStatus('cancelled'),null)})
|
||||
@@ -0,0 +1,8 @@
|
||||
import {test} from 'node:test'
|
||||
import {strict as assert} from 'node:assert'
|
||||
import {api} from '../src/api.ts'
|
||||
test('client uses centralized same-origin API, Bearer token, JSON body and invalidates expired sessions',async()=>{
|
||||
let stored:string|null='test-session',events:string[]=[];Object.defineProperty(globalThis,'sessionStorage',{value:{getItem:()=>stored,removeItem:()=>{stored=null}},configurable:true});Object.defineProperty(globalThis,'window',{value:{dispatchEvent:(event:Event)=>events.push(event.type)},configurable:true});let previous=globalThis.fetch;let calls:{url:string;init:RequestInit}[]=[];
|
||||
globalThis.fetch=async(url,init)=>{calls.push({url:String(url),init:init!});return new Response(JSON.stringify({code:200,msg:'ok',data:{status:'accepted'}}),{headers:{'Content-Type':'application/json'}})};
|
||||
try {assert.deepEqual(await api('/service-requests/task','PATCH',{status:'accepted'}),{status:'accepted'});assert.equal(calls[0]!.url,'/api/service-requests/task');assert.equal((calls[0]!.init.headers as Record<string,string>).Authorization,'Bearer test-session');assert.equal(calls[0]!.init.body,'{"status":"accepted"}');globalThis.fetch=async()=>new Response(JSON.stringify({code:403,msg:'会话过期',data:null}));await assert.rejects(api('/me'),/会话过期/);assert.equal(stored,null);assert.deepEqual(events,['auth-expired'])}finally{globalThis.fetch=previous}
|
||||
})
|
||||
@@ -0,0 +1,16 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { CallTracker, announcement, SpeechQueue, selectVoice } from '../src/notifications/core.ts'
|
||||
import type { Task } from '../src/contract.ts'
|
||||
function task(id:string,status='pending'):Task{return{id,status,tableCode:'T01',kind:'help',note:'私人备注不要播报',createdAt:'2026-10-03T09:00:00Z',updatedAt:''}}
|
||||
test('首次只建基线;重复轮询和已处理任务不播报',()=>{let t=new CallTracker();assert.deepEqual(t.accept([task('old')]),[]);assert.deepEqual(t.accept([task('old'),task('new'),task('done','accepted')]).map(x=>x.id),['new']);assert.deepEqual(t.accept([task('new')]),[]);assert.deepEqual(t.accept([task('old')]),[])})
|
||||
test('静音快照仍去重;重置后历史任务不重播',()=>{let t=new CallTracker();t.accept([]);t.accept([task('muted')]);assert.deepEqual(t.accept([task('muted'),task('new')]).map(x=>x.id),['new']);t.reset();assert.deepEqual(t.accept([task('new')]),[])})
|
||||
test('播报只包括桌位与服务类型,不包含顾客备注',()=>{assert.equal(announcement(task('x')),'T01 桌,呼叫员工。');assert.equal(announcement({...task('x'),kind:'refill'}),'T01 桌,需要补充豆料。')})
|
||||
test('仅选择本机中文语音,优先普通话',()=>{let remote={lang:'zh-CN',localService:false};let cantonese={lang:'zh-HK',localService:true};let mandarin={lang:'zh-CN',localService:true};assert.equal(selectVoice([remote,cantonese,mandarin]),mandarin);assert.equal(selectVoice([remote,{lang:'en-US',localService:true}]),undefined)})
|
||||
function harness(){let spoken:string[]=[];let callbacks:Array<(error?:string)=>void>=[];let cancelled=0;let errors:string[]=[];let q=new SpeechQueue({speak(text,done){spoken.push(text);callbacks.push(done)},cancel(){cancelled++}},e=>errors.push(e));return {q,spoken,callbacks,errors,get cancelled(){return cancelled}}}
|
||||
test('队列串行播报且待接单任务被取消后不再播报',()=>{let h=harness();h.q.add([task('a'),task('b'),task('c')]);assert.equal(h.spoken.length,1);h.q.retain(new Set(['a','c']));h.callbacks[0]();assert.equal(h.spoken.length,2);h.q.stop();h.callbacks[1]();assert.equal(h.spoken.length,2);assert.equal(h.cancelled,1)})
|
||||
test('关闭后的旧回调不影响新队列;播报错误清空并上报',()=>{let h=harness();h.q.add([task('a')]);h.q.stop();h.q.add([task('b'),task('c')]);h.callbacks[0]();assert.equal(h.spoken.length,2);h.callbacks[1]('播放被浏览器阻止');assert.deepEqual(h.errors,['播放被浏览器阻止']);assert.equal(h.spoken.length,2)})
|
||||
|
||||
test('队列过载明确提示,不产生无限语音积压',()=>{let h=harness();h.q.add(Array.from({length:33},(_,i)=>task(String(i))));assert.equal(h.spoken.length,0);assert.equal(h.errors.length,1)})
|
||||
|
||||
test('试听期间收到的新呼叫排队保留,已开启时试听也不清空任务',()=>{let h=harness();let completed=0;h.q.preview('语音开启',()=>completed++);h.q.add([task('new')]);h.callbacks[0]();assert.equal(completed,1);assert.equal(h.spoken[1],'T01 桌,呼叫员工。');h.q.preview('再次试听',()=>completed++);h.q.add([task('second')]);h.callbacks[1]();assert.equal(h.spoken[2],'再次试听');h.callbacks[2]();assert.equal(h.spoken.length,4);h.q.stop()})
|
||||
@@ -0,0 +1,11 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import {RealtimeLink} from '../src/notifications/realtime.ts'
|
||||
function harness(){
|
||||
let sockets:any[]=[],states:string[]=[],syncs=0,expired=0,timers=new Map<any,{fn:()=>void,ms:number}>(),id=0
|
||||
let link=new RealtimeLink({socket(){let s:any={onopen:null,onmessage:null,onclose:null,onerror:null,sent:[],send(data:string){this.sent.push(JSON.parse(data))},close(){}};sockets.push(s);return s},token:()=> 'test-token',state:s=>states.push(s),sync:()=>syncs++,expired:()=>expired++,later(fn,ms){let key=++id as any;timers.set(key,{fn,ms});return key},clear(key){timers.delete(key)}})
|
||||
return {link,sockets,states,timers,get syncs(){return syncs},get expired(){return expired}}
|
||||
}
|
||||
test('首帧鉴权,就绪与推送都触发补查,心跳不重复拉取',()=>{let h=harness();h.link.start();let s=h.sockets[0];s.onopen({});assert.deepEqual(s.sent,[{type:'auth',token:'test-token'}]);s.onmessage({data:'{"type":"tasks.changed"}'});assert.equal(h.syncs,0);s.onmessage({data:'{"type":"ready"}'});s.onmessage({data:'{"type":"tasks.changed"}'});s.onmessage({data:'{"type":"heartbeat"}'});assert.equal(h.syncs,2);assert.equal(h.states.at(-1),'online');h.link.stop();assert.equal(h.timers.size,0)})
|
||||
test('网络断线退避重连;认证失效停止连接',()=>{let h=harness();h.link.start();h.sockets[0].onclose({code:1006});let retry=[...h.timers.values()].find(t=>t.ms===1000)!;assert.ok(retry);retry.fn();assert.equal(h.sockets.length,2);h.sockets[1].onclose({code:4403});assert.equal(h.expired,1);assert.equal(h.states.at(-1),'closed');assert.equal(h.timers.size,0)})
|
||||
test('未收到心跳时主动重连,关闭后不残留监听',()=>{let h=harness();h.link.start();let s=h.sockets[0];s.onmessage({data:'{"type":"ready"}'});[...h.timers.values()].find(t=>t.ms===45000)!.fn();assert.equal(h.states.at(-1),'retrying');assert.equal(s.onmessage,null);h.link.stop();assert.equal(h.timers.size,0)})
|
||||
Reference in New Issue
Block a user