Files
cinny/src/app/utils/roomUpgradeState.test.ts
T
Lotus CIandClaude Opus 5.5 57036bb6a4
CI / Build & Quality Checks (pull_request) Successful in 4m0s
CI / Trigger Desktop Build (pull_request) Skipped
CI / Docker image build & smoke test (pull_request) Skipped
CI / Secret scan (gitleaks) (pull_request) Successful in 9s
CI / Playwright smoke (e2e) (pull_request) Successful in 14m50s
fix(rooms): upgrading a room keeps its Lotus settings (#327)
POST /rooms/{id}/upgrade copies only Synapse's fixed list of state, so an
upgrade silently dropped everything Lotus keeps in room state: an
audio-only voice channel became camera/screenshare-allowed, user limits
and bitrate caps vanished, the soundboard pack was lost, and a room set
to delete messages after 7 days kept new ones forever.

RoomUpgrade now reads the client-owned state first (utils/roomUpgradeState,
unit-tested: io.lotus.*, m.room.retention, role tags, room emoji packs)
and re-creates it in the replacement room; a partial copy is reported in
the dialog instead of closing silently.

Verified headless through the UI (v10 room with voice limit, quality
policy, a soundboard pack and retention → upgrade): before, none
survived; now all four are in the new room.

Follow-up (not here): for spaces, the children's m.space.parent still
point at the old space.

Fixes #327

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

36 lines
1.5 KiB
TypeScript

import { test } from 'node:test';
import assert from 'node:assert/strict';
import { isCarriedStateType, stateToCarry } from './roomUpgradeState';
const ev = (type: string, content: Record<string, unknown>, stateKey = '') => ({
getType: () => type,
getStateKey: () => stateKey,
getContent: () => content,
});
test('Lotus room state is carried; state Synapse already copies is not (#327)', () => {
assert.equal(isCarriedStateType('io.lotus.voice_limit'), true);
assert.equal(isCarriedStateType('io.lotus.room_quality'), true);
assert.equal(isCarriedStateType('io.lotus.soundboard'), true);
assert.equal(isCarriedStateType('m.room.retention'), true);
assert.equal(isCarriedStateType('in.cinny.room.power_level_tags'), true);
assert.equal(isCarriedStateType('im.ponies.room_emotes'), true);
assert.equal(isCarriedStateType('m.room.power_levels'), false);
assert.equal(isCarriedStateType('m.room.name'), false);
assert.equal(isCarriedStateType('m.room.member'), false);
});
test('stateToCarry keeps state keys and skips emptied events', () => {
const out = stateToCarry([
ev('io.lotus.voice_limit', { max_users: 5 }),
ev('io.lotus.soundboard', { clips: [1] }, 'pack-a'),
ev('io.lotus.room_quality', {}),
ev('m.room.name', { name: 'x' }),
ev('m.room.retention', { max_lifetime: 604800000 }),
]);
assert.deepEqual(
out.map((s) => `${s.type}|${s.stateKey}`),
['io.lotus.voice_limit|', 'io.lotus.soundboard|pack-a', 'm.room.retention|'],
);
});