139 lines
6.2 KiB
Go
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
|
|
}
|