feat(FN-3400): add native shell connection handoff, plugin management CLI/l

This merge adds a complete plugin management system (FN-3565) with CLI commands, a loader, runner, and dashboard routes, along with project-scoped auth storage (FN-3544), native shell connection support for mobile (FN-3400) spanning onboarding, connection manager, and remote desktop handoff, and ref

Fusion-Task-Id: FN-3400
This commit is contained in:
Fusion
2026-05-06 05:24:01 -07:00
committed by gsxdsm
parent fb0e15562d
commit 419c6f3b31
20 changed files with 266 additions and 25 deletions

View File

@@ -31,6 +31,17 @@ describe("connection-profiles", () => {
});
});
it("rejects invalid server URLs", async () => {
const { saveShellProfile } = await import("../plugins/connection-profiles.js");
await expect(saveShellProfile({ name: "Prod", serverUrl: "not-a-url" })).rejects.toThrow(
"Server URL must be a valid absolute URL",
);
await expect(saveShellProfile({ name: "Prod", serverUrl: "ftp://fusion.example.com" })).rejects.toThrow(
"Server URL must use http or https",
);
});
it("clears active profile when deleted", async () => {
const { saveShellProfile, setActiveShellProfile, loadShellProfiles, deleteShellProfile } = await import("../plugins/connection-profiles.js");

View File

@@ -53,6 +53,21 @@ describe("MobileNativeShellBridge", () => {
unsubscribe();
});
it("dispatches shell open manager event", async () => {
const { MobileNativeShellBridge } = await import("../plugins/native-shell.js");
const bridge = new MobileNativeShellBridge(scanner as never);
const listener = vi.fn();
const originalWindow = (globalThis as { window?: Window }).window;
const mockWindow = new EventTarget() as Window;
(globalThis as { window?: Window }).window = mockWindow;
mockWindow.addEventListener("shell:open-connection-manager", listener as EventListener);
await bridge.openConnectionManager();
expect(listener).toHaveBeenCalledTimes(1);
(globalThis as { window?: Window }).window = originalWindow;
});
it("rejects desktop mode switch", async () => {
const { MobileNativeShellBridge } = await import("../plugins/native-shell.js");
const bridge = new MobileNativeShellBridge(scanner as never);

View File

@@ -12,6 +12,11 @@ describe("qr-scanner", () => {
expect(parsed).toEqual({ serverUrl: "https://fusion.example.com", authToken: "abc" });
});
it("parses remote-login rt token payload", () => {
const parsed = parseQrConnectionPayload("https://fusion.example.com/remote-login?rt=abc");
expect(parsed).toEqual({ serverUrl: "https://fusion.example.com", authToken: "abc" });
});
it("uses adapter scanning", async () => {
const scanner = new QrScanner({ scan: vi.fn(async () => "https://fusion.example.com") });
await expect(scanner.scanConnection()).resolves.toEqual({ serverUrl: "https://fusion.example.com", authToken: null });

View File

@@ -17,7 +17,17 @@ function createId(): string {
}
function normalizeUrl(serverUrl: string): string {
return serverUrl.trim().replace(/\/$/, "");
const normalized = serverUrl.trim().replace(/\/$/, "");
let parsed: URL;
try {
parsed = new URL(normalized);
} catch {
throw new Error("Server URL must be a valid absolute URL");
}
if (!parsed.protocol || !/^https?:$/.test(parsed.protocol)) {
throw new Error("Server URL must use http or https");
}
return normalized;
}
function toPersisted(input: unknown): PersistedShellState {

View File

@@ -69,7 +69,9 @@ export class MobileNativeShellBridge implements FusionShellApi {
}
async openConnectionManager(): Promise<void> {
// Handled by dashboard shell context state.
if (typeof window !== "undefined") {
window.dispatchEvent(new CustomEvent("shell:open-connection-manager"));
}
}
subscribe(listener: Listener): () => void {

View File

@@ -27,7 +27,7 @@ function parsePayload(raw: string): QrScanResult {
try {
const url = new URL(trimmed);
const authToken = url.searchParams.get("authToken");
const authToken = url.searchParams.get("authToken") ?? url.searchParams.get("rt");
return {
serverUrl: `${url.protocol}//${url.host}`,
authToken,