Skip to content

Commit 439dc74

Browse files
committed
fix(desktop): classify invalid release feeds
1 parent eff2a95 commit 439dc74

4 files changed

Lines changed: 30 additions & 13 deletions

File tree

apps/sim/app/api/desktop/update/download/route.test.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,17 @@ describe('desktop update download route', () => {
137137
expect(await response.json()).toMatchObject({ error: 'Release installer unavailable' })
138138
})
139139

140+
it('reports an invalid feed when the release has no updater manifest', async () => {
141+
const incomplete = release('v1.4.0', DESKTOP_STABLE_RELEASE_REPOSITORY)
142+
incomplete.assets = incomplete.assets.filter((asset) => asset.name !== MANIFEST_ASSET_NAME)
143+
mockReleases([incomplete])
144+
145+
const response = await getDownload()
146+
147+
expect(response.status).toBe(502)
148+
expect(await response.json()).toMatchObject({ error: 'Release installer unavailable' })
149+
})
150+
140151
it('surfaces an unreadable release list instead of redirecting', async () => {
141152
fetchMock.mockResolvedValueOnce(new Response(null, { status: 500 }))
142153

apps/sim/app/api/desktop/update/latest-mac.yml/route.test.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,17 @@ describe('desktop update manifest route', () => {
167167
expect(fetchMock).toHaveBeenCalledTimes(1)
168168
})
169169

170+
it('reports an invalid feed when the release has no updater manifest', async () => {
171+
const incomplete = release('v1.1.0')
172+
incomplete.assets = incomplete.assets.filter((asset) => asset.name !== MANIFEST_ASSET_NAME)
173+
fetchMock.mockResolvedValueOnce(Response.json([incomplete]))
174+
175+
const response = await getFeed('www.sim.ai')
176+
177+
expect(response.status).toBe(502)
178+
expect(await response.json()).toMatchObject({ error: 'Release manifest unavailable' })
179+
})
180+
170181
it('walks past a page of unrelated releases to reach the newest desktop build', async () => {
171182
const filler = Array.from({ length: DESKTOP_RELEASES_PAGE_SIZE }, (_, index) => ({
172183
tag_name: `python-sdk-v0.${index}.0`,

apps/sim/lib/desktop/update-feed.test.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -111,19 +111,17 @@ describe('selectReleaseForChannel', () => {
111111
expect(selectReleaseForChannel(flagged, 'latest')?.tag_name).toBe('v0.5.24')
112112
})
113113

114-
it('skips releases missing the updater manifest asset', () => {
115-
// A release whose build failed (or is mid-upload) must not take the
116-
// channel down; the previous good release keeps serving.
114+
it('keeps releases missing the updater manifest eligible for candidate validation', () => {
117115
const withBrokenNewest = [
118116
release('v0.5.25-dev.413', { assets: [{ name: 'Sim-0.5.25-dev.413-universal.dmg' }] }),
119117
release('v0.5.25-dev.412'),
120118
]
121-
expect(selectReleaseForChannel(withBrokenNewest, 'dev')?.tag_name).toBe('v0.5.25-dev.412')
119+
expect(selectReleaseForChannel(withBrokenNewest, 'dev')?.tag_name).toBe('v0.5.25-dev.413')
122120
})
123121

124-
it('skips release listings without asset data', () => {
122+
it('keeps release listings without asset data eligible for candidate validation', () => {
125123
const bare = { tag_name: 'v0.5.25', draft: false, prerelease: false }
126-
expect(selectReleaseForChannel([bare, release('v0.5.24')], 'latest')?.tag_name).toBe('v0.5.24')
124+
expect(selectReleaseForChannel([bare, release('v0.5.24')], 'latest')?.tag_name).toBe('v0.5.25')
127125
})
128126

129127
it('skips drafts and unparseable tags', () => {

apps/sim/lib/desktop/update-feed.ts

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -80,9 +80,9 @@ export interface DesktopReleaseCandidate {
8080

8181
/**
8282
* Lists releases of the channel's own kind, newest first. Channels never see
83-
* another channel's artifacts (see module docs). Releases without their
84-
* updater manifest asset are skipped — a release created before its build
85-
* finished (or whose build failed) must not take the channel down.
83+
* another channel's artifacts (see module docs). Artifact validation happens
84+
* in the candidate resolver so invalid releases remain distinguishable from
85+
* a channel with no releases.
8686
*/
8787
function releasesForChannel(
8888
releases: DesktopReleaseCandidate[],
@@ -96,17 +96,14 @@ function releasesForChannel(
9696
// Defense in depth: a bare vX.Y.Z tag manually marked "pre-release" on
9797
// GitHub must not reach stable clients.
9898
if (channel === 'latest' && release.prerelease) continue
99-
if (!release.assets?.some((asset) => asset.name === MANIFEST_ASSET_NAME)) {
100-
continue
101-
}
10299
if (compareVersions(version, '0.0.0') === null) continue
103100
candidates.push({ release, version })
104101
}
105102
candidates.sort((left, right) => compareVersions(right.version, left.version) ?? 0)
106103
return candidates.map(({ release }) => release)
107104
}
108105

109-
/** Picks the newest release that passes the channel and manifest-presence checks. */
106+
/** Picks the newest release that passes the channel and version checks. */
110107
export function selectReleaseForChannel(
111108
releases: DesktopReleaseCandidate[],
112109
channel: DesktopUpdateChannel

0 commit comments

Comments
 (0)