fix(ui): stop settings save echoes from clobbering theme preferences
Every updateDesktopSettings PUT replays the server's full settings document as an openchamber:settings-synced event, and the theme listener adopted it unconditionally - so switching sessions across directories (any lastDirectory/activeProjectId write) could flip the theme to whatever the server document held at that moment. A bootstrap GET with missing theme fields made it worse: materializeAuthoritativeUiSettings invented useSystemTheme + openchamber defaults and the persist effect wrote them back to the server and the scoped localStorage entry. Theme is now adopted only from bootstrap-grade syncs (the renamed bootstrap flag, formerly adoptWorkspace), missing fields mean 'not set' and keep the current preference, and the materializer no longer invents theme defaults. Cross-window same-instance theme sync still rides the scoped-key storage event; runtime endpoint switches still adopt the new instance's server theme. Closes the 'theme flashes to OpenChamber when switching sessions' report; same family as the pre-#2897 'color mode forced to light' symptom.
This commit is contained in:
@@ -15,6 +15,7 @@ import {
|
||||
subscribeToSettingsSaveState,
|
||||
syncDesktopSettings,
|
||||
updateDesktopSettings,
|
||||
type SettingsSyncedDetail,
|
||||
} from './persistence';
|
||||
import { switchRuntimeEndpoint } from './runtime-switch';
|
||||
|
||||
@@ -843,6 +844,69 @@ describe('updateDesktopSettings', () => {
|
||||
expect(useUIStore.getState().autoSaveEnabled).toBe(true);
|
||||
expect(saveCalls.some((changes) => changes.autoSaveEnabled === true)).toBe(true);
|
||||
});
|
||||
|
||||
test('does not invent theme defaults when the authoritative snapshot omits theme fields', async () => {
|
||||
getWindow();
|
||||
invalidateSettingsCache();
|
||||
registerSettingsApi(
|
||||
// SAFETY: this mock echoes back exactly the partial changes it received;
|
||||
// the tests below only read fields the changes actually contain.
|
||||
async (changes) => ({ ...changes } as SettingsPayload),
|
||||
async () => ({
|
||||
settings: { draftStartersCraftGoalAdded: true, draftStartersScheduleTaskAdded: true },
|
||||
source: 'web',
|
||||
}),
|
||||
);
|
||||
|
||||
const synced: SettingsSyncedDetail[] = [];
|
||||
const listener = (event: Event): void => {
|
||||
// SAFETY: dispatchSettingsSynced is the only emitter for this key and
|
||||
// always sends a CustomEvent<SettingsSyncedDetail>.
|
||||
const detail = (event as CustomEvent<SettingsSyncedDetail>).detail;
|
||||
if (detail) synced.push(detail);
|
||||
};
|
||||
window.addEventListener('openchamber:settings-synced', listener);
|
||||
try {
|
||||
await syncDesktopSettings();
|
||||
} finally {
|
||||
window.removeEventListener('openchamber:settings-synced', listener);
|
||||
}
|
||||
|
||||
expect(synced.length).toBeGreaterThan(0);
|
||||
const bootstrapSync = synced.find((detail) => detail.bootstrap);
|
||||
expect(bootstrapSync).toBeTruthy();
|
||||
expect(bootstrapSync?.settings.useSystemTheme).toBe(undefined);
|
||||
expect(bootstrapSync?.settings.lightThemeId).toBe(undefined);
|
||||
expect(bootstrapSync?.settings.darkThemeId).toBe(undefined);
|
||||
});
|
||||
|
||||
test('marks settings save echoes as non-bootstrap syncs', async () => {
|
||||
getWindow();
|
||||
invalidateSettingsCache();
|
||||
registerSettingsApi(
|
||||
// SAFETY: this mock echoes back exactly the partial changes it received;
|
||||
// the assertions below only read fields the changes actually contain.
|
||||
async (changes) => ({ ...changes } as SettingsPayload),
|
||||
);
|
||||
|
||||
const synced: SettingsSyncedDetail[] = [];
|
||||
const listener = (event: Event): void => {
|
||||
// SAFETY: dispatchSettingsSynced is the only emitter for this key and
|
||||
// always sends a CustomEvent<SettingsSyncedDetail>.
|
||||
const detail = (event as CustomEvent<SettingsSyncedDetail>).detail;
|
||||
if (detail) synced.push(detail);
|
||||
};
|
||||
window.addEventListener('openchamber:settings-synced', listener);
|
||||
try {
|
||||
await updateDesktopSettings({ themeVariant: 'dark' });
|
||||
} finally {
|
||||
window.removeEventListener('openchamber:settings-synced', listener);
|
||||
}
|
||||
|
||||
expect(synced.length).toBeGreaterThan(0);
|
||||
expect(synced.every((detail) => detail.bootstrap === false)).toBe(true);
|
||||
expect(synced.every((detail) => detail.settings.themeVariant === 'dark')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('unload lifecycle flush (#2197)', () => {
|
||||
|
||||
Reference in New Issue
Block a user