diff --git a/src/commands/doctor-lint.test.ts b/src/commands/doctor-lint.test.ts index 4d4c0762651..c6ccc4bb9a0 100644 --- a/src/commands/doctor-lint.test.ts +++ b/src/commands/doctor-lint.test.ts @@ -1,4 +1,5 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; +import type { OpenClawConfig } from "../config/types.openclaw.js"; import { resetCoreHealthChecksForTest } from "../flows/doctor-core-checks.js"; import { clearHealthChecksForTest } from "../flows/health-check-registry.js"; import { runDoctorLintCli } from "./doctor-lint.js"; @@ -141,6 +142,55 @@ describe("runDoctorLintCli", () => { } }); + it("reports disabled Codex plugin routes through doctor lint", async () => { + mocks.readConfigFileSnapshot.mockResolvedValue({ + exists: true, + valid: true, + config: { + plugins: { + entries: { + codex: { enabled: false }, + }, + }, + agents: { + defaults: { + model: { + primary: "gpt-5.5", + }, + }, + }, + } as unknown as OpenClawConfig, + path: "/tmp/openclaw.json", + }); + + const stdout = vi.spyOn(process.stdout, "write").mockImplementation(() => true); + try { + const exitCode = await runDoctorLintCli(runtime, { + json: true, + onlyIds: ["core/doctor/codex-session-routes"], + }); + + expect(exitCode).toBe(1); + const payload = JSON.parse(String(stdout.mock.calls.at(-1)?.[0])); + expect(payload).toMatchObject({ + ok: false, + checksRun: 1, + findings: [ + { + checkId: "core/doctor/codex-session-routes", + severity: "warning", + path: "agents.defaults.model.primary", + target: "openai/gpt-5.5", + }, + ], + }); + expect(payload.findings[0].message).toContain("Codex plugin is disabled by config"); + expect(payload.findings[0].fixHint).toContain("openclaw doctor --fix"); + } finally { + stdout.mockRestore(); + } + }); + it("rejects invalid severity thresholds", async () => { await expect(runDoctorLintCli(runtime, { severityMin: "warnng" })).rejects.toThrow( "Invalid --severity-min value", diff --git a/src/commands/doctor/shared/codex-route-warnings.ts b/src/commands/doctor/shared/codex-route-warnings.ts index 6be7b1a7616..2550d9ddd6c 100644 --- a/src/commands/doctor/shared/codex-route-warnings.ts +++ b/src/commands/doctor/shared/codex-route-warnings.ts @@ -43,6 +43,12 @@ type DisabledCodexPluginRouteHit = { modelRef: string; canonicalModel: string; }; +export type DisabledCodexPluginRouteIssue = { + path: string; + modelRef: string; + canonicalModel: string; + blockedOutsideEntry: boolean; +}; type SharedDefaultCompactionOverrideConsumers = Record; type MutableRecord = Record; @@ -1173,6 +1179,18 @@ function collectDisabledCodexPluginRouteHits(cfg: OpenClawConfig): DisabledCodex return hits; } +export function collectDisabledCodexPluginRouteIssues( + cfg: OpenClawConfig, +): DisabledCodexPluginRouteIssue[] { + const blockedOutsideEntry = codexPluginIsBlockedOutsideEntry(cfg); + return collectDisabledCodexPluginRouteHits(cfg).map((hit) => ({ + path: hit.path, + modelRef: hit.modelRef, + canonicalModel: hit.canonicalModel, + blockedOutsideEntry, + })); +} + function enableCodexPluginForRequiredRoutes(params: { cfg: OpenClawConfig; routeHits: DisabledCodexPluginRouteHit[]; diff --git a/src/flows/doctor-core-checks.test.ts b/src/flows/doctor-core-checks.test.ts index e0c4f3dd5c3..0595db9e4f0 100644 --- a/src/flows/doctor-core-checks.test.ts +++ b/src/flows/doctor-core-checks.test.ts @@ -327,6 +327,45 @@ describe("registerCoreHealthChecks", () => { ); }); + it("reports disabled Codex plugin routes as core health findings", async () => { + const check = getCheck( + createCoreHealthChecks(createDeps()), + "core/doctor/codex-session-routes", + ); + + const findings = await check.detect({ + mode: "lint", + runtime, + cfg: { + plugins: { + entries: { + codex: { enabled: false }, + }, + }, + agents: { + defaults: { + model: { + primary: "gpt-5.5", + }, + }, + }, + } as unknown as OpenClawConfig, + }); + + expect(findings).toStrictEqual([ + expect.objectContaining({ + checkId: "core/doctor/codex-session-routes", + severity: "warning", + path: "agents.defaults.model.primary", + target: "openai/gpt-5.5", + requirement: "Codex plugin enabled for routes that use the Codex runtime.", + fixHint: + "Run `openclaw doctor --fix`: it enables plugins.entries.codex, or set the affected OpenAI models to an OpenClaw runtime policy.", + }), + ]); + expect(findings[0]?.message).toContain("Codex plugin is disabled by config"); + }); + it("uses the read-only model catalog for hooks.gmail.model checks", async () => { const cfg: OpenClawConfig = { hooks: { diff --git a/src/flows/doctor-core-checks.ts b/src/flows/doctor-core-checks.ts index 32f82bf4d0e..e0ff055e668 100644 --- a/src/flows/doctor-core-checks.ts +++ b/src/flows/doctor-core-checks.ts @@ -18,6 +18,7 @@ import { uiProtocolFreshnessIssueToHealthFinding, uiProtocolFreshnessIssueToRepairEffects, } from "../commands/doctor-ui.js"; +import { collectDisabledCodexPluginRouteIssues } from "../commands/doctor/shared/codex-route-warnings.js"; import type { ConfigValidationIssue, OpenClawConfig } from "../config/types.openclaw.js"; import { resolveSecretInputRef, type SecretRef } from "../config/types.secrets.js"; import { hasAmbiguousGatewayAuthModeConfig } from "../gateway/auth-mode-policy.js"; @@ -29,6 +30,7 @@ import { registerHealthCheck } from "./health-check-registry.js"; import type { HealthCheck, HealthCheckContext, HealthFinding } from "./health-checks.js"; const BROWSER_CLAWD_PROFILE_RESIDUE_CHECK_ID = "core/doctor/browser-clawd-profile-residue"; +const CODEX_SESSION_ROUTES_CHECK_ID = "core/doctor/codex-session-routes"; const FINAL_CONFIG_VALIDATION_CHECK_ID = "core/doctor/final-config-validation"; const loadDoctorCoreChecksRuntimeModule = async () => @@ -616,6 +618,37 @@ const legacyWhatsAppCrontabCheck: HealthCheck = { }, }; +const codexSessionRoutesCheck: HealthCheck = { + id: CODEX_SESSION_ROUTES_CHECK_ID, + kind: "core", + description: "Codex runtime routes have a registered Codex plugin harness before sessions run.", + source: "doctor", + async detect(ctx) { + return collectDisabledCodexPluginRouteIssues(ctx.cfg).map( + (issue): HealthFinding => ({ + checkId: CODEX_SESSION_ROUTES_CHECK_ID, + severity: "warning", + message: [ + `${issue.path} routes ${issue.modelRef} to ${issue.canonicalModel}`, + "with Codex runtime, but the Codex plugin is disabled by config.", + ].join(" "), + path: issue.path, + target: issue.canonicalModel, + requirement: "Codex plugin enabled for routes that use the Codex runtime.", + fixHint: issue.blockedOutsideEntry + ? [ + "Enable plugin loading and remove codex from plugins.deny,", + "or set the affected OpenAI models to an OpenClaw runtime policy.", + ].join(" ") + : [ + "Run `openclaw doctor --fix`: it enables plugins.entries.codex,", + "or set the affected OpenAI models to an OpenClaw runtime policy.", + ].join(" "), + }), + ); + }, +}; + const gatewayPlatformNotesCheck: HealthCheck = { id: "core/doctor/gateway-services/platform-notes", kind: "core", @@ -913,6 +946,7 @@ function createConvertedWorkflowChecks(deps: CoreHealthCheckDeps): readonly Heal gatewayAuthCheck, legacyStateCheck, legacyWhatsAppCrontabCheck, + codexSessionRoutesCheck, shellCompletionCheck, uiProtocolFreshnessCheck, gatewayPlatformNotesCheck,