Files
cinny/src/app/utils/settingsSync.test.ts
T
Lotus CIandClaude Opus 5.5 5130dfc9c8
CI / Build & Quality Checks (pull_request) Successful in 3m6s
CI / Trigger Desktop Build (pull_request) Skipped
CI / Docker image build & smoke test (pull_request) Skipped
CI / Secret scan (gitleaks) (pull_request) Successful in 13s
CI / Playwright smoke (e2e) (pull_request) Successful in 14m37s
fix(settings): "ENTER for Newline" stays per device (#348)
Settings sync mirrored enterForNewline to io.lotus.settings, so turning it
on for a phone keyboard switched the desktop to Ctrl+Enter-to-send (and
vice versa). It is now in DEVICE_LOCAL_KEYS.

Verified with one account on a phone and a desktop profile: toggling it
on the phone leaves the desktop at Enter-to-send, and it is no longer in
the synced account data.

Fixes #348

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017PCb59uY3UKqDqt7B6hzd5
2026-10-12 15:27:01 -04:00

113 lines
4.0 KiB
TypeScript

import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import {
DEVICE_LOCAL_KEYS,
SETTINGS_SYNC_VERSION,
buildSyncedContent,
isSyncedSettingsContent,
mergeRemoteSettings,
pickSyncable,
shouldApplyRemote,
syncableEqual,
} from './settingsSync';
import { Settings } from '../state/settings';
// A representative slice of Settings; the helpers are generic over keys so a
// partial object cast is enough to exercise every branch.
const base = {
settingsSync: true,
pageZoom: 110,
isMarkdown: true,
quietHoursEnabled: false,
quietHoursStart: '22:00',
composerToolbarButtons: { showGif: true, order: ['showEmoji', 'showGif'] },
pttKey: 'KeyV',
callDenoiseModel: 'rnnoise',
} as unknown as Settings;
describe('pickSyncable', () => {
it('drops device-local keys and deep-copies objects', () => {
const picked = pickSyncable(base);
assert.equal('pageZoom' in picked, false);
assert.equal('settingsSync' in picked, false);
assert.equal('callDenoiseModel' in picked, false);
assert.equal(picked.isMarkdown, true);
assert.equal(picked.pttKey, 'KeyV');
assert.notEqual(picked.composerToolbarButtons, base.composerToolbarButtons);
assert.deepEqual(picked.composerToolbarButtons, base.composerToolbarButtons);
});
it('never syncs the sync toggle itself', () => {
assert.equal(DEVICE_LOCAL_KEYS.has('settingsSync'), true);
});
});
describe('syncableEqual', () => {
it('is structural and key-order independent', () => {
const a = pickSyncable(base);
const b = Object.fromEntries(Object.entries(a).reverse()) as Partial<Settings>;
assert.equal(syncableEqual(a, b), true);
assert.equal(syncableEqual(a, { ...a, isMarkdown: false }), false);
assert.equal(syncableEqual(a, { ...a, extra: 1 } as Partial<Settings>), false);
});
});
describe('isSyncedSettingsContent', () => {
it('accepts a well-formed event and rejects junk', () => {
assert.equal(isSyncedSettingsContent(buildSyncedContent(base, 5)), true);
assert.equal(isSyncedSettingsContent({}), false);
assert.equal(isSyncedSettingsContent(null), false);
assert.equal(isSyncedSettingsContent({ version: 1, updatedAt: 'x', settings: {} }), false);
assert.equal(isSyncedSettingsContent({ version: 1, updatedAt: 1, settings: [] }), false);
});
});
describe('mergeRemoteSettings', () => {
it('applies syncable keys, ignores device-local and unknown keys and wrong shapes', () => {
const remote = {
version: SETTINGS_SYNC_VERSION,
updatedAt: 10,
settings: {
isMarkdown: false,
pageZoom: 200, // device-local: ignored
quietHoursStart: 42, // wrong type: ignored
composerToolbarButtons: 'nope', // wrong type: ignored
futureKey: true, // unknown: ignored
} as unknown as Partial<Settings>,
};
const merged = mergeRemoteSettings(base, remote);
assert.equal(merged.isMarkdown, false);
assert.equal(merged.pageZoom, 110);
assert.equal(merged.quietHoursStart, '22:00');
assert.deepEqual(merged.composerToolbarButtons, base.composerToolbarButtons);
assert.equal('futureKey' in merged, false);
// Never mutates the input.
assert.equal(base.isMarkdown, true);
});
});
describe('shouldApplyRemote', () => {
it('applies when never synced or strictly newer; ignores echoes', () => {
const remote = buildSyncedContent(base, 100);
assert.equal(shouldApplyRemote(remote, null), true);
assert.equal(shouldApplyRemote(remote, 99), true);
assert.equal(shouldApplyRemote(remote, 100), false);
assert.equal(shouldApplyRemote(remote, 101), false);
});
});
describe('buildSyncedContent', () => {
it('stamps version and updatedAt around the syncable subset', () => {
const c = buildSyncedContent(base, 7);
assert.equal(c.version, SETTINGS_SYNC_VERSION);
assert.equal(c.updatedAt, 7);
assert.deepEqual(c.settings, pickSyncable(base));
});
});
describe('device-local keys', () => {
it('keeps enterForNewline on each device (#348)', () => {
assert.equal(DEVICE_LOCAL_KEYS.has('enterForNewline'), true);
});
});