Let a project turn a globally-enabled Claude Code setting back off
The six boolean settings were plain `bool`s merged with
`if p.x { true } else { g.x }`, so a project could only ever add to the
global set. There was no project value that produced `false` — turning a
switch off at project level simply fell through to the global value and
the control did nothing.
Widen them to `Option<bool>`. `None` means "not set at this level":
inherit the global on a project, leave Claude Code's own default alone
globally. `Some(false)` is a deliberate off and wins outright.
The fingerprint now formats with `{:?}` rather than `{}` — `None` and
`Some(false)` mean different things, and conflating them would leave the
container un-recreated when a project switched from inherit to off.
The project editor grows a third "Global" state per switch; the global
editor has nothing to inherit from, so it stays a plain toggle and keeps
collapsing to null at the default. Its three existing tests passed
unchanged and caught a first attempt that rendered unset as off, which
would have told every user their session recap was disabled.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBq2rGum6GX7xXgsas1fDc
This commit is contained in:
@@ -3,10 +3,18 @@ import { render, screen, fireEvent } from "@testing-library/react";
|
||||
import ClaudeCodeSettingsEditor, { CLAUDE_CODE_DEFAULTS } from "./ClaudeCodeSettingsEditor";
|
||||
import type { ClaudeCodeSettings } from "../../lib/types";
|
||||
|
||||
function renderEditor(settings: ClaudeCodeSettings | null) {
|
||||
function renderEditor(
|
||||
settings: ClaudeCodeSettings | null,
|
||||
scope: "global" | "project" = "global",
|
||||
) {
|
||||
const onSave = vi.fn().mockResolvedValue(undefined);
|
||||
render(
|
||||
<ClaudeCodeSettingsEditor settings={settings} disabled={false} onSave={onSave} />,
|
||||
<ClaudeCodeSettingsEditor
|
||||
scope={scope}
|
||||
settings={settings}
|
||||
disabled={false}
|
||||
onSave={onSave}
|
||||
/>,
|
||||
);
|
||||
return onSave;
|
||||
}
|
||||
@@ -59,4 +67,57 @@ describe("ClaudeCodeSettingsEditor", () => {
|
||||
).map((o) => o.getAttribute("value")),
|
||||
).toEqual(["", "low", "medium", "high", "xhigh"]);
|
||||
});
|
||||
|
||||
describe("project scope", () => {
|
||||
it("offers Global as a third state so a project can decline to have an opinion", () => {
|
||||
renderEditor(null, "project");
|
||||
const focus = screen.getByLabelText("Focus mode");
|
||||
expect(
|
||||
Array.from(focus.querySelectorAll("option")).map((o) => o.getAttribute("value")),
|
||||
).toEqual(["global", "off", "on"]);
|
||||
expect((focus as HTMLSelectElement).value).toBe("global");
|
||||
});
|
||||
|
||||
it("stores a deliberate false so the project can turn a global On back off", () => {
|
||||
// The reason the field widened from boolean to boolean|null. Under the
|
||||
// old merge there was no project value that could produce this.
|
||||
const onSave = renderEditor(null, "project");
|
||||
fireEvent.change(screen.getByLabelText("Focus mode"), { target: { value: "off" } });
|
||||
expect(onSave).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ focus_mode: false }),
|
||||
);
|
||||
});
|
||||
|
||||
it("does not collapse a deliberate off to null", () => {
|
||||
// `null` means inherit. Collapsing here would silently hand the setting
|
||||
// straight back to the global value the user just overrode.
|
||||
const onSave = renderEditor(null, "project");
|
||||
fireEvent.change(screen.getByLabelText("Focus mode"), { target: { value: "off" } });
|
||||
expect(onSave).not.toHaveBeenCalledWith(null);
|
||||
});
|
||||
|
||||
it("round-trips the inverted fields through the disabled sense", () => {
|
||||
// Session recap stores `session_recap_disabled`, so choosing "off" has to
|
||||
// store `true` and choosing "on" has to store `false`.
|
||||
const onSave = renderEditor(null, "project");
|
||||
const recap = screen.getByLabelText("Session recap");
|
||||
|
||||
fireEvent.change(recap, { target: { value: "off" } });
|
||||
expect(onSave).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ session_recap_disabled: true }),
|
||||
);
|
||||
|
||||
fireEvent.change(recap, { target: { value: "on" } });
|
||||
expect(onSave).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ session_recap_disabled: false }),
|
||||
);
|
||||
});
|
||||
|
||||
it("shows a stored override rather than the inherited state", () => {
|
||||
renderEditor({ ...CLAUDE_CODE_DEFAULTS, session_recap_disabled: true }, "project");
|
||||
expect((screen.getByLabelText("Session recap") as HTMLSelectElement).value).toBe(
|
||||
"off",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user