import "fake-indexeddb/auto";
import { test } from "node:test";
import assert from "node:assert/strict";
import parse from "ltx/lib/parse.js";
import { ChatDB, type StoredMessage } from "../src/store/db.ts";
import { parseMessage } from "../src/xmpp/parse.ts";
import { composeReply, displayQuote, matchesReply, parseReply, replyBody, replyDraft } from "../src/xmpp/replies.ts";
const me = "me@example.org", peer = "you@example.org";
const el = (s: string) => parse(s) as any;
const original: StoredMessage = { key: "original", peer, type: "chat", dir: "in", from: peer + "/phone", id: "sender-id", originId: "origin-id", body: "Original š±\nsecond line", ts: 1, encrypted: true };
test("reply wire uses origin IDs and Unicode codepoint fallback; unsupported clients retain one useful quote", () => {
const target = replyDraft(original)!;
const content = composeReply("Answer š", target);
const stanza = el(`${content.body}${content.elements.join("")}`);
const parsed = parseMessage(stanza, { me })!;
assert.equal(parsed.body, "Answer š");
assert.equal(parsed.reply?.id, "origin-id");
assert.equal(parsed.reply?.quote, "you@example.org:\nOriginal š±\nsecond line");
assert.equal((content.body.match(/Original/g) ?? []).length, 1);
assert.equal(content.elements[1].getChild("body")?.attrs.end, String(Array.from(content.body.slice(0, content.body.indexOf("Answer"))).length));
});
test("group replies require room-stamped IDs, distinct from the archive cursor", () => {
const room = "room@conference.example.org";
const group = (extra: string, archiveId?: string) => parseMessage(el(`hello${extra}`), { me, archiveId })!;
assert.equal(replyDraft(group("", "mam-cursor")), undefined);
assert.equal(replyDraft(group(``)), undefined);
assert.equal(replyDraft(group(``)), undefined);
const valid = group(``, "mam-cursor");
assert.equal(replyDraft(valid)?.id, "room-id");
assert.equal(valid.archiveId, "mam-cursor");
});
test("malformed, unrelated and ambiguous reply metadata cannot hide text or choose another conversation", async () => {
const xml = (metadata: string) => el(`${metadata}`);
assert.equal(parseReply(xml(''), peer, me, false), undefined);
assert.equal(parseReply(xml(''), peer, me, false), undefined);
const reply = { id: "origin-id", to: peer };
for (const range of ['start="0" end="999"', 'start="0" end="3.5"', 'start="0"', 'start="3" end="4"', 'start="0" end="5"']) {
assert.equal(replyBody(xml(``), "hello", reply).body, "hello");
}
assert.equal(matchesReply(original, "elsewhere@example.org", reply), false);
const db = await ChatDB.open("reply-isolation@example.org");
try {
await db.putMessage(original, false);
await db.putMessage({ ...original, key: "collision", from: me, dir: "out" }, false);
assert.equal(await db.resolveReply(peer, { id: "origin-id" }), undefined);
assert.equal((await db.resolveReply(peer, reply))?.key, original.key);
assert.equal(await db.resolveReply("elsewhere@example.org", reply), undefined);
} finally { db.close(); await ChatDB.destroy("reply-isolation@example.org"); }
});
test("encrypted parsing defers fallback until real plaintext and refuses arbitrary ciphertext-metadata stripping", () => {
const content = composeReply("Secret answer", replyDraft(original)!);
const stanza = el(`This message is encrypted${content.elements.join("")}`);
const parsed = parseMessage(stanza, { me })!;
assert.equal(parsed.body, "This message is encrypted");
assert.equal(parsed.reply?.quote, undefined);
assert.deepEqual(replyBody(stanza, content.body, parsed.reply), { body: "Secret answer", reply: content.reply });
const hostile = el('');
assert.equal(replyBody(hostile, "Secret answer", parsed.reply).body, "Secret answer");
assert.equal(replyDraft({ ...original, undecryptable: true }), undefined);
});
test("reply drafts persist per chat through restart/cancel and survive competing send completion", async () => {
const account = "reply-draft@example.org", target = replyDraft(original)!;
let db = await ChatDB.open(account);
try {
await db.saveDraft(peer, "chat", { text: "", reply: target, revision: "quote-only" });
db.close(); db = await ChatDB.open(account);
assert.deepEqual((await db.getDraft(peer))?.reply, target);
assert.equal((await db.getConversations())[0].draft, "Reply draft");
assert.equal(await db.getDraft("elsewhere@example.org"), undefined);
await db.saveDraft(peer, "chat", { text: "text while sending", reply: target, revision: "new" });
assert.equal(await db.clearSentDraft(peer, "quote-only"), false);
assert.deepEqual((await db.getDraft(peer))?.reply, target);
await db.saveDraft(peer, "chat", { text: "text while sending", revision: "cancel" });
assert.equal((await db.getDraft(peer))?.reply, undefined);
assert.equal((await db.getDraft(peer))?.text, "text while sending");
} finally { db.close(); await ChatDB.destroy(account); }
});
test("an edited original retains its reference; room reflection supplies the local echo's stable ID", async () => {
const room = "room@conference.example.org", account = "reply-reflection@example.org";
const message: StoredMessage = { key: "room-original", peer: room, type: "groupchat", dir: "out", from: room + "/Me", body: "before edit", ts: 1, id: "sender-id" };
const db = await ChatDB.open(account);
try {
await db.putMessage(message, false);
assert.equal(replyDraft(message), undefined);
await db.putMessage({ ...message, stanzaId: "room-stable-id", archiveId: "cursor" }, false);
const reflected = (await db.messagesByKeys([message.key]))[0];
const editedDraft = replyDraft(reflected, "after edit")!;
assert.equal(editedDraft.id, "room-stable-id");
assert.equal(editedDraft.text, "after edit");
await db.putMessage({ ...message, key: "correction", id: "correction-id", replaceId: "sender-id", body: "after edit", ts: 2 }, false);
assert.equal((await db.resolveReply(room, { id: "room-stable-id", to: message.from }))?.key, message.key);
assert.equal(composeReply("yes", editedDraft).reply?.quote, "Me:\nafter edit");
} finally { db.close(); await ChatDB.destroy(account); }
});
test("quoting an attachment names it; an encrypted one never carries its key into the quote", () => {
const key = "76396656ed08305a82762013b3722d174cdaa876c450176cf4a5b7065eec5352adebe8c9a38a49e48e34242d";
const voice = `aesgcm://upload.0ff.space/file_share/01a11c4b/C8331F4B.m4a#${key}`;
const original: StoredMessage = {
key: "k", peer, type: "chat", dir: "in", from: peer + "/phone", id: "i", originId: "o",
body: voice, ts: 1, encrypted: true,
};
const target = replyDraft(original)!;
assert.equal(target.text, "š¤ Voice message");
const content = composeReply("Nice one", target);
// The fragment is the file key; it has no business in a second message.
assert.equal(content.body.includes(key), false);
assert.equal(content.body.includes("aesgcm://"), false);
assert.equal(content.reply?.quote?.includes(key), false);
assert.ok(content.body.startsWith(`> ${peer}:\n> š¤ Voice message\n\n`));
// A photo and a plaintext upload are named too, from their own shapes.
const photo = { ...original, body: "aesgcm://upload.0ff.space/f/x.jpg#" + key };
assert.equal(replyDraft(photo)!.text, "š· Photo");
const plain = { ...original, body: "https://upload.0ff.space/f/notes.pdf", oob: "https://upload.0ff.space/f/notes.pdf", encrypted: undefined };
assert.equal(replyDraft(plain)!.text, "š notes.pdf");
// Ordinary text, including a bare link somebody typed, is quoted as written.
const text = { ...original, body: "look at https://example.org/thing", encrypted: undefined };
assert.equal(replyDraft(text)!.text, "look at https://example.org/thing");
});
test("a quote that already holds an aesgcm URL is named at render time, not shown", () => {
const key = "a".repeat(64);
// History written before quotedText existed, and anything a remote client sends.
assert.equal(displayQuote(`${peer}:\naesgcm://upload.0ff.space/f/v.m4a#${key}`), `${peer}:\nš¤ Voice message`);
assert.equal(displayQuote(`aesgcm://upload.0ff.space/f/p.png#${key}`), "š· Photo");
// A plain link may just be a link; it stays as written.
assert.equal(displayQuote("see https://example.org/x"), "see https://example.org/x");
assert.equal(displayQuote("https://example.org/x"), "https://example.org/x");
assert.equal(displayQuote("nothing special"), "nothing special");
});