fix(settings): prevent project-level global keys from overriding global settings
getSettingsFast() and getSettings() spread project-level settings over
global settings without filtering by scope. A stale project-level
experimentalFeatures: {} in SQLite would override the global
experimentalFeatures: { insights: true, ... }, making all experimental
features invisible in the dashboard.
Now both methods strip global-only keys from project settings before
the merge, matching the behavior of getSettingsByScopeFast().
Also adds max-height and overflow-y to the overflow dropdown menu.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -3062,6 +3062,25 @@ describe("TaskStore", () => {
|
||||
const settings = await store.getSettingsFast();
|
||||
expect(settings.experimentalFeatures).toEqual({ "fast-feature": true });
|
||||
});
|
||||
|
||||
it("project-level experimentalFeatures does not override global value", async () => {
|
||||
// Set global experimentalFeatures
|
||||
await store.updateGlobalSettings({ experimentalFeatures: { insights: true, roadmap: true } });
|
||||
|
||||
// Simulate stale project-level config with empty experimentalFeatures
|
||||
// (can happen from older clients or direct DB writes)
|
||||
store.getDatabase()
|
||||
.prepare("UPDATE config SET settings = ? WHERE id = 1")
|
||||
.run(JSON.stringify({ experimentalFeatures: {} }));
|
||||
|
||||
// getSettingsFast should ignore the project-level global key
|
||||
const fastSettings = await store.getSettingsFast();
|
||||
expect(fastSettings.experimentalFeatures).toEqual({ insights: true, roadmap: true });
|
||||
|
||||
// getSettings should also ignore the project-level global key
|
||||
const settings = await store.getSettings();
|
||||
expect(settings.experimentalFeatures).toEqual({ insights: true, roadmap: true });
|
||||
});
|
||||
});
|
||||
|
||||
// ── Concurrent stress test ───────────────────────────────────────
|
||||
|
||||
@@ -1645,10 +1645,15 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
this.globalSettingsStore.getSettings(),
|
||||
this.readConfig(),
|
||||
]);
|
||||
// Strip global-only keys from project-level settings so stale project-scoped
|
||||
// values don't override the correct global value during the spread merge.
|
||||
const projectSettings = Object.fromEntries(
|
||||
Object.entries(config.settings ?? {}).filter(([key]) => !isGlobalSettingsKey(key)),
|
||||
);
|
||||
return canonicalizeSettings({
|
||||
...DEFAULT_SETTINGS,
|
||||
...globalSettings,
|
||||
...config.settings,
|
||||
...projectSettings,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1670,7 +1675,17 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
this.db.prepare("SELECT settings FROM config WHERE id = 1").get() as { settings?: string } | undefined,
|
||||
]);
|
||||
|
||||
const projectSettings = row?.settings ? fromJson<Settings>(row.settings) : undefined;
|
||||
const raw = row?.settings ? fromJson<Settings>(row.settings) : undefined;
|
||||
|
||||
// Strip global-only keys from the project-level row so stale project-scoped
|
||||
// values (e.g. an empty experimentalFeatures={}) don't override the correct
|
||||
// global value during the spread merge below. getSettingsByScopeFast() has
|
||||
// always done this; getSettingsFast() was missing the filter.
|
||||
const projectSettings: Partial<Settings> | undefined = raw
|
||||
? (Object.fromEntries(
|
||||
Object.entries(raw).filter(([key]) => !isGlobalSettingsKey(key)),
|
||||
) as Partial<Settings>)
|
||||
: undefined;
|
||||
|
||||
return canonicalizeSettings({
|
||||
...DEFAULT_SETTINGS,
|
||||
|
||||
Reference in New Issue
Block a user