Files
Behavision/agent/pkg/cameras/cameras_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

242 lines
7.1 KiB
Go

package cameras
import (
"context"
"errors"
"testing"
)
type fakeEngine struct {
cams map[string]Local
added []string
updated []string
removed []string
listErr error
snapshot []byte
}
func newEngine(cams ...Local) *fakeEngine {
m := map[string]Local{}
for _, c := range cams {
m[c.ID] = c
}
return &fakeEngine{cams: m, snapshot: []byte("\xff\xd8jpeg")}
}
func (f *fakeEngine) List(context.Context) ([]Local, error) {
if f.listErr != nil {
return nil, f.listErr
}
var out []Local
for _, c := range f.cams {
out = append(out, c)
}
return out, nil
}
func (f *fakeEngine) Add(_ context.Context, c Local) error {
f.cams[c.ID] = c
f.added = append(f.added, c.ID)
return nil
}
func (f *fakeEngine) Update(_ context.Context, id string, c Local) error {
c.ID = id
f.cams[id] = c
f.updated = append(f.updated, id)
return nil
}
func (f *fakeEngine) Remove(_ context.Context, id string) error {
delete(f.cams, id)
f.removed = append(f.removed, id)
return nil
}
func (f *fakeEngine) Snapshot(context.Context, string) ([]byte, error) {
return f.snapshot, nil
}
type fakeCloud struct {
desired []Desired
desiredErr error
reports []Report
uploads int
uploadErr error
}
func (f *fakeCloud) Desired(context.Context) ([]Desired, error) {
return f.desired, f.desiredErr
}
func (f *fakeCloud) Report(_ context.Context, r Report) error {
f.reports = append(f.reports, r)
return nil
}
func (f *fakeCloud) UploadSnapshot(context.Context, []byte) (string, error) {
if f.uploadErr != nil {
return "", f.uploadErr
}
f.uploads++
return "snap/key.jpg", nil
}
func syncer(e *fakeEngine, c *fakeCloud) *Syncer {
return &Syncer{Engine: e, Cloud: c}
}
func TestACameraAddedAtHeadOfficeAppearsOnTheShopPC(t *testing.T) {
e, c := newEngine(), &fakeCloud{desired: []Desired{{
CameraID: "entrance", Label: "Entrance", Host: "192.168.0.138",
Port: 554, Path: "/ch0_0.264", Username: "admin", Password: "s3cret",
MaxWidth: 1280, Enabled: true, Revision: 1,
}}}
syncer(e, c).Once(context.Background())
got, ok := e.cams["entrance"]
if !ok {
t.Fatal("the camera was never created on the PC")
}
if got.Host != "192.168.0.138" || got.Password != "s3cret" {
t.Fatalf("connection details did not travel: %+v", got)
}
}
// The whole reason adoption exists. Every existing site is already running
// cameras configured locally - including the office camera this was tested with
// - and a reconcile that only pushed downwards would delete all of them the
// first time it ran.
func TestACameraAlreadyRunningLocallyIsOfferedToHeadOfficeNotDeleted(t *testing.T) {
e := newEngine(Local{ID: "office", Label: "Office", Host: "192.168.0.138",
Port: 554, Username: "admin", Password: "s3cret", Connected: true})
c := &fakeCloud{}
syncer(e, c).Once(context.Background())
if _, ok := e.cams["office"]; !ok {
t.Fatal("an existing camera was deleted by the first sync")
}
if len(c.reports) == 0 || len(c.reports[0].Adopt) != 1 {
t.Fatalf("the camera was not offered for adoption: %+v", c.reports)
}
if got := c.reports[0].Adopt[0]; got.CameraID != "office" || got.Password != "s3cret" {
t.Fatalf("adoption dropped details the camera needs: %+v", got)
}
}
// A tombstone must win over adoption, or a deleted camera comes straight back
// on the next sync and the operator cannot work out why.
func TestADeletedCameraIsRemovedAndNotReadopted(t *testing.T) {
e := newEngine(Local{ID: "entrance", Host: "10.0.0.5", Connected: true})
c := &fakeCloud{desired: []Desired{
{CameraID: "entrance", Enabled: true, Revision: 3, Deleted: true},
}}
s := syncer(e, c)
s.Once(context.Background())
if _, ok := e.cams["entrance"]; ok {
t.Fatal("a camera deleted at head office is still running")
}
for _, r := range c.reports {
for _, a := range r.Adopt {
if a.CameraID == "entrance" {
t.Fatal("the deleted camera was offered back for adoption")
}
}
}
}
// A PATCH restarts the camera connection, so re-applying unchanged config every
// two minutes would make a healthy site drop its own video permanently.
func TestUnchangedConfigurationTouchesTheEngineOnce(t *testing.T) {
e := newEngine()
c := &fakeCloud{desired: []Desired{{CameraID: "entrance", Host: "10.0.0.5",
Enabled: true, Revision: 7}}}
s := syncer(e, c)
s.Once(context.Background())
s.Once(context.Background())
s.Once(context.Background())
if len(e.added) != 1 {
t.Fatalf("added %d times, want 1", len(e.added))
}
if len(e.updated) != 0 {
t.Fatalf("updated %d times with no change - every one restarts the stream", len(e.updated))
}
}
func TestANewRevisionIsApplied(t *testing.T) {
e := newEngine()
c := &fakeCloud{desired: []Desired{{CameraID: "entrance", Host: "10.0.0.5",
Enabled: true, Revision: 1}}}
s := syncer(e, c)
s.Once(context.Background())
c.desired[0].Host = "10.0.0.9"
c.desired[0].Revision = 2
s.Once(context.Background())
if len(e.updated) != 1 {
t.Fatalf("updated %d times, want 1", len(e.updated))
}
if e.cams["entrance"].Host != "10.0.0.9" {
t.Fatalf("the new address was not applied: %+v", e.cams["entrance"])
}
}
// An unclaimed PC, or one with no internet, must keep running the cameras it
// already has. Wiping local config because head office is unreachable would
// stop a shop recognising anybody for the duration of an outage.
func TestAnUnreachableHeadOfficeChangesNothingLocally(t *testing.T) {
e := newEngine(Local{ID: "entrance", Host: "10.0.0.5", Connected: true})
c := &fakeCloud{desiredErr: errors.New("this PC is not claimed by a company yet")}
syncer(e, c).Once(context.Background())
if _, ok := e.cams["entrance"]; !ok {
t.Fatal("local cameras were removed because the cloud was unreachable")
}
if len(e.removed) != 0 {
t.Fatalf("removed %v", e.removed)
}
}
// Knowing a camera is down matters far more than having a picture of it, and
// the picture is the part most likely to fail.
func TestAFailedSnapshotStillReportsWhetherTheCameraIsUp(t *testing.T) {
e := newEngine(Local{ID: "entrance", Connected: true})
c := &fakeCloud{uploadErr: errors.New("bucket unreachable")}
syncer(e, c).Once(context.Background())
if len(c.reports) == 0 || len(c.reports[0].State) != 1 {
t.Fatalf("no state was reported: %+v", c.reports)
}
st := c.reports[0].State[0]
if !st.Connected {
t.Error("connected state was lost with the snapshot")
}
if st.SnapshotKey != "" {
t.Error("a failed upload reported a key anyway")
}
}
// No point photographing a camera that is not producing frames, and the attempt
// costs a request per sync per dead camera.
func TestADisconnectedCameraIsNotPhotographed(t *testing.T) {
e := newEngine(Local{ID: "entrance", Connected: false})
c := &fakeCloud{}
syncer(e, c).Once(context.Background())
if c.uploads != 0 {
t.Fatalf("uploaded %d snapshots of a disconnected camera", c.uploads)
}
if c.reports[0].State[0].Connected {
t.Error("a disconnected camera was reported as up")
}
}
func TestAnEngineThatIsNotRunningIsNotAnError(t *testing.T) {
e := newEngine()
e.listErr = errors.New("connection refused")
c := &fakeCloud{desired: []Desired{{CameraID: "entrance", Enabled: true, Revision: 1}}}
syncer(e, c).Once(context.Background()) // must not panic
if len(c.reports) != 0 {
t.Fatal("reported state it could not have observed")
}
}