const test = require('node:test'); const assert = require('node:assert/strict'); const fs = require('node:fs'); const vm = require('node:vm'); const path = require('node:path'); const source = fs.readFileSync(path.join(__dirname, '../../static/js/admin_routing.js'), 'utf8'); function snapshot(online = false){ return {ok:true, username:'alice', presence:{online}, groups:[{id:1,name:'综合客服'},{id:2,name:'售后组'}], agents:[{username:'alice',name:'客服甲',group_id:1,group_name:'综合客服',online,active_count:1}, {username:'bob',name:'客服乙',group_id:2,group_name:'售后组',online:true,active_count:0}], sessions:[{id:7,group_id:1,group_name:'综合客服',owner_username:'alice',owner_name:'客服甲',status:'active',version:3}], events:[]}; } function desk(){ const elements = new Map(), calls = [], hooks = {}; function element(id){ if(!elements.has(id)) elements.set(id, {value:'',innerHTML:'',textContent:'',hidden:false,disabled:false, dataset:{},attributes:{},listeners:{},classList:{toggle(){},add(){},remove(){}}, setAttribute(name,value){this.attributes[name]=value;}, addEventListener(name,listener){this.listeners[name]=listener;}}); return elements.get(id); } const context = vm.createContext({window:{},document:{getElementById:element},console}); vm.runInContext(source,context); const api = context.window.AdminRouting.create({username:'alice', request:async(url,options)=>{ const data = options && options.body ? JSON.parse(options.body) : undefined; calls.push({url,data}); if(hooks.request) return hooks.request(url,data); if(url === '/api/admin/routing') return snapshot(); return {ok:true,online:data && data.online}; },changed(){calls.push({changed:true});}}); return {api,element,calls,hooks}; } test('loading the desk shows offline without silently enabling automatic assignment',async()=>{ const app = desk(); await app.api.refresh(); assert.match(app.element('routing-presence-text').textContent,/离线/); assert.equal(app.calls.filter(item=>item.data && item.data.online === true).length,0); assert.match(app.element('routing-members').innerHTML,/客服乙/); assert.match(app.element('routing-members').innerHTML,/售后组/); }); test('heartbeat continues while offline and never sends an online claim',async()=>{ const app = desk(); await app.api.refresh(); await app.api.heartbeat(); const sent = app.calls.find(item=>item.url === '/api/admin/routing/presence'); assert.deepEqual(sent.data,{}); assert.match(app.element('routing-presence-text').textContent,/离线/); }); test('failed presence change retains the confirmed status and provides retry feedback',async()=>{ const app = desk(); await app.api.refresh(); app.hooks.request=async(url)=>{ if(url.endsWith('/presence')) throw new Error('network'); return snapshot(); }; await app.api.setPresence(); assert.match(app.element('routing-presence-text').textContent,/离线/); assert.match(app.element('routing-status').textContent,/未确认/); assert.equal(app.element('routing-presence-toggle').disabled,false); }); test('changing group only offers online members of that group for reassignment',async()=>{ const app = desk(); await app.api.refresh(); app.element('routing-session').value='7'; app.element('routing-session').listeners.change(); app.element('routing-target-group').value='2'; app.element('routing-target-group').listeners.change(); assert.match(app.element('routing-target-owner').innerHTML,/bob/); assert.doesNotMatch(app.element('routing-target-owner').innerHTML,/alice/); }); test('refresh does not silently rebase the selected assignment version',async()=>{ const app = desk(); await app.api.refresh(); app.element('routing-session').value='7'; app.element('routing-session').listeners.change(); app.element('routing-target-group').value='2'; app.element('routing-target-owner').value='bob'; const updated = snapshot(); updated.sessions[0].version=4; app.hooks.request=async(url)=>url === '/api/admin/routing' ? updated : {ok:true}; await app.api.refresh(); await app.api.assign(); const sent=app.calls.find(item=>item.url === '/api/admin/routing/assign'); assert.equal(sent.data.version,3); assert.equal(sent.data.username,'bob'); }); test('a late background refresh cannot override a confirmed presence switch',async()=>{ const app=desk(); await app.api.refresh(); let resolve; app.hooks.request=async(url)=>url === '/api/admin/routing' ? new Promise(done=>{resolve=done;}) : {ok:true,online:true}; const pending=app.api.refresh(); const switching=app.api.setPresence(); await switching; resolve(snapshot(false)); await pending; assert.match(app.element('routing-presence-text').textContent,/在线/); }); test('a manual refresh during a presence write cannot restore an older availability state',async()=>{ const app=desk(); await app.api.refresh(); let confirmWrite, confirmRead; app.hooks.request=(url)=>url === '/api/admin/routing' ? new Promise(done=>{confirmRead=done;}) : new Promise(done=>{confirmWrite=done;}); const switching=app.api.setPresence(); const refreshing=app.api.refresh(); confirmWrite({ok:true,online:true}); await switching; if(confirmRead) confirmRead(snapshot(false)); await refreshing; assert.match(app.element('routing-presence-text').textContent,/在线/); }); test('an unavailable selected owner is not silently replaced with automatic queueing',async()=>{ const app=desk(); await app.api.refresh(); app.element('routing-session').value='7'; app.element('routing-session').listeners.change(); app.element('routing-target-group').value='2'; app.element('routing-target-group').listeners.change(); app.element('routing-target-owner').value='bob'; const updated=snapshot(); updated.agents[1].online=false; app.hooks.request=async(url)=>url === '/api/admin/routing' ? updated : {ok:true}; await app.api.refresh(); await app.api.assign(); const sent=app.calls.find(item=>item.url === '/api/admin/routing/assign'); assert.equal(sent.data.username,'bob'); });