package display import ( "testing" "git.piksar.eu/piotr/groundstation/internal/osd" ) // stubSource serves one fixed snapshot, or none. type stubSource struct { snapshot osd.Snapshot ok bool } func (s stubSource) Latest() (osd.Snapshot, bool) { return s.snapshot, s.ok } // armedPendingSource is the contract state until the adaptive-link // producers exist: armed with a CH6 ceiling, everything else pending. func armedPendingSource(ceiling uint8) stubSource { return stubSource{snapshot: osd.Snapshot{ Armed: true, OpConfig: osd.OperatorConfig{MaxPower: ceiling}, Policy: osd.GSPolicy{Pending: true}, Ack: osd.VTXAck{Pending: true, Lifecycle: osd.VTXLifecyclePending}, Link: osd.GSObservedLink{Known: false}, }, ok: true} } func TestOSDTemplateStaysTransparentWhileVideoAbsent(t *testing.T) { canvas := &fakeCanvas{width: 800, height: 600} NewOSDTemplate(NewVideoPresence(), armedPendingSource(63)).Draw(canvas) clears, flushes := canvas.drawCounts() if clears != 1 || flushes != 1 { t.Fatalf("clears=%d flushes=%d want 1 and 1", clears, flushes) } if lines := canvas.lines(); len(lines) != 0 { t.Fatalf("OSD lines=%q want none: without video the fallback canvas owns the screen", lines) } } func TestOSDTemplateDrawsArmStateAndPendingContractWhileVideoPresent(t *testing.T) { canvas := &fakeCanvas{width: 800, height: 600} presence := NewVideoPresence() presence.SetActive(true) NewOSDTemplate(presence, armedPendingSource(63)).Draw(canvas) clears, flushes := canvas.drawCounts() if clears != 1 || flushes != 1 { t.Fatalf("clears=%d flushes=%d want 1 and 1", clears, flushes) } lines := canvas.lines() want := []string{"ARM ARMED", "VTX tbl:-- ceil:63 req:-- ack:--"} if len(lines) != len(want) { t.Fatalf("lines=%q want %q", lines, want) } for i := range want { if lines[i] != want[i] { t.Fatalf("line %d=%q want %q", i, lines[i], want[i]) } } } func TestOSDTemplateDrawsDisarmedState(t *testing.T) { canvas := &fakeCanvas{width: 800, height: 600} presence := NewVideoPresence() presence.SetActive(true) NewOSDTemplate(presence, stubSource{ok: true}).Draw(canvas) lines := canvas.lines() if len(lines) != 2 || lines[0] != "ARM OFF" { t.Fatalf("lines=%q want the disarmed arm state and the pending contract line", lines) } } func TestOSDTemplateRendersSelectedTableRequestedAndAppliedStates(t *testing.T) { canvas := &fakeCanvas{width: 800, height: 600} presence := NewVideoPresence() presence.SetActive(true) NewOSDTemplate(presence, stubSource{snapshot: osd.Snapshot{ Armed: true, OpConfig: osd.OperatorConfig{ Table: osd.Table{Gen: 3, Hash: 0xcafe}, MaxPower: 127, }, Policy: osd.GSPolicy{Level: 42}, Ack: osd.VTXAck{ Table: osd.Table{Gen: 3}, Level: 40, Lifecycle: osd.VTXLifecycleApplied, }, }, ok: true}).Draw(canvas) lines := canvas.lines() want := []string{"ARM ARMED", "VTX tbl:3 ceil:127 req:42 ack:40"} if len(lines) != len(want) { t.Fatalf("lines=%q want %q", lines, want) } for i := range want { if lines[i] != want[i] { t.Fatalf("line %d=%q want %q", i, lines[i], want[i]) } } } func TestOSDTemplateRendersAckErrorState(t *testing.T) { canvas := &fakeCanvas{width: 800, height: 600} presence := NewVideoPresence() presence.SetActive(true) NewOSDTemplate(presence, stubSource{snapshot: osd.Snapshot{ OpConfig: osd.OperatorConfig{MaxPower: 10}, Policy: osd.GSPolicy{Pending: true}, Ack: osd.VTXAck{Lifecycle: osd.VTXLifecycleError}, }, ok: true}).Draw(canvas) lines := canvas.lines() if len(lines) != 2 || lines[1] != "VTX tbl:-- ceil:10 req:-- ack:ERR" { t.Fatalf("lines=%q want the acknowledged error state", lines) } } func TestOSDTemplateRendersLevelZeroAsExplicitOff(t *testing.T) { // Level 0 in an available policy or acknowledgment is an explicit // off / no-runtime-command state and must render as 0; only a pending // value — no producer yet — renders as the pending mark. canvas := &fakeCanvas{width: 800, height: 600} presence := NewVideoPresence() presence.SetActive(true) NewOSDTemplate(presence, stubSource{snapshot: osd.Snapshot{ OpConfig: osd.OperatorConfig{MaxPower: 0}, Policy: osd.GSPolicy{Level: 0}, Ack: osd.VTXAck{Level: 0, Lifecycle: osd.VTXLifecycleApplied}, }, ok: true}).Draw(canvas) lines := canvas.lines() want := "VTX tbl:-- ceil:0 req:0 ack:0" if len(lines) != 2 || lines[1] != want { t.Fatalf("lines=%q want %q", lines, want) } } func TestOSDTemplateStaysTransparentWithoutASnapshotWhileVideoPresent(t *testing.T) { canvas := &fakeCanvas{width: 800, height: 600} presence := NewVideoPresence() presence.SetActive(true) NewOSDTemplate(presence, stubSource{}).Draw(canvas) if lines := canvas.lines(); len(lines) != 0 { t.Fatalf("OSD lines=%q want none without a published snapshot", lines) } } func TestOSDTemplateGatesOnObservedVideoPresenceNotSnapshotVideo(t *testing.T) { // The rendering gate is the video lifecycle the display observes on the // display server, never the snapshot's Video field: the supervisor's // field is its launcher knowledge, while the display trusts only the // presented video output itself. canvas := &fakeCanvas{width: 800, height: 600} presence := NewVideoPresence() claimingVideo := stubSource{snapshot: osd.Snapshot{Armed: true, Video: true}, ok: true} NewOSDTemplate(presence, claimingVideo).Draw(canvas) if lines := canvas.lines(); len(lines) != 0 { t.Fatalf("lines=%q want none: snapshot presence must never replace the observed lifecycle", lines) } denyingVideo := stubSource{snapshot: osd.Snapshot{Armed: true}, ok: true} presence.SetActive(true) NewOSDTemplate(presence, denyingVideo).Draw(canvas) if lines := canvas.lines(); len(lines) == 0 { t.Fatal("the observed video lifecycle must gate rendering even when the snapshot denies video") } } func TestOSDTemplateNeverDrawsTheFallbackText(t *testing.T) { canvas := &fakeCanvas{width: 800, height: 600} presence := NewVideoPresence() template := NewOSDTemplate(presence, armedPendingSource(63)) for _, active := range []bool{false, true, false} { presence.SetActive(active) template.Draw(canvas) for _, line := range canvas.lines() { if line == wantFallbackText { t.Fatalf("the OSD is a telemetry surface, not a fallback: drew %q", line) } } } }