package monitor import ( "context" "errors" "testing" "time" "silverpose/v2/internal/alert" "silverpose/v2/internal/fall" "silverpose/v2/internal/pose" "silverpose/v2/internal/source" ) func TestMonitorPublishesLatestCompletedFrameAndOneAlert(t *testing.T) { frames := make(chan source.Frame, 2) frames <- source.Frame{BGR: []byte{1, 1, 1, 1, 1, 1}, Width: 2, Height: 1, Sequence: 1} frames <- source.Frame{BGR: []byte{2, 2, 2, 2, 2, 2}, Width: 2, Height: 1, Sequence: 2} close(frames) statuses := make(chan source.Update, 1) statuses <- source.Update{Status: source.Connected, Message: "视频流已连接"} close(statuses) processor := &fakeProcessor{result: fall.FrameResult{ People: []fall.PersonAnalysis{{State: fall.Confirmed}}, Events: []fall.Event{{EventID: "FALL-run-000001", TrackID: "P-0001", ConfigVersion: "cfg-safe", State: fall.Confirmed}}, }} runtime := &fakeRuntime{output: make([]float32, pose.YOLOPoseOutputValues)} monitor, err := New(fakeSource{frames: frames, statuses: statuses}, runtime, processor, 0.25, alert.NewDispatcher(t.TempDir(), "lobby"), func() time.Time { return time.Date(2026, 7, 22, 8, 0, 0, 0, time.UTC) }) if err != nil { t.Fatal(err) } go monitor.Run(context.Background()) <-monitor.Done() update, ok := <-monitor.Updates() if !ok { t.Fatal("monitor emitted no update") } if update.Sequence != 2 || update.Image == nil || update.SourceStatus != source.Connected { t.Fatalf("update = %#v", update) } record, ok := <-monitor.Alerts() if !ok || !record.Written || record.Event.EventID != "FALL-run-000001" { t.Fatalf("alert record = %#v, open = %t", record, ok) } if !runtime.closed { t.Fatal("runtime was not closed") } } func TestMonitorDoesNotCreateFallEvidenceOnSourceFailure(t *testing.T) { frames := make(chan source.Frame) close(frames) statuses := make(chan source.Update, 1) statuses <- source.Update{Status: source.Retrying, Message: "视频流已断开,正在重连"} close(statuses) processor := &fakeProcessor{} monitor, err := New(fakeSource{frames: frames, statuses: statuses}, &fakeRuntime{}, processor, 0.25, alert.NewDispatcher(t.TempDir(), "lobby"), time.Now) if err != nil { t.Fatal(err) } go monitor.Run(context.Background()) <-monitor.Done() update := <-monitor.Updates() if update.SourceStatus != source.Retrying || len(update.Events) != 0 || processor.calls != 0 { t.Fatalf("update = %#v, process calls = %d", update, processor.calls) } } type fakeSource struct { frames <-chan source.Frame statuses <-chan source.Update } func (source fakeSource) Frames() <-chan source.Frame { return source.frames } func (source fakeSource) Statuses() <-chan source.Update { return source.statuses } type fakeRuntime struct { output []float32 err error closed bool } func (runtime *fakeRuntime) Run(input []float32) ([]float32, error) { if len(input) == 0 { return nil, errors.New("missing input") } return runtime.output, runtime.err } func (runtime *fakeRuntime) Close() { runtime.closed = true } type fakeProcessor struct { result fall.FrameResult calls int } func (processor *fakeProcessor) Process(frame fall.Frame) fall.FrameResult { processor.calls++ return processor.result }