Clean up legacy sort order

This commit is contained in:
Jack Kingsman
2026-04-02 20:16:16 -07:00
parent 975bf7f03f
commit 1895e6a919
26 changed files with 77 additions and 137 deletions
+1 -1
View File
@@ -477,7 +477,7 @@ mc.subscribe(EventType.ACK, handler)
| `MESHCORE_ENABLE_MESSAGE_POLL_FALLBACK` | `false` | Switch the always-on radio audit task from hourly checks to aggressive 10-second polling; the audit checks both missed message drift and channel-slot cache drift |
| `MESHCORE_FORCE_CHANNEL_SLOT_RECONFIGURE` | `false` | Disable channel-slot reuse and force `set_channel(...)` before every channel send, even on serial/BLE |
**Note:** Runtime app settings are stored in the database (`app_settings` table), not environment variables. These include `max_radio_contacts`, `auto_decrypt_dm_on_advert`, `sidebar_sort_order`, `advert_interval`, `last_advert_time`, `favorites`, `last_message_times`, `flood_scope`, `blocked_keys`, `blocked_names`, and `discovery_blocked_types`. `max_radio_contacts` is the configured radio contact capacity baseline used by background maintenance: favorites reload first, non-favorite fill targets about 80% of that value, and full offload/reload triggers around 95% occupancy. They are configured via `GET/PATCH /api/settings`. The backend still carries `sidebar_sort_order` for compatibility and migration, but the current frontend sidebar stores sort order per section (`Channels`, `Contacts`, `Repeaters`) in localStorage rather than treating it as one shared server-backed preference. MQTT, bot, webhook, Apprise, and SQS configs are stored in the `fanout_configs` table, managed via `/api/fanout`. If the radio's channel slots appear unstable or another client is mutating them underneath this app, operators can force the old always-reconfigure send path with `MESHCORE_FORCE_CHANNEL_SLOT_RECONFIGURE=true`.
**Note:** Runtime app settings are stored in the database (`app_settings` table), not environment variables. These include `max_radio_contacts`, `auto_decrypt_dm_on_advert`, `advert_interval`, `last_advert_time`, `favorites`, `last_message_times`, `flood_scope`, `blocked_keys`, `blocked_names`, and `discovery_blocked_types`. `max_radio_contacts` is the configured radio contact capacity baseline used by background maintenance: favorites reload first, non-favorite fill targets about 80% of that value, and full offload/reload triggers around 95% occupancy. They are configured via `GET/PATCH /api/settings`. MQTT, bot, webhook, Apprise, and SQS configs are stored in the `fanout_configs` table, managed via `/api/fanout`. If the radio's channel slots appear unstable or another client is mutating them underneath this app, operators can force the old always-reconfigure send path with `MESHCORE_FORCE_CHANNEL_SLOT_RECONFIGURE=true`.
Byte-perfect channel retries are user-triggered via `POST /api/messages/channel/{message_id}/resend` and are allowed for 30 seconds after the original send.
-3
View File
@@ -302,7 +302,6 @@ Repository writes should prefer typed models such as `ContactUpsert` over ad hoc
- `max_radio_contacts`
- `favorites`
- `auto_decrypt_dm_on_advert`
- `sidebar_sort_order`
- `last_message_times`
- `preferences_migrated`
- `advert_interval`
@@ -310,8 +309,6 @@ Repository writes should prefer typed models such as `ContactUpsert` over ad hoc
- `flood_scope`
- `blocked_keys`, `blocked_names`, `discovery_blocked_types`
Note: `sidebar_sort_order` remains in the backend model for compatibility and migration, but the current frontend sidebar uses per-section localStorage sort preferences instead of a single shared server-backed sort mode.
Note: MQTT, community MQTT, and bot configs were migrated to the `fanout_configs` table (migrations 36-38).
## Security Posture (intentional)
-1
View File
@@ -96,7 +96,6 @@ CREATE TABLE IF NOT EXISTS app_settings (
max_radio_contacts INTEGER DEFAULT 200,
favorites TEXT DEFAULT '[]',
auto_decrypt_dm_on_advert INTEGER DEFAULT 1,
sidebar_sort_order TEXT DEFAULT 'recent',
last_message_times TEXT DEFAULT '{}',
preferences_migrated INTEGER DEFAULT 0,
advert_interval INTEGER DEFAULT 0,
+28 -7
View File
@@ -389,6 +389,12 @@ async def run_migrations(conn: aiosqlite.Connection) -> int:
await set_version(conn, 50)
applied += 1
if version < 51:
logger.info("Applying migration 51: drop sidebar_sort_order from app_settings")
await _migrate_051_drop_sidebar_sort_order(conn)
await set_version(conn, 51)
applied += 1
if applied > 0:
logger.info(
"Applied %d migration(s), schema now at version %d", applied, await get_version(conn)
@@ -859,13 +865,9 @@ async def _migrate_009_create_app_settings_table(conn: aiosqlite.Connection) ->
"""
)
# Initialize with default row
await conn.execute(
"""
INSERT OR IGNORE INTO app_settings (id, max_radio_contacts, favorites, auto_decrypt_dm_on_advert, sidebar_sort_order, last_message_times, preferences_migrated)
VALUES (1, 200, '[]', 1, 'recent', '{}', 0)
"""
)
# Initialize with default row (use only the id column so this works
# regardless of which columns exist — defaults fill the rest).
await conn.execute("INSERT OR IGNORE INTO app_settings (id) VALUES (1)")
await conn.commit()
logger.debug("Created app_settings table with default values")
@@ -3128,3 +3130,22 @@ async def _migrate_050_repeater_telemetry_history(conn: aiosqlite.Connection) ->
"""
)
await conn.commit()
async def _migrate_051_drop_sidebar_sort_order(conn: aiosqlite.Connection) -> None:
"""Remove vestigial sidebar_sort_order column from app_settings."""
col_cursor = await conn.execute("PRAGMA table_info(app_settings)")
columns = {row[1] for row in await col_cursor.fetchall()}
if "sidebar_sort_order" in columns:
try:
await conn.execute("ALTER TABLE app_settings DROP COLUMN sidebar_sort_order")
await conn.commit()
except Exception as e:
error_msg = str(e).lower()
if "syntax error" in error_msg or "drop column" in error_msg:
logger.debug(
"SQLite doesn't support DROP COLUMN, sidebar_sort_order column will remain"
)
await conn.commit()
else:
raise
-4
View File
@@ -787,10 +787,6 @@ class AppSettings(BaseModel):
default=True,
description="Whether to attempt historical DM decryption on new contact advertisement",
)
sidebar_sort_order: Literal["recent", "alpha"] = Field(
default="recent",
description="Sidebar sort order: 'recent' or 'alpha'",
)
last_message_times: dict[str, int] = Field(
default_factory=dict,
description="Map of conversation state keys to last message timestamps",
+3 -6
View File
@@ -395,12 +395,9 @@ class ContactRepository:
@staticmethod
async def delete(public_key: str) -> None:
normalized = public_key.lower()
# contact_name_history and contact_advert_paths cascade via FK, but
# messages has no FK to contacts — clean up DMs explicitly.
await db.conn.execute(
"DELETE FROM messages WHERE type = 'PRIV' AND conversation_key = ?",
(normalized,),
)
# contact_name_history and contact_advert_paths cascade via FK.
# Messages are intentionally preserved so history re-surfaces
# if the contact is re-added later.
await db.conn.execute("DELETE FROM contacts WHERE public_key = ?", (normalized,))
await db.conn.commit()
+1 -1
View File
@@ -675,7 +675,7 @@ class MessageRepository:
ELSE 0
END) > 0 as has_mention
FROM messages m
JOIN contacts ct ON m.conversation_key = ct.public_key
LEFT JOIN contacts ct ON m.conversation_key = ct.public_key
WHERE m.type = 'PRIV' AND m.outgoing = 0
AND m.received_at > COALESCE(ct.last_read_at, 0)
{blocked_sql}
+1 -13
View File
@@ -27,7 +27,7 @@ class AppSettingsRepository:
cursor = await db.conn.execute(
"""
SELECT max_radio_contacts, favorites, auto_decrypt_dm_on_advert,
sidebar_sort_order, last_message_times, preferences_migrated,
last_message_times, preferences_migrated,
advert_interval, last_advert_time, flood_scope,
blocked_keys, blocked_names, discovery_blocked_types
FROM app_settings WHERE id = 1
@@ -89,16 +89,10 @@ class AppSettingsRepository:
except (json.JSONDecodeError, TypeError):
discovery_blocked_types = []
# Validate sidebar_sort_order (fallback to "recent" if invalid)
sort_order = row["sidebar_sort_order"]
if sort_order not in ("recent", "alpha"):
sort_order = "recent"
return AppSettings(
max_radio_contacts=row["max_radio_contacts"],
favorites=favorites,
auto_decrypt_dm_on_advert=bool(row["auto_decrypt_dm_on_advert"]),
sidebar_sort_order=sort_order,
last_message_times=last_message_times,
preferences_migrated=bool(row["preferences_migrated"]),
advert_interval=row["advert_interval"] or 0,
@@ -114,7 +108,6 @@ class AppSettingsRepository:
max_radio_contacts: int | None = None,
favorites: list[Favorite] | None = None,
auto_decrypt_dm_on_advert: bool | None = None,
sidebar_sort_order: str | None = None,
last_message_times: dict[str, int] | None = None,
preferences_migrated: bool | None = None,
advert_interval: int | None = None,
@@ -141,10 +134,6 @@ class AppSettingsRepository:
updates.append("auto_decrypt_dm_on_advert = ?")
params.append(1 if auto_decrypt_dm_on_advert else 0)
if sidebar_sort_order is not None:
updates.append("sidebar_sort_order = ?")
params.append(sidebar_sort_order)
if last_message_times is not None:
updates.append("last_message_times = ?")
params.append(json.dumps(last_message_times))
@@ -252,7 +241,6 @@ class AppSettingsRepository:
# Update with migrated preferences and mark as migrated
settings = await AppSettingsRepository.update(
favorites=new_favorites,
sidebar_sort_order=sort_order if sort_order in ("recent", "alpha") else "recent",
last_message_times=last_message_times,
preferences_migrated=True,
)
+1
View File
@@ -250,6 +250,7 @@ async def batch_cli_fetch(
# Re-ensure contact is loaded each iteration; another operation
# may have evicted it while we didn't hold the lock.
await _ensure_on_radio(mc, contact)
await asyncio.sleep(1.0) # settle after add_contact
send_result = await mc.commands.send_cmd(contact.public_key, cmd)
if send_result.type == EventType.ERROR:
-8
View File
@@ -27,10 +27,6 @@ class AppSettingsUpdate(BaseModel):
default=None,
description="Whether to attempt historical DM decryption on new contact advertisement",
)
sidebar_sort_order: Literal["recent", "alpha"] | None = Field(
default=None,
description="Sidebar sort order: 'recent' or 'alpha'",
)
advert_interval: int | None = Field(
default=None,
ge=0,
@@ -111,10 +107,6 @@ async def update_settings(update: AppSettingsUpdate) -> AppSettings:
logger.info("Updating auto_decrypt_dm_on_advert to %s", update.auto_decrypt_dm_on_advert)
kwargs["auto_decrypt_dm_on_advert"] = update.auto_decrypt_dm_on_advert
if update.sidebar_sort_order is not None:
logger.info("Updating sidebar_sort_order to %s", update.sidebar_sort_order)
kwargs["sidebar_sort_order"] = update.sidebar_sort_order
if update.advert_interval is not None:
# Enforce minimum 1-hour interval; 0 means disabled
interval = update.advert_interval
-2
View File
@@ -350,7 +350,6 @@ LocalStorage migration helpers for favorites; canonical favorites are server-sid
- `max_radio_contacts`
- `favorites`
- `auto_decrypt_dm_on_advert`
- `sidebar_sort_order`
- `last_message_times`
- `preferences_migrated`
- `advert_interval`
@@ -358,7 +357,6 @@ LocalStorage migration helpers for favorites; canonical favorites are server-sid
- `flood_scope`
- `blocked_keys`, `blocked_names`, `discovery_blocked_types`
The backend still carries `sidebar_sort_order` for compatibility and old preference migration, but the current sidebar UI stores sort order per section (`Channels`, `Contacts`, `Repeaters`) in frontend localStorage rather than treating it as one global server-backed setting.
Note: MQTT, bot, and community MQTT settings were migrated to the `fanout_configs` table (managed via `/api/fanout`). They are no longer part of `AppSettings`.
-1
View File
@@ -490,7 +490,6 @@ export function App() {
void markAllRead();
},
favorites,
legacySortOrder: appSettings?.sidebar_sort_order,
isConversationNotificationsEnabled,
blockedKeys: appSettings?.blocked_keys ?? [],
blockedNames: appSettings?.blocked_names ?? [],
+7 -42
View File
@@ -107,36 +107,19 @@ interface SidebarProps {
onToggleCracker: () => void;
onMarkAllRead: () => void;
favorites: Favorite[];
/** Legacy global sort order, used only to seed per-section local preferences. */
legacySortOrder?: SortOrder;
isConversationNotificationsEnabled?: (type: 'channel' | 'contact', id: string) => boolean;
blockedKeys?: string[];
blockedNames?: string[];
}
type InitialSectionSortState = {
orders: SidebarSectionSortOrders;
source: 'section' | 'legacy' | 'none';
};
function loadInitialSectionSortOrders(): InitialSectionSortState {
function loadInitialSectionSortOrders(): SidebarSectionSortOrders {
const storedOrders = loadLocalStorageSidebarSectionSortOrders();
if (storedOrders) {
return { orders: storedOrders, source: 'section' };
}
if (storedOrders) return storedOrders;
const legacyOrder = loadLegacyLocalStorageSortOrder();
if (legacyOrder) {
return {
orders: buildSidebarSectionSortOrders(legacyOrder),
source: 'legacy',
};
}
return {
orders: buildSidebarSectionSortOrders(),
source: 'none',
};
const orders = buildSidebarSectionSortOrders(legacyOrder ?? undefined);
saveLocalStorageSidebarSectionSortOrders(orders);
return orders;
}
export function Sidebar({
@@ -153,7 +136,6 @@ export function Sidebar({
onToggleCracker,
onMarkAllRead,
favorites,
legacySortOrder,
isConversationNotificationsEnabled,
blockedKeys = [],
blockedNames = [],
@@ -166,8 +148,8 @@ export function Sidebar({
);
const [searchQuery, setSearchQuery] = useState('');
const initialSectionSortState = useMemo(loadInitialSectionSortOrders, []);
const [sectionSortOrders, setSectionSortOrders] = useState(initialSectionSortState.orders);
const initialSectionSortOrders = useMemo(loadInitialSectionSortOrders, []);
const [sectionSortOrders, setSectionSortOrders] = useState(initialSectionSortOrders);
const initialCollapsedState = useMemo(loadCollapsedState, []);
const [toolsCollapsed, setToolsCollapsed] = useState(initialCollapsedState.tools);
const [favoritesCollapsed, setFavoritesCollapsed] = useState(initialCollapsedState.favorites);
@@ -176,29 +158,12 @@ export function Sidebar({
const [roomsCollapsed, setRoomsCollapsed] = useState(initialCollapsedState.rooms);
const [repeatersCollapsed, setRepeatersCollapsed] = useState(initialCollapsedState.repeaters);
const collapseSnapshotRef = useRef<CollapseState | null>(null);
const sectionSortSourceRef = useRef(initialSectionSortState.source);
useEffect(() => {
if (sectionSortSourceRef.current === 'legacy') {
saveLocalStorageSidebarSectionSortOrders(sectionSortOrders);
sectionSortSourceRef.current = 'section';
return;
}
if (sectionSortSourceRef.current !== 'none' || legacySortOrder === undefined) return;
const seededOrders = buildSidebarSectionSortOrders(legacySortOrder);
setSectionSortOrders(seededOrders);
saveLocalStorageSidebarSectionSortOrders(seededOrders);
sectionSortSourceRef.current = 'section';
}, [legacySortOrder, sectionSortOrders]);
const handleSortToggle = (section: SidebarSortableSection) => {
setSectionSortOrders((prev) => {
const nextOrder = prev[section] === 'alpha' ? 'recent' : 'alpha';
const updated = { ...prev, [section]: nextOrder };
saveLocalStorageSidebarSectionSortOrders(updated);
sectionSortSourceRef.current = 'section';
return updated;
});
};
-1
View File
@@ -190,7 +190,6 @@ const baseSettings = {
max_radio_contacts: 200,
favorites: [] as Array<{ type: 'channel' | 'contact'; id: string }>,
auto_decrypt_dm_on_advert: false,
sidebar_sort_order: 'recent' as const,
last_message_times: {},
preferences_migrated: false,
advert_interval: 0,
-1
View File
@@ -218,7 +218,6 @@ describe('App search jump target handling', () => {
max_radio_contacts: 200,
favorites: [],
auto_decrypt_dm_on_advert: false,
sidebar_sort_order: 'recent',
last_message_times: {},
preferences_migrated: true,
advert_interval: 0,
@@ -169,7 +169,6 @@ describe('App startup hash resolution', () => {
max_radio_contacts: 200,
favorites: [],
auto_decrypt_dm_on_advert: false,
sidebar_sort_order: 'recent',
last_message_times: {},
preferences_migrated: true,
advert_interval: 0,
-1
View File
@@ -61,7 +61,6 @@ const baseSettings: AppSettings = {
max_radio_contacts: 200,
favorites: [],
auto_decrypt_dm_on_advert: false,
sidebar_sort_order: 'recent',
last_message_times: {},
preferences_migrated: false,
advert_interval: 0,
-9
View File
@@ -92,7 +92,6 @@ function renderSidebar(overrides?: {
onToggleCracker={vi.fn()}
onMarkAllRead={vi.fn()}
favorites={favorites}
legacySortOrder="recent"
isConversationNotificationsEnabled={overrides?.isConversationNotificationsEnabled}
/>
);
@@ -140,7 +139,6 @@ describe('Sidebar section summaries', () => {
onToggleCracker={vi.fn()}
onMarkAllRead={vi.fn()}
favorites={[]}
legacySortOrder="recent"
/>
);
@@ -300,7 +298,6 @@ describe('Sidebar section summaries', () => {
onToggleCracker={vi.fn()}
onMarkAllRead={vi.fn()}
favorites={[]}
legacySortOrder="recent"
/>
);
@@ -397,7 +394,6 @@ describe('Sidebar section summaries', () => {
onToggleCracker: vi.fn(),
onMarkAllRead: vi.fn(),
favorites: [],
legacySortOrder: 'recent' as const,
};
const getChannelsOrder = () => screen.getAllByText(/^#/).map((node) => node.textContent);
@@ -469,7 +465,6 @@ describe('Sidebar section summaries', () => {
onToggleCracker={vi.fn()}
onMarkAllRead={vi.fn()}
favorites={[]}
legacySortOrder="recent"
/>
);
@@ -504,7 +499,6 @@ describe('Sidebar section summaries', () => {
onToggleCracker={vi.fn()}
onMarkAllRead={vi.fn()}
favorites={[]}
legacySortOrder="recent"
/>
);
@@ -553,7 +547,6 @@ describe('Sidebar section summaries', () => {
onToggleCracker={vi.fn()}
onMarkAllRead={vi.fn()}
favorites={[]}
legacySortOrder="recent"
/>
);
@@ -586,7 +579,6 @@ describe('Sidebar section summaries', () => {
onToggleCracker={vi.fn()}
onMarkAllRead={vi.fn()}
favorites={[]}
legacySortOrder="alpha"
/>
);
@@ -623,7 +615,6 @@ describe('Sidebar section summaries', () => {
{ type: 'contact', id: zed.public_key },
{ type: 'contact', id: amy.public_key },
] satisfies Favorite[],
legacySortOrder: 'recent' as const,
};
const getFavoritesOrder = () =>
-2
View File
@@ -316,7 +316,6 @@ export interface AppSettings {
max_radio_contacts: number;
favorites: Favorite[];
auto_decrypt_dm_on_advert: boolean;
sidebar_sort_order: 'recent' | 'alpha';
last_message_times: Record<string, number>;
preferences_migrated: boolean;
advert_interval: number;
@@ -330,7 +329,6 @@ export interface AppSettings {
export interface AppSettingsUpdate {
max_radio_contacts?: number;
auto_decrypt_dm_on_advert?: boolean;
sidebar_sort_order?: 'recent' | 'alpha';
advert_interval?: number;
flood_scope?: string;
blocked_keys?: string[];
-1
View File
@@ -222,7 +222,6 @@ export interface AppSettings {
max_radio_contacts: number;
favorites: Favorite[];
auto_decrypt_dm_on_advert: boolean;
sidebar_sort_order: string;
last_message_times: Record<string, number>;
preferences_migrated: boolean;
advert_interval: number;
+14 -5
View File
@@ -25,6 +25,16 @@ test.describe('Apprise integration settings', () => {
receiver.close();
});
test.beforeEach(async () => {
// Clean up any stale configs from previous failed runs
const configs = await getFanoutConfigs();
for (const c of configs.filter((c) => c.name === 'E2E Apprise')) {
try {
await deleteFanoutConfig(c.id);
} catch { /* ignore */ }
}
});
test.afterEach(async () => {
if (createdAppriseId) {
try {
@@ -66,16 +76,15 @@ test.describe('Apprise integration settings', () => {
await page.getByRole('button', { name: /Save as Enabled/i }).click();
await expect(page.getByText('Integration saved and enabled')).toBeVisible();
// Should be back on list view with our apprise config visible
await expect(page.getByText('E2E Apprise')).toBeVisible();
await expect(page.getByText(appriseUrl)).toBeVisible();
// Clean up via API
// Capture ID for cleanup before assertions that might fail
const configs = await getFanoutConfigs();
const apprise = configs.find((c) => c.name === 'E2E Apprise');
if (apprise) {
createdAppriseId = apprise.id;
}
// Should be back on list view with our apprise config visible
await expect(fanoutHeader(page, 'E2E Apprise')).toBeVisible();
});
test('create apprise via API, verify options persist after edit', async ({ page }) => {
-1
View File
@@ -293,7 +293,6 @@ class TestDebugEndpoint:
json={
"max_radio_contacts": 321,
"auto_decrypt_dm_on_advert": True,
"sidebar_sort_order": "alpha",
"advert_interval": 7200,
"flood_scope": "US-CA",
"blocked_keys": [pub_key],
+3 -5
View File
@@ -364,7 +364,7 @@ class TestDeleteContactCascade:
assert len(await ContactAdvertPathRepository.get_recent_for_contact(KEY_A)) == 0
@pytest.mark.asyncio
async def test_delete_removes_direct_messages(self, test_db, client):
async def test_delete_preserves_direct_messages(self, test_db, client):
await _insert_contact(KEY_A, "Alice")
# Create a DM for this contact
@@ -375,8 +375,6 @@ class TestDeleteContactCascade:
sender_timestamp=1000,
received_at=1000,
)
msgs = await MessageRepository.get_all(msg_type="PRIV", conversation_key=KEY_A)
assert len(msgs) == 1
with patch("app.routers.contacts.radio_manager") as mock_rm:
mock_rm.is_connected = False
@@ -387,9 +385,9 @@ class TestDeleteContactCascade:
assert response.status_code == 200
# DMs for the deleted contact should be gone
# DMs are preserved so they re-surface if the contact is re-added
msgs = await MessageRepository.get_all(msg_type="PRIV", conversation_key=KEY_A)
assert len(msgs) == 0
assert len(msgs) == 1
class TestMarkRead:
+16 -16
View File
@@ -1249,8 +1249,8 @@ class TestMigration039:
applied = await run_migrations(conn)
assert applied == 12
assert await get_version(conn) == 50
assert applied == 13
assert await get_version(conn) == 51
cursor = await conn.execute(
"""
@@ -1321,8 +1321,8 @@ class TestMigration039:
applied = await run_migrations(conn)
assert applied == 12
assert await get_version(conn) == 50
assert applied == 13
assert await get_version(conn) == 51
cursor = await conn.execute(
"""
@@ -1388,8 +1388,8 @@ class TestMigration039:
applied = await run_migrations(conn)
assert applied == 6
assert await get_version(conn) == 50
assert applied == 7
assert await get_version(conn) == 51
cursor = await conn.execute(
"""
@@ -1441,8 +1441,8 @@ class TestMigration040:
applied = await run_migrations(conn)
assert applied == 11
assert await get_version(conn) == 50
assert applied == 12
assert await get_version(conn) == 51
await conn.execute(
"""
@@ -1503,8 +1503,8 @@ class TestMigration041:
applied = await run_migrations(conn)
assert applied == 10
assert await get_version(conn) == 50
assert applied == 11
assert await get_version(conn) == 51
await conn.execute(
"""
@@ -1556,8 +1556,8 @@ class TestMigration042:
applied = await run_migrations(conn)
assert applied == 9
assert await get_version(conn) == 50
assert applied == 10
assert await get_version(conn) == 51
await conn.execute(
"""
@@ -1696,8 +1696,8 @@ class TestMigration046:
applied = await run_migrations(conn)
assert applied == 5
assert await get_version(conn) == 50
assert applied == 6
assert await get_version(conn) == 51
cursor = await conn.execute(
"""
@@ -1790,8 +1790,8 @@ class TestMigration047:
applied = await run_migrations(conn)
assert applied == 4
assert await get_version(conn) == 50
assert applied == 5
assert await get_version(conn) == 51
cursor = await conn.execute(
"""
+2 -4
View File
@@ -622,7 +622,6 @@ class TestAppSettingsRepository:
"max_radio_contacts": 250,
"favorites": "{not-json",
"auto_decrypt_dm_on_advert": 1,
"sidebar_sort_order": "invalid",
"last_message_times": "{also-not-json",
"preferences_migrated": 0,
"advert_interval": None,
@@ -645,7 +644,6 @@ class TestAppSettingsRepository:
assert settings.max_radio_contacts == 250
assert settings.favorites == []
assert settings.last_message_times == {}
assert settings.sidebar_sort_order == "recent"
assert settings.advert_interval == 0
assert settings.last_advert_time == 0
@@ -680,7 +678,7 @@ class TestAppSettingsRepository:
from app.models import AppSettings
current = AppSettings(preferences_migrated=False)
migrated = AppSettings(preferences_migrated=True, sidebar_sort_order="recent")
migrated = AppSettings(preferences_migrated=True)
with (
patch(
@@ -704,7 +702,7 @@ class TestAppSettingsRepository:
assert did_migrate is True
assert result.preferences_migrated is True
assert mock_update.call_args.kwargs["sidebar_sort_order"] == "recent"
assert "sidebar_sort_order" not in mock_update.call_args.kwargs
assert mock_update.call_args.kwargs["preferences_migrated"] is True
-1
View File
@@ -177,7 +177,6 @@ class TestMigratePreferences:
assert response.migrated is True
assert response.settings.preferences_migrated is True
assert response.settings.sidebar_sort_order == "alpha"
assert len(response.settings.favorites) == 1
assert response.settings.favorites[0].type == "contact"
assert response.settings.favorites[0].id == "aa" * 32