fix(ui): bound settings mutation history
Skip mutation history when no settings operation is in flight and merge updates that share the same operation visibility window. This keeps stale-response reconciliation unchanged while preventing history from growing with repeated debounced updates. Add a repeated-update regression covering an older pending load and latest-value reconciliation. Tests: bun test src/lib/persistence.test.ts
This commit is contained in:
@@ -641,6 +641,43 @@ describe('updateDesktopSettings', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('preserves only the latest settings values across repeated pending updates', async () => {
|
||||||
|
const loadedSettings = deferred<{ settings: SettingsPayload; source: 'web' | 'vscode' }>();
|
||||||
|
registerSettingsApi(async (changes) => changes as SettingsPayload, () => loadedSettings.promise);
|
||||||
|
invalidateSettingsCache();
|
||||||
|
const syncedSettings: SettingsPayload[] = [];
|
||||||
|
const handleSettingsSynced = (event: Event) => {
|
||||||
|
syncedSettings.push((event as CustomEvent<SettingsPayload>).detail);
|
||||||
|
};
|
||||||
|
getWindow().addEventListener('openchamber:settings-synced', handleSettingsSynced);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const sync = syncDesktopSettings();
|
||||||
|
const updates = Array.from({ length: 100 }, (_, index) => updateDesktopSettings({
|
||||||
|
activeProjectId: `project-${index}`,
|
||||||
|
showReasoningTraces: index % 2 === 0,
|
||||||
|
}));
|
||||||
|
|
||||||
|
loadedSettings.resolve({
|
||||||
|
settings: {
|
||||||
|
activeProjectId: 'stale-project',
|
||||||
|
showReasoningTraces: true,
|
||||||
|
draftStartersCraftGoalAdded: true,
|
||||||
|
draftStartersScheduleTaskAdded: true,
|
||||||
|
},
|
||||||
|
source: 'web',
|
||||||
|
});
|
||||||
|
await sync;
|
||||||
|
|
||||||
|
expect(syncedSettings.at(-1)?.activeProjectId).toBe('project-99');
|
||||||
|
expect(syncedSettings.at(-1)?.showReasoningTraces).toBe(false);
|
||||||
|
|
||||||
|
await Promise.all(updates);
|
||||||
|
} finally {
|
||||||
|
getWindow().removeEventListener('openchamber:settings-synced', handleSettingsSynced);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test('applies model selector settings from server settings', async () => {
|
test('applies model selector settings from server settings', async () => {
|
||||||
getWindow();
|
getWindow();
|
||||||
const settings = {
|
const settings = {
|
||||||
|
|||||||
@@ -1679,7 +1679,19 @@ class SettingsMutationTracker {
|
|||||||
|
|
||||||
record(changes: Partial<DesktopSettings>): number {
|
record(changes: Partial<DesktopSettings>): number {
|
||||||
this.revision += 1;
|
this.revision += 1;
|
||||||
this.mutations.push({ revision: this.revision, changes });
|
if (this.operations.size > 0) {
|
||||||
|
const latest = this.mutations.at(-1);
|
||||||
|
// A new segment is only needed when an operation started after the last one.
|
||||||
|
const crossedOperationBoundary = latest
|
||||||
|
? [...this.operations].some((operation) => operation.revision >= latest.revision)
|
||||||
|
: true;
|
||||||
|
if (latest && !crossedOperationBoundary) {
|
||||||
|
latest.revision = this.revision;
|
||||||
|
latest.changes = { ...latest.changes, ...changes };
|
||||||
|
} else {
|
||||||
|
this.mutations.push({ revision: this.revision, changes });
|
||||||
|
}
|
||||||
|
}
|
||||||
return this.revision;
|
return this.revision;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user