mirror of
https://github.com/screentinker/screentinker.git
synced 2026-08-14 06:16:20 -06:00
Handing someone a test build was a trap. A prerelease sorts BELOW its own release — 1.9.25-fix234d is semver-older than 1.9.25 — so a sideloaded display asked "anything newer?", was correctly told yes, and updated itself straight back off the build we had asked someone to test. Same versionCode, so Android installed it without complaint. Silent, and within minutes. That is what happened on #234: the reporter installed the fix, tested for an evening, and reported nothing had changed. They were right. Their tablet was running the old code again by then, and I had told them it was fixed without ever checking what the device reported. Adds a per-display opt-in (devices.ota_beta, default 0, checkbox next to the OTA toggle). When set, the display keeps a prerelease of the CURRENT core instead of being pulled back to its release. Deliberately narrow in one direction and deliberately wide in the other: - Narrow: it only holds a prerelease of the core already installed. A plain release, a -patchN build, an upgrade to a newer core, and a display ahead of the server all behave exactly as before, and the flag defaults off so a fleet that never sets it is unaffected. - Wide: an opted-in display is exempted from the superseded-prerelease guard. That guard would otherwise pin a tester on an old test build permanently — an older-core prerelease is never offered anything, so they would have to notice and sideload their way out. Writing the test is what surfaced that; opting in must never mean never updating again. 9 tests covering both directions, including that shipping a newer release pulls a beta display back onto the release line. 845 server tests green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Uaeo9MvzKoyXuN6ZsbhtkL
80 lines
4.1 KiB
JavaScript
80 lines
4.1 KiB
JavaScript
'use strict';
|
|
|
|
// Handing someone a test build was a trap.
|
|
//
|
|
// A prerelease sorts BELOW its own release: 1.9.25-fix234d < 1.9.25. So a display sideloaded with a
|
|
// test build asked the server "anything newer?", was correctly told yes — the released 1.9.25 — and
|
|
// updated itself straight back off the build we had asked someone to test. Same versionCode, so
|
|
// Android installed it without complaint. Silent, within minutes.
|
|
//
|
|
// It happened on #234: the reporter installed the fix, tested for an evening, and reported that
|
|
// nothing had changed. They were right — their tablet was running the old code again by then.
|
|
//
|
|
// The opt-in is per display and deliberately narrow: it holds a prerelease of the CURRENT core
|
|
// only. An older-core prerelease is genuinely stale and must still be offered an update, and once a
|
|
// newer release ships the display must rejoin it — otherwise "beta" quietly becomes "abandoned on a
|
|
// branch nobody maintains".
|
|
|
|
const { test } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const breaker = require('../lib/ota-breaker');
|
|
|
|
// decide(client, latest, deviceId, now, betaChannel)
|
|
const ask = (client, latest, beta) => breaker.decide(client, latest, null, Date.now(), beta);
|
|
|
|
test('THE BUG: without the opt-in, a test build is offered its own release and reverts', () => {
|
|
const v = ask('1.9.25-fix234d', '1.9.25', false);
|
|
assert.equal(v.update_available, true, 'this is the revert that cost a reporter an evening');
|
|
assert.equal(v.reason, 'offer');
|
|
});
|
|
|
|
test('THE FIX: an opted-in display keeps a prerelease of the current release', () => {
|
|
const v = ask('1.9.25-fix234d', '1.9.25', true);
|
|
assert.equal(v.update_available, false);
|
|
assert.equal(v.reason, 'beta-channel');
|
|
});
|
|
|
|
test('opting in does NOT strand a display once a newer release ships', () => {
|
|
// The whole risk of a beta flag is that it becomes permanent. 1.9.26 is a real newer core, so an
|
|
// opted-in display on any 1.9.25 build must take it.
|
|
const v = ask('1.9.25-fix234d', '1.9.26', true);
|
|
assert.equal(v.update_available, true, 'a beta display must rejoin the next real release');
|
|
});
|
|
|
|
test('the superseded-prerelease guard is untouched for displays that did NOT opt in', () => {
|
|
// #144's phantom protection: a device reporting an ancient beta is not chased with offers.
|
|
const v = ask('1.9.1-beta4', '1.9.25', false);
|
|
assert.equal(v.update_available, false);
|
|
assert.equal(v.reason, 'superseded-prerelease');
|
|
});
|
|
|
|
test('but an opted-in display on an old prerelease IS offered the current release', () => {
|
|
// This is the escape hatch, and it is the difference between "beta" and "abandoned". Without
|
|
// it the superseded guard pins a tester on an old test build permanently — they would have to
|
|
// notice and sideload their way out, which is exactly the trap the opt-in exists to remove.
|
|
const v = ask('1.9.1-beta4', '1.9.25', true);
|
|
assert.equal(v.update_available, true, 'opting in must never mean never updating again');
|
|
});
|
|
|
|
test('the opt-in changes nothing for a display on a plain release', () => {
|
|
assert.equal(ask('1.9.25', '1.9.25', true).reason, 'up-to-date');
|
|
assert.equal(ask('1.9.24', '1.9.25', true).update_available, true, 'a real upgrade is unaffected');
|
|
assert.equal(ask('1.9.24', '1.9.25', false).update_available, true);
|
|
});
|
|
|
|
test('a display ahead of the server is never downgraded, opted in or not', () => {
|
|
assert.equal(ask('1.9.26', '1.9.25', true).reason, 'client-newer');
|
|
assert.equal(ask('1.9.26', '1.9.25', false).reason, 'client-newer');
|
|
});
|
|
|
|
test('a -patchN build is a release, not a prerelease, so beta does not pin it', () => {
|
|
// isReleased() treats patchN as released; it must keep being offered real updates.
|
|
const v = ask('1.9.2-patch3', '1.9.25', true);
|
|
assert.equal(v.update_available, true, 'a patch release must not be mistaken for a beta build');
|
|
});
|
|
|
|
test('the flag defaults to off, so nothing changes for a fleet that never sets it', () => {
|
|
const withDefault = breaker.decide('1.9.25-fix234d', '1.9.25', null, Date.now());
|
|
assert.equal(withDefault.update_available, true, 'default must match pre-existing behaviour');
|
|
});
|