From 29052d27cdfc3d37fcd771facf326534a49ff4df Mon Sep 17 00:00:00 2001 From: phantomptr Date: Sun, 27 Sep 2026 23:20:37 -0700 Subject: [PATCH] feat(viewer): firmware, installed-version and space checks Co-Authored-By: Claude Opus 5.5 --- client/src/lib/packageChecks.test.ts | 28 +++++++++++++++ client/src/lib/packageChecks.ts | 51 ++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+) create mode 100644 client/src/lib/packageChecks.test.ts create mode 100644 client/src/lib/packageChecks.ts diff --git a/client/src/lib/packageChecks.test.ts b/client/src/lib/packageChecks.test.ts new file mode 100644 index 00000000..635beaed --- /dev/null +++ b/client/src/lib/packageChecks.test.ts @@ -0,0 +1,28 @@ +import { describe, expect, it } from "vitest"; +import { firmwareCheck, installedCheck, spaceCheck, versionCompare } from "./packageChecks"; + +describe("package checks", () => { + it("firmware: bad when the console is older than the package needs", () => { + expect(firmwareCheck("12.00", "5.10").verdict).toBe("bad"); + expect(firmwareCheck("5.10", "9.60").verdict).toBe("ok"); + expect(firmwareCheck("5.10", "5.10").verdict).toBe("ok"); + expect(firmwareCheck(null, "9.60").verdict).toBe("unknown"); + expect(firmwareCheck("5.10", null).verdict).toBe("unknown"); + }); + it("versions compare numerically across 2- and 3-part forms", () => { + expect(versionCompare("01.04", "01.02")).toBeGreaterThan(0); + expect(versionCompare("01.002.000", "01.002")).toBe(0); + expect(versionCompare("01.10", "01.9")).toBeGreaterThan(0); + }); + it("installed: newer / older / same / not installed", () => { + expect(installedCheck("01.04", null).relation).toBe("not-installed"); + expect(installedCheck("01.04", "01.02").relation).toBe("newer"); + expect(installedCheck("01.02", "01.04").relation).toBe("older"); + expect(installedCheck("01.04", "01.04").relation).toBe("same"); + }); + it("space: bad with the shortfall when it clearly doesn't fit; unknown without a reading", () => { + expect(spaceCheck(100, 40)).toEqual({ verdict: "bad", shortBy: 60 }); + expect(spaceCheck(100, 400).verdict).toBe("ok"); + expect(spaceCheck(100, null).verdict).toBe("unknown"); + }); +}); diff --git a/client/src/lib/packageChecks.ts b/client/src/lib/packageChecks.ts new file mode 100644 index 00000000..d967c2c1 --- /dev/null +++ b/client/src/lib/packageChecks.ts @@ -0,0 +1,51 @@ +// Checks the package viewer shows against the selected console: can it run +// this package, is it already installed, and does it fit. Pure, so the panel +// stays a view and these stay tested. + +export type Verdict = "ok" | "warn" | "bad" | "unknown"; + +/** "01.002.000" / "1.2" → [1, 2, 0] for comparison. */ +function parts(v: string): number[] { + return v.split(".").map((p) => parseInt(p, 10) || 0); +} + +/** Numeric, part-by-part; missing parts count as 0 ("01.002" = "01.002.000"). */ +export function versionCompare(a: string, b: string): number { + const x = parts(a); + const y = parts(b); + for (let i = 0; i < Math.max(x.length, y.length); i++) { + const d = (x[i] ?? 0) - (y[i] ?? 0); + if (d !== 0) return d; + } + return 0; +} + +/** Whether the console's firmware meets the package's minimum. */ +export function firmwareCheck(minFw: string | null, consoleFw: string | null) { + const verdict: Verdict = + !minFw || !consoleFw + ? "unknown" + : versionCompare(consoleFw, minFw) >= 0 + ? "ok" + : "bad"; + return { verdict, minFw, consoleFw }; +} + +/** This package against the version installed on the console. */ +export function installedCheck(pkgVer: string, installedVer: string | null) { + if (!installedVer) { + return { verdict: "ok" as Verdict, relation: "not-installed" as const }; + } + const c = versionCompare(pkgVer, installedVer); + if (c > 0) return { verdict: "ok" as Verdict, relation: "newer" as const }; + if (c < 0) return { verdict: "warn" as Verdict, relation: "older" as const }; + return { verdict: "warn" as Verdict, relation: "same" as const }; +} + +/** Whether `size` fits in `freeBytes`; unknown when free space can't be read. */ +export function spaceCheck(size: number, freeBytes: number | null) { + if (freeBytes == null) return { verdict: "unknown" as Verdict, shortBy: 0 }; + return size <= freeBytes + ? { verdict: "ok" as Verdict, shortBy: 0 } + : { verdict: "bad" as Verdict, shortBy: size - freeBytes }; +}