diff --git a/src/commands/doctor.warns-state-directory-is-missing.e2e.test.ts b/src/commands/doctor.warns-state-directory-is-missing.e2e.test.ts index 4295dc739d4..9ddf5b1a644 100644 --- a/src/commands/doctor.warns-state-directory-is-missing.e2e.test.ts +++ b/src/commands/doctor.warns-state-directory-is-missing.e2e.test.ts @@ -12,6 +12,75 @@ import "./doctor.fast-path-mocks.js"; let doctorCommand: typeof import("./doctor.js").doctorCommand; +const CODEX_PROVIDER_ID = "openai-codex"; +const CODEX_PROFILE_ID = "openai-codex:user@example.com"; +const CODEX_PROFILE_EMAIL = "user@example.com"; + +function configCodexOAuthProfile() { + return { + provider: CODEX_PROVIDER_ID, + mode: "oauth", + email: CODEX_PROFILE_EMAIL, + }; +} + +function storedCodexOAuthProfile() { + return { + type: "oauth", + provider: CODEX_PROVIDER_ID, + access: "access-token", + refresh: "refresh-token", + expires: Date.now() + 60_000, + email: CODEX_PROFILE_EMAIL, + }; +} + +function mockAuthProfileStore(profiles: Record = {}): void { + ensureAuthProfileStore.mockReturnValue({ + version: 1, + profiles, + }); +} + +function mockCodexProviderSnapshot(params: { + provider: Record; + withConfigOAuth?: boolean; +}): void { + mockDoctorConfigSnapshot({ + config: { + models: { + providers: { + [CODEX_PROVIDER_ID]: params.provider, + }, + }, + ...(params.withConfigOAuth + ? { + auth: { + profiles: { + [CODEX_PROFILE_ID]: configCodexOAuthProfile(), + }, + }, + } + : {}), + }, + }); +} + +async function runDoctorNonInteractive(): Promise { + await doctorCommand(createDoctorRuntime(), { + nonInteractive: true, + workspaceSuggestions: false, + }); +} + +function hasCodexOAuthWarning(messageIncludes?: string): boolean { + return terminalNoteMock.mock.calls.some( + ([message, title]) => + title === "Codex OAuth" && + (messageIncludes === undefined || String(message).includes(messageIncludes)), + ); +} + describe("doctor command", () => { beforeEach(async () => { doctorCommand = await loadDoctorCommandForTest({ @@ -70,223 +139,98 @@ describe("doctor command", () => { }); it("warns when a legacy openai-codex provider override shadows configured Codex OAuth", async () => { - mockDoctorConfigSnapshot({ - config: { - models: { - providers: { - "openai-codex": { - api: "openai-responses", - baseUrl: "https://api.openai.com/v1", - }, - }, - }, - auth: { - profiles: { - "openai-codex:user@example.com": { - provider: "openai-codex", - mode: "oauth", - email: "user@example.com", - }, - }, - }, + mockCodexProviderSnapshot({ + provider: { + api: "openai-responses", + baseUrl: "https://api.openai.com/v1", }, + withConfigOAuth: true, }); - ensureAuthProfileStore.mockReturnValue({ - version: 1, - profiles: {}, - }); + mockAuthProfileStore(); - await doctorCommand(createDoctorRuntime(), { - nonInteractive: true, - workspaceSuggestions: false, - }); + await runDoctorNonInteractive(); - const warned = terminalNoteMock.mock.calls.some( - ([message, title]) => - title === "Codex OAuth" && String(message).includes("models.providers.openai-codex"), - ); - expect(warned).toBe(true); + expect(hasCodexOAuthWarning("models.providers.openai-codex")).toBe(true); }); it("warns when a legacy openai-codex provider override shadows stored Codex OAuth", async () => { - mockDoctorConfigSnapshot({ - config: { - models: { - providers: { - "openai-codex": { - api: "openai-responses", - baseUrl: "https://api.openai.com/v1", - }, - }, - }, + mockCodexProviderSnapshot({ + provider: { + api: "openai-responses", + baseUrl: "https://api.openai.com/v1", }, }); - ensureAuthProfileStore.mockReturnValue({ - version: 1, - profiles: { - "openai-codex:user@example.com": { - type: "oauth", - provider: "openai-codex", - access: "access-token", - refresh: "refresh-token", - expires: Date.now() + 60_000, - email: "user@example.com", - }, - }, + mockAuthProfileStore({ + [CODEX_PROFILE_ID]: storedCodexOAuthProfile(), }); - await doctorCommand(createDoctorRuntime(), { - nonInteractive: true, - workspaceSuggestions: false, - }); + await runDoctorNonInteractive(); - const warned = terminalNoteMock.mock.calls.some( - ([message, title]) => - title === "Codex OAuth" && String(message).includes("models.providers.openai-codex"), - ); - expect(warned).toBe(true); + expect(hasCodexOAuthWarning("models.providers.openai-codex")).toBe(true); }); it("warns when an inline openai-codex model keeps the legacy OpenAI transport", async () => { - mockDoctorConfigSnapshot({ - config: { - models: { - providers: { - "openai-codex": { - models: [ - { - id: "gpt-5.4", - api: "openai-responses", - }, - ], - }, + mockCodexProviderSnapshot({ + provider: { + models: [ + { + id: "gpt-5.4", + api: "openai-responses", }, - }, - auth: { - profiles: { - "openai-codex:user@example.com": { - provider: "openai-codex", - mode: "oauth", - email: "user@example.com", - }, - }, - }, + ], }, + withConfigOAuth: true, }); - ensureAuthProfileStore.mockReturnValue({ - version: 1, - profiles: {}, - }); + mockAuthProfileStore(); - await doctorCommand(createDoctorRuntime(), { - nonInteractive: true, - workspaceSuggestions: false, - }); + await runDoctorNonInteractive(); - const warned = terminalNoteMock.mock.calls.some( - ([message, title]) => - title === "Codex OAuth" && String(message).includes("legacy transport override"), - ); - expect(warned).toBe(true); + expect(hasCodexOAuthWarning("legacy transport override")).toBe(true); }); it("does not warn for a custom openai-codex proxy override", async () => { - mockDoctorConfigSnapshot({ - config: { - models: { - providers: { - "openai-codex": { - api: "openai-responses", - baseUrl: "https://custom.example.com", - }, - }, - }, - auth: { - profiles: { - "openai-codex:user@example.com": { - provider: "openai-codex", - mode: "oauth", - email: "user@example.com", - }, - }, - }, + mockCodexProviderSnapshot({ + provider: { + api: "openai-responses", + baseUrl: "https://custom.example.com", }, + withConfigOAuth: true, }); - ensureAuthProfileStore.mockReturnValue({ - version: 1, - profiles: {}, - }); + mockAuthProfileStore(); - await doctorCommand(createDoctorRuntime(), { - nonInteractive: true, - workspaceSuggestions: false, - }); + await runDoctorNonInteractive(); - const warned = terminalNoteMock.mock.calls.some(([, title]) => title === "Codex OAuth"); - expect(warned).toBe(false); + expect(hasCodexOAuthWarning()).toBe(false); }); it("does not warn for header-only openai-codex overrides", async () => { - mockDoctorConfigSnapshot({ - config: { - models: { - providers: { - "openai-codex": { - baseUrl: "https://custom.example.com", - headers: { "X-Custom-Auth": "token-123" }, - models: [{ id: "gpt-5.4" }], - }, - }, - }, - auth: { - profiles: { - "openai-codex:user@example.com": { - provider: "openai-codex", - mode: "oauth", - email: "user@example.com", - }, - }, - }, + mockCodexProviderSnapshot({ + provider: { + baseUrl: "https://custom.example.com", + headers: { "X-Custom-Auth": "token-123" }, + models: [{ id: "gpt-5.4" }], }, + withConfigOAuth: true, }); - ensureAuthProfileStore.mockReturnValue({ - version: 1, - profiles: {}, - }); + mockAuthProfileStore(); - await doctorCommand(createDoctorRuntime(), { - nonInteractive: true, - workspaceSuggestions: false, - }); + await runDoctorNonInteractive(); - const warned = terminalNoteMock.mock.calls.some(([, title]) => title === "Codex OAuth"); - expect(warned).toBe(false); + expect(hasCodexOAuthWarning()).toBe(false); }); + it("does not warn about an openai-codex provider override without Codex OAuth", async () => { - mockDoctorConfigSnapshot({ - config: { - models: { - providers: { - "openai-codex": { - api: "openai-responses", - baseUrl: "https://api.openai.com/v1", - }, - }, - }, + mockCodexProviderSnapshot({ + provider: { + api: "openai-responses", + baseUrl: "https://api.openai.com/v1", }, }); - ensureAuthProfileStore.mockReturnValue({ - version: 1, - profiles: {}, - }); + mockAuthProfileStore(); - await doctorCommand(createDoctorRuntime(), { - nonInteractive: true, - workspaceSuggestions: false, - }); + await runDoctorNonInteractive(); - const warned = terminalNoteMock.mock.calls.some(([, title]) => title === "Codex OAuth"); - expect(warned).toBe(false); + expect(hasCodexOAuthWarning()).toBe(false); }); it("skips gateway auth warning when OPENCLAW_GATEWAY_TOKEN is set", async () => {