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
114 lines
4.5 KiB
TypeScript
114 lines
4.5 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
import { buildMobileShellHandoff } from "../plugins/shell-handoff.js";
|
|
|
|
const state = {
|
|
activeProfileId: null as string | null,
|
|
profiles: [] as Array<{ id: string; name: string; serverUrl: string; authToken?: string | null; createdAt: string; updatedAt: string; lastUsedAt?: string | null }>,
|
|
};
|
|
|
|
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; authToken?: string | null }) => {
|
|
const saved = {
|
|
id: "p1",
|
|
name: profile.name,
|
|
serverUrl: profile.serverUrl,
|
|
authToken: profile.authToken ?? null,
|
|
createdAt: "2026-01-01T00:00:00.000Z",
|
|
updatedAt: "2026-01-01T00:00:00.000Z",
|
|
lastUsedAt: null,
|
|
};
|
|
state.profiles = [saved];
|
|
return saved;
|
|
}),
|
|
deleteShellProfile: vi.fn(async () => {
|
|
state.profiles = [];
|
|
state.activeProfileId = null;
|
|
}),
|
|
setActiveShellProfile: vi.fn(async (profileId: string | null) => {
|
|
state.activeProfileId = profileId;
|
|
return state;
|
|
}),
|
|
}));
|
|
|
|
describe("MobileNativeShellBridge", () => {
|
|
const scanner = { scanConnection: vi.fn(async () => ({ serverUrl: "https://fusion.example.com", authToken: null })) };
|
|
|
|
beforeEach(() => {
|
|
state.activeProfileId = null;
|
|
state.profiles = [];
|
|
scanner.scanConnection.mockClear();
|
|
vi.resetModules();
|
|
});
|
|
|
|
it("emits state updates to subscribers", async () => {
|
|
const { MobileNativeShellBridge } = await import("../plugins/native-shell.js");
|
|
const bridge = new MobileNativeShellBridge(scanner as never);
|
|
const listener = vi.fn();
|
|
|
|
const unsubscribe = bridge.subscribe(listener);
|
|
await bridge.saveProfile({ name: "Prod", serverUrl: "https://fusion.example.com" });
|
|
|
|
expect(listener).toHaveBeenCalled();
|
|
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("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);
|
|
|
|
await expect(bridge.setDesktopMode("local")).rejects.toThrow("Desktop mode is not supported");
|
|
});
|
|
});
|