bastion 0.53.2
ci / personal-data (push) Successful in 5s
ci / test (push) Successful in 3m4s
ci / fuzz (push) Successful in 3m45s

This commit is contained in:
Privatae
2026-10-20 04:42:14 +00:00
commit e4c9b69b46
471 changed files with 159794 additions and 0 deletions
+203
View File
@@ -0,0 +1,203 @@
package main
// "Is there a newer version?" — the question the box never asked.
// fetchManifest had exactly one caller, maybeSelfUpdate, which runs only when
// the HOST directs an update (update.go), so a machine could sit months behind
// and say nothing about it to the person sitting at it.
import (
"encoding/json"
"net/http"
"net/http/httptest"
"sync/atomic"
"testing"
"time"
)
// freshVersionState isolates the package-level answer and the build stamp.
func freshVersionState(t *testing.T) {
t.Helper()
latestMu.Lock()
old := latestPublished
latestPublished = ""
latestMu.Unlock()
oldClient, oldURL := clientVersion, manifestURL
t.Cleanup(func() {
latestMu.Lock()
latestPublished = old
latestMu.Unlock()
clientVersion, manifestURL = oldClient, oldURL
})
}
// The trap this function exists to avoid is a string compare. "0.9.0" sorts
// ABOVE "0.24.3" lexically, so a plain > would have told every 0.24 machine to
// go back to a release from months earlier — and every 0.9 machine that it was
// ahead of the world.
func TestVersionNewerIsNotAStringCompare(t *testing.T) {
cases := []struct {
a, b string
want bool
}{
{"0.24.4", "0.24.3", true},
{"0.24.3", "0.24.4", false},
{"0.24.3", "0.24.3", false},
{"0.9.0", "0.24.3", false}, // the lexical trap, in the direction that matters
{"0.24.3", "0.9.0", true}, // ...and in the other
{"1.0.0", "0.99.99", true},
{"0.24", "0.24.0", false}, // a missing component is zero, not "older"
{"0.24.0", "0.24", false},
{"0.24.1", "0.24", true},
{"v0.25.0", "0.24.3", true}, // a stray v is not a different release
// Unanswerable, in both directions: a `dev` build (run.go:32) and a
// manifest this code cannot parse must produce NO claim rather than a
// guessed ordering.
{"0.24.4", "dev", false},
{"dev", "0.24.3", false},
{"0.24.4", "", false},
{"", "0.24.3", false},
{"0.24.4", "0.24.3-rc1", false},
{"0.24.-1", "0.24.3", false},
{"0.24.+4", "0.24.3", false},
}
for _, c := range cases {
if got := versionNewer(c.a, c.b); got != c.want {
t.Errorf("versionNewer(%q, %q) = %v, want %v", c.a, c.b, got, c.want)
}
}
}
func TestUpdateAvailableOnlyClaimsWhatItCanStandBehind(t *testing.T) {
freshVersionState(t)
clientVersion = "0.24.3"
if got := updateAvailable(); got != "" {
t.Errorf("nothing has been checked yet, so there is nothing to say: %q", got)
}
recordLatestPublished("0.24.4")
if got := updateAvailable(); got != "0.24.4" {
t.Errorf("a newer published version must reach the page: %q", got)
}
recordLatestPublished("0.24.3")
if got := updateAvailable(); got != "" {
t.Errorf("the version we are already on is not an update: %q", got)
}
recordLatestPublished("0.9.9")
if got := updateAvailable(); got != "" {
t.Errorf("an OLDER published version must never read as available: %q", got)
}
// A local build cannot be compared, so it claims nothing — rather than
// telling every developer their machine is out of date.
recordLatestPublished("0.24.4")
clientVersion = "dev"
if got := updateAvailable(); got != "" {
t.Errorf("an unparseable running version must produce no claim: %q", got)
}
// An empty or blank manifest version never overwrites a real answer.
clientVersion = "0.24.3"
recordLatestPublished(" ")
if got := updateAvailable(); got != "0.24.4" {
t.Errorf("a blank manifest version must not erase what is known: %q", got)
}
}
// manifestServer serves a version.json and counts every hit, including the hit
// on the BINARY the manifest points at — which the availability check must
// never make.
func manifestServer(t *testing.T, version string) (url string, manifestHits, binaryHits *atomic.Int64) {
t.Helper()
manifestHits, binaryHits = &atomic.Int64{}, &atomic.Int64{}
mux := http.NewServeMux()
var srv *httptest.Server
mux.HandleFunc("/version.json", func(w http.ResponseWriter, r *http.Request) {
manifestHits.Add(1)
bin := map[string]any{"url": srv.URL + "/dl", "sha256": "00"}
_ = json.NewEncoder(w).Encode(map[string]any{
"version": version,
"binaries": map[string]any{
"linux-amd64": bin, "linux-arm64": bin,
"darwin-amd64": bin, "darwin-arm64": bin,
"windows-amd64.exe": bin, "windows-arm64.exe": bin,
},
})
})
mux.HandleFunc("/dl", func(w http.ResponseWriter, r *http.Request) {
binaryHits.Add(1)
_, _ = w.Write([]byte("this must never be fetched by a status page"))
})
srv = httptest.NewServer(signedStub(releaseKindBastion, mux))
t.Cleanup(srv.Close)
return srv.URL + "/version.json", manifestHits, binaryHits
}
// The check READS. It never downloads, never verifies, never swaps — the
// directed path is the only thing allowed to touch the binary, and a local UI
// that could start one would be across the line in ui.go's header.
func TestTheAvailabilityCheckOnlyReadsTheManifest(t *testing.T) {
freshVersionState(t)
clientVersion = "0.24.3"
url, manifestHits, binaryHits := manifestServer(t, "0.24.4")
manifestURL = url
checkLatestPublished()
if got := updateAvailable(); got != "0.24.4" {
t.Fatalf("the check did not record what was published: %q", got)
}
if n := manifestHits.Load(); n != 1 {
t.Errorf("one check is one manifest fetch, got %d", n)
}
if n := binaryHits.Load(); n != 0 {
t.Fatalf("the availability check downloaded the binary %d time(s) — it is a "+
"status page, not an updater", n)
}
// An unreachable manifest leaves the last known answer alone and says
// nothing: running with no route to privatae.ai is a supported way to use
// this, and it must not be reported as a fault.
manifestURL = "http://127.0.0.1:1/version.json"
checkLatestPublished()
if got := updateAvailable(); got != "0.24.4" {
t.Errorf("a failed check must not erase the answer: %q", got)
}
}
// A host-directed update reads the same manifest, and on the mismatch path it
// does NOT exit — so what it learned is the freshest thing the page can have.
func TestADirectedUpdateAlsoRecordsWhatIsPublished(t *testing.T) {
uiTestConfigDir(t)
freshVersionState(t)
t.Cleanup(func() { clearDirectiveRefusal("self-update") })
clientVersion = "0.24.3"
url, _, binaryHits := manifestServer(t, "0.24.4")
manifestURL = url
// The host asks for a version the manifest does not carry: maybeSelfUpdate
// fetches, finds the mismatch, and returns without swapping anything.
maybeSelfUpdate("member.host.privatae.ai", "", []byte(`{"update":{"version":"9.9.9"}}`))
if n := binaryHits.Load(); n != 0 {
t.Fatalf("a target the manifest does not carry must download nothing (%d hits)", n)
}
if got := updateAvailable(); got != "0.24.4" {
t.Fatalf("the directed path must record what the manifest actually said: %q", got)
}
}
// The cadence, as a constraint rather than a restatement of the number: this is
// the only repeating request in the binary that leaves for privatae.ai, so it
// must stay far slower than everything that talks to the user's own host or
// reads local disk. The slowest of those is faReconcileEvery (10 min,
// freeagent_managed.go:114). Breaking this is what "the banner feels slow"
// turns into if nobody writes down why it is slow on purpose.
func TestTheAvailabilityCheckStaysSlow(t *testing.T) {
if updateCheckEvery < 20*faReconcileEvery {
t.Errorf("updateCheckEvery = %v — a public poll must stay an order of magnitude "+
"slower than the slowest local refresh (%v); update.go carries the measurement",
updateCheckEvery, faReconcileEvery)
}
if updateCheckFirst < 10*time.Second || updateCheckFirst > updateCheckEvery {
t.Errorf("updateCheckFirst = %v — it must clear daemon startup and still be the "+
"first of the series", updateCheckFirst)
}
}