git » sdk » commit 6c2742e

Share IDB and SQLite persistence tests

author Eric Roberts
2026-08-25 14:36:46 UTC
committer Stephen Paul Weber
2026-08-26 13:25:55 UTC
parent 51690545210e6f55693cbd6ebf8d65728fb07969

Share IDB and SQLite persistence tests

Really the tests should be fully shared unless there are specific behaviours of
sqlite or idb we want to test.

One implementation detail changed in IDB to sort members the same way SQLite
did. Everything else should be strictly test changes. There is one test left
that is duplicated between IDB and SQLite test but the code under test seems to
have real functional differences that I think should be addressed in a separate
patch.

For tests that call storeChats, the SQLite ones were adding a timer to wait for
the write because storeChats isn't an awaitable promise right now. That seemed
like a can of worms to change, so instead I wrapped the store chats
implementation in the sqlite tests to have the timer, so that the same test
body can execute against IDB or SQLite without changing. If we fix storeChats,
then we can remove the wrapper and everything will just work.

Other fixtures were added to resolve differences between the two persistence
mechanisms so that the tests themselves could be shared.

borogove/persistence/IDB.js +1 -0
test/browser-test.ts +89 -0
test/idb.spec.ts +101 -1348
test/persistence-tests.ts +1466 -0
test/sqlite.spec.ts +2 -1668

diff --git a/borogove/persistence/IDB.js b/borogove/persistence/IDB.js
index 6277596..9bdedb4 100644
--- a/borogove/persistence/IDB.js
+++ b/borogove/persistence/IDB.js
@@ -481,6 +481,7 @@ export default async (dbname, media, tokenize, stemmer) => {
 			cresult.continue();
 		}
 
+		membersForName?.sort((a, b) => a.displayName.localeCompare(b.displayName));
 		return [presence, membersForName];
 	};
 
diff --git a/test/browser-test.ts b/test/browser-test.ts
index 39a2da9..8f49cdd 100644
--- a/test/browser-test.ts
+++ b/test/browser-test.ts
@@ -1,8 +1,11 @@
 import { test as base, expect, type JSHandle } from "@playwright/test";
+import type { borogove_persistence_Sqlite } from "../npm/sqlite-wasm";
 
 type BrowserFixtures = {
 	borogove: JSHandle<any>;
+	createChannel: JSHandle<any>;
 	persistence: JSHandle<any>;
+	storeIncompleteMember: JSHandle<any>;
 };
 
 type SqliteFixtures = BrowserFixtures & {
@@ -26,6 +29,14 @@ export const idbTest = base.extend<BrowserFixtures>({
 		const borogove = await page.evaluateHandle(() => window.borogove);
 		await use(borogove);
 	},
+	createChannel: async ({ page, borogove }, use) => {
+		const createChannel = await page.evaluateHandle(
+			(borogove) => (persistence, chatId) =>
+				new borogove.Channel(null, null, persistence, chatId),
+			borogove,
+		);
+		await use(createChannel);
+	},
 	persistence: async ({ page, borogove }, use) => {
 		const persistence = await page.evaluateHandle(async (borogove) => {
 			const mediaStore = await borogove.persistence.MediaStoreCache("snikket");
@@ -33,6 +44,35 @@ export const idbTest = base.extend<BrowserFixtures>({
 		}, borogove);
 		await use(persistence);
 	},
+	storeIncompleteMember: async ({ page }, use) => {
+		const storeIncompleteMember = await page.evaluateHandle(
+			() => async (chatId) => {
+				const request = indexedDB.open("snikket");
+				const db = await new Promise<IDBDatabase>((resolve, reject) => {
+					request.onsuccess = () => resolve(request.result);
+					request.onerror = () => reject(request.error);
+				});
+				const transaction = db.transaction(["members"], "readwrite");
+				transaction.objectStore("members").put({
+					account: "alice@example.com",
+					chatId,
+					id: "room-members-7@example.com/incomplete",
+					displayName: "",
+					photoUri: null,
+					isSelf: 0,
+					chat: "",
+					roles: [],
+					presence: new Map(),
+					jid: "",
+				});
+				await new Promise((resolve, reject) => {
+					transaction.oncomplete = () => resolve(null);
+					transaction.onerror = () => reject(transaction.error);
+				});
+			},
+		);
+		await use(storeIncompleteMember);
+	},
 });
 
 export const sqliteTest = base.extend<SqliteFixtures>({
@@ -48,6 +88,14 @@ export const sqliteTest = base.extend<SqliteFixtures>({
 		const sqlite = await page.evaluateHandle(() => window.sqlite);
 		await use(sqlite);
 	},
+	createChannel: async ({ page, sqlite }, use) => {
+		const createChannel = await page.evaluateHandle(
+			(sqlite) => (persistence, chatId) =>
+				new sqlite.Channel(null, null, persistence, chatId),
+			sqlite,
+		);
+		await use(createChannel);
+	},
 	persistence: async ({ page, borogove, sqlite }, use) => {
 		const persistence = await page.evaluateHandle(
 			async ({ borogove, sqlite }) => {
@@ -57,8 +105,49 @@ export const sqliteTest = base.extend<SqliteFixtures>({
 			},
 			{ borogove, sqlite },
 		);
+		await page.evaluate((persistence: borogove_persistence_Sqlite) => {
+			// TODO: remove this wrapper
+			//
+			// storeChats is not currently awaitable. The Sqlite tests were adding
+			// manual delays that the IDB tests didn't need. I wanted to share the
+			// tests, so wrapping the implementation with a delay here lets me do
+			// that.
+			//
+			// Changing storeChats to return a Promise that could be awaited would
+			// be the better long term solution, but there's already a debounce
+			// thing in storeChats itself that maybe should be revisited and that
+			// seemed like a riskier change, so I went with this for now.
+			const storeChats = persistence.storeChats.bind(persistence);
+			persistence.storeChats = (...args) => {
+				storeChats(...args);
+				return new Promise((resolve) => setTimeout(resolve, 200));
+			};
+		}, persistence);
 		await use(persistence);
 	},
+	storeIncompleteMember: async ({ page, persistence }, use) => {
+		const storeIncompleteMember = await page.evaluateHandle(
+			(persistence) => async (chatId) => {
+				await persistence.db.exec(
+					"INSERT INTO members(account_id, chat_id, member_id, display_name, photo_uri, is_self, chat, roles, presence, jid) VALUES(?, ?, ?, ?, ?, ?, ?, jsonb(?), jsonb(?), ?)",
+					[
+						"alice@example.com",
+						chatId,
+						"room-members-7@example.com/incomplete",
+						"",
+						null,
+						0,
+						"{}",
+						"[]",
+						"{}",
+						"",
+					],
+				);
+			},
+			persistence,
+		);
+		await use(storeIncompleteMember);
+	},
 });
 
 export { expect };
diff --git a/test/idb.spec.ts b/test/idb.spec.ts
index c730880..a8e52e0 100644
--- a/test/idb.spec.ts
+++ b/test/idb.spec.ts
@@ -1,1216 +1,56 @@
 import { idbTest as test, expect } from "./browser-test";
+import { sharedPersistenceTests } from "./persistence-tests";
 
-test("1:1 come back ordered by sortId", async ({ page, borogove, persistence }) => {
-	const result = await page.evaluate(async ({ borogove, persistence }) => {
-		const builder = new borogove.ChatMessageBuilder({
-			serverId: "1",
-			serverIdBy: "alice@example.com",
-			senderId: "hatter@example.com",
-			direction: 0,
-		});
-		builder.sortId = "a0";
-		builder.to = borogove.JID.parse("alice@example.com");
-		builder.from = borogove.JID.parse("hatter@example.com");
-		builder.replyTo = [builder.from];
-
-		const builder2 = new borogove.ChatMessageBuilder({
-			serverId: "2",
-			serverIdBy: "alice@example.com",
-			senderId: "hatter@example.com",
-			direction: 0,
-		});
-		builder2.sortId = "b0";
-		builder2.to = borogove.JID.parse("alice@example.com");
-		builder2.from = borogove.JID.parse("hatter@example.com");
-		builder2.replyTo = [builder.from];
-
-		await persistence.storeMessages("alice@example.com", [
-			builder2.build(),
-			builder.build(),
-		]);
-
-		return await persistence.getMessagesBefore(
-			"alice@example.com",
-			"hatter@example.com",
-		);
-	}, { borogove, persistence });
-
-	expect(result.length).toBe(2);
-	expect(result[0].serverId).toBe("1");
-	expect(result[1].serverId).toBe("2");
-});
-
-test("getMessagesBefore the end: MUC come back ordered by sortId, PM by timestamp", async ({
-	page,
-	borogove,
-	persistence,
-}) => {
-	const result = await page.evaluate(async ({ borogove, persistence }) => {
-		const builder = new borogove.ChatMessageBuilder({
-			serverId: "1",
-			serverIdBy: "teaparty@example.com",
-			senderId: "teaparty@example.com/hatter",
-			direction: 0,
-			type: borogove.MessageType.MessageChannel,
-			timestamp: "2020-01-01T00:00:01Z",
-		});
-		builder.sortId = "a0";
-		builder.to = borogove.JID.parse("alice@example.com");
-		builder.from = borogove.JID.parse("teaparty@example.com/hatter");
-		builder.replyTo = [builder.from.asBare()];
-
-		const builder2 = new borogove.ChatMessageBuilder({
-			serverId: "2",
-			serverIdBy: "teaparty@example.com",
-			senderId: "teaparty@example.com/hatter",
-			direction: 0,
-			type: borogove.MessageType.MessageChannel,
-			timestamp: "2020-01-01T00:00:00Z",
-		});
-		builder2.sortId = "b0";
-		builder2.to = borogove.JID.parse("alice@example.com");
-		builder2.from = borogove.JID.parse("teaparty@example.com/hatter");
-		builder2.replyTo = [builder.from.asBare()];
-
-		const builder3 = new borogove.ChatMessageBuilder({
-			serverId: "3",
-			serverIdBy: "alice@example.com",
-			senderId: "teaparty@example.com/hatter",
-			direction: 0,
-			type: borogove.MessageType.MessageChannelPrivate,
-			timestamp: "2020-01-01T00:00:03Z",
-		});
-		builder3.sortId = "a0";
-		builder3.to = borogove.JID.parse("alice@example.com");
-		builder3.from = borogove.JID.parse("teaparty@example.com/hatter");
-		builder3.replyTo = [builder.from.asBare()];
-
-		await persistence.storeMessages("alice@example.com", [
-			builder2.build(),
-			builder3.build(),
-			builder.build(),
-		]);
-
-		return await persistence.getMessagesBefore(
-			"alice@example.com",
-			"teaparty@example.com",
-		);
-	}, { borogove, persistence });
-
-	expect(result.length).toBe(3);
-	expect(result[0].serverId).toBe("1");
-	expect(result[1].serverId).toBe("2");
-	expect(result[2].serverId).toBe("3");
-});
-
-test("getMessagesBefore some point: MUC come back ordered by sortId, PM by timestamp", async ({
-	page,
-	borogove,
-	persistence,
-}) => {
-	const result = await page.evaluate(async ({ borogove, persistence }) => {
-		const builder = new borogove.ChatMessageBuilder({
-			serverId: "1",
-			serverIdBy: "teaparty@example.com",
-			senderId: "teaparty@example.com/hatter",
-			direction: 0,
-			type: borogove.MessageType.MessageChannel,
-			timestamp: "2020-01-01T00:00:01Z",
-		});
-		builder.sortId = "a0";
-		builder.to = borogove.JID.parse("alice@example.com");
-		builder.from = borogove.JID.parse("teaparty@example.com/hatter");
-		builder.replyTo = [builder.from.asBare()];
-
-		const builder2 = new borogove.ChatMessageBuilder({
-			serverId: "2",
-			serverIdBy: "teaparty@example.com",
-			senderId: "teaparty@example.com/hatter",
-			direction: 0,
-			type: borogove.MessageType.MessageChannel,
-			timestamp: "2020-01-01T00:00:00Z",
-		});
-		builder2.sortId = "b0";
-		builder2.to = borogove.JID.parse("alice@example.com");
-		builder2.from = borogove.JID.parse("teaparty@example.com/hatter");
-		builder2.replyTo = [builder.from.asBare()];
-
-		const builder3 = new borogove.ChatMessageBuilder({
-			serverId: "3",
-			serverIdBy: "alice@example.com",
-			senderId: "teaparty@example.com/hatter",
-			direction: 0,
-			type: borogove.MessageType.MessageChannelPrivate,
-			timestamp: "2020-01-01T00:00:03Z",
-		});
-		builder3.sortId = "Z~";
-		builder3.to = borogove.JID.parse("alice@example.com");
-		builder3.from = borogove.JID.parse("teaparty@example.com/hatter");
-		builder3.replyTo = [builder.from.asBare()];
-
-		const builder4 = new borogove.ChatMessageBuilder({
-			serverId: "4",
-			serverIdBy: "teaparty@example.com",
-			senderId: "teaparty@example.com/hatter",
-			direction: 0,
-			type: borogove.MessageType.MessageChannel,
-			timestamp: "2020-01-01T00:00:04Z",
-		});
-		builder4.sortId = "c0";
-		builder4.to = borogove.JID.parse("alice@example.com");
-		builder4.from = borogove.JID.parse("teaparty@example.com/hatter");
-		builder4.replyTo = [builder.from.asBare()];
-
-		await persistence.storeMessages("alice@example.com", [
-			builder2.build(),
-			builder4.build(),
-			builder3.build(),
-			builder.build(),
-		]);
-
-		return await persistence.getMessagesBefore(
-			"alice@example.com",
-			"teaparty@example.com",
-			builder4.build(),
-		);
-	}, { borogove, persistence });
-
-	expect(result.length).toBe(3);
-	expect(result[0].serverId).toBe("1");
-	expect(result[1].serverId).toBe("2");
-	expect(result[2].serverId).toBe("3");
-});
-
-test("getMessagesBefore a PM", async ({ page, borogove, persistence }) => {
-	const result = await page.evaluate(async ({ borogove, persistence }) => {
-		const builder = new borogove.ChatMessageBuilder({
-			serverId: "1",
-			serverIdBy: "teaparty@example.com",
-			senderId: "teaparty@example.com/hatter",
-			direction: 0,
-			type: borogove.MessageType.MessageChannel,
-			timestamp: "2020-01-01T00:00:00Z",
-		});
-		builder.sortId = "a0";
-		builder.to = borogove.JID.parse("alice@example.com");
-		builder.from = borogove.JID.parse("teaparty@example.com/hatter");
-		builder.replyTo = [builder.from.asBare()];
-
-		const builder2 = new borogove.ChatMessageBuilder({
-			serverId: "2",
-			serverIdBy: "teaparty@example.com",
-			senderId: "teaparty@example.com/hatter",
-			direction: 0,
-			type: borogove.MessageType.MessageChannel,
-			timestamp: "2020-01-01T00:00:01Z",
-		});
-		builder2.sortId = "b0";
-		builder2.to = borogove.JID.parse("alice@example.com");
-		builder2.from = borogove.JID.parse("teaparty@example.com/hatter");
-		builder2.replyTo = [builder.from.asBare()];
-
-		const builder3 = new borogove.ChatMessageBuilder({
-			serverId: "3",
-			serverIdBy: "alice@example.com",
-			senderId: "teaparty@example.com/hatter",
-			direction: 0,
-			type: borogove.MessageType.MessageChannelPrivate,
-			timestamp: "2020-01-01T00:00:03Z",
-		});
-		builder3.sortId = "Z~";
-		builder3.to = borogove.JID.parse("alice@example.com");
-		builder3.from = borogove.JID.parse("teaparty@example.com/hatter");
-		builder3.replyTo = [builder.from.asBare()];
-
-		const builder4 = new borogove.ChatMessageBuilder({
-			serverId: "4",
-			serverIdBy: "teaparty@example.com",
-			senderId: "teaparty@example.com/hatter",
-			direction: 0,
-			type: borogove.MessageType.MessageChannel,
-			timestamp: "2020-01-01T00:00:04Z",
-		});
-		builder4.sortId = "c0";
-		builder4.to = borogove.JID.parse("alice@example.com");
-		builder4.from = borogove.JID.parse("teaparty@example.com/hatter");
-		builder4.replyTo = [builder.from.asBare()];
-
-		await persistence.storeMessages("alice@example.com", [
-			builder2.build(),
-			builder4.build(),
-			builder3.build(),
-			builder.build(),
-		]);
-
-		return await persistence.getMessagesBefore(
-			"alice@example.com",
-			"teaparty@example.com",
-			builder3.build(),
-		);
-	}, { borogove, persistence });
-
-	expect(result.length).toBe(2);
-	expect(result[0].serverId).toBe("1");
-	expect(result[1].serverId).toBe("2");
-});
-
-test("getMessagesAfter the start: MUC come back ordered by sortId, PM by timestamp", async ({
-	page,
-	borogove,
-	persistence,
-}) => {
-	const result = await page.evaluate(async ({ borogove, persistence }) => {
-		const builder = new borogove.ChatMessageBuilder({
-			serverId: "1",
-			serverIdBy: "teaparty@example.com",
-			senderId: "teaparty@example.com/hatter",
-			direction: 0,
-			type: borogove.MessageType.MessageChannel,
-			timestamp: "2020-01-01T00:00:00Z",
-		});
-		builder.sortId = "a0";
-		builder.to = borogove.JID.parse("alice@example.com");
-		builder.from = borogove.JID.parse("teaparty@example.com/hatter");
-		builder.replyTo = [builder.from.asBare()];
-
-		const builder2 = new borogove.ChatMessageBuilder({
-			serverId: "2",
-			serverIdBy: "teaparty@example.com",
-			senderId: "teaparty@example.com/hatter",
-			direction: 0,
-			type: borogove.MessageType.MessageChannel,
-			timestamp: "2020-01-01T00:00:01Z",
-		});
-		builder2.sortId = "b0";
-		builder2.to = borogove.JID.parse("alice@example.com");
-		builder2.from = borogove.JID.parse("teaparty@example.com/hatter");
-		builder2.replyTo = [builder.from.asBare()];
-
-		const builder3 = new borogove.ChatMessageBuilder({
-			serverId: "3",
-			serverIdBy: "alice@example.com",
-			senderId: "teaparty@example.com/hatter",
-			direction: 0,
-			type: borogove.MessageType.MessageChannelPrivate,
-			timestamp: "2020-01-01T00:00:03Z",
-		});
-		builder3.sortId = "a1";
-		builder3.to = borogove.JID.parse("alice@example.com");
-		builder3.from = borogove.JID.parse("teaparty@example.com/hatter");
-		builder3.replyTo = [builder.from.asBare()];
-
-		await persistence.storeMessages("alice@example.com", [
-			builder2.build(),
-			builder3.build(),
-			builder.build(),
-		]);
-
-		return await persistence.getMessagesAfter(
-			"alice@example.com",
-			"teaparty@example.com",
-		);
-	}, { borogove, persistence });
-
-	expect(result.length).toBe(3);
-	expect(result[0].serverId).toBe("1");
-	expect(result[1].serverId).toBe("2");
-	expect(result[2].serverId).toBe("3");
-});
-
-test("getMessagesAfter some point: MUC come back ordered by sortId, PM by timestamp", async ({
-	page,
-	borogove,
-	persistence,
-}) => {
-	const result = await page.evaluate(async ({ borogove, persistence }) => {
-		const builder = new borogove.ChatMessageBuilder({
-			serverId: "1",
-			serverIdBy: "teaparty@example.com",
-			senderId: "teaparty@example.com/hatter",
-			direction: 0,
-			type: borogove.MessageType.MessageChannel,
-			timestamp: "2020-01-01T00:00:01Z",
-		});
-		builder.sortId = "a0";
-		builder.to = borogove.JID.parse("alice@example.com");
-		builder.from = borogove.JID.parse("teaparty@example.com/hatter");
-		builder.replyTo = [builder.from.asBare()];
-
-		const builder2 = new borogove.ChatMessageBuilder({
-			serverId: "2",
-			serverIdBy: "teaparty@example.com",
-			senderId: "teaparty@example.com/hatter",
-			direction: 0,
-			type: borogove.MessageType.MessageChannel,
-			timestamp: "2020-01-01T00:00:00Z",
-		});
-		builder2.sortId = "b0";
-		builder2.to = borogove.JID.parse("alice@example.com");
-		builder2.from = borogove.JID.parse("teaparty@example.com/hatter");
-		builder2.replyTo = [builder.from.asBare()];
-
-		const builder3 = new borogove.ChatMessageBuilder({
-			serverId: "3",
-			serverIdBy: "alice@example.com",
-			senderId: "teaparty@example.com/hatter",
-			direction: 0,
-			type: borogove.MessageType.MessageChannelPrivate,
-			timestamp: "2020-01-01T00:00:03Z",
-		});
-		builder3.sortId = "Z~";
-		builder3.to = borogove.JID.parse("alice@example.com");
-		builder3.from = borogove.JID.parse("teaparty@example.com/hatter");
-		builder3.replyTo = [builder.from.asBare()];
-
-		const builder4 = new borogove.ChatMessageBuilder({
-			serverId: "4",
-			serverIdBy: "teaparty@example.com",
-			senderId: "teaparty@example.com/hatter",
-			direction: 0,
-			type: borogove.MessageType.MessageChannel,
-			timestamp: "2020-01-01T00:00:04Z",
-		});
-		builder4.sortId = "c0";
-		builder4.to = borogove.JID.parse("alice@example.com");
-		builder4.from = borogove.JID.parse("teaparty@example.com/hatter");
-		builder4.replyTo = [builder.from.asBare()];
-
-		await persistence.storeMessages("alice@example.com", [
-			builder2.build(),
-			builder4.build(),
-			builder3.build(),
-			builder.build(),
-		]);
-
-		return await persistence.getMessagesAfter(
-			"alice@example.com",
-			"teaparty@example.com",
-			builder.build(),
-		);
-	}, { borogove, persistence });
-
-	expect(result.length).toBe(3);
-	expect(result[0].serverId).toBe("2");
-	expect(result[1].serverId).toBe("3");
-	expect(result[2].serverId).toBe("4");
-});
-
-test("getMessagesAfter a PM", async ({ page, borogove, persistence }) => {
-	const result = await page.evaluate(async ({ borogove, persistence }) => {
-		const builder = new borogove.ChatMessageBuilder({
-			serverId: "1",
-			serverIdBy: "teaparty@example.com",
-			senderId: "teaparty@example.com/hatter",
-			direction: 0,
-			type: borogove.MessageType.MessageChannel,
-			timestamp: "2020-01-01T00:00:00Z",
-		});
-		builder.sortId = "a0";
-		builder.to = borogove.JID.parse("alice@example.com");
-		builder.from = borogove.JID.parse("teaparty@example.com/hatter");
-		builder.replyTo = [builder.from.asBare()];
-
-		const builder2 = new borogove.ChatMessageBuilder({
-			serverId: "2",
-			serverIdBy: "teaparty@example.com",
-			senderId: "teaparty@example.com/hatter",
-			direction: 0,
-			type: borogove.MessageType.MessageChannel,
-			timestamp: "2020-01-01T00:00:01Z",
-		});
-		builder2.sortId = "b0";
-		builder2.to = borogove.JID.parse("alice@example.com");
-		builder2.from = borogove.JID.parse("teaparty@example.com/hatter");
-		builder2.replyTo = [builder.from.asBare()];
-
-		const builder3 = new borogove.ChatMessageBuilder({
-			serverId: "3",
-			serverIdBy: "alice@example.com",
-			senderId: "teaparty@example.com/hatter",
-			direction: 0,
-			type: borogove.MessageType.MessageChannelPrivate,
-			timestamp: "2020-01-01T00:00:03Z",
-		});
-		builder3.sortId = "Z~";
-		builder3.to = borogove.JID.parse("alice@example.com");
-		builder3.from = borogove.JID.parse("teaparty@example.com/hatter");
-		builder3.replyTo = [builder.from.asBare()];
-
-		const builder4 = new borogove.ChatMessageBuilder({
-			serverId: "4",
-			serverIdBy: "teaparty@example.com",
-			senderId: "teaparty@example.com/hatter",
-			direction: 0,
-			type: borogove.MessageType.MessageChannel,
-			timestamp: "2020-01-01T00:00:04Z",
-		});
-		builder4.sortId = "c0";
-		builder4.to = borogove.JID.parse("alice@example.com");
-		builder4.from = borogove.JID.parse("teaparty@example.com/hatter");
-		builder4.replyTo = [builder.from.asBare()];
-
-		await persistence.storeMessages("alice@example.com", [
-			builder2.build(),
-			builder4.build(),
-			builder3.build(),
-			builder.build(),
-		]);
-
-		return await persistence.getMessagesAfter(
-			"alice@example.com",
-			"teaparty@example.com",
-			builder3.build(),
-		);
-	}, { borogove, persistence });
-
-	expect(result.length).toBe(1);
-	expect(result[0].serverId).toBe("4");
-});
-
-test("storeChats and getChats", async ({ page, borogove, persistence }) => {
-	const result = await page.evaluate(async ({ borogove, persistence }) => {
-		const chat = Object.create(borogove.DirectChat.prototype);
-		chat.chatId = "hatter@example.com";
-		chat.displayName = "The Mad Hatter";
-		chat.trusted = true;
-		chat.presence = new Map();
-		chat.threads = new Map([
-			[null, "Tea Time"],
-			["thread-1", "Introductions"],
-		]);
-
-		await persistence.storeChats("alice@example.com", [chat]);
-		const chats = await persistence.getChats("alice@example.com");
-		return {
-			length: chats.length,
-			chatId: chats[0]?.chatId,
-			displayName: chats[0]?.displayName,
-			trusted: chats[0]?.trusted,
-			klass: chats[0]?.klass,
-			channelSubject: chats[0]?.threads?.get(null),
-			threadSubject: chats[0]?.threads?.get("thread-1"),
-		};
-	}, { borogove, persistence });
-
-	expect(result.length).toBe(1);
-	expect(result.chatId).toBe("hatter@example.com");
-	expect(result.displayName).toBe("The Mad Hatter");
-	expect(result.trusted).toBe(true);
-	expect(result.klass).toBe("DirectChat");
-	expect(result.channelSubject).toBe("Tea Time");
-	expect(result.threadSubject).toBe("Introductions");
-});
-
-test("getMessage by serverId and localId", async ({ page, borogove, persistence }) => {
-	const result = await page.evaluate(async ({ borogove, persistence }) => {
-		const builder = new borogove.ChatMessageBuilder({
-			serverId: "srv1",
-			serverIdBy: "hatter@example.com",
-			localId: "loc1",
-			senderId: "hatter@example.com",
-			direction: 0,
-		});
-		builder.sortId = "a0";
-		builder.to = borogove.JID.parse("alice@example.com");
-		builder.from = borogove.JID.parse("hatter@example.com");
-		builder.recipients = [builder.to];
-		builder.replyTo = [builder.from];
-		const msg = builder.build();
-
-		await persistence.storeMessages("alice@example.com", [msg]);
-
-		const byServerId = await persistence.getMessage(
-			"alice@example.com",
-			"hatter@example.com",
-			"srv1",
-			null,
-		);
-		const byLocalId = await persistence.getMessage(
-			"alice@example.com",
-			"hatter@example.com",
-			null,
-			"loc1",
-		);
-
-		return {
-			byServerId: byServerId
-				? { serverId: byServerId.serverId, localId: byServerId.localId }
-				: null,
-			byLocalId: byLocalId
-				? { serverId: byLocalId.serverId, localId: byLocalId.localId }
-				: null,
-		};
-	}, { borogove, persistence });
-
-	expect(result.byServerId).not.toBeNull();
-	expect(result.byServerId.serverId).toBe("srv1");
-	expect(result.byLocalId).not.toBeNull();
-	expect(result.byLocalId.localId).toBe("loc1");
-});
-
-test("storeReaction", async ({ page, borogove, persistence }) => {
-	const result = await page.evaluate(async ({ borogove, persistence }) => {
-		const builder = new borogove.ChatMessageBuilder({
-			serverId: "srv1",
-			serverIdBy: "hatter@example.com",
-			senderId: "hatter@example.com",
-			direction: 0,
-		});
-		builder.sortId = "a0";
-		builder.to = borogove.JID.parse("alice@example.com");
-		builder.from = borogove.JID.parse("hatter@example.com");
-		builder.recipients = [builder.to];
-		builder.replyTo = [builder.from];
-		await persistence.storeMessages("alice@example.com", [builder.build()]);
-
-		const reaction = new borogove.Reaction(
-			"alice@example.com",
-			"2020-01-01T00:00:01Z",
-			"👍",
-		);
-		const update = new borogove.ReactionUpdate(
-			"up1",
-			"srv1",
-			"hatter@example.com",
-			null,
-			"hatter@example.com",
-			"alice@example.com",
-			"2020-01-01T00:00:01Z",
-			[reaction],
-			borogove.ReactionUpdateKind.EmojiReactions,
-		);
-
-		const msg = await persistence.storeReaction("alice@example.com", update);
-		return {
-			reactions: [...msg.reactions.entries()].map(([k, v]) => ({
-				key: k,
-				count: v.length,
-			})),
-		};
-	}, { borogove, persistence });
-
-	expect(result.reactions.length).toBe(1);
-	expect(result.reactions[0].key).toBe("👍");
-	expect(result.reactions[0].count).toBe(1);
-});
-
-test("updateMessageStatus", async ({ page, borogove, persistence }) => {
-	const result = await page.evaluate(async ({ borogove, persistence }) => {
-		const builder = new borogove.ChatMessageBuilder({
-			localId: "loc1",
-			senderId: "alice@example.com",
-			direction: 1, // MessageSent
-		});
-		builder.sortId = "a0";
-		builder.to = borogove.JID.parse("hatter@example.com");
-		builder.from = borogove.JID.parse("alice@example.com");
-		builder.recipients = [builder.to];
-		builder.replyTo = [builder.from];
-		await persistence.storeMessages("alice@example.com", [builder.build()]);
-
-		const updated = await persistence.updateMessageStatus(
-			"alice@example.com",
-			"loc1",
-			1,
-			"Delivered",
-		); // MessageDelivered
-		return { status: updated.status, statusText: updated.statusText };
-	}, { borogove, persistence });
-
-	expect(result.status).toBe(1);
-	expect(result.statusText).toBe("Delivered");
-});
-
-test("searchMessages", async ({ page, borogove, persistence }) => {
-	const result = await page.evaluate(async ({ borogove, persistence }) => {
-		const builder = new borogove.ChatMessageBuilder({
-			serverId: "srv1",
-			serverIdBy: "hatter@example.com",
-			senderId: "hatter@example.com",
-			direction: 0,
-		});
-		builder.sortId = "a0";
-		builder.text = "Hello world";
-		builder.to = borogove.JID.parse("alice@example.com");
-		builder.from = borogove.JID.parse("hatter@example.com");
-		builder.recipients = [builder.to];
-		builder.replyTo = [builder.from];
-
-		const builder2 = new borogove.ChatMessageBuilder({
-			serverId: "srv2",
-			serverIdBy: "hatter@example.com",
-			senderId: "hatter@example.com",
-			direction: 0,
-		});
-		builder2.sortId = "a1";
-		builder2.text = "Goodbye world";
-		builder2.to = borogove.JID.parse("alice@example.com");
-		builder2.from = borogove.JID.parse("hatter@example.com");
-		builder2.recipients = [builder2.to];
-		builder2.replyTo = [builder2.from];
-
-		await persistence.storeMessages("alice@example.com", [
-			builder.build(),
-			builder2.build(),
-		]);
-
-		const results = await persistence.searchMessages(
-			"alice@example.com",
-			"hatter@example.com",
-			"hello",
-		);
-		return results.map((m) => m.text);
-	}, { borogove, persistence });
-
-	expect(result.length).toBe(1);
-	expect(result[0]).toBe("Hello world");
-});
-
-test("removeAccount and listAccounts", async ({ page, borogove, persistence }) => {
-	const result = await page.evaluate(async ({ borogove, persistence }) => {
-		await persistence.storeLogin("alice@example.com", "client1", "Alice", null);
-		await persistence.storeLogin("bob@example.com", "client2", "Bob", null);
-
-		const accountsBefore = await persistence.listAccounts();
-		await persistence.removeAccount("alice@example.com", true);
-		const accountsAfter = await persistence.listAccounts();
-
-		return { accountsBefore, accountsAfter };
-	}, { borogove, persistence });
-
-	expect(result.accountsBefore).toContain("alice@example.com");
-	expect(result.accountsBefore).toContain("bob@example.com");
-	expect(result.accountsAfter).not.toContain("alice@example.com");
-	expect(result.accountsAfter).toContain("bob@example.com");
-});
-
-test("getChatUnreadDetails", async ({ page, borogove, persistence }) => {
-	const result = await page.evaluate(async ({ borogove, persistence }) => {
-		const chat = Object.create(borogove.DirectChat.prototype);
-		chat.chatId = "hatter@example.com";
-		chat.readUpToId = "srv1";
-		chat.notificationsFiltered = () => false;
-
-		const builder = new borogove.ChatMessageBuilder({
-			serverId: "srv1",
-			serverIdBy: "hatter@example.com",
-			senderId: "hatter@example.com",
-			direction: 0,
-		});
-		builder.sortId = "a0";
-		builder.to = borogove.JID.parse("alice@example.com");
-		builder.from = borogove.JID.parse("hatter@example.com");
-		builder.recipients = [builder.to];
-		builder.replyTo = [builder.from];
-
-		const builder2 = new borogove.ChatMessageBuilder({
-			serverId: "srv2",
-			serverIdBy: "hatter@example.com",
-			senderId: "hatter@example.com",
-			direction: 0,
-		});
-		builder2.sortId = "a1";
-		builder2.to = borogove.JID.parse("alice@example.com");
-		builder2.from = borogove.JID.parse("hatter@example.com");
-		builder2.recipients = [builder2.to];
-		builder2.replyTo = [builder2.from];
-
-		await persistence.storeMessages("alice@example.com", [
-			builder.build(),
-			builder2.build(),
-		]);
-
-		return await persistence.getChatUnreadDetails("alice@example.com", chat);
-	}, { borogove, persistence });
-
-	expect(result.unreadCount).toBe(1);
-	expect(result.message.serverId).toBe("srv2");
-});
-
-test("media storage functions", async ({ page, borogove, persistence }) => {
-	const result = await page.evaluate(async ({ borogove, persistence }) => {
-		function bufferToByteStream(buffer) {
-			let done = false;
-			return new ReadableStream({
-				type: "bytes",
-				async pull(controller) {
-					if (done) {
-						controller.close();
-					} else {
-						controller.enqueue(buffer);
-						done = true;
-					}
-				},
-			});
-		}
-
-		const buffer = new Uint8Array([1, 2, 3]);
-		const sha256 = await crypto.subtle.digest("SHA-256", buffer.buffer);
-		await persistence.storeMedia("image/png", bufferToByteStream(buffer));
-		const hash = new borogove.Hash("sha-256", sha256);
-		const hasBefore = await persistence.hasMedia(hash);
-		await persistence.removeMedia("sha-256", sha256);
-		const hasAfter = await persistence.hasMedia(hash);
-
-		return { hasBefore, hasAfter };
-	}, { borogove, persistence });
-
-	expect(result.hasBefore).toBe(
-		"/.well-known/ni/sha-256/A5BYxvLAy0ksUzsKTRTvd8wPeKvMztUofYShogEc-4E",
-	);
-	expect(result.hasAfter).toBe(null);
-});
-
-test("hydrate message with incomplete replyToMessage keys", async ({
-	page,
-	borogove,
-	persistence,
-}) => {
-	const result = await page.evaluate(async ({ borogove, persistence }) => {
-		const builder = new borogove.ChatMessageBuilder({
-			serverId: "parent",
-			serverIdBy: "hatter@example.com",
-			localId: "loc1",
-			senderId: "hatter@example.com",
-			direction: 0,
-		});
-		builder.sortId = "a0";
-		builder.to = borogove.JID.parse("alice@example.com");
-		builder.from = borogove.JID.parse("hatter@example.com");
-		builder.recipients = [builder.to];
-		builder.replyTo = [builder.from];
-		const parentMsg = builder.build();
-
-		const builder2 = new borogove.ChatMessageBuilder({
-			serverId: "child",
-			serverIdBy: "hatter@example.com",
-			localId: "loc2",
-			senderId: "hatter@example.com",
-			direction: 0,
-		});
-		builder2.sortId = "a1";
-		builder2.to = borogove.JID.parse("alice@example.com");
-		builder2.from = borogove.JID.parse("hatter@example.com");
-		builder2.recipients = [builder2.to];
-		builder2.replyTo = [builder2.from];
-		builder2.replyToMessage = parentMsg;
-		const childMsg = builder2.build();
-
-		await persistence.storeMessages("alice@example.com", [parentMsg, childMsg]);
-
-		const db = await new Promise<IDBDatabase>((resolve, reject) => {
-			const req = indexedDB.open("snikket");
-			req.onsuccess = () => resolve(req.result);
-			req.onerror = () => reject(req.error);
-		});
-		const tx = db.transaction(["messages"], "readwrite");
-		const store = tx.objectStore("messages");
-		const key = ["alice@example.com", "child", "hatter@example.com", "loc2"];
-		const rawChild = await new Promise<{ replyToMessage: string[] }>((resolve) => {
-			const req = store.get(key);
-			req.onsuccess = () => resolve(req.result);
-		});
-
-		rawChild.replyToMessage = ["alice@example.com", "parent", "", ""];
-
-		await new Promise<void>((resolve) => {
-			const req = store.put(rawChild);
-			req.onsuccess = () => resolve();
-		});
-		await new Promise<void>((resolve) => {
-			tx.oncomplete = () => resolve();
-		});
-
-		const retrievedChild = await persistence.getMessage(
-			"alice@example.com",
-			"hatter@example.com",
-			"child",
-			"loc2",
-		);
-		return {
-			hasReply: !!retrievedChild.replyToMessage,
-			replyServerId: retrievedChild.replyToMessage
-				? retrievedChild.replyToMessage.serverId
-				: null,
-		};
-	}, { borogove, persistence });
-
-	expect(result.hasReply).toBe(true);
-	expect(result.replyServerId).toBe("parent");
-});
-
-test("hydrate message with incomplete replyToMessage", async ({ page, borogove, persistence }) => {
-	const result = await page.evaluate(async ({ borogove, persistence }) => {
-		const builder = new borogove.ChatMessageBuilder({
-			serverId: "parent",
-			serverIdBy: "hatter@example.com",
-			localId: "loc1",
-			senderId: "hatter@example.com",
-			direction: 0,
-		});
-		builder.sortId = "a0";
-		builder.to = borogove.JID.parse("alice@example.com");
-		builder.from = borogove.JID.parse("hatter@example.com");
-		builder.recipients = [builder.to];
-		builder.replyTo = [builder.from];
-		const parentStub = builder.build();
-
-		builder.setBody(borogove.Html.text("Hello"));
-		const parentMsg = builder.build();
-
-		const builder2 = new borogove.ChatMessageBuilder({
-			serverId: "child",
-			serverIdBy: "hatter@example.com",
-			localId: "loc2",
-			senderId: "hatter@example.com",
-			direction: 0,
-		});
-		builder2.sortId = "a1";
-		builder2.to = borogove.JID.parse("alice@example.com");
-		builder2.from = borogove.JID.parse("hatter@example.com");
-		builder2.recipients = [builder2.to];
-		builder2.replyTo = [builder2.from];
-		builder2.replyToMessage = parentStub;
-		const childMsg = builder2.build();
-
-		await persistence.storeMessages("alice@example.com", [parentMsg]);
-		const [childStored] = await persistence.storeMessages("alice@example.com", [
-			childMsg,
-		]);
-
-		return childStored.replyToMessage.body().toPlainText();
-	}, { borogove, persistence });
-
-	expect(result).toBe("Hello");
-});
-
-test("storeChats and getChats with status", async ({ page, borogove, persistence }) => {
-	const result = await page.evaluate(async ({ borogove, persistence }) => {
-		const chat = Object.create(borogove.DirectChat.prototype);
-		chat.chatId = "hatter@example.com";
-		chat.displayName = "The Mad Hatter";
-		chat.trusted = true;
-		chat.presence = new Map();
-		chat.threads = new Map();
-		chat.status = new borogove.Status("🎩", "Time for tea!");
-
-		await persistence.storeChats("alice@example.com", [chat]);
-		const chats = await persistence.getChats("alice@example.com");
-		return {
-			length: chats.length,
-			chatId: chats[0]?.chatId,
-			statusEmoji: chats[0]?.status?.emoji,
-			statusText: chats[0]?.status?.text,
-		};
-	}, { borogove, persistence });
-
-	expect(result.length).toBe(1);
-	expect(result.chatId).toBe("hatter@example.com");
-	expect(result.statusEmoji).toBe("🎩");
-	expect(result.statusText).toBe("Time for tea!");
-});
-
-test("storeStreamManamagement and getStreamManagement", async ({ page, borogove, persistence }) => {
-	const result = await page.evaluate(async ({ borogove, persistence }) => {
-		await persistence.storeLogin("alice@example.com", "", "", null); // or updating with SM may not work
-		await persistence.storeStreamManagement(
-			"alice@example.com",
-			new Uint8Array([1, 2, 0, 4]).buffer,
-			"ZZ",
-		);
-		const result = await persistence.getStreamManagement("alice@example.com");
-		return {
-			smIsArrayBuffer: result.sm instanceof ArrayBuffer,
-			smIsEq: result.sm
-				? indexedDB.cmp(result.sm, new Uint8Array([1, 2, 0, 4]).buffer)
-				: "null",
-			sortId: result.sortId,
-		};
-	}, { borogove, persistence });
-
-	expect(result.smIsEq).toBe(0);
-	expect(result.smIsArrayBuffer).toBe(true);
-	expect(result.sortId).toBe("ZZ");
-});
-
-test("getMembers hydrates persisted member data", async ({ page, borogove, persistence }) => {
-	const result = await page.evaluate(async ({ borogove, persistence }) => {
-		const chat = Object.create(borogove.Channel.prototype);
-		chat.chatId = "room-members-1@example.com";
-		chat.getDisplayName = () => "Tea Room";
-
-		const member = {
-			id: "room-members-1@example.com/occ-1",
-			displayName: "Alice",
-			photoUri: "photo:alice",
-			isSelf: false,
-			roles: [{ id: "admin", title: "Admin" }],
-			jid: borogove.JID.parse("alice@example.com"),
-			presence: new Map([
-				[
-					"laptop",
-					borogove.Stanza.parse("<presence><show>away</show></presence>"),
-				],
-			]),
-			chat: { chatId: "alice@example.com" },
-		};
-
-		await persistence.storeMembers("alice@example.com", chat.chatId, [member]);
-		const [stored] = await persistence.getMembers(
-			"alice@example.com",
-			chat,
-			false,
-		);
-
-		return {
-			id: stored.id,
-			displayName: stored.displayName,
-			chatId: stored.chat?.chatId,
-			roleIds: stored.roles.map((r) => r.id),
-			presenceKeys: [...stored.presence.keys()],
-			showPresence: stored.showPresence,
-		};
-	}, { borogove, persistence });
-
-	expect(result.id).toBe("room-members-1@example.com/occ-1");
-	expect(result.displayName).toBe("Alice");
-	expect(result.chatId).toBe("alice@example.com");
-	expect(result.roleIds).toEqual(["admin"]);
-	expect(result.presenceKeys).toEqual(["laptop"]);
-	expect(result.showPresence).toBe(1);
-});
-
-test("storeMemberUpdates merges existing member data", async ({ page, borogove, persistence }) => {
-	const result = await page.evaluate(async ({ borogove, persistence }) => {
-		const chat = Object.create(borogove.Channel.prototype);
-		chat.chatId = "room-members-2@example.com";
-		chat.getDisplayName = () => "Tea Room";
-
-		await persistence.storeMembers("alice@example.com", chat.chatId, [
-			{
-				id: "room-members-2@example.com/occ-1",
-				displayName: "Alice",
-				photoUri: null,
-				isSelf: false,
-				roles: [
-					{ id: "admin", title: "Admin" },
-					{ id: "urn:xmpp:hats:test", title: "Tea Host" },
-				],
-				jid: borogove.JID.parse("alice@example.com"),
-				presence: new Map([["desk", borogove.Stanza.parse("<presence />")]]),
-				chat: { chatId: "alice@example.com" },
-			},
-		]);
-
-		const updates = [
-			new borogove.MemberUpdate(
-				"room-members-2@example.com/occ-1",
-				borogove.JID.parse("alice@example.com"),
-				"Alice Cooper",
-				false,
-				null,
-				new Map([["mobile", borogove.Stanza.parse("<presence />")]]),
-			),
-		];
-
-		const updated = await persistence.storeMemberUpdates(
-			"alice@example.com",
-			chat,
-			updates,
-			false,
-		);
-
-		return {
-			updatedRoleIds: updated[0].roles.map((r) => r.id),
-			updatedPresenceKeys: [...updated[0].presence.keys()].sort(),
-			updatedDisplayName: updated[0].displayName,
-		};
-	}, { borogove, persistence });
-
-	expect(result.updatedRoleIds).toEqual(["urn:xmpp:hats:test"]);
-	expect(result.updatedPresenceKeys).toEqual(["desk", "mobile"]);
-	expect(result.updatedDisplayName).toBe("Alice Cooper");
-});
-
-test("storeMemberUpdates clears omitted full-list affiliations", async ({
-	page,
-	borogove,
-	persistence,
-}) => {
-	const result = await page.evaluate(async ({ borogove, persistence }) => {
-		const chat = Object.create(borogove.Channel.prototype);
-		chat.chatId = "room-members-2b@example.com";
-		chat.getDisplayName = () => "Tea Room";
-
-		await persistence.storeMembers("alice@example.com", chat.chatId, [
-			{
-				id: "room-members-2b@example.com/occ-1",
-				displayName: "Alice",
-				photoUri: null,
-				isSelf: false,
-				roles: [{ id: "admin", title: "Admin" }],
-				jid: borogove.JID.parse("alice@example.com"),
-				presence: new Map(),
-				chat: { chatId: "alice@example.com" },
-			},
-			{
-				id: "room-members-2b@example.com/occ-2",
-				displayName: "Bob",
-				photoUri: null,
-				isSelf: false,
-				roles: [{ id: "owner", title: "Owner" }],
-				jid: borogove.JID.parse("bob@example.com"),
-				presence: new Map(),
-				chat: { chatId: "bob@example.com" },
-			},
-		]);
-
-		await persistence.storeMemberUpdates(
-			"alice@example.com",
-			chat,
-			[
-				new borogove.MemberUpdate(
-					"room-members-2b@example.com/occ-1",
-					borogove.JID.parse("alice@example.com"),
-					"Alice",
-					false,
-					null,
-					new Map(),
-				),
-			],
-			true,
-		);
-		const members = await persistence.getMembers(
-			"alice@example.com",
-			chat,
-			true,
-		);
 
-		return members.find((m) => m.id.endsWith("occ-2")).roles.map((r) => r.id);
-	}, { borogove, persistence });
-
-	expect(result).toEqual([]);
-});
+sharedPersistenceTests(test);
 
-test("storeMemberUpdates matches existing member by true JID", async ({
-	page,
-	borogove,
-	persistence,
-}) => {
+// TODO: Share this with SQLite once direct-chat reply hydration is fixed.
+test("hydrate message with incomplete replyToMessage", async ({ page, borogove, persistence }) => {
 	const result = await page.evaluate(async ({ borogove, persistence }) => {
-		const chat1 = Object.create(borogove.Channel.prototype);
-		chat1.chatId = "room-members-3@example.com";
-		chat1.getDisplayName = () => "Tea Room";
-		const chat2 = Object.create(borogove.Channel.prototype);
-		chat2.chatId = "room-members-4@example.com";
-		chat2.getDisplayName = () => "Other Room";
-
-		await persistence.storeMembers("alice@example.com", chat1.chatId, [
-			{
-				id: "room-members-3@example.com/occ-1",
-				displayName: "Alice",
-				photoUri: null,
-				isSelf: false,
-				roles: [{ id: "admin", title: "Admin" }],
-				jid: borogove.JID.parse("alice@example.com"),
-				presence: new Map([["desk", borogove.Stanza.parse("<presence />")]]),
-				chat: { chatId: "alice@example.com" },
-			},
-			{
-				id: "room-members-4@example.com/occ-1",
-				displayName: "Bob",
-				photoUri: null,
-				isSelf: false,
-				roles: [{ id: "admin", title: "Admin" }],
-				jid: borogove.JID.parse("bob@example.com"),
-				presence: new Map([["phone", borogove.Stanza.parse("<presence />")]]),
-				chat: { chatId: "bob@example.com" },
-			},
-		]);
-
-		await persistence.storeMemberUpdates(
-			"alice@example.com",
-			chat1,
-			[
-				new borogove.MemberUpdate(
-					null,
-					borogove.JID.parse("alice@example.com"),
-					"Alice Renamed",
-					false,
-					null,
-					new Map(),
-				),
-			],
-			false,
-		);
-		const [chat1Member] = await persistence.getMemberDetails(
-			"alice@example.com",
-			chat1,
-			["room-members-3@example.com/occ-1"],
-		);
-
-		return chat1Member.displayName;
-	}, { borogove, persistence });
+		const builder = new borogove.ChatMessageBuilder({
+			serverId: "parent",
+			serverIdBy: "hatter@example.com",
+			localId: "loc1",
+			senderId: "hatter@example.com",
+			direction: 0,
+		});
+		builder.sortId = "a0";
+		builder.to = borogove.JID.parse("alice@example.com");
+		builder.from = borogove.JID.parse("hatter@example.com");
+		builder.recipients = [builder.to];
+		builder.replyTo = [builder.from];
+		const parentStub = builder.build();
 
-	expect(result).toBe("Alice Renamed");
-});
+		builder.setBody(borogove.Html.text("Hello"));
+		const parentMsg = builder.build();
 
-test("clearMemberPresence only clears the targeted chat", async ({ page, borogove, persistence }) => {
-	const result = await page.evaluate(async ({ borogove, persistence }) => {
-		const chat1 = Object.create(borogove.Channel.prototype);
-		chat1.chatId = "room-members-4a@example.com";
-		chat1.getDisplayName = () => "Tea Room";
-		const chat2 = Object.create(borogove.Channel.prototype);
-		chat2.chatId = "room-members-4b@example.com";
-		chat2.getDisplayName = () => "Other Room";
+		const builder2 = new borogove.ChatMessageBuilder({
+			serverId: "child",
+			serverIdBy: "hatter@example.com",
+			localId: "loc2",
+			senderId: "hatter@example.com",
+			direction: 0,
+		});
+		builder2.sortId = "a1";
+		builder2.to = borogove.JID.parse("alice@example.com");
+		builder2.from = borogove.JID.parse("hatter@example.com");
+		builder2.recipients = [builder2.to];
+		builder2.replyTo = [builder2.from];
+		builder2.replyToMessage = parentStub;
+		const childMsg = builder2.build();
 
-		await persistence.storeMembers("alice@example.com", chat1.chatId, [
-			{
-				id: "room-members-4a@example.com/occ-1",
-				displayName: "Alice",
-				photoUri: null,
-				isSelf: false,
-				roles: [{ id: "admin", title: "Admin" }],
-				jid: borogove.JID.parse("alice@example.com"),
-				presence: new Map([["desk", borogove.Stanza.parse("<presence />")]]),
-				chat: { chatId: "alice@example.com" },
-			},
-		]);
-		await persistence.storeMembers("alice@example.com", chat2.chatId, [
-			{
-				id: "room-members-4b@example.com/occ-1",
-				displayName: "Bob",
-				photoUri: null,
-				isSelf: false,
-				roles: [{ id: "admin", title: "Admin" }],
-				jid: borogove.JID.parse("bob@example.com"),
-				presence: new Map([["phone", borogove.Stanza.parse("<presence />")]]),
-				chat: { chatId: "bob@example.com" },
-			},
+		await persistence.storeMessages("alice@example.com", [parentMsg]);
+		const [childStored] = await persistence.storeMessages("alice@example.com", [
+			childMsg,
 		]);
 
-		await persistence.clearMemberPresence("alice@example.com", chat1.chatId);
-		const [chat1Member] = await persistence.getMemberDetails(
-			"alice@example.com",
-			chat1,
-			["room-members-4a@example.com/occ-1"],
-		);
-		const [chat2Member] = await persistence.getMemberDetails(
-			"alice@example.com",
-			chat2,
-			["room-members-4b@example.com/occ-1"],
-		);
-
-		return {
-			chat1PresenceKeys: [...chat1Member.presence.keys()],
-			chat2PresenceKeys: [...chat2Member.presence.keys()],
-		};
+		return childStored.replyToMessage.body().toPlainText();
 	}, { borogove, persistence });
 
-	expect(result.chat1PresenceKeys).toEqual([]);
-	expect(result.chat2PresenceKeys).toEqual(["phone"]);
+	expect(result).toBe("Hello");
 });
 
+
 test("getMembers filters hidden rows for non-moderators", async ({ page, borogove, persistence }) => {
 	const result = await page.evaluate(async ({ borogove, persistence }) => {
 		const chat = Object.create(borogove.Channel.prototype);
@@ -1284,168 +124,81 @@ test("getMembers filters hidden rows for non-moderators", async ({ page, borogov
 	expect(result).toEqual(["Zulu", "Alpha"]);
 });
 
-test("getMembers includes moderator-visible rows", async ({ page, borogove, persistence }) => {
-	const result = await page.evaluate(async ({ borogove, persistence }) => {
-		const chat = Object.create(borogove.Channel.prototype);
-		chat.chatId = "room-members-6@example.com";
-		chat.getDisplayName = () => "Tea Room";
-
-		await persistence.storeMembers("alice@example.com", chat.chatId, [
-			{
-				id: "room-members-6@example.com/owner",
-				displayName: "Zulu",
-				photoUri: null,
-				isSelf: false,
-				roles: [{ id: "owner", title: "Owner" }],
-				jid: borogove.JID.parse("zulu@example.com"),
-				presence: new Map([["desk", borogove.Stanza.parse("<presence />")]]),
-				chat: { chatId: "zulu@example.com" },
-			},
-			{
-				id: "room-members-6@example.com/outcast",
-				displayName: "Banned",
-				photoUri: null,
-				isSelf: false,
-				roles: [{ id: "outcast", title: "Banned" }],
-				jid: borogove.JID.parse("banned@example.com"),
-				presence: new Map([["desk", borogove.Stanza.parse("<presence />")]]),
-				chat: { chatId: "banned@example.com" },
-			},
-			{
-				id: "room-members-6@example.com/guest-offline",
-				displayName: "Guest",
-				photoUri: null,
-				isSelf: false,
-				roles: [{ id: "none", title: "Guest" }],
-				jid: borogove.JID.parse("guest@example.com"),
-				presence: new Map([
-					["desk", borogove.Stanza.parse('<presence type="unavailable" />')],
-				]),
-				chat: { chatId: "guest@example.com" },
-			},
-			{
-				id: "room-members-6@example.com/admin",
-				displayName: "Alpha",
-				photoUri: null,
-				isSelf: false,
-				roles: [{ id: "admin", title: "Admin" }],
-				jid: borogove.JID.parse("alpha@example.com"),
-				presence: new Map([["desk", borogove.Stanza.parse("<presence />")]]),
-				chat: { chatId: "alpha@example.com" },
-			},
-		]);
-
-		const moderator = await persistence.getMembers(
-			"alice@example.com",
-			chat,
-			true,
-		);
-		return moderator.map((m) => m.displayName);
-	}, { borogove, persistence });
-
-	expect(result).toEqual(["Zulu", "Alpha", "Banned"]);
-});
 
-test("getMemberDetails returns null for incomplete rows", async ({ page, borogove, persistence }) => {
+test("hydrate message with incomplete replyToMessage keys", async ({
+	page,
+	borogove,
+	persistence,
+}) => {
 	const result = await page.evaluate(async ({ borogove, persistence }) => {
-		const chat = Object.create(borogove.Channel.prototype);
-		chat.chatId = "room-members-7@example.com";
-		chat.getDisplayName = () => "Tea Room";
+		const builder = new borogove.ChatMessageBuilder({
+			serverId: "parent",
+			serverIdBy: "hatter@example.com",
+			localId: "loc1",
+			senderId: "hatter@example.com",
+			direction: 0,
+		});
+		builder.sortId = "a0";
+		builder.to = borogove.JID.parse("alice@example.com");
+		builder.from = borogove.JID.parse("hatter@example.com");
+		builder.recipients = [builder.to];
+		builder.replyTo = [builder.from];
+		const parentMsg = builder.build();
 
-		await persistence.storeMembers("alice@example.com", chat.chatId, [
-			{
-				id: "room-members-7@example.com/admin",
-				displayName: "Alpha",
-				photoUri: null,
-				isSelf: false,
-				roles: [{ id: "admin", title: "Admin" }],
-				jid: borogove.JID.parse("alpha@example.com"),
-				presence: new Map([["desk", borogove.Stanza.parse("<presence />")]]),
-				chat: { chatId: "alpha@example.com" },
-			},
-		]);
+		const builder2 = new borogove.ChatMessageBuilder({
+			serverId: "child",
+			serverIdBy: "hatter@example.com",
+			localId: "loc2",
+			senderId: "hatter@example.com",
+			direction: 0,
+		});
+		builder2.sortId = "a1";
+		builder2.to = borogove.JID.parse("alice@example.com");
+		builder2.from = borogove.JID.parse("hatter@example.com");
+		builder2.recipients = [builder2.to];
+		builder2.replyTo = [builder2.from];
+		builder2.replyToMessage = parentMsg;
+		const childMsg = builder2.build();
+
+		await persistence.storeMessages("alice@example.com", [parentMsg, childMsg]);
 
-		const tx = indexedDB.open("snikket");
 		const db = await new Promise<IDBDatabase>((resolve, reject) => {
-			tx.onsuccess = () => resolve(tx.result);
-			tx.onerror = () => reject(tx.error);
+			const req = indexedDB.open("snikket");
+			req.onsuccess = () => resolve(req.result);
+			req.onerror = () => reject(req.error);
+		});
+		const tx = db.transaction(["messages"], "readwrite");
+		const store = tx.objectStore("messages");
+		const key = ["alice@example.com", "child", "hatter@example.com", "loc2"];
+		const rawChild = await new Promise<{ replyToMessage: string[] }>((resolve) => {
+			const req = store.get(key);
+			req.onsuccess = () => resolve(req.result);
 		});
-		const write = db.transaction(["members"], "readwrite");
-		write.objectStore("members").put({
-			account: "alice@example.com",
-			chatId: chat.chatId,
-			id: "room-members-7@example.com/incomplete",
-			displayName: "",
-			photoUri: null,
-			isSelf: 0,
-			chat: "",
-			roles: [],
-			presence: new Map(),
-			jid: "",
+
+		rawChild.replyToMessage = ["alice@example.com", "parent", "", ""];
+
+		await new Promise<void>((resolve) => {
+			const req = store.put(rawChild);
+			req.onsuccess = () => resolve();
 		});
-		await new Promise((resolve, reject) => {
-			write.oncomplete = () => resolve(null);
-			write.onerror = () => reject(write.error);
+		await new Promise<void>((resolve) => {
+			tx.oncomplete = () => resolve();
 		});
 
-		const details = await persistence.getMemberDetails(
+		const retrievedChild = await persistence.getMessage(
 			"alice@example.com",
-			chat,
-			[
-				"room-members-7@example.com/admin",
-				"room-members-7@example.com/incomplete",
-			],
+			"hatter@example.com",
+			"child",
+			"loc2",
 		);
-		return details.map((m) => (m ? m.displayName : null));
-	}, { borogove, persistence });
-
-	expect(result).toEqual(["Alpha", null]);
-});
-
-test("storeVoiceRequest and listVoiceRequests", async ({ page, borogove, persistence }) => {
-	const result = await page.evaluate(async ({ borogove, persistence }) => {
-		const chat = Object.create(borogove.Channel.prototype);
-		chat.chatId = "room-voice-requests@example.com";
-		chat.getDisplayName = () => "Tea Room";
-
-		await persistence.storeMembers("alice@example.com", chat.chatId, [
-			{
-				id: "room-voice-requests@example.com/occ-1",
-				displayName: "Bob",
-				photoUri: null,
-				isSelf: false,
-				roles: [{ id: "none", title: "Participant" }],
-				jid: borogove.JID.parse("bob@example.com"),
-				presence: new Map([["desk", borogove.Stanza.parse("<presence />")]]),
-				chat: { chatId: "bob@example.com" },
-			},
-			{
-				id: "room-voice-requests@example.com/occ-2",
-				displayName: "Charlie",
-				photoUri: null,
-				isSelf: false,
-				roles: [{ id: "none", title: "Participant" }],
-				jid: borogove.JID.parse("charlie@example.com"),
-				presence: new Map([["desk", borogove.Stanza.parse("<presence />")]]),
-				chat: { chatId: "charlie@example.com" },
-			}
-		]);
-
-		await persistence.storeVoiceRequest("alice@example.com", chat, "bob@example.com", true);
-		await persistence.storeVoiceRequest("alice@example.com", chat, "charlie@example.com", true);
-
-		const requests1 = await persistence.listVoiceRequests("alice@example.com", chat);
-
-		await persistence.storeVoiceRequest("alice@example.com", chat, "bob@example.com", false);
-
-		const requests2 = await persistence.listVoiceRequests("alice@example.com", chat);
-
 		return {
-			requests1: requests1.map((m) => m.displayName).sort(),
-			requests2: requests2.map((m) => m.displayName).sort(),
+			hasReply: !!retrievedChild.replyToMessage,
+			replyServerId: retrievedChild.replyToMessage
+				? retrievedChild.replyToMessage.serverId
+				: null,
 		};
 	}, { borogove, persistence });
 
-	expect(result.requests1).toEqual(["Bob", "Charlie"]);
-	expect(result.requests2).toEqual(["Charlie"]);
+	expect(result.hasReply).toBe(true);
+	expect(result.replyServerId).toBe("parent");
 });
diff --git a/test/persistence-tests.ts b/test/persistence-tests.ts
new file mode 100644
index 0000000..0e00dac
--- /dev/null
+++ b/test/persistence-tests.ts
@@ -0,0 +1,1466 @@
+import { idbTest, sqliteTest, expect } from "./browser-test";
+
+type PersistenceTest = typeof idbTest | typeof sqliteTest;
+
+export function sharedPersistenceTests(test: PersistenceTest) {
+	test("storeChats and getChats", async ({ page, borogove, persistence }) => {
+		const result = await page.evaluate(
+			async ({ borogove, persistence }) => {
+				const chat = new borogove.DirectChat(
+					null,
+					null,
+					persistence,
+					"hatter@example.com",
+				);
+				chat.displayName = "The Mad Hatter";
+				chat.trusted = true;
+				chat.threads = new Map([
+					[null, "Tea Time"],
+					["thread-1", "Introductions"],
+				]);
+
+				await persistence.storeChats("alice@example.com", [chat]);
+				const chats = await persistence.getChats("alice@example.com");
+				return {
+					length: chats.length,
+					chatId: chats[0]?.chatId,
+					displayName: chats[0]?.displayName,
+					trusted: chats[0]?.trusted,
+					klass: chats[0]?.klass,
+					channelSubject: chats[0]?.threads?.get(null),
+					threadSubject: chats[0]?.threads?.get("thread-1"),
+				};
+			},
+			{ borogove, persistence },
+		);
+
+		expect(result.length).toBe(1);
+		expect(result.chatId).toBe("hatter@example.com");
+		expect(result.displayName).toBe("The Mad Hatter");
+		expect(result.trusted).toBe(true);
+		expect(result.klass).toBe("DirectChat");
+		expect(result.channelSubject).toBe("Tea Time");
+		expect(result.threadSubject).toBe("Introductions");
+	});
+
+	test("storeChats and getChats with status", async ({ page, borogove, persistence }) => {
+		const result = await page.evaluate(
+			async ({ borogove, persistence }) => {
+				const chat = new borogove.DirectChat(
+					null,
+					null,
+					persistence,
+					"hatter@example.com",
+				);
+				chat.displayName = "The Mad Hatter";
+				chat.trusted = true;
+				chat.status = new borogove.Status("🎩", "Time for tea!");
+
+				await persistence.storeChats("alice@example.com", [chat]);
+				const chats = await persistence.getChats("alice@example.com");
+				return {
+					length: chats.length,
+					chatId: chats[0]?.chatId,
+					statusEmoji: chats[0]?.status?.emoji,
+					statusText: chats[0]?.status?.text,
+				};
+			},
+			{ borogove, persistence },
+		);
+
+		expect(result.length).toBe(1);
+		expect(result.chatId).toBe("hatter@example.com");
+		expect(result.statusEmoji).toBe("🎩");
+		expect(result.statusText).toBe("Time for tea!");
+	});
+
+	test("getChats uses member presence for direct chats", async ({ page, borogove, persistence }) => {
+		const result = await page.evaluate(
+			async ({ borogove, persistence }) => {
+				const chat = new borogove.DirectChat(
+					null,
+					null,
+					persistence,
+					"hatter@example.com",
+				);
+				chat.displayName = "The Mad Hatter";
+				chat.trusted = true;
+
+				await persistence.storeChats("alice@example.com", [chat]);
+				await persistence.storeMembers("alice@example.com", chat.chatId, [
+					{
+						id: "hatter@example.com",
+						displayName: "The Mad Hatter",
+						photoUri: null,
+						isSelf: false,
+						roles: [],
+						jid: borogove.JID.parse("hatter@example.com"),
+						presence: new Map([
+							["phone", borogove.Stanza.parse("<presence />")],
+						]),
+						chat: null,
+					},
+				]);
+				const [stored] = await persistence.getChats("alice@example.com");
+				return [...stored.presence.keys()].sort();
+			},
+			{ borogove, persistence },
+		);
+
+		expect(result).toEqual(["phone"]);
+	});
+
+	test("getChats hydrates membersForName and mavUntil from members", async ({
+		page,
+		borogove,
+		persistence,
+		createChannel,
+	}) => {
+		const result = await page.evaluate(
+			async ({ borogove, persistence, createChannel }) => {
+				const chat = createChannel(
+					persistence,
+					"room-chat-hydrate@example.com",
+				);
+				chat.displayName = "Tea Room";
+				chat.trusted = true;
+				chat.mavUntil = "2024-05-01T12:00:00Z";
+
+				await persistence.storeChats("alice@example.com", [chat]);
+				await persistence.storeMembers("alice@example.com", chat.chatId, [
+					{
+						id: chat.chatId,
+						displayName: "Tea Room",
+						photoUri: null,
+						isSelf: false,
+						roles: [],
+						jid: borogove.JID.parse(chat.chatId),
+						presence: new Map(),
+						chat: null,
+					},
+					{
+						id: `${chat.chatId}/self`,
+						displayName: "Myself",
+						photoUri: null,
+						isSelf: true,
+						roles: [{ id: "owner", title: "Owner" }],
+						jid: borogove.JID.parse("alice@example.com"),
+						presence: new Map([
+							["desk", borogove.Stanza.parse("<presence />")],
+						]),
+						chat: null,
+					},
+					{
+						id: `${chat.chatId}/zulu`,
+						displayName: "Zulu",
+						photoUri: null,
+						isSelf: false,
+						roles: [{ id: "admin", title: "Admin" }],
+						jid: borogove.JID.parse("zulu@example.com"),
+						presence: new Map([
+							["desk", borogove.Stanza.parse("<presence />")],
+						]),
+						chat: { chatId: "zulu@example.com" },
+					},
+					{
+						id: `${chat.chatId}/alpha`,
+						displayName: "Alpha",
+						photoUri: null,
+						isSelf: false,
+						roles: [],
+						jid: borogove.JID.parse("alpha@example.com"),
+						presence: new Map([
+							["desk", borogove.Stanza.parse("<presence />")],
+						]),
+						chat: { chatId: "alpha@example.com" },
+					},
+					{
+						id: `${chat.chatId}/hidden`,
+						displayName: "Hidden",
+						photoUri: null,
+						isSelf: false,
+						roles: [{ id: "none", title: "Guest" }],
+						jid: borogove.JID.parse("hidden@example.com"),
+						presence: new Map([
+							["desk", borogove.Stanza.parse("<presence />")],
+						]),
+						chat: { chatId: "hidden@example.com" },
+					},
+				]);
+				const [stored] = await persistence.getChats("alice@example.com");
+				return {
+					mavUntil: stored.mavUntil,
+					membersForName: stored.membersForName.map((m) => m.displayName),
+					presenceKeys: [...stored.presence.keys()].sort(),
+				};
+			},
+			{ borogove, persistence, createChannel },
+		);
+
+		expect(result.mavUntil).toBe("2024-05-01T12:00:00Z");
+		expect(result.membersForName).toEqual(["Alpha", "Zulu"]);
+		expect(result.presenceKeys).toEqual(["desk"]);
+	});
+
+
+	test("hydrate replyToMessage for groupchats", async ({ page, borogove, persistence }) => {
+		const result = await page.evaluate(
+			async ({ borogove, persistence }) => {
+				const builder = new borogove.ChatMessageBuilder({
+					serverId: "parent",
+					serverIdBy: "hatter@example.com",
+					localId: "loc1",
+					senderId: "hatter@example.com",
+					direction: 0,
+				});
+				builder.sortId = "a0";
+				builder.to = borogove.JID.parse("alice@example.com");
+				builder.from = borogove.JID.parse("hatter@example.com");
+				builder.recipients = [builder.to];
+				builder.replyTo = [builder.from];
+				builder.type = borogove.MessageType.MessageChannel;
+				const parentStub = builder.build();
+
+				builder.setBody(borogove.Html.text("Hello"));
+				const parentMsg = builder.build();
+
+				const builder2 = new borogove.ChatMessageBuilder({
+					serverId: "child",
+					serverIdBy: "hatter@example.com",
+					localId: "loc2",
+					senderId: "hatter@example.com",
+					direction: 0,
+				});
+				builder2.sortId = "a1";
+				builder2.to = borogove.JID.parse("alice@example.com");
+				builder2.from = borogove.JID.parse("hatter@example.com");
+				builder2.recipients = [builder2.to];
+				builder2.replyTo = [builder2.from];
+				builder2.replyToMessage = parentStub;
+				builder2.type = borogove.MessageType.MessageChannel;
+
+				await persistence.storeMessages("alice@example.com", [parentMsg]);
+				const [childStored] = await persistence.storeMessages(
+					"alice@example.com",
+					[builder2.build()],
+				);
+				return childStored.replyToMessage.body().toPlainText();
+			},
+			{ borogove, persistence },
+		);
+
+		expect(result).toBe("Hello");
+	});
+
+	test("getMessage by serverId and localId", async ({
+		page,
+		borogove,
+		persistence,
+	}) => {
+		const result = await page.evaluate(
+			async ({ borogove, persistence }) => {
+				const builder = new borogove.ChatMessageBuilder({
+					serverId: "srv1",
+					serverIdBy: "hatter@example.com",
+					localId: "loc1",
+					senderId: "hatter@example.com",
+					direction: 0,
+				});
+				builder.sortId = "a0";
+				builder.to = borogove.JID.parse("alice@example.com");
+				builder.from = borogove.JID.parse("hatter@example.com");
+				builder.recipients = [builder.to];
+				builder.replyTo = [builder.from];
+				const msg = builder.build();
+
+				await persistence.storeMessages("alice@example.com", [msg]);
+
+				const byServerId = await persistence.getMessage(
+					"alice@example.com",
+					"hatter@example.com",
+					"srv1",
+					null,
+				);
+				const byLocalId = await persistence.getMessage(
+					"alice@example.com",
+					"hatter@example.com",
+					null,
+					"loc1",
+				);
+
+				return {
+					byServerId: byServerId
+						? { serverId: byServerId.serverId, localId: byServerId.localId }
+						: null,
+					byLocalId: byLocalId
+						? { serverId: byLocalId.serverId, localId: byLocalId.localId }
+						: null,
+				};
+			},
+			{ borogove, persistence },
+		);
+
+		expect(result.byServerId).not.toBeNull();
+		expect(result.byServerId.serverId).toBe("srv1");
+		expect(result.byLocalId).not.toBeNull();
+		expect(result.byLocalId.localId).toBe("loc1");
+	});
+
+	test("storeReaction", async ({ page, borogove, persistence }) => {
+		const result = await page.evaluate(
+			async ({ borogove, persistence }) => {
+				const builder = new borogove.ChatMessageBuilder({
+					serverId: "srv1",
+					serverIdBy: "hatter@example.com",
+					senderId: "hatter@example.com",
+					direction: 0,
+				});
+				builder.sortId = "a0";
+				builder.to = borogove.JID.parse("alice@example.com");
+				builder.from = borogove.JID.parse("hatter@example.com");
+				builder.recipients = [builder.to];
+				builder.replyTo = [builder.from];
+				await persistence.storeMessages("alice@example.com", [builder.build()]);
+
+				const reaction = new borogove.Reaction(
+					"alice@example.com",
+					"2020-01-01T00:00:01Z",
+					"👍",
+				);
+				const update = new borogove.ReactionUpdate(
+					"up1",
+					"srv1",
+					"hatter@example.com",
+					null,
+					"hatter@example.com",
+					"alice@example.com",
+					"2020-01-01T00:00:01Z",
+					[reaction],
+					borogove.ReactionUpdateKind.EmojiReactions,
+				);
+
+				const msg = await persistence.storeReaction(
+					"alice@example.com",
+					update,
+				);
+				return {
+					reactions: [...msg.reactions.entries()].map(([k, v]) => ({
+						key: k,
+						count: v.length,
+					})),
+				};
+			},
+			{ borogove, persistence },
+		);
+
+		expect(result.reactions.length).toBe(1);
+		expect(result.reactions[0].key).toBe("👍");
+		expect(result.reactions[0].count).toBe(1);
+	});
+
+	test("searchMessages", async ({ page, borogove, persistence }) => {
+		const result = await page.evaluate(
+			async ({ borogove, persistence }) => {
+				const builder = new borogove.ChatMessageBuilder({
+					serverId: "srv1",
+					serverIdBy: "hatter@example.com",
+					senderId: "hatter@example.com",
+					direction: 0,
+				});
+				builder.sortId = "a0";
+				builder.text = "Hello world";
+				builder.to = borogove.JID.parse("alice@example.com");
+				builder.from = borogove.JID.parse("hatter@example.com");
+				builder.recipients = [builder.to];
+				builder.replyTo = [builder.from];
+
+				const builder2 = new borogove.ChatMessageBuilder({
+					serverId: "srv2",
+					serverIdBy: "hatter@example.com",
+					senderId: "hatter@example.com",
+					direction: 0,
+				});
+				builder2.sortId = "a1";
+				builder2.text = "Goodbye world";
+				builder2.to = borogove.JID.parse("alice@example.com");
+				builder2.from = borogove.JID.parse("hatter@example.com");
+				builder2.recipients = [builder2.to];
+				builder2.replyTo = [builder2.from];
+
+				await persistence.storeMessages("alice@example.com", [
+					builder.build(),
+					builder2.build(),
+				]);
+
+				const results = await persistence.searchMessages(
+					"alice@example.com",
+					"hatter@example.com",
+					"hello",
+				);
+				return results.map((m) => m.text);
+			},
+			{ borogove, persistence },
+		);
+
+		expect(result.length).toBe(1);
+		expect(result[0]).toBe("Hello world");
+	});
+
+	test("1:1 come back ordered by sortId", async ({ page, borogove, persistence }) => {
+		const result = await page.evaluate(async ({ borogove, persistence }) => {
+			const builder = new borogove.ChatMessageBuilder({
+				serverId: "1",
+				serverIdBy: "alice@example.com",
+				senderId: "hatter@example.com",
+				direction: 0,
+			});
+			builder.sortId = "a0";
+			builder.to = borogove.JID.parse("alice@example.com");
+			builder.from = borogove.JID.parse("hatter@example.com");
+			builder.replyTo = [builder.from];
+
+			const builder2 = new borogove.ChatMessageBuilder({
+				serverId: "2",
+				serverIdBy: "alice@example.com",
+				senderId: "hatter@example.com",
+				direction: 0,
+			});
+			builder2.sortId = "b0";
+			builder2.to = borogove.JID.parse("alice@example.com");
+			builder2.from = borogove.JID.parse("hatter@example.com");
+			builder2.replyTo = [builder.from];
+
+			await persistence.storeMessages("alice@example.com", [
+				builder2.build(),
+				builder.build(),
+			]);
+
+			return await persistence.getMessagesBefore(
+				"alice@example.com",
+				"hatter@example.com",
+			);
+		}, { borogove, persistence });
+
+		expect(result.length).toBe(2);
+		expect(result[0].serverId).toBe("1");
+		expect(result[1].serverId).toBe("2");
+	});
+
+	test("getMessagesBefore the end: MUC come back ordered by sortId, PM by timestamp", async ({
+		page,
+		borogove,
+		persistence,
+	}) => {
+		const result = await page.evaluate(async ({ borogove, persistence }) => {
+			const builder = new borogove.ChatMessageBuilder({
+				serverId: "1",
+				serverIdBy: "teaparty@example.com",
+				senderId: "teaparty@example.com/hatter",
+				direction: 0,
+				type: borogove.MessageType.MessageChannel,
+				timestamp: "2020-01-01T00:00:01Z",
+			});
+			builder.sortId = "a0";
+			builder.to = borogove.JID.parse("alice@example.com");
+			builder.from = borogove.JID.parse("teaparty@example.com/hatter");
+			builder.replyTo = [builder.from.asBare()];
+
+			const builder2 = new borogove.ChatMessageBuilder({
+				serverId: "2",
+				serverIdBy: "teaparty@example.com",
+				senderId: "teaparty@example.com/hatter",
+				direction: 0,
+				type: borogove.MessageType.MessageChannel,
+				timestamp: "2020-01-01T00:00:00Z",
+			});
+			builder2.sortId = "b0";
+			builder2.to = borogove.JID.parse("alice@example.com");
+			builder2.from = borogove.JID.parse("teaparty@example.com/hatter");
+			builder2.replyTo = [builder.from.asBare()];
+
+			const builder3 = new borogove.ChatMessageBuilder({
+				serverId: "3",
+				serverIdBy: "alice@example.com",
+				senderId: "teaparty@example.com/hatter",
+				direction: 0,
+				type: borogove.MessageType.MessageChannelPrivate,
+				timestamp: "2020-01-01T00:00:03Z",
+			});
+			builder3.sortId = "a0";
+			builder3.to = borogove.JID.parse("alice@example.com");
+			builder3.from = borogove.JID.parse("teaparty@example.com/hatter");
+			builder3.replyTo = [builder.from.asBare()];
+
+			await persistence.storeMessages("alice@example.com", [
+				builder2.build(),
+				builder3.build(),
+				builder.build(),
+			]);
+
+			return await persistence.getMessagesBefore(
+				"alice@example.com",
+				"teaparty@example.com",
+			);
+		}, { borogove, persistence });
+
+		expect(result.length).toBe(3);
+		expect(result[0].serverId).toBe("1");
+		expect(result[1].serverId).toBe("2");
+		expect(result[2].serverId).toBe("3");
+	});
+
+	test("getMessagesBefore some point: MUC come back ordered by sortId, PM by timestamp", async ({
+		page,
+		borogove,
+		persistence,
+	}) => {
+		const result = await page.evaluate(async ({ borogove, persistence }) => {
+			const builder = new borogove.ChatMessageBuilder({
+				serverId: "1",
+				serverIdBy: "teaparty@example.com",
+				senderId: "teaparty@example.com/hatter",
+				direction: 0,
+				type: borogove.MessageType.MessageChannel,
+				timestamp: "2020-01-01T00:00:01Z",
+			});
+			builder.sortId = "a0";
+			builder.to = borogove.JID.parse("alice@example.com");
+			builder.from = borogove.JID.parse("teaparty@example.com/hatter");
+			builder.replyTo = [builder.from.asBare()];
+
+			const builder2 = new borogove.ChatMessageBuilder({
+				serverId: "2",
+				serverIdBy: "teaparty@example.com",
+				senderId: "teaparty@example.com/hatter",
+				direction: 0,
+				type: borogove.MessageType.MessageChannel,
+				timestamp: "2020-01-01T00:00:00Z",
+			});
+			builder2.sortId = "b0";
+			builder2.to = borogove.JID.parse("alice@example.com");
+			builder2.from = borogove.JID.parse("teaparty@example.com/hatter");
+			builder2.replyTo = [builder.from.asBare()];
+
+			const builder3 = new borogove.ChatMessageBuilder({
+				serverId: "3",
+				serverIdBy: "alice@example.com",
+				senderId: "teaparty@example.com/hatter",
+				direction: 0,
+				type: borogove.MessageType.MessageChannelPrivate,
+				timestamp: "2020-01-01T00:00:03Z",
+			});
+			builder3.sortId = "Z~";
+			builder3.to = borogove.JID.parse("alice@example.com");
+			builder3.from = borogove.JID.parse("teaparty@example.com/hatter");
+			builder3.replyTo = [builder.from.asBare()];
+
+			const builder4 = new borogove.ChatMessageBuilder({
+				serverId: "4",
+				serverIdBy: "teaparty@example.com",
+				senderId: "teaparty@example.com/hatter",
+				direction: 0,
+				type: borogove.MessageType.MessageChannel,
+				timestamp: "2020-01-01T00:00:04Z",
+			});
+			builder4.sortId = "c0";
+			builder4.to = borogove.JID.parse("alice@example.com");
+			builder4.from = borogove.JID.parse("teaparty@example.com/hatter");
+			builder4.replyTo = [builder.from.asBare()];
+
+			await persistence.storeMessages("alice@example.com", [
+				builder2.build(),
+				builder4.build(),
+				builder3.build(),
+				builder.build(),
+			]);
+
+			return await persistence.getMessagesBefore(
+				"alice@example.com",
+				"teaparty@example.com",
+				builder4.build(),
+			);
+		}, { borogove, persistence });
+
+		expect(result.length).toBe(3);
+		expect(result[0].serverId).toBe("1");
+		expect(result[1].serverId).toBe("2");
+		expect(result[2].serverId).toBe("3");
+	});
+
+	test("getMessagesBefore a PM", async ({ page, borogove, persistence }) => {
+		const result = await page.evaluate(async ({ borogove, persistence }) => {
+			const builder = new borogove.ChatMessageBuilder({
+				serverId: "1",
+				serverIdBy: "teaparty@example.com",
+				senderId: "teaparty@example.com/hatter",
+				direction: 0,
+				type: borogove.MessageType.MessageChannel,
+				timestamp: "2020-01-01T00:00:00Z",
+			});
+			builder.sortId = "a0";
+			builder.to = borogove.JID.parse("alice@example.com");
+			builder.from = borogove.JID.parse("teaparty@example.com/hatter");
+			builder.replyTo = [builder.from.asBare()];
+
+			const builder2 = new borogove.ChatMessageBuilder({
+				serverId: "2",
+				serverIdBy: "teaparty@example.com",
+				senderId: "teaparty@example.com/hatter",
+				direction: 0,
+				type: borogove.MessageType.MessageChannel,
+				timestamp: "2020-01-01T00:00:01Z",
+			});
+			builder2.sortId = "b0";
+			builder2.to = borogove.JID.parse("alice@example.com");
+			builder2.from = borogove.JID.parse("teaparty@example.com/hatter");
+			builder2.replyTo = [builder.from.asBare()];
+
+			const builder3 = new borogove.ChatMessageBuilder({
+				serverId: "3",
+				serverIdBy: "alice@example.com",
+				senderId: "teaparty@example.com/hatter",
+				direction: 0,
+				type: borogove.MessageType.MessageChannelPrivate,
+				timestamp: "2020-01-01T00:00:03Z",
+			});
+			builder3.sortId = "Z~";
+			builder3.to = borogove.JID.parse("alice@example.com");
+			builder3.from = borogove.JID.parse("teaparty@example.com/hatter");
+			builder3.replyTo = [builder.from.asBare()];
+
+			const builder4 = new borogove.ChatMessageBuilder({
+				serverId: "4",
+				serverIdBy: "teaparty@example.com",
+				senderId: "teaparty@example.com/hatter",
+				direction: 0,
+				type: borogove.MessageType.MessageChannel,
+				timestamp: "2020-01-01T00:00:04Z",
+			});
+			builder4.sortId = "c0";
+			builder4.to = borogove.JID.parse("alice@example.com");
+			builder4.from = borogove.JID.parse("teaparty@example.com/hatter");
+			builder4.replyTo = [builder.from.asBare()];
+
+			await persistence.storeMessages("alice@example.com", [
+				builder2.build(),
+				builder4.build(),
+				builder3.build(),
+				builder.build(),
+			]);
+
+			return await persistence.getMessagesBefore(
+				"alice@example.com",
+				"teaparty@example.com",
+				builder3.build(),
+			);
+		}, { borogove, persistence });
+
+		expect(result.length).toBe(2);
+		expect(result[0].serverId).toBe("1");
+		expect(result[1].serverId).toBe("2");
+	});
+
+	test("getMessagesAfter the start: MUC come back ordered by sortId, PM by timestamp", async ({
+		page,
+		borogove,
+		persistence,
+	}) => {
+		const result = await page.evaluate(async ({ borogove, persistence }) => {
+			const builder = new borogove.ChatMessageBuilder({
+				serverId: "1",
+				serverIdBy: "teaparty@example.com",
+				senderId: "teaparty@example.com/hatter",
+				direction: 0,
+				type: borogove.MessageType.MessageChannel,
+				timestamp: "2020-01-01T00:00:00Z",
+			});
+			builder.sortId = "a0";
+			builder.to = borogove.JID.parse("alice@example.com");
+			builder.from = borogove.JID.parse("teaparty@example.com/hatter");
+			builder.replyTo = [builder.from.asBare()];
+
+			const builder2 = new borogove.ChatMessageBuilder({
+				serverId: "2",
+				serverIdBy: "teaparty@example.com",
+				senderId: "teaparty@example.com/hatter",
+				direction: 0,
+				type: borogove.MessageType.MessageChannel,
+				timestamp: "2020-01-01T00:00:01Z",
+			});
+			builder2.sortId = "b0";
+			builder2.to = borogove.JID.parse("alice@example.com");
+			builder2.from = borogove.JID.parse("teaparty@example.com/hatter");
+			builder2.replyTo = [builder.from.asBare()];
+
+			const builder3 = new borogove.ChatMessageBuilder({
+				serverId: "3",
+				serverIdBy: "alice@example.com",
+				senderId: "teaparty@example.com/hatter",
+				direction: 0,
+				type: borogove.MessageType.MessageChannelPrivate,
+				timestamp: "2020-01-01T00:00:03Z",
+			});
+			builder3.sortId = "a1";
+			builder3.to = borogove.JID.parse("alice@example.com");
+			builder3.from = borogove.JID.parse("teaparty@example.com/hatter");
+			builder3.replyTo = [builder.from.asBare()];
+
+			await persistence.storeMessages("alice@example.com", [
+				builder2.build(),
+				builder3.build(),
+				builder.build(),
+			]);
+
+			return await persistence.getMessagesAfter(
+				"alice@example.com",
+				"teaparty@example.com",
+			);
+		}, { borogove, persistence });
+
+		expect(result.length).toBe(3);
+		expect(result[0].serverId).toBe("1");
+		expect(result[1].serverId).toBe("2");
+		expect(result[2].serverId).toBe("3");
+	});
+
+	test("getMessagesAfter some point: MUC come back ordered by sortId, PM by timestamp", async ({
+		page,
+		borogove,
+		persistence,
+	}) => {
+		const result = await page.evaluate(async ({ borogove, persistence }) => {
+			const builder = new borogove.ChatMessageBuilder({
+				serverId: "1",
+				serverIdBy: "teaparty@example.com",
+				senderId: "teaparty@example.com/hatter",
+				direction: 0,
+				type: borogove.MessageType.MessageChannel,
+				timestamp: "2020-01-01T00:00:01Z",
+			});
+			builder.sortId = "a0";
+			builder.to = borogove.JID.parse("alice@example.com");
+			builder.from = borogove.JID.parse("teaparty@example.com/hatter");
+			builder.replyTo = [builder.from.asBare()];
+
+			const builder2 = new borogove.ChatMessageBuilder({
+				serverId: "2",
+				serverIdBy: "teaparty@example.com",
+				senderId: "teaparty@example.com/hatter",
+				direction: 0,
+				type: borogove.MessageType.MessageChannel,
+				timestamp: "2020-01-01T00:00:00Z",
+			});
+			builder2.sortId = "b0";
+			builder2.to = borogove.JID.parse("alice@example.com");
+			builder2.from = borogove.JID.parse("teaparty@example.com/hatter");
+			builder2.replyTo = [builder.from.asBare()];
+
+			const builder3 = new borogove.ChatMessageBuilder({
+				serverId: "3",
+				serverIdBy: "alice@example.com",
+				senderId: "teaparty@example.com/hatter",
+				direction: 0,
+				type: borogove.MessageType.MessageChannelPrivate,
+				timestamp: "2020-01-01T00:00:03Z",
+			});
+			builder3.sortId = "Z~";
+			builder3.to = borogove.JID.parse("alice@example.com");
+			builder3.from = borogove.JID.parse("teaparty@example.com/hatter");
+			builder3.replyTo = [builder.from.asBare()];
+
+			const builder4 = new borogove.ChatMessageBuilder({
+				serverId: "4",
+				serverIdBy: "teaparty@example.com",
+				senderId: "teaparty@example.com/hatter",
+				direction: 0,
+				type: borogove.MessageType.MessageChannel,
+				timestamp: "2020-01-01T00:00:04Z",
+			});
+			builder4.sortId = "c0";
+			builder4.to = borogove.JID.parse("alice@example.com");
+			builder4.from = borogove.JID.parse("teaparty@example.com/hatter");
+			builder4.replyTo = [builder.from.asBare()];
+
+			await persistence.storeMessages("alice@example.com", [
+				builder2.build(),
+				builder4.build(),
+				builder3.build(),
+				builder.build(),
+			]);
+
+			return await persistence.getMessagesAfter(
+				"alice@example.com",
+				"teaparty@example.com",
+				builder.build(),
+			);
+		}, { borogove, persistence });
+
+		expect(result.length).toBe(3);
+		expect(result[0].serverId).toBe("2");
+		expect(result[1].serverId).toBe("3");
+		expect(result[2].serverId).toBe("4");
+	});
+
+	test("getMessagesAfter a PM", async ({ page, borogove, persistence }) => {
+		const result = await page.evaluate(async ({ borogove, persistence }) => {
+			const builder = new borogove.ChatMessageBuilder({
+				serverId: "1",
+				serverIdBy: "teaparty@example.com",
+				senderId: "teaparty@example.com/hatter",
+				direction: 0,
+				type: borogove.MessageType.MessageChannel,
+				timestamp: "2020-01-01T00:00:00Z",
+			});
+			builder.sortId = "a0";
+			builder.to = borogove.JID.parse("alice@example.com");
+			builder.from = borogove.JID.parse("teaparty@example.com/hatter");
+			builder.replyTo = [builder.from.asBare()];
+
+			const builder2 = new borogove.ChatMessageBuilder({
+				serverId: "2",
+				serverIdBy: "teaparty@example.com",
+				senderId: "teaparty@example.com/hatter",
+				direction: 0,
+				type: borogove.MessageType.MessageChannel,
+				timestamp: "2020-01-01T00:00:01Z",
+			});
+			builder2.sortId = "b0";
+			builder2.to = borogove.JID.parse("alice@example.com");
+			builder2.from = borogove.JID.parse("teaparty@example.com/hatter");
+			builder2.replyTo = [builder.from.asBare()];
+
+			const builder3 = new borogove.ChatMessageBuilder({
+				serverId: "3",
+				serverIdBy: "alice@example.com",
+				senderId: "teaparty@example.com/hatter",
+				direction: 0,
+				type: borogove.MessageType.MessageChannelPrivate,
+				timestamp: "2020-01-01T00:00:03Z",
+			});
+			builder3.sortId = "Z~";
+			builder3.to = borogove.JID.parse("alice@example.com");
+			builder3.from = borogove.JID.parse("teaparty@example.com/hatter");
+			builder3.replyTo = [builder.from.asBare()];
+
+			const builder4 = new borogove.ChatMessageBuilder({
+				serverId: "4",
+				serverIdBy: "teaparty@example.com",
+				senderId: "teaparty@example.com/hatter",
+				direction: 0,
+				type: borogove.MessageType.MessageChannel,
+				timestamp: "2020-01-01T00:00:04Z",
+			});
+			builder4.sortId = "c0";
+			builder4.to = borogove.JID.parse("alice@example.com");
+			builder4.from = borogove.JID.parse("teaparty@example.com/hatter");
+			builder4.replyTo = [builder.from.asBare()];
+
+			await persistence.storeMessages("alice@example.com", [
+				builder2.build(),
+				builder4.build(),
+				builder3.build(),
+				builder.build(),
+			]);
+
+			return await persistence.getMessagesAfter(
+				"alice@example.com",
+				"teaparty@example.com",
+				builder3.build(),
+			);
+		}, { borogove, persistence });
+
+		expect(result.length).toBe(1);
+		expect(result[0].serverId).toBe("4");
+	});
+
+	test("updateMessageStatus", async ({ page, borogove, persistence }) => {
+		const result = await page.evaluate(async ({ borogove, persistence }) => {
+			const builder = new borogove.ChatMessageBuilder({
+				localId: "loc1",
+				senderId: "alice@example.com",
+				direction: 1, // MessageSent
+			});
+			builder.sortId = "a0";
+			builder.to = borogove.JID.parse("hatter@example.com");
+			builder.from = borogove.JID.parse("alice@example.com");
+			builder.recipients = [builder.to];
+			builder.replyTo = [builder.from];
+			await persistence.storeMessages("alice@example.com", [builder.build()]);
+
+			const updated = await persistence.updateMessageStatus(
+				"alice@example.com",
+				"loc1",
+				1,
+				"Delivered",
+			); // MessageDelivered
+			return { status: updated.status, statusText: updated.statusText };
+		}, { borogove, persistence });
+
+		expect(result.status).toBe(1);
+		expect(result.statusText).toBe("Delivered");
+	});
+
+	test("removeAccount and listAccounts", async ({ page, borogove, persistence }) => {
+		const result = await page.evaluate(async ({ borogove, persistence }) => {
+			await persistence.storeLogin("alice@example.com", "client1", "Alice", null);
+			await persistence.storeLogin("bob@example.com", "client2", "Bob", null);
+
+			const accountsBefore = await persistence.listAccounts();
+			await persistence.removeAccount("alice@example.com", true);
+			const accountsAfter = await persistence.listAccounts();
+
+			return { accountsBefore, accountsAfter };
+		}, { borogove, persistence });
+
+		expect(result.accountsBefore).toContain("alice@example.com");
+		expect(result.accountsBefore).toContain("bob@example.com");
+		expect(result.accountsAfter).not.toContain("alice@example.com");
+		expect(result.accountsAfter).toContain("bob@example.com");
+	});
+
+	test("getChatUnreadDetails", async ({ page, borogove, persistence }) => {
+		const result = await page.evaluate(async ({ borogove, persistence }) => {
+			const chat = Object.create(borogove.DirectChat.prototype);
+			chat.chatId = "hatter@example.com";
+			chat.readUpToId = "srv1";
+			chat.notificationsFiltered = () => false;
+
+			const builder = new borogove.ChatMessageBuilder({
+				serverId: "srv1",
+				serverIdBy: "hatter@example.com",
+				senderId: "hatter@example.com",
+				direction: 0,
+			});
+			builder.sortId = "a0";
+			builder.to = borogove.JID.parse("alice@example.com");
+			builder.from = borogove.JID.parse("hatter@example.com");
+			builder.recipients = [builder.to];
+			builder.replyTo = [builder.from];
+
+			const builder2 = new borogove.ChatMessageBuilder({
+				serverId: "srv2",
+				serverIdBy: "hatter@example.com",
+				senderId: "hatter@example.com",
+				direction: 0,
+			});
+			builder2.sortId = "a1";
+			builder2.to = borogove.JID.parse("alice@example.com");
+			builder2.from = borogove.JID.parse("hatter@example.com");
+			builder2.recipients = [builder2.to];
+			builder2.replyTo = [builder2.from];
+
+			await persistence.storeMessages("alice@example.com", [
+				builder.build(),
+				builder2.build(),
+			]);
+
+			return await persistence.getChatUnreadDetails("alice@example.com", chat);
+		}, { borogove, persistence });
+
+		expect(result.unreadCount).toBe(1);
+		expect(result.message.serverId).toBe("srv2");
+	});
+
+	test("media storage functions", async ({ page, borogove, persistence }) => {
+		const result = await page.evaluate(async ({ borogove, persistence }) => {
+			function bufferToByteStream(buffer) {
+				let done = false;
+				return new ReadableStream({
+					type: "bytes",
+					async pull(controller) {
+						if (done) {
+							controller.close();
+						} else {
+							controller.enqueue(buffer);
+							done = true;
+						}
+					},
+				});
+			}
+
+			const buffer = new Uint8Array([1, 2, 3]);
+			const sha256 = await crypto.subtle.digest("SHA-256", buffer.buffer);
+			await persistence.storeMedia("image/png", bufferToByteStream(buffer));
+			const hash = new borogove.Hash("sha-256", sha256);
+			const hasBefore = await persistence.hasMedia(hash);
+			await persistence.removeMedia("sha-256", sha256);
+			const hasAfter = await persistence.hasMedia(hash);
+
+			return { hasBefore, hasAfter };
+		}, { borogove, persistence });
+
+		expect(result.hasBefore).toBe(
+			"/.well-known/ni/sha-256/A5BYxvLAy0ksUzsKTRTvd8wPeKvMztUofYShogEc-4E",
+		);
+		expect(result.hasAfter).toBe(null);
+	});
+
+	test("storeStreamManamagement and getStreamManagement", async ({ page, borogove, persistence }) => {
+		const result = await page.evaluate(async ({ borogove, persistence }) => {
+			await persistence.storeLogin("alice@example.com", "", "", null); // or updating with SM may not work
+			await persistence.storeStreamManagement(
+				"alice@example.com",
+				new Uint8Array([1, 2, 0, 4]).buffer,
+				"ZZ",
+			);
+			const result = await persistence.getStreamManagement("alice@example.com");
+			return {
+				smIsArrayBuffer: result.sm instanceof ArrayBuffer,
+				smIsEq: result.sm
+					? indexedDB.cmp(result.sm, new Uint8Array([1, 2, 0, 4]).buffer)
+					: "null",
+				sortId: result.sortId,
+			};
+		}, { borogove, persistence });
+
+		expect(result.smIsEq).toBe(0);
+		expect(result.smIsArrayBuffer).toBe(true);
+		expect(result.sortId).toBe("ZZ");
+	});
+
+	test("getMembers hydrates persisted member data", async ({ page, borogove, persistence }) => {
+		const result = await page.evaluate(async ({ borogove, persistence }) => {
+			const chat = Object.create(borogove.Channel.prototype);
+			chat.chatId = "room-members-1@example.com";
+			chat.getDisplayName = () => "Tea Room";
+
+			const member = {
+				id: "room-members-1@example.com/occ-1",
+				displayName: "Alice",
+				photoUri: "photo:alice",
+				isSelf: false,
+				roles: [{ id: "admin", title: "Admin" }],
+				jid: borogove.JID.parse("alice@example.com"),
+				presence: new Map([
+					[
+						"laptop",
+						borogove.Stanza.parse("<presence><show>away</show></presence>"),
+					],
+				]),
+				chat: { chatId: "alice@example.com" },
+			};
+
+			await persistence.storeMembers("alice@example.com", chat.chatId, [member]);
+			const [stored] = await persistence.getMembers(
+				"alice@example.com",
+				chat,
+				false,
+			);
+
+			return {
+				id: stored.id,
+				displayName: stored.displayName,
+				chatId: stored.chat?.chatId,
+				roleIds: stored.roles.map((r) => r.id),
+				presenceKeys: [...stored.presence.keys()],
+				showPresence: stored.showPresence,
+			};
+		}, { borogove, persistence });
+
+		expect(result.id).toBe("room-members-1@example.com/occ-1");
+		expect(result.displayName).toBe("Alice");
+		expect(result.chatId).toBe("alice@example.com");
+		expect(result.roleIds).toEqual(["admin"]);
+		expect(result.presenceKeys).toEqual(["laptop"]);
+		expect(result.showPresence).toBe(1);
+	});
+
+	test("getMemberDetails returns null for incomplete rows", async ({
+		page,
+		borogove,
+		createChannel,
+		persistence,
+		storeIncompleteMember,
+	}) => {
+		const result = await page.evaluate(
+			async ({
+				borogove,
+				createChannel,
+				persistence,
+				storeIncompleteMember,
+			}) => {
+				const chat = createChannel(
+					persistence,
+					"room-members-7@example.com",
+				);
+				chat.displayName = "A Chat";
+				chat.trusted = true;
+
+				await persistence.storeMembers("alice@example.com", chat.chatId, [
+					{
+						id: "room-members-7@example.com/admin",
+						displayName: "Alpha",
+						photoUri: null,
+						isSelf: false,
+						roles: [{ id: "admin", title: "Admin" }],
+						jid: borogove.JID.parse("alpha@example.com"),
+						presence: new Map([
+							["desk", borogove.Stanza.parse("<presence />")],
+						]),
+						chat: { chatId: "alpha@example.com" },
+					},
+				]);
+
+				await storeIncompleteMember(chat.chatId);
+				const details = await persistence.getMemberDetails(
+					"alice@example.com",
+					chat,
+					[
+						"room-members-7@example.com/admin",
+						"room-members-7@example.com/incomplete",
+					],
+				);
+				return details.map((m) => (m ? m.displayName : null));
+			},
+			{
+				borogove,
+				createChannel,
+				persistence,
+				storeIncompleteMember,
+			},
+		);
+
+		expect(result).toEqual(["Alpha", null]);
+	});
+
+	test("storeMemberUpdates merges existing member data", async ({ page, borogove, persistence }) => {
+		const result = await page.evaluate(async ({ borogove, persistence }) => {
+			const chat = Object.create(borogove.Channel.prototype);
+			chat.chatId = "room-members-2@example.com";
+			chat.getDisplayName = () => "Tea Room";
+
+			await persistence.storeMembers("alice@example.com", chat.chatId, [
+				{
+					id: "room-members-2@example.com/occ-1",
+					displayName: "Alice",
+					photoUri: null,
+					isSelf: false,
+					roles: [
+						{ id: "admin", title: "Admin" },
+						{ id: "urn:xmpp:hats:test", title: "Tea Host" },
+					],
+					jid: borogove.JID.parse("alice@example.com"),
+					presence: new Map([["desk", borogove.Stanza.parse("<presence />")]]),
+					chat: { chatId: "alice@example.com" },
+				},
+			]);
+
+			const updates = [
+				new borogove.MemberUpdate(
+					"room-members-2@example.com/occ-1",
+					borogove.JID.parse("alice@example.com"),
+					"Alice Cooper",
+					false,
+					null,
+					new Map([["mobile", borogove.Stanza.parse("<presence />")]]),
+				),
+			];
+
+			const updated = await persistence.storeMemberUpdates(
+				"alice@example.com",
+				chat,
+				updates,
+				false,
+			);
+
+			return {
+				updatedRoleIds: updated[0].roles.map((r) => r.id),
+				updatedPresenceKeys: [...updated[0].presence.keys()].sort(),
+				updatedDisplayName: updated[0].displayName,
+			};
+		}, { borogove, persistence });
+
+		expect(result.updatedRoleIds).toEqual(["urn:xmpp:hats:test"]);
+		expect(result.updatedPresenceKeys).toEqual(["desk", "mobile"]);
+		expect(result.updatedDisplayName).toBe("Alice Cooper");
+	});
+
+	test("storeMemberUpdates clears omitted full-list affiliations", async ({
+		page,
+		borogove,
+		persistence,
+	}) => {
+		const result = await page.evaluate(async ({ borogove, persistence }) => {
+			const chat = Object.create(borogove.Channel.prototype);
+			chat.chatId = "room-members-2b@example.com";
+			chat.getDisplayName = () => "Tea Room";
+
+			await persistence.storeMembers("alice@example.com", chat.chatId, [
+				{
+					id: "room-members-2b@example.com/occ-1",
+					displayName: "Alice",
+					photoUri: null,
+					isSelf: false,
+					roles: [{ id: "admin", title: "Admin" }],
+					jid: borogove.JID.parse("alice@example.com"),
+					presence: new Map(),
+					chat: { chatId: "alice@example.com" },
+				},
+				{
+					id: "room-members-2b@example.com/occ-2",
+					displayName: "Bob",
+					photoUri: null,
+					isSelf: false,
+					roles: [{ id: "owner", title: "Owner" }],
+					jid: borogove.JID.parse("bob@example.com"),
+					presence: new Map(),
+					chat: { chatId: "bob@example.com" },
+				},
+			]);
+
+			await persistence.storeMemberUpdates(
+				"alice@example.com",
+				chat,
+				[
+					new borogove.MemberUpdate(
+						"room-members-2b@example.com/occ-1",
+						borogove.JID.parse("alice@example.com"),
+						"Alice",
+						false,
+						null,
+						new Map(),
+					),
+				],
+				true,
+			);
+			const members = await persistence.getMembers(
+				"alice@example.com",
+				chat,
+				true,
+			);
+
+			return members.find((m) => m.id.endsWith("occ-2")).roles.map((r) => r.id);
+		}, { borogove, persistence });
+
+		expect(result).toEqual([]);
+	});
+
+	test("storeMemberUpdates matches existing member by true JID", async ({
+		page,
+		borogove,
+		persistence,
+	}) => {
+		const result = await page.evaluate(async ({ borogove, persistence }) => {
+			const chat1 = Object.create(borogove.Channel.prototype);
+			chat1.chatId = "room-members-3@example.com";
+			chat1.getDisplayName = () => "Tea Room";
+			const chat2 = Object.create(borogove.Channel.prototype);
+			chat2.chatId = "room-members-4@example.com";
+			chat2.getDisplayName = () => "Other Room";
+
+			await persistence.storeMembers("alice@example.com", chat1.chatId, [
+				{
+					id: "room-members-3@example.com/occ-1",
+					displayName: "Alice",
+					photoUri: null,
+					isSelf: false,
+					roles: [{ id: "admin", title: "Admin" }],
+					jid: borogove.JID.parse("alice@example.com"),
+					presence: new Map([["desk", borogove.Stanza.parse("<presence />")]]),
+					chat: { chatId: "alice@example.com" },
+				},
+				{
+					id: "room-members-4@example.com/occ-1",
+					displayName: "Bob",
+					photoUri: null,
+					isSelf: false,
+					roles: [{ id: "admin", title: "Admin" }],
+					jid: borogove.JID.parse("bob@example.com"),
+					presence: new Map([["phone", borogove.Stanza.parse("<presence />")]]),
+					chat: { chatId: "bob@example.com" },
+				},
+			]);
+
+			await persistence.storeMemberUpdates(
+				"alice@example.com",
+				chat1,
+				[
+					new borogove.MemberUpdate(
+						null,
+						borogove.JID.parse("alice@example.com"),
+						"Alice Renamed",
+						false,
+						null,
+						new Map(),
+					),
+				],
+				false,
+			);
+			const [chat1Member] = await persistence.getMemberDetails(
+				"alice@example.com",
+				chat1,
+				["room-members-3@example.com/occ-1"],
+			);
+
+			return chat1Member.displayName;
+		}, { borogove, persistence });
+
+		expect(result).toBe("Alice Renamed");
+	});
+
+	test("clearMemberPresence only clears the targeted chat", async ({ page, borogove, persistence }) => {
+		const result = await page.evaluate(async ({ borogove, persistence }) => {
+			const chat1 = Object.create(borogove.Channel.prototype);
+			chat1.chatId = "room-members-4a@example.com";
+			chat1.getDisplayName = () => "Tea Room";
+			const chat2 = Object.create(borogove.Channel.prototype);
+			chat2.chatId = "room-members-4b@example.com";
+			chat2.getDisplayName = () => "Other Room";
+
+			await persistence.storeMembers("alice@example.com", chat1.chatId, [
+				{
+					id: "room-members-4a@example.com/occ-1",
+					displayName: "Alice",
+					photoUri: null,
+					isSelf: false,
+					roles: [{ id: "admin", title: "Admin" }],
+					jid: borogove.JID.parse("alice@example.com"),
+					presence: new Map([["desk", borogove.Stanza.parse("<presence />")]]),
+					chat: { chatId: "alice@example.com" },
+				},
+			]);
+			await persistence.storeMembers("alice@example.com", chat2.chatId, [
+				{
+					id: "room-members-4b@example.com/occ-1",
+					displayName: "Bob",
+					photoUri: null,
+					isSelf: false,
+					roles: [{ id: "admin", title: "Admin" }],
+					jid: borogove.JID.parse("bob@example.com"),
+					presence: new Map([["phone", borogove.Stanza.parse("<presence />")]]),
+					chat: { chatId: "bob@example.com" },
+				},
+			]);
+
+			await persistence.clearMemberPresence("alice@example.com", chat1.chatId);
+			const [chat1Member] = await persistence.getMemberDetails(
+				"alice@example.com",
+				chat1,
+				["room-members-4a@example.com/occ-1"],
+			);
+			const [chat2Member] = await persistence.getMemberDetails(
+				"alice@example.com",
+				chat2,
+				["room-members-4b@example.com/occ-1"],
+			);
+
+			return {
+				chat1PresenceKeys: [...chat1Member.presence.keys()],
+				chat2PresenceKeys: [...chat2Member.presence.keys()],
+			};
+		}, { borogove, persistence });
+
+		expect(result.chat1PresenceKeys).toEqual([]);
+		expect(result.chat2PresenceKeys).toEqual(["phone"]);
+	});
+
+	test("getMembers includes moderator-visible rows", async ({ page, borogove, persistence }) => {
+		const result = await page.evaluate(async ({ borogove, persistence }) => {
+			const chat = Object.create(borogove.Channel.prototype);
+			chat.chatId = "room-members-6@example.com";
+			chat.getDisplayName = () => "Tea Room";
+
+			await persistence.storeMembers("alice@example.com", chat.chatId, [
+				{
+					id: "room-members-6@example.com/owner",
+					displayName: "Zulu",
+					photoUri: null,
+					isSelf: false,
+					roles: [{ id: "owner", title: "Owner" }],
+					jid: borogove.JID.parse("zulu@example.com"),
+					presence: new Map([["desk", borogove.Stanza.parse("<presence />")]]),
+					chat: { chatId: "zulu@example.com" },
+				},
+				{
+					id: "room-members-6@example.com/outcast",
+					displayName: "Banned",
+					photoUri: null,
+					isSelf: false,
+					roles: [{ id: "outcast", title: "Banned" }],
+					jid: borogove.JID.parse("banned@example.com"),
+					presence: new Map([["desk", borogove.Stanza.parse("<presence />")]]),
+					chat: { chatId: "banned@example.com" },
+				},
+				{
+					id: "room-members-6@example.com/guest-offline",
+					displayName: "Guest",
+					photoUri: null,
+					isSelf: false,
+					roles: [{ id: "none", title: "Guest" }],
+					jid: borogove.JID.parse("guest@example.com"),
+					presence: new Map([
+						["desk", borogove.Stanza.parse('<presence type="unavailable" />')],
+					]),
+					chat: { chatId: "guest@example.com" },
+				},
+				{
+					id: "room-members-6@example.com/admin",
+					displayName: "Alpha",
+					photoUri: null,
+					isSelf: false,
+					roles: [{ id: "admin", title: "Admin" }],
+					jid: borogove.JID.parse("alpha@example.com"),
+					presence: new Map([["desk", borogove.Stanza.parse("<presence />")]]),
+					chat: { chatId: "alpha@example.com" },
+				},
+			]);
+
+			const moderator = await persistence.getMembers(
+				"alice@example.com",
+				chat,
+				true,
+			);
+			return moderator.map((m) => m.displayName);
+		}, { borogove, persistence });
+
+		expect(result).toEqual(["Zulu", "Alpha", "Banned"]);
+	});
+
+	test("storeVoiceRequest and listVoiceRequests", async ({ page, borogove, persistence }) => {
+		const result = await page.evaluate(async ({ borogove, persistence }) => {
+			const chat = Object.create(borogove.Channel.prototype);
+			chat.chatId = "room-voice-requests@example.com";
+			chat.getDisplayName = () => "Tea Room";
+
+			await persistence.storeMembers("alice@example.com", chat.chatId, [
+				{
+					id: "room-voice-requests@example.com/occ-1",
+					displayName: "Bob",
+					photoUri: null,
+					isSelf: false,
+					roles: [{ id: "none", title: "Participant" }],
+					jid: borogove.JID.parse("bob@example.com"),
+					presence: new Map([["desk", borogove.Stanza.parse("<presence />")]]),
+					chat: { chatId: "bob@example.com" },
+				},
+				{
+					id: "room-voice-requests@example.com/occ-2",
+					displayName: "Charlie",
+					photoUri: null,
+					isSelf: false,
+					roles: [{ id: "none", title: "Participant" }],
+					jid: borogove.JID.parse("charlie@example.com"),
+					presence: new Map([["desk", borogove.Stanza.parse("<presence />")]]),
+					chat: { chatId: "charlie@example.com" },
+				}
+			]);
+
+			await persistence.storeVoiceRequest("alice@example.com", chat, "bob@example.com", true);
+			await persistence.storeVoiceRequest("alice@example.com", chat, "charlie@example.com", true);
+
+			const requests1 = await persistence.listVoiceRequests("alice@example.com", chat);
+
+			await persistence.storeVoiceRequest("alice@example.com", chat, "bob@example.com", false);
+
+			const requests2 = await persistence.listVoiceRequests("alice@example.com", chat);
+
+			return {
+				requests1: requests1.map((m) => m.displayName).sort(),
+				requests2: requests2.map((m) => m.displayName).sort(),
+			};
+		}, { borogove, persistence });
+
+		expect(result.requests1).toEqual(["Bob", "Charlie"]);
+		expect(result.requests2).toEqual(["Charlie"]);
+	});
+}
diff --git a/test/sqlite.spec.ts b/test/sqlite.spec.ts
index 35f636d..22af01e 100644
--- a/test/sqlite.spec.ts
+++ b/test/sqlite.spec.ts
@@ -1,4 +1,5 @@
 import { sqliteTest as test, expect } from "./browser-test";
+import { sharedPersistenceTests } from "./persistence-tests";
 
 test.describe("not webkit", () => {
 	test.skip(
@@ -6,1483 +7,7 @@ test.describe("not webkit", () => {
 		"Skip on webkit because the version in playwright lacks OPFS",
 	);
 
-	test("1:1 come back ordered by sortId", async ({ page, borogove, persistence }) => {
-		const result = await page.evaluate(
-			async ({ borogove, persistence }) => {
-				const builder = new borogove.ChatMessageBuilder({
-					serverId: "1",
-					serverIdBy: "alice@example.com",
-					senderId: "hatter@example.com",
-					direction: 0,
-				});
-				builder.sortId = "a0";
-				builder.to = borogove.JID.parse("alice@example.com");
-				builder.from = borogove.JID.parse("hatter@example.com");
-				builder.recipients = [builder.to];
-				builder.replyTo = [builder.from];
-
-				const builder2 = new borogove.ChatMessageBuilder({
-					serverId: "2",
-					serverIdBy: "alice@example.com",
-					senderId: "hatter@example.com",
-					direction: 0,
-				});
-				builder2.sortId = "b0";
-				builder2.to = borogove.JID.parse("alice@example.com");
-				builder2.from = borogove.JID.parse("hatter@example.com");
-				builder2.recipients = [builder2.to];
-				builder2.replyTo = [builder2.from];
-
-				try {
-					await persistence.storeMessages("alice@example.com", [
-						builder2.build(),
-						builder.build(),
-					]);
-
-					return await persistence.getMessagesBefore(
-						"alice@example.com",
-						"hatter@example.com",
-					);
-				} catch (e) {
-					console.error(e, e.result);
-					throw e.result ? JSON.stringify(e.result) : e.message;
-				}
-			},
-			{ borogove, persistence },
-		);
-
-		expect(result.length).toBe(2);
-		expect(result[0].serverId).toBe("1");
-		expect(result[1].serverId).toBe("2");
-	});
-
-	test("getMessagesBefore the end: MUC come back ordered by sortId, PM by timestamp", async ({
-		page,
-		borogove,
-		persistence,
-	}) => {
-		const result = await page.evaluate(
-			async ({ borogove, persistence }) => {
-				const builder = new borogove.ChatMessageBuilder({
-					serverId: "1",
-					serverIdBy: "teaparty@example.com",
-					senderId: "teaparty@example.com/hatter",
-					direction: 0,
-					type: borogove.MessageType.MessageChannel,
-					timestamp: "2020-01-01T00:00:01Z",
-				});
-				builder.sortId = "a0";
-				builder.to = borogove.JID.parse("alice@example.com");
-				builder.from = borogove.JID.parse("teaparty@example.com/hatter");
-				builder.replyTo = [builder.from.asBare()];
-
-				const builder2 = new borogove.ChatMessageBuilder({
-					serverId: "2",
-					serverIdBy: "teaparty@example.com",
-					senderId: "teaparty@example.com/hatter",
-					direction: 0,
-					type: borogove.MessageType.MessageChannel,
-					timestamp: "2020-01-01T00:00:00Z",
-				});
-				builder2.sortId = "b0";
-				builder2.to = borogove.JID.parse("alice@example.com");
-				builder2.from = borogove.JID.parse("teaparty@example.com/hatter");
-				builder2.replyTo = [builder2.from.asBare()];
-
-				const builder3 = new borogove.ChatMessageBuilder({
-					serverId: "3",
-					serverIdBy: "alice@example.com",
-					senderId: "teaparty@example.com/hatter",
-					direction: 0,
-					type: borogove.MessageType.MessageChannelPrivate,
-					timestamp: "2020-01-01T00:00:03Z",
-				});
-				builder3.sortId = "a0";
-				builder3.to = borogove.JID.parse("alice@example.com");
-				builder3.from = borogove.JID.parse("teaparty@example.com/hatter");
-				builder3.replyTo = [builder3.from.asBare()];
-
-				try {
-					await persistence.storeMessages("alice@example.com", [
-						builder2.build(),
-						builder3.build(),
-						builder.build(),
-					]);
-					return await persistence.getMessagesBefore(
-						"alice@example.com",
-						"teaparty@example.com",
-					);
-				} catch (e) {
-					console.error(e, e.result);
-					throw e.result ? JSON.stringify(e.result) : e.message;
-				}
-			},
-			{ borogove, persistence },
-		);
-
-		expect(result.length).toBe(3);
-		expect(result[0].serverId).toBe("1");
-		expect(result[1].serverId).toBe("2");
-		expect(result[2].serverId).toBe("3");
-	});
-
-	test("getMessagesBefore some point: MUC come back ordered by sortId, PM by timestamp", async ({
-		page,
-		borogove,
-		persistence,
-	}) => {
-		const result = await page.evaluate(
-			async ({ borogove, persistence }) => {
-				const builder = new borogove.ChatMessageBuilder({
-					serverId: "1",
-					serverIdBy: "teaparty@example.com",
-					senderId: "teaparty@example.com/hatter",
-					direction: 0,
-					type: borogove.MessageType.MessageChannel,
-					timestamp: "2020-01-01T00:00:01Z",
-				});
-				builder.sortId = "a0";
-				builder.to = borogove.JID.parse("alice@example.com");
-				builder.from = borogove.JID.parse("teaparty@example.com/hatter");
-				builder.replyTo = [builder.from.asBare()];
-
-				const builder2 = new borogove.ChatMessageBuilder({
-					serverId: "2",
-					serverIdBy: "teaparty@example.com",
-					senderId: "teaparty@example.com/hatter",
-					direction: 0,
-					type: borogove.MessageType.MessageChannel,
-					timestamp: "2020-01-01T00:00:00Z",
-				});
-				builder2.sortId = "b0";
-				builder2.to = borogove.JID.parse("alice@example.com");
-				builder2.from = borogove.JID.parse("teaparty@example.com/hatter");
-				builder2.replyTo = [builder2.from.asBare()];
-
-				const builder3 = new borogove.ChatMessageBuilder({
-					serverId: "3",
-					serverIdBy: "alice@example.com",
-					senderId: "teaparty@example.com/hatter",
-					direction: 0,
-					type: borogove.MessageType.MessageChannelPrivate,
-					timestamp: "2020-01-01T00:00:03Z",
-				});
-				builder3.sortId = "Z~";
-				builder3.to = borogove.JID.parse("alice@example.com");
-				builder3.from = borogove.JID.parse("teaparty@example.com/hatter");
-				builder3.replyTo = [builder3.from.asBare()];
-
-				const builder4 = new borogove.ChatMessageBuilder({
-					serverId: "4",
-					serverIdBy: "teaparty@example.com",
-					senderId: "teaparty@example.com/hatter",
-					direction: 0,
-					type: borogove.MessageType.MessageChannel,
-					timestamp: "2020-01-01T00:00:04Z",
-				});
-				builder4.sortId = "c0";
-				builder4.to = borogove.JID.parse("alice@example.com");
-				builder4.from = borogove.JID.parse("teaparty@example.com/hatter");
-				builder4.replyTo = [builder4.from.asBare()];
-
-				try {
-					await persistence.storeMessages("alice@example.com", [
-						builder2.build(),
-						builder4.build(),
-						builder3.build(),
-						builder.build(),
-					]);
-					return await persistence.getMessagesBefore(
-						"alice@example.com",
-						"teaparty@example.com",
-						builder4.build(),
-					);
-				} catch (e) {
-					console.error(e, e.result);
-					throw e.result ? JSON.stringify(e.result) : e.message;
-				}
-			},
-			{ borogove, persistence },
-		);
-
-		expect(result.length).toBe(3);
-		expect(result[0].serverId).toBe("1");
-		expect(result[1].serverId).toBe("2");
-		expect(result[2].serverId).toBe("3");
-	});
-
-	test("getMessagesBefore a PM", async ({ page, borogove, persistence }) => {
-		const result = await page.evaluate(
-			async ({ borogove, persistence }) => {
-				const builder = new borogove.ChatMessageBuilder({
-					serverId: "1",
-					serverIdBy: "teaparty@example.com",
-					senderId: "teaparty@example.com/hatter",
-					direction: 0,
-					type: borogove.MessageType.MessageChannel,
-					timestamp: "2020-01-01T00:00:00Z",
-				});
-				builder.sortId = "a0";
-				builder.to = borogove.JID.parse("alice@example.com");
-				builder.from = borogove.JID.parse("teaparty@example.com/hatter");
-				builder.replyTo = [builder.from.asBare()];
-
-				const builder2 = new borogove.ChatMessageBuilder({
-					serverId: "2",
-					serverIdBy: "teaparty@example.com",
-					senderId: "teaparty@example.com/hatter",
-					direction: 0,
-					type: borogove.MessageType.MessageChannel,
-					timestamp: "2020-01-01T00:00:01Z",
-				});
-				builder2.sortId = "b0";
-				builder2.to = borogove.JID.parse("alice@example.com");
-				builder2.from = borogove.JID.parse("teaparty@example.com/hatter");
-				builder2.replyTo = [builder2.from.asBare()];
-
-				const builder3 = new borogove.ChatMessageBuilder({
-					serverId: "3",
-					serverIdBy: "alice@example.com",
-					senderId: "teaparty@example.com/hatter",
-					direction: 0,
-					type: borogove.MessageType.MessageChannelPrivate,
-					timestamp: "2020-01-01T00:00:03Z",
-				});
-				builder3.sortId = "Z~";
-				builder3.to = borogove.JID.parse("alice@example.com");
-				builder3.from = borogove.JID.parse("teaparty@example.com/hatter");
-				builder3.replyTo = [builder3.from.asBare()];
-
-				const builder4 = new borogove.ChatMessageBuilder({
-					serverId: "4",
-					serverIdBy: "teaparty@example.com",
-					senderId: "teaparty@example.com/hatter",
-					direction: 0,
-					type: borogove.MessageType.MessageChannel,
-					timestamp: "2020-01-01T00:00:04Z",
-				});
-				builder4.sortId = "c0";
-				builder4.to = borogove.JID.parse("alice@example.com");
-				builder4.from = borogove.JID.parse("teaparty@example.com/hatter");
-				builder4.replyTo = [builder4.from.asBare()];
-
-				try {
-					await persistence.storeMessages("alice@example.com", [
-						builder2.build(),
-						builder4.build(),
-						builder3.build(),
-						builder.build(),
-					]);
-					return await persistence.getMessagesBefore(
-						"alice@example.com",
-						"teaparty@example.com",
-						builder3.build(),
-					);
-				} catch (e) {
-					console.error(e, e.result);
-					throw e.result ? JSON.stringify(e.result) : e.message;
-				}
-			},
-			{ borogove, persistence },
-		);
-
-		expect(result.length).toBe(2);
-		expect(result[0].serverId).toBe("1");
-		expect(result[1].serverId).toBe("2");
-	});
-
-	test("getMessagesAfter the start: MUC come back ordered by sortId, PM by timestamp", async ({
-		page,
-		borogove,
-		persistence,
-	}) => {
-		const result = await page.evaluate(
-			async ({ borogove, persistence }) => {
-				const builder = new borogove.ChatMessageBuilder({
-					serverId: "1",
-					serverIdBy: "teaparty@example.com",
-					senderId: "teaparty@example.com/hatter",
-					direction: 0,
-					type: borogove.MessageType.MessageChannel,
-					timestamp: "2020-01-01T00:00:00Z",
-				});
-				builder.sortId = "a0";
-				builder.to = borogove.JID.parse("alice@example.com");
-				builder.from = borogove.JID.parse("teaparty@example.com/hatter");
-				builder.replyTo = [builder.from.asBare()];
-
-				const builder2 = new borogove.ChatMessageBuilder({
-					serverId: "2",
-					serverIdBy: "teaparty@example.com",
-					senderId: "teaparty@example.com/hatter",
-					direction: 0,
-					type: borogove.MessageType.MessageChannel,
-					timestamp: "2020-01-01T00:00:01Z",
-				});
-				builder2.sortId = "b0";
-				builder2.to = borogove.JID.parse("alice@example.com");
-				builder2.from = borogove.JID.parse("teaparty@example.com/hatter");
-				builder2.replyTo = [builder2.from.asBare()];
-
-				const builder3 = new borogove.ChatMessageBuilder({
-					serverId: "3",
-					serverIdBy: "alice@example.com",
-					senderId: "teaparty@example.com/hatter",
-					direction: 0,
-					type: borogove.MessageType.MessageChannelPrivate,
-					timestamp: "2020-01-01T00:00:03Z",
-				});
-				builder3.sortId = "a1";
-				builder3.to = borogove.JID.parse("alice@example.com");
-				builder3.from = borogove.JID.parse("teaparty@example.com/hatter");
-				builder3.replyTo = [builder3.from.asBare()];
-
-				try {
-					await persistence.storeMessages("alice@example.com", [
-						builder2.build(),
-						builder3.build(),
-						builder.build(),
-					]);
-					return await persistence.getMessagesAfter(
-						"alice@example.com",
-						"teaparty@example.com",
-					);
-				} catch (e) {
-					console.error(e, e.result);
-					throw e.result ? JSON.stringify(e.result) : e.message;
-				}
-			},
-			{ borogove, persistence },
-		);
-
-		expect(result.length).toBe(3);
-		expect(result[0].serverId).toBe("1");
-		expect(result[1].serverId).toBe("2");
-		expect(result[2].serverId).toBe("3");
-	});
-
-	test("getMessagesAfter some point: MUC come back ordered by sortId, PM by timestamp", async ({
-		page,
-		borogove,
-		persistence,
-	}) => {
-		const result = await page.evaluate(
-			async ({ borogove, persistence }) => {
-				const builder = new borogove.ChatMessageBuilder({
-					serverId: "1",
-					serverIdBy: "teaparty@example.com",
-					senderId: "teaparty@example.com/hatter",
-					direction: 0,
-					type: borogove.MessageType.MessageChannel,
-					timestamp: "2020-01-01T00:00:01Z",
-				});
-				builder.sortId = "a0";
-				builder.to = borogove.JID.parse("alice@example.com");
-				builder.from = borogove.JID.parse("teaparty@example.com/hatter");
-				builder.replyTo = [builder.from.asBare()];
-
-				const builder2 = new borogove.ChatMessageBuilder({
-					serverId: "2",
-					serverIdBy: "teaparty@example.com",
-					senderId: "teaparty@example.com/hatter",
-					direction: 0,
-					type: borogove.MessageType.MessageChannel,
-					timestamp: "2020-01-01T00:00:00Z",
-				});
-				builder2.sortId = "b0";
-				builder2.to = borogove.JID.parse("alice@example.com");
-				builder2.from = borogove.JID.parse("teaparty@example.com/hatter");
-				builder2.replyTo = [builder2.from.asBare()];
-
-				const builder3 = new borogove.ChatMessageBuilder({
-					serverId: "3",
-					serverIdBy: "alice@example.com",
-					senderId: "teaparty@example.com/hatter",
-					direction: 0,
-					type: borogove.MessageType.MessageChannelPrivate,
-					timestamp: "2020-01-01T00:00:03Z",
-				});
-				builder3.sortId = "Z~";
-				builder3.to = borogove.JID.parse("alice@example.com");
-				builder3.from = borogove.JID.parse("teaparty@example.com/hatter");
-				builder3.replyTo = [builder3.from.asBare()];
-
-				const builder4 = new borogove.ChatMessageBuilder({
-					serverId: "4",
-					serverIdBy: "teaparty@example.com",
-					senderId: "teaparty@example.com/hatter",
-					direction: 0,
-					type: borogove.MessageType.MessageChannel,
-					timestamp: "2020-01-01T00:00:04Z",
-				});
-				builder4.sortId = "c0";
-				builder4.to = borogove.JID.parse("alice@example.com");
-				builder4.from = borogove.JID.parse("teaparty@example.com/hatter");
-				builder4.replyTo = [builder4.from.asBare()];
-
-				try {
-					await persistence.storeMessages("alice@example.com", [
-						builder2.build(),
-						builder4.build(),
-						builder3.build(),
-						builder.build(),
-					]);
-					return await persistence.getMessagesAfter(
-						"alice@example.com",
-						"teaparty@example.com",
-						builder.build(),
-					);
-				} catch (e) {
-					console.error(e, e.result);
-					throw e.result ? JSON.stringify(e.result) : e.message;
-				}
-			},
-			{ borogove, persistence },
-		);
-
-		expect(result.length).toBe(3);
-		expect(result[0].serverId).toBe("2");
-		expect(result[1].serverId).toBe("3");
-		expect(result[2].serverId).toBe("4");
-	});
-
-	test("getMessagesAfter a PM", async ({ page, borogove, persistence }) => {
-		const result = await page.evaluate(
-			async ({ borogove, persistence }) => {
-				const builder = new borogove.ChatMessageBuilder({
-					serverId: "1",
-					serverIdBy: "teaparty@example.com",
-					senderId: "teaparty@example.com/hatter",
-					direction: 0,
-					type: borogove.MessageType.MessageChannel,
-					timestamp: "2020-01-01T00:00:00Z",
-				});
-				builder.sortId = "a0";
-				builder.to = borogove.JID.parse("alice@example.com");
-				builder.from = borogove.JID.parse("teaparty@example.com/hatter");
-				builder.replyTo = [builder.from.asBare()];
-
-				const builder2 = new borogove.ChatMessageBuilder({
-					serverId: "2",
-					serverIdBy: "teaparty@example.com",
-					senderId: "teaparty@example.com/hatter",
-					direction: 0,
-					type: borogove.MessageType.MessageChannel,
-					timestamp: "2020-01-01T00:00:01Z",
-				});
-				builder2.sortId = "b0";
-				builder2.to = borogove.JID.parse("alice@example.com");
-				builder2.from = borogove.JID.parse("teaparty@example.com/hatter");
-				builder2.replyTo = [builder2.from.asBare()];
-
-				const builder3 = new borogove.ChatMessageBuilder({
-					serverId: "3",
-					serverIdBy: "alice@example.com",
-					senderId: "teaparty@example.com/hatter",
-					direction: 0,
-					type: borogove.MessageType.MessageChannelPrivate,
-					timestamp: "2020-01-01T00:00:03Z",
-				});
-				builder3.sortId = "Z~";
-				builder3.to = borogove.JID.parse("alice@example.com");
-				builder3.from = borogove.JID.parse("teaparty@example.com/hatter");
-				builder3.replyTo = [builder3.from.asBare()];
-
-				const builder4 = new borogove.ChatMessageBuilder({
-					serverId: "4",
-					serverIdBy: "teaparty@example.com",
-					senderId: "teaparty@example.com/hatter",
-					direction: 0,
-					type: borogove.MessageType.MessageChannel,
-					timestamp: "2020-01-01T00:00:04Z",
-				});
-				builder4.sortId = "c0";
-				builder4.to = borogove.JID.parse("alice@example.com");
-				builder4.from = borogove.JID.parse("teaparty@example.com/hatter");
-				builder4.replyTo = [builder4.from.asBare()];
-
-				try {
-					await persistence.storeMessages("alice@example.com", [
-						builder2.build(),
-						builder4.build(),
-						builder3.build(),
-						builder.build(),
-					]);
-					return await persistence.getMessagesAfter(
-						"alice@example.com",
-						"teaparty@example.com",
-						builder3.build(),
-					);
-				} catch (e) {
-					console.error(e, e.result);
-					throw e.result ? JSON.stringify(e.result) : e.message;
-				}
-			},
-			{ borogove, persistence },
-		);
-
-		expect(result.length).toBe(1);
-		expect(result[0].serverId).toBe("4");
-	});
-
-	test("storeChats and getChats", async ({ page, borogove, persistence }) => {
-		const result = await page.evaluate(
-			async ({ borogove, persistence }) => {
-				const chat = new borogove.DirectChat(
-					null,
-					null,
-					persistence,
-					"hatter@example.com",
-				);
-				chat.displayName = "The Mad Hatter";
-				chat.trusted = true;
-				chat.threads = new Map([
-					[null, "Tea Time"],
-					["thread-1", "Introductions"],
-				]);
-
-				try {
-					await persistence.storeChats("alice@example.com", [chat]);
-					await new Promise((resolve) => setTimeout(resolve, 200));
-					const chats = await persistence.getChats("alice@example.com");
-					return {
-						length: chats.length,
-						chatId: chats[0]?.chatId,
-						displayName: chats[0]?.displayName,
-						trusted: chats[0]?.trusted,
-						klass: chats[0]?.klass,
-						channelSubject: chats[0]?.threads?.get(null),
-						threadSubject: chats[0]?.threads?.get("thread-1"),
-					};
-				} catch (e) {
-					console.error(e, e.result);
-					throw e.result ? JSON.stringify(e.result) : e.message;
-				}
-			},
-			{ borogove, persistence },
-		);
-
-		expect(result.length).toBe(1);
-		expect(result.chatId).toBe("hatter@example.com");
-		expect(result.displayName).toBe("The Mad Hatter");
-		expect(result.trusted).toBe(true);
-		expect(result.klass).toBe("DirectChat");
-		expect(result.channelSubject).toBe("Tea Time");
-		expect(result.threadSubject).toBe("Introductions");
-	});
-
-	test("getMessage by serverId and localId", async ({ page, borogove, persistence }) => {
-		const result = await page.evaluate(
-			async ({ borogove, persistence }) => {
-				const builder = new borogove.ChatMessageBuilder({
-					serverId: "srv1",
-					serverIdBy: "hatter@example.com",
-					localId: "loc1",
-					senderId: "hatter@example.com",
-					direction: 0,
-				});
-				builder.sortId = "a0";
-				builder.to = borogove.JID.parse("alice@example.com");
-				builder.from = borogove.JID.parse("hatter@example.com");
-				builder.recipients = [builder.to];
-				builder.replyTo = [builder.from];
-
-				try {
-					await persistence.storeMessages("alice@example.com", [
-						builder.build(),
-					]);
-
-					const byServerId = await persistence.getMessage(
-						"alice@example.com",
-						"hatter@example.com",
-						"srv1",
-						null,
-					);
-					const byLocalId = await persistence.getMessage(
-						"alice@example.com",
-						"hatter@example.com",
-						null,
-						"loc1",
-					);
-					return {
-						byServerId: byServerId
-							? { serverId: byServerId.serverId, localId: byLocalId.localId }
-							: null,
-						byLocalId: byLocalId
-							? { serverId: byLocalId.serverId, localId: byLocalId.localId }
-							: null,
-					};
-				} catch (e) {
-					console.error(e, e.result);
-					throw e.result ? JSON.stringify(e.result) : e.message;
-				}
-			},
-			{ borogove, persistence },
-		);
-
-		expect(result.byServerId).not.toBeNull();
-		expect(result.byServerId.serverId).toBe("srv1");
-		expect(result.byLocalId).not.toBeNull();
-		expect(result.byLocalId.localId).toBe("loc1");
-	});
-
-	test("storeReaction", async ({ page, borogove, persistence }) => {
-		const result = await page.evaluate(
-			async ({ borogove, persistence }) => {
-				const builder = new borogove.ChatMessageBuilder({
-					serverId: "srv1",
-					serverIdBy: "hatter@example.com",
-					senderId: "hatter@example.com",
-					direction: 0,
-				});
-				builder.sortId = "a0";
-				builder.to = borogove.JID.parse("alice@example.com");
-				builder.from = borogove.JID.parse("hatter@example.com");
-				builder.recipients = [builder.to];
-				builder.replyTo = [builder.from];
-
-				try {
-					await persistence.storeMessages("alice@example.com", [
-						builder.build(),
-					]);
-
-					const reaction = new borogove.Reaction(
-						"alice@example.com",
-						"2020-01-01T00:00:01Z",
-						"👍",
-					);
-					const update = new borogove.ReactionUpdate(
-						"up1",
-						"srv1",
-						"hatter@example.com",
-						null,
-						"hatter@example.com",
-						"alice@example.com",
-						"2020-01-01T00:00:01Z",
-						[reaction],
-						borogove.ReactionUpdateKind.EmojiReactions,
-					);
-					const msg = await persistence.storeReaction(
-						"alice@example.com",
-						update,
-					);
-					return {
-						reactions: [...msg.reactions.entries()].map(([k, v]) => ({
-							key: k,
-							count: v.length,
-						})),
-					};
-				} catch (e) {
-					console.error(e, e.result);
-					throw e.result ? JSON.stringify(e.result) : e.message;
-				}
-			},
-			{ borogove, persistence },
-		);
-
-		expect(result.reactions.length).toBe(1);
-		expect(result.reactions[0].key).toBe("👍");
-		expect(result.reactions[0].count).toBe(1);
-	});
-
-	test("updateMessageStatus", async ({ page, borogove, persistence }) => {
-		const result = await page.evaluate(
-			async ({ borogove, persistence }) => {
-				const builder = new borogove.ChatMessageBuilder({
-					localId: "loc1",
-					senderId: "alice@example.com",
-					direction: 1,
-				});
-				builder.sortId = "a0";
-				builder.to = borogove.JID.parse("hatter@example.com");
-				builder.from = borogove.JID.parse("alice@example.com");
-				builder.recipients = [builder.to];
-				builder.replyTo = [builder.from];
-
-				try {
-					await persistence.storeMessages("alice@example.com", [
-						builder.build(),
-					]);
-
-					const updated = await persistence.updateMessageStatus(
-						"alice@example.com",
-						"loc1",
-						1,
-						"Delivered",
-					);
-					return { status: updated.status, statusText: updated.statusText };
-				} catch (e) {
-					console.error(e, e.result);
-					throw e.result ? JSON.stringify(e.result) : e.message;
-				}
-			},
-			{ borogove, persistence },
-		);
-
-		expect(result.status).toBe(1);
-		expect(result.statusText).toBe("Delivered");
-	});
-
-	test("searchMessages", async ({ page, borogove, persistence }) => {
-		const result = await page.evaluate(
-			async ({ borogove, persistence }) => {
-				const builder = new borogove.ChatMessageBuilder({
-					serverId: "srv1",
-					serverIdBy: "hatter@example.com",
-					senderId: "hatter@example.com",
-					direction: 0,
-				});
-				builder.sortId = "a0";
-				builder.text = "Hello world";
-				builder.to = borogove.JID.parse("alice@example.com");
-				builder.from = borogove.JID.parse("hatter@example.com");
-				builder.recipients = [builder.to];
-				builder.replyTo = [builder.from];
-
-				const builder2 = new borogove.ChatMessageBuilder({
-					serverId: "srv2",
-					serverIdBy: "hatter@example.com",
-					senderId: "hatter@example.com",
-					direction: 0,
-				});
-				builder2.sortId = "a1";
-				builder2.text = "Goodbye world";
-				builder2.to = borogove.JID.parse("alice@example.com");
-				builder2.from = borogove.JID.parse("hatter@example.com");
-				builder2.recipients = [builder2.to];
-				builder2.replyTo = [builder2.from];
-
-				try {
-					await persistence.storeMessages("alice@example.com", [
-						builder.build(),
-						builder2.build(),
-					]);
-					const results = await persistence.searchMessages(
-						"alice@example.com",
-						"hatter@example.com",
-						"hello",
-					);
-					return results.map((m) => m.text);
-				} catch (e) {
-					console.error(e, e.result);
-					throw e.result ? JSON.stringify(e.result) : e.message;
-				}
-			},
-			{ borogove, persistence },
-		);
-
-		expect(result.length).toBe(1);
-		expect(result[0]).toBe("Hello world");
-	});
-
-	test("removeAccount and listAccounts", async ({ page, borogove, persistence }) => {
-		const result = await page.evaluate(
-			async ({ borogove, persistence }) => {
-				try {
-					await persistence.storeLogin(
-						"alice@example.com",
-						"client1",
-						"Alice",
-						null,
-					);
-					await persistence.storeLogin(
-						"bob@example.com",
-						"client2",
-						"Bob",
-						null,
-					);
-					const accountsBefore = await persistence.listAccounts();
-					await persistence.removeAccount("alice@example.com", true);
-					const accountsAfter = await persistence.listAccounts();
-					return { accountsBefore, accountsAfter };
-				} catch (e) {
-					console.error(e, e.result);
-					throw e.result ? JSON.stringify(e.result) : e.message;
-				}
-			},
-			{ borogove, persistence },
-		);
-
-		expect(result.accountsBefore).toContain("alice@example.com");
-		expect(result.accountsBefore).toContain("bob@example.com");
-		expect(result.accountsAfter).not.toContain("alice@example.com");
-		expect(result.accountsAfter).toContain("bob@example.com");
-	});
-
-	test("getChatUnreadDetails", async ({ page, borogove, persistence }) => {
-		const result = await page.evaluate(
-			async ({ borogove, persistence }) => {
-				const chat = Object.create(borogove.DirectChat.prototype);
-				chat.constructor = borogove.DirectChat;
-				chat.chatId = "hatter@example.com";
-				chat.readUpToId = "srv1";
-				chat.notificationsFiltered = () => false;
-
-				const builder = new borogove.ChatMessageBuilder({
-					serverId: "srv1",
-					serverIdBy: "hatter@example.com",
-					senderId: "hatter@example.com",
-					direction: 0,
-				});
-				builder.sortId = "a0";
-				builder.to = borogove.JID.parse("alice@example.com");
-				builder.from = borogove.JID.parse("hatter@example.com");
-				builder.recipients = [builder.to];
-				builder.replyTo = [builder.from];
-
-				const builder2 = new borogove.ChatMessageBuilder({
-					serverId: "srv2",
-					serverIdBy: "hatter@example.com",
-					senderId: "hatter@example.com",
-					direction: 0,
-				});
-				builder2.sortId = "a1";
-				builder2.to = borogove.JID.parse("alice@example.com");
-				builder2.from = borogove.JID.parse("hatter@example.com");
-				builder2.recipients = [builder2.to];
-				builder2.replyTo = [builder2.from];
-
-				try {
-					await persistence.storeMessages("alice@example.com", [
-						builder.build(),
-						builder2.build(),
-					]);
-					return await persistence.getChatUnreadDetails(
-						"alice@example.com",
-						chat,
-					);
-				} catch (e) {
-					console.error(e, e.result);
-					throw e.result ? JSON.stringify(e.result) : e.message;
-				}
-			},
-			{ borogove, persistence },
-		);
-
-		expect(result.unreadCount).toBe(1);
-		expect(result.message.serverId).toBe("srv2");
-	});
-
-	test("media storage functions", async ({ page, borogove, persistence }) => {
-		const result = await page.evaluate(
-			async ({ borogove, persistence }) => {
-				try {
-					function bufferToByteStream(buffer) {
-						let done = false;
-						return new ReadableStream({
-							type: "bytes",
-							async pull(controller) {
-								if (done) {
-									controller.close();
-								} else {
-									controller.enqueue(buffer);
-									done = true;
-								}
-							},
-						});
-					}
-
-					const buffer = new Uint8Array([1, 2, 3]);
-					const sha256 = await crypto.subtle.digest("SHA-256", buffer.buffer);
-					await persistence.storeMedia("image/png", bufferToByteStream(buffer));
-					const hash = new borogove.Hash("sha-256", sha256);
-					const hasBefore = await persistence.hasMedia(hash);
-					await persistence.removeMedia("sha-256", sha256);
-					const hasAfter = await persistence.hasMedia(hash);
-
-					return { hasBefore, hasAfter };
-				} catch (e) {
-					console.error(e, e.result);
-					throw e.result ? JSON.stringify(e.result) : e.message;
-				}
-			},
-			{ borogove, persistence },
-		);
-
-		expect(result.hasBefore).toBe(
-			"/.well-known/ni/sha-256/A5BYxvLAy0ksUzsKTRTvd8wPeKvMztUofYShogEc-4E",
-		);
-		expect(result.hasAfter).toBe(null);
-	});
-
-	test("hydrate message with incomplete replyToMessage", async ({ page, borogove, persistence }) => {
-		const result = await page.evaluate(
-			async ({ borogove, persistence }) => {
-				const builder = new borogove.ChatMessageBuilder({
-					serverId: "parent",
-					serverIdBy: "hatter@example.com",
-					localId: "loc1",
-					senderId: "hatter@example.com",
-					direction: 0,
-				});
-				builder.sortId = "a0";
-				builder.to = borogove.JID.parse("alice@example.com");
-				builder.from = borogove.JID.parse("hatter@example.com");
-				builder.recipients = [builder.to];
-				builder.replyTo = [builder.from];
-				builder.type = borogove.MessageType.MessageChannel;
-				const parentStub = builder.build();
-
-				builder.setBody(borogove.Html.text("Hello"));
-				const parentMsg = builder.build();
-
-				const builder2 = new borogove.ChatMessageBuilder({
-					serverId: "child",
-					serverIdBy: "hatter@example.com",
-					localId: "loc2",
-					senderId: "hatter@example.com",
-					direction: 0,
-				});
-				builder2.sortId = "a1";
-				builder2.to = borogove.JID.parse("alice@example.com");
-				builder2.from = borogove.JID.parse("hatter@example.com");
-				builder2.recipients = [builder2.to];
-				builder2.replyTo = [builder2.from];
-				builder2.replyToMessage = parentStub;
-				builder2.type = borogove.MessageType.MessageChannel;
-				const childMsg = builder2.build();
-
-				await persistence.storeMessages("alice@example.com", [parentMsg]);
-				const [childStored] = await persistence.storeMessages(
-					"alice@example.com",
-					[childMsg],
-				);
-
-				return childStored.replyToMessage.body().toPlainText();
-			},
-			{ borogove, persistence },
-		);
-
-		expect(result).toBe("Hello");
-	});
-
-	test("storeChats and getChats with status", async ({ page, borogove, persistence }) => {
-		const result = await page.evaluate(
-			async ({ borogove, persistence }) => {
-				const chat = new borogove.DirectChat(
-					null,
-					null,
-					persistence,
-					"hatter@example.com",
-				);
-				chat.displayName = "The Mad Hatter";
-				chat.trusted = true;
-				chat.status = new borogove.Status("🎩", "Time for tea!");
-
-				try {
-					await persistence.storeChats("alice@example.com", [chat]);
-					await new Promise((resolve) => setTimeout(resolve, 200));
-					const chats = await persistence.getChats("alice@example.com");
-					return {
-						length: chats.length,
-						chatId: chats[0]?.chatId,
-						statusEmoji: chats[0]?.status?.emoji,
-						statusText: chats[0]?.status?.text,
-					};
-				} catch (e) {
-					console.error(e, e.result);
-					throw e.result ? JSON.stringify(e.result) : e.message;
-				}
-			},
-			{ borogove, persistence },
-		);
-
-		expect(result.length).toBe(1);
-		expect(result.chatId).toBe("hatter@example.com");
-		expect(result.statusEmoji).toBe("🎩");
-		expect(result.statusText).toBe("Time for tea!");
-	});
-
-	test("getChats uses member presence for direct chats", async ({ page, borogove, persistence }) => {
-		const result = await page.evaluate(
-			async ({ borogove, persistence }) => {
-				const chat = new borogove.DirectChat(
-					null,
-					null,
-					persistence,
-					"hatter@example.com",
-				);
-				chat.displayName = "The Mad Hatter";
-				chat.trusted = true;
-
-				try {
-					await persistence.storeChats("alice@example.com", [chat]);
-					await new Promise((resolve) => setTimeout(resolve, 200));
-					await persistence.storeMembers("alice@example.com", chat.chatId, [
-						{
-							id: "hatter@example.com",
-							displayName: "The Mad Hatter",
-							photoUri: null,
-							isSelf: false,
-							roles: [],
-							jid: borogove.JID.parse("hatter@example.com"),
-							presence: new Map([
-								["phone", borogove.Stanza.parse("<presence />")],
-							]),
-							chat: null,
-						},
-					]);
-					const [stored] = await persistence.getChats("alice@example.com");
-					return [...stored.presence.keys()].sort();
-				} catch (e) {
-					console.error(e, e.result);
-					throw e.result ? JSON.stringify(e.result) : e.message;
-				}
-			},
-			{ borogove, persistence },
-		);
-
-		expect(result).toEqual(["phone"]);
-	});
-
-	test("getChats hydrates membersForName and mavUntil from members", async ({
-		page,
-		borogove,
-		persistence,
-		sqlite,
-	}) => {
-		const result = await page.evaluate(
-			async ({ borogove, persistence, sqlite }) => {
-				const chat = new sqlite.Channel(
-					null,
-					null,
-					persistence,
-					"room-chat-hydrate@example.com",
-				);
-				chat.displayName = "Tea Room";
-				chat.trusted = true;
-				chat.mavUntil = "2024-05-01T12:00:00Z";
-
-				await persistence.storeChats("alice@example.com", [chat]);
-				await new Promise((resolve) => setTimeout(resolve, 200));
-				await persistence.storeMembers("alice@example.com", chat.chatId, [
-					{
-						id: chat.chatId,
-						displayName: "Tea Room",
-						photoUri: null,
-						isSelf: false,
-						roles: [],
-						jid: borogove.JID.parse(chat.chatId),
-						presence: new Map(),
-						chat: null,
-					},
-					{
-						id: `${chat.chatId}/self`,
-						displayName: "Myself",
-						photoUri: null,
-						isSelf: true,
-						roles: [{ id: "owner", title: "Owner" }],
-						jid: borogove.JID.parse("alice@example.com"),
-						presence: new Map([
-							["desk", borogove.Stanza.parse("<presence />")],
-						]),
-						chat: null,
-					},
-					{
-						id: `${chat.chatId}/zulu`,
-						displayName: "Zulu",
-						photoUri: null,
-						isSelf: false,
-						roles: [{ id: "admin", title: "Admin" }],
-						jid: borogove.JID.parse("zulu@example.com"),
-						presence: new Map([
-							["desk", borogove.Stanza.parse("<presence />")],
-						]),
-						chat: { chatId: "zulu@example.com" },
-					},
-					{
-						id: `${chat.chatId}/alpha`,
-						displayName: "Alpha",
-						photoUri: null,
-						isSelf: false,
-						roles: [],
-						jid: borogove.JID.parse("alpha@example.com"),
-						presence: new Map([
-							["desk", borogove.Stanza.parse("<presence />")],
-						]),
-						chat: { chatId: "alpha@example.com" },
-					},
-					{
-						id: `${chat.chatId}/hidden`,
-						displayName: "Hidden",
-						photoUri: null,
-						isSelf: false,
-						roles: [{ id: "none", title: "Guest" }],
-						jid: borogove.JID.parse("hidden@example.com"),
-						presence: new Map([
-							["desk", borogove.Stanza.parse("<presence />")],
-						]),
-						chat: { chatId: "hidden@example.com" },
-					},
-				]);
-				const [stored] = await persistence.getChats("alice@example.com");
-				return {
-					mavUntil: stored.mavUntil,
-					membersForName: stored.membersForName.map((m) => m.displayName),
-					presenceKeys: [...stored.presence.keys()].sort(),
-				};
-			},
-			{ borogove, persistence, sqlite },
-		);
-
-		expect(result.mavUntil).toBe("2024-05-01T12:00:00Z");
-		expect(result.membersForName).toEqual(["Alpha", "Zulu"]);
-		expect(result.presenceKeys).toEqual(["desk"]);
-	});
-
-	test("storeStreamManamagement and getStreamManagement", async ({ page, borogove, persistence }) => {
-		const result = await page.evaluate(
-			async ({ borogove, persistence }) => {
-				try {
-					await persistence.storeLogin("alice@example.com", "", "", null); // or updating with SM may not work
-					await persistence.storeStreamManagement(
-						"alice@example.com",
-						new Uint8Array([1, 2, 0, 4]).buffer,
-						"ZZ",
-					);
-					const result =
-						await persistence.getStreamManagement("alice@example.com");
-					return {
-						smIsArrayBuffer: result.sm instanceof ArrayBuffer,
-						smIsEq: result.sm
-							? indexedDB.cmp(result.sm, new Uint8Array([1, 2, 0, 4]).buffer)
-							: "null",
-						sortId: result.sortId,
-					};
-				} catch (e) {
-					console.error(e, e.result);
-					throw e.result ? JSON.stringify(e.result) : e.message;
-				}
-			},
-			{ borogove, persistence },
-		);
-
-		expect(result.smIsEq).toBe(0);
-		expect(result.smIsArrayBuffer).toBe(true);
-		expect(result.sortId).toBe("ZZ");
-	});
-
-	test("getMembers hydrates persisted member data", async ({ page, borogove, persistence }) => {
-		const result = await page.evaluate(
-			async ({ borogove, persistence }) => {
-				const chat = new borogove.Channel(
-					null,
-					null,
-					persistence,
-					"room-members-1@example.com",
-				);
-				chat.displayName = "A Chat";
-				chat.trusted = true;
-
-				await persistence.storeMembers("alice@example.com", chat.chatId, [
-					{
-						id: "room-members-1@example.com/occ-1",
-						displayName: "Alice",
-						photoUri: "photo:alice",
-						isSelf: false,
-						roles: [{ id: "admin", title: "Admin" }],
-						jid: borogove.JID.parse("alice@example.com"),
-						presence: new Map([
-							[
-								"laptop",
-								borogove.Stanza.parse("<presence><show>away</show></presence>"),
-							],
-						]),
-						chat: { chatId: "alice@example.com" },
-					},
-				]);
-
-				const members = await persistence.getMembers(
-					"alice@example.com",
-					chat,
-					false,
-				);
-				return {
-					id: members[0]?.id,
-					displayName: members[0]?.displayName,
-					chatId: members[0]?.chat?.chatId,
-					roleIds: members[0]?.roles?.map((r) => r.id),
-					presenceKeys: members[0] ? [...members[0].presence.keys()] : [],
-					showPresence: members[0]?.showPresence,
-				};
-			},
-			{ borogove, persistence },
-		);
-
-		expect(result.id).toBe("room-members-1@example.com/occ-1");
-		expect(result.displayName).toBe("Alice");
-		expect(result.chatId).toBe("alice@example.com");
-		expect(result.roleIds).toEqual(["admin"]);
-		expect(result.presenceKeys).toEqual(["laptop"]);
-		expect(result.showPresence).toBe(1);
-	});
-
-	test("storeMemberUpdates merges existing member data", async ({ page, borogove, persistence }) => {
-		const result = await page.evaluate(
-			async ({ borogove, persistence }) => {
-				const chat = new borogove.Channel(
-					null,
-					null,
-					persistence,
-					"room-members-2@example.com",
-				);
-				chat.displayName = "A Chat";
-				chat.trusted = true;
-
-				await persistence.storeMembers("alice@example.com", chat.chatId, [
-					{
-						id: "room-members-2@example.com/occ-1",
-						displayName: "Alice",
-						photoUri: null,
-						isSelf: false,
-						roles: [
-							{ id: "admin", title: "Admin" },
-							{ id: "urn:xmpp:hats:test", title: "Tea Host" },
-						],
-						jid: borogove.JID.parse("alice@example.com"),
-						presence: new Map([
-							["desk", borogove.Stanza.parse("<presence />")],
-						]),
-						chat: { chatId: "alice@example.com" },
-					},
-				]);
-
-				const updated = await persistence.storeMemberUpdates(
-					"alice@example.com",
-					chat,
-					[
-						new borogove.MemberUpdate(
-							"room-members-2@example.com/occ-1",
-							borogove.JID.parse("alice@example.com"),
-							"Alice Cooper",
-							false,
-							null,
-							new Map([["mobile", borogove.Stanza.parse("<presence />")]]),
-						),
-					],
-					false,
-				);
-
-				return {
-					displayName: updated[0]?.displayName,
-					roleIds: updated[0]?.roles?.map((r) => r.id),
-					presenceKeys: updated[0]
-						? [...updated[0].presence.keys()].sort()
-						: [],
-				};
-			},
-			{ borogove, persistence },
-		);
-
-		expect(result.displayName).toBe("Alice Cooper");
-		expect(result.roleIds).toEqual(["urn:xmpp:hats:test"]);
-		expect(result.presenceKeys).toEqual(["desk", "mobile"]);
-	});
-
-	test("storeMemberUpdates clears omitted full-list affiliations", async ({
-		page,
-		borogove,
-		persistence,
-	}) => {
-		const result = await page.evaluate(
-			async ({ borogove, persistence }) => {
-				const chat = new borogove.Channel(
-					null,
-					null,
-					persistence,
-					"room-members-2b@example.com",
-				);
-				chat.displayName = "A Chat";
-				chat.trusted = true;
-
-				await persistence.storeMembers("alice@example.com", chat.chatId, [
-					{
-						id: "room-members-2b@example.com/occ-1",
-						displayName: "Alice",
-						photoUri: null,
-						isSelf: false,
-						roles: [{ id: "admin", title: "Admin" }],
-						jid: borogove.JID.parse("alice@example.com"),
-						presence: new Map(),
-						chat: { chatId: "alice@example.com" },
-					},
-					{
-						id: "room-members-2b@example.com/occ-2",
-						displayName: "Bob",
-						photoUri: null,
-						isSelf: false,
-						roles: [{ id: "owner", title: "Owner" }],
-						jid: borogove.JID.parse("bob@example.com"),
-						presence: new Map(),
-						chat: { chatId: "bob@example.com" },
-					},
-				]);
-
-				await persistence.storeMemberUpdates(
-					"alice@example.com",
-					chat,
-					[
-						new borogove.MemberUpdate(
-							"room-members-2b@example.com/occ-1",
-							borogove.JID.parse("alice@example.com"),
-							"Alice",
-							false,
-							null,
-							new Map(),
-						),
-					],
-					true,
-				);
-				const members = await persistence.getMembers(
-					"alice@example.com",
-					chat,
-					true,
-				);
-				return members
-					.find((m) => m.id.endsWith("occ-2"))
-					?.roles?.map((r) => r.id);
-			},
-			{ borogove, persistence },
-		);
-
-		expect(result).toEqual([]);
-	});
-
-	test("storeMemberUpdates matches existing member by true JID", async ({
-		page,
-		borogove,
-		persistence,
-	}) => {
-		const result = await page.evaluate(
-			async ({ borogove, persistence }) => {
-				const chat = new borogove.Channel(
-					null,
-					null,
-					persistence,
-					"room-members-3@example.com",
-				);
-				chat.displayName = "A Chat";
-				chat.trusted = true;
-
-				await persistence.storeMembers("alice@example.com", chat.chatId, [
-					{
-						id: "room-members-3@example.com/occ-1",
-						displayName: "Alice",
-						photoUri: null,
-						isSelf: false,
-						roles: [{ id: "admin", title: "Admin" }],
-						jid: borogove.JID.parse("alice@example.com"),
-						presence: new Map(),
-						chat: { chatId: "alice@example.com" },
-					},
-				]);
-
-				await persistence.storeMemberUpdates(
-					"alice@example.com",
-					chat,
-					[
-						new borogove.MemberUpdate(
-							null,
-							borogove.JID.parse("alice@example.com"),
-							"Alice Renamed",
-							false,
-							null,
-							new Map(),
-						),
-					],
-					false,
-				);
-				const [member] = await persistence.getMemberDetails(
-					"alice@example.com",
-					chat,
-					["room-members-3@example.com/occ-1"],
-				);
-				return member?.displayName;
-			},
-			{ borogove, persistence },
-		);
-
-		expect(result).toBe("Alice Renamed");
-	});
-
-	test("clearMemberPresence only clears the targeted chat", async ({
-		page,
-		borogove,
-		persistence,
-	}) => {
-		const result = await page.evaluate(
-			async ({ borogove, persistence }) => {
-				const chat1 = new borogove.Channel(
-					null,
-					null,
-					persistence,
-					"room-members-4a@example.com",
-				);
-				chat1.displayName = "A Chat";
-				chat1.trusted = true;
-				const chat2 = new borogove.Channel(
-					null,
-					null,
-					persistence,
-					"room-members-4b@example.com",
-				);
-				chat2.displayName = "A Chat";
-				chat2.trusted = true;
-
-				await persistence.storeMembers("alice@example.com", chat1.chatId, [
-					{
-						id: "room-members-4a@example.com/occ-1",
-						displayName: "Alice",
-						photoUri: null,
-						isSelf: false,
-						roles: [{ id: "admin", title: "Admin" }],
-						jid: borogove.JID.parse("alice@example.com"),
-						presence: new Map([
-							["desk", borogove.Stanza.parse("<presence />")],
-						]),
-						chat: { chatId: "alice@example.com" },
-					},
-				]);
-				await persistence.storeMembers("alice@example.com", chat2.chatId, [
-					{
-						id: "room-members-4b@example.com/occ-1",
-						displayName: "Bob",
-						photoUri: null,
-						isSelf: false,
-						roles: [{ id: "admin", title: "Admin" }],
-						jid: borogove.JID.parse("bob@example.com"),
-						presence: new Map([
-							["phone", borogove.Stanza.parse("<presence />")],
-						]),
-						chat: { chatId: "bob@example.com" },
-					},
-				]);
-
-				await persistence.clearMemberPresence(
-					"alice@example.com",
-					chat1.chatId,
-				);
-				const [member1] = await persistence.getMemberDetails(
-					"alice@example.com",
-					chat1,
-					["room-members-4a@example.com/occ-1"],
-				);
-				const [member2] = await persistence.getMemberDetails(
-					"alice@example.com",
-					chat2,
-					["room-members-4b@example.com/occ-1"],
-				);
-				return {
-					chat1PresenceKeys: member1 ? [...member1.presence.keys()] : [],
-					chat2PresenceKeys: member2 ? [...member2.presence.keys()] : [],
-				};
-			},
-			{ borogove, persistence },
-		);
-
-		expect(result.chat1PresenceKeys).toEqual([]);
-		expect(result.chat2PresenceKeys).toEqual(["phone"]);
-	});
+	sharedPersistenceTests(test);
 
 	test("getMembers filters hidden rows for non-moderators", async ({
 		page,
@@ -1567,195 +92,4 @@ test.describe("not webkit", () => {
 		expect(result).toEqual(["Zulu", "Alpha"]);
 	});
 
-	test("getMembers includes moderator-visible rows", async ({ page, borogove, persistence }) => {
-		const result = await page.evaluate(
-			async ({ borogove, persistence }) => {
-				const chat = new borogove.Channel(
-					null,
-					null,
-					persistence,
-					"room-members-6@example.com",
-				);
-				chat.displayName = "A Chat";
-				chat.trusted = true;
-
-				await persistence.storeMembers("alice@example.com", chat.chatId, [
-					{
-						id: "room-members-6@example.com/owner",
-						displayName: "Zulu",
-						photoUri: null,
-						isSelf: false,
-						roles: [{ id: "owner", title: "Owner" }],
-						jid: borogove.JID.parse("zulu@example.com"),
-						presence: new Map([
-							["desk", borogove.Stanza.parse("<presence />")],
-						]),
-						chat: { chatId: "zulu@example.com" },
-					},
-					{
-						id: "room-members-6@example.com/outcast",
-						displayName: "Banned",
-						photoUri: null,
-						isSelf: false,
-						roles: [{ id: "outcast", title: "Banned" }],
-						jid: borogove.JID.parse("banned@example.com"),
-						presence: new Map([
-							["desk", borogove.Stanza.parse("<presence />")],
-						]),
-						chat: { chatId: "banned@example.com" },
-					},
-					{
-						id: "room-members-6@example.com/guest-offline",
-						displayName: "Guest",
-						photoUri: null,
-						isSelf: false,
-						roles: [{ id: "none", title: "Guest" }],
-						jid: borogove.JID.parse("guest@example.com"),
-						presence: new Map([
-							[
-								"desk",
-								borogove.Stanza.parse('<presence type="unavailable" />'),
-							],
-						]),
-						chat: { chatId: "guest@example.com" },
-					},
-					{
-						id: "room-members-6@example.com/admin",
-						displayName: "Alpha",
-						photoUri: null,
-						isSelf: false,
-						roles: [{ id: "admin", title: "Admin" }],
-						jid: borogove.JID.parse("alpha@example.com"),
-						presence: new Map([
-							["desk", borogove.Stanza.parse("<presence />")],
-						]),
-						chat: { chatId: "alpha@example.com" },
-					},
-				]);
-
-				const members = await persistence.getMembers(
-					"alice@example.com",
-					chat,
-					true,
-				);
-				return members.map((m) => m.displayName);
-			},
-			{ borogove, persistence },
-		);
-
-		expect(result).toEqual(["Zulu", "Alpha", "Banned"]);
-	});
-
-	test("getMemberDetails returns null for incomplete rows", async ({
-		page,
-		borogove,
-		persistence,
-	}) => {
-		const result = await page.evaluate(
-			async ({ borogove, persistence }) => {
-				const chat = new borogove.Channel(
-					null,
-					null,
-					persistence,
-					"room-members-7@example.com",
-				);
-				chat.displayName = "A Chat";
-				chat.trusted = true;
-
-				await persistence.storeMembers("alice@example.com", chat.chatId, [
-					{
-						id: "room-members-7@example.com/admin",
-						displayName: "Alpha",
-						photoUri: null,
-						isSelf: false,
-						roles: [{ id: "admin", title: "Admin" }],
-						jid: borogove.JID.parse("alpha@example.com"),
-						presence: new Map([
-							["desk", borogove.Stanza.parse("<presence />")],
-						]),
-						chat: { chatId: "alpha@example.com" },
-					},
-				]);
-
-				await persistence.db.exec(
-					"INSERT INTO members(account_id, chat_id, member_id, display_name, photo_uri, is_self, chat, roles, presence, jid) VALUES(?, ?, ?, ?, ?, ?, ?, jsonb(?), jsonb(?), ?)",
-					[
-						"alice@example.com",
-						chat.chatId,
-						"room-members-7@example.com/incomplete",
-						"",
-						null,
-						0,
-						"{}",
-						"[]",
-						"{}",
-						"",
-					],
-				);
-				const details = await persistence.getMemberDetails(
-					"alice@example.com",
-					chat,
-					[
-						"room-members-7@example.com/admin",
-						"room-members-7@example.com/incomplete",
-					],
-				);
-				return details.map((m) => (m ? m.displayName : null));
-			},
-			{ borogove, persistence },
-		);
-
-		expect(result).toEqual(["Alpha", null]);
-	});
-
-	test("storeVoiceRequest and listVoiceRequests", async ({ page, borogove, persistence }) => {
-		const result = await page.evaluate(
-			async ({ borogove, persistence }) => {
-				const chat = Object.create(borogove.Channel.prototype);
-				chat.chatId = "room-voice-requests@example.com";
-				chat.getDisplayName = () => "Tea Room";
-
-				await persistence.storeMembers("alice@example.com", chat.chatId, [
-					{
-						id: "room-voice-requests@example.com/occ-1",
-						displayName: "Bob",
-						photoUri: null,
-						isSelf: false,
-						roles: [{ id: "none", title: "Participant" }],
-						jid: borogove.JID.parse("bob@example.com"),
-						presence: new Map([["desk", borogove.Stanza.parse("<presence />")]]),
-						chat: { chatId: "bob@example.com" },
-					},
-					{
-						id: "room-voice-requests@example.com/occ-2",
-						displayName: "Charlie",
-						photoUri: null,
-						isSelf: false,
-						roles: [{ id: "none", title: "Participant" }],
-						jid: borogove.JID.parse("charlie@example.com"),
-						presence: new Map([["desk", borogove.Stanza.parse("<presence />")]]),
-						chat: { chatId: "charlie@example.com" },
-					}
-				]);
-
-				await persistence.storeVoiceRequest("alice@example.com", chat, "bob@example.com", true);
-				await persistence.storeVoiceRequest("alice@example.com", chat, "charlie@example.com", true);
-
-				const requests1 = await persistence.listVoiceRequests("alice@example.com", chat);
-
-				await persistence.storeVoiceRequest("alice@example.com", chat, "bob@example.com", false);
-
-				const requests2 = await persistence.listVoiceRequests("alice@example.com", chat);
-
-				return {
-					requests1: requests1.map((m) => m.displayName).sort(),
-					requests2: requests2.map((m) => m.displayName).sort(),
-				};
-			},
-			{ borogove, persistence },
-		);
-
-		expect(result.requests1).toEqual(["Bob", "Charlie"]);
-		expect(result.requests2).toEqual(["Charlie"]);
-	});
 });