From 236695a547ffd7dcccc5a33e43fd1be5be660fda Mon Sep 17 00:00:00 2001 From: Sivert Date: Wed, 9 Sep 2026 16:49:55 +0200 Subject: [PATCH] An install that never recorded a version is not a fresh one (GRYT-1102) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GRYT-1101 stopped the dialog reading the per-user store before it loaded. One consequence was left: nothing wrote whatsNewSeenVersion while that bug was live, so every install that ran 1.11.0 or 1.11.1 has no value stored. On the first fixed build the dialog would read null, call it a fresh install, record the version and say nothing — so it would only appear on the release after that one. The two cases are separable. A fresh install has joined no servers. One that has been in use has an entry under `servers`, so `seen === null` with servers present is an install that predates the recording rather than a new one. A true fresh install stays quiet, which is the case that rule was written for: announcing the version somebody has just chosen to download reads as a bug. The check runs the branch both ways and executes hasJoinedAnything itself against no entry, an empty object and a joined server. Four mutations, four caught. Co-Authored-By: Claude Opus 5 --- scripts/check-whats-new.mjs | 32 +++++++++++++++++++++++++++++--- src/components/whatsNew.tsx | 12 +++++++++--- 2 files changed, 38 insertions(+), 6 deletions(-) diff --git a/scripts/check-whats-new.mjs b/scripts/check-whats-new.mjs index cabb60cd1..5b74c9688 100644 --- a/scripts/check-whats-new.mjs +++ b/scripts/check-whats-new.mjs @@ -44,13 +44,13 @@ assert.match( ); /** One run of the effect, with the store and the network faked. */ -async function run({ seen, version, app, offline, storeUser }) { +async function run({ seen, version, app, offline, storeUser, joined }) { const store = { value: seen }; const shown = []; const fetched = []; const fn = new Function( - "getUserValue", "setUserValue", "fetch", "setEntry", "version", "AbortController", "SEEN_KEY", "CHANGELOG_URL", "storeUser", + "getUserValue", "setUserValue", "fetch", "setEntry", "version", "AbortController", "SEEN_KEY", "CHANGELOG_URL", "storeUser", "hasJoinedAnything", `return (async () => { const cleanup = (() => ${body})(); await new Promise(r => setTimeout(r, 0)); return cleanup; })();`, ); @@ -72,6 +72,7 @@ async function run({ seen, version, app, offline, storeUser }) { SEEN_KEY, CHANGELOG_URL, storeUser === undefined ? "user_1" : storeUser, + () => joined ?? false, ); return { seen: store.value, shown, fetched }; @@ -95,7 +96,7 @@ const LINE = { version: "1.10.3", date: "2026-09-08", line: "Joining voice waits // A fresh install announces nothing, and records so the next update does. { - const r = await run({ seen: null, version: "1.10.3", app: [LINE] }); + const r = await run({ seen: null, version: "1.10.3", app: [LINE], joined: false }); assert.deepEqual(r.shown, [], "a fresh install is greeted with a what's-new modal"); assert.deepEqual(r.fetched, [], "a fresh install fetches the changelog for nothing"); assert.equal(r.seen, "1.10.3", "a fresh install did not record its version"); @@ -233,6 +234,31 @@ assert.deepEqual( assert.equal(readableDate("not-a-date"), "not-a-date", "a bad date renders as Invalid Date"); } +/* An install that has joined a server and has nothing recorded ran a build + where the recording never happened. It is not new. GRYT-1102. */ +{ + const r = await run({ seen: null, version: "1.10.3", app: [LINE], joined: true }); + assert.deepEqual( + r.shown, + [LINE], + "an install that has joined servers was treated as fresh, so the release that fixed this says nothing", + ); + assert.equal(r.seen, "1.10.3", "it showed the line without recording the version"); +} + +// And the two cases are told apart by what the helper actually reads. +{ + const fn = new Function( + "getUserValue", + block(source, "function hasJoinedAnything(): boolean {", "hasJoinedAnything") + .slice(1, -1) + .replace(" | null>", ""), + ); + assert.equal(fn(() => null), false, "no servers entry reads as having joined something"); + assert.equal(fn(() => ({})), false, "an empty servers entry reads as having joined something"); + assert.equal(fn(() => ({ "host:1": {} })), true, "a joined server reads as a fresh install"); +} + /* ── it waits for the per-user store ─────────────────────────────────────── */ // Read on mount, the store is empty every launch: a fresh install that says diff --git a/src/components/whatsNew.tsx b/src/components/whatsNew.tsx index 8381a3606..bfe147422 100644 --- a/src/components/whatsNew.tsx +++ b/src/components/whatsNew.tsx @@ -20,6 +20,12 @@ interface Entry { note?: boolean; } +/** Whether this install has ever joined a server, which a fresh one has not. */ +function hasJoinedAnything(): boolean { + const servers = getUserValue | null>("servers", null); + return !!servers && Object.keys(servers).length > 0; +} + /** * What changed, the first time somebody opens a version they have not seen. The * hand-written changelog lines, not the release body of commit subjects. @@ -39,9 +45,9 @@ export function WhatsNew() { const seen = getUserValue(SEEN_KEY, null); if (seen === version) return; - /* A fresh install has nothing to compare against. Announcing the version - somebody just chose to install reads as a bug, so record and stay quiet. */ - if (seen === null) { + /* Nothing recorded: a fresh install, or one that ran 1.11.0 or 1.11.1 and + never got to write it (GRYT-1101). Only the first should stay quiet. */ + if (seen === null && !hasJoinedAnything()) { setUserValue(SEEN_KEY, version); return; }