Unify ChatGPT background use and account management

This commit is contained in:
amosblomqvist
2026-10-24 15:28:26 +03:00
parent 8691470d03
commit 42abeb65ae
19 changed files with 687 additions and 416 deletions
+179 -10
View File
@@ -82,12 +82,15 @@ test("PKCE/state/nonce/host identity and exact ephemeral 127.0.0.1 callback; no
assert.equal(createHash("sha256").update(form.get("code_verifier")!).digest("base64url"), params.get("code_challenge"));
assert.equal(form.get("resource"), "https://api.openai.com/v1");
const status = await f.service.status();
assert.equal(status.accounts[0].background, false);
assert.equal(status.allowBackgroundUse, true);
assert.equal(f.saved.registrations[0].background, undefined, "new registrations do not store obsolete per-account preferences");
assert.equal(status.activeId, f.saved.activeId);
assert(!JSON.stringify(status).includes("scratch-oauth-token"));
assert(!JSON.stringify(status).includes("scratch-refresh"));
assert(!JSON.stringify(status).includes("idToken"));
assert.equal((await f.service.lease(false)).accessToken, "scratch-oauth-token");
assert.equal((await f.service.lease(true)).accessToken, "scratch-oauth-token");
await f.service.consent(false);
await assert.rejects(f.service.lease(true), /background/);
} finally { f.service.close(); }
});
@@ -243,7 +246,7 @@ test("invalid rotating replacement is durably revocation-only; verified mapping
try {
await restarted.service.status();
await restarted.service.select(restricted.id);
restarted.service.resume(); await restarted.service.consent(restricted.id, true);
restarted.service.resume(); await restarted.service.consent(true);
await assert.rejects(restarted.service.lease(false));
assert.equal((await restarted.service.status()).accounts[0].connected, false);
assert.deepEqual((await restarted.service.models()).models, []);
@@ -268,7 +271,7 @@ test("detected secure backend loss emits full invalidation, denies all leases an
assert.equal((await f.service.status()).available, false);
assert(pushes.some((p) => p.invalidate === "all" && p.stamp.epoch > epoch));
await assert.rejects(f.service.lease(false)); await assert.rejects(f.service.lease(true));
lost = false; f.service.resume(); await f.service.consent(f.saved.activeId, true);
lost = false; f.service.resume(); await f.service.consent(true);
await assert.rejects(f.service.lease(false));
assert.equal((await f.service.status()).accounts[0].connected, false);
} finally { f.service.close(); }
@@ -280,7 +283,7 @@ test("strict encrypted schema rejects malformed marker/duplicates/token/active i
const file = path.join(dir, "credentials.enc.json");
const secure = { isEncryptionAvailable: () => true, encryptString: (s: string) => Buffer.from(s), decryptString: (b: Buffer) => b.toString() };
try {
for (const mutate of [(s: any) => s.registrations[0].reconnectRequired = false, (s: any) => s.registrations[0].reconnectRequired = "true", (s: any) => s.registrations.push(s.registrations[0]), (s: any) => s.activeId = "missing", (s: any) => s.registrations[0].tokens.scopes = "not-array", (s: any) => s.registrations[0].tokens.expiresAt = -1]) {
for (const mutate of [(s: any) => s.registrations[0].reconnectRequired = false, (s: any) => s.registrations[0].reconnectRequired = "true", (s: any) => s.registrations.push(s.registrations[0]), (s: any) => s.activeId = "missing", (s: any) => s.registrations[0].tokens.scopes = "not-array", (s: any) => s.registrations[0].tokens.expiresAt = -1, (s: any) => s.allowBackgroundUse = "true", (s: any) => s.allowBackgroundUse = null, (s: any) => s.registrations[0].background = "false"]) {
const state = structuredClone(f.saved); mutate(state);
const bytes = JSON.stringify({ version: 1, encrypted: Buffer.from(JSON.stringify(state)).toString("base64") });
await fs.writeFile(file, bytes);
@@ -293,15 +296,15 @@ test("strict encrypted schema rejects malformed marker/duplicates/token/active i
test("consent OFF wins an older ON save; foreground catalog authority remains unchanged", async () => {
let hold = false; const entered = deferred(); const release = deferred();
const f = await loggedIn({ write: async (next) => { if (hold && next.registrations[0]?.background) { entered.resolve(); await release.promise; } } });
const f = await loggedIn({ write: async (next) => { if (hold && next.allowBackgroundUse === true) { entered.resolve(); await release.promise; } } });
try {
const catalog = await f.service.models(); const id = f.saved.activeId;
hold = true;
const on = f.service.consent(id, true); await entered.promise;
const off = f.service.consent(id, false);
assert.equal((await f.service.status()).accounts[0].background, false);
const on = f.service.consent(true); await entered.promise;
const off = f.service.consent(false);
assert.equal((await f.service.status()).allowBackgroundUse, false);
release.resolve(); await on; await off;
assert.equal(f.saved.registrations[0].background, false);
assert.equal(f.saved.allowBackgroundUse, false);
assert.equal(f.service.stamp().authorityRevision, catalog.authorityRevision);
assert.equal((await f.rawLease(false, { expectedAccountId: id, expectedAuthorityRevision: catalog.authorityRevision, selectedModelId: "gpt-5" })).accountId, id);
await assert.rejects(f.rawLease(true, { expectedAccountId: id, expectedAuthorityRevision: catalog.authorityRevision, selectedModelId: "gpt-5" }), /background/);
@@ -325,12 +328,178 @@ test("scope-disabled credentials remain revocable and unconfirmed revoke never c
try {
await assert.rejects(f.service.lease(false));
const status = await f.service.logout(f.saved.activeId);
assert.match(status.error, /Remote revocation was not confirmed/);
assert.equal(status.error, "Account disconnected locally. Remote revocation could not be confirmed.");
assert.equal(f.requests.filter((r) => r.form?.get("token") === "scope-disabled-refresh").length, 2);
assert.equal(f.saved.registrations[0].tokens, undefined);
} finally { f.service.close(); }
});
test("repeated real verified sign-in creates one visible account while preserving separately bound registrations until explicit forgetting", async () => {
const f = await loggedIn();
try {
const original = f.saved.registrations[0];
await f.service.login(); await f.callback({ client_id: "oaiapp_repeat" });
for (let i = 0; i < 100 && (await f.service.status()).pending; i++) await new Promise((r) => setImmediate(r));
const status = await f.service.status();
assert.equal(f.saved.registrations.length, 2); assert.equal(status.accounts.length, 1);
assert.equal(status.activeId, status.accounts[0].id); assert.notEqual(status.activeId, original.id);
assert.equal(f.saved.registrations[0].clientId, original.clientId); assert.equal(f.saved.registrations[1].clientId, "oaiapp_repeat");
await f.service.disconnect(status.activeId);
assert.deepEqual(f.saved.registrations, []); assert.equal((await f.service.status()).accounts.length, 0);
assert.equal(f.requests.filter((r) => r.form?.has("token_type_hint")).length, 2);
} finally { f.service.close(); }
});
test("verified identity grouping hides legacy duplicate registrations without merging client credentials or matching emails", async () => {
const first = await loggedIn(); first.service.close();
const state = structuredClone(first.saved);
const original = state.registrations[0];
const duplicate = { ...structuredClone(original), id: "legacy-duplicate", clientId: "oaiapp_legacy" };
duplicate.tokens.idToken = await signed(duplicate.clientId, "legacy-nonce", original.subject);
const distinct = { ...structuredClone(original), id: "different-subject", clientId: "oaiapp_distinct", subject: "different-verified-subject" };
distinct.tokens.idToken = await signed(distinct.clientId, "legacy-nonce", distinct.subject);
state.registrations.push(duplicate, distinct);
state.activeId = duplicate.id;
const f = fixture({ state });
try {
const status = await f.service.status();
assert.equal(status.accounts.length, 2, "same issuer+subject groups; equal emails alone do not");
assert.equal(status.activeId, duplicate.id, "private transport keeps the actual active registration");
assert(status.accounts.some((a: any) => a.id === duplicate.id));
assert(status.accounts.some((a: any) => a.id === distinct.id));
assert.equal(f.saved.registrations.length, 3, "a status read never purges or merges stored credential handles");
assert.deepEqual(f.saved, state);
await f.service.select(distinct.id);
assert.equal((await f.service.status()).activeId, distinct.id);
assert.equal((await f.service.status()).accounts.length, 2);
} finally { f.service.close(); }
});
test("explicit disconnect forgets all exact-identity registrations/handles, aborts active leases, and never auto-selects a same-email distinct account", async () => {
const first = await loggedIn(); first.service.close();
const state = structuredClone(first.saved), original = state.registrations[0];
const duplicate = { ...structuredClone(original), id: "duplicate-client", clientId: "oaiapp_duplicate", tokens: { ...original.tokens, refreshToken: "duplicate-refresh" } };
duplicate.tokens.idToken = await signed(duplicate.clientId, "historical", original.subject);
const other = { ...structuredClone(original), id: "distinct-account", clientId: "oaiapp_other", subject: "other-subject" };
other.tokens.idToken = await signed(other.clientId, "historical", other.subject);
state.registrations.push(duplicate, other);
const pushes: any[] = [], entered = deferred(), release = deferred();
let held = false;
const f = fixture({ state, change: (push) => pushes.push(push), provider: async (url) => {
if (url.endsWith("/revoke") && !held) { held = true; entered.resolve(); await release.promise; }
return undefined;
} });
try {
await f.service.status(); const catalog = await f.service.models();
const expected = { expectedAccountId: state.activeId, expectedAuthorityRevision: catalog.authorityRevision, selectedModelId: "gpt-5" };
await f.rawLease(false, expected);
const removing = f.service.disconnect(original.id); await entered.promise;
assert(pushes.some((push) => push.invalidate === "all"));
assert.equal((await f.service.status()).accounts.find((a: any) => a.id === original.id).connected, false);
release.resolve(); const result = await removing;
assert.equal(result.activeId, null); assert.equal(result.accounts.length, 1); assert.equal(result.accounts[0].id, other.id);
assert.deepEqual(f.saved.registrations, [other], "all duplicate handles gone; other verified subject untouched");
const revocations = f.requests.filter((r) => r.form?.has("token_type_hint"));
assert.equal(revocations.length, 2); assert(revocations.some((r) => r.form!.get("client_id") === duplicate.clientId && r.form!.get("token") === "duplicate-refresh"));
await assert.rejects(f.rawLease(false, expected));
await assert.rejects(f.service.select(original.id));
assert.equal(f.saved.activeId, null, "no automatic account/provider fallback");
} finally { release.resolve(); f.service.close(); }
});
test("disconnecting an inactive identity preserves the active identity/credentials/catalog authority", async () => {
const first = await loggedIn(); first.service.close();
const state = structuredClone(first.saved), original = state.registrations[0];
const other = { ...structuredClone(original), id: "inactive-account", clientId: "oaiapp_inactive", subject: "inactive-subject" };
other.tokens.idToken = await signed(other.clientId, "historical", other.subject);
state.registrations.push(other); const f = fixture({ state });
try {
await f.service.status(); const catalog = await f.service.models(); const before = f.service.stamp();
await f.service.disconnect(other.id);
assert.equal(f.saved.activeId, original.id); assert.deepEqual(f.saved.registrations, [original]);
assert.equal(f.service.stamp().epoch, before.epoch); assert.equal(f.service.stamp().authorityRevision, before.authorityRevision);
assert.equal((await f.rawLease(false, { expectedAccountId: original.id, expectedAuthorityRevision: catalog.authorityRevision, selectedModelId: "gpt-5" })).accountId, original.id);
} finally { f.service.close(); }
});
test("signed-out legacy registrations can be explicitly forgotten; failed revocation never retains forgotten handles", async () => {
const first = await loggedIn(); await first.service.logout(first.saved.activeId); first.service.close();
const state = structuredClone(first.saved), duplicate = { ...structuredClone(state.registrations[0]), id: "signed-out-duplicate", clientId: "oaiapp_old" };
state.registrations.push(duplicate);
const f = fixture({ state });
try {
const status = await f.service.status(); assert.equal(status.accounts.length, 1); assert.equal(status.accounts[0].connected, false);
const result = await f.service.disconnect(status.accounts[0].id);
assert.equal(result.accounts.length, 0); assert.equal(result.activeId, null); assert.equal(f.saved.registrations.length, 0);
assert.equal(result.error, "Account disconnected locally. Remote revocation could not be confirmed.");
} finally { f.service.close(); }
const unconfirmed = await loggedIn({ provider: async (url) => url.endsWith("/revoke") ? new Response(null, { status: 400 }) : undefined });
try {
const result = await unconfirmed.service.disconnect(unconfirmed.saved.activeId);
assert.equal(result.error, "Account disconnected locally. Remote revocation could not be confirmed."); assert.deepEqual(unconfirmed.saved.registrations, []);
assert.equal(unconfirmed.saved.activeId, null); assert.equal(JSON.stringify(unconfirmed.saved).includes("scratch-refresh"), false);
} finally { unconfirmed.service.close(); }
});
test("disconnect racing refresh revokes the latest rotated handle before forgetting, never its stale predecessor", async () => {
const entered = deferred(), release = deferred();
const f = await loggedIn({ token: async (form, url) => {
if (form.get("grant_type") === "refresh_token") {
entered.resolve(); await release.promise;
return Response.json({ access_token: "latest-access", refresh_token: "latest-refresh", token_type: "Bearer", expires_in: 3600, scope: SCOPES.join(" ") });
}
return Response.json({ access_token: "scratch-access", refresh_token: "scratch-refresh", id_token: await signed(form.get("client_id")!, url.searchParams.get("nonce")!), token_type: "Bearer", expires_in: 1, scope: SCOPES.join(" ") });
} });
try {
const lease = f.service.lease(false); const rejected = assert.rejects(lease); await entered.promise;
const forgetting = f.service.disconnect(f.saved.activeId); release.resolve(); await rejected; await forgetting;
const revoked = f.requests.filter((r) => r.form?.has("token_type_hint"));
assert.equal(revoked.length, 1); assert.equal(revoked[0]!.form!.get("token"), "latest-refresh");
assert.deepEqual(f.saved.registrations, []); assert.equal(f.saved.activeId, null);
} finally { release.resolve(); f.service.close(); }
});
test("disconnect persistence failure is not reported as local forgetting and leaves encrypted state fail-closed", async () => {
let failWrite = false;
const f = await loggedIn({ write: async () => { if (failWrite) throw Object.assign(new Error("storage"), { code: "storage" }); } });
try {
const before = structuredClone(f.saved); failWrite = true;
await assert.rejects(f.service.disconnect(f.saved.activeId));
assert.deepEqual(f.saved, before); assert.equal((await f.service.status()).accounts[0].connected, false);
await assert.rejects(f.service.lease(false));
} finally { f.service.close(); }
});
test("one app-wide background preference defaults ON over legacy OFF, persists with zero accounts, and never changes on auth/account operations", async () => {
const first = await loggedIn(); first.service.close();
const legacy = structuredClone(first.saved); legacy.registrations[0].background = false;
const f = fixture({ state: legacy });
try {
assert.equal((await f.service.status()).allowBackgroundUse, true, "absent new setting ignores legacy per-account OFF");
await f.service.lease(true);
await f.service.consent(false);
assert.equal(f.saved.allowBackgroundUse, false); assert.equal(f.saved.registrations[0].background, false);
await assert.rejects(f.service.lease(true), /background/);
await f.service.lease(false);
const id = f.saved.activeId;
await f.service.select(id); assert.equal((await f.service.status()).allowBackgroundUse, false);
await f.service.login(id); await f.callback();
for (let i = 0; i < 100 && (await f.service.status()).pending; i++) await new Promise((r) => setImmediate(r));
assert.equal(f.saved.allowBackgroundUse, false, "reconnecting cannot overwrite app preference");
assert.equal(f.saved.registrations[0].background, undefined, "new verified registrations omit obsolete per-account preference");
await f.service.disconnect(f.saved.activeId);
assert.deepEqual(f.saved.registrations, []); assert.equal(f.saved.allowBackgroundUse, false);
const restarted = fixture({ state: f.saved });
try {
assert.equal((await restarted.service.status()).allowBackgroundUse, false, "zero-account restart preserves global OFF");
await restarted.service.login(); await restarted.callback();
for (let i = 0; i < 100 && (await restarted.service.status()).pending; i++) await new Promise((r) => setImmediate(r));
assert.equal(restarted.saved.allowBackgroundUse, false);
await restarted.service.consent(true); assert.equal(restarted.saved.allowBackgroundUse, true);
} finally { restarted.service.close(); }
} finally { f.service.close(); }
});
test("private leases bind active account, authority revision and raw visible model membership", async () => {
const f = await loggedIn();
try {
+1 -1
View File
@@ -19,7 +19,7 @@ const { persistTurn } = await import("../src/server/turn-service.ts");
const ipc = (value: unknown) => (process.emit as (name: string, value: unknown) => boolean)("message", value);
let revision = 1;
function connection(background: boolean) {
ipc({ channel: "lh:chatgpt", kind: "state", stamp: { epoch: 1, authorityRevision: 1, revision: revision++ }, invalidate: background ? "none" : "background", status: { available: true, pending: false, activeId: "scratch-account", accounts: [{ id: "scratch-account", connected: true, planEnabled: true, background }], error: null } });
ipc({ channel: "lh:chatgpt", kind: "state", stamp: { epoch: 1, authorityRevision: 1, revision: revision++ }, invalidate: background ? "none" : "background", status: { available: true, pending: false, allowBackgroundUse: background, activeId: "scratch-account", accounts: [{ id: "scratch-account", connected: true, planEnabled: true, background: !background }], error: null } });
}
after(() => { fs.rmSync(dir, { recursive: true, force: true }); });
+2 -2
View File
@@ -6,7 +6,7 @@ const send = Object.getOwnPropertyDescriptor(process, "send"), connected = Objec
const pending: any[] = [];
const model = piModels().getModels("openai").find((m) => m.api === "openai-responses")!;
const ipc = (m: unknown) => (process.emit as (name: string, value: unknown) => boolean)("message", m);
const status = (account: string, background = true) => ({ available: true, pending: false, activeId: account, accounts: [{ id: account, connected: true, background, planEnabled: true }], error: null });
const status = (account: string, background = true) => ({ available: true, pending: false, allowBackgroundUse: background, activeId: account, accounts: [{ id: account, connected: true, background: !background, planEnabled: true }], error: null });
const value = (name: string, revision: number, account = "A", authorityRevision = 1) => ({ epoch: account === "A" ? 1 : 2, authorityRevision, revision, status: status(account), accountId: account, models: [{ id: model.id, name }] });
function reply(request: any, v: unknown) { ipc({ channel: "lh:chatgpt", id: request.id, op: request.op, ok: true, value: v }); }
before(() => {
@@ -27,7 +27,7 @@ test("consent-only status does not discard current-authority catalog or overwrit
const refresh = refreshChatGPTModels([model as never]); const request = pending.at(-1);
ipc({ channel: "lh:chatgpt", kind: "state", stamp: { epoch: 1, authorityRevision: 1, revision: 3 }, status: status("A", false), invalidate: "background" });
reply(request, value("same authority", 2)); await refresh;
assert.equal(chatGPTModels()[0]?.name, "same authority"); assert.equal(chatGPTStatus().accounts[0]?.background, false);
assert.equal(chatGPTModels()[0]?.name, "same authority"); assert.equal(chatGPTStatus().allowBackgroundUse, false);
const scope = chatGPTScope(model.id); assert(chatGPTScopeCurrent(scope));
const foreground = chatGPTAbortController(false), background = chatGPTAbortController(true);
ipc({ channel: "lh:chatgpt", kind: "state", stamp: { epoch: 1, authorityRevision: 1, revision: 4 }, status: status("A", false), invalidate: "background" });
+259 -267
View File
@@ -5,8 +5,11 @@ import { afterEach, test } from "node:test";
import { createRoot, type Root } from "react-dom/client";
import { renderToStaticMarkup } from "react-dom/server";
import { parseHTML } from "linkedom";
import { ChatGPTBackgroundAccess, ChatGPTConnection, ChatGPTUIProvider, CHATGPT_NOTICE } from "../components/settings/chatgpt-connection.tsx";
import { createRequire } from "node:module";
import { generateKeyPair, exportJWK, SignJWT } from "jose";
import { ChatGPTBackgroundAccess, ChatGPTConnection, ChatGPTUIProvider, CHATGPT_NOTICE, CHATGPT_DISCONNECT_COPY } from "../components/settings/chatgpt-connection.tsx";
import { SettingsDialogBody } from "../components/settings/settings-dialog.tsx";
import { KeysSection } from "../components/settings/keys-section.tsx";
import { ModelControl } from "../components/model-picker.tsx";
import type { ChatGPTStatus, ChatGPTControlBridge } from "../src/core/index.ts";
@@ -14,26 +17,30 @@ const dom = parseHTML("<html><body></body></html>");
Object.assign(globalThis, { window: dom.window, document: dom.document, Element: dom.window.Element, HTMLElement: dom.window.HTMLElement, CustomEvent: dom.window.CustomEvent, IS_REACT_ACT_ENVIRONMENT: true, localStorage: { getItem: () => null, setItem() {} } });
const originalFetch = globalThis.fetch;
let root: Root | undefined, host: HTMLDivElement | undefined;
const account = (id = "private-registration-id", email = "learner@example.invalid", connected = true, background = false) => ({ id, email, registration: "private-issued-client", connected, background, planEnabled: true });
const disconnected = (): ChatGPTStatus => ({ available: true, pending: false, activeId: null, accounts: [account("signed-out-record", "signed-out@example.invalid", false)], error: null });
const connected = (background = false): ChatGPTStatus => ({ ...disconnected(), activeId: "private-registration-id", accounts: [account(undefined, undefined, true, background), ...disconnected().accounts] });
const account = (id = "private-registration-id", email = "learner@example.invalid", connected = true) => ({ id, email, registration: "private-issued-client", connected, planEnabled: connected });
const disconnected = (allowBackgroundUse = true): ChatGPTStatus => ({ available: true, pending: false, allowBackgroundUse, activeId: null, accounts: [account("signed-out-record", "signed-out@example.invalid", false)], error: null });
const connected = (allowBackgroundUse = true): ChatGPTStatus => ({ ...disconnected(allowBackgroundUse), activeId: "private-registration-id", accounts: [account(), ...disconnected().accounts] });
const storageError = "ChatGPT credentials could not be saved securely. Reconnect before making requests.";
const warning = "Account disconnected locally. Remote revocation could not be confirmed.";
const errorReply = () => ({ error: storageError } as ChatGPTStatus);
function deferred<T>() { let resolve!: (v: T) => void; const promise = new Promise<T>((done) => { resolve = done; }); return { promise, resolve }; }
function fixture(initial = disconnected(), overrides: Partial<ChatGPTControlBridge> = {}) {
let status = initial;
const calls = { login: 0, cancel: 0, consent: [] as boolean[], consentIds: [] as string[], logout: 0 };
const calls = { login: [] as (string | undefined)[], cancel: 0, consent: [] as boolean[], disconnect: [] as string[], select: [] as string[], logout: 0 };
const listeners = new Set<() => void>();
function push(next: ChatGPTStatus) { status = next; for (const cb of listeners) cb(); }
const bridge: ChatGPTControlBridge = {
status: async () => status,
login: async () => { calls.login++; return status = connected(); },
cancel: async () => { calls.cancel++; return status = disconnected(); },
select: async (id) => { status = { ...status, activeId: id }; return status; },
logout: async () => { calls.logout++; return status = disconnected(); },
consent: async (id, enabled) => { calls.consentIds.push(id); calls.consent.push(enabled); status = { ...status, accounts: status.accounts.map((a) => a.id === id ? { ...a, background: enabled } : a) }; return status; },
onChange: (cb) => { listeners.add(cb); return () => listeners.delete(cb); },
...overrides,
login: async (id) => { calls.login.push(id); push({ ...status, pending: true, error: null }); return status; },
cancel: async () => { calls.cancel++; push({ ...status, pending: false, error: null }); return status; },
select: async (id) => { calls.select.push(id); push({ ...status, activeId: id }); return status; },
disconnect: async (id) => { calls.disconnect.push(id); push({ ...status, activeId: status.activeId === id ? null : status.activeId, accounts: status.accounts.filter((a) => a.id !== id) }); return status; },
logout: async () => { calls.logout++; return status; },
consent: async (enabled) => { calls.consent.push(enabled); push({ ...status, allowBackgroundUse: enabled }); return status; },
onChange: (cb) => { listeners.add(cb); return () => listeners.delete(cb); }, ...overrides,
};
window.lh = { expandWindow() {}, chatgpt: bridge };
return { calls, bridge, push(next: ChatGPTStatus) { status = next; for (const cb of listeners) cb(); } };
return { calls, bridge, push, get status() { return status; }, complete() { push({ ...connected(status.allowBackgroundUse), pending: false }); } };
}
async function mount(children: React.ReactNode = <ChatGPTConnection />) {
host = document.createElement("div"); document.body.append(host); root = createRoot(host);
@@ -42,288 +49,273 @@ async function mount(children: React.ReactNode = <ChatGPTConnection />) {
const byTest = (id: string) => document.querySelector<HTMLElement>(`[data-testid="${id}"]`)!;
async function click(element: HTMLElement) { assert(element); await act(async () => element.click()); }
function button(text: string, scope: ParentNode = document) { return [...scope.querySelectorAll<HTMLButtonElement>("button")].find((el) => el.textContent === text)!; }
const toggle = () => document.querySelector<HTMLElement>('[data-testid="chatgpt-account-dialog"] [role="switch"]')!;
async function selectAccount(label: string, id: string) {
const select = document.querySelector<HTMLSelectElement>(`select[aria-label="${label}"]`)!; assert(select);
await act(async () => { Object.defineProperty(select, "value", { configurable: true, writable: true, value: id }); select.dispatchEvent(new window.Event("change", { bubbles: true })); });
}
async function begin() { await click(byTest("chatgpt-login")); await click(byTest("chatgpt-proceed")); }
async function confirmDisconnect() { await click(byTest("chatgpt-disconnect")); await click(byTest("chatgpt-confirm-disconnect")); }
afterEach(async () => { await act(async () => root?.unmount()); host?.remove(); root = undefined; host = undefined; delete window.lh; globalThis.fetch = originalFetch; });
test("settings put Model providers first and select it by default", () => {
test("Providers is the first/default page; General is last and other pages keep relative order", () => {
const html = renderToStaticMarkup(<SettingsDialogBody onClose={() => {}} />);
assert.match(html, /settings-nav-keys[^>]*aria-current="true"/);
assert(html.indexOf("settings-nav-keys") < html.indexOf("settings-nav-general"));
assert.match(html, />Model providers<\/button>/);
assert.doesNotMatch(html, />Connections<\/button>/);
const ids = [...html.matchAll(/data-testid="settings-nav-([^"]+)"/g)].map((m) => m[1]);
assert.deepEqual(ids, ["keys", "agent", "appearance", "graph", "keyboard", "general"]);
assert.match(html, /settings-nav-keys[^>]*aria-current="true"/); assert.match(html, />Providers<\/button>/);
assert.doesNotMatch(html, />Model providers<\/button>/);
});
test("setup and disconnected rows are secondary buttons, not disclosure walls; cancel never logs in or changes consent", async () => {
test("Providers has exactly four ordered sections; unknown variables stay masked/read-only at the end of Model API keys", async () => {
globalThis.fetch = async (url) => {
assert.equal(url, "/api/keys");
return Response.json({ providers: [{ id: "openai", envVar: "OPENAI_API_KEY", label: "OpenAI" }], keys: [{ id: "FUTURE_VAR", info: null, present: true, masked: "…abcd", source: "file", shadowedByEnv: false }] });
};
fixture(); await mount(<KeysSection />);
const headings = [...document.querySelectorAll('[data-testid^="group-"] > p')].map((el) => el.textContent);
assert.deepEqual(headings, ["Model API keys", "Subscriptions", "Dictation", "Search"]);
assert(byTest("key-deepgram_api_key").closest('[data-testid="group-dictation"]'));
assert(byTest("key-tavily_api_key").closest('[data-testid="group-search"]'));
assert(byTest("key-brave_api_key").closest('[data-testid="group-search"]'));
assert(byTest("unknown-FUTURE_VAR").closest('[data-testid="group-provider-keys"]'));
assert(byTest("group-unknown-keys").parentElement?.lastElementChild === byTest("group-unknown-keys"));
assert.equal(byTest("unknown-FUTURE_VAR").textContent, "FUTURE_VAR…abcd"); assert(!byTest("unknown-FUTURE_VAR").querySelector("input, button"));
});
test("disconnected row is only secondary Continue; initial default is ON; cancel discards draft without OAuth/global mutation", async () => {
const f = fixture(); await mount(<ChatGPTConnection setup />);
assert(!document.body.textContent?.includes(CHATGPT_NOTICE));
assert(!document.body.textContent?.includes("Manage usage"));
assert(!document.body.textContent?.includes("signed-out@example.invalid"));
assert.equal(f.calls.login, 0);
const trigger = byTest("chatgpt-login");
assert.match(trigger.className, /cursor-pointer/);
assert.match(trigger.className, /hover:bg-accent/);
assert.match(trigger.className, /focus-visible:ring/);
await click(trigger);
assert.equal(f.calls.login, 0, "opening disclosure is not OAuth");
assert(document.body.textContent?.includes(CHATGPT_NOTICE));
const toggle = document.querySelector<HTMLElement>('[role="switch"][aria-label="Allow background use"]')!;
assert.equal(toggle.getAttribute("aria-checked"), "false");
await click(toggle); assert.equal(toggle.getAttribute("aria-checked"), "true");
assert.equal(byTest("chatgpt-connection").textContent, "Continue with ChatGPT");
assert(!document.body.textContent?.includes(CHATGPT_NOTICE)); const trigger = byTest("chatgpt-login");
assert.match(trigger.className, /cursor-pointer/); assert.match(trigger.className, /hover:bg-accent/); assert.match(trigger.className, /focus-visible:ring/);
await click(trigger); assert.equal(f.calls.login.length, 0); assert(document.body.textContent?.includes(CHATGPT_NOTICE));
assert.equal(toggle().getAttribute("aria-checked"), "true");
assert.equal(toggle().getAttribute("aria-label"), "Allow background use for all ChatGPT accounts");
assert(byTest("chatgpt-global-background").textContent?.includes("Background work counts toward your ChatGPT plan usage."));
await click(toggle());
await click(button("Cancel", byTest("chatgpt-account-dialog")));
assert.equal(f.calls.login.length, 0); assert.deepEqual(f.calls.consent, []); assert.equal(f.calls.cancel, 0);
await click(byTest("chatgpt-login")); assert.equal(toggle().getAttribute("aria-checked"), "true");
});
for (const value of [true, false]) test(`untouched initial sign-in preserves existing global ${value} through verified completion`, async () => {
const f = fixture(disconnected(value)); await mount(); await click(byTest("chatgpt-login"));
assert.equal(toggle().getAttribute("aria-checked"), String(value)); await click(byTest("chatgpt-proceed"));
assert(document.body.textContent?.includes("Waiting for browser sign-in")); assert.deepEqual(f.calls.consent, []);
await act(async () => f.complete()); assert.deepEqual(f.calls.consent, []); assert.equal(f.status.allowBackgroundUse, value);
assert(!document.querySelector('[data-testid="chatgpt-account-dialog"]')); assert(button("Manage…"));
});
for (const value of [true, false]) test(`explicit global draft ${!value} is applied only after actual pending/onChange verified completion`, async () => {
const f = fixture(disconnected(value)); await mount(); await click(byTest("chatgpt-login")); await click(toggle()); await click(byTest("chatgpt-proceed"));
assert.deepEqual(f.calls.consent, []); assert((byTest("chatgpt-proceed") as HTMLButtonElement).disabled);
await act(async () => f.complete()); assert.deepEqual(f.calls.consent, [!value]); assert.equal(f.status.allowBackgroundUse, !value);
assert(!document.querySelector('[data-testid="chatgpt-account-dialog"]'));
assert.equal(f.calls.login, 0); assert.deepEqual(f.calls.consent, []); assert.equal(f.calls.cancel, 0);
await click(byTest("chatgpt-login"));
assert.equal(document.querySelector('[role="switch"]')?.getAttribute("aria-checked"), "false", "each initial connection choice starts OFF");
await act(async () => f.push(f.status)); assert.deepEqual(f.calls.consent, [!value]);
});
test("only explicit proceed invokes OAuth; initial background opt-in waits for verified connection", async () => {
const f = fixture(); await mount(); await click(byTest("chatgpt-login"));
await click(document.querySelector<HTMLElement>('[role="switch"]')!);
assert.deepEqual(f.calls.consent, []);
await click(byTest("chatgpt-proceed"));
assert.equal(f.calls.login, 1); assert.deepEqual(f.calls.consent, [true]);
assert(!document.querySelector('[data-testid="chatgpt-account-dialog"]'));
assert(button("Manage…"));
test("Cancel after initial IPC promise ends cancels actual OAuth; late completion cannot apply draft or close another dialog", async () => {
const f = fixture(); await mount(); await click(byTest("chatgpt-login")); await click(toggle()); await click(byTest("chatgpt-proceed"));
await click(button("Cancel", byTest("chatgpt-account-dialog"))); assert.equal(f.calls.cancel, 1);
await click(byTest("chatgpt-login")); const dialog = byTest("chatgpt-account-dialog");
await act(async () => f.complete()); assert.deepEqual(f.calls.consent, []); assert(byTest("chatgpt-account-dialog") === dialog); assert.equal(f.calls.login.length, 1);
});
test("closing pending OAuth cancels and a late login result cannot apply draft consent", async () => {
const login = deferred<ChatGPTStatus>();
const f = fixture(disconnected(), { login: () => { f.calls.login++; return login.promise; } });
await mount(); await click(byTest("chatgpt-login")); await click(document.querySelector<HTMLElement>('[role="switch"]')!);
await click(byTest("chatgpt-proceed"));
await click(button("Cancel", byTest("chatgpt-account-dialog")));
assert.equal(f.calls.cancel, 1);
await act(async () => login.resolve(connected()));
assert.deepEqual(f.calls.consent, []);
assert(!button("Manage…"));
test("email-only connected selector and Manage remain responsive; account selection never changes global OFF", async () => {
const initial = connected(false); initial.accounts.push(account("second", "second@example.invalid"), account("missing", ""));
const f = fixture(initial); await mount();
assert.deepEqual([...document.querySelectorAll('select[aria-label="ChatGPT account"] option')].map((o) => o.textContent), ["learner@example.invalid", "second@example.invalid", "ChatGPT account"]);
assert.doesNotMatch(byTest("chatgpt-connection").textContent!, /private-registration|private-issued| · active|signed-out/);
assert.match(byTest("chatgpt-connection").firstElementChild!.className, /items-center/);
await selectAccount("ChatGPT account", "second"); assert.deepEqual(f.calls.select, ["second"]); assert.equal(f.status.allowBackgroundUse, false);
await click(button("Manage…")); assert(byTest("chatgpt-managed-identity").textContent?.includes("second@example.invalid"));
assert.deepEqual([...byTest("chatgpt-account-footer").querySelectorAll("button")].map((b) => b.textContent), ["Close"]);
const dialog = byTest("chatgpt-account-dialog").innerHTML;
assert(dialog.indexOf('data-testid="chatgpt-managed-identity"') < dialog.indexOf('data-testid="chatgpt-account-actions"'));
assert(dialog.indexOf('data-testid="chatgpt-account-actions"') < dialog.indexOf('data-testid="chatgpt-global-background"'));
await click(toggle()); assert.deepEqual(f.calls.consent, [true]); assert.equal(toggle().getAttribute("aria-checked"), "true");
await click(button("Close")); await selectAccount("ChatGPT account", "private-registration-id"); assert.equal(f.status.allowBackgroundUse, true);
assert.equal(f.calls.logout, 0);
});
test("connected account selector displays email only, excludes signed-out registrations, and retains safe fallback", async () => {
const status = connected(); status.accounts.push(account("second-id", "second@example.invalid"), account("missing-email", ""));
fixture(status); await mount();
const select = document.querySelector<HTMLSelectElement>('select[aria-label="ChatGPT account"]')!;
assert.deepEqual([...select.querySelectorAll("option")].map((o) => o.textContent), ["learner@example.invalid", "second@example.invalid", "ChatGPT account"]);
assert.doesNotMatch(document.body.textContent!, /private-registration-id|private-issued-client|second-id|signed-out@example| · active|registration /);
assert(!document.body.textContent?.includes(CHATGPT_NOTICE));
await click(button("Manage…"));
assert(document.body.textContent?.includes(CHATGPT_NOTICE));
assert(byTest("chatgpt-disconnect"));
test("Disconnect account is the sole secondary action under identity; confirmation Cancel never forgets; Close is only Manage footer action", async () => {
assert.deepEqual(CHATGPT_DISCONNECT_COPY, { title: "Disconnect account?", body: "This removes the saved account and its credentials from this device." });
const f = fixture(connected()); await mount(); await click(button("Manage…"));
assert.equal(byTest("chatgpt-account-actions").textContent, "Disconnect account"); assert.match(byTest("chatgpt-disconnect").className, /text-red/);
assert(!byTest("chatgpt-account-footer").contains(byTest("chatgpt-disconnect")));
await click(byTest("chatgpt-disconnect")); assert.equal(f.calls.disconnect.length, 0);
assert(byTest("chatgpt-disconnect-confirmation").textContent?.includes(CHATGPT_DISCONNECT_COPY.title));
assert(byTest("chatgpt-disconnect-confirmation").textContent?.includes(CHATGPT_DISCONNECT_COPY.body));
assert(!document.body.textContent?.includes("delete your OpenAI"));
await click(button("Cancel", byTest("chatgpt-disconnect-confirmation"))); assert(byTest("chatgpt-disconnect")); assert.deepEqual(f.calls.disconnect, []);
await click(button("Close")); assert(!document.querySelector('[data-testid="chatgpt-account-dialog"]'));
});
test("successful local disconnect removes action, resets dialog, and prevents repeated pending clicks without deleting registration UI state", async () => {
const logout = deferred<ChatGPTStatus>();
const f = fixture(connected(), { logout: () => { f.calls.logout++; return logout.promise; } });
await mount(); await click(button("Manage…"));
const disconnect = byTest("chatgpt-disconnect") as HTMLButtonElement;
await act(async () => { disconnect.click(); disconnect.click(); });
assert.equal(f.calls.logout, 1); assert.equal(disconnect.disabled, true);
await act(async () => logout.resolve(disconnected()));
assert(!document.querySelector('[data-testid="chatgpt-disconnect"]'));
assert(!document.querySelector('[data-testid="chatgpt-account-dialog"]'));
assert(byTest("chatgpt-login"));
assert(!document.body.textContent?.includes("Signed out locally"));
assert(!document.querySelector('[role="alert"]'));
await click(byTest("chatgpt-login"));
assert(document.body.textContent?.includes("signed-out@example.invalid"), "retained registration can still be explicitly reauthorized inside sign-in dialog");
test("confirmed disconnect preserves identity/pending content until outcome, prevents duplicate clicks, then closes without selecting another account", async () => {
const done = deferred<ChatGPTStatus>();
const f = fixture(connected(false), { disconnect: (id) => { f.calls.disconnect.push(id); return done.promise; } });
await mount(); await click(button("Manage…")); await confirmDisconnect();
await click(byTest("chatgpt-confirm-disconnect")); assert.deepEqual(f.calls.disconnect, ["private-registration-id"]);
const final = { ...disconnected(false), accounts: [account("other", "other@example.invalid")] };
await act(async () => f.push(final));
assert(byTest("chatgpt-disconnect-confirmation").textContent?.includes("learner@example.invalid"));
assert((byTest("chatgpt-confirm-disconnect") as HTMLButtonElement).disabled); assert.equal(byTest("chatgpt-account-dialog").getAttribute("aria-busy"), "true");
await act(async () => done.resolve(final));
assert(!document.querySelector('[data-testid="chatgpt-account-dialog"]')); assert(byTest("chatgpt-login")); assert.equal(f.status.activeId, null);
assert.equal(f.status.allowBackgroundUse, false); assert.equal(f.calls.logout, 0);
});
test("real unconfirmed revoke is a distinct contextual dismissible warning, not permanent global red error", async () => {
const warning = "Signed out locally. Remote revocation was not confirmed; disconnect LearnHarness in ChatGPT Settings.";
fixture(connected(), { logout: async () => ({ ...disconnected(), error: warning }) });
await mount(); await click(button("Manage…")); await click(byTest("chatgpt-disconnect"));
assert(byTest("chatgpt-login")); assert(!document.querySelector('[data-testid="chatgpt-disconnect"]'));
const alert = byTest("chatgpt-revocation-warning");
assert.equal(alert.textContent, warning); assert.doesNotMatch(alert.className, /text-red/);
await click(button("Dismiss", byTest("chatgpt-account-dialog")));
assert(!document.querySelector('[data-testid="chatgpt-revocation-warning"]'));
assert(!document.querySelector('[role="alert"]'));
test("signed-out legacy accounts remain manageable/forgettable without pretending selection is a usable connection", async () => {
const f = fixture(); await mount(); assert.equal(byTest("chatgpt-connection").textContent, "Continue with ChatGPT");
await click(byTest("chatgpt-login")); await selectAccount("Saved ChatGPT account", "signed-out-record"); await click(button("Manage…"));
assert(byTest("chatgpt-managed-identity").textContent?.includes("signed-out@example.invalid"));
assert.deepEqual(f.calls.select, []); assert.equal(f.calls.login.length, 0); await confirmDisconnect();
assert.deepEqual(f.calls.disconnect, ["signed-out-record"]); assert.equal(f.status.accounts.length, 0); assert.equal(f.status.allowBackgroundUse, true);
});
test("background model choice has contextual Enable action only while connected, selected, and consent OFF", async () => {
const f = fixture(connected());
await mount(<><ChatGPTBackgroundAccess provider="chatgpt" /><ChatGPTBackgroundAccess provider="openai" /><ChatGPTConnection /></>);
test("unconfirmed provider outcome is exactly the approved concise dismissible warning, never permanent red success/navigation advice", async () => {
const f = fixture(connected(), { disconnect: async () => { const result = { ...disconnected(), accounts: [], error: warning }; f.push(result); return result; } });
await mount(); await click(button("Manage…")); await confirmDisconnect();
const alert = byTest("chatgpt-revocation-warning"); assert.equal(alert.textContent, warning); assert.doesNotMatch(alert.className, /text-red/);
assert(!document.querySelector('[data-testid="chatgpt-disconnect"]')); assert(byTest("chatgpt-login"));
await click(button("Dismiss", byTest("chatgpt-account-dialog"))); assert(!document.querySelector('[role="alert"]'));
assert(!document.body.textContent?.includes("ChatGPT Settings"));
});
test("dismissed revocation warning stays dismissed across global setting changes but a new unconfirmed disconnect is shown", async () => {
const initial = connected(); initial.error = warning;
const f = fixture(initial, { disconnect: async () => { const next = { ...disconnected(false), accounts: [], error: warning }; f.push(next); return next; } });
await mount(); await click(button("Dismiss")); await click(button("Manage…")); await click(toggle());
assert.equal(toggle().getAttribute("aria-checked"), "false"); assert(!document.querySelector('[role="alert"]'));
await confirmDisconnect(); assert.equal(byTest("chatgpt-revocation-warning").textContent, warning);
});
test("contextual Enable action opens the same global control without silently enabling; no foreground/provider clutter", async () => {
const f = fixture(connected(false)); await mount(<><ChatGPTBackgroundAccess provider="chatgpt" /><ChatGPTBackgroundAccess provider="openai" /><ChatGPTConnection /></>);
assert.equal([...document.querySelectorAll("button")].filter((b) => b.textContent === "Enable background use").length, 1);
await click(button("Enable background use"));
assert.deepEqual(f.calls.consent, []);
await click(document.querySelector<HTMLElement>('[role="switch"]')!);
assert.deepEqual(f.calls.consent, [true]);
assert(!button("Enable background use"));
await act(async () => f.push(disconnected()));
assert(!button("Enable background use"));
await click(button("Enable background use")); assert.deepEqual(f.calls.consent, []);
assert.equal(toggle().getAttribute("aria-checked"), "false"); await click(toggle()); assert.deepEqual(f.calls.consent, [true]); assert(!button("Enable background use"));
await act(async () => f.push(disconnected(false))); assert(!button("Enable background use"));
});
test("browser-only sign-in is disabled without invoking OAuth", async () => {
await mount();
const trigger = byTest("chatgpt-login") as HTMLButtonElement;
assert(trigger.disabled); assert.match(trigger.className, /disabled:opacity/);
await click(trigger); assert(!document.querySelector('[data-testid="chatgpt-account-dialog"]'));
test("error-only disconnect reply preserves coherent status/confirmation and permits explicit retry, not false forgotten/connected success", async () => {
const f = fixture(connected(), { disconnect: async () => errorReply() }); await mount(); await click(button("Manage…"));
await assert.doesNotReject(confirmDisconnect); assert.equal(document.querySelector('[role="alert"]')?.textContent, storageError);
assert.equal((byTest("chatgpt-confirm-disconnect") as HTMLButtonElement).disabled, false); assert(byTest("chatgpt-disconnect-confirmation"));
f.bridge.disconnect = async () => { const next = { ...disconnected(), accounts: [] }; f.push(next); return next; };
await click(byTest("chatgpt-confirm-disconnect")); assert(!document.querySelector('[data-testid="chatgpt-account-dialog"]')); assert(byTest("chatgpt-login"));
});
test("foreground model/dictation toolbar has no plan badge or background-consent action", async () => {
test("error-only global consent reply keeps saved state visible and a recoverable error", async () => {
fixture(connected(), { consent: async () => errorReply() }); await mount(); await click(button("Manage…"));
await assert.doesNotReject(() => click(toggle())); assert.equal(toggle().getAttribute("aria-checked"), "true"); assert.equal(document.querySelector('[role="alert"]')?.textContent, storageError);
assert.equal((toggle() as HTMLButtonElement).disabled, false);
});
test("error-only login reply releases its operation without corrupting status or auto-retrying", async () => {
let calls = 0; fixture(disconnected(), { login: async () => { calls++; return errorReply(); } }); await mount(); await assert.doesNotReject(begin);
assert.equal(document.querySelector('[role="alert"]')?.textContent, storageError); assert.equal((byTest("chatgpt-proceed") as HTMLButtonElement).disabled, false); assert.equal(calls, 1);
});
test("error-only initial global draft save leaves disclosure/error and duplicate completion cannot retry the setting", async () => {
let calls = 0; const f = fixture(disconnected(), { consent: async () => { calls++; return errorReply(); } });
await mount(); await click(byTest("chatgpt-login")); await click(toggle()); await click(byTest("chatgpt-proceed")); await act(async () => f.complete());
assert.equal(document.querySelector('[role="alert"]')?.textContent, storageError); assert(byTest("chatgpt-account-dialog"));
await act(async () => f.push(f.status)); assert.equal(calls, 1); assert.equal(f.calls.login.length, 1);
});
test("error-only pending cancellation remains retryable and cannot apply a global draft", async () => {
const f = fixture(disconnected(), { cancel: async () => { f.calls.cancel++; return errorReply(); } });
await mount(); await begin(); await click(button("Cancel", byTest("chatgpt-account-dialog")));
assert.equal(document.querySelector('[role="alert"]')?.textContent, storageError); assert(byTest("chatgpt-account-dialog"));
f.bridge.cancel = async () => { f.calls.cancel++; const next = { ...f.status, pending: false }; f.push(next); return next; };
await click(button("Cancel", byTest("chatgpt-account-dialog"))); assert.equal(f.calls.cancel, 2); assert.deepEqual(f.calls.consent, []);
assert(!document.querySelector('[data-testid="chatgpt-account-dialog"]'));
});
test("failed pending verification never applies draft/starts login/closes disclosure on a later clean status", async () => {
const f = fixture(); await mount(); await click(byTest("chatgpt-login")); await click(toggle()); await click(byTest("chatgpt-proceed"));
await act(async () => f.push({ ...connected(), error: "ChatGPT sign-in could not be verified." }));
await act(async () => f.push(connected())); assert.deepEqual(f.calls.consent, []); assert.equal(f.calls.login.length, 1); assert(byTest("chatgpt-account-dialog"));
});
test("saved-registration mismatch cannot complete the attempt or apply its global draft", async () => {
const f = fixture(); await mount(); await click(byTest("chatgpt-login")); await selectAccount("Saved ChatGPT account", "signed-out-record");
await click(toggle()); await click(byTest("chatgpt-proceed")); await act(async () => f.complete());
assert.deepEqual(f.calls.consent, []); assert.deepEqual(f.calls.login, ["signed-out-record"]); assert(document.querySelector('[role="alert"]')); assert(byTest("chatgpt-account-dialog"));
});
test("account invalidation while initial global consent awaits IPC fences stale reply and preserves an unrelated later dialog", async () => {
const held = deferred<ChatGPTStatus>(); const f = fixture(disconnected(), { consent: () => held.promise });
await mount(); await click(byTest("chatgpt-login")); await click(toggle()); await click(byTest("chatgpt-proceed")); await act(async () => f.complete());
const other = { ...connected(), activeId: "other", accounts: [account("other", "other@example.invalid")] }; await act(async () => f.push(other));
await click(button("Cancel", byTest("chatgpt-account-dialog"))); await click(button("Manage…")); const dialog = byTest("chatgpt-account-dialog");
await act(async () => held.resolve(connected(false))); assert(byTest("chatgpt-account-dialog") === dialog);
assert(dialog.textContent?.includes("other@example.invalid")); assert.equal(toggle().getAttribute("aria-checked"), "true");
});
test("late global-control reply cannot overwrite a newer authoritative global value or leave control stuck", async () => {
const held = deferred<ChatGPTStatus>(); const f = fixture(connected(), { consent: () => held.promise });
await mount(); await click(button("Manage…")); await click(toggle());
await act(async () => { f.push(connected(false)); f.push(connected(true)); });
await act(async () => held.resolve(connected(false)));
assert.equal(toggle().getAttribute("aria-checked"), "true"); assert.equal((toggle() as HTMLButtonElement).disabled, false);
await click(button("Close")); assert(!document.querySelector('[data-testid="chatgpt-account-dialog"]'));
});
test("late initial global draft reply cannot resurrect an older global value after a newer authoritative setting", async () => {
const held = deferred<ChatGPTStatus>(); const f = fixture(disconnected(), { consent: () => held.promise });
await mount(<><ChatGPTConnection /><ChatGPTBackgroundAccess provider="chatgpt" /></>);
await click(byTest("chatgpt-login")); await click(toggle()); await click(byTest("chatgpt-proceed")); await act(async () => f.complete());
await act(async () => { f.push(connected(false)); f.push(connected(true)); });
await act(async () => held.resolve(connected(false)));
assert(!button("Enable background use")); assert(document.querySelector('[role="alert"]')); assert.equal((byTest("chatgpt-proceed") as HTMLButtonElement).disabled, false);
});
test("coalesced verified completion before late pending acknowledgement is reconciled once from fresh status", async () => {
const ack = deferred<ChatGPTStatus>(); const f = fixture(disconnected(), { login: () => { f.calls.login.push(undefined); return ack.promise; } });
await mount(); await click(byTest("chatgpt-login")); await click(toggle()); await click(byTest("chatgpt-proceed"));
await act(async () => f.complete()); assert.deepEqual(f.calls.consent, []);
await act(async () => ack.resolve({ ...disconnected(), pending: true })); assert.deepEqual(f.calls.consent, [false]); assert(!document.querySelector('[data-testid="chatgpt-account-dialog"]'));
});
test("canceled attempt's late reply cannot release a newer pending attempt or apply its old draft", async () => {
const old = deferred<ChatGPTStatus>(); const f = fixture(disconnected(), { login: async () => { f.calls.login.push(undefined); if (f.calls.login.length === 1) return old.promise; f.push({ ...f.status, pending: true }); return f.status; } });
await mount(); await click(byTest("chatgpt-login")); await click(toggle()); await click(byTest("chatgpt-proceed"));
await click(button("Cancel", byTest("chatgpt-account-dialog"))); await begin(); await act(async () => old.resolve(connected(false)));
assert((byTest("chatgpt-proceed") as HTMLButtonElement).disabled); assert.deepEqual(f.calls.consent, []);
await act(async () => f.complete()); assert.deepEqual(f.calls.consent, []); assert.equal(f.status.allowBackgroundUse, true); assert(!document.querySelector('[data-testid="chatgpt-account-dialog"]'));
});
test("real main/control bridge: immediate pending, actual cancellation, verified callback, then explicit global draft persistence", async () => {
const require = createRequire(import.meta.url); const { createChatGPT, SCOPES } = require("../electron/chatgpt.cjs"); const { chatGPTControl } = require("../electron/chatgpt-ipc.cjs");
const { privateKey, publicKey } = await generateKeyPair("RS256"); const jwk = { ...await exportJWK(publicKey), kid: "ui-fixture", alg: "RS256", use: "sig" };
let saved: any = null, authorize!: URL, tokenCalls = 0; const listeners = new Set<() => void>();
const service = createChatGPT({ store: { secure() {}, async read() { return saved; }, async write(s: any) { saved = structuredClone(s); } }, openExternal: async (url: string) => { authorize = new URL(url); }, onChange: () => { for (const cb of listeners) cb(); }, fetch: async (url: string, init: RequestInit) => {
if (url.endsWith("jwks.json")) return Response.json({ keys: [jwk] });
assert.equal(url, "https://auth.openai.com/api/accounts/oauth/token"); tokenCalls++;
const form = init.body as URLSearchParams;
const jwt = await new SignJWT({ nonce: authorize.searchParams.get("nonce"), email: "fixture@example.invalid" }).setProtectedHeader({ alg: "RS256", kid: "ui-fixture" }).setIssuer("https://auth.openai.com").setSubject("fixture-subject").setAudience(form.get("client_id")!).setIssuedAt().setExpirationTime("1h").sign(privateKey);
return Response.json({ access_token: "fixture-access", refresh_token: "fixture-refresh", id_token: jwt, token_type: "Bearer", expires_in: 3600, scope: SCOPES.join(" ") });
} });
window.lh = { expandWindow() {}, chatgpt: { status: () => chatGPTControl({ op: "status" }, service), login: (id) => chatGPTControl({ op: "login", id }, service), cancel: () => chatGPTControl({ op: "cancel" }, service), select: (id) => chatGPTControl({ op: "select", id }, service), disconnect: (id) => chatGPTControl({ op: "disconnect", id }, service), logout: (id) => chatGPTControl({ op: "logout", id }, service), consent: (enabled) => chatGPTControl({ op: "consent", enabled }, service), onChange: (cb) => { listeners.add(cb); return () => listeners.delete(cb); } } };
try {
await mount(); await begin(); assert.equal((await service.status()).pending, true); await click(button("Cancel", byTest("chatgpt-account-dialog"))); assert.equal((await service.status()).pending, false); assert.equal(tokenCalls, 0);
await click(byTest("chatgpt-login")); await click(toggle()); await click(byTest("chatgpt-proceed"));
const callback = new URL(authorize.searchParams.get("redirect_uri")!); callback.search = new URLSearchParams({ state: authorize.searchParams.get("state")!, code: "fixture-code", client_id: "oaiapp_ui_fixture" }).toString();
await act(async () => { const response = await originalFetch(callback); await response.text(); for (let i = 0; i < 100 && (saved.allowBackgroundUse !== false || (await service.status()).pending); i++) await new Promise((r) => setImmediate(r)); });
assert.equal(tokenCalls, 1); assert.equal(saved.allowBackgroundUse, false); assert.equal(saved.registrations[0].background, undefined);
assert(!document.querySelector('[data-testid="chatgpt-account-dialog"]')); assert(button("Manage…"));
} finally { await act(async () => service.close()); }
});
test("browser-only sign-in is disabled; foreground toolbar has no plan badge or background action", async () => {
await mount(); assert((byTest("chatgpt-login") as HTMLButtonElement).disabled); await click(byTest("chatgpt-login")); assert(!document.querySelector('[data-testid="chatgpt-account-dialog"]'));
await act(async () => root!.unmount()); host?.remove(); root = undefined;
globalThis.fetch = async (input) => {
if (input === "/api/models") return Response.json({ models: [], providers: [], slots: {}, defaults: {} });
if (input === "/api/settings") return Response.json({ settings: { models: { strong: { model: { provider: "chatgpt", id: "gpt-5" } } } } });
throw new Error(`unexpected fixture request ${input}`);
};
fixture(connected()); await mount(<ModelControl />);
assert.match(byTest("stats-model").textContent!, /gpt/i);
assert(!document.body.textContent?.includes("Using ChatGPT plan"));
assert(!document.body.textContent?.includes("Enable background use"));
assert(!document.querySelector('a[href="https://chatgpt.com/settings/usage"]'));
});
const storageError = "ChatGPT credentials could not be saved securely. Reconnect before making requests.";
const errorReply = () => ({ error: storageError } as ChatGPTStatus); // Actual IPC catch envelope, deliberately not a snapshot.
function pendingFixture(initial = disconnected(), overrides: Partial<ChatGPTControlBridge> = {}) {
const f = fixture(initial, { login: async () => { f.calls.login++; const pending = { ...initial, pending: true }; f.push(pending); return pending; }, ...overrides });
return f;
}
test("production lifecycle: Cancel after the immediate pending reply cancels OAuth; later events cannot apply the old choice or close a new dialog", async () => {
const f = pendingFixture(); await mount(); await click(byTest("chatgpt-login"));
await click(document.querySelector<HTMLElement>('[role="switch"]')!); await click(byTest("chatgpt-proceed"));
assert(document.body.textContent?.includes("Waiting for browser sign-in"));
await click(button("Cancel", byTest("chatgpt-account-dialog"))); assert.equal(f.calls.cancel, 1);
await click(byTest("chatgpt-login")); const nextDialog = byTest("chatgpt-account-dialog");
await act(async () => f.push(connected(false)));
assert.deepEqual(f.calls.consent, []); assert.equal(f.calls.login, 1); assert.equal(byTest("chatgpt-account-dialog"), nextDialog);
});
test("production lifecycle: verified onChange applies explicit ON once to the completed account and closes only its disclosure", async () => {
const f = pendingFixture(); await mount(); await click(byTest("chatgpt-login"));
await click(document.querySelector<HTMLElement>('[role="switch"]')!); await click(byTest("chatgpt-proceed"));
assert.equal(f.calls.login, 1); assert.deepEqual(f.calls.consent, []);
assert((byTest("chatgpt-proceed") as HTMLButtonElement).disabled);
await act(async () => f.push(connected(false)));
assert.deepEqual(f.calls.consent, [true]); assert.deepEqual(f.calls.consentIds, ["private-registration-id"]);
assert(!document.querySelector('[data-testid="chatgpt-account-dialog"]'));
await act(async () => f.push(connected(true))); assert.deepEqual(f.calls.consent, [true]);
});
test("production lifecycle: initial OFF revokes retained ON only after verified returning-account completion", async () => {
const initial = connected(true); initial.activeId = null; initial.accounts[0]!.connected = false;
const f = pendingFixture(initial); await mount(); await click(byTest("chatgpt-login"));
assert.equal(document.querySelector('[role="switch"]')?.getAttribute("aria-checked"), "false");
await click(byTest("chatgpt-proceed")); assert.deepEqual(f.calls.consent, []);
await act(async () => f.push(connected(true)));
assert.deepEqual(f.calls.consent, [false]); assert.deepEqual(f.calls.consentIds, ["private-registration-id"]);
assert(!document.querySelector('[data-testid="chatgpt-account-dialog"]'));
});
test("error-only logout reply preserves valid status, shows recoverable error, never claims disconnect, and permits explicit retry", async () => {
const f = fixture(connected(), { logout: async () => { f.calls.logout++; return errorReply(); } });
await mount(); await click(button("Manage…"));
await assert.doesNotReject(() => click(byTest("chatgpt-disconnect")));
assert.equal(document.querySelector('[role="alert"]')?.textContent, storageError);
assert.equal((byTest("chatgpt-disconnect") as HTMLButtonElement).disabled, false);
assert.equal(document.querySelector('select[aria-label="ChatGPT account"] option')?.textContent, "learner@example.invalid");
assert(!document.querySelector('[data-testid="chatgpt-login"]')); assert(!document.body.textContent?.includes("Signed out locally"));
f.bridge.logout = async () => { f.calls.logout++; return disconnected(); };
await click(byTest("chatgpt-disconnect")); assert.equal(f.calls.logout, 2);
assert(byTest("chatgpt-login")); assert(!document.querySelector('[data-testid="chatgpt-disconnect"]'));
});
test("error-only consent reply leaves the saved toggle unchanged with a contextual error", async () => {
fixture(connected(), { consent: async () => errorReply() }); await mount(); await click(button("Manage…"));
await assert.doesNotReject(() => click(document.querySelector<HTMLElement>('[role="switch"]')!));
assert.equal(document.querySelector('[role="switch"]')?.getAttribute("aria-checked"), "false");
assert.equal(document.querySelector('[role="alert"]')?.textContent, storageError); assert(byTest("chatgpt-disconnect"));
});
test("error-only initial login reply is recoverable without corrupting status or automatically retrying", async () => {
const f = fixture(disconnected(), { login: async () => { f.calls.login++; return errorReply(); } });
await mount(); await click(byTest("chatgpt-login")); await assert.doesNotReject(() => click(byTest("chatgpt-proceed")));
assert.equal(document.querySelector('[role="alert"]')?.textContent, storageError);
assert.equal((byTest("chatgpt-proceed") as HTMLButtonElement).disabled, false); assert.equal(f.calls.login, 1);
assert.deepEqual(f.calls.consent, []);
});
test("error-only consent after verified login keeps disclosure/error open and cannot be retried by a duplicate completion push", async () => {
let consentCalls = 0;
const f = pendingFixture(disconnected(), { consent: async () => { consentCalls++; return errorReply(); } });
await mount(); await click(byTest("chatgpt-login")); await click(document.querySelector<HTMLElement>('[role="switch"]')!); await click(byTest("chatgpt-proceed"));
await act(async () => f.push(connected(false)));
assert.equal(document.querySelector('[role="alert"]')?.textContent, storageError); assert(byTest("chatgpt-account-dialog"));
await act(async () => f.push(connected(false))); assert.equal(consentCalls, 1); assert.equal(f.calls.login, 1);
});
test("failed pending attempt never applies draft consent, starts another login, or closes disclosure on a later clean status", async () => {
const f = pendingFixture(); await mount(); await click(byTest("chatgpt-login")); await click(document.querySelector<HTMLElement>('[role="switch"]')!); await click(byTest("chatgpt-proceed"));
await act(async () => f.push({ ...connected(false), error: "ChatGPT sign-in could not be verified." }));
assert.equal(document.querySelector('[role="alert"]')?.textContent, "ChatGPT sign-in could not be verified.");
await act(async () => f.push(connected(false)));
assert.deepEqual(f.calls.consent, []); assert.equal(f.calls.login, 1); assert(byTest("chatgpt-account-dialog"));
});
test("saved-registration attempt cannot apply its choice to a different active account", async () => {
const f = pendingFixture(); await mount(); await click(byTest("chatgpt-login"));
const select = document.querySelector<HTMLSelectElement>('select[aria-label="Saved ChatGPT account"]')!;
await act(async () => { select.querySelector<HTMLOptionElement>('option[value="signed-out-record"]')!.selected = true; select.dispatchEvent(new window.Event("change", { bubbles: true })); });
await click(document.querySelector<HTMLElement>('[role="switch"]')!); await click(byTest("chatgpt-proceed"));
await act(async () => f.push(connected(false)));
assert.deepEqual(f.calls.consent, []); assert(byTest("chatgpt-account-dialog")); assert(document.querySelector('[role="alert"]'));
});
test("account invalidation while initial consent is awaiting IPC fences its stale reply and preserves an unrelated later Manage dialog", async () => {
const consent = deferred<ChatGPTStatus>();
const f = pendingFixture(disconnected(), { consent: () => consent.promise });
await mount(); await click(byTest("chatgpt-login")); await click(document.querySelector<HTMLElement>('[role="switch"]')!); await click(byTest("chatgpt-proceed"));
await act(async () => f.push(connected(false)));
const other = { ...connected(), activeId: "other", accounts: [account("other", "other@example.invalid")] };
await act(async () => f.push(other));
await click(button("Cancel", byTest("chatgpt-account-dialog"))); await click(button("Manage…")); const manage = byTest("chatgpt-account-dialog");
await act(async () => consent.resolve(connected(true)));
assert.equal(byTest("chatgpt-account-dialog"), manage); assert(manage.textContent?.includes("other@example.invalid"));
assert.equal(document.querySelector('select[aria-label="ChatGPT account"] option')?.textContent, "other@example.invalid");
});
test("verified event before the initial IPC acknowledgement is handled once; the late pending reply cannot reopen or overwrite it", async () => {
const ack = deferred<ChatGPTStatus>();
const f = fixture(disconnected(), { login: () => { f.calls.login++; return ack.promise; } });
await mount(); await click(byTest("chatgpt-login")); await click(byTest("chatgpt-proceed"));
await act(async () => f.push({ ...disconnected(), pending: true }));
await act(async () => f.push(connected(false)));
assert(!document.querySelector('[data-testid="chatgpt-account-dialog"]'));
await act(async () => ack.resolve({ ...disconnected(), pending: true }));
assert(!document.querySelector('[data-testid="chatgpt-account-dialog"]')); assert(button("Manage…")); assert.equal(f.calls.login, 1);
});
test("error-only pending cancellation reply remains visible and recoverable without replacing status", async () => {
const f = pendingFixture(disconnected(), { cancel: async () => { f.calls.cancel++; return errorReply(); } });
await mount(); await click(byTest("chatgpt-login")); await click(byTest("chatgpt-proceed"));
await assert.doesNotReject(() => click(button("Cancel", byTest("chatgpt-account-dialog"))));
assert.equal(f.calls.cancel, 1); assert.equal(document.querySelector('[role="alert"]')?.textContent, storageError);
assert(document.querySelector('[data-testid="chatgpt-connection"]')); assert.deepEqual(f.calls.consent, []);
assert(byTest("chatgpt-account-dialog"));
f.bridge.cancel = async () => { f.calls.cancel++; return disconnected(); };
await click(button("Cancel", byTest("chatgpt-account-dialog")));
assert.equal(f.calls.cancel, 2); assert(!document.querySelector('[data-testid="chatgpt-account-dialog"]'));
});
test("coalesced completion before the pending acknowledgement is reconciled from fresh post-ack status", async () => {
const ack = deferred<ChatGPTStatus>();
const f = fixture(disconnected(), { login: () => { f.calls.login++; return ack.promise; } });
await mount(); await click(byTest("chatgpt-login")); await click(document.querySelector<HTMLElement>('[role="switch"]')!); await click(byTest("chatgpt-proceed"));
// A status reader can miss pending entirely when two notifications coalesce.
await act(async () => f.push(connected(false))); assert.deepEqual(f.calls.consent, []);
await act(async () => ack.resolve({ ...disconnected(), pending: true }));
assert.deepEqual(f.calls.consent, [true]); assert.deepEqual(f.calls.consentIds, ["private-registration-id"]);
assert(!document.querySelector('[data-testid="chatgpt-account-dialog"]'));
});
test("a canceled attempt's late IPC reply cannot release a newer pending attempt or apply its old ON choice", async () => {
const oldAck = deferred<ChatGPTStatus>();
const f = fixture(disconnected(), { login: async () => {
f.calls.login++;
if (f.calls.login === 1) return oldAck.promise;
const pending = { ...disconnected(), pending: true }; f.push(pending); return pending;
} });
await mount(); await click(byTest("chatgpt-login")); await click(document.querySelector<HTMLElement>('[role="switch"]')!); await click(byTest("chatgpt-proceed"));
await act(async () => f.push({ ...disconnected(), pending: true })); await click(button("Cancel", byTest("chatgpt-account-dialog")));
await click(byTest("chatgpt-login")); await click(byTest("chatgpt-proceed"));
await act(async () => oldAck.resolve(connected(false)));
assert((byTest("chatgpt-proceed") as HTMLButtonElement).disabled); assert.deepEqual(f.calls.consent, []);
const next = { ...connected(), activeId: "new-attempt-account", accounts: [account("new-attempt-account", "new@example.invalid", true, true)] };
await act(async () => f.push(next));
assert.deepEqual(f.calls.consent, [false]); assert.deepEqual(f.calls.consentIds, ["new-attempt-account"]);
assert(!document.querySelector('[data-testid="chatgpt-account-dialog"]')); assert.equal(f.calls.login, 2);
fixture(connected(false)); await mount(<ModelControl />); assert.match(byTest("stats-model").textContent!, /gpt/i);
assert(!document.body.textContent?.includes("Using ChatGPT plan")); assert(!document.body.textContent?.includes("Enable background use"));
});
+1 -1
View File
@@ -24,7 +24,7 @@ const model = piModels().getModel("openai", "gpt-5") as Model<"openai-responses"
let epoch = 1, revision = 1, background = true;
let cut: "lease" | "payload" | "full" | "afterFetch" | "none" = "none";
let leases: boolean[] = [], fetches = 0, toolFirst = false;
const status = () => ({ available: true, pending: false, activeId: "scratch", accounts: [{ id: "scratch", registration: "oaiapp_scratch", email: "dummy@example.invalid", connected: true, planEnabled: true, background }], error: null });
const status = () => ({ available: true, pending: false, allowBackgroundUse: background, activeId: "scratch", accounts: [{ id: "scratch", registration: "oaiapp_scratch", email: "dummy@example.invalid", connected: true, planEnabled: true, background: !background }], error: null });
const ipc = (m: unknown) => (process.emit as (name: string, m: unknown) => boolean)("message", m);
function revoke(full = false) {
background = false; if (full) epoch++;
+1 -1
View File
@@ -15,7 +15,7 @@ let requests: { authorization: string | null; body: Record<string, any>; url: st
let responseEvents: unknown[] = [];
const ipc = (value: unknown) => (process.emit as (name: string, value: unknown) => boolean)("message", value);
const model = piModels().getModel("openai", "gpt-5") as Model<"openai-responses">;
const status = { available: true, pending: false, activeId: "scratch-account", accounts: [{ id: "scratch-account", email: "dummy@example.invalid", registration: "oaiapp_scratch", connected: true, planEnabled: true, background: false }], error: null };
const status = { available: true, pending: false, allowBackgroundUse: false, activeId: "scratch-account", accounts: [{ id: "scratch-account", email: "dummy@example.invalid", registration: "oaiapp_scratch", connected: true, planEnabled: true, background: false }], error: null };
before(async () => {
assert(model);
process.env.OPENAI_API_KEY = "sk-scratch-must-never-be-used";
+2 -2
View File
@@ -63,8 +63,8 @@ test("plan plus unknown graph rows keep the existing unknown suffix without fabr
test("root-lifetime safe bridge publishes with Connections unmounted; commands/panel pushes do not duplicate global signals", async () => {
let pushes: (() => void)[] = [], subscriptions = 0, unsubscriptions = 0, fetches = 0;
const safe: ChatGPTStatus = { available: true, pending: false, activeId: "a", accounts: [{ id: "a", email: "dummy@example.invalid", registration: "oaiapp_dummy", connected: true, background: false, planEnabled: true }], error: null };
window.lh = { expandWindow() {}, chatgpt: { status: async () => safe, login: async () => safe, cancel: async () => safe, select: async () => safe, logout: async () => safe, consent: async () => safe, onChange: (cb) => { subscriptions++; pushes.push(cb); return () => { unsubscriptions++; pushes = pushes.filter((c) => c !== cb); }; } } };
const safe: ChatGPTStatus = { available: true, pending: false, allowBackgroundUse: false, activeId: "a", accounts: [{ id: "a", email: "dummy@example.invalid", registration: "oaiapp_dummy", connected: true, planEnabled: true }], error: null };
window.lh = { expandWindow() {}, chatgpt: { status: async () => safe, login: async () => safe, cancel: async () => safe, select: async () => safe, disconnect: async () => safe, logout: async () => safe, consent: async () => safe, onChange: (cb) => { subscriptions++; pushes.push(cb); return () => { unsubscriptions++; pushes = pushes.filter((c) => c !== cb); }; } } };
globalThis.fetch = async (input) => { assert.equal(input, "/api/models"); fetches++; return Response.json(body(`model-${fetches}`)); };
await mount(<SidebarProvider><Reader id="root" /></SidebarProvider>);
assert.equal(subscriptions, 1);
+10
View File
@@ -69,6 +69,16 @@ test("all methods fail closed for desktop missing/wrong secret; API-only matcher
}
});
test("renderer controls route explicit account forgetting and account-independent boolean consent only", async () => {
const forgotten: string[] = [], changes: boolean[] = [];
const connection = { disconnect: async (id: string) => { forgotten.push(id); return { ok: true }; }, consent: async (enabled: boolean) => { changes.push(enabled); return { ok: true }; } };
assert.deepEqual(await chatGPTControl({ op: "disconnect", id: "fixture-account" }, connection), { ok: true });
assert.deepEqual(await chatGPTControl({ op: "consent", enabled: false }, connection), { ok: true });
assert.deepEqual(forgotten, ["fixture-account"]); assert.deepEqual(changes, [false]);
for (const command of [{ op: "disconnect" }, { op: "disconnect", id: 7 }, { op: "consent", enabled: "false" }]) assert.deepEqual(await chatGPTControl(command, connection), { error: "Unsupported ChatGPT control." });
assert.deepEqual(changes, [false]);
});
test("renderer controls have no lease or general token bridge, and require the trusted main frame", async () => {
let leased = false;
const connection = { lease: async () => { leased = true; return { accessToken: "scratch-private-token" }; } };
+2 -2
View File
@@ -41,7 +41,7 @@ describe("SettingsDialog (the designed surface, SSR)", () => {
assert.ok(!html.includes("settings-dialog"));
});
it("the dialog frame: role=dialog + the ✕ + the six-section nav, Model providers first and active", () => {
it("the dialog frame: role=dialog + the ✕ + the six-section nav, Providers first and General last", () => {
const html = renderToStaticMarkup(<SettingsDialogBody onClose={() => {}} />);
assert.ok(html.includes('role="dialog"'));
assert.ok(html.includes('aria-modal="true"'));
@@ -49,7 +49,7 @@ describe("SettingsDialog (the designed surface, SSR)", () => {
for (const id of ["general", "keys", "agent", "appearance", "graph", "keyboard"]) {
assert.ok(html.includes(`data-testid="settings-nav-${id}"`), id);
}
// Model providers is the first landing section; internal keys deep links survive.
// Providers is the first landing section; internal keys deep links survive.
assert.ok(tagOf(html, "settings-nav-keys").includes('aria-current="true"'));
assert.ok(html.indexOf('data-testid="settings-nav-keys"') < html.indexOf('data-testid="settings-nav-general"'));
assert.ok(tagOf(html, "settings-nav-keyboard").includes('aria-current="false"'));