From 988a1c025ca4f50e616252a7449536dd8430c4d8 Mon Sep 17 00:00:00 2001 From: WZBbiao <16611004+WZBbiao@users.noreply.github.com> Date: Tue, 4 Aug 2026 19:08:33 +0800 Subject: [PATCH] fix(codex): avoid false project config warnings --- src/codex/project-config-warnings.ts | 22 ++++++++-- tests/project-config-warnings.test.ts | 60 ++++++++++++++++++++++++++- 2 files changed, 76 insertions(+), 6 deletions(-) diff --git a/src/codex/project-config-warnings.ts b/src/codex/project-config-warnings.ts index f31295fc038..ba5a2f268bd 100644 --- a/src/codex/project-config-warnings.ts +++ b/src/codex/project-config-warnings.ts @@ -1,4 +1,4 @@ -import { existsSync, readFileSync } from "node:fs"; +import { existsSync, readFileSync, realpathSync } from "node:fs"; import path, { dirname, join, resolve } from "node:path"; import { expandUserPath } from "../config"; import { defaultCodexHome } from "./home"; @@ -252,9 +252,23 @@ export function discoverProjectCodexConfigPaths(options: { } = {}): string[] { const found = new Set(); const codexConfigPath = options.codexConfigPath ?? resolveCodexConfigPath(); + // A parent walk can reach the user's home and rediscover this global file as + // `$HOME/.codex/config.toml`; compare real paths so symlink aliases are excluded too. + const normalizeExistingPath = (candidate: string): string | null => { + if (!existsSync(candidate)) return null; + let canonical: string; + try { + canonical = realpathSync.native(candidate); + } catch { + canonical = resolve(candidate); + } + return process.platform === "win32" ? canonical.toLowerCase() : canonical; + }; + const globalConfigIdentity = normalizeExistingPath(codexConfigPath); const addIfExists = (projectRoot: string) => { - const path = join(resolve(projectRoot), ".codex", "config.toml"); - if (existsSync(path)) found.add(path); + const candidate = join(resolve(projectRoot), ".codex", "config.toml"); + const candidateIdentity = normalizeExistingPath(candidate); + if (candidateIdentity && candidateIdentity !== globalConfigIdentity) found.add(candidate); }; let cwd = resolve(options.cwd ?? process.cwd()); @@ -339,7 +353,7 @@ export function summarizeProjectCodexIssue(warning: ProjectCodexConfigWarning): function humanizeProviderDetail(detail: string): string { if (detail === "opencode_go") return "OpenCode Go"; - if (detail.startsWith("opencode")) return "OpenCode"; + if (/^opencode(?:$|[-_.:/])/.test(detail)) return "OpenCode"; if (detail === "opencodex") return "OpenCodex"; return detail; } diff --git a/tests/project-config-warnings.test.ts b/tests/project-config-warnings.test.ts index 6aa74e29066..c5609f9c565 100644 --- a/tests/project-config-warnings.test.ts +++ b/tests/project-config-warnings.test.ts @@ -1,10 +1,12 @@ import { afterEach, beforeEach, describe, expect, test } from "bun:test"; -import { mkdirSync, rmSync, writeFileSync } from "node:fs"; +import { mkdirSync, rmSync, symlinkSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join, posix, win32 } from "node:path"; import { analyzeProjectCodexConfig, collectProjectCodexConfigWarnings, + discoverProjectCodexConfigPaths, + explainProjectConfigBypass, isGlobalOpencodexRoutingActive, invalidateProjectConfigDiagnosticsCache, parseTrustedProjectPathsFromCodexConfig, @@ -210,9 +212,41 @@ model_provider = "openai" }); describe("collectProjectCodexConfigWarnings", () => { + test("does not discover the global config when walking through its parent directory", () => { + const userHome = join(testDir, "user-home"); + const codexConfigPath = join(userHome, ".codex", "config.toml"); + const projectDir = join(userHome, "work", "project"); + const projectConfigPath = join(projectDir, ".codex", "config.toml"); + const nestedCwd = join(projectDir, "nested"); + mkdirSync(join(userHome, ".codex"), { recursive: true }); + mkdirSync(join(projectDir, ".codex"), { recursive: true }); + mkdirSync(nestedCwd, { recursive: true }); + writeFileSync(codexConfigPath, `model_provider = "opencodex-retry"`); + writeFileSync(projectConfigPath, `model_provider = "anthropic"`); + + expect(discoverProjectCodexConfigPaths({ cwd: nestedCwd, codexConfigPath })) + .toEqual([projectConfigPath]); + }); + + test("does not discover a project candidate that aliases the global config through a symlink", () => { + if (process.platform === "win32") return; + const userHome = join(testDir, "symlink-home"); + const candidatePath = join(userHome, ".codex", "config.toml"); + const globalAlias = join(testDir, "global-config-link.toml"); + const projectDir = join(userHome, "work", "project"); + mkdirSync(join(userHome, ".codex"), { recursive: true }); + mkdirSync(projectDir, { recursive: true }); + writeFileSync(candidatePath, `model_provider = "opencodex-retry"`); + symlinkSync(candidatePath, globalAlias); + + expect(discoverProjectCodexConfigPaths({ cwd: projectDir, codexConfigPath: globalAlias })) + .not.toContain(candidatePath); + }); + test("skips untrusted projects even when they define bypass config", () => { const escaped = testDir.replace(/\\/g, "\\\\"); const projectDir = join(testDir, "proj"); + const codexConfigPath = join(process.env.CODEX_HOME!, "config.toml"); writeGlobalRoutingConfig(` [projects.'${escaped}\\proj'] trust_level = "untrusted" @@ -223,7 +257,7 @@ model_provider = "anthropic" [model_providers.anthropic] name = "anthropic" `); - expect(collectProjectCodexConfigWarnings()).toEqual([]); + expect(collectProjectCodexConfigWarnings({ cwd: testDir, codexConfigPath })).toEqual([]); }); test("uncached collection reflects project config changes", () => { @@ -252,3 +286,25 @@ name = "anthropic" expect(second.length).toBe(0); }); }); + +describe("explainProjectConfigBypass", () => { + const warningFor = (detail: string) => [{ + path: "/repo/.codex/config.toml", + code: "model_provider_root" as const, + detail, + message: "fixture", + }]; + + test("humanizes OpenCode provider families only at an identifier boundary", () => { + expect(explainProjectConfigBypass(warningFor("opencode"))).toContain("uses OpenCode "); + expect(explainProjectConfigBypass(warningFor("opencode-go"))).toContain("uses OpenCode "); + expect(explainProjectConfigBypass(warningFor("opencode_go"))).toContain("uses OpenCode Go "); + }); + + test("does not mislabel OpenCodex-prefixed provider ids as OpenCode", () => { + expect(explainProjectConfigBypass(warningFor("opencodex"))).toContain("uses OpenCodex "); + expect(explainProjectConfigBypass(warningFor("opencodex-retry"))) + .toContain("uses opencodex-retry "); + expect(explainProjectConfigBypass(warningFor("opencodeish"))).toContain("uses opencodeish "); + }); +});