From 0f21769f346e64c2c6b2a03ff8d09b3deaab9562 Mon Sep 17 00:00:00 2001 From: Robin Bially <7304732+RobinBially@users.noreply.github.com> Date: Mon, 7 Sep 2026 09:57:05 +0200 Subject: [PATCH] feat(gui): add opt-in Codex Desktop sign-in toggle --- .../content/docs/guides/codex-integration.md | 9 ++ docs/pr-assets/codex-desktop-opt-in.jpg | Bin 0 -> 29051 bytes gui/src/i18n/de.ts | 2 + gui/src/i18n/en.ts | 2 + gui/src/i18n/fr.ts | 2 + gui/src/i18n/ja.ts | 2 + gui/src/i18n/ko.ts | 2 + gui/src/i18n/ru.ts | 2 + gui/src/i18n/tr.ts | 2 + gui/src/i18n/zh-TW.ts | 2 + gui/src/i18n/zh.ts | 2 + gui/src/pages/dashboard-overview-sections.tsx | 28 ++++++- gui/src/pages/dashboard-shared.ts | 2 + gui/src/pages/use-dashboard-data.ts | 23 +++-- gui/tests/vision-sidecar-dashboard.test.tsx | 79 +++++++++++++++++- tests/codex-integration/codex-inject.test.ts | 4 +- 16 files changed, 146 insertions(+), 17 deletions(-) create mode 100644 docs/pr-assets/codex-desktop-opt-in.jpg diff --git a/docs-site/src/content/docs/guides/codex-integration.md b/docs-site/src/content/docs/guides/codex-integration.md index 7d66e72c3f..ff2df04fdd 100644 --- a/docs-site/src/content/docs/guides/codex-integration.md +++ b/docs-site/src/content/docs/guides/codex-integration.md @@ -215,6 +215,15 @@ HTTP/SSE. ### Authless Codex Desktop (opt-in) +In **Dashboard → Overview**, **Open Codex without signing in** controls this existing +opt-in preference. The switch defaults to **off** when the setting is absent or false; +an existing explicit `codexDesktopAuthless: true` stays enabled. The dashboard saves +the preference and runs a full sync. Restart Codex Desktop after changing it. +If synchronization fails, the saved preference remains and the dashboard shows the error; +retry **Sync** before restarting. Account-gated Desktop features may be unavailable +when enabled. Upstream credentials, local eligibility, remote admission authentication +and user-owned gateway settings retain their existing requirements. + Codex Desktop shows its ChatGPT login screen whenever the active provider requires OpenAI auth. If your OpenCodex setup never uses ChatGPT credentials (routed providers only, or a blocked `chatgpt.com`), you can opt out of that gate: diff --git a/docs/pr-assets/codex-desktop-opt-in.jpg b/docs/pr-assets/codex-desktop-opt-in.jpg new file mode 100644 index 0000000000000000000000000000000000000000..d06203e7113cc54e87b1272e272d8f943c5b4805 GIT binary patch literal 29051 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 literal 0 HcmV?d00001 diff --git a/gui/src/i18n/de.ts b/gui/src/i18n/de.ts index 5faab4b356..495104bd42 100644 --- a/gui/src/i18n/de.ts +++ b/gui/src/i18n/de.ts @@ -299,6 +299,8 @@ export const de: Record = { "models.staleBanner": "Codex zeigt eine ältere Modellliste als dieser Katalog. Starte Codex neu, um sie neu zu laden.", "dash.codexAutoStart": "opencodex mit Codex starten", "dash.codexAutoStartHint": "Erlaubt einem installierten Launcher-Shim, ocx ensure auszuführen. Diese Einstellung installiert keinen Neustartschutz; prüfe den effektiven Zustand unter Startsicherheit.", + "dash.codexDesktopAuthless": "Codex ohne Anmeldung öffnen", + "dash.codexDesktopAuthlessHint": "Standardmäßig aus. Überspringt die separate Desktop-Anmeldung bei geeigneten lokalen Verbindungen. Zugangsdaten für den Anbieter bleiben erforderlich. Codex nach einer Änderung neu starten. Kontogebundene Desktop-Funktionen können fehlen.", "dash.searchModel": "Such-Sidecar-Modell", "dash.searchModelHint": "Modell für web_search bei nicht über OpenAI gerouteten Modellen. Erfordert ChatGPT-Login.", "dash.searchReasoning": "Such-Reasoning-Aufwand", diff --git a/gui/src/i18n/en.ts b/gui/src/i18n/en.ts index c71208942f..22a3807858 100644 --- a/gui/src/i18n/en.ts +++ b/gui/src/i18n/en.ts @@ -311,6 +311,8 @@ export const en = { "models.staleBanner": "Codex is showing an older model list than this catalog. Restart Codex to reload it.", "dash.codexAutoStart": "Start opencodex with Codex", "dash.codexAutoStartHint": "Allows an installed launcher shim to run ocx ensure. This setting does not install restart protection; check Startup safety for the effective state.", + "dash.codexDesktopAuthless": "Open Codex without signing in", + "dash.codexDesktopAuthlessHint": "Off by default. Skip the separate Desktop sign-in for eligible local connections. Upstream credentials are still required. Restart Codex after changing this setting. Account-gated Desktop features may be unavailable.", "dash.searchModel": "Search sidecar model", "dash.searchModelHint": "Model used for web_search on non-OpenAI routed models. Requires ChatGPT login.", "dash.searchReasoning": "Search reasoning effort", diff --git a/gui/src/i18n/fr.ts b/gui/src/i18n/fr.ts index e1b3519ef1..9f0f265173 100644 --- a/gui/src/i18n/fr.ts +++ b/gui/src/i18n/fr.ts @@ -301,6 +301,8 @@ export const fr: Record = { "models.staleBanner": "Codex affiche une liste de modèles plus ancienne que ce catalogue. Redémarrez Codex pour la recharger.", "dash.codexAutoStart": "Démarrer opencodex avec Codex", "dash.codexAutoStartHint": "Permet à un mécanisme de lancement installé d’exécuter ocx ensure. Ce réglage n’installe pas de protection au redémarrage ; consultez Sécurité du démarrage pour connaître l’état effectif.", + "dash.codexDesktopAuthless": "Ouvrir Codex sans se connecter", + "dash.codexDesktopAuthlessHint": "Désactivé par défaut. Ignore la connexion Desktop séparée pour les connexions locales admissibles. Les identifiants du fournisseur restent nécessaires. Redémarrez Codex après toute modification. Certaines fonctions Desktop liées au compte peuvent être indisponibles.", "dash.searchModel": "Modèle auxiliaire de recherche", "dash.searchModelHint": "Modèle utilisé pour web_search sur les modèles routés autres qu’OpenAI. Nécessite une connexion à ChatGPT.", "dash.searchReasoning": "Effort de raisonnement pour la recherche", diff --git a/gui/src/i18n/ja.ts b/gui/src/i18n/ja.ts index cf94831583..55a6fe2495 100644 --- a/gui/src/i18n/ja.ts +++ b/gui/src/i18n/ja.ts @@ -308,6 +308,8 @@ export const ja: Record = { "models.staleBanner": "Codex はこのカタログより古いモデル一覧を表示しています。Codex を再起動すると読み直されます。", "dash.codexAutoStart": "Codex と一緒に opencodex を起動", "dash.codexAutoStartHint": "インストール済み launcher shim に ocx ensure の実行を許可します。この設定だけでは再起動保護はインストールされません。起動安全性で実際の状態を確認してください。", + "dash.codexDesktopAuthless": "ログインせずに Codex を開く", + "dash.codexDesktopAuthlessHint": "既定ではオフです。対象のローカル接続で Desktop の個別ログインを省略します。上流プロバイダーの認証情報は引き続き必要です。変更後は Codex を再起動してください。アカウントに依存する Desktop 機能が利用できない場合があります。", "dash.searchModel": "検索サイドカーモデル", "dash.searchModelHint": "非 OpenAI ルーティングモデルで web_search に使うモデル。ChatGPT ログインが必要です。", "dash.searchReasoning": "検索の推論負荷", diff --git a/gui/src/i18n/ko.ts b/gui/src/i18n/ko.ts index c1959482b7..19285b150e 100644 --- a/gui/src/i18n/ko.ts +++ b/gui/src/i18n/ko.ts @@ -303,6 +303,8 @@ export const ko: Record = { "models.staleBanner": "Codex가 이 카탈로그보다 오래된 모델 목록을 보여주고 있습니다. Codex를 재시작하면 새로 읽습니다.", "dash.codexAutoStart": "Codex 실행 시 opencodex 시작", "dash.codexAutoStartHint": "설치된 launcher shim이 ocx ensure를 실행하도록 허용합니다. 이 설정은 재부팅 보호를 설치하지 않으므로 시작 안전성에서 실제 상태를 확인하세요.", + "dash.codexDesktopAuthless": "로그인 없이 Codex 열기", + "dash.codexDesktopAuthlessHint": "기본값은 꺼짐입니다. 지원되는 로컬 연결에서 별도의 Desktop 로그인을 건너뜁니다. 업스트림 인증 정보는 여전히 필요합니다. 변경 후 Codex를 다시 시작하세요. 계정에 연결된 Desktop 기능을 사용하지 못할 수 있습니다.", "dash.searchModel": "서치 사이드카 모델", "dash.searchModelHint": "비-OpenAI 라우팅 모델의 web_search에 사용되는 모델입니다. ChatGPT 로그인 필요.", "dash.searchReasoning": "서치 추론 강도", diff --git a/gui/src/i18n/ru.ts b/gui/src/i18n/ru.ts index 0109f5ebdc..87704912ac 100644 --- a/gui/src/i18n/ru.ts +++ b/gui/src/i18n/ru.ts @@ -308,6 +308,8 @@ export const ru: Record = { "models.staleBanner": "Codex показывает список моделей старее этого каталога. Перезапустите Codex, чтобы перечитать его.", "dash.codexAutoStart": "Запускать opencodex вместе с Codex", "dash.codexAutoStartHint": "Разрешает установленному launcher shim выполнять ocx ensure. Эта настройка не устанавливает защиту перезапуска; проверьте фактическое состояние в разделе безопасности запуска.", + "dash.codexDesktopAuthless": "Открывать Codex без входа", + "dash.codexDesktopAuthlessHint": "По умолчанию выключено. Пропускает отдельный вход в Desktop для допустимых локальных подключений. Учётные данные провайдера по-прежнему нужны. После изменения перезапустите Codex. Функции Desktop, связанные с аккаунтом, могут быть недоступны.", "dash.searchModel": "Модель сайдкара поиска", "dash.searchModelHint": "Модель, используемая для web_search на маршрутизируемых моделях, отличных от OpenAI. Требуется вход в аккаунт ChatGPT.", "dash.searchReasoning": "Уровень рассуждений для поиска", diff --git a/gui/src/i18n/tr.ts b/gui/src/i18n/tr.ts index fa8b8e9c25..807eeae32b 100644 --- a/gui/src/i18n/tr.ts +++ b/gui/src/i18n/tr.ts @@ -309,6 +309,8 @@ export const tr: Record = { "models.staleBanner": "Codex, bu katalogdan daha eski bir model listesi gösteriyor. Yeniden okumak için Codex'i yeniden başlatın.", "dash.codexAutoStart": "opencodex'i Codex ile başlat", "dash.codexAutoStartHint": "Yüklü bir shim'in ocx ensure çalıştırmasına izin verir. Arka plan servisi veya yeniden başlatma koruması kurmaz; sistem durumu için Başlatma Güvenliği'ne bakın.", + "dash.codexDesktopAuthless": "Codex’i oturum açmadan başlat", + "dash.codexDesktopAuthlessHint": "Varsayılan olarak kapalıdır. Uygun yerel bağlantılarda ayrı Desktop oturum açma adımını atlar. Sağlayıcı kimlik bilgileri yine gereklidir. Değişiklikten sonra Codex’i yeniden başlatın. Hesaba bağlı Desktop özellikleri kullanılamayabilir.", "dash.searchModel": "Arama yan araç modeli", "dash.searchModelHint": "OpenAI dışı yönlendirilen modellerde web_search için kullanılan model. ChatGPT girişi gerektirir.", "dash.searchReasoning": "Arama akıl yürütme çabası", diff --git a/gui/src/i18n/zh-TW.ts b/gui/src/i18n/zh-TW.ts index 3bc246543a..62e1f07110 100644 --- a/gui/src/i18n/zh-TW.ts +++ b/gui/src/i18n/zh-TW.ts @@ -200,6 +200,8 @@ export const zhTW: Record = { "models.staleBanner": "Codex 顯示的模型清單比目前的目錄舊。重新啟動 Codex 即可重新讀取。", "dash.codexAutoStart": "隨 Codex 啟動 opencodex", "dash.codexAutoStartHint": "允許已安裝的 launcher shim 執行 ocx ensure。此設定不會安裝重新啟動保護;請在啟動安全中檢查實際狀態。", + "dash.codexDesktopAuthless": "無需登入即可開啟 Codex", + "dash.codexDesktopAuthlessHint": "預設關閉。為符合條件的本機連線略過獨立的 Desktop 登入。仍需上游供應商憑證。變更後請重新啟動 Codex。依賴帳戶的 Desktop 功能可能無法使用。", "dash.searchModel": "搜尋附屬模型", "dash.searchModelHint": "用於非 OpenAI 路由模型的 web_search 的模型。需要 ChatGPT 登入。", "dash.searchReasoning": "搜尋推理強度", diff --git a/gui/src/i18n/zh.ts b/gui/src/i18n/zh.ts index b10c48688d..9946914426 100644 --- a/gui/src/i18n/zh.ts +++ b/gui/src/i18n/zh.ts @@ -303,6 +303,8 @@ export const zh: Record = { "models.staleBanner": "Codex 显示的模型列表比当前目录旧。重启 Codex 即可重新读取。", "dash.codexAutoStart": "随 Codex 启动 opencodex", "dash.codexAutoStartHint": "允许已安装的 launcher shim 运行 ocx ensure。此设置不会安装重启保护;请在启动安全中检查实际状态。", + "dash.codexDesktopAuthless": "无需登录即可打开 Codex", + "dash.codexDesktopAuthlessHint": "默认关闭。为符合条件的本地连接跳过单独的 Desktop 登录。仍需上游提供商凭据。更改后请重启 Codex。依赖账户的 Desktop 功能可能不可用。", "dash.searchModel": "搜索附属模型", "dash.searchModelHint": "用于非 OpenAI 路由模型的 web_search 的模型。需要 ChatGPT 登录。", "dash.searchReasoning": "搜索推理强度", diff --git a/gui/src/pages/dashboard-overview-sections.tsx b/gui/src/pages/dashboard-overview-sections.tsx index 8da531f97c..6606c4f560 100644 --- a/gui/src/pages/dashboard-overview-sections.tsx +++ b/gui/src/pages/dashboard-overview-sections.tsx @@ -163,7 +163,7 @@ export function DashboardInjectionPanel({ d }: { apiBase: string; d: Dash }) { export function DashboardMaintenancePanel({ d }: { d: Dash }) { const { - t, runSync, syncing, updateTriggerRef, openUpdateDialog, updateLoading, updateOpen, + t, runSync, syncing, settingsSaving, updateTriggerRef, openUpdateDialog, updateLoading, updateOpen, syncResult, syncError, updateJob, reconnecting, clearSyncFeedback, } = d; const syncHoldsWarning = !!syncResult && ( @@ -211,7 +211,7 @@ export function DashboardMaintenancePanel({ d }: { d: Dash }) {
{t("dash.syncModelsHint")}
-
+
+
+
+
{t("dash.codexDesktopAuthless")}
+
{t("dash.codexDesktopAuthlessHint")}
+ {settings?.catalogRefreshPending &&
{t("codexAuth.catalogRefreshPending")}
} +
+ +
+
+
{/* Both sidecar cards wear the DashboardInjectionPanel shell: the PANEL is the flex row, copy left, controls right. */} diff --git a/gui/src/pages/dashboard-shared.ts b/gui/src/pages/dashboard-shared.ts index 0793a7def2..d240510286 100644 --- a/gui/src/pages/dashboard-shared.ts +++ b/gui/src/pages/dashboard-shared.ts @@ -48,6 +48,8 @@ export interface ProviderInfo { name: string; adapter: string; baseUrl: string; export interface ModelInfo { id: string; provider: string; namespaced: string; owned_by?: string; reasoningEfforts?: string[] } export interface SettingsData { codexAutoStart: boolean; + codexDesktopAuthless?: boolean; + catalogRefreshPending?: boolean; /** Whether a login may open a browser on the machine running the proxy. */ oauthOpenBrowser?: boolean; port: number; diff --git a/gui/src/pages/use-dashboard-data.ts b/gui/src/pages/use-dashboard-data.ts index 6f84950ce1..6da776ea18 100644 --- a/gui/src/pages/use-dashboard-data.ts +++ b/gui/src/pages/use-dashboard-data.ts @@ -607,23 +607,24 @@ export function useDashboardData(apiBase: string) { finally { setInjectionSaving(false); } }; - const toggleCodexAutoStart = async () => { - if (!settings || settingsSaving) return; - const next = !settings.codexAutoStart; + const toggleCodexSetting = async (key: "codexAutoStart" | "codexDesktopAuthless") => { + if (!settings || settingsSaving || syncing) return; + const next = !(settings[key] ?? (key === "codexAutoStart")); setSettingsSaving(true); settingsMutationInFlightRef.current = true; - setSettings({ ...settings, codexAutoStart: next }); + setSettings({ ...settings, [key]: next }); try { const res = await fetch(`${apiBase}/api/settings`, { method: "PUT", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ codexAutoStart: next }), + body: JSON.stringify({ [key]: next }), }); - const data = await requireJson<{ codexAutoStart: boolean; startupHealth?: SettingsData["startupHealth"] }>(res, "save failed"); + const data = await requireJson(res, "save failed"); settingsMutationEpochRef.current += 1; - setSettings(prev => prev ? { ...prev, codexAutoStart: data.codexAutoStart, startupHealth: data.startupHealth ?? prev.startupHealth } : prev); + setSettings(prev => prev ? { ...prev, [key]: data[key], catalogRefreshPending: key === "codexDesktopAuthless" ? data.catalogRefreshPending : prev.catalogRefreshPending, startupHealth: data.startupHealth ?? prev.startupHealth } : prev); + if (key === "codexDesktopAuthless") await runSync(); } catch { - setSettings(prev => prev ? { ...prev, codexAutoStart: !next } : prev); + setSettings(prev => prev ? { ...prev, [key]: !next } : prev); setError(true); } finally { settingsMutationInFlightRef.current = false; @@ -631,6 +632,9 @@ export function useDashboardData(apiBase: string) { } }; + const toggleCodexAutoStart = () => toggleCodexSetting("codexAutoStart"); + const toggleCodexDesktopAuthless = () => toggleCodexSetting("codexDesktopAuthless"); + // Clears the sync result/error in this hook. The dashboard toast owns its own dismissal // timer but must publish the dismissal here: syncResult/syncError live above the dashboard // tabs, so a component-local flag alone would let a stale result remount as a fresh toast @@ -649,6 +653,7 @@ export function useDashboardData(apiBase: string) { const res = await fetch(`${apiBase}/api/sync`, { method: "POST" }); const data = await requireJson(res, "sync failed"); setSyncResult(data); + setSettings(prev => prev ? { ...prev, catalogRefreshPending: false } : prev); if (data.projectConfigGrouped) setProjectConfigWarnings(data.projectConfigGrouped); } catch (err) { setSyncError(err instanceof Error ? err.message : String(err)); @@ -789,7 +794,7 @@ export function useDashboardData(apiBase: string) { effortCapHelpTriggerRef, updateTriggerRef, maHelpTriggerRef, shadowCallHelpTriggerRef, effortCapHelpDialogRef, updateDialogRef, maHelpDialogRef, shadowCallHelpDialogRef, filteredGroups, sidecarModels, visionModels, - saveSidecar, saveShadowCall, switchMaMode, toggleCodexAutoStart, runSync, clearSyncFeedback, + saveSidecar, saveShadowCall, switchMaMode, toggleCodexAutoStart, toggleCodexDesktopAuthless, runSync, clearSyncFeedback, fetchUpdateCheck, closeUpdateDialog, openUpdateDialog, changeUpdateChannel, runUpdate, }; } diff --git a/gui/tests/vision-sidecar-dashboard.test.tsx b/gui/tests/vision-sidecar-dashboard.test.tsx index dc762de58f..994a409126 100644 --- a/gui/tests/vision-sidecar-dashboard.test.tsx +++ b/gui/tests/vision-sidecar-dashboard.test.tsx @@ -12,7 +12,7 @@ import { LanguageProvider } from "../src/i18n/provider"; import { DashboardSidecarPanels } from "../src/pages/dashboard-overview-sections"; import type { SidecarData, SidecarPatch } from "../src/pages/dashboard-shared"; import { mergeSidecarSetting } from "../src/pages/dashboard-shared"; -import type { useDashboardData } from "../src/pages/use-dashboard-data"; +import { useDashboardData } from "../src/pages/use-dashboard-data"; const globals = ["document", "window", "navigator", "IS_REACT_ACT_ENVIRONMENT"] as const; let previousGlobals: Record<(typeof globals)[number], PropertyDescriptor | undefined>; @@ -382,4 +382,79 @@ test("model and reasoning saves still omit enabled, limit, and timeout", async ( expect(patches).toHaveLength(2); expect(patches[1]).toEqual({ vision: { reasoning: "high" } }); assertVisionControlFieldsOmitted(patches[1]!); -}); \ No newline at end of file +}); + +test("Desktop login switch defaults off, preserves explicit opt-in, and disables while saving", async () => { + const { d } = harness(); + let clicks = 0; + d.toggleCodexDesktopAuthless = async () => { clicks += 1; }; + d.settings = { codexAutoStart: true, port: 10100, hostname: "127.0.0.1" }; + await mount(d); + const toggle = () => host.querySelector(`button[aria-label="${en["dash.codexDesktopAuthless"]}"]`)!; + expect(toggle().getAttribute("aria-pressed")).toBe("false"); + d.settings.codexDesktopAuthless = true; + await mount(d); + expect(toggle().getAttribute("aria-pressed")).toBe("true"); + await act(async () => { toggle().click(); }); + expect(clicks).toBe(1); + d.settings.codexDesktopAuthless = false; + d.settings.catalogRefreshPending = true; + d.settingsSaving = true; + await mount(d); + expect(toggle().getAttribute("aria-pressed")).toBe("false"); + expect(toggle().disabled).toBe(true); + expect(host.textContent).toContain(en["codexAuth.catalogRefreshPending"]); +}); + + +test.each([undefined, false, true])("Desktop login preference %s persists before full sync; sync failure keeps the saved preference", async (initial) => { + const originalFetch = globalThis.fetch; + const writes: Array<{ path: string; body: unknown }> = []; + let latest: Dash | undefined; + let saved = initial; + const apiBase = `/authless-test-${String(initial)}`; + globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => { + const path = String(input); + if (init?.method === "PUT") { + const body = JSON.parse(String(init.body)); + writes.push({ path, body }); + if (body.codexDesktopAuthless !== undefined) { + saved = body.codexDesktopAuthless; + return Response.json({ codexDesktopAuthless: saved, catalogRefreshPending: true }); + } + return Response.json({ codexAutoStart: body.codexAutoStart, catalogRefreshPending: false }); + } + if (path.endsWith("/api/sync")) { + writes.push({ path, body: null }); + return Response.json({ error: "sync unavailable" }, { status: 503 }); + } + if (path.endsWith("/api/settings")) { + return Response.json({ codexAutoStart: true, codexDesktopAuthless: saved, port: 10100, hostname: "127.0.0.1" }); + } + return Response.json({}, { status: 503 }); + }) as typeof fetch; + function Harness() { latest = useDashboardData(apiBase); return null; } + try { + const { createRoot } = await import("react-dom/client"); + await act(async () => { + root = createRoot(host); + root.render(); + }); + expect(latest?.settings?.codexDesktopAuthless).toBe(initial); + await act(async () => { await latest!.toggleCodexDesktopAuthless(); }); + expect(writes).toEqual([ + { path: `${apiBase}/api/settings`, body: { codexDesktopAuthless: !initial } }, + { path: `${apiBase}/api/sync`, body: null }, + ]); + expect(latest?.settings?.codexDesktopAuthless).toBe(!initial); + expect(latest?.syncError).toBe("sync unavailable"); + expect(latest?.settings?.catalogRefreshPending).toBe(true); + await act(async () => { await latest!.toggleCodexAutoStart(); }); + expect(latest?.settings?.codexAutoStart).toBe(false); + expect(latest?.settings?.catalogRefreshPending).toBe(true); + } finally { + await act(async () => { root?.unmount(); }); + root = null; + globalThis.fetch = originalFetch; + } +}); diff --git a/tests/codex-integration/codex-inject.test.ts b/tests/codex-integration/codex-inject.test.ts index 84ac5f67b6..b6be3c2f69 100644 --- a/tests/codex-integration/codex-inject.test.ts +++ b/tests/codex-integration/codex-inject.test.ts @@ -31,8 +31,8 @@ describe("Codex config injection", () => { }); describe("authless Codex Desktop opt-in (#1107)", () => { - test("default target on loopback stays Design B and byte-identical", () => { - const target = standaloneCodexRoutingTarget(10100, {}); + test.each([undefined, false])("disabled preference %s on loopback stays Design B and byte-identical", (codexDesktopAuthless) => { + const target = standaloneCodexRoutingTarget(10100, { codexDesktopAuthless }); expect(target.desktopAuthless).toBeUndefined(); expect(buildProfileFile(target, null)).toBe(buildProfileFile(10100, null)); expect(buildProviderTableBlock(target)).toContain("requires_openai_auth = true");