This commit is contained in:
@@ -256,6 +256,22 @@ describe("AnprBridge", () => {
|
|||||||
expect((skips[0].detail as { plate?: string }).plate).toBe("ZZ999ZZ");
|
expect((skips[0].detail as { plate?: string }).plate).toBe("ZZ999ZZ");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("analyzes AT LEAST ONE frame even if the poll window already elapsed (loaded host)", async () => {
|
||||||
|
// Regression for a CI flake (2026-07-04): with a plain `while`, a window that lapsed
|
||||||
|
// between deadline-set and loop-entry (slow runner; here forced with a 0ms window)
|
||||||
|
// meant ZERO analyze attempts — the detection was silently dropped ("gave up") and no
|
||||||
|
// skip was recorded. The do-while guarantees one frame per detection regardless of load.
|
||||||
|
process.env.ANPR_POLL_WINDOW_MS = "0";
|
||||||
|
const cam = seedCamera({ anpr: true });
|
||||||
|
const vision = fakeVision({ plate: "ZZ999ZZ", confidence: 0.97 });
|
||||||
|
const bridge = new AnprBridge(db, vision, fakeSubFlow(null), silentLogger());
|
||||||
|
|
||||||
|
await captureReads(() => bridge.onVehicleDetected(cam));
|
||||||
|
expect(captureSnapshot).toHaveBeenCalledTimes(1); // the guaranteed first attempt
|
||||||
|
const skips = db.select().from(deviceEventsTable).where(eq(deviceEventsTable.kind, "anpr-skip")).all();
|
||||||
|
expect(skips).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
it("debounces: two vehicle events within the window analyze/emit at most once", async () => {
|
it("debounces: two vehicle events within the window analyze/emit at most once", async () => {
|
||||||
const cam = seedCamera({ anpr: true });
|
const cam = seedCamera({ anpr: true });
|
||||||
const vision = fakeVision({ plate: "AA111BB", confidence: 0.97 });
|
const vision = fakeVision({ plate: "AA111BB", confidence: 0.97 });
|
||||||
|
|||||||
@@ -192,7 +192,12 @@ export class AnprBridge {
|
|||||||
const hardCap = Date.now() + this.#pollMaxMs;
|
const hardCap = Date.now() + this.#pollMaxMs;
|
||||||
let attempts = 0;
|
let attempts = 0;
|
||||||
try {
|
try {
|
||||||
while (Date.now() < Math.min(this.#pollDeadline.get(deviceId) ?? 0, hardCap)) {
|
// DO-while: a detection always analyzes AT LEAST ONE frame, however loaded the
|
||||||
|
// host — a plain while could zero-iterate if the window elapsed between setting
|
||||||
|
// the deadline and reaching the loop (seen as a CI flake with the tests' 5ms
|
||||||
|
// window; on a busy booth it would silently drop a real car's detection). Exit
|
||||||
|
// is via the breaks below (confident read, or next tick would pass the deadline).
|
||||||
|
do {
|
||||||
attempts++;
|
attempts++;
|
||||||
const shot = await camera.captureSnapshot({ direction });
|
const shot = await camera.captureSnapshot({ direction });
|
||||||
const r = await this.#vision.analyze(shot.bytes, shot.contentType);
|
const r = await this.#vision.analyze(shot.bytes, shot.contentType);
|
||||||
@@ -229,7 +234,7 @@ export class AnprBridge {
|
|||||||
const effDeadline = Math.min(this.#pollDeadline.get(deviceId) ?? 0, hardCap);
|
const effDeadline = Math.min(this.#pollDeadline.get(deviceId) ?? 0, hardCap);
|
||||||
if (Date.now() + this.#pollMs >= effDeadline) break;
|
if (Date.now() + this.#pollMs >= effDeadline) break;
|
||||||
await sleep(this.#pollMs);
|
await sleep(this.#pollMs);
|
||||||
}
|
} while (true);
|
||||||
} finally {
|
} finally {
|
||||||
this.#polling.delete(deviceId);
|
this.#polling.delete(deviceId);
|
||||||
this.#pollDeadline.delete(deviceId);
|
this.#pollDeadline.delete(deviceId);
|
||||||
|
|||||||
Reference in New Issue
Block a user