fix(uptime): сохранить неизвестную скорость и проверить подтверждение теста

This commit is contained in:
Denozordec committed 2026-10-07 14:15:56 +07:00
1 parent 1904c86657
commit 9d5959601f
10 files changed
+160 -58

No files matched your search

@@ -0,0 +1,22 @@
import {test} from "node:test"
import assert from "node:assert/strict"
import {parseBandwidthRateMbps,readBandwidthTestRates} from "./bandwidth-test-rates.js"
test("native units preserve decimal and explicit zero; absence and malformed rate are unknown",()=>{
for(const raw of [undefined,null,"","bad","-1Mbps",false,"Infinity","5MiB/s"])assert.equal(parseBandwidthRateMbps(raw),null)
assert.equal(parseBandwidthRateMbps("0bps"),0);assert.equal(parseBandwidthRateMbps("150.25Mbps"),150.25)
assert.equal(parseBandwidthRateMbps("125kbps"),0.125);assert.equal(parseBandwidthRateMbps(1000000),1)
assert.equal(parseBandwidthRateMbps("1.5Gbps"),1500)
})
test("explicit total-average zero wins over nonzero samples",()=>{
const result=readBandwidthTestRates([{"tx-current":"20Mbps"},{"tx-total-average":"0bps","rx-total-average":"150.25Mbps"}])
assert.equal(result.txAvgMbps,0);assert.equal(result.rxAvgMbps,150.25)
})
test("one-direction test leaves the other average and curve unknown",()=>{
const result=readBandwidthTestRates([{"rx-current":"10Mbps"},{"rx-total-average":"20Mbps"}])
assert.equal(result.txAvgMbps,null);assert.deepEqual(result.txSeries,[]);assert.equal(result.rxAvgMbps,20)
assert.deepEqual(readBandwidthTestRates([]),{txAvgMbps:null,rxAvgMbps:null,txSeries:[],rxSeries:[]})
})
test("fallback uses only valid samples and never fabricates zero",()=>{
const result=readBandwidthTestRates([{"tx-current":"bad"},{"tx-current":"10Mbps"},{"tx-current":"20Mbps"}])
assert.equal(result.txAvgMbps,15);assert.deepEqual(result.txSeries,[10,20]);assert.equal(result.rxAvgMbps,null)
})
@@ -0,0 +1,30 @@
/** RouterOS rates are bits/s unless a decimal SI prefix is present. */
export function parseBandwidthRateMbps(raw: unknown): number | null {
if (typeof raw !== "string" && typeof raw !== "number") return null
const match = String(raw).trim().toLowerCase().match(/^(\d+(?:\.\d+)?|\.\d+)\s*(g|m|k)?(?:bps|bit\/s|bits\/s)?$/)
if (!match) return null
const factor = match[2] === "g" ? 1000 : match[2] === "m" ? 1 : match[2] === "k" ? 0.001 : 0.000001
const value = Number(match[1]) * factor
return Number.isFinite(value) && value >= 0 ? value : null
}
export function readBandwidthTestRates(rows: readonly Record<string, unknown>[]) {
function rate(row: Record<string, unknown>, keys: string[]) {
for (const key of keys) {
const value = parseBandwidthRateMbps(row[key])
if (value !== null) return value
}
return null
}
function series(side: "tx" | "rx") {
return rows.flatMap(row => {
const value = rate(row, [`${side}-current`, `${side}-10-second-average`, `${side}-total-average`])
return value === null ? [] : [value]
})
}
const txSeries = series("tx"), rxSeries = series("rx"), last = rows.at(-1) ?? {}
const average = (side: "tx" | "rx", values: number[]) =>
rate(last, [`${side}-total-average`, `${side}-10-second-average`]) ??
(values.length ? values.reduce((sum, value) => sum + value / values.length, 0) : null)
return { txAvgMbps: average("tx", txSeries), rxAvgMbps: average("rx", rxSeries), txSeries, rxSeries }
}
+4 -31
View File
@@ -7,6 +7,7 @@ import type { RosIpAddress } from "../types/server.js"
import { withBtestNodeLocks } from "./btest-node-locks.js"
import { MikrotikClient } from "./mikrotik.js"
import { parsePing } from "./uptime-collector.js"
import { readBandwidthTestRates } from "./bandwidth-test-rates.js"
function firstUsableIpOnInterface(rows: RosIpAddress[], iface: string): string {
const name = iface.trim()
@@ -26,24 +27,6 @@ function firstUsableIpOnInterface(rows: RosIpAddress[], iface: string): string {
return raw ? String(raw).split("/")[0]?.trim() ?? "" : ""
}
function parseRateToMbps(raw: unknown): number {
const txt = String(raw ?? "").trim().toLowerCase()
if (!txt) return 0
const m = txt.match(/^(\d+(?:\.\d+)?)\s*(g|m|k)?(?:bps|bit\/s|bits\/s)?$/)
if (m) {
const n = Number.parseFloat(m[1])
const u = m[2] ?? ""
if (!Number.isFinite(n)) return 0
if (u === "g") return n * 1000
if (u === "m") return n
if (u === "k") return n / 1000
return n / 1_000_000
}
const n = Number.parseFloat(txt.replace(/[^\d.]/g, ""))
if (!Number.isFinite(n)) return 0
return n > 10000 ? n / 1_000_000 : n
}
export interface SpeedTestRunParams {
runId?: string
probeId?: string
@@ -68,8 +51,8 @@ export interface SpeedTestRunResult {
protocol: "tcp" | "udp"
direction: "transmit" | "receive" | "both"
durationSec: number
txAvgMbps: number
rxAvgMbps: number
txAvgMbps: number | null
rxAvgMbps: number | null
txSeries: number[]
rxSeries: number[]
raw: Array<Record<string, string>>
@@ -123,17 +106,7 @@ export async function executeBandwidthSpeedTestInner(params: SpeedTestRunParams)
direction,
durationSec,
})
const txSeries = rows
.map((r) => parseRateToMbps(r["tx-current"] ?? r["tx-10-second-average"] ?? r["tx-total-average"]))
.filter((v) => Number.isFinite(v) && v >= 0)
const rxSeries = rows
.map((r) => parseRateToMbps(r["rx-current"] ?? r["rx-10-second-average"] ?? r["rx-total-average"]))
.filter((v) => Number.isFinite(v) && v >= 0)
const last = rows[rows.length - 1] ?? {}
const txAvg = parseRateToMbps(last["tx-total-average"] ?? last["tx-10-second-average"])
const rxAvg = parseRateToMbps(last["rx-total-average"] ?? last["rx-10-second-average"])
const txFinal = txAvg || (txSeries.length ? txSeries.reduce((a, b) => a + b, 0) / txSeries.length : 0)
const rxFinal = rxAvg || (rxSeries.length ? rxSeries.reduce((a, b) => a + b, 0) / rxSeries.length : 0)
const { txAvgMbps: txFinal, rxAvgMbps: rxFinal, txSeries, rxSeries } = readBandwidthTestRates(rows)
await new Promise<void>((resolve) => setTimeout(resolve, 3000))