feat(FN-3399): route desktop remote mode into shell onboarding flow

Removes the standalone `DesktopShellBootstrap` component from the desktop package, routing desktop remote mode into the shell onboarding flow instead. Updates the corresponding test file to reflect the component removal and adjusts the README.

Fusion-Task-Id: FN-3399
This commit is contained in:
Fusion
2026-05-04 22:53:57 -07:00
committed by gsxdsm
parent 8565cdcff7
commit f7995bd4e7
22 changed files with 521 additions and 40 deletions

View File

@@ -17,7 +17,12 @@ const mocks = vi.hoisted(() => {
const showExportSettingsDialog = vi.fn();
const showImportSettingsDialog = vi.fn();
const setupAutoUpdater = vi.fn();
const readShellSettings = vi.fn(async () => ({ desktopMode: "remote", activeProfileId: null, profiles: [] }));
const readShellSettings = vi.fn(async () => ({
desktopMode: "remote",
hasCompletedModeSelection: true,
activeProfileId: null,
profiles: [],
}));
const writeShellSettings = vi.fn(async () => undefined);
return {
@@ -51,6 +56,10 @@ vi.mock("../native.js", () => ({
vi.mock("../shell-settings.js", () => ({
readShellSettings: mocks.readShellSettings,
writeShellSettings: mocks.writeShellSettings,
getDesktopShellModeState: (settings: { hasCompletedModeSelection?: boolean; desktopMode?: "local" | "remote" | null }) => ({
isFirstRun: !settings.hasCompletedModeSelection || !settings.desktopMode,
desktopMode: settings.desktopMode ?? null,
}),
}));
function createWindowMock() {
@@ -92,6 +101,7 @@ describe("ipc handlers", () => {
const channels = new Set(mocks.ipcMain.handle.mock.calls.map(([channel]) => channel));
expect(channels.has("shell:getState")).toBe(true);
expect(channels.has("shell:saveProfile")).toBe(true);
expect(channels.has("shell:getDesktopModeState")).toBe(true);
expect(channels.has("shell:setDesktopMode")).toBe(true);
expect(channels.has("platform:get")).toBe(true);
});
@@ -102,6 +112,7 @@ describe("ipc handlers", () => {
const result = await handler?.({});
expect(result).toMatchObject({ host: "desktop-shell", desktopMode: "remote" });
expect(result).toMatchObject({ desktopModeState: { isFirstRun: false, desktopMode: "remote" } });
});
it("shell:setDesktopMode persists mode and emits state", async () => {
@@ -110,7 +121,9 @@ describe("ipc handlers", () => {
const handler = mocks.ipcHandlers.get("shell:setDesktopMode");
await handler?.({}, "local");
expect(mocks.writeShellSettings).toHaveBeenCalled();
expect(mocks.writeShellSettings).toHaveBeenCalledWith(
expect.objectContaining({ desktopMode: "local", hasCompletedModeSelection: true }),
);
expect(onDesktopModeChange).toHaveBeenCalledWith("local");
expect(window.webContents.send).toHaveBeenCalledWith("shell:state", expect.any(Object));
});

View File

@@ -49,7 +49,15 @@ vi.mock("../tray.js", () => ({ setupTray: vi.fn() }));
vi.mock("../ipc.js", () => ({ registerIpcHandlers: vi.fn() }));
vi.mock("../native.js", () => ({ DEFAULT_WINDOW_STATE: { width: 1000, height: 800 }, loadWindowState: vi.fn(async () => null), saveWindowState: vi.fn(), setupAutoUpdater: vi.fn() }));
vi.mock("../deep-link.js", () => ({ registerDeepLinkProtocol: vi.fn(), setupDeepLinkHandler: vi.fn() }));
vi.mock("../shell-settings.js", () => ({ readShellSettings: vi.fn(async () => ({ desktopMode: "local", activeProfileId: null, profiles: [] })) }));
vi.mock("../shell-settings.js", () => ({
readShellSettings: vi.fn(async () => ({
desktopMode: "local",
hasCompletedModeSelection: true,
activeProfileId: null,
profiles: [],
})),
getDesktopShellModeState: () => ({ isFirstRun: false, desktopMode: "local" }),
}));
vi.mock("../local-server.js", () => ({ DesktopLocalServerManager: vi.fn(() => mocks.localServerManager) }));
describe("main local mode", () => {

View File

@@ -100,6 +100,55 @@ vi.mock("electron", () => ({
shell: mocks.shell,
}));
const mainDeps = vi.hoisted(() => {
const start = vi.fn(async () => undefined);
const stop = vi.fn(async () => undefined);
const getState = vi.fn(() => ({ status: "idle", error: null }));
const getPort = vi.fn(() => 0);
return {
registerIpcHandlers: vi.fn(),
buildAppMenu: vi.fn(),
setupTray: vi.fn(),
registerDeepLinkProtocol: vi.fn(),
setupDeepLinkHandler: vi.fn(),
setupAutoUpdater: vi.fn(),
loadWindowState: vi.fn(async () => null),
saveWindowState: vi.fn(),
readShellSettings: vi.fn(async () => ({
desktopMode: null,
hasCompletedModeSelection: false,
activeProfileId: null,
profiles: [],
})),
DesktopLocalServerManager: vi.fn(() => ({ start, stop, getState, getPort })),
start,
};
});
vi.mock("../ipc.js", () => ({ registerIpcHandlers: mainDeps.registerIpcHandlers }));
vi.mock("../menu.js", () => ({ buildAppMenu: mainDeps.buildAppMenu }));
vi.mock("../tray.js", () => ({ setupTray: mainDeps.setupTray }));
vi.mock("../deep-link.js", () => ({
registerDeepLinkProtocol: mainDeps.registerDeepLinkProtocol,
setupDeepLinkHandler: mainDeps.setupDeepLinkHandler,
}));
vi.mock("../native.js", () => ({
DEFAULT_WINDOW_STATE: { width: 1280, height: 900, isMaximized: false },
loadWindowState: mainDeps.loadWindowState,
saveWindowState: mainDeps.saveWindowState,
setupAutoUpdater: mainDeps.setupAutoUpdater,
}));
vi.mock("../shell-settings.js", () => ({
readShellSettings: mainDeps.readShellSettings,
getDesktopShellModeState: (settings: { hasCompletedModeSelection: boolean; desktopMode: "local" | "remote" | null }) => ({
isFirstRun: !settings.hasCompletedModeSelection || settings.desktopMode === null,
desktopMode: settings.desktopMode,
}),
}));
vi.mock("../local-server.js", () => ({
DesktopLocalServerManager: mainDeps.DesktopLocalServerManager,
}));
async function importMainModule() {
return import("../main.ts");
}
@@ -111,6 +160,12 @@ describe("main process", () => {
beforeEach(() => {
vi.clearAllMocks();
vi.resetModules();
mainDeps.readShellSettings.mockResolvedValue({
desktopMode: null,
hasCompletedModeSelection: false,
activeProfileId: null,
profiles: [],
});
if (originalDashboardUrl === undefined) {
delete process.env.FUSION_DASHBOARD_URL;
} else {
@@ -202,6 +257,28 @@ describe("main process", () => {
expect(typeof mainModule.initializeApp).toBe("function");
});
it("initializeApp starts local server only when persisted mode is local and not first run", async () => {
mainDeps.readShellSettings.mockResolvedValueOnce({
desktopMode: "local",
hasCompletedModeSelection: true,
activeProfileId: null,
profiles: [],
});
const { initializeApp } = await importMainModule();
await initializeApp();
expect(mainDeps.start).toHaveBeenCalledTimes(1);
});
it("initializeApp does not start local server on first run without mode selection", async () => {
const { initializeApp } = await importMainModule();
await initializeApp();
expect(mainDeps.start).not.toHaveBeenCalled();
});
it("createMainWindow registers close and closed handlers", async () => {
const { createMainWindow } = await importMainModule();

View File

@@ -36,12 +36,18 @@ describe("shell-settings", () => {
});
it("returns defaults when file missing", async () => {
const { readShellSettings } = await import("../shell-settings.ts");
const { readShellSettings, getDesktopShellModeState } = await import("../shell-settings.ts");
await expect(readShellSettings()).resolves.toEqual({
desktopMode: "remote",
desktopMode: null,
hasCompletedModeSelection: false,
activeProfileId: null,
profiles: [],
});
const settings = await readShellSettings();
expect(getDesktopShellModeState(settings)).toEqual({
isFirstRun: true,
desktopMode: null,
});
});
it("writes and reads persisted settings", async () => {
@@ -49,6 +55,7 @@ describe("shell-settings", () => {
await writeShellSettings({
desktopMode: "local",
hasCompletedModeSelection: true,
activeProfileId: "p1",
profiles: [
{
@@ -65,8 +72,33 @@ describe("shell-settings", () => {
await expect(readShellSettings()).resolves.toMatchObject({
desktopMode: "local",
hasCompletedModeSelection: true,
activeProfileId: "p1",
profiles: [{ id: "p1" }],
});
});
it("infers completed selection from legacy desktopMode payload", async () => {
mockState.content.set("/tmp/fusion/shell-connections.json", JSON.stringify({ desktopMode: "remote" }));
const { readShellSettings, getDesktopShellModeState } = await import("../shell-settings.ts");
const settings = await readShellSettings();
expect(settings.hasCompletedModeSelection).toBe(true);
expect(getDesktopShellModeState(settings)).toEqual({
isFirstRun: false,
desktopMode: "remote",
});
});
it("treats invalid persisted mode as first-run", async () => {
mockState.content.set(
"/tmp/fusion/shell-connections.json",
JSON.stringify({ desktopMode: "invalid", hasCompletedModeSelection: true }),
);
const { readShellSettings, getDesktopShellModeState } = await import("../shell-settings.ts");
const settings = await readShellSettings();
expect(getDesktopShellModeState(settings)).toEqual({
isFirstRun: true,
desktopMode: null,
});
});
});