diff --git a/releases.js b/releases.js index b6a1b3e..76bee48 100644 --- a/releases.js +++ b/releases.js @@ -23,18 +23,45 @@ function clip(text) { return plain.length > 280 ? `${plain.slice(0, 280)}…` : plain; } +// Words a version question shares with every release note. They do not say which release is meant. +const GENERIC = new Set( + ('the and for with from that this what whats which where when how are was can does did you your any there have has get ' + + 'new newest latest current version versions release releases note notes update updates fix fixed fixes bug bugs ' + + 'app apps omi download downloads install installer firmware desktop mac macos macbook windows computer phone ' + + 'mobile android ios iphone ipad testflight').split(' ') +); + +// The one product a text names, or '' when it names none or more than one. +function productOf(text) { + const s = String(text || ''); + const named = []; + if (/\bfirmware\b/i.test(s)) named.push('firmware'); + if (/\b(?:desktop|mac(?!\s+address)|macos|macbook|windows|computer)\b/i.test(s)) named.push('desktop'); + if (/\b(?:phone|mobile|android|ios|iphone|ipad|testflight)\b/i.test(s)) named.push('phone'); + return named.length === 1 ? named[0] : ''; +} + +function publishedTime(note) { + const time = Date.parse(note?.publishedAt || ''); + return Number.isFinite(time) ? time : 0; +} + function bestNote(question, notes) { - const wanted = new Set(words(question)); + const product = productOf(question); + const eligible = (notes || []).filter((note) => !product || productOf(`${note.name} ${note.tag}`) === product); + const wanted = new Set(words(question).filter((word) => !GENERIC.has(word))); let best = null; - let score = 0; - for (const note of notes || []) { - const shared = words(`${note.name} ${note.body}`).filter((word) => wanted.has(word)).length; - if (shared > score) { + let score = -1; + for (const note of eligible) { + const shared = new Set(words(`${note.name} ${note.body}`).filter((word) => wanted.has(word))).size; + // Lists arrive newest first, so on a tie the earlier note stays unless a later one is dated newer. + if (shared > score || (shared === score && publishedTime(note) > publishedTime(best))) { best = note; score = shared; } } - if (!best || score < 1) return ''; + // A question about one change needs a note that mentions it. A plain "latest version" takes the newest. + if (!best || (wanted.size && score < 1)) return ''; const releaseUrl = best.tag ? `https://github.com/BasedHardware/omi/releases/tag/${encodeURIComponent(best.tag)}` : ''; diff --git a/test/releases.test.js b/test/releases.test.js index 2713d97..9470010 100644 --- a/test/releases.test.js +++ b/test/releases.test.js @@ -34,3 +34,54 @@ test('a charging question does not pull a release note', async () => { assert.equal(text, ''); assert.equal(called, false); }); + +const desktopFeed = [ + { + tag_name: 'v0.12.464+12464-macos', + name: 'Omi Desktop v0.12.464 (candidate)', + body: "## OMI Desktop v0.12.464\n\n### What's New\n- Bug fixes and improvements", + published_at: '2026-10-10T12:00:00Z', + }, + { + tag_name: 'v0.12.463+12463-macos', + name: 'Omi Desktop v0.12.463 (candidate)', + body: "## OMI Desktop v0.12.463\n\n### What's New\n- Settings now say plainly what the app does with the latest version of your data", + published_at: '2026-10-10T09:00:00Z', + }, + { + tag_name: 'v0.12.452+12452-macos', + name: 'Omi Desktop v0.12.452 (candidate)', + body: "## OMI Desktop v0.12.452\n\n### What's New\n- Fixed chat failing with a generic error for BYOK users in the desktop app", + published_at: '2026-10-08T12:00:00Z', + }, +]; + +function releaseFor(question, feed = desktopFeed) { + resetReleaseCache(); + return matchingRelease(question, { fetchImpl: async () => ({ ok: true, json: async () => feed }), store: null }); +} + +test('a latest-version question gets the newest release, not the one that shares the most words', async () => { + for (const question of [ + 'What is the latest version of the desktop app?', + 'Where do I download the desktop app, which version?', + "What's new in the desktop app?", + ]) { + assert.match(await releaseFor(question), /v0\.12\.464/, question); + } +}); + +test('a firmware or phone app question is not handed a desktop release note', async () => { + for (const question of [ + 'Is there a firmware update for my Omi?', + "What's new in the latest version of the phone app?", + 'Any new version of the Android app?', + ]) { + assert.equal(await releaseFor(question), '', question); + } +}); + +test('a question about one fix still gets the release that mentions it', async () => { + assert.match(await releaseFor('Which desktop version fixed the BYOK chat error?'), /v0\.12\.452/); + assert.equal(await releaseFor('Which desktop version fixed the calendar sync?'), ''); +});