Skip to content

Commit 2fdd5bd

Browse files
committed
test: direct suite for settings-hub shared merge/persist helpers
lib/codex-manager/settings-hub/shared.ts ships ForTests injection variants that nothing exercised. These 14 cases pin: - copyDashboardSettingValue array cloning (no shared references) - applyDashboardDefaultsForKeys resetting only listed keys without mutating the draft - mergeDashboardSettingsForKeys taking only the panel's keys - persistDashboardSettingsSelection re-reading the latest settings so a concurrent edit to an unrelated key is preserved, retrying EBUSY through the real queued-retry policy, and warning + returning the clone-normalized selection when the write keeps failing - persistBackendConfigSelection saving the backend patch, returning a defensive clone (fallback chain not shared), and the warn + fallback path - clampBackendNumber min/max clamping and rounding https://claude.ai/code/session_01XNtnkLbBiXZxfQQYLMpucB
1 parent 6ede089 commit 2fdd5bd

1 file changed

Lines changed: 220 additions & 0 deletions

File tree

test/settings-hub-shared.test.ts

Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2+
3+
const {
4+
loadDashboardDisplaySettingsMock,
5+
saveDashboardDisplaySettingsMock,
6+
savePluginConfigMock,
7+
} = vi.hoisted(() => ({
8+
loadDashboardDisplaySettingsMock: vi.fn(),
9+
saveDashboardDisplaySettingsMock: vi.fn(),
10+
savePluginConfigMock: vi.fn(),
11+
}));
12+
13+
vi.mock("../lib/dashboard-settings.js", async (importOriginal) => ({
14+
...(await importOriginal<typeof import("../lib/dashboard-settings.js")>()),
15+
loadDashboardDisplaySettings: loadDashboardDisplaySettingsMock,
16+
saveDashboardDisplaySettings: saveDashboardDisplaySettingsMock,
17+
getDashboardSettingsPath: () => "/tmp/settings-hub-shared-test/settings.json",
18+
}));
19+
20+
vi.mock("../lib/config.js", async (importOriginal) => ({
21+
...(await importOriginal<typeof import("../lib/config.js")>()),
22+
savePluginConfig: savePluginConfigMock,
23+
}));
24+
25+
import {
26+
applyDashboardDefaultsForKeys,
27+
clampBackendNumber,
28+
cloneDashboardSettings,
29+
copyDashboardSettingValue,
30+
mergeDashboardSettingsForKeys,
31+
persistBackendConfigSelectionForTests,
32+
persistDashboardSettingsSelectionForTests,
33+
} from "../lib/codex-manager/settings-hub/shared.js";
34+
import { DEFAULT_DASHBOARD_DISPLAY_SETTINGS } from "../lib/dashboard-settings.js";
35+
import { backendSettingsEqual } from "../lib/codex-manager/backend-settings-helpers.js";
36+
import type { DashboardDisplaySettings } from "../lib/dashboard-settings.js";
37+
import type { PluginConfig } from "../lib/types.js";
38+
import type { BackendNumberSettingOption } from "../lib/codex-manager/backend-settings-schema.js";
39+
40+
function busyError(): NodeJS.ErrnoException {
41+
const error = new Error("locked") as NodeJS.ErrnoException;
42+
error.code = "EBUSY";
43+
return error;
44+
}
45+
46+
describe("settings hub shared helpers", () => {
47+
beforeEach(() => {
48+
vi.clearAllMocks();
49+
loadDashboardDisplaySettingsMock.mockResolvedValue({});
50+
saveDashboardDisplaySettingsMock.mockResolvedValue(undefined);
51+
savePluginConfigMock.mockResolvedValue(undefined);
52+
});
53+
54+
afterEach(() => {
55+
vi.restoreAllMocks();
56+
});
57+
58+
describe("copyDashboardSettingValue", () => {
59+
it("copies scalars and clones array values", () => {
60+
const source: DashboardDisplaySettings = {
61+
menuShowLastUsed: false,
62+
menuStatuslineFields: ["status", "limits"],
63+
};
64+
const target: DashboardDisplaySettings = {};
65+
copyDashboardSettingValue(target, source, "menuShowLastUsed");
66+
copyDashboardSettingValue(target, source, "menuStatuslineFields");
67+
expect(target.menuShowLastUsed).toBe(false);
68+
expect(target.menuStatuslineFields).toStrictEqual(["status", "limits"]);
69+
// Arrays must be copied, not shared: mutating the target later must
70+
// never write through to the source settings object.
71+
expect(target.menuStatuslineFields).not.toBe(source.menuStatuslineFields);
72+
});
73+
});
74+
75+
describe("applyDashboardDefaultsForKeys", () => {
76+
it("resets only the listed keys and leaves the draft untouched", () => {
77+
const draft: DashboardDisplaySettings = {
78+
menuShowLastUsed: false,
79+
menuShowQuotaSummary: false,
80+
};
81+
const next = applyDashboardDefaultsForKeys(draft, ["menuShowLastUsed"]);
82+
expect(next.menuShowLastUsed).toBe(
83+
DEFAULT_DASHBOARD_DISPLAY_SETTINGS.menuShowLastUsed,
84+
);
85+
expect(next.menuShowQuotaSummary).toBe(false);
86+
expect(draft.menuShowLastUsed).toBe(false);
87+
});
88+
});
89+
90+
describe("mergeDashboardSettingsForKeys", () => {
91+
it("takes only the listed keys from the selection", () => {
92+
const base: DashboardDisplaySettings = {
93+
menuShowLastUsed: false,
94+
menuShowQuotaSummary: false,
95+
};
96+
const selected: DashboardDisplaySettings = {
97+
menuShowLastUsed: true,
98+
menuShowQuotaSummary: true,
99+
};
100+
const merged = mergeDashboardSettingsForKeys(base, selected, [
101+
"menuShowLastUsed",
102+
]);
103+
expect(merged.menuShowLastUsed).toBe(true);
104+
expect(merged.menuShowQuotaSummary).toBe(false);
105+
expect(base.menuShowLastUsed).toBe(false);
106+
});
107+
});
108+
109+
describe("persistDashboardSettingsSelection", () => {
110+
it("re-reads the latest settings and merges only the panel's keys onto them", async () => {
111+
// A concurrent edit to an unrelated key landed on disk after the panel
112+
// loaded: the write must preserve it instead of clobbering with the
113+
// panel's stale view.
114+
loadDashboardDisplaySettingsMock.mockResolvedValue({
115+
menuHighlightCurrentRow: false,
116+
});
117+
const result = await persistDashboardSettingsSelectionForTests(
118+
{ menuShowLastUsed: false, menuHighlightCurrentRow: true },
119+
["menuShowLastUsed"],
120+
"dashboard",
121+
);
122+
expect(saveDashboardDisplaySettingsMock).toHaveBeenCalledTimes(1);
123+
const saved = saveDashboardDisplaySettingsMock.mock
124+
.calls[0]?.[0] as DashboardDisplaySettings;
125+
expect(saved.menuShowLastUsed).toBe(false);
126+
expect(saved.menuHighlightCurrentRow).toBe(false);
127+
expect(result.menuHighlightCurrentRow).toBe(false);
128+
});
129+
130+
it("retries transient write failures through the queued-retry policy", async () => {
131+
saveDashboardDisplaySettingsMock
132+
.mockRejectedValueOnce(busyError())
133+
.mockResolvedValueOnce(undefined);
134+
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
135+
const result = await persistDashboardSettingsSelectionForTests(
136+
{ menuShowLastUsed: false },
137+
["menuShowLastUsed"],
138+
"dashboard",
139+
);
140+
expect(saveDashboardDisplaySettingsMock).toHaveBeenCalledTimes(2);
141+
expect(warnSpy).not.toHaveBeenCalled();
142+
expect(result.menuShowLastUsed).toBe(false);
143+
});
144+
145+
it("warns and returns the selection as fallback when the write keeps failing", async () => {
146+
saveDashboardDisplaySettingsMock.mockRejectedValue(
147+
new Error("disk gone"),
148+
);
149+
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
150+
const selected: DashboardDisplaySettings = { menuShowLastUsed: false };
151+
const result = await persistDashboardSettingsSelectionForTests(
152+
selected,
153+
["menuShowLastUsed"],
154+
"dashboard",
155+
);
156+
expect(warnSpy).toHaveBeenCalledWith(
157+
"Settings save failed (dashboard) after retries: disk gone",
158+
);
159+
// The fallback is the clone-normalized selection (the clone fills
160+
// documented defaults), never the caller's object itself.
161+
expect(result).toStrictEqual(cloneDashboardSettings(selected));
162+
expect(result.menuShowLastUsed).toBe(false);
163+
expect(result).not.toBe(selected);
164+
});
165+
});
166+
167+
describe("persistBackendConfigSelection", () => {
168+
it("saves the backend patch and returns a defensive clone of the selection", async () => {
169+
const selected = {
170+
unsupportedCodexFallbackChain: { "gpt-5.3-codex": ["gpt-5.2"] },
171+
} as PluginConfig;
172+
const result = await persistBackendConfigSelectionForTests(
173+
selected,
174+
"backend",
175+
);
176+
expect(savePluginConfigMock).toHaveBeenCalledTimes(1);
177+
expect(backendSettingsEqual(result, selected)).toBe(true);
178+
expect(result).not.toBe(selected);
179+
expect(result.unsupportedCodexFallbackChain).not.toBe(
180+
selected.unsupportedCodexFallbackChain,
181+
);
182+
});
183+
184+
it("warns and falls back to the selection clone when the save fails", async () => {
185+
savePluginConfigMock.mockRejectedValue(new Error("config locked"));
186+
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
187+
const selected = {} as PluginConfig;
188+
const result = await persistBackendConfigSelectionForTests(
189+
selected,
190+
"backend",
191+
);
192+
expect(warnSpy).toHaveBeenCalledWith(
193+
"Settings save failed (backend) after retries: config locked",
194+
);
195+
expect(backendSettingsEqual(result, selected)).toBe(true);
196+
});
197+
});
198+
199+
describe("clampBackendNumber", () => {
200+
const option = {
201+
key: "fetchTimeoutMs",
202+
label: "fetch timeout",
203+
min: 10,
204+
max: 100,
205+
step: 5,
206+
unit: "duration",
207+
} as unknown as BackendNumberSettingOption;
208+
209+
it.each([
210+
[5, 10],
211+
[10, 10],
212+
[55.4, 55],
213+
[55.5, 56],
214+
[100, 100],
215+
[250, 100],
216+
])("clamps and rounds %d to %d", (value, expected) => {
217+
expect(clampBackendNumber(option, value)).toBe(expected);
218+
});
219+
});
220+
});

0 commit comments

Comments
 (0)