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

This commit is contained in:
Denozordec committed 2026-10-07 14:17:36 +07:00
1 parent 9d5959601f
commit fb32f87d33
6 files changed
+62 -2

No files matched your search

@@ -0,0 +1,24 @@
import { test } from "node:test"
import assert from "node:assert/strict"
import { assertBandwidthTestCompleted } from "./bandwidth-test-status.js"
import { readBandwidthTestRates } from "./bandwidth-test-rates.js"
test("confirmed completion preserves one-direction unknown and explicit zero", () => {
const rows = [{ status: "connecting" }, { status: "running", "rx-current": "20Mbps" }, { status: "done testing", "rx-total-average": "0bps" }]
assert.doesNotThrow(() => assertBandwidthTestCompleted(rows))
const result = readBandwidthTestRates(rows)
assert.equal(result.txAvgMbps, null)
assert.equal(result.rxAvgMbps, 0)
assert.doesNotThrow(() => assertBandwidthTestCompleted([{ status: "done testing" }, { "tx-total-average": "10Mbps" }]))
})
test("all native failures reject even with valid rates or a subsequent success marker", () => {
for (const status of ["authentication failed", "can not connect", "can not start test", "remote is busy", "test unsupported", "disconnected"]) {
assert.throws(() => assertBandwidthTestCompleted([{ status, "tx-total-average": "50Mbps" }]))
assert.throws(() => assertBandwidthTestCompleted([{ status }, { status: "done testing" }]))
}
})
test("missing, incomplete and malformed native replies never count as completed", () => {
for (const raw of [null, {}, [], [null], [[]], [{ status: 8 }], [{ status: "" }], [{ "tx-total-average": "50Mbps" }], [{ status: "running" }], [{ status: "connecting" }], [{ status: "done testing" }, { status: "running" }], [{ status: "done testing" }, { status: "future status" }], [{ status: "done testing", error: "failure" }], [{ status: "done testing", failure: "failure" }], [{ status: "done testing", "!trap": true }]]) {
assert.throws(() => assertBandwidthTestCompleted(raw), /Результат операции неизвестен/)
}
})
@@ -0,0 +1,25 @@
const failures: Record<string, string> = {
"authentication failed": "RouterOS отклонил авторизацию bandwidth-test",
"can not connect": "Bandwidth-test не смог подключиться к назначению",
"can not start test": "RouterOS не смог начать bandwidth-test",
"remote is busy": "Устройство назначения занято",
"test unsupported": "Устройство назначения не поддерживает bandwidth-test",
"disconnected": "Соединение bandwidth-test прервано до завершения",
}
/** HTTP success is insufficient: the last reported native state must confirm completion. */
export function assertBandwidthTestCompleted(raw: unknown): asserts raw is Array<Record<string, unknown>> {
const unknown = () => new Error("RouterOS не подтвердил завершение bandwidth-test. Результат операции неизвестен")
if (!Array.isArray(raw) || !raw.length) throw unknown()
let lastStatus: string | undefined
for (const row of raw) {
if (!row || typeof row !== "object" || Array.isArray(row)) throw unknown()
if ("error" in row || "failure" in row || "!trap" in row) throw unknown()
if (row.status === undefined) continue
if (typeof row.status !== "string" || !row.status.trim()) throw unknown()
const status = row.status.trim().toLowerCase()
if (Object.hasOwn(failures, status)) throw new Error(failures[status])
lastStatus = status
}
if (lastStatus !== "done testing") throw unknown()
}
@@ -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 { assertBandwidthTestCompleted } from "./bandwidth-test-status.js"
import { readBandwidthTestRates } from "./bandwidth-test-rates.js"
function firstUsableIpOnInterface(rows: RosIpAddress[], iface: string): string {
@@ -106,6 +107,7 @@ export async function executeBandwidthSpeedTestInner(params: SpeedTestRunParams)
direction,
durationSec,
})
assertBandwidthTestCompleted(rows)
const { txAvgMbps: txFinal, rxAvgMbps: rxFinal, txSeries, rxSeries } = readBandwidthTestRates(rows)
await new Promise<void>((resolve) => setTimeout(resolve, 3000))
+1 -1
View File
@@ -1,6 +1,6 @@
# План реализации UX/UI по пятичасовым окнам
Актуальное продолжение 2026-10-07: 12.3.73 завершён: nullable bandwidth producer/POST consumer, проверка runId, 16pure/71browser/8context и frontend/backend build. Следующий12.3.74: terminal confirmation RouterOS; затем75OSPF RX/TX mapping,76mobile pinned-column readable geometry. Команды/права не менять,12.5 отдельно.
Актуальное продолжение 2026-10-07: 12.3.74 завершён: backend проверяет native terminal status перед Ping/DB success write,16pure и backend build. Следующий12.3.75: OSPF RX/TX mapping; затем76mobile pinned-column readable geometry. Команды/права не менять,12.5 отдельно.
Общий план проекта MikrotikManager. Основан на согласованном плане из задачи; точка продолжения обновлена 2026-10-04. Источник фактического прогресса, проверок и ограничений — [журнал реализации](ux-ui-progress.md). Этот документ задаёт объём и критерии завершения, журнал фиксирует результат.
+1 -1
View File
@@ -1,6 +1,6 @@
# UX/UI: журнал реализации
Актуальное продолжение 2026-10-07: 12.3.73 завершён: nullable bandwidth producer/POST consumer, проверка runId, 16pure/71browser/8context и frontend/backend build. Следующий12.3.74: terminal confirmation RouterOS; затем75OSPF RX/TX mapping,76mobile pinned-column readable geometry. Команды/права не менять,12.5 отдельно.
Актуальное продолжение 2026-10-07: 12.3.74 завершён: backend проверяет native terminal status перед Ping/DB success write,16pure и backend build. Следующий12.3.75: OSPF RX/TX mapping; затем76mobile pinned-column readable geometry. Команды/права не менять,12.5 отдельно.
Общий план: [UX/UI по этапам и пятичасовым окнам](ux-ui-plan.md). План задаёт объём, этот журнал фиксирует выполненные блоки и ограничения.
+9
View File
@@ -817,3 +817,12 @@ CPU final:1000routes13.3ms;100000(10×100×100)49.7ms;100000(100×10×100)44.0ms
7 pure parser/DTO +9 isolated scheduler journal tests прошли.71 main browser (uptime-links, speed-data, large-speed-history),8 context прошли; non-incremental TypeScript, targeted lint (9 существующих предупреждений uptime,0 errors), backend и frontend production build успешны. Первые два новых браузерных прогона обнаружили неверный KPI role selector и неоднозначность одинаковой подписи в истории/строке; selectors уточнены, финальный связанный профиль полностью passed. Historical large-speed-history-320 PNG восстановлен.
Следующий74: native terminal status ещё не проверяется перед done/DB write.75OSPF direction и76mobile metric geometry остаются. DTO доказывает целостность ответа, но не факт выполнения на устройстве. Реальная RouterOS/PG приёмка12.5 отдельно.
## 12.3.74 — подтверждённое завершение bandwidth-test
2026-10-07. Pure guard проверяет структуру native rows, все известные failure statuses и последний сообщённый status. Только done testing допускает дальнейший расчёт, after-ping и DB success write. Connecting/running, отсутствие статуса, неизвестный status, malformed response или error/failure/trap marker отклоняются с неизвестным результатом; native failures имеют краткие русские сообщения. Числовой enum не принимается как строковое подтверждение. Подтверждённый one-direction result может иметь null на другой стороне и0 на измеренной.
16 pure tests (3status,4rates,9scheduler) и backend build passed. Проверка placement/callers по source: HTTP и scheduled collection используют общий execute, их catch вызывает прежний failure recorder, который сохраняет старые probe averages и пишет error run с nullable rates. Полная DB/network orchestration не запускалась; это source review и pure guard tests, а не внешняя приёмка. Команда bandwidth-test и её параметры, locks, routing rules, permissions и DB schema не менялись.
Источник native statuses: [RouterOS CLI bandwidth-test](https://manual.mikrotik.com/docs/cli-reference/tool/bandwidth-test/). Последний reported done допускает последующие rows без status, но любые failure markers отклоняются даже после completion. Реальный REST shape/версия RouterOS и PG writes требуют12.5. Следующие75direction OSPF и76mobile geometry.