100 lines
5.2 KiB
TypeScript
100 lines
5.2 KiB
TypeScript
import { test } from "node:test"
|
|
import assert from "node:assert/strict"
|
|
import { assertSpeedProbeEndpoints, matchesSpeedProbeEndpoints, persistSpeedRun, SpeedProbeEndpointsChanged, assertSpeedProbeContext, matchesSpeedProbeContext, SpeedProbeContextChanged, type SpeedProbeContext } from "./speed-run-persistence.js"
|
|
const target = { id: "probe", srcServerId: 1, dstServerId: 2 }
|
|
type State = { probe: (typeof target & { average: number }) | null; runs: Array<{ probeId: string | null; average: number }> }
|
|
function harness(failure?: "update" | "insert" | "commit") {
|
|
let state: State = { probe: { ...target, average: 7 }, runs: [] }
|
|
let updates = 0
|
|
return {
|
|
state: () => state,
|
|
updates: () => updates,
|
|
deps: {
|
|
transaction: async (work: (tx: State) => Promise<void>) => {
|
|
const tx = structuredClone(state)
|
|
await work(tx)
|
|
if (failure === "commit") throw Error("commit failed")
|
|
state = tx
|
|
},
|
|
updateProbe: async (tx: State, expected: typeof target) => {
|
|
updates++
|
|
if (!matchesSpeedProbeEndpoints(tx.probe, expected)) return false
|
|
tx.probe!.average = 0
|
|
if (failure === "update") throw Error("update failed")
|
|
return true
|
|
},
|
|
insertRun: async (tx: State, probeId: string | null) => {
|
|
tx.runs.push({ probeId, average: 0 })
|
|
if (failure === "insert") throw Error("insert failed")
|
|
},
|
|
},
|
|
}
|
|
}
|
|
test("preflight rejects foreign, changed or deleted probe endpoints", () => {
|
|
assert.doesNotThrow(() => assertSpeedProbeEndpoints(target, target))
|
|
for (const row of [null, undefined, {}, { ...target, id: "other" }, { ...target, srcServerId: 3 }, { ...target, dstServerId: 3 }, { ...target, srcServerId: "1" }]) {
|
|
assert.throws(() => assertSpeedProbeEndpoints(row, target), SpeedProbeEndpointsChanged)
|
|
}
|
|
})
|
|
test("one transaction publishes an explicit zero snapshot and associated history", async () => {
|
|
const h = harness()
|
|
await persistSpeedRun(target, h.deps)
|
|
assert.equal(h.state().probe!.average, 0)
|
|
assert.deepEqual(h.state().runs, [{ probeId: "probe", average: 0 }])
|
|
assert.equal(h.updates(), 1)
|
|
})
|
|
for (const changed of ["source", "destination", "deleted"] as const) test(`probe ${changed} during native test retains its snapshot and unlinks old run`, async () => {
|
|
const h = harness()
|
|
if (changed === "deleted") h.state().probe = null
|
|
else h.state().probe![changed === "source" ? "srcServerId" : "dstServerId"] = 3
|
|
await persistSpeedRun(target, h.deps)
|
|
assert.equal(h.state().probe?.average ?? 7, 7)
|
|
assert.deepEqual(h.state().runs, [{ probeId: null, average: 0 }])
|
|
})
|
|
for (const failure of ["update", "insert", "commit"] as const) test(`${failure} error never publishes a partial snapshot/history`, async () => {
|
|
const h = harness(failure)
|
|
await assert.rejects(persistSpeedRun(target, h.deps), new RegExp(failure))
|
|
assert.equal(h.state().probe!.average, 7)
|
|
assert.deepEqual(h.state().runs, [])
|
|
})
|
|
test("ad-hoc run writes history without touching any probe", async () => {
|
|
const h = harness()
|
|
await persistSpeedRun(null, h.deps)
|
|
assert.equal(h.state().probe!.average, 7)
|
|
assert.equal(h.updates(), 0)
|
|
assert.deepEqual(h.state().runs, [{ probeId: null, average: 0 }])
|
|
})
|
|
|
|
const context: SpeedProbeContext = { ...target, srcInterface: "ether1", dstInterface: "ether2", protocol: "tcp", direction: "both", durationSec: 10 }
|
|
const edits: Partial<SpeedProbeContext>[] = [{ srcInterface: "ether3" }, { dstInterface: "ether3" }, { protocol: "udp" }, { direction: "receive" }, { durationSec: 20 }]
|
|
test("preflight validates full context and ignores enable/order/telemetry changes", () => {
|
|
assert.doesNotThrow(() => assertSpeedProbeContext({ ...context, enabled: false, sortOrder: 9, lastTxAvgMbps: 0 }, context))
|
|
for (const edit of edits) assert.throws(() => assertSpeedProbeContext({ ...context, ...edit }, context), SpeedProbeContextChanged)
|
|
assert.throws(() => assertSpeedProbeContext(null, context), SpeedProbeEndpointsChanged)
|
|
})
|
|
for (const edit of edits) test(`changed ${Object.keys(edit)[0]} cannot publish success or failure into new context`, async () => {
|
|
for (const status of ["done", "error"]) {
|
|
const probe = { ...context, ...edit, average: 7, status: "done" }
|
|
const runs: Array<{ probeId: string | null; status: string }> = []
|
|
await persistSpeedRun(context, {
|
|
transaction: async work => work(probe),
|
|
updateProbe: async (_tx, expected) => {
|
|
if (!matchesSpeedProbeContext(probe, expected)) return false
|
|
probe.average = 0; probe.status = status; return true
|
|
},
|
|
insertRun: async (_tx, probeId) => { runs.push({ probeId, status }) },
|
|
})
|
|
assert.equal(probe.average, 7); assert.equal(probe.status, "done")
|
|
assert.deepEqual(runs, [{ probeId: null, status }])
|
|
}
|
|
})
|
|
test("matching full context publishes explicit zero with associated history", async () => {
|
|
const probe = { ...context, average: 7 }; let associated: string | null = null
|
|
await persistSpeedRun(context, {
|
|
transaction: async work => work(probe),
|
|
updateProbe: async (_tx, expected) => { assert.equal(matchesSpeedProbeContext(probe, expected), true); probe.average = 0; return true },
|
|
insertRun: async (_tx, probeId) => { associated = probeId },
|
|
})
|
|
assert.equal(probe.average, 0); assert.equal(associated, context.id)
|
|
})
|