Files
multica/apps/web/features/landing/utils/github-release.test.ts
Naiyuan Qing 101da19b02 feat(download): fall back to previous release within 1h freshness window (#1514)
New /download visitors were seeing grayed-out macOS buttons in the 20-ish
minutes after a tag push because CI only builds Linux/Windows — Mac is
still packaged manually and uploads tens of minutes later. Swap the
`/releases/latest` fetch for `/releases?per_page=2` and, when the latest
release is under an hour old, render the previous (fully-populated)
release instead. After the freshness window, page auto-switches to latest.

Frontend-only change — GitHub "latest" marker, electron-updater, and
homebrew paths are untouched.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 19:05:36 +08:00

150 lines
4.3 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { fetchLatestRelease } from "./github-release";
const SAMPLE_LATEST_ASSET = {
name: "multica-desktop-0.2.14-mac-arm64.dmg",
browser_download_url:
"https://github.com/multica-ai/multica/releases/download/v0.2.14/multica-desktop-0.2.14-mac-arm64.dmg",
};
const SAMPLE_PREV_ASSET = {
name: "multica-desktop-0.2.13-mac-arm64.dmg",
browser_download_url:
"https://github.com/multica-ai/multica/releases/download/v0.2.13/multica-desktop-0.2.13-mac-arm64.dmg",
};
function releasePayload(overrides: {
tag: string;
publishedMinutesAgo?: number;
asset?: { name: string; browser_download_url: string };
prerelease?: boolean;
draft?: boolean;
}) {
const published = new Date(
Date.now() - (overrides.publishedMinutesAgo ?? 0) * 60_000,
).toISOString();
return {
tag_name: overrides.tag,
published_at: published,
html_url: `https://github.com/multica-ai/multica/releases/tag/${overrides.tag}`,
prerelease: overrides.prerelease ?? false,
draft: overrides.draft ?? false,
assets: overrides.asset ? [overrides.asset] : [],
};
}
function mockFetchWithReleases(releases: unknown[]) {
const fetchMock = vi.fn().mockResolvedValue(
new Response(JSON.stringify(releases), {
status: 200,
headers: { "Content-Type": "application/json" },
}),
);
vi.stubGlobal("fetch", fetchMock);
return fetchMock;
}
afterEach(() => {
vi.unstubAllGlobals();
});
describe("fetchLatestRelease", () => {
it("uses previous release when latest was published within the fresh window", async () => {
mockFetchWithReleases([
releasePayload({
tag: "v0.2.14",
publishedMinutesAgo: 10,
asset: SAMPLE_LATEST_ASSET,
}),
releasePayload({
tag: "v0.2.13",
publishedMinutesAgo: 60 * 24,
asset: SAMPLE_PREV_ASSET,
}),
]);
const result = await fetchLatestRelease();
expect(result.version).toBe("v0.2.13");
expect(result.assets.macArm64Dmg).toBe(SAMPLE_PREV_ASSET.browser_download_url);
});
it("uses latest release once it is older than the fresh window", async () => {
mockFetchWithReleases([
releasePayload({
tag: "v0.2.14",
publishedMinutesAgo: 120,
asset: SAMPLE_LATEST_ASSET,
}),
releasePayload({
tag: "v0.2.13",
publishedMinutesAgo: 60 * 24,
asset: SAMPLE_PREV_ASSET,
}),
]);
const result = await fetchLatestRelease();
expect(result.version).toBe("v0.2.14");
expect(result.assets.macArm64Dmg).toBe(SAMPLE_LATEST_ASSET.browser_download_url);
});
it("falls back to latest when there is no previous release", async () => {
mockFetchWithReleases([
releasePayload({
tag: "v0.0.1",
publishedMinutesAgo: 5,
asset: SAMPLE_LATEST_ASSET,
}),
]);
const result = await fetchLatestRelease();
expect(result.version).toBe("v0.0.1");
});
it("skips prereleases and drafts in the candidate list", async () => {
mockFetchWithReleases([
releasePayload({
tag: "v0.2.15-rc.1",
publishedMinutesAgo: 30,
prerelease: true,
}),
releasePayload({
tag: "v0.2.14",
publishedMinutesAgo: 120,
asset: SAMPLE_LATEST_ASSET,
}),
]);
const result = await fetchLatestRelease();
expect(result.version).toBe("v0.2.14");
});
it("returns an empty release shape when the API errors", async () => {
const fetchMock = vi.fn().mockResolvedValue(
new Response("rate limited", { status: 403 }),
);
vi.stubGlobal("fetch", fetchMock);
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
const result = await fetchLatestRelease();
expect(result).toEqual({
version: null,
publishedAt: null,
htmlUrl: null,
assets: {},
});
expect(warnSpy).toHaveBeenCalled();
warnSpy.mockRestore();
});
it("returns an empty release shape when all candidates are filtered out", async () => {
mockFetchWithReleases([
releasePayload({ tag: "v0.2.15-rc.1", prerelease: true }),
releasePayload({ tag: "v0.2.14-draft", draft: true }),
]);
const result = await fetchLatestRelease();
expect(result.version).toBeNull();
expect(result.assets).toEqual({});
});
});