Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 22 additions & 2 deletions src/lib/update-check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,18 +83,22 @@ type FetchLike = (url: string, init?: RequestInit) => Promise<Response>

let fetchImpl: FetchLike = fetch
let nowMs: () => number = Date.now
let versionImpl: string = BUILD_VERSION

export function __setUpdateCheckDepsForTests(o: {
fetch?: FetchLike
now?: () => number
currentVersion?: string
}): void {
if (o.fetch) fetchImpl = o.fetch
if (o.now) nowMs = o.now
if (o.currentVersion) versionImpl = o.currentVersion
}

export function __resetUpdateCheckDepsForTests(): void {
fetchImpl = fetch
nowMs = Date.now
versionImpl = BUILD_VERSION
cache = null
}

Expand Down Expand Up @@ -140,6 +144,21 @@ export function isNewerVersion(a: string, b: string): boolean {
return false
}

/**
* Strip a local-build suffix (`-dev+<sha>`) so a dev binary compares on its
* core version. build-sidecar.ts stamps non-release binaries as
* `<pkg.version>-dev+<sha>`; without this, a dev build of the current release
* (e.g. `0.4.35-dev+abc`) reads as *older* than the published `0.4.35` — since
* semver ranks a prerelease below its release — and perpetually self-reports
* "update available" for the version it's already running. A real prerelease
* channel (`-beta.N`, `-rc.N`) is left intact: those genuinely precede the
* release and should still see it as an upgrade.
*/
function normalizeCurrent(version: string): string {
const devAt = version.indexOf("-dev+")
return devAt === -1 ? version : version.slice(0, devAt)
}

interface UpdateManifest {
channels?: Record<string, { version?: unknown } | undefined>
}
Expand Down Expand Up @@ -176,7 +195,7 @@ export function parseManifestVersion(
* a real result. `force` bypasses the cache.
*/
export async function getUpdateStatus(force = false): Promise<UpdateStatus> {
const current = BUILD_VERSION
const current = versionImpl
const enabled = isUpdateCheckEnabled()
const checkedAt = cache ? new Date(cache.atMs).toISOString() : null

Expand Down Expand Up @@ -227,7 +246,8 @@ export async function getUpdateStatus(force = false): Promise<UpdateStatus> {
const status: UpdateStatus = {
current,
latest,
update_available: latest !== null && isNewerVersion(latest, current),
update_available:
latest !== null && isNewerVersion(latest, normalizeCurrent(current)),
url: DOWNLOAD_URL,
enabled: true,
checked_at: new Date(atMs).toISOString(),
Expand Down
40 changes: 40 additions & 0 deletions tests/update-check.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,46 @@ describe("getUpdateStatus", () => {
expect(status.update_available).toBe(false)
})

test("a dev build of the current release is up to date, not an upgrade", async () => {
// build-sidecar.ts stamps local binaries `<version>-dev+<sha>`; the
// published release of that same version must not read as an upgrade
// (a prerelease ranks below its release, so the naive compare flips it).
__setUpdateCheckDepsForTests({
fetch: () => Promise.resolve(manifestJson("0.4.35")),
currentVersion: "0.4.35-dev+abc12345",
})

const status = await getUpdateStatus()

expect(status.current).toBe("0.4.35-dev+abc12345") // full string, for diagnostics
expect(status.latest).toBe("0.4.35")
expect(status.update_available).toBe(false)
})

test("a dev build still sees a genuinely newer release", async () => {
__setUpdateCheckDepsForTests({
fetch: () => Promise.resolve(manifestJson("0.4.36")),
currentVersion: "0.4.35-dev+abc12345",
})

const status = await getUpdateStatus()

expect(status.update_available).toBe(true)
})

test("a real prerelease still sees its release as an upgrade", async () => {
// Only the `-dev+` local suffix is normalized; a beta/rc genuinely precedes
// the release and should still be offered the upgrade.
__setUpdateCheckDepsForTests({
fetch: () => Promise.resolve(manifestJson("0.5.0")),
currentVersion: "0.5.0-beta.2",
})

const status = await getUpdateStatus()

expect(status.update_available).toBe(true)
})

test("caches within the TTL; force bypasses it", async () => {
let clock = 1_000_000
__setUpdateCheckDepsForTests({
Expand Down
Loading