import assert from "node:assert/strict"; import test, { afterEach } from "node:test"; import { parseHTML } from "linkedom"; import { createNotifyHub } from "../components/server-events.tsx"; import { createWebChatGPTBridge, createWebNativeBridge } from "../components/settings/subscription-client.ts"; import { createRequire } from "node:module"; const dom = parseHTML(""); Object.assign(globalThis, { window: dom.window, document: dom.document }); const originalFetch = globalThis.fetch; const action = "/api/subscriptions/browser?id=00000000-0000-4000-8000-000000000001"; afterEach(() => { globalThis.fetch = originalFetch; delete window.lh; }); test("web ChatGPT reserves a new tab before async login, navigates only to an opaque action and receives push/resync", async () => { const calls: string[] = []; const tab = { opener: {}, closed: false, location: { href: "about:blank" }, close() { this.closed = true; } }; window.open = (() => { calls.push("tab"); return tab; }) as unknown as typeof window.open; const safe = { available: true, pending: true, allowBackgroundUse: true, activeId: null, accounts: [], error: null }; globalThis.fetch = async (url, options) => { assert.equal(url, "/api/subscriptions"); assert.equal(new Headers(options?.headers).get("x-learnharness-subscriptions"), "1"); if (!options?.method) return Response.json({ chatgpt: safe, native: [] }); calls.push("login"); assert.equal(tab.opener, null); assert.deepEqual(JSON.parse(String(options.body)), { providerId: "chatgpt", op: "login" }); return Response.json({ value: safe, browserAction: action }); }; const hub = createNotifyHub(), bridge = createWebChatGPTBridge(hub); assert.deepEqual(await bridge.login(), safe); assert.deepEqual(calls, ["tab", "login"]); assert.equal(tab.location.href, action); assert(!tab.closed); let updates = 0; const off = bridge.onChange(() => updates++); hub.dispatch("subscriptions-changed", "{}"); hub.fireOpen(); assert.equal(updates, 2); off(); hub.fireOpen(); assert.equal(updates, 2); assert.deepEqual(await bridge.status(), safe); }); test("blocked popups do not start OAuth; failed login closes the reserved tab", async () => { const bridge = createWebChatGPTBridge(null); window.open = () => null; globalThis.fetch = async () => assert.fail("blocked popup must not start sign-in"); await assert.rejects(bridge.login(), /Allow a sign-in tab/); const tab = { opener: {}, closed: false, close() { this.closed = true; } }; window.open = (() => tab) as unknown as typeof window.open; globalThis.fetch = async () => new Response(null, { status: 400 }); await assert.rejects(bridge.login()); assert(tab.closed); }); test("native web login retains device prompts; only the explicit Open browser action opens a tab", async () => { const requests: Record[] = []; let tabs = 0; const tab = { opener: {}, closed: false, location: { href: "" }, close() {} }; window.open = (() => { tabs++; return tab; }) as unknown as typeof window.open; globalThis.fetch = async (_url, options) => { const command = JSON.parse(String(options?.body)); requests.push(command); return Response.json(command.op === "open" ? { browserAction: action } : {}); }; const bridge = createWebNativeBridge(null); await bridge.login("github-copilot", false); assert.equal(tabs, 0); await bridge.respond("github-copilot", "attempt", "prompt", ""); await bridge.open("github-copilot", "attempt", "action"); assert.equal(tabs, 1); assert.equal(tab.location.href, action); assert.deepEqual(requests, [{ providerId: "github-copilot", op: "login", enabled: false }, { providerId: "github-copilot", op: "respond", attemptId: "attempt", promptId: "prompt", value: "" }, { providerId: "github-copilot", op: "open", attemptId: "attempt", actionId: "action" }]); }); test("Electron dev uses its narrow external-browser bridge, without attempting a renderer popup", async () => { const opened: string[] = []; window.lh = { expandWindow() {}, subscriptionTransport: "web", openSubscriptionBrowser: async (target) => { opened.push(target); } }; window.open = () => assert.fail("Electron denies renderer popups"); globalThis.fetch = async () => Response.json({ value: { pending: true }, browserAction: action }); await createWebChatGPTBridge(null).login(); assert.deepEqual(opened, [action]); }); test("web notices identify the server machine and same-machine ChatGPT sign-in; desktop copy stays unchanged", async () => { const { CHATGPT_NOTICE, chatGPTNotice } = await import("../components/settings/chatgpt-connection.tsx"); const { subscriptionNotice, subscriptionDisconnectCopy } = await import("../components/settings/subscription-connections.tsx"); assert.equal(chatGPTNotice(), CHATGPT_NOTICE); assert(CHATGPT_NOTICE.includes("on this device")); assert(chatGPTNotice(true).includes("on the machine running LearnHarness")); assert(chatGPTNotice(true).endsWith("Complete ChatGPT sign-in in a browser on that same machine.")); for (const label of ["GitHub Copilot", "Grok"]) { assert(subscriptionNotice(label).includes("stored locally")); assert(subscriptionDisconnectCopy(label).includes("from this device")); assert(subscriptionNotice(label, true).includes("on the machine running LearnHarness")); assert(subscriptionDisconnectCopy(label, true).includes("from the machine running LearnHarness")); } }); const require = createRequire(import.meta.url); const { openWebSubscriptionBrowser } = require("../electron/web-subscription-browser.cjs"); test("desktop-dev opener accepts only local action IDs, does not follow redirects or use browser cookies", async () => { const opened: string[] = []; const options = { openExternal: async (url: string) => { opened.push(url); }, fetch: async (url: URL, init: RequestInit) => { assert.equal(url.origin, "http://localhost:3000"); assert.equal(url.pathname, "/api/subscriptions/browser"); assert.equal(init.method, "POST"); assert.equal(init.redirect, "manual"); assert.equal(init.credentials, "omit"); assert.equal(new Headers(init.headers).get("origin"), "http://localhost:3000"); return new Response(null, { status: 303, headers: { Location: "https://auth.openai.com/authorize?state=fixture" } }); }, }; await openWebSubscriptionBrowser(action, "http://localhost:3000", options); assert.equal(opened.length, 1); for (const target of ["https://unrelated.invalid", "//unrelated.invalid", "/api/keys", `${action}&target=evil`]) await assert.rejects(openWebSubscriptionBrowser(target, "http://localhost:3000", options), /Invalid sign-in action/); for (const target of ["file:///private/fixture", "http://unrelated.invalid", "https://user:pass@unrelated.invalid"]) await assert.rejects(openWebSubscriptionBrowser(action, "http://localhost:3000", { ...options, fetch: async () => new Response(null, { status: 303, headers: { Location: target } }) })); assert.equal(opened.length, 1); });