feat(FN-3409): add shell regression matrix for mobile, desktop, and dashboa
The merge completes a multi-step shell regression hardening effort spanning desktop, mobile, and dashboard entry points, adding 12 test files or test expansions to lock down the shell preload and native contracts. A companion fix exempts internal coordination tools (`list_agents`, `delegate_task`, e Fusion-Task-Id: FN-3409
This commit is contained in:
@@ -42,6 +42,45 @@ describe("connection-profiles", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("updates existing saved profile by id", async () => {
|
||||
const { saveShellProfile, listShellProfiles } = await import("../plugins/connection-profiles.js");
|
||||
|
||||
const profile = await saveShellProfile({ name: "Prod", serverUrl: "https://fusion.example.com", authToken: "old" });
|
||||
const updated = await saveShellProfile({
|
||||
id: profile.id,
|
||||
name: "Production",
|
||||
serverUrl: "https://fusion.example.com/root/",
|
||||
authToken: "new",
|
||||
});
|
||||
|
||||
expect(updated.id).toBe(profile.id);
|
||||
expect(updated.name).toBe("Production");
|
||||
expect(updated.serverUrl).toBe("https://fusion.example.com/root");
|
||||
expect(updated.authToken).toBe("new");
|
||||
|
||||
const profiles = await listShellProfiles();
|
||||
expect(profiles).toHaveLength(1);
|
||||
expect(profiles[0]?.id).toBe(profile.id);
|
||||
});
|
||||
|
||||
it("switches active profile and restores state across module re-init", async () => {
|
||||
const { saveShellProfile, setActiveShellProfile, loadShellProfiles } = await import("../plugins/connection-profiles.js");
|
||||
|
||||
const first = await saveShellProfile({ name: "Prod", serverUrl: "https://fusion.example.com" });
|
||||
const second = await saveShellProfile({ name: "Staging", serverUrl: "https://staging.example.com" });
|
||||
await setActiveShellProfile(first.id);
|
||||
const switched = await setActiveShellProfile(second.id);
|
||||
|
||||
expect(switched.activeProfileId).toBe(second.id);
|
||||
expect(switched.profiles.find((profile) => profile.id === second.id)?.lastUsedAt).toBeTruthy();
|
||||
|
||||
vi.resetModules();
|
||||
const reloadedModule = await import("../plugins/connection-profiles.js");
|
||||
const reloaded = await reloadedModule.loadShellProfiles();
|
||||
expect(reloaded.activeProfileId).toBe(second.id);
|
||||
expect(reloaded.profiles).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("clears active profile when deleted", async () => {
|
||||
const { saveShellProfile, setActiveShellProfile, loadShellProfiles, deleteShellProfile } = await import("../plugins/connection-profiles.js");
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { buildMobileShellHandoff } from "../plugins/shell-handoff.js";
|
||||
|
||||
const state = {
|
||||
activeProfileId: null as string | null,
|
||||
@@ -8,12 +9,12 @@ const state = {
|
||||
vi.mock("../plugins/connection-profiles.js", () => ({
|
||||
loadShellProfiles: vi.fn(async () => state),
|
||||
listShellProfiles: vi.fn(async () => state.profiles),
|
||||
saveShellProfile: vi.fn(async (profile: { name: string; serverUrl: string }) => {
|
||||
saveShellProfile: vi.fn(async (profile: { name: string; serverUrl: string; authToken?: string | null }) => {
|
||||
const saved = {
|
||||
id: "p1",
|
||||
name: profile.name,
|
||||
serverUrl: profile.serverUrl,
|
||||
authToken: null,
|
||||
authToken: profile.authToken ?? null,
|
||||
createdAt: "2026-01-01T00:00:00.000Z",
|
||||
updatedAt: "2026-01-01T00:00:00.000Z",
|
||||
lastUsedAt: null,
|
||||
@@ -68,6 +69,41 @@ describe("MobileNativeShellBridge", () => {
|
||||
(globalThis as { window?: Window }).window = originalWindow;
|
||||
});
|
||||
|
||||
it("returns state and listProfiles from persisted storage", async () => {
|
||||
const { MobileNativeShellBridge } = await import("../plugins/native-shell.js");
|
||||
const bridge = new MobileNativeShellBridge(scanner as never);
|
||||
|
||||
const profile = await bridge.saveProfile({ name: "Prod", serverUrl: "https://fusion.example.com" });
|
||||
await bridge.setActiveProfile(profile.id);
|
||||
|
||||
const stateSnapshot = await bridge.getState();
|
||||
const profiles = await bridge.listProfiles();
|
||||
|
||||
expect(stateSnapshot.host).toBe("mobile-shell");
|
||||
expect(stateSnapshot.activeProfileId).toBe(profile.id);
|
||||
expect(profiles).toHaveLength(1);
|
||||
expect(profiles[0]?.id).toBe(profile.id);
|
||||
});
|
||||
|
||||
it("supports QR onboarding handoff with optional auth token", async () => {
|
||||
scanner.scanConnection.mockResolvedValueOnce({ serverUrl: "https://fusion.example.com", authToken: "token-123" });
|
||||
const { MobileNativeShellBridge } = await import("../plugins/native-shell.js");
|
||||
const bridge = new MobileNativeShellBridge(scanner as never);
|
||||
|
||||
const scan = await bridge.startQrScan();
|
||||
const saved = await bridge.saveProfile({ name: "QR Remote", serverUrl: scan.serverUrl, authToken: scan.authToken ?? null });
|
||||
const state = await bridge.setActiveProfile(saved.id);
|
||||
const handoff = buildMobileShellHandoff(state);
|
||||
|
||||
expect(scan).toEqual({ serverUrl: "https://fusion.example.com", authToken: "token-123" });
|
||||
expect(handoff.kind).toBe("remote-launch");
|
||||
if (handoff.kind === "remote-launch") {
|
||||
const url = new URL(handoff.url);
|
||||
expect(url.searchParams.get("profileId")).toBe(saved.id);
|
||||
expect(url.searchParams.get("token")).toBe("token-123");
|
||||
}
|
||||
});
|
||||
|
||||
it("rejects desktop mode switch", async () => {
|
||||
const { MobileNativeShellBridge } = await import("../plugins/native-shell.js");
|
||||
const bridge = new MobileNativeShellBridge(scanner as never);
|
||||
|
||||
@@ -17,6 +17,21 @@ describe("qr-scanner", () => {
|
||||
expect(parsed).toEqual({ serverUrl: "https://fusion.example.com", authToken: "abc" });
|
||||
});
|
||||
|
||||
it("throws for empty payload", () => {
|
||||
expect(() => parseQrConnectionPayload(" ")).toThrow("QR scan returned empty payload");
|
||||
});
|
||||
|
||||
it("throws for invalid payload", () => {
|
||||
expect(() => parseQrConnectionPayload("not-a-fusion-connection")).toThrow(
|
||||
"QR payload is not a valid Fusion connection payload",
|
||||
);
|
||||
});
|
||||
|
||||
it("throws when scanner is unavailable", async () => {
|
||||
const scanner = new QrScanner();
|
||||
await expect(scanner.scanConnection()).rejects.toThrow("QR scanner is not available on this platform");
|
||||
});
|
||||
|
||||
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 });
|
||||
|
||||
Reference in New Issue
Block a user