fix: an app update is offered only when the app changed; casting no longer sticks on "loading" when the same queue plays again (#6455, #6456)
release / govulncheck (push) Successful in 16s
release / web (push) Successful in 1m30s
release / go (push) Successful in 1m46s
release / android (push) Successful in 5m2s
release / Build signed APK (releases and dev) (push) Successful in 5m13s
release / integration (push) Successful in 5m22s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 13s
release / Verify release artifacts (tag releases only) (push) Skipped
release / govulncheck (push) Successful in 16s
release / web (push) Successful in 1m30s
release / go (push) Successful in 1m46s
release / android (push) Successful in 5m2s
release / Build signed APK (releases and dev) (push) Successful in 5m13s
release / integration (push) Successful in 5m22s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 13s
release / Verify release artifacts (tag releases only) (push) Skipped
ci/version.sh --apk names the APK from android/ alone, and the APK lane calls it. The app offers an update when the server's APK name differs from its own, so a name derived from the whole repo offered every phone a rebuild of the app it already had on each server-only deploy. The image keeps the union set: it bundles the APK under client/. Casting (#6456): - setMediaItems with the tracks already queued (play the queued album from another track) cleared the cast and waited for a resync that the id-keyed collector never ran. It is now a move within the queue: Seek TRACK_NR on a Sonos, start that track on a one-track renderer; the play() goes through. - A queue replaced through setMediaItems could take the Sonos incremental diff, which cleared target without restoring active, so the app thought it was back on the phone while the speaker played on, and the chosen start track was ignored. The diff now runs only while the route is still active; a replaced queue reloads in full at its start index. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -455,7 +455,7 @@ func TestImageBuild_StampsADerivedVersionAndAChannel(t *testing.T) {
|
||||
t.Errorf("the main arm stamps the literal string \"main\" as a version; two images months apart would be indistinguishable\n%s", mainArm)
|
||||
}
|
||||
if !strings.Contains(yaml, "ci/version.sh HEAD | sed") {
|
||||
t.Error("the image job no longer derives its version from ci/version.sh — the version and the APK's version can now drift apart")
|
||||
t.Error("the image job no longer derives its version from ci/version.sh — the image's version has drifted out of test coverage")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -508,10 +508,11 @@ func commitFile(t *testing.T, dir, path, epoch string) {
|
||||
}
|
||||
|
||||
// versionIn runs ci/version.sh inside dir, reading real git rather than the
|
||||
// pinned-clock override, so the PATHSPEC is what is under test.
|
||||
func versionIn(t *testing.T, dir string) (string, error) {
|
||||
// pinned-clock override, so the PATHSPEC is what is under test. flags go
|
||||
// before the ref, as in `--apk`.
|
||||
func versionIn(t *testing.T, dir string, flags ...string) (string, error) {
|
||||
t.Helper()
|
||||
cmd := exec.Command(filepath.Join(repoRoot(t), "ci", "version.sh"), "HEAD")
|
||||
cmd := exec.Command(filepath.Join(repoRoot(t), "ci", "version.sh"), append(flags, "HEAD")...)
|
||||
cmd.Dir = dir
|
||||
cmd.Env = append(os.Environ(),
|
||||
"GIT_CONFIG_GLOBAL=/dev/null", "GIT_CONFIG_SYSTEM=/dev/null",
|
||||
@@ -558,11 +559,9 @@ func TestVersionName_IgnoresCommitsThatShipNothing(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// android/ is deliberately NOT excluded, and that is the subtle half of the
|
||||
// list. It ships in no server image — but it is the APK's entire source, and
|
||||
// ONE script derives the version for both artifacts. Excluding it would stop
|
||||
// an Android-only commit from moving the APK's own version, which is exactly
|
||||
// the silent downgrade the versioning rework exists to prevent.
|
||||
// android/ is deliberately NOT excluded from the image's name: the image
|
||||
// bundles the APK under client/, so an Android-only commit changes what the
|
||||
// image carries, and an image that changed must not keep its old name.
|
||||
func TestVersionName_AndroidSourcesCount(t *testing.T) {
|
||||
dir := gitRepo(t)
|
||||
commitFile(t, dir, "internal/server/thing.go", "1757443736")
|
||||
@@ -573,7 +572,71 @@ func TestVersionName_AndroidSourcesCount(t *testing.T) {
|
||||
t.Fatalf("version.sh failed: %v\n%s", err, out)
|
||||
}
|
||||
if !strings.Contains(out, "name=2026.09.10.0042") {
|
||||
t.Errorf("an Android commit did not move the version; the APK would ship new code under its old version name\n%s", out)
|
||||
t.Errorf("an Android commit did not move the image's version; it carries a new APK under its old name\n%s", out)
|
||||
}
|
||||
}
|
||||
|
||||
// The app offers an update whenever the server's APK has a different name
|
||||
// from its own, so the APK's name must move for Android commits and ONLY for
|
||||
// them. Named from the whole repo, every server-only deploy offered each
|
||||
// phone a rebuild of the app it already had.
|
||||
func TestApkName_MovesOnlyForAndroidCommits(t *testing.T) {
|
||||
const (
|
||||
app = "1757443736" // 2025.09.09.1848
|
||||
server = "1789000920" // 2026.09.10.0042, later
|
||||
again = "1789600000" // 2026.09.16.2306, later still
|
||||
)
|
||||
dir := gitRepo(t)
|
||||
commitFile(t, dir, "android/app/src/main/Thing.kt", app)
|
||||
commitFile(t, dir, "internal/server/thing.go", server)
|
||||
commitFile(t, dir, "web/src/routes/page.svelte", server)
|
||||
|
||||
out, err := versionIn(t, dir, "--apk")
|
||||
if err != nil {
|
||||
t.Fatalf("version.sh --apk failed: %v\n%s", err, out)
|
||||
}
|
||||
if !strings.Contains(out, "name=2025.09.09.1848") {
|
||||
t.Errorf("a server-only commit moved the APK's name; every phone would be offered the app it already has\n%s", out)
|
||||
}
|
||||
// The image's name still follows the server commit, so the test above
|
||||
// is not passing because the flag is ignored and everything is stale.
|
||||
out, err = versionIn(t, dir)
|
||||
if err != nil {
|
||||
t.Fatalf("version.sh failed: %v\n%s", err, out)
|
||||
}
|
||||
if !strings.Contains(out, "name=2026.09.10.0042") {
|
||||
t.Errorf("without --apk the name should follow the server commit\n%s", out)
|
||||
}
|
||||
|
||||
// A test-only Android commit ships nothing either.
|
||||
commitFile(t, dir, "android/app/src/test/java/A.kt", again)
|
||||
out, err = versionIn(t, dir, "--apk")
|
||||
if err != nil {
|
||||
t.Fatalf("version.sh --apk failed: %v\n%s", err, out)
|
||||
}
|
||||
if !strings.Contains(out, "name=2025.09.09.1848") {
|
||||
t.Errorf("an Android test-only commit moved the APK's name\n%s", out)
|
||||
}
|
||||
|
||||
// ...and a real Android commit must move it, or new app code would ship
|
||||
// under the name of the old and never be offered.
|
||||
commitFile(t, dir, "android/app/src/main/Thing.kt", again)
|
||||
out, err = versionIn(t, dir, "--apk")
|
||||
if err != nil {
|
||||
t.Fatalf("version.sh --apk failed: %v\n%s", err, out)
|
||||
}
|
||||
if !strings.Contains(out, "name=2026.09.16.2306") {
|
||||
t.Errorf("an Android commit did not move the APK's name; the new app would never be offered\n%s", out)
|
||||
}
|
||||
}
|
||||
|
||||
// The APK lane must ask for the APK's name. Without the flag it would still
|
||||
// build, sign and publish, and quietly go back to offering an update on every
|
||||
// server deploy.
|
||||
func TestReleaseWorkflow_NamesTheApkFromAndroidAlone(t *testing.T) {
|
||||
step := stepBody(t, releaseYAML(t), "Compute release version")
|
||||
if !strings.Contains(step, "ci/version.sh --apk HEAD") {
|
||||
t.Errorf("the APK lane does not derive its version with --apk\n%s", step)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user