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:
Timothy Laurent
2026-05-06 15:49:29 -07:00
parent c2b01ffab1
commit 97b3727a3a
3 changed files with 38 additions and 2 deletions

View File

@@ -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 ───────────────────────────────────────

View File

@@ -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,