Files
groundstation/internal/osd/contract_test.go
T

132 lines
3.6 KiB
Go

package osd
import (
"testing"
)
func TestTableSelectionRequiresAnIdentifier(t *testing.T) {
if (Table{}).Selected() {
t.Fatal("zero generation and hash mean no table is selected yet")
}
if !(Table{Gen: 3}).Selected() || !(Table{Hash: 9}).Selected() {
t.Fatal("either identifier alone selects a table")
}
}
func TestCeilingFromChannelScalesTheKnobLinearly(t *testing.T) {
// The temporary operator maxPower ceiling maps the full CH6 channel
// range onto the 0..127 level domain. It is operator intent only.
cases := []struct {
channel int16
want uint8
}{
{-1024, 0},
{-1023, 0},
{0, 63},
{512, 95},
{1023, 126},
{1024, 127},
}
for _, c := range cases {
if got := CeilingFromChannel(c.channel); got != c.want {
t.Fatalf("CeilingFromChannel(%d)=%d want %d", c.channel, got, c.want)
}
}
if CeilingFromChannel(-2000) != 0 || CeilingFromChannel(2000) != 127 {
t.Fatal("out-of-range channels must clamp to the level domain")
}
}
func TestSnapshotCarriesFourDistinctContractValues(t *testing.T) {
// The four contract values must stay distinct: every field survives a
// roundtrip at its own offset, so no value can silently alias another
// (for example a mode/power collapse of CH6 ceiling and applied VTX
// state).
snapshot := Snapshot{
Sequence: 7,
TimestampMS: 99,
Armed: true,
Video: true,
OpConfig: OperatorConfig{
Table: Table{Gen: 3, Hash: 0xdeadbeefcafe},
MaxPower: 95,
},
Policy: GSPolicy{
Pending: false,
Level: 42,
Reason: [8]byte{'a', 'd', 'a', 'p', 't'},
TimestampMS: 1234,
},
Ack: VTXAck{
Pending: false,
Table: Table{Gen: 2, Hash: 0xfeed},
Level: 40,
Lifecycle: VTXLifecycleApplied,
},
Link: GSObservedLink{
Known: true,
AgeMS: 250,
},
PublishedMS: 555,
}
snapshot.Channels[5] = 100
got, err := Decode(Encode(snapshot))
if err != nil {
t.Fatal(err)
}
if got != snapshot {
t.Fatalf("roundtrip lost contract state: %+v != %+v", got, snapshot)
}
}
func TestSnapshotCarriesExplicitPendingStates(t *testing.T) {
// Until the policy, acknowledgment and link producers exist the
// snapshot must carry their explicit pending/unknown states, never
// fabricated levels or metrics.
snapshot := Snapshot{
Armed: true,
OpConfig: OperatorConfig{
MaxPower: 63,
},
Policy: GSPolicy{Pending: true},
Ack: VTXAck{Pending: true, Lifecycle: VTXLifecyclePending},
Link: GSObservedLink{Known: false},
}
got, err := Decode(Encode(snapshot))
if err != nil {
t.Fatal(err)
}
if !got.Policy.Pending || got.Policy.Level != 0 {
t.Fatalf("policy=%+v must stay explicitly pending", got.Policy)
}
if !got.Ack.Pending || got.Ack.Level != 0 || got.Ack.Lifecycle != VTXLifecyclePending {
t.Fatalf("ack=%+v must stay explicitly pending", got.Ack)
}
if got.Link.Known || got.Link.AgeMS != 0 {
t.Fatalf("link=%+v must stay explicitly unknown", got.Link)
}
if got.OpConfig != snapshot.OpConfig {
t.Fatalf("operator config=%+v must survive", got.OpConfig)
}
}
func TestDecodeRejectsV1Snapshots(t *testing.T) {
// A v1-sized record cannot carry the contract; a v2-sized record that
// claims v1 must be rejected by version, not parsed loosely.
v1Shaped := make([]byte, Size)
v1Shaped[0], v1Shaped[1], v1Shaped[2] = Magic0, Magic1, Version-1
if _, err := Decode(v1Shaped); err == nil {
t.Fatal("a v1-marked record must be rejected")
}
}
func TestVTXLifecycleStatesAreDistinct(t *testing.T) {
if VTXLifecyclePending == VTXLifecycleApplied ||
VTXLifecycleApplied == VTXLifecycleError ||
VTXLifecyclePending == VTXLifecycleError {
t.Fatal("pending, applied and error must be distinct lifecycle states")
}
}