FN-7471: fix desktop update version fallback
Use packaged runtime metadata so desktop update checks do not compare npm releases against an unresolved current version. - Resolve @fusion/desktop package metadata as a fallback when dashboard code runs from a desktop deployment. - Fail closed when the current version is the 0.0.0 sentinel in both update-check paths. - Cover CLI, desktop, and unresolved-version update-check behavior with regression tests. - Add a patch changeset for the published Fusion CLI package. Files changed: .changeset/fn-7471-desktop-update-version.md | 7 ++ .../src/__tests__/cli-package-version.test.ts | 59 +++++++++++++++- .../src/__tests__/update-check-route.test.ts | 78 +++++++++++++++++++++- .../dashboard/src/__tests__/update-check.test.ts | 15 +++++ packages/dashboard/src/cli-package-version.ts | 37 +++++++++- packages/dashboard/src/server.ts | 12 +++- packages/dashboard/src/update-check.ts | 15 +++++ 7 files changed, 216 insertions(+), 7 deletions(-) Fusion-Task-Id: FN-7471 Fusion-Task-Lineage: ee633b8c-766b-43f3-855d-3ce7abd41550 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/fn-7471-desktop-update-version.md
Normal file
7
.changeset/fn-7471-desktop-update-version.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
summary: Prevent Desktop update banners from using 0.0.0 as the current version.
|
||||||
|
category: fix
|
||||||
|
dev: Dashboard update checks now resolve packaged @fusion/desktop metadata and fail closed for unresolved versions.
|
||||||
@@ -1,13 +1,19 @@
|
|||||||
// @vitest-environment node
|
// @vitest-environment node
|
||||||
|
|
||||||
import { readFileSync } from "node:fs";
|
import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
import { dirname, join } from "node:path";
|
import { dirname, join } from "node:path";
|
||||||
import { fileURLToPath } from "node:url";
|
import { pathToFileURL, fileURLToPath } from "node:url";
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { getCliPackageVersion, resolveCliPackageVersionInfo } from "../cli-package-version.js";
|
import { getCliPackageVersion, resolveCliPackageVersionInfo } from "../cli-package-version.js";
|
||||||
|
|
||||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
|
function writePackageJson(dir: string, manifest: { name: string; version?: string }): void {
|
||||||
|
mkdirSync(dir, { recursive: true });
|
||||||
|
writeFileSync(join(dir, "package.json"), JSON.stringify(manifest, null, 2), "utf-8");
|
||||||
|
}
|
||||||
|
|
||||||
describe("cli-package-version", () => {
|
describe("cli-package-version", () => {
|
||||||
it("resolves the published CLI package from dashboard source directories", () => {
|
it("resolves the published CLI package from dashboard source directories", () => {
|
||||||
const versionInfo = resolveCliPackageVersionInfo(join(__dirname, ".."));
|
const versionInfo = resolveCliPackageVersionInfo(join(__dirname, ".."));
|
||||||
@@ -21,6 +27,55 @@ describe("cli-package-version", () => {
|
|||||||
expect(versionInfo?.packageJsonPath).not.toBe(dashboardPackageJson);
|
expect(versionInfo?.packageJsonPath).not.toBe(dashboardPackageJson);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("resolves the published CLI package from an installed CLI ancestor", () => {
|
||||||
|
const root = mkdtempSync(join(tmpdir(), "fusion-cli-version-installed-"));
|
||||||
|
try {
|
||||||
|
const cliRoot = join(root, "node_modules", "@runfusion", "fusion");
|
||||||
|
const startDir = join(cliRoot, "dist", "dashboard");
|
||||||
|
writePackageJson(cliRoot, { name: "@runfusion/fusion", version: "8.7.6" });
|
||||||
|
mkdirSync(startDir, { recursive: true });
|
||||||
|
|
||||||
|
expect(resolveCliPackageVersionInfo(startDir)).toEqual({
|
||||||
|
packageJsonPath: join(cliRoot, "package.json"),
|
||||||
|
version: "8.7.6",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
rmSync(root, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("resolves packaged desktop metadata when no CLI manifest is present", () => {
|
||||||
|
const root = mkdtempSync(join(tmpdir(), "fusion-cli-version-desktop-"));
|
||||||
|
try {
|
||||||
|
const dashboardDist = join(root, "node_modules", "@fusion", "dashboard", "dist");
|
||||||
|
writePackageJson(root, { name: "@fusion/desktop", version: "5.4.3" });
|
||||||
|
writePackageJson(join(root, "node_modules", "@fusion", "dashboard"), { name: "@fusion/dashboard", version: "0.0.0" });
|
||||||
|
mkdirSync(dashboardDist, { recursive: true });
|
||||||
|
|
||||||
|
expect(resolveCliPackageVersionInfo(dashboardDist)).toEqual({
|
||||||
|
packageJsonPath: join(root, "package.json"),
|
||||||
|
version: "5.4.3",
|
||||||
|
});
|
||||||
|
expect(getCliPackageVersion(pathToFileURL(join(dashboardDist, "server.js")).href)).toBe("5.4.3");
|
||||||
|
} finally {
|
||||||
|
rmSync(root, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not treat missing or malformed metadata as a resolved version", () => {
|
||||||
|
const root = mkdtempSync(join(tmpdir(), "fusion-cli-version-missing-"));
|
||||||
|
try {
|
||||||
|
const dashboardDist = join(root, "node_modules", "@fusion", "dashboard", "dist");
|
||||||
|
writePackageJson(root, { name: "@fusion/desktop" });
|
||||||
|
writePackageJson(join(root, "node_modules", "@fusion", "dashboard"), { name: "@fusion/dashboard", version: "0.0.0" });
|
||||||
|
mkdirSync(dashboardDist, { recursive: true });
|
||||||
|
|
||||||
|
expect(resolveCliPackageVersionInfo(dashboardDist)).toBeNull();
|
||||||
|
} finally {
|
||||||
|
rmSync(root, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it("returns the published CLI version for dashboard consumers", () => {
|
it("returns the published CLI version for dashboard consumers", () => {
|
||||||
const expectedCliPackageJson = join(__dirname, "..", "..", "..", "cli", "package.json");
|
const expectedCliPackageJson = join(__dirname, "..", "..", "..", "cli", "package.json");
|
||||||
const expectedVersion = JSON.parse(readFileSync(expectedCliPackageJson, "utf-8")).version;
|
const expectedVersion = JSON.parse(readFileSync(expectedCliPackageJson, "utf-8")).version;
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
// @vitest-environment node
|
// @vitest-environment node
|
||||||
|
|
||||||
import { readFileSync } from "node:fs";
|
import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
import { dirname, join } from "node:path";
|
import { dirname, join } from "node:path";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||||
import { describe, it, expect, vi, afterEach } from "vitest";
|
import { describe, it, expect, vi, afterEach } from "vitest";
|
||||||
import type { TaskStore } from "@fusion/core";
|
import type { TaskStore } from "@fusion/core";
|
||||||
import { createServer } from "../server.js";
|
import { createServer } from "../server.js";
|
||||||
@@ -11,6 +12,7 @@ import { get as performGet, request as performRequest } from "../test-request.js
|
|||||||
const updateCheckMocks = vi.hoisted(() => ({
|
const updateCheckMocks = vi.hoisted(() => ({
|
||||||
performUpdateCheck: vi.fn(),
|
performUpdateCheck: vi.fn(),
|
||||||
performUpdateInstall: vi.fn(),
|
performUpdateInstall: vi.fn(),
|
||||||
|
cliPackageImportMetaUrl: undefined as string | undefined,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("../update-check.js", async () => {
|
vi.mock("../update-check.js", async () => {
|
||||||
@@ -22,6 +24,14 @@ vi.mock("../update-check.js", async () => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
vi.mock("../cli-package-version.js", async () => {
|
||||||
|
const actual = await vi.importActual<typeof import("../cli-package-version.js")>("../cli-package-version.js");
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
getCliPackageVersion: (importMetaUrl?: string) => actual.getCliPackageVersion(updateCheckMocks.cliPackageImportMetaUrl ?? importMetaUrl),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
vi.mock("@fusion/core", async () => {
|
vi.mock("@fusion/core", async () => {
|
||||||
const actual = await vi.importActual<typeof import("@fusion/core")>("@fusion/core");
|
const actual = await vi.importActual<typeof import("@fusion/core")>("@fusion/core");
|
||||||
return {
|
return {
|
||||||
@@ -88,11 +98,18 @@ function createMockStore(overrides: Partial<TaskStore> = {}): TaskStore {
|
|||||||
} as unknown as TaskStore;
|
} as unknown as TaskStore;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function writePackageJson(dir: string, manifest: { name: string; version?: string }): void {
|
||||||
|
mkdirSync(dir, { recursive: true });
|
||||||
|
writeFileSync(join(dir, "package.json"), JSON.stringify(manifest, null, 2), "utf-8");
|
||||||
|
}
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
updateCheckMocks.performUpdateCheck.mockReset();
|
updateCheckMocks.performUpdateCheck.mockReset();
|
||||||
updateCheckMocks.performUpdateInstall.mockReset();
|
updateCheckMocks.performUpdateInstall.mockReset();
|
||||||
|
updateCheckMocks.cliPackageImportMetaUrl = undefined;
|
||||||
vi.restoreAllMocks();
|
vi.restoreAllMocks();
|
||||||
vi.unstubAllGlobals();
|
vi.unstubAllGlobals();
|
||||||
|
vi.unstubAllEnvs();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("POST /api/update-check/install", () => {
|
describe("POST /api/update-check/install", () => {
|
||||||
@@ -190,6 +207,63 @@ describe("GET /api/updates/check", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("returns updateAvailable=false for packaged desktop when registry latest matches desktop version", async () => {
|
||||||
|
const root = mkdtempSync(join(tmpdir(), "fusion-update-route-desktop-"));
|
||||||
|
try {
|
||||||
|
const dashboardDist = join(root, "node_modules", "@fusion", "dashboard", "dist");
|
||||||
|
writePackageJson(root, { name: "@fusion/desktop", version: "5.4.3" });
|
||||||
|
writePackageJson(join(root, "node_modules", "@fusion", "dashboard"), { name: "@fusion/dashboard", version: "0.0.0" });
|
||||||
|
mkdirSync(dashboardDist, { recursive: true });
|
||||||
|
updateCheckMocks.cliPackageImportMetaUrl = pathToFileURL(join(dashboardDist, "server.js")).href;
|
||||||
|
vi.stubGlobal(
|
||||||
|
"fetch",
|
||||||
|
vi.fn().mockResolvedValue({
|
||||||
|
ok: true,
|
||||||
|
json: async () => ({ version: "5.4.3" }),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const app = createServer(createMockStore());
|
||||||
|
const response = await performGet(app, "/api/updates/check");
|
||||||
|
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
expect(response.body).toEqual({
|
||||||
|
currentVersion: "5.4.3",
|
||||||
|
latestVersion: "5.4.3",
|
||||||
|
updateAvailable: false,
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
rmSync(root, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("fails closed when the current version is unresolved", async () => {
|
||||||
|
const root = mkdtempSync(join(tmpdir(), "fusion-update-route-unresolved-"));
|
||||||
|
try {
|
||||||
|
const dashboardDist = join(root, "node_modules", "@fusion", "dashboard", "dist");
|
||||||
|
writePackageJson(join(root, "node_modules", "@fusion", "dashboard"), { name: "@fusion/dashboard", version: "0.0.0" });
|
||||||
|
mkdirSync(dashboardDist, { recursive: true });
|
||||||
|
updateCheckMocks.cliPackageImportMetaUrl = pathToFileURL(join(dashboardDist, "server.js")).href;
|
||||||
|
vi.stubEnv("npm_package_version", undefined);
|
||||||
|
const fetchSpy = vi.fn();
|
||||||
|
vi.stubGlobal("fetch", fetchSpy);
|
||||||
|
|
||||||
|
const app = createServer(createMockStore());
|
||||||
|
const response = await performGet(app, "/api/updates/check");
|
||||||
|
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
expect(fetchSpy).not.toHaveBeenCalled();
|
||||||
|
expect(response.body).toEqual({
|
||||||
|
currentVersion: "0.0.0",
|
||||||
|
latestVersion: null,
|
||||||
|
updateAvailable: false,
|
||||||
|
error: "Current Fusion version is unavailable",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
rmSync(root, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it("gracefully returns an error payload when npm registry is unreachable", async () => {
|
it("gracefully returns an error payload when npm registry is unreachable", async () => {
|
||||||
vi.stubGlobal("fetch", vi.fn().mockRejectedValue(new Error("network down")));
|
vi.stubGlobal("fetch", vi.fn().mockRejectedValue(new Error("network down")));
|
||||||
|
|
||||||
|
|||||||
@@ -125,6 +125,21 @@ describe("update-check", () => {
|
|||||||
expect(olderResult.updateAvailable).toBe(false);
|
expect(olderResult.updateAvailable).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("fails closed without fetching when current version is unresolved", async () => {
|
||||||
|
const fetchSpy = vi.fn();
|
||||||
|
vi.stubGlobal("fetch", fetchSpy);
|
||||||
|
|
||||||
|
await expect(performUpdateCheck(fusionDir, "0.0.0", { force: true })).resolves.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
currentVersion: "0.0.0",
|
||||||
|
latestVersion: null,
|
||||||
|
updateAvailable: false,
|
||||||
|
error: "Current Fusion version is unavailable",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(fetchSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
it("returns a non-throwing error result when network fetch fails", async () => {
|
it("returns a non-throwing error result when network fetch fails", async () => {
|
||||||
vi.stubGlobal("fetch", vi.fn().mockRejectedValue(new Error("network down")));
|
vi.stubGlobal("fetch", vi.fn().mockRejectedValue(new Error("network down")));
|
||||||
|
|
||||||
|
|||||||
@@ -3,20 +3,23 @@ import { dirname, resolve } from "node:path";
|
|||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
const CLI_PACKAGE_NAME = "@runfusion/fusion";
|
const CLI_PACKAGE_NAME = "@runfusion/fusion";
|
||||||
|
const DESKTOP_PACKAGE_NAME = "@fusion/desktop";
|
||||||
|
const CLI_PACKAGE_NAMES = new Set([CLI_PACKAGE_NAME]);
|
||||||
|
const DESKTOP_PACKAGE_NAMES = new Set([DESKTOP_PACKAGE_NAME]);
|
||||||
|
|
||||||
export interface CliPackageVersionInfo {
|
export interface CliPackageVersionInfo {
|
||||||
packageJsonPath: string;
|
packageJsonPath: string;
|
||||||
version: string;
|
version: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function readCliPackageVersion(pkgPath: string): CliPackageVersionInfo | null {
|
function readPackageVersion(pkgPath: string, packageNames: ReadonlySet<string>): CliPackageVersionInfo | null {
|
||||||
if (!existsSync(pkgPath)) {
|
if (!existsSync(pkgPath)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(readFileSync(pkgPath, "utf-8")) as { name?: string; version?: string };
|
const parsed = JSON.parse(readFileSync(pkgPath, "utf-8")) as { name?: string; version?: string };
|
||||||
if (parsed.name === CLI_PACKAGE_NAME && typeof parsed.version === "string" && parsed.version.length > 0) {
|
if (parsed.name && packageNames.has(parsed.name) && typeof parsed.version === "string" && parsed.version.length > 0) {
|
||||||
return {
|
return {
|
||||||
packageJsonPath: pkgPath,
|
packageJsonPath: pkgPath,
|
||||||
version: parsed.version,
|
version: parsed.version,
|
||||||
@@ -29,6 +32,14 @@ function readCliPackageVersion(pkgPath: string): CliPackageVersionInfo | null {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function readCliPackageVersion(pkgPath: string): CliPackageVersionInfo | null {
|
||||||
|
return readPackageVersion(pkgPath, CLI_PACKAGE_NAMES);
|
||||||
|
}
|
||||||
|
|
||||||
|
function readDesktopPackageVersion(pkgPath: string): CliPackageVersionInfo | null {
|
||||||
|
return readPackageVersion(pkgPath, DESKTOP_PACKAGE_NAMES);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve the published CLI package version from dashboard code.
|
* Resolve the published CLI package version from dashboard code.
|
||||||
*
|
*
|
||||||
@@ -36,6 +47,7 @@ function readCliPackageVersion(pkgPath: string): CliPackageVersionInfo | null {
|
|||||||
* - Monorepo source: `packages/dashboard/src/...` with sibling `packages/cli/package.json`
|
* - Monorepo source: `packages/dashboard/src/...` with sibling `packages/cli/package.json`
|
||||||
* - Installed dependency: `node_modules/@runfusion/fusion/dist/...`
|
* - Installed dependency: `node_modules/@runfusion/fusion/dist/...`
|
||||||
* - Bundled CLI: dashboard code inlined into `dist/bin.js` next to the CLI manifest
|
* - Bundled CLI: dashboard code inlined into `dist/bin.js` next to the CLI manifest
|
||||||
|
* - Packaged Desktop: `node_modules/@fusion/dashboard/dist/...` under the staged `@fusion/desktop` app manifest
|
||||||
*/
|
*/
|
||||||
export function resolveCliPackageVersionInfo(startDir: string): CliPackageVersionInfo | null {
|
export function resolveCliPackageVersionInfo(startDir: string): CliPackageVersionInfo | null {
|
||||||
// First pass: walk ancestors looking for the @runfusion/fusion package.json directly.
|
// First pass: walk ancestors looking for the @runfusion/fusion package.json directly.
|
||||||
@@ -71,9 +83,30 @@ export function resolveCliPackageVersionInfo(startDir: string): CliPackageVersio
|
|||||||
currentDir = parentDir;
|
currentDir = parentDir;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* FNXC:DesktopUpdates 2026-07-03-15:30:
|
||||||
|
* Desktop embeds @fusion/dashboard inside a staged @fusion/desktop deploy tree, not inside the published @runfusion/fusion package. The dashboard npm update banner still compares against @runfusion/fusion releases, so use the desktop app manifest as the deterministic packaged-runtime version fallback after CLI-specific probes fail; never use the private @fusion/dashboard version for user-facing update availability.
|
||||||
|
*/
|
||||||
|
currentDir = startDir;
|
||||||
|
for (let i = 0; i < 8; i += 1) {
|
||||||
|
const versionInfo = readDesktopPackageVersion(resolve(currentDir, "package.json"));
|
||||||
|
if (versionInfo) {
|
||||||
|
return versionInfo;
|
||||||
|
}
|
||||||
|
const parentDir = resolve(currentDir, "..");
|
||||||
|
if (parentDir === currentDir) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
currentDir = parentDir;
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isUnresolvedCliPackageVersion(version: string): boolean {
|
||||||
|
return version === "0.0.0";
|
||||||
|
}
|
||||||
|
|
||||||
export function getCliPackageVersion(importMetaUrl: string = import.meta.url): string {
|
export function getCliPackageVersion(importMetaUrl: string = import.meta.url): string {
|
||||||
const startDir = dirname(fileURLToPath(importMetaUrl));
|
const startDir = dirname(fileURLToPath(importMetaUrl));
|
||||||
return resolveCliPackageVersionInfo(startDir)?.version ?? process.env.npm_package_version ?? "0.0.0";
|
return resolveCliPackageVersionInfo(startDir)?.version ?? process.env.npm_package_version ?? "0.0.0";
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ import { createCliSessionsRouter } from "./routes/cli-sessions.js";
|
|||||||
import { getProjectIdFromRequest } from "./routes/context.js";
|
import { getProjectIdFromRequest } from "./routes/context.js";
|
||||||
import type { CliRelaunchRegistry } from "./cli-session-transport.js";
|
import type { CliRelaunchRegistry } from "./cli-session-transport.js";
|
||||||
import { validateRemoteAuthToken } from "./remote-auth.js";
|
import { validateRemoteAuthToken } from "./remote-auth.js";
|
||||||
import { getCliPackageVersion } from "./cli-package-version.js";
|
import { getCliPackageVersion, isUnresolvedCliPackageVersion } from "./cli-package-version.js";
|
||||||
import {
|
import {
|
||||||
dayHasSamples,
|
dayHasSamples,
|
||||||
fileScopeInvariantFailuresPerDay,
|
fileScopeInvariantFailuresPerDay,
|
||||||
@@ -1719,6 +1719,16 @@ export function createServer(store: TaskStore, options?: ServerOptions): ReturnT
|
|||||||
const currentVersion = cliPackageVersion;
|
const currentVersion = cliPackageVersion;
|
||||||
res.set("Cache-Control", "no-store");
|
res.set("Cache-Control", "no-store");
|
||||||
|
|
||||||
|
if (isUnresolvedCliPackageVersion(currentVersion)) {
|
||||||
|
res.status(200).json({
|
||||||
|
currentVersion,
|
||||||
|
latestVersion: null,
|
||||||
|
updateAvailable: false,
|
||||||
|
error: "Current Fusion version is unavailable",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
const timeout = setTimeout(() => controller.abort(), 10_000);
|
const timeout = setTimeout(() => controller.abort(), 10_000);
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ const INSTALL_MAX_BUFFER = 10 * 1024 * 1024;
|
|||||||
|
|
||||||
const DAY_MS = 24 * 60 * 60 * 1000;
|
const DAY_MS = 24 * 60 * 60 * 1000;
|
||||||
const execAsync = promisify(exec);
|
const execAsync = promisify(exec);
|
||||||
|
const UNRESOLVED_VERSION = "0.0.0";
|
||||||
|
|
||||||
/** Allowed update-check cadences from GlobalSettings. */
|
/** Allowed update-check cadences from GlobalSettings. */
|
||||||
export type UpdateCheckFrequency = "manual" | "on-startup" | "daily" | "weekly";
|
export type UpdateCheckFrequency = "manual" | "on-startup" | "daily" | "weekly";
|
||||||
@@ -202,6 +203,20 @@ export async function performUpdateCheck(
|
|||||||
options: { frequency?: UpdateCheckFrequency; force?: boolean } = {},
|
options: { frequency?: UpdateCheckFrequency; force?: boolean } = {},
|
||||||
): Promise<UpdateCheckResult> {
|
): Promise<UpdateCheckResult> {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
|
||||||
|
/*
|
||||||
|
* FNXC:DesktopUpdates 2026-07-03-15:35:
|
||||||
|
* `0.0.0` is a resolver sentinel, not an installed Fusion version. If packaged/runtime metadata is missing, update surfaces must fail closed instead of comparing npm's latest release against zero and showing a false-positive desktop update banner.
|
||||||
|
*/
|
||||||
|
if (currentVersion === UNRESOLVED_VERSION) {
|
||||||
|
return {
|
||||||
|
currentVersion,
|
||||||
|
latestVersion: null,
|
||||||
|
updateAvailable: false,
|
||||||
|
lastChecked: now,
|
||||||
|
error: "Current Fusion version is unavailable",
|
||||||
|
};
|
||||||
|
}
|
||||||
const cached = readCachedUpdateCheck(fusionDir);
|
const cached = readCachedUpdateCheck(fusionDir);
|
||||||
const cacheMatchesCurrentVersion = !cached || cached.currentVersion === currentVersion;
|
const cacheMatchesCurrentVersion = !cached || cached.currentVersion === currentVersion;
|
||||||
const ttl = ttlForFrequency(options.frequency);
|
const ttl = ttlForFrequency(options.frequency);
|
||||||
|
|||||||
Reference in New Issue
Block a user