package main import ( "database/sql" "os" "path/filepath" "testing" ) // A fleet sweep (`deploy --all --version`) must leave pinned instances alone — // the pin used to gate only the AUTO-updater, and a manual promote stomped a // pinned canary while its pin sat intact. sweepSkipsPinned is the decision. func TestSweepSkipsPinned(t *testing.T) { dir := t.TempDir() // A hibernating host reads prefs straight from its parked DB — build one // with a pin, exactly as the instance's own settings write would. prev := instanceDataDir instanceDataDir = dir defer func() { instanceDataDir = prev }() mkDB := func(host, pin string) { db, err := sql.Open("sqlite", filepath.Join(dir, host+".db")) if err != nil { t.Fatal(err) } defer db.Close() if _, err := db.Exec("CREATE TABLE config (key TEXT PRIMARY KEY, value TEXT)"); err != nil { t.Fatal(err) } if pin != "" { if _, err := db.Exec("INSERT INTO config VALUES('update_pin', ?)", pin); err != nil { t.Fatal(err) } } } // Hibernating + pinned to something else → skipped. mkDB("pinned.example", "aaaa111") rt := &Route{Host: "pinned.example", Hibernate: true} if skip, why := sweepSkipsPinned(rt, "bbbb222"); !skip || why == "" { t.Fatalf("pinned canary must be skipped by the sweep, got skip=%v why=%q", skip, why) } // Pinned to the sweep's own target → deploy (harmless repoint/no-op). if skip, _ := sweepSkipsPinned(rt, "aaaa111"); skip { t.Fatal("a host pinned to the sweep's target version should deploy") } // Hibernating, no pin → deploys. mkDB("plain.example", "") rt2 := &Route{Host: "plain.example", Hibernate: true} if skip, _ := sweepSkipsPinned(rt2, "bbbb222"); skip { t.Fatal("an unpinned host must not be skipped") } // Hibernating with an UNREADABLE parked DB fails closed (the updater's // rule): the pin may live only in the copy we can't read. rt3 := &Route{Host: "missing.example", Hibernate: true} if skip, why := sweepSkipsPinned(rt3, "bbbb222"); !skip || why == "" { t.Fatalf("unreadable hibernating prefs must fail closed, got skip=%v why=%q", skip, why) } // A RUNNING host whose socket is dead is probably crashed — the sweep is // its rescue, so it deploys anyway (with a note, not silence). rt4 := &Route{Host: "down.example", Socket: filepath.Join(dir, "no-such.sock")} if skip, why := sweepSkipsPinned(rt4, "bbbb222"); skip || why == "" { t.Fatalf("a dead-socket running host should deploy with a note, got skip=%v why=%q", skip, why) } _ = os.Remove(filepath.Join(dir, "pinned.example.db")) }