Files

139 lines
6.2 KiB
Go

package brain_control_test
import (
"crypto/ed25519"
"crypto/rand"
"encoding/json"
"strings"
"sync"
"testing"
"time"
bc "git.ilapage.cn/ila/yovision/Sense/server/app/sense/integration/brain_control"
mi "git.ilapage.cn/ila/yovision/Sense/server/app/sense/integration/machine_identity"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
func testDB(t *testing.T, name string) *gorm.DB {
t.Helper()
db, err := gorm.Open(sqlite.Open("file:"+name+"?mode=memory&cache=shared"), &gorm.Config{})
if err != nil {
t.Fatal(err)
}
if err = db.AutoMigrate(&bc.ReplayToken{}, &bc.RuntimeProjection{}, &bc.SourceRevision{}); err != nil {
t.Fatal(err)
}
return db
}
func TestMapperProducesCredentialFreeFrozenContract(t *testing.T) {
now := time.Date(2026, 8, 31, 0, 0, 0, 0, time.UTC)
c, err := bc.MapSourceConfig(bc.SourceFacts{ConfigID: "gate-primary", SiteID: "site-east", LogicalDeviceID: "camera-1", MediaPath: "site-east/camera-1/main", Revision: 1, PublishedAt: now, EffectiveAt: now, Profile: bc.Profile{ID: "main", Width: 1920, Height: 1080, Encoding: "h264", FrameRate: 25}, RuleSetVersion: "rules-1", Areas: []bc.AreaRule{{ID: "danger", Version: 1, Kind: "danger_area", Enabled: true, Points: []bc.Point{{X: .1, Y: .1}, {X: .8, Y: .1}, {X: .5, Y: .8}}}}})
if err != nil {
t.Fatal(err)
}
if err = bc.ValidateSourceConfig(c); err != nil {
t.Fatal(err)
}
raw, _ := json.Marshal(c)
text := strings.ToLower(string(raw))
for _, secret := range []string{"password", "username", "rtsp://", "stream_uri", "credential"} {
if strings.Contains(text, secret) {
t.Fatalf("leaked %q", secret)
}
}
c2, err := bc.MapSourceConfig(bc.SourceFacts{ConfigID: "gate-primary", SiteID: "site-east", LogicalDeviceID: "camera-1", MediaPath: "site-east/camera-1/main", Revision: 2, PublishedAt: now, EffectiveAt: now, Profile: bc.Profile{ID: "main-v2", Width: 1280, Height: 720, Encoding: "H265", FrameRate: 20}, RuleSetVersion: "rules-2", NeedsRecalibration: true, Areas: []bc.AreaRule{{ID: "danger", Version: 2, Kind: "danger_area", Enabled: true, Points: []bc.Point{{X: .1, Y: .1}, {X: .8, Y: .1}, {X: .5, Y: .8}}}}})
if err != nil {
t.Fatal(err)
}
if c2.RuleSet.State != "recalibration_required" || c2.RuleSet.Areas[0].Enabled {
t.Fatal("recalibration must disable rules")
}
}
func TestDurableReplayIsAtomicAndSurvivesVerifierRestart(t *testing.T) {
db := testDB(t, "sense-replay")
pub, priv, _ := ed25519.GenerateKey(rand.Reader)
now := time.Date(2026, 8, 31, 0, 0, 0, 0, time.UTC)
registry, _ := mi.NewRegistry(mi.KeyRecord{Principal: "yv:brain:east", KeyID: "brain-key-01", PublicKey: pub, Audience: "yovision-sense", Scopes: []string{"runtime-status:write"}, Enabled: true})
signer := mi.Signer{Principal: "yv:brain:east", KeyID: "brain-key-01", PrivateKey: priv, Now: func() time.Time { return now }}
body := []byte(`{"ok":true}`)
token, _ := signer.Mint("yovision-sense", []string{"runtime-status:write"}, "POST", "/machine/v1/runtime-status", body)
results := make(chan bool, 8)
var wg sync.WaitGroup
for i := 0; i < 8; i++ {
wg.Add(1)
go func() {
defer wg.Done()
v := mi.Verifier{Registry: registry, Replay: bc.GORMReplayStore{DB: db}, Now: func() time.Time { return now }}
_, err := v.Verify(token, "yovision-sense", "runtime-status:write", "POST", "/machine/v1/runtime-status", body)
results <- err == nil
}()
}
wg.Wait()
close(results)
accepted := 0
for ok := range results {
if ok {
accepted++
}
}
if accepted != 1 {
t.Fatalf("accepted=%d", accepted)
}
v2 := mi.Verifier{Registry: registry, Replay: bc.GORMReplayStore{DB: db}, Now: func() time.Time { return now }}
if _, err := v2.Verify(token, "yovision-sense", "runtime-status:write", "POST", "/machine/v1/runtime-status", body); err == nil {
t.Fatal("replay accepted after verifier restart")
}
}
func TestRevisionStoreConcurrentAndRestart(t *testing.T) {
db := testDB(t, "sense-revisions")
store := bc.RevisionStore{DB: db}
for want := int64(1); want <= 3; want++ {
got, err := store.Next("gate-primary")
if err != nil || got != want {
t.Fatalf("got %d err %v", got, err)
}
}
restarted := bc.RevisionStore{DB: db}
got, err := restarted.Next("gate-primary")
if err != nil || got != 4 {
t.Fatalf("restart got %d err %v", got, err)
}
}
func TestRuntimeProjectionStaleRecoveryMismatchAndOrdering(t *testing.T) {
db := testDB(t, "sense-projection")
now := time.Date(2026, 8, 31, 0, 0, 0, 0, time.UTC)
store := bc.ProjectionStore{DB: db, Clock: func() time.Time { return now }, StaleAfter: 90 * time.Second}
raw := statusJSON("018f4d6a-8d1b-4a25-8b37-9085f9c0d101", 41, now, "running", 20)
view, err := store.Ingest(raw, map[string]int64{"gate-primary": 21})
if err != nil {
t.Fatal(err)
}
if !view.RevisionMismatch || view.Stale {
t.Fatalf("bad initial view %+v", view)
}
now = now.Add(91 * time.Second)
view, err = store.View("brain-east-01")
if err != nil || !view.Offline || !view.Stale {
t.Fatalf("offline %+v %v", view, err)
}
raw = statusJSON("018f4d6a-8d1b-4a25-8b37-9085f9c0d102", 42, now, "running", 21)
view, err = store.Ingest(raw, map[string]int64{"gate-primary": 21})
if err != nil || !view.Recovered || view.RevisionMismatch {
t.Fatalf("recovery %+v %v", view, err)
}
if _, err = store.Ingest(statusJSON("018f4d6a-8d1b-4a25-8b37-9085f9c0d103", 41, now, "running", 21), nil); err == nil || err.Error() != "OUT_OF_ORDER_STATUS" {
t.Fatalf("expected ordering rejection: %v", err)
}
}
func statusJSON(id string, seq int64, observed time.Time, state string, revision int64) []byte {
v := map[string]any{"schema_version": bc.RuntimeStatusVersion, "status_id": id, "brain_instance_ref": "brain-east-01", "sequence": seq, "observed_at": observed.Format(time.RFC3339), "runtime": map[string]any{"state": state, "version": "1.0.0", "started_at": observed.Add(-time.Minute).Format(time.RFC3339)}, "model": map[string]any{"model_ref": "people-detection", "version": "2026.08.1"}, "configurations": []any{map[string]any{"config_id": "gate-primary", "apply_state": "applied", "applied_revision": revision, "error_code": nil}}, "health": map[string]any{"overall": "healthy", "error_codes": []any{}, "metrics": map[string]any{"load_percent": 1.0, "queue_depth": 0, "latency_ms": 2.0}}, "inputs": []any{}}
raw, _ := json.Marshal(v)
return raw
}