FN-7360: fix Windows desktop runtime packaging
Fix packaged Windows desktop startup by preserving Node-safe runtime dependencies and dashboard registry assets. - Externalize Electron main/preload npm packages so CommonJS updater dependencies load natively. - Load the dashboard plugin registry manifest through file IO and copy it into dashboard server dist from all build paths. - Split Windows NSIS and portable artifact names and verify required Electron runtime resources in CI. - Cover the packaging/runtime invariants with desktop and dashboard regression tests. Files changed: .changeset/fn-7360-desktop-windows.md | 7 +++ .github/workflows/desktop-windows.yml | 21 ++++++++ packages/dashboard/package.json | 2 +- .../src/__tests__/plugin-registry-dist.test.ts | 32 ++++++++++++ .../src/__tests__/routes-plugin-registry.test.ts | 10 ++-- packages/dashboard/src/plugin-routes.ts | 27 +++++++++- packages/desktop/README.md | 5 +- packages/desktop/electron-builder.yml | 9 ++++ packages/desktop/scripts/build.ts | 29 ++++++++-- packages/desktop/scripts/workspace-tools.ts | 4 ++ .../desktop/src/__tests__/build-bundling.test.ts | 61 ++++++++++++++++++++++ .../src/__tests__/electron-builder-config.test.ts | 15 ++++++ packages/desktop/src/__tests__/ipc.test.ts | 15 ++++++ .../desktop/src/__tests__/local-runtime.test.ts | 20 +++++++ 14 files changed, 247 insertions(+), 10 deletions(-) Fusion-Task-Id: FN-7360 Fusion-Task-Lineage: a2d88130-c24f-43e2-932a-1e174b6afa35 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/fn-7360-desktop-windows.md
Normal file
7
.changeset/fn-7360-desktop-windows.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
summary: Fix Windows desktop startup failures in packaged builds.
|
||||
category: fix
|
||||
dev: Externalizes Electron updater CJS dependencies, loads the dashboard registry manifest through Node-safe file IO, and separates NSIS/portable Windows artifacts.
|
||||
21
.github/workflows/desktop-windows.yml
vendored
21
.github/workflows/desktop-windows.yml
vendored
@@ -58,6 +58,27 @@ jobs:
|
||||
if ($sig.Status -ne 'Valid') { Write-Error "Signature invalid: $($exe.Name) ($($sig.Status))"; exit 1 }
|
||||
}
|
||||
|
||||
- name: Verify Windows runtime resources
|
||||
shell: pwsh
|
||||
run: |
|
||||
# FNXC:WindowsDesktopPackaging 2026-07-01-08:08:
|
||||
# The Windows app must install Electron's root .pak runtime resources;
|
||||
# missing chrome_100_percent.pak, chrome_200_percent.pak, or resources.pak
|
||||
# leaves Fusion.exe unable to start even when the NSIS installer succeeds.
|
||||
$requiredResources = @('chrome_100_percent.pak', 'chrome_200_percent.pak', 'resources.pak')
|
||||
$unpackedRoots = Get-ChildItem packages/desktop/dist-electron -Directory -Filter 'win*-unpacked'
|
||||
if ($unpackedRoots.Count -eq 0) { Write-Error "No win-unpacked directory produced"; exit 1 }
|
||||
foreach ($root in $unpackedRoots) {
|
||||
foreach ($resource in $requiredResources) {
|
||||
$resourcePath = Join-Path $root.FullName $resource
|
||||
if (!(Test-Path $resourcePath)) { Write-Error "Missing Electron runtime resource: $resourcePath"; exit 1 }
|
||||
}
|
||||
}
|
||||
$nsis = Get-ChildItem packages/desktop/dist-electron -Filter 'Fusion-*-win-*.exe' | Where-Object { $_.Name -notmatch '-portable\.exe$' }
|
||||
$portable = Get-ChildItem packages/desktop/dist-electron -Filter 'Fusion-*-win-*-portable.exe'
|
||||
if ($nsis.Count -eq 0) { Write-Error "No NSIS installer artifact produced"; exit 1 }
|
||||
if ($portable.Count -eq 0) { Write-Error "No portable EXE artifact produced"; exit 1 }
|
||||
|
||||
# Automated publish is intentionally deferred to FN-5593.
|
||||
# Keep a single artifact; filenames include -x64 / -arm64 so both arches are captured.
|
||||
- name: Upload Windows artifacts
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
"gen:locales": "node scripts/sync-locales.mjs",
|
||||
"verify:locale-chunks": "node scripts/assert-locale-chunks.mjs",
|
||||
"prebuild": "node scripts/sync-locales.mjs",
|
||||
"build": "vite build && tsc",
|
||||
"build": "vite build && tsc && node -e \"require('node:fs').copyFileSync('src/registry-manifest.json','dist/registry-manifest.json')\"",
|
||||
"prebuild:client": "node scripts/sync-locales.mjs",
|
||||
"build:client": "vite build",
|
||||
"predev:serve": "node scripts/sync-locales.mjs",
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
// @vitest-environment node
|
||||
|
||||
import { access, readFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const dashboardRoot = path.resolve(__dirname, "../..");
|
||||
|
||||
async function readDashboardFile(relativePath: string): Promise<string> {
|
||||
return readFile(path.join(dashboardRoot, relativePath), "utf-8");
|
||||
}
|
||||
|
||||
describe("plugin registry production output", () => {
|
||||
it("does not emit a Node 22-invalid static JSON import for registry-manifest.json", async () => {
|
||||
const pluginRoutesDist = await readDashboardFile("dist/plugin-routes.js");
|
||||
|
||||
expect(pluginRoutesDist).not.toMatch(/import\s+\w+\s+from\s+["']\.\/registry-manifest\.json["'];?/);
|
||||
expect(pluginRoutesDist).toContain('new URL("./registry-manifest.json", import.meta.url)');
|
||||
expect(pluginRoutesDist).toContain("JSON.parse");
|
||||
});
|
||||
|
||||
it("copies a readable registry manifest beside the emitted dashboard server files", async () => {
|
||||
const manifestPath = path.join(dashboardRoot, "dist/registry-manifest.json");
|
||||
await expect(access(manifestPath)).resolves.toBeUndefined();
|
||||
|
||||
const manifest = JSON.parse(await readFile(manifestPath, "utf-8")) as { plugins?: unknown };
|
||||
expect(Array.isArray(manifest.plugins)).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -4,8 +4,7 @@ import { describe, expect, it, vi, beforeEach } from "vitest";
|
||||
import express from "express";
|
||||
import type { PluginInstallation, PluginStore } from "@fusion/core";
|
||||
|
||||
import registryManifest from "../registry-manifest.json";
|
||||
import { buildRegistryPluginEntries, createPluginRouter } from "../plugin-routes.js";
|
||||
import { buildRegistryPluginEntries, createPluginRouter, loadRegistryManifest } from "../plugin-routes.js";
|
||||
import { get as performGet } from "../test-request.js";
|
||||
import * as projectStoreResolver from "../project-store-resolver.js";
|
||||
|
||||
@@ -68,9 +67,12 @@ describe("GET /api/plugins/registry", () => {
|
||||
const res = await performGet(buildApp(pluginStore), "/api/plugins/registry");
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
const registryManifest = await loadRegistryManifest();
|
||||
const manifestPlugins = Array.isArray(registryManifest.plugins) ? registryManifest.plugins : [];
|
||||
|
||||
// Registry currently includes Agent Browser as metadata-only plus 3 discovery-only partner/plugin ideas.
|
||||
expect(registryManifest.plugins.filter((plugin) => !plugin.path)).toHaveLength(4);
|
||||
expect((res.body as { plugins: unknown[] }).plugins).toHaveLength(registryManifest.plugins.length);
|
||||
expect(manifestPlugins.filter((plugin) => typeof plugin === "object" && plugin && !("path" in plugin))).toHaveLength(4);
|
||||
expect((res.body as { plugins: unknown[] }).plugins).toHaveLength(manifestPlugins.length);
|
||||
});
|
||||
|
||||
it("filters by q across searchable text", async () => {
|
||||
|
||||
@@ -19,7 +19,6 @@ import { Router, type Request, type Response } from "express";
|
||||
import { access, stat, readFile } from "node:fs/promises";
|
||||
import { join, isAbsolute, dirname, basename } from "node:path";
|
||||
import { emitPluginCustomSseEvent } from "./sse.js";
|
||||
import registryManifest from "./registry-manifest.json";
|
||||
import type {
|
||||
PluginInstallation,
|
||||
PluginLoader,
|
||||
@@ -80,6 +79,31 @@ interface RegistryManifestShape {
|
||||
plugins?: unknown;
|
||||
}
|
||||
|
||||
const registryManifestUrl = new URL("./registry-manifest.json", import.meta.url);
|
||||
let cachedRegistryManifest: RegistryManifestShape | null = null;
|
||||
|
||||
export async function loadRegistryManifest(): Promise<RegistryManifestShape> {
|
||||
if (cachedRegistryManifest) {
|
||||
return cachedRegistryManifest;
|
||||
}
|
||||
|
||||
try {
|
||||
const raw = await readFile(registryManifestUrl, "utf-8");
|
||||
cachedRegistryManifest = JSON.parse(raw) as RegistryManifestShape;
|
||||
return cachedRegistryManifest;
|
||||
} catch (error) {
|
||||
// FNXC:PluginRegistry 2026-07-01-07:45:
|
||||
// Desktop local mode imports the dashboard server under Node 22+, where static
|
||||
// JSON imports require attributes that TypeScript did not emit. Read the
|
||||
// registry manifest as data at request time and degrade to an empty registry
|
||||
// when the packaged manifest is missing or malformed so startup never fails at
|
||||
// module load with ERR_IMPORT_ATTRIBUTE_MISSING.
|
||||
console.warn("[dashboard/plugins] Registry manifest unavailable; serving an empty plugin registry", error);
|
||||
cachedRegistryManifest = {};
|
||||
return cachedRegistryManifest;
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeRegistryManifestEntries(manifest: RegistryManifestShape): RegistryManifestEntry[] {
|
||||
if (!manifest || !Array.isArray(manifest.plugins)) {
|
||||
return [];
|
||||
@@ -330,6 +354,7 @@ export function createPluginRouter(
|
||||
: undefined;
|
||||
const scopedStore = projectId ? await getOrCreateProjectStore(projectId) : null;
|
||||
const store = scopedStore?.getPluginStore?.() ?? pluginStore;
|
||||
const registryManifest = await loadRegistryManifest();
|
||||
const plugins = await buildRegistryPluginEntries(registryManifest, store, { q, category });
|
||||
res.json({ plugins });
|
||||
}));
|
||||
|
||||
@@ -358,7 +358,10 @@ Desktop packaging is configured in `electron-builder.yml`.
|
||||
|
||||
- Output directory: `packages/desktop/dist-electron`
|
||||
- Targets: macOS (`dmg`, `zip`), Windows (`nsis`, `portable`), Linux (`AppImage`, `deb`, `tar.gz`)
|
||||
- Windows artifacts: `Fusion-<version>-win-x64.exe` and `Fusion-<version>-win-arm64.exe` (both NSIS + portable variants) in `packages/desktop/dist-electron/`
|
||||
- Windows NSIS installer artifacts: `Fusion-<version>-win-x64.exe` and `Fusion-<version>-win-arm64.exe` in `packages/desktop/dist-electron/`
|
||||
- Windows portable artifacts: `Fusion-<version>-win-x64-portable.exe` and `Fusion-<version>-win-arm64-portable.exe` in `packages/desktop/dist-electron/`
|
||||
- Silent NSIS installs support a custom destination with `/S /D=<absolute path>`; keep `/D=...` as the final installer argument (for example, `Fusion-<version>-win-x64.exe /S /D=C:\\Users\\me\\Tools\\fusion`).
|
||||
- The Windows packaging workflow verifies `win*-unpacked` contains Electron root runtime resources (`chrome_100_percent.pak`, `chrome_200_percent.pak`, and `resources.pak`) before uploading artifacts.
|
||||
- Binary GitHub Release workflow (`.github/workflows/release.yml`) now attaches desktop artifacts for all supported platforms:
|
||||
- Electron-updater feed files are also published per platform: `latest.yml` (Windows), `latest-mac.yml` (macOS), and `latest-linux.yml` (Linux). `setupAutoUpdater` / `triggerUpdateCheck` resolve these feeds from the GitHub Release channel.
|
||||
- Windows: x64 + arm64 outputs (NSIS + portable), matching `.exe.sha256` sidecars, and `.blockmap` files.
|
||||
|
||||
@@ -142,11 +142,20 @@ win:
|
||||
publisherName: Fusion
|
||||
|
||||
nsis:
|
||||
# FNXC:WindowsDesktopPackaging 2026-07-01-08:02:
|
||||
# NSIS is the installable Windows artifact and must keep the 0.50-style
|
||||
# Fusion-<version>-win-<arch>.exe name so updater/install docs remain stable.
|
||||
# The portable target writes a separate -portable EXE below to avoid colliding
|
||||
# with or overwriting the NSIS installer during x64/arm64 packaging.
|
||||
artifactName: "${productName}-${version}-${os}-${arch}.${ext}"
|
||||
oneClick: false
|
||||
perMachine: false
|
||||
allowElevation: false
|
||||
allowToChangeInstallationDirectory: true
|
||||
|
||||
portable:
|
||||
artifactName: "${productName}-${version}-${os}-${arch}-portable.${ext}"
|
||||
|
||||
linux:
|
||||
category: Development
|
||||
target:
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { build } from "esbuild";
|
||||
import { cp, mkdir, rm, stat } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { buildDashboardClient, packageRoot, workspaceRoot } from "./workspace-tools";
|
||||
const dashboardClientDir = join(workspaceRoot, "packages", "dashboard", "dist", "client");
|
||||
import { buildDashboard, buildDashboardClient, packageRoot, workspaceRoot } from "./workspace-tools";
|
||||
const dashboardRoot = join(workspaceRoot, "packages", "dashboard");
|
||||
const dashboardClientDir = join(dashboardRoot, "dist", "client");
|
||||
const dashboardRegistryManifestSource = join(dashboardRoot, "src", "registry-manifest.json");
|
||||
const dashboardRegistryManifestDist = join(dashboardRoot, "dist", "registry-manifest.json");
|
||||
const desktopDistDir = join(packageRoot, "dist");
|
||||
const desktopClientDistDir = join(desktopDistDir, "client");
|
||||
// FNXC:DesktopBuild 2026-06-25-09:45:
|
||||
@@ -23,7 +26,15 @@ const mainExternals = sharedExternals;
|
||||
const preloadExternals = sharedExternals;
|
||||
|
||||
async function ensureDashboardBuild(): Promise<void> {
|
||||
console.log("[desktop:build] Building dashboard client...");
|
||||
// FNXC:DesktopBuild 2026-07-01-11:35:
|
||||
// Windows release packaging invokes only `@fusion/desktop build` before electron-builder.
|
||||
// Build the dashboard server dist and copy registry-manifest.json here so the packaged
|
||||
// embedded runtime never depends on a separate `@fusion/dashboard build` workflow step.
|
||||
console.log("[desktop:build] Building dashboard server runtime...");
|
||||
await buildDashboard();
|
||||
await cp(dashboardRegistryManifestSource, dashboardRegistryManifestDist);
|
||||
|
||||
console.log("[desktop:build] Building dashboard client for file:// desktop loading...");
|
||||
await buildDashboardClient();
|
||||
|
||||
try {
|
||||
@@ -31,6 +42,12 @@ async function ensureDashboardBuild(): Promise<void> {
|
||||
} catch {
|
||||
throw new Error(`Dashboard client assets not found: ${dashboardClientDir}`);
|
||||
}
|
||||
|
||||
try {
|
||||
await stat(dashboardRegistryManifestDist);
|
||||
} catch {
|
||||
throw new Error(`Dashboard registry manifest not found: ${dashboardRegistryManifestDist}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function buildElectronEntrypoints(): Promise<void> {
|
||||
@@ -45,6 +62,12 @@ async function buildElectronEntrypoints(): Promise<void> {
|
||||
platform: "node",
|
||||
target: "node22",
|
||||
sourcemap: true,
|
||||
// FNXC:DesktopBuild 2026-07-01-07:31:
|
||||
// Windows Electron main output is ESM, but electron-updater loads CJS deps
|
||||
// such as fs-extra/graceful-fs that dynamically require built-ins. Keep all
|
||||
// npm packages external so Node/Electron evaluates those CJS modules natively
|
||||
// instead of esbuild emitting a __require("fs") trap in dist/main.js.
|
||||
packages: "external",
|
||||
external: mainExternals,
|
||||
logLevel: "info",
|
||||
}),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { spawn } from "node:child_process";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { existsSync } from "node:fs";
|
||||
import { cp } from "node:fs/promises";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
@@ -64,6 +65,9 @@ export async function buildDashboard(): Promise<void> {
|
||||
await buildDashboardRuntimePlugins();
|
||||
await runWorkspaceBin("vite", ["build"], dashboardRoot);
|
||||
await runWorkspaceBin("tsc", [], dashboardRoot);
|
||||
// FNXC:DesktopBuild 2026-07-01-11:45:
|
||||
// Desktop release and test paths call this helper directly instead of the dashboard package script, so copy the Node-read registry manifest beside server dist here as the shared build invariant.
|
||||
await cp(resolve(dashboardRoot, "src", "registry-manifest.json"), resolve(dashboardRoot, "dist", "registry-manifest.json"));
|
||||
}
|
||||
|
||||
export async function buildDashboardClient(): Promise<void> {
|
||||
|
||||
61
packages/desktop/src/__tests__/build-bundling.test.ts
Normal file
61
packages/desktop/src/__tests__/build-bundling.test.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { readFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const desktopRoot = path.resolve(__dirname, "../..");
|
||||
|
||||
async function readDesktopFile(relativePath: string): Promise<string> {
|
||||
return readFile(path.join(desktopRoot, relativePath), "utf-8");
|
||||
}
|
||||
|
||||
describe("desktop Electron main bundling", () => {
|
||||
it("builds dashboard server artifacts and registry manifest from the desktop release build path", async () => {
|
||||
const buildScript = await readDesktopFile("scripts/build.ts");
|
||||
|
||||
expect(buildScript).toContain("buildDashboard()");
|
||||
expect(buildScript).toContain("dashboardRegistryManifestSource");
|
||||
expect(buildScript).toContain("dashboardRegistryManifestDist");
|
||||
expect(buildScript).toContain("await cp(dashboardRegistryManifestSource, dashboardRegistryManifestDist)");
|
||||
});
|
||||
|
||||
it("externalizes production main-process packages so updater CJS deps are not bundled into ESM", async () => {
|
||||
const buildScript = await readDesktopFile("scripts/build.ts");
|
||||
const mainBuildBlock = buildScript.match(/entryPoints: \[join\(packageRoot, "src", "main\.ts"\)\],[\s\S]*?logLevel: "info",/m)?.[0];
|
||||
|
||||
expect(mainBuildBlock).toBeDefined();
|
||||
expect(mainBuildBlock).toContain('format: "esm"');
|
||||
expect(mainBuildBlock).toContain('platform: "node"');
|
||||
expect(mainBuildBlock).toContain('packages: "external"');
|
||||
expect(mainBuildBlock).toContain("external: mainExternals");
|
||||
|
||||
const preloadBuildBlock = buildScript.match(/entryPoints: \[join\(packageRoot, "src", "preload\.ts"\)\],[\s\S]*?logLevel: "info",/m)?.[0];
|
||||
expect(preloadBuildBlock).toBeDefined();
|
||||
expect(preloadBuildBlock).toContain('format: "cjs"');
|
||||
expect(preloadBuildBlock).toContain('packages: "external"');
|
||||
});
|
||||
|
||||
it("keeps development main-process bundling aligned with the production package-external invariant", async () => {
|
||||
const devScript = await readDesktopFile("scripts/dev.ts");
|
||||
const mainBuildBlock = devScript.match(/entryPoints: \[join\(packageRoot, "src", "main\.ts"\)\],[\s\S]*?logLevel: "info",/m)?.[0];
|
||||
|
||||
expect(mainBuildBlock).toBeDefined();
|
||||
expect(mainBuildBlock).toContain('format: "esm"');
|
||||
expect(mainBuildBlock).toContain('packages: "external"');
|
||||
expect(mainBuildBlock).toContain('external: ["electron"]');
|
||||
});
|
||||
|
||||
it("keeps known updater CommonJS dependencies available as packaged runtime files", async () => {
|
||||
const builderConfig = await readDesktopFile("electron-builder.yml");
|
||||
|
||||
for (const runtimeDependency of [
|
||||
"node_modules/electron-updater/**/*",
|
||||
"node_modules/fs-extra/**/*",
|
||||
"node_modules/graceful-fs/**/*",
|
||||
]) {
|
||||
expect(builderConfig).toContain(`- ${runtimeDependency}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -35,8 +35,10 @@ describe("electron-builder desktop config", () => {
|
||||
expect(extractArchValues(nsisArchMatch![1])).toEqual(["arm64", "x64"]);
|
||||
expect(extractArchValues(portableArchMatch![1])).toEqual(["arm64", "x64"]);
|
||||
|
||||
expect(builderConfig).toMatch(/nsis:\s*[\s\S]*?artifactName:\s*"\$\{productName\}-\$\{version\}-\$\{os\}-\$\{arch\}\.\$\{ext\}"/m);
|
||||
expect(builderConfig).toMatch(/nsis:\s*[\s\S]*?oneClick:\s*false/m);
|
||||
expect(builderConfig).toMatch(/nsis:\s*[\s\S]*?allowToChangeInstallationDirectory:\s*true/m);
|
||||
expect(builderConfig).toMatch(/portable:\s*[\s\S]*?artifactName:\s*"\$\{productName\}-\$\{version\}-\$\{os\}-\$\{arch\}-portable\.\$\{ext\}"/m);
|
||||
|
||||
expect(builderConfig).toMatch(/artifactName:\s*"\$\{productName\}-\$\{version\}-\$\{os\}-\$\{arch\}\.\$\{ext\}"/m);
|
||||
expect(builderConfig).toMatch(/appId:\s*com\.gsxdsm\.fusion\.desktop/m);
|
||||
@@ -107,6 +109,14 @@ describe("electron-builder desktop config", () => {
|
||||
expect(linuxArchByTarget.get("tar.gz")).toEqual(["arm64", "x64"]);
|
||||
});
|
||||
|
||||
it("does not exclude Electron runtime pak resources from Windows unpacked output", async () => {
|
||||
const builderConfig = await readDesktopFile("electron-builder.yml");
|
||||
|
||||
for (const requiredPak of ["chrome_100_percent.pak", "chrome_200_percent.pak", "resources.pak"]) {
|
||||
expect(builderConfig).not.toMatch(new RegExp(`!.*${requiredPak.replaceAll(".", "\\.")}`));
|
||||
}
|
||||
});
|
||||
|
||||
it("packages @fusion/core runtime dependencies used during desktop startup", async () => {
|
||||
const builderConfig = await readDesktopFile("electron-builder.yml");
|
||||
const requiredRuntimeDependencyGlobs = [
|
||||
@@ -175,6 +185,11 @@ describe("desktop windows workflow signing guards", () => {
|
||||
expect(workflow).toContain("CSC_KEY_PASSWORD:");
|
||||
expect(workflow).toContain("WINDOWS_CERTIFICATE_BASE64 != ''");
|
||||
expect(workflow).toContain("Get-AuthenticodeSignature");
|
||||
expect(workflow).toContain("Verify Windows runtime resources");
|
||||
expect(workflow).toContain("chrome_100_percent.pak");
|
||||
expect(workflow).toContain("chrome_200_percent.pak");
|
||||
expect(workflow).toContain("resources.pak");
|
||||
expect(workflow).toContain("Fusion-*-win-*-portable.exe");
|
||||
expect(workflow).toContain("intentionally deferred");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -169,6 +169,21 @@ describe("ipc handlers", () => {
|
||||
expect(window.webContents.send).toHaveBeenCalledWith("shell:state", expect.any(Object));
|
||||
});
|
||||
|
||||
it("shell:setDesktopMode propagates local runtime startup errors", async () => {
|
||||
const onDesktopModeChange = vi.fn(async () => {
|
||||
throw new Error("dashboard import failed: ERR_IMPORT_ATTRIBUTE_MISSING");
|
||||
});
|
||||
const { window } = await registerHandlers({ onDesktopModeChange, getRuntimeStatus: () => ({ source: "embedded-local", state: "error", error: "dashboard import failed: ERR_IMPORT_ATTRIBUTE_MISSING" }) });
|
||||
const handler = mocks.ipcHandlers.get("shell:setDesktopMode");
|
||||
|
||||
await expect(handler?.({}, "local")).rejects.toThrow("ERR_IMPORT_ATTRIBUTE_MISSING");
|
||||
|
||||
expect(mocks.writeShellSettings).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ desktopMode: "local", hasCompletedModeSelection: true }),
|
||||
);
|
||||
expect(window.webContents.send).not.toHaveBeenCalledWith("shell:state", expect.any(Object));
|
||||
});
|
||||
|
||||
it("desktop launch mode handlers return mode/context and validate payload", async () => {
|
||||
const getDesktopLaunchContext = vi.fn(() => ({ mode: "remote", profileId: "profile_1", serverBaseUrl: "https://remote.example.com" }));
|
||||
const onDesktopLaunchModeChange = vi.fn(async () => undefined);
|
||||
|
||||
@@ -114,6 +114,26 @@ describe("LocalRuntimeManager", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("surfaces dashboard import failures instead of replacing them with a generic timeout", async () => {
|
||||
const { LocalRuntimeManager } = await import("../local-runtime.ts");
|
||||
const importError = new TypeError("ERR_IMPORT_ATTRIBUTE_MISSING: registry-manifest.json requires an import attribute");
|
||||
const manager = new LocalRuntimeManager({
|
||||
rootDir: "/repo",
|
||||
createStore: async () => store,
|
||||
createDashboardServer: async () => {
|
||||
throw importError;
|
||||
},
|
||||
});
|
||||
|
||||
await expect(manager.startLocal()).rejects.toThrow("ERR_IMPORT_ATTRIBUTE_MISSING");
|
||||
expect(store.close).toHaveBeenCalledTimes(1);
|
||||
expect(manager.getStatus()).toMatchObject({
|
||||
source: "embedded-local",
|
||||
state: "error",
|
||||
error: "ERR_IMPORT_ATTRIBUTE_MISSING: registry-manifest.json requires an import attribute",
|
||||
});
|
||||
});
|
||||
|
||||
it("stopLocal is idempotent and no-op when inactive", async () => {
|
||||
const { LocalRuntimeManager } = await import("../local-runtime.ts");
|
||||
const server = new FakeServer(4545);
|
||||
|
||||
Reference in New Issue
Block a user