package server import ( "os" "os/exec" "path/filepath" "strconv" "strings" "testing" ) // Guards the version derivation that stamps every build. // // These assertions used to be impossible to run. The derivation lived inline // in release.yml, which triggers only on `main` and on tags — so a mistake in // it could not surface until a release was already under way, and its failure // mode is silence: a version nobody can compare looks exactly like being up to // date, and nobody reports an update they were never offered. // // Moving it to ci/version.sh made it executable, so this runs on every push // that touches the release machinery. That is the whole point of the file; the // specific assertions below matter less than the fact that they run at all. // // The tests EXECUTE the script rather than asserting on its text, so they // break when the behaviour changes rather than when the wording does. // highestOldSchemeCode is the largest versionCode ever shipped under the // retired commit-count scheme (v2026.09.09 shipped 1895). Every code the new // scheme emits must clear it, or Android would refuse the upgrade as a // downgrade and the update channel would be a one-way door. const highestOldSchemeCode = 1895 func repoRoot(t *testing.T) string { t.Helper() dir, err := os.Getwd() if err != nil { t.Fatal(err) } for { if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil { return dir } parent := filepath.Dir(dir) if parent == dir { t.Fatalf("no go.mod above %s", dir) } dir = parent } } // runVersion executes ci/version.sh with both clocks pinned, so the result is // deterministic. Returns the parsed KEY=VALUE output. func runVersion(t *testing.T, commitEpoch, nowEpoch string) map[string]string { t.Helper() out, err := versionScript(t, commitEpoch, nowEpoch) if err != nil { t.Fatalf("ci/version.sh failed: %v\n%s", err, out) } parsed := map[string]string{} for _, line := range strings.Split(strings.TrimSpace(out), "\n") { if k, v, ok := strings.Cut(line, "="); ok { parsed[k] = v } } return parsed } func versionScript(t *testing.T, commitEpoch, nowEpoch string) (string, error) { t.Helper() root := repoRoot(t) cmd := exec.Command(filepath.Join(root, "ci", "version.sh")) cmd.Dir = root cmd.Env = append(os.Environ(), "MINSTREL_COMMIT_EPOCH="+commitEpoch, "MINSTREL_NOW_EPOCH="+nowEpoch, ) out, err := cmd.CombinedOutput() return string(out), err } func TestVersionName_IsCommitTimeToTheMinute(t *testing.T) { // 2025-09-09T18:48:56Z got := runVersion(t, "1757443736", "1789000920") if want := "2025.09.09.1848"; got["name"] != want { t.Errorf("name = %q, want %q", got["name"], want) } } // HHMM is the segment most likely to be silently mangled, and it only bites // for about a tenth of the day — a build just after midnight must emit "0042", // never "42". A stripped leading zero shifts the segment by two orders of // magnitude and reverses comparisons against every other build that day. func TestVersionName_PadsTheMinuteSegment(t *testing.T) { // 2026-09-10T00:42:00Z got := runVersion(t, "1789000920", "1789000920") if want := "2026.09.10.0042"; got["name"] != want { t.Errorf("name = %q, want %q — leading zero lost?", got["name"], want) } } func TestVersionName_DerivesFromCommitNotBuildClock(t *testing.T) { // Same commit, two different build clocks: the NAME must not move, or a // dev build and a main build of one commit would report different strings // and the channel field would stop being the only thing separating them. a := runVersion(t, "1757443736", "1789000920") b := runVersion(t, "1757443736", "1789500000") if a["name"] != b["name"] { t.Errorf("name moved with the build clock: %q vs %q", a["name"], b["name"]) } if a["code"] == b["code"] { t.Errorf("code did NOT move with the build clock (%q) — it is not build-derived", a["code"]) } } func TestVersionCode_IsMinutesSince2020AndClearsTheOldScheme(t *testing.T) { got := runVersion(t, "1789000920", "1789000920") code, err := strconv.Atoi(got["code"]) if err != nil { t.Fatalf("code %q is not an integer: %v", got["code"], err) } if want := (1789000920 - 1577836800) / 60; code != want { t.Errorf("code = %d, want %d", code, want) } if code <= highestOldSchemeCode { t.Errorf("code %d does not clear the retired commit-count scheme (%d) — "+ "Android would refuse the upgrade as a downgrade", code, highestOldSchemeCode) } if int64(code) > 2147483647 { t.Errorf("code %d overflows versionCode's int32 ceiling", code) } } // The tag is not chosen, it is the name with a `v`. Anything else reintroduces // the mismatch between what a tag claims and what the artifact reports. func TestTag_IsTheNameWithAPrefix(t *testing.T) { got := runVersion(t, "1757443736", "1789000920") if want := "v" + got["name"]; got["tag"] != want { t.Errorf("tag = %q, want %q", got["tag"], want) } } // A guard that cannot fail is worse than no guard. These prove the script // rejects the shapes it claims to reject, rather than emitting something // plausible and letting it ship. func TestVersionScript_RejectsUnusableClocks(t *testing.T) { for _, tc := range []struct{ name, commit, now string }{ {"unreadable commit timestamp", "notanumber", "1789000920"}, {"non-numeric build clock", "1789000920", "abc"}, {"build clock before 2020", "1789000920", "1000000000"}, } { t.Run(tc.name, func(t *testing.T) { out, err := versionScript(t, tc.commit, tc.now) if err == nil { t.Errorf("script succeeded on %s, output: %s", tc.name, out) } }) } } // Pins the wiring, not the formula: if release.yml stops calling the script, // every assertion above keeps passing while the thing that actually ships goes // unguarded again. That silent decoupling is the specific regression here. func TestReleaseWorkflow_UsesTheSharedDerivation(t *testing.T) { body, err := os.ReadFile(filepath.Join(repoRoot(t), ".gitea", "workflows", "release.yml")) if err != nil { t.Fatal(err) } yaml := string(body) if !strings.Contains(yaml, "ci/version.sh") { t.Error("release.yml no longer calls ci/version.sh — the derivation has drifted out of test coverage") } // The retired scheme, which must not come back. A presence check is safe // against prose; the historical note in that file names the old formula // only in comments, so match the executable form. if strings.Contains(yaml, "$(git rev-list --count") { t.Error("release.yml derives a commit count again — that is not monotonic across branches") } } // devArm returns the branch of "Compute image tags" that handles refs/heads/dev. func devArm(t *testing.T, yaml string) string { t.Helper() const marker = `elif [[ "${GITHUB_REF}" == "refs/heads/dev" ]]; then` i := strings.Index(yaml, marker) if i < 0 { t.Fatal("no refs/heads/dev arm in Compute image tags — the dev channel is not wired") } rest := yaml[i+len(marker):] if j := strings.Index(rest, "\n else"); j >= 0 { return rest[:j] } return rest } // The worst regression this wiring can produce: a dev push that also moves // :latest would ship untested code to every stable operator, silently, on the // next pull. Nothing else in the suite would notice — the build stays green // and the image is valid, it is simply the wrong audience. func TestDevChannel_PublishesDevAloneAndNeverLatest(t *testing.T) { body, err := os.ReadFile(filepath.Join(repoRoot(t), ".gitea", "workflows", "release.yml")) if err != nil { t.Fatal(err) } arm := devArm(t, string(body)) if !strings.Contains(arm, "${IMAGE}:dev") { t.Errorf("dev arm does not publish :dev\n%s", arm) } if strings.Contains(arm, ":latest") { t.Errorf("dev arm moves :latest — that ships dev code to every stable operator\n%s", arm) } // Rule 145: a rolling channel gets no commit-addressable tag. if strings.Contains(arm, "GITHUB_SHA") { t.Errorf("dev arm publishes a per-commit tag; a rolling channel should not\n%s", arm) } } // The two bundling paths must stay mutually exclusive. If the rebundle step's // condition were relaxed back to "not a tag", a dev push would run BOTH: stage // its freshly-built APK, then overwrite it with the previous release's. The // image would still build and the sidecar would still parse — it would just // quietly serve stale art to the channel whose whole job is being current. func TestDevChannel_RebundlePathIsMainOnly(t *testing.T) { body, err := os.ReadFile(filepath.Join(repoRoot(t), ".gitea", "workflows", "release.yml")) if err != nil { t.Fatal(err) } yaml := string(body) i := strings.Index(yaml, "- name: Bundle latest release APK") if i < 0 { t.Fatal("no 'Bundle latest release APK' step") } step := yaml[i:] if j := strings.Index(step, "\n - name:"); j >= 0 { step = step[:j] } if !strings.Contains(step, "github.ref == 'refs/heads/main'") { t.Errorf("the rebundle step is not gated to main; a dev push would overwrite its own APK\n%s", step) } }