dd0f6e483a
Two genuinely-offline QR readers showed GREEN: the adapter's healthCheck was
hardcoded to { ready, "stub" } and never probed. These are PUSH devices (scan →
GET our backend, resolve by serial) with NO TCP port, so a connect probe has
nothing to hit — the stub "solved" that by lying. False-healthy is the worst
failure for a status bar.
- Optional reader IP field (monitor-ONLY; scans still resolve by serial,
operation unchanged).
- Unprivileged ICMP ping (drivers/icmp.ts): shells /bin/ping -c1, exit-0 = reply.
No native dep, no CAP_NET_RAW. docker-compose.prod.yml sets
net.ipv4.ping_group_range so it works for the non-root container user.
- healthCheck: replies → ready, no reply → offline, NO IP → degraded
("set IP to monitor") — never a false green.
Verified on hardware: readers (10.0.10.7/.8) answer ICMP on the device VLAN;
UI Test connection → "● ready — ping 10.0.10.7". Tests: reader.test.ts (4).
Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
44 lines
1.9 KiB
TypeScript
44 lines
1.9 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
|
|
// Reader health: push-only QR readers expose no TCP port, so liveness is an ICMP
|
|
// ping of the (optional) configured IP. With no IP we must NOT claim "ready" (the old
|
|
// stub did, hiding offline readers behind a green dot) — we report degraded instead.
|
|
// icmpPing is mocked so the test is deterministic + offline.
|
|
|
|
const icmpPing = vi.fn<(host: string, timeoutMs?: number) => Promise<boolean>>();
|
|
vi.mock("./icmp.js", () => ({ icmpPing: (...a: [string, number?]) => icmpPing(...a) }));
|
|
|
|
const { geeQrReaderDriver } = await import("./reader.js");
|
|
|
|
afterEach(() => {
|
|
icmpPing.mockReset();
|
|
});
|
|
|
|
describe("QR reader healthCheck (ICMP liveness)", () => {
|
|
it("with an IP that replies → ready", async () => {
|
|
icmpPing.mockResolvedValue(true);
|
|
const r = geeQrReaderDriver.create({ serial: "H05M2AFA", host: "10.0.10.7" });
|
|
expect(await r.healthCheck()).toEqual({ status: "ready", detail: "ping 10.0.10.7" });
|
|
expect(icmpPing).toHaveBeenCalledWith("10.0.10.7");
|
|
});
|
|
|
|
it("with an IP that does NOT reply → offline (this is the bug fix)", async () => {
|
|
icmpPing.mockResolvedValue(false);
|
|
const r = geeQrReaderDriver.create({ serial: "H05M2AFA", host: "10.0.10.7" });
|
|
expect(await r.healthCheck()).toEqual({ status: "offline", detail: "no ping reply from 10.0.10.7" });
|
|
});
|
|
|
|
it("with NO IP → degraded (never a false 'ready')", async () => {
|
|
const r = geeQrReaderDriver.create({ serial: "H05M2AFA" });
|
|
const h = await r.healthCheck();
|
|
expect(h.status).toBe("degraded");
|
|
expect(icmpPing).not.toHaveBeenCalled(); // nothing to ping
|
|
});
|
|
|
|
it("exposes an optional host field for monitoring", () => {
|
|
const hostField = geeQrReaderDriver.configFields.find((f) => f.key === "host");
|
|
expect(hostField).toBeDefined();
|
|
expect(hostField!.required).toBe(false); // operation is push-by-serial; IP is monitor-only
|
|
});
|
|
});
|