fix(codex-config): close fix-round-1 parity gaps against CcConfig

Isolate the codex-CLI-backed reads (mcp/plugins/marketplaces) from the
file-backed ones in fetchAll via two independent Promise.all groups, so a
missing `codex` binary degrades only the mcp/plugins tabs instead of
leaving the whole panel stuck on skeletons. Generalize EditorModal's
nameless-type detection behind a `namelessTypes` prop (defaulting to
Claude's own ["memory"]) so Codex's config.toml can be created without a
name. Port BackupsModal, disable the plugin/mcp Remove button when no
stable identifier resolved, hide the inert search box on memory/config,
drop a redundant empty state, and reword the delete-dialog copy to be
tool-neutral.

Co-authored-by: Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
This commit is contained in:
2026-09-08 21:34:06 +07:00
parent 8015730adf
commit 56bf5bbf4d
8 changed files with 432 additions and 73 deletions
@@ -42,6 +42,7 @@ const codexConfigMock = vi.hoisted(() => {
pluginAction: r({ ok: true, stdout: "", stderr: "" }),
marketplaceAdd: r({ ok: true, stdout: "", stderr: "" }),
marketplaceAction: r({ ok: true, stdout: "", stderr: "" }),
backups: r({ items: [] }),
};
});
@@ -124,4 +125,63 @@ describe("CodexConfig", () => {
expect(write).toHaveBeenCalledWith(expect.objectContaining({ type: "rules", name: "default" }));
});
it("keeps the file-backed tabs working when the codex CLI read rejects", async () => {
stubCodexApi({ skills: [{ scope: "user", name: "my-skill", file: "/s/my-skill", size: 3 }] });
codexConfigMock.mcp.mockRejectedValueOnce(new Error("the codex CLI was not found on PATH"));
render(<CodexConfig />);
await settle();
// No page-wide error banner - the CLI-backed rejection is isolated.
expect(screen.queryByText(/failed to load/i)).not.toBeInTheDocument();
// Pure file reads (skills) still rendered their data.
await userEvent.click(screen.getByRole("tab", { name: /skills/i }));
expect(screen.getByText("my-skill")).toBeInTheDocument();
// The CLI-owned tab degrades gracefully instead of spinning forever.
await userEvent.click(screen.getByRole("tab", { name: /mcp/i }));
expect(screen.getByText(/codex cli isn't available/i)).toBeInTheDocument();
});
it("creates a nameless-type artifact (config.toml) without requiring a name", async () => {
const write = vi.fn().mockResolvedValue({ file: "/c/config.toml", created: true });
stubCodexApi({ write });
render(<CodexConfig />);
await settle();
await userEvent.click(screen.getByRole("tab", { name: /config/i }));
const newButtons = screen.getAllByRole("button", { name: /new/i });
await userEvent.click(newButtons[0]!);
await settle();
// No name field, no client-side "Name" validation error blocking save.
expect(screen.queryByPlaceholderText(/name/i)).not.toBeInTheDocument();
await userEvent.click(screen.getByRole("button", { name: /save/i }));
expect(write).toHaveBeenCalledWith(
expect.objectContaining({ type: "config", name: undefined })
);
});
it("opens the backups modal and lists what the codex API returns", async () => {
codexConfigMock.backups.mockResolvedValueOnce({
items: [
{
type: "rules",
name: "default.2026-09-08.bak",
file: "/r/default.bak",
mtime: 0,
size: 3,
},
],
});
render(<CodexConfig />);
await settle();
await userEvent.click(screen.getByRole("button", { name: /backups/i }));
await settle();
expect(screen.getByText("default.2026-09-08.bak")).toBeInTheDocument();
});
});