Files
Behavision/server/internal/contract/contract_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

127 lines
3.6 KiB
Go

package contract
import (
"errors"
"strings"
"testing"
"time"
)
func validVisit() Visit {
return Visit{EventID: "evt-1", OccurredAt: time.Now(), CameraID: "entrance"}
}
func TestTopicMustCarryClientAndSite(t *testing.T) {
got, err := ParseTopic("bv/acme.store1/visit")
if err != nil {
t.Fatal(err)
}
if got.Client != "acme" || got.Site != "store1" || got.Kind != "visit" {
t.Fatalf("%+v", got)
}
if got.Username != "acme.store1" {
t.Fatalf("username %q must match what the broker authenticated", got.Username)
}
}
func TestTopicWithExtraDotsIsRejected(t *testing.T) {
// Splitting on the FIRST dot would read "acme.store.1" as client "acme",
// site "store.1" - a different site than the broker authenticated, and a
// way to write rows against a tenant you do not own.
for _, topic := range []string{
"bv/acme.store.1/visit",
"bv/acme/visit",
"bv/.store1/visit",
"bv/acme./visit",
"other/acme.store1/visit",
"bv/acme.store1",
} {
if _, err := ParseTopic(topic); err == nil {
t.Errorf("%q was accepted", topic)
}
}
}
func TestCommandSubtopicSurvivesParsing(t *testing.T) {
got, err := ParseTopic("bv/acme.store1/cmd/reload/now")
if err != nil {
t.Fatal(err)
}
if got.Kind != "cmd" || got.Rest != "reload/now" {
t.Fatalf("%+v", got)
}
}
func TestEventIDIsRequired(t *testing.T) {
// It is the idempotency key. Without it an at-least-once redelivery
// silently doubles a store's footfall - the one number they pay for.
v := validVisit()
v.EventID = " "
assertPermanent(t, v.Validate(), "event_id")
}
func TestAFutureTimestampIsRejectedNotClamped(t *testing.T) {
// A site with a skewed clock would otherwise park a visit at the top of
// every "recent" report forever. Clamping it silently makes the report a
// lie that looks fine.
v := validVisit()
v.OccurredAt = time.Now().Add(72 * time.Hour)
assertPermanent(t, v.Validate(), "clock")
}
func TestSlightlyFutureIsToleratedForClockSkew(t *testing.T) {
v := validVisit()
v.OccurredAt = time.Now().Add(30 * time.Minute)
if err := v.Validate(); err != nil {
t.Fatalf("ordinary clock skew rejected: %v", err)
}
}
func TestWrongLengthEmbeddingIsRejected(t *testing.T) {
// A wrong-length vector cannot be compared with anything and would sit in
// the gallery poisoning every future search.
v := validVisit()
v.Model = "w600k_r50.onnx"
v.Embedding = make([]float32, 128)
assertPermanent(t, v.Validate(), "dimensions")
}
func TestEmbeddingWithoutAModelTagIsRejected(t *testing.T) {
// Vectors from different encoders occupy different spaces. An untagged one
// cannot be stored safely because nothing later can tell what it is.
v := validVisit()
v.Embedding = make([]float32, EmbeddingDim)
assertPermanent(t, v.Validate(), "model tag")
}
func TestAVisitWithNoEmbeddingIsValid(t *testing.T) {
// A site may be configured to keep templates local and send only counts.
v := validVisit()
if err := v.Validate(); err != nil {
t.Fatalf("counts-only visit rejected: %v", err)
}
}
func TestValidationFailuresArePermanentSoTheQueueDrains(t *testing.T) {
// If a malformed message were retried forever, one bad payload at the head
// would stop every good one behind it - the same failure the agent's spool
// quarantine exists to prevent.
v := Visit{}
if !errors.Is(v.Validate(), ErrPermanent) {
t.Fatal("a malformed visit was not marked permanent")
}
}
func assertPermanent(t *testing.T, err error, want string) {
t.Helper()
if err == nil {
t.Fatalf("expected an error mentioning %q", want)
}
if !errors.Is(err, ErrPermanent) {
t.Fatalf("error is not permanent: %v", err)
}
if !strings.Contains(err.Error(), want) {
t.Fatalf("error %q does not mention %q", err, want)
}
}