Files
backnotprop__plannotator/packages/ui/hooks/useAgentSettings.test.ts
Michael Ramos 6fbf0ef560 fix: align Codex model catalog and reasoning efforts with the current Codex CLI (#1096)
* fix(agents): align Codex model catalog and reasoning efforts with Codex CLI 0.144

- Remove gpt-5.3-codex from the Codex model picker: saved picks of it are
  already force-migrated to the fallback (ChatGPT-account Codex rejects it),
  so offering it was incoherent. The 5.2/5.1 family stays — those models
  remain available to API-key Codex users.
- Per-model reasoning-effort gating: the catalog (now in
  packages/ui/utils/codexModels.ts, re-exported from AgentsTab) carries each
  model's supported efforts and default effort from the CLI catalog. The
  reasoning pickers only offer the selected model's efforts, including the
  new max/ultra tiers on the GPT-5.6 family, and useAgentSettings clamps an
  unsupported saved effort to the model's catalog default before it reaches
  the pickers or a launch payload.
- Drop `minimal` everywhere: no current Codex model supports it. Saved
  perModel reasoning entries migrate minimal -> low, and the guide repair
  launch no longer hardcodes it.
- Ask AI: bump the codex-app-server static fallback model and DEFAULT_MODEL
  from gpt-5.4 to gpt-5.6-sol (runtime model/list discovery still replaces
  the fallback and already carries per-model efforts; added Max/Ultra labels).

Claude-Session: https://claude.ai/code/session_01YXkgsNucxDwAL4GdR4XYRk

* fix(agents): prune API-shut-down Codex models per OpenAI deprecations

OpenAI's API deprecations page schedules an API-level shutdown on
2026-07-23 for gpt-5.2-codex (-> gpt-5.5), gpt-5.1-codex-max (-> gpt-5.5),
and gpt-5.1-codex-mini (-> gpt-5.4-mini) — dead for ALL auth modes, not
just ChatGPT accounts — so they leave the picker and saved picks migrate:
the first two to the surface fallback (already gpt-5.5), codex-mini to
gpt-5.4-mini with its perModel preference carried under the new key (same
pattern as the gpt-5.6 -> gpt-5.6-sol rename).

gpt-5.2 stays selectable with no migration: it was retired from the
ChatGPT product (steered to 5.5) but the API still serves it, so API-key
Codex users retain it.

Claude-Session: https://claude.ai/code/session_01YXkgsNucxDwAL4GdR4XYRk
2026-07-20 11:54:09 -07:00

260 lines
7.8 KiB
TypeScript

import { describe, expect, test } from "bun:test";
import {
DEFAULT_CODEX_REASONING,
migrateCodexSection,
parseReviewProfileByEngine,
sanitizeCodexPerModel,
} from "./useAgentSettings";
import { clampCodexReasoning } from "../utils/codexModels";
describe("sanitizeCodexPerModel", () => {
test("returns empty object for undefined/empty input", () => {
expect(sanitizeCodexPerModel(undefined)).toEqual({});
expect(sanitizeCodexPerModel({})).toEqual({});
});
test("drops stale reasoning: 'none' entry when fast is false", () => {
const result = sanitizeCodexPerModel({
"gpt-5.3-codex": { reasoning: "none", fast: false },
});
expect(result).toEqual({});
});
test("retains entry with reasoning: 'none' but fast: true, replacing reasoning with default", () => {
const result = sanitizeCodexPerModel({
"gpt-5.3-codex": { reasoning: "none", fast: true },
});
expect(result).toEqual({
"gpt-5.3-codex": { reasoning: DEFAULT_CODEX_REASONING, fast: true },
});
});
test("migrates reasoning: 'minimal' to 'low', preserving fast", () => {
expect(
sanitizeCodexPerModel({
"gpt-5.5": { reasoning: "minimal", fast: true },
"gpt-5.6-sol": { reasoning: "minimal", fast: false },
}),
).toEqual({
"gpt-5.5": { reasoning: "low", fast: true },
"gpt-5.6-sol": { reasoning: "low", fast: false },
});
});
test("passes through valid entries unchanged", () => {
const input = {
"gpt-5.3-codex": { reasoning: "high", fast: false },
"gpt-5.3-pro": { reasoning: "medium", fast: true },
};
expect(sanitizeCodexPerModel(input)).toEqual(input);
});
test("skips non-object entries", () => {
const input = {
valid: { reasoning: "high", fast: false },
nullish: null as unknown as { reasoning: string; fast: boolean },
stringy: "bad" as unknown as { reasoning: string; fast: boolean },
};
expect(sanitizeCodexPerModel(input)).toEqual({
valid: { reasoning: "high", fast: false },
});
});
});
describe("migrateCodexSection", () => {
test("moves a stale GPT-5.6 selection and preference while preserving unrelated preferences", () => {
expect(
migrateCodexSection(
{
model: "gpt-5.6",
perModel: {
"gpt-5.6": { reasoning: "xhigh", fast: true },
"gpt-5.5": { reasoning: "medium", fast: false },
},
},
"gpt-5.5",
),
).toEqual({
model: "gpt-5.6-sol",
perModel: {
"gpt-5.6-sol": { reasoning: "xhigh", fast: true },
"gpt-5.5": { reasoning: "medium", fast: false },
},
});
});
test("keeps the canonical preference when stale and canonical keys both exist", () => {
expect(
migrateCodexSection(
{
model: "gpt-5.6",
perModel: {
"gpt-5.6": { reasoning: "low", fast: false },
"gpt-5.6-sol": { reasoning: "high", fast: true },
},
},
"gpt-5.5",
),
).toEqual({
model: "gpt-5.6-sol",
perModel: {
"gpt-5.6-sol": { reasoning: "high", fast: true },
},
});
});
test("passes valid and unknown model IDs through unchanged", () => {
for (const model of ["gpt-5.5", "future-codex-model"]) {
const section = {
model,
perModel: {
[model]: { reasoning: "medium", fast: false },
},
};
expect(migrateCodexSection(section, "gpt-5.5")).toEqual(section);
}
});
test("keeps gpt-5.2 (still API-valid — only the ChatGPT product retired it)", () => {
expect(migrateCodexSection({ model: "gpt-5.2", perModel: {} }, "gpt-5.5").model).toBe("gpt-5.2");
});
test("moves API-shut-down picks with no direct replacement to the fallback", () => {
for (const model of ["gpt-5.2-codex", "gpt-5.1-codex-max"]) {
expect(migrateCodexSection({ model, perModel: {} }, "gpt-5.5").model).toBe("gpt-5.5");
}
});
test("moves a gpt-5.1-codex-mini pick and preference to gpt-5.4-mini", () => {
expect(
migrateCodexSection(
{
model: "gpt-5.1-codex-mini",
perModel: {
"gpt-5.1-codex-mini": { reasoning: "xhigh", fast: true },
"gpt-5.5": { reasoning: "medium", fast: false },
},
},
"gpt-5.5",
),
).toEqual({
model: "gpt-5.4-mini",
perModel: {
"gpt-5.4-mini": { reasoning: "xhigh", fast: true },
"gpt-5.5": { reasoning: "medium", fast: false },
},
});
});
test("keeps the canonical gpt-5.4-mini preference when both keys exist", () => {
expect(
migrateCodexSection(
{
model: "gpt-5.1-codex-mini",
perModel: {
"gpt-5.1-codex-mini": { reasoning: "low", fast: false },
"gpt-5.4-mini": { reasoning: "high", fast: true },
},
},
"gpt-5.5",
),
).toEqual({
model: "gpt-5.4-mini",
perModel: {
"gpt-5.4-mini": { reasoning: "high", fast: true },
},
});
});
test("moves a saved gpt-5.3-codex pick to the fallback and minimal reasoning to low", () => {
expect(
migrateCodexSection(
{
model: "gpt-5.3-codex",
perModel: {
"gpt-5.3-codex": { reasoning: "minimal", fast: true },
},
},
"gpt-5.5",
),
).toEqual({
model: "gpt-5.5",
perModel: {
"gpt-5.3-codex": { reasoning: "low", fast: true },
},
});
});
});
describe("clampCodexReasoning", () => {
test("keeps a supported effort", () => {
expect(clampCodexReasoning("gpt-5.6-sol", "ultra")).toBe("ultra");
expect(clampCodexReasoning("gpt-5.6-luna", "max")).toBe("max");
expect(clampCodexReasoning("gpt-5.5", "xhigh")).toBe("xhigh");
});
test("snaps an unsupported effort to the model's catalog default", () => {
expect(clampCodexReasoning("gpt-5.5", "max")).toBe("medium");
expect(clampCodexReasoning("gpt-5.6-luna", "ultra")).toBe("medium");
expect(clampCodexReasoning("gpt-5.3-codex-spark", "minimal")).toBe("high");
expect(clampCodexReasoning("gpt-5.6-sol", "minimal")).toBe("low");
});
test("passes unknown models through unchanged", () => {
expect(clampCodexReasoning("future-codex-model", "max")).toBe("max");
});
});
describe("parseReviewProfileByEngine", () => {
test("empty cookie → every engine defaults to builtin", () => {
expect(parseReviewProfileByEngine({})).toEqual({
claude: "builtin:default",
codex: "builtin:default",
cursor: "builtin:default",
opencode: "builtin:default",
pi: "builtin:default",
copilot: "builtin:default",
});
});
test("migrates the old flat reviewProfileId by seeding every engine with it", () => {
expect(parseReviewProfileByEngine({ reviewProfileId: "skill:security" })).toEqual({
claude: "skill:security",
codex: "skill:security",
cursor: "skill:security",
opencode: "skill:security",
pi: "skill:security",
copilot: "skill:security",
});
});
test("keeps per-engine picks; missing engines fall back to legacy flat value", () => {
expect(
parseReviewProfileByEngine({
reviewProfileByEngine: { claude: "skill:a", cursor: "skill:b" },
reviewProfileId: "skill:legacy",
}),
).toEqual({
claude: "skill:a",
codex: "skill:legacy",
cursor: "skill:b",
opencode: "skill:legacy",
pi: "skill:legacy",
copilot: "skill:legacy",
});
});
test("missing engines fall back to builtin when there is no legacy value", () => {
expect(
parseReviewProfileByEngine({ reviewProfileByEngine: { codex: "skill:x" } }),
).toEqual({
claude: "builtin:default",
codex: "skill:x",
cursor: "builtin:default",
opencode: "builtin:default",
pi: "builtin:default",
copilot: "builtin:default",
});
});
});