Files
Behavision/agent/pkg/bridge/bridge_test.go
Suriyakumarvijayanayagam dad04e8cda Behavision: face recognition for retail, edge to head office
Five components that ship as one product:

- behavision/  the recognition engine. RTSP ingest, YuNet detection, IoU
               tracking, ArcFace embeddings, a FAISS/SQLite gallery, and a
               FastAPI dashboard. Identity is decided once per TRACK from an
               average of at least three embeddings, never per frame.
- agent/       the Go edge agent: supervises the engine, holds a durable
               spool, and drains it to MQTT. Nothing is acked before the
               broker confirms.
- desktop/     the shop PC application (Wails + React + tray).
- server/      the cloud API, MQTT consumer, reports and assistant.
- web/         platform.loyaly.ai, the head-office app, embedded in the
               server binary.

The gallery stores 512-float embeddings and timestamps - no images unless
`app.store_faces` is switched on. Those embeddings are biometric personal
data under GDPR and India's DPDP: template inversion reconstructs a
recognisable face from an ArcFace vector, so data/behavision.db is treated
as a biometric database and DELETE /api/visitors/{id} is a real erasure.

CLAUDE.md carries the reasoning behind every non-obvious decision here,
including the ones that were measured and the ones that were wrong first.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HViLj9gYNRtSr7YVZmW5sn
2026-09-04 11:14:18 +05:30

289 lines
8.2 KiB
Go

package bridge
import (
"context"
"encoding/json"
"errors"
"net/http"
"net/http/httptest"
"strings"
"testing"
)
type fakeQueue struct {
topics []string
payloads []map[string]any
err error
}
func (q *fakeQueue) Append(topic string, payload any) error {
if q.err != nil {
return q.err
}
b, _ := json.Marshal(payload)
var m map[string]any
json.Unmarshal(b, &m)
q.topics = append(q.topics, topic)
q.payloads = append(q.payloads, m)
return nil
}
type fakeEmb struct {
calls int
err error
}
func (f *fakeEmb) Embedding(_ context.Context, id int64) ([]float32, string, error) {
f.calls++
if f.err != nil {
return nil, "", f.err
}
return make([]float32, 512), "w600k_r50.onnx", nil
}
func newBridge() (*Bridge, *fakeQueue, *fakeEmb) {
q, e := &fakeQueue{}, &fakeEmb{}
return &Bridge{Queue: q, Embeddings: e, TopicPrefix: "bv/acme.store1"}, q, e
}
func seen(id int64, ts float64) Event {
return Event{Type: "person.seen", CameraID: "entrance", TS: ts,
Data: map[string]any{"identity_id": float64(id), "similarity": 0.58,
"quality": 0.74, "gender": "Male", "age": float64(34)}}
}
func TestADetectionIsQueuedForTheRightTopic(t *testing.T) {
b, q, _ := newBridge()
if err := b.Handle(context.Background(), seen(7, 1787996491)); err != nil {
t.Fatal(err)
}
if len(q.topics) != 1 || q.topics[0] != "bv/acme.store1/visit" {
t.Fatalf("topics=%v", q.topics)
}
p := q.payloads[0]
if p["camera_id"] != "entrance" || p["is_new"] != false {
t.Fatalf("%+v", p)
}
if p["quality"] != 0.74 || p["similarity"] != 0.58 {
t.Fatalf("measurements lost: %+v", p)
}
}
func TestTheEventIDIsStableForTheSameSighting(t *testing.T) {
// This is what makes at-least-once delivery safe. A random id here would
// defeat the server's idempotency check and double the store's footfall
// after every reconnect.
b, q, _ := newBridge()
ctx := context.Background()
b.Handle(ctx, seen(7, 1787996491.20))
b.Handle(ctx, seen(7, 1787996491.86)) // same second, redelivered
if q.payloads[0]["event_id"] != q.payloads[1]["event_id"] {
t.Fatalf("ids differ: %v vs %v",
q.payloads[0]["event_id"], q.payloads[1]["event_id"])
}
}
func TestDifferentPeopleAndCamerasGetDifferentIDs(t *testing.T) {
b, q, _ := newBridge()
ctx := context.Background()
b.Handle(ctx, seen(7, 1787996491))
b.Handle(ctx, seen(8, 1787996491)) // different person, same instant
other := seen(7, 1787996491)
other.CameraID = "till"
b.Handle(ctx, other) // same person, different camera
ids := map[any]bool{}
for _, p := range q.payloads {
ids[p["event_id"]] = true
}
if len(ids) != 3 {
t.Fatalf("collided: %d distinct ids from 3 sightings", len(ids))
}
}
func TestLocalDiagnosticsAreNotCountedAsVisits(t *testing.T) {
// person.missed and camera.up are real events, but sending them down the
// footfall stream would inflate the headcount with things that are not
// people.
b, q, _ := newBridge()
for _, typ := range []string{"person.missed", "camera.up", "camera.down",
"identity.merged"} {
b.Handle(context.Background(), Event{Type: typ, CameraID: "entrance"})
}
if len(q.payloads) != 0 {
t.Fatalf("queued %d non-visits", len(q.payloads))
}
if b.Skipped != 4 {
t.Fatalf("skipped=%d", b.Skipped)
}
}
func TestAnUnclaimedPCQueuesNothing(t *testing.T) {
// Without a tenant prefix an event would be addressed to nowhere, and the
// broker would refuse it anyway.
b, q, _ := newBridge()
b.TopicPrefix = ""
b.Handle(context.Background(), seen(7, 1))
if len(q.payloads) != 0 {
t.Fatal("queued an event with no tenant")
}
}
func TestTheTemplateIsFetchedOncePerIdentity(t *testing.T) {
// A returning customer seen forty times a day would otherwise pull the
// same 512 floats out of SQLite forty times.
b, _, e := newBridge()
ctx := context.Background()
for i := 0; i < 5; i++ {
b.Handle(ctx, seen(7, float64(1787996491+i*60)))
}
if e.calls != 1 {
t.Fatalf("fetched the template %d times", e.calls)
}
}
func TestAMissingTemplateStillQueuesTheVisit(t *testing.T) {
// A footfall count without a template is still a real visit. Dropping it
// would lose the number the customer is paying for over an optional field.
b, q, e := newBridge()
e.err = errors.New("no embedding")
if err := b.Handle(context.Background(), seen(7, 1)); err != nil {
t.Fatal(err)
}
if len(q.payloads) != 1 {
t.Fatal("visit dropped because the template was missing")
}
if _, has := q.payloads[0]["embedding"]; has {
t.Fatal("queued an embedding key with no embedding")
}
}
func TestAttributesSurviveButBookkeepingIsDropped(t *testing.T) {
b, q, _ := newBridge()
b.Handle(context.Background(), seen(7, 1))
attrs := q.payloads[0]["attributes"].(map[string]any)
if attrs["gender"] != "Male" || attrs["age"] != float64(34) {
t.Fatalf("attributes lost: %+v", attrs)
}
for _, k := range []string{"identity_id", "similarity", "quality"} {
if _, dup := attrs[k]; dup {
t.Errorf("%q duplicated into attributes; it is already a column", k)
}
}
}
func TestMalformedJSONIsRejectedNotRetried(t *testing.T) {
b, _, _ := newBridge()
srv := httptest.NewServer(b.Handler())
defer srv.Close()
resp, err := http.Post(srv.URL+"/events", "application/json",
strings.NewReader("{ truncated"))
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusBadRequest {
t.Fatalf("status %d — the engine would retry this forever", resp.StatusCode)
}
}
func TestTheWebhookQueuesARealPost(t *testing.T) {
b, q, _ := newBridge()
srv := httptest.NewServer(b.Handler())
defer srv.Close()
body, _ := json.Marshal(seen(7, 1787996491))
resp, err := http.Post(srv.URL+"/events", "application/json",
strings.NewReader(string(body)))
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusNoContent {
t.Fatalf("status %d", resp.StatusCode)
}
if len(q.payloads) != 1 {
t.Fatal("nothing queued")
}
}
func TestListenBindsLoopbackOnly(t *testing.T) {
// The webhook accepts unauthenticated posts that become footfall rows;
// it must not be reachable from the network.
b, _, _ := newBridge()
url, stop, err := b.Listen(context.Background())
if err != nil {
t.Fatal(err)
}
defer stop()
if !strings.HasPrefix(url, "http://127.0.0.1:") {
t.Fatalf("bridge listening on %s", url)
}
}
// ---------------------------------------------------------------- waking
// Waking BEFORE the append would send the pump to look at a queue the event
// has not reached yet: it finds nothing, goes back to sleep, and the visit then
// waits out the full idle interval anyway - the exact delay the wake exists to
// remove, with an extra wasted drain on top.
func TestTheQueueIsWokenOnlyAfterTheVisitIsOnDisk(t *testing.T) {
b, q, _ := newBridge()
var depthWhenWoken = -1
b.Wake = func() { depthWhenWoken = len(q.payloads) }
if err := b.Handle(context.Background(), seen(7, 1_700_000_000)); err != nil {
t.Fatal(err)
}
if depthWhenWoken != 1 {
t.Fatalf("woken with %d events queued, want 1 - the event must be durable first",
depthWhenWoken)
}
}
// A visit that never reached the queue must not wake anything: there is nothing
// to drain, and the pump would spin on an empty spool.
func TestAFailedAppendDoesNotWakeThePump(t *testing.T) {
b, q, _ := newBridge()
q.err = errors.New("disk full")
woken := 0
b.Wake = func() { woken++ }
if err := b.Handle(context.Background(), seen(7, 1_700_000_000)); err == nil {
t.Fatal("a failed append should surface as an error")
}
if woken != 0 {
t.Fatalf("woke the pump %d times for an event that was never queued", woken)
}
}
// Diagnostics are not visits. They are skipped before the queue, so they must
// not wake a pump either.
func TestASkippedEventDoesNotWakeThePump(t *testing.T) {
b, _, _ := newBridge()
woken := 0
b.Wake = func() { woken++ }
ev := seen(7, 1_700_000_000)
ev.Type = "person.missed"
if err := b.Handle(context.Background(), ev); err != nil {
t.Fatal(err)
}
if woken != 0 {
t.Fatalf("a diagnostic event woke the pump %d times", woken)
}
}
// The bridge must run unchanged with no waker wired, which is what an agent
// built before this existed looks like.
func TestNoWakerIsFine(t *testing.T) {
b, q, _ := newBridge()
b.Wake = nil
if err := b.Handle(context.Background(), seen(7, 1_700_000_000)); err != nil {
t.Fatal(err)
}
if len(q.payloads) != 1 {
t.Fatalf("queued %d visits, want 1", len(q.payloads))
}
}