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:
@@ -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");
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user