228 lines
7.2 KiB
TypeScript
228 lines
7.2 KiB
TypeScript
import { describe, expect, it } from "bun:test";
|
|
|
|
import {
|
|
createUnavailableState,
|
|
detectDesktopRuntimeMarker,
|
|
detectDesktopShell,
|
|
resolveDesktopAiRuntime,
|
|
} from "./desktop-ai";
|
|
import {
|
|
getDesktopAiModelListEmptyCopy,
|
|
getDesktopAiModelSelectLabel,
|
|
getDesktopAiProfileBadgeLabel,
|
|
getDesktopAiSettingsBridgeNotice,
|
|
createCopilotTaskCacheKey,
|
|
getCachedCopilotTaskId,
|
|
getCopilotTaskSurfaceState,
|
|
requireDesktopActionCopy,
|
|
shouldShowCopilotRegenerate,
|
|
} from "./desktop-ai-panels";
|
|
|
|
describe("desktop ai runtime detection", () => {
|
|
it("recognizes the explicit desktop preload marker before the bridge is available", () => {
|
|
const runtime = resolveDesktopAiRuntime({
|
|
islandflowDesktopRuntime: {
|
|
app: "islandflow",
|
|
shell: "electron",
|
|
},
|
|
navigator: {
|
|
userAgent: "Mozilla/5.0 Safari/537.36",
|
|
},
|
|
});
|
|
|
|
expect(runtime.shellAvailable).toBe(true);
|
|
expect(runtime.bridgeAvailable).toBe(false);
|
|
expect(runtime.bridge).toBeNull();
|
|
});
|
|
|
|
it("recognizes Electron user agents before the bridge is available", () => {
|
|
const runtime = resolveDesktopAiRuntime({
|
|
navigator: {
|
|
userAgent: "Mozilla/5.0 Islandflow Electron/39.0.0 Safari/537.36",
|
|
},
|
|
});
|
|
|
|
expect(runtime.shellAvailable).toBe(true);
|
|
expect(runtime.bridgeAvailable).toBe(false);
|
|
expect(runtime.bridge).toBeNull();
|
|
});
|
|
|
|
it("treats a bridged window as desktop even without an Electron user agent", () => {
|
|
const runtime = resolveDesktopAiRuntime({
|
|
islandflowDesktop: {
|
|
ai: {
|
|
getState: async () =>
|
|
createUnavailableState({
|
|
shellAvailable: true,
|
|
bridgeAvailable: true,
|
|
}),
|
|
loginWithBrowser: async () => {},
|
|
loginWithDeviceCode: async () => {},
|
|
cancelLogin: async () => {},
|
|
logout: async () => {},
|
|
updatePreferences: async () => {},
|
|
runTask: async () => ({ taskId: "task-1" }),
|
|
cancelTask: async () => {},
|
|
subscribe: () => () => {},
|
|
},
|
|
},
|
|
navigator: { userAgent: "Mozilla/5.0" },
|
|
});
|
|
|
|
expect(runtime.shellAvailable).toBe(true);
|
|
expect(runtime.bridgeAvailable).toBe(true);
|
|
expect(runtime.bridge).not.toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("desktop ai unavailable state", () => {
|
|
it("keeps desktopAvailable false for real browser fallbacks", () => {
|
|
const state = createUnavailableState();
|
|
|
|
expect(state.desktopAvailable).toBe(false);
|
|
expect(state.transportStatus).toBe("stopped");
|
|
expect(state.transportError).toContain("Electron app");
|
|
});
|
|
|
|
it("reports desktop-shell bridge failures without pretending the app is a browser", () => {
|
|
const state = createUnavailableState({ shellAvailable: true });
|
|
|
|
expect(state.desktopAvailable).toBe(true);
|
|
expect(state.transportStatus).toBe("error");
|
|
expect(state.transportError).toContain("native AI bridge");
|
|
expect(state.account.login.message).toContain("Reload the window");
|
|
});
|
|
});
|
|
|
|
describe("desktop action copy", () => {
|
|
it("asks for the desktop app only when the shell is genuinely absent", () => {
|
|
expect(requireDesktopActionCopy(false, false, false)).toContain(
|
|
"Open Islandflow Desktop",
|
|
);
|
|
});
|
|
|
|
it("surfaces bridge recovery guidance inside the desktop shell", () => {
|
|
expect(requireDesktopActionCopy(true, false, false)).toContain(
|
|
"missing the native AI bridge",
|
|
);
|
|
});
|
|
|
|
it("asks for login once the bridge is present", () => {
|
|
expect(requireDesktopActionCopy(true, true, false)).toContain(
|
|
"Connect a ChatGPT or Codex account",
|
|
);
|
|
});
|
|
|
|
it("clears helper copy when the action is ready", () => {
|
|
expect(requireDesktopActionCopy(true, true, true)).toBe("");
|
|
});
|
|
});
|
|
|
|
describe("desktop shell detection", () => {
|
|
it("matches the explicit preload runtime marker", () => {
|
|
expect(
|
|
detectDesktopRuntimeMarker({ app: "islandflow", shell: "electron" }),
|
|
).toBe(true);
|
|
expect(
|
|
detectDesktopRuntimeMarker({ app: "islandflow", shell: "browser" }),
|
|
).toBe(false);
|
|
});
|
|
|
|
it("matches Electron signatures", () => {
|
|
expect(detectDesktopShell("Mozilla/5.0 Electron/39.0.0")).toBe(true);
|
|
expect(detectDesktopShell("Mozilla/5.0 IslandflowDesktop/0.1.0")).toBe(
|
|
true,
|
|
);
|
|
expect(
|
|
detectDesktopShell("Mozilla/5.0 Chrome/136.0.0.0 Safari/537.36"),
|
|
).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("desktop ai settings copy", () => {
|
|
it("explains when the desktop app itself is required", () => {
|
|
expect(getDesktopAiSettingsBridgeNotice(false, false)).toEqual({
|
|
title: "Desktop app required",
|
|
body: "Open Islandflow Desktop to connect ChatGPT, load managed models, and use native Copilot controls.",
|
|
});
|
|
});
|
|
|
|
it("explains when the native bridge is missing from the desktop window", () => {
|
|
expect(getDesktopAiSettingsBridgeNotice(true, false)?.title).toBe(
|
|
"Bridge unavailable in this window",
|
|
);
|
|
});
|
|
|
|
it("keeps the model selector explicit before login", () => {
|
|
expect(getDesktopAiModelSelectLabel(true, true, false, 0)).toBe(
|
|
"Connect ChatGPT to load models",
|
|
);
|
|
expect(getDesktopAiModelListEmptyCopy(true, true, false)).toContain(
|
|
"Connect a ChatGPT or Codex account",
|
|
);
|
|
});
|
|
|
|
it("keeps the model selector explicit while the bridge is disconnected", () => {
|
|
expect(getDesktopAiModelSelectLabel(true, false, false, 0)).toBe(
|
|
"Bridge unavailable",
|
|
);
|
|
expect(getDesktopAiModelListEmptyCopy(true, false, false)).toContain(
|
|
"native AI bridge reconnects",
|
|
);
|
|
});
|
|
|
|
it("shows the real status label when a selected profile is unusable", () => {
|
|
expect(
|
|
getDesktopAiProfileBadgeLabel(true, "Bridge unavailable", false),
|
|
).toBe("Bridge unavailable");
|
|
expect(
|
|
getDesktopAiProfileBadgeLabel(true, "Bridge unavailable", true),
|
|
).toBe("Selected");
|
|
});
|
|
});
|
|
|
|
describe("desktop ai panel task helpers", () => {
|
|
it("looks up cached task ids by action kind and context key", () => {
|
|
const key = createCopilotTaskCacheKey("smart-money-explain", "AAPL:1:event-1");
|
|
|
|
expect(key).toBe("smart-money-explain:AAPL:1:event-1");
|
|
expect(
|
|
getCachedCopilotTaskId(
|
|
{ [key]: "task-1" },
|
|
"smart-money-explain",
|
|
"AAPL:1:event-1",
|
|
),
|
|
).toBe("task-1");
|
|
});
|
|
|
|
it("only shows regenerate once a completed result has output", () => {
|
|
expect(
|
|
shouldShowCopilotRegenerate({
|
|
status: "completed",
|
|
text: "## Thesis",
|
|
compiledScreen: null,
|
|
}),
|
|
).toBe(true);
|
|
expect(
|
|
shouldShowCopilotRegenerate({
|
|
status: "running",
|
|
text: "partial",
|
|
compiledScreen: null,
|
|
}),
|
|
).toBe(false);
|
|
expect(shouldShowCopilotRegenerate(null)).toBe(false);
|
|
});
|
|
|
|
it("maps queued, running, failed, and cancelled tasks to result surface states", () => {
|
|
expect(getCopilotTaskSurfaceState(null)).toBe("empty");
|
|
expect(getCopilotTaskSurfaceState({ status: "queued", text: "", compiledScreen: null, error: null })).toBe(
|
|
"running",
|
|
);
|
|
expect(getCopilotTaskSurfaceState({ status: "running", text: "", compiledScreen: null, error: null })).toBe(
|
|
"running",
|
|
);
|
|
expect(getCopilotTaskSurfaceState({ status: "cancelled", text: "", compiledScreen: null, error: null })).toBe(
|
|
"cancelled",
|
|
);
|
|
});
|
|
});
|