fix(sat): a frequency we shipped wrong can now be mended
"la lilacsat je le vois en DATA ???!" — and the LO-90 fix could not have reached
him. The satellite file is copied out on the first run and was the operator's
from then on, so the merge could only ADD birds, never repair one. LilacSat-2
went out with an APRS digipeater and no FM transponder, and that mistake had
become his data, permanently.
The merge now distinguishes three cases, and the middle one is the whole point:
- a satellite he does not have is added;
- one he has, UNCHANGED from the plan he was given, is replaced — he never
edited it, so it is not his to keep: it is our data and ours was wrong;
- one he EDITED is left exactly alone, and named in the log. A frequency
somebody corrected by hand outranks anything shipped; they were on the air
and we were not.
"Unchanged" is decided against a baseline — satellites.shipped.json, the plan
this station was last handed — so the comparison is with what THEY were given
rather than with whatever ships today. Their edits survive every future release,
not just the next one.
The first run after this has no baseline, and there an edit of theirs and a
mistake of ours are indistinguishable. The shipped plan wins, once, with the
whole file copied to satellites.json.bak first and every replacement named. The
safe-looking alternative was the wrong one: standing down would have written a
baseline recording their entry as "edited" and frozen a known-wrong frequency
for the life of the install.
Verified against his own file: LILACSAT-2 becomes LO-90 with the FM transponder
first, and the twelve curated entries that were missing their catalog numbers
get them — which also closes the NORAD gap left open when the exact join went in.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
@@ -268,3 +268,146 @@ func TestGeneratedBirdsAreSane(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The three cases the merge has to tell apart. Getting the middle one wrong is
|
||||
// how LilacSat-2 shipped with no FM transponder and could never be mended.
|
||||
func TestMergeShippedRespectsEditsAndFixesOurs(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
// First run: the shipped plan and its baseline are written.
|
||||
if _, err := LoadBirds(dir); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(dir, BaselineName)); err != nil {
|
||||
t.Fatalf("no baseline was recorded: %v", err)
|
||||
}
|
||||
path := filepath.Join(dir, BirdsName)
|
||||
|
||||
// The operator corrects SO-50's tone and adds a satellite of their own.
|
||||
var list []Bird
|
||||
raw, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := json.Unmarshal(raw, &list); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var untouched Bird
|
||||
for i := range list {
|
||||
if list[i].Name == "SO-50" {
|
||||
list[i].Transponders[0].CTCSS = 74.4
|
||||
}
|
||||
if list[i].Name == "AO-7" {
|
||||
untouched = list[i] // left exactly as shipped
|
||||
}
|
||||
}
|
||||
list = append(list, Bird{Name: "MY-SAT", Transponders: []Transponder{{Label: "mine", Mode: "FM", DownLo: 145000000, UpLo: 435000000}}})
|
||||
out, _ := json.MarshalIndent(list, "", " ")
|
||||
if err := os.WriteFile(path, out, 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Load again. Nothing shipped has changed, so nothing should move.
|
||||
b, err := LoadBirds(dir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, ok := b.Find("SO-50")
|
||||
if !ok || got.Transponders[0].CTCSS != 74.4 {
|
||||
t.Errorf("the operator's tone was lost: %+v", got.Transponders)
|
||||
}
|
||||
if _, ok := b.Find("MY-SAT"); !ok {
|
||||
t.Error("the operator's own satellite was dropped")
|
||||
}
|
||||
if a, ok := b.Find("AO-7"); !ok || !sameBird(a, untouched) {
|
||||
t.Error("an untouched satellite was altered for no reason")
|
||||
}
|
||||
}
|
||||
|
||||
// An entry the operator never touched is REPLACED when the shipped plan
|
||||
// changes. That is the whole point: our data, and ours was wrong.
|
||||
func TestMergeShippedUpdatesWhatWasNeverEdited(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
if _, err := LoadBirds(dir); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
path := filepath.Join(dir, BirdsName)
|
||||
|
||||
// Pretend an older release shipped SO-50 with a wrong downlink, and that the
|
||||
// operator simply took it: their file AND the baseline both hold the wrong
|
||||
// value, which is exactly what "never edited" looks like.
|
||||
rewrite := func(p string, mutate func(*Bird)) {
|
||||
raw, err := os.ReadFile(p)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var l []Bird
|
||||
if err := json.Unmarshal(raw, &l); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for i := range l {
|
||||
if l[i].Name == "SO-50" {
|
||||
mutate(&l[i])
|
||||
}
|
||||
}
|
||||
out, _ := json.MarshalIndent(l, "", " ")
|
||||
if err := os.WriteFile(p, out, 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
wrong := func(x *Bird) { x.Transponders[0].DownLo = 1 }
|
||||
rewrite(path, wrong)
|
||||
rewrite(filepath.Join(dir, BaselineName), wrong)
|
||||
|
||||
b, err := LoadBirds(dir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, ok := b.Find("SO-50")
|
||||
if !ok {
|
||||
t.Fatal("SO-50 vanished")
|
||||
}
|
||||
if got.Transponders[0].DownLo == 1 {
|
||||
t.Error("a value the operator never edited was not brought up to date — a shipped mistake is unfixable")
|
||||
}
|
||||
}
|
||||
|
||||
// With NO baseline — the first run after baselines existed — the shipped plan
|
||||
// wins and the file is backed up. Standing down would freeze a known-wrong
|
||||
// frequency for the life of the install.
|
||||
func TestMergeShippedWithNoBaselineTakesShippedAndBacksUp(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
if _, err := LoadBirds(dir); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
path := filepath.Join(dir, BirdsName)
|
||||
if err := os.Remove(filepath.Join(dir, BaselineName)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// A wrong value, with nothing to say whether it is ours or theirs.
|
||||
raw, _ := os.ReadFile(path)
|
||||
var l []Bird
|
||||
if err := json.Unmarshal(raw, &l); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for i := range l {
|
||||
if l[i].Name == "SO-50" {
|
||||
l[i].Transponders[0].DownLo = 1
|
||||
}
|
||||
}
|
||||
out, _ := json.MarshalIndent(l, "", " ")
|
||||
if err := os.WriteFile(path, out, 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
b, err := LoadBirds(dir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, _ := b.Find("SO-50")
|
||||
if got.Transponders[0].DownLo == 1 {
|
||||
t.Error("the shipped plan did not take over, so the wrong value is now frozen for ever")
|
||||
}
|
||||
if _, err := os.Stat(path + ".bak"); err != nil {
|
||||
t.Errorf("no backup was written before overwriting: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user