54 lines
3.4 KiB
TypeScript
54 lines
3.4 KiB
TypeScript
import { test } from "node:test"
|
|
import assert from "node:assert/strict"
|
|
import { buildLiveOptimizerData, DEFAULT_ROUTE_AI_OPTIMIZER_SETTINGS, type OptimizerApiServer, type RouteOptimizerSpeedProbe } from "./route-optimizer-data"
|
|
|
|
// Independent legacy oracle: forced pick, original pool order, first equal total.
|
|
function legacy(wans: string[], pool: RouteOptimizerSpeedProbe[], now: number) {
|
|
const score = (p: RouteOptimizerSpeedProbe, iface: string) => {
|
|
const want=iface.trim().toLowerCase(), got=p.srcInterface.trim().toLowerCase()
|
|
if(got && want && got!==want)return -1
|
|
return (got&&want?100:!got&&want?35:!want&&got?15:5)
|
|
+(p.lastPingRttMs!==null?20:0)+(p.lastTxAvgMbps!==null?10:0)+(p.lastRxAvgMbps!==null?10:0)
|
|
+(p.lastPingAt?Math.max(0,20-Math.floor((now-Date.parse(p.lastPingAt))/3600000)):0)
|
|
}
|
|
let best=-Infinity, result: (string|null)[]=[]
|
|
function visit(i:number,used:Set<string>,picked:(string|null)[],sum:number){
|
|
if(i===wans.length){if(sum>best){best=sum;result=[...picked]}return}
|
|
let matched=false
|
|
for(const p of pool){if(used.has(p.id))continue;const value=score(p,wans[i]);if(value<0)continue
|
|
matched=true;used.add(p.id);visit(i+1,used,[...picked,p.id],sum+value);used.delete(p.id)
|
|
}
|
|
if(!matched)visit(i+1,used,[...picked,null],sum)
|
|
}
|
|
visit(0,new Set(),[],0);return result
|
|
}
|
|
|
|
const common={host:"192.0.2.1",enabled:true,status:"online" as const,latency:10,country:"RU",site:"TEST",model:"CHR"}
|
|
function chosen(wans:string[],probes:RouteOptimizerSpeedProbe[]){
|
|
const rows:OptimizerApiServer[]=[{...common,id:1,name:"Home",type:"home-router",wanUplinks:wans.map((iface,i)=>({id:`w${i}`,name:`WAN${i}`,iface,isp:"ISP",ip:"198.51.100.1",maxDl:100,maxUl:100}))},{...common,id:2,name:"Jump",type:"jump-host"}]
|
|
return buildLiveOptimizerData(rows,[],DEFAULT_ROUTE_AI_OPTIMIZER_SETTINGS,probes).homes[0].wanJhLegs.map(leg=>leg.observed?.probeId??null)
|
|
}
|
|
|
|
function probe(i:number,iface:string):RouteOptimizerSpeedProbe{return {id:`p${i}`,srcServerId:"1",dstServerId:"2",srcInterface:iface,dstInterface:"",enabled:true,lastPingRttMs:10,lastPingLossPct:0,lastTxAvgMbps:100,lastRxAvgMbps:100,lastPingAt:null}}
|
|
|
|
test("branch bound preserves legacy assignments for ties, incompatibility and partial matching",context=>{
|
|
const now=Date.parse("2026-10-04T12:00:00Z");context.mock.method(Date,"now",()=>now)
|
|
let seed=917;const random=(n:number)=>{seed=(Math.imul(seed,1664525)+1013904223)>>>0;return (seed>>>8)%n}
|
|
const names=["","ether1","ether2"," ETHER1 "]
|
|
for(let fixture=0;fixture<1200;fixture++){
|
|
const count=1+random(4),wans=Array.from({length:count},()=>names[1+random(names.length-1)])
|
|
const pool=Array.from({length:count+random(3)},(_,i)=>({...probe(i,names[random(names.length)]),lastPingRttMs:random(2)?0:null,lastTxAvgMbps:random(2)?100:null,lastRxAvgMbps:random(2)?100:null,lastPingAt:random(3)?null:"2026-10-04T10:00:00Z"}))
|
|
assert.deepEqual(chosen(wans,pool),legacy(wans,pool,now),`fixture ${fixture}`)
|
|
}
|
|
})
|
|
|
|
test("twenty equal wildcard probes keep the first seven in pool order",()=>{
|
|
const wans=Array.from({length:7},(_,i)=>`ether${i}`),pool=Array.from({length:20},(_,i)=>probe(i,""))
|
|
assert.deepEqual(chosen(wans,pool),Array.from({length:7},(_,i)=>`p${i}`))
|
|
})
|
|
|
|
test("early forced choice remains unchanged when a later WAN loses its compatible probe",()=>{
|
|
const wans=["ether1", "ether1"],pool=[probe(0,"ether1"),probe(1,"ether2")]
|
|
assert.deepEqual(chosen(wans,pool),["p0",null])
|
|
})
|