From 529f641365a2dc1787a863c2adaa841255119544 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 8 Oct 2026 18:37:48 +0000 Subject: [PATCH 1/2] feat: sort graduated launches by price Market cap folds in effective supply, so a launch that burned part of its supply ranks below one that did not at the same per-token price. Add a Price sort that orders by XCP per whole token: SORT_SQL's `price` for the indexed path and a matching comparator for the live fallback. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LkjKD3QZivr3Qr2cKdjfbz --- apps/api/src/queries/launches.ts | 9 +++++++++ apps/web/src/app/[lang]/_components/launch-sections.tsx | 3 +++ 2 files changed, 12 insertions(+) diff --git a/apps/api/src/queries/launches.ts b/apps/api/src/queries/launches.ts index bf75d365..6e28de38 100644 --- a/apps/api/src/queries/launches.ts +++ b/apps/api/src/queries/launches.ts @@ -250,6 +250,15 @@ export function paceOrder(tip: number): string { const SORT_SQL = { progress: "rank_key DESC", mcap: "market_cap_rank DESC", + // XCP per whole token, the pool ratio the price column shows. Unlike market + // cap this ignores supply, so a launch that burned part of its supply ranks + // by what one token costs rather than by what all of them are worth. NULL + // (no pool yet) sorts last under DESC. + price: `CASE + WHEN CAST(pool_token_reserve AS REAL) > 0 + THEN CAST(pool_xcp_reserve AS REAL) / CAST(pool_token_reserve AS REAL) + ELSE NULL + END DESC`, // USD return is current TOKEN/XCP * current XCP/USD divided by mint // TOKEN/XCP * graduation XCP/USD. The current XCP/USD factor is common to // every launch, so omitting it preserves the exact rank. NULL sorts last diff --git a/apps/web/src/app/[lang]/_components/launch-sections.tsx b/apps/web/src/app/[lang]/_components/launch-sections.tsx index 24dee313..c708e1a3 100644 --- a/apps/web/src/app/[lang]/_components/launch-sections.tsx +++ b/apps/web/src/app/[lang]/_components/launch-sections.tsx @@ -194,6 +194,9 @@ const compareRecent = (a: SectionRow, b: SectionRow): number => { export const SORTS: Record = { graduated: [ { id: "mcap", label: msg("Market cap"), by: (a, b) => b.marketCapXcp - a.marketCapXcp }, + // Per token, not per supply: burns lower market cap but not price. A + // launch with no pool prices at 0 and lands last, as NULL does in SQL. + { id: "price", label: msg("Price"), by: (a, b) => b.priceXcp - a.priceXcp }, { id: "performance", label: msg("Performance (All)"), by: comparePerformance }, { id: "performance_24h", label: msg("Performance (24h)"), by: compareRecent }, { id: "minters", label: msg("Minters"), by: (a, b) => minterRank(b) - minterRank(a) }, From ce369e327b28c3e5325abd9c58989990f36d0474 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 8 Oct 2026 18:41:21 +0000 Subject: [PATCH 2/2] fix(chat): keep the last 200 messages instead of expiring them after a day A quiet room used to delete every message 24 hours after it was posted, so low chat volume meant visitors opened an empty transcript. History is now bounded by count alone: the newest 200 messages stay until newer posts push them out. Retry receipts, which hold no text and only back retries and the cooldown, keep their 24-hour expiry. The client drops its age filter and the history frame size limit scales with the new cap. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LkjKD3QZivr3Qr2cKdjfbz --- apps/api/src/durable/chat-room.ts | 24 +++++++--------- apps/web/src/hooks/use-chat.ts | 9 ++---- packages/chat/src/protocol.ts | 11 +++++-- tests/chat-panel.test.tsx | 8 +++--- tests/chat-protocol.test.ts | 4 +-- tests/chat-room.test.ts | 48 +++++++++++++++---------------- 6 files changed, 51 insertions(+), 53 deletions(-) diff --git a/apps/api/src/durable/chat-room.ts b/apps/api/src/durable/chat-room.ts index 843cacb2..80b91864 100644 --- a/apps/api/src/durable/chat-room.ts +++ b/apps/api/src/durable/chat-room.ts @@ -1,5 +1,5 @@ import { DurableObject } from "cloudflare:workers"; -import { CHAT_COOLDOWN_MS, CHAT_MAX_BANS, CHAT_MAX_CONNECTIONS, CHAT_MAX_MESSAGES, CHAT_TTL_MS, chatHandle, isChatAuthorId, normalizeChatPost, type ChatBan, type ChatBanResult, type ChatFrame, type ChatMessage, type ChatPost, type ChatPostResult } from "@launchpad/chat"; +import { CHAT_COOLDOWN_MS, CHAT_MAX_BANS, CHAT_MAX_CONNECTIONS, CHAT_MAX_MESSAGES, chatHandle, isChatAuthorId, normalizeChatPost, type ChatBan, type ChatBanResult, type ChatFrame, type ChatMessage, type ChatPost, type ChatPostResult } from "@launchpad/chat"; import type { Env } from "#api/env"; import { closeWebSocket } from "#api/durable/websocket"; @@ -8,6 +8,9 @@ const GLOBAL_WINDOW_MS = 10_000; const GLOBAL_MAX_POSTS = 20; /** Retry receipts contain no text and have a bounded lifetime and row count. */ const MAX_RECEIPTS = 2_048; +/** Receipts back retries and the per-author cooldown, so they still expire. + * Messages do not: history is bounded by CHAT_MAX_MESSAGES alone. */ +const RECEIPT_TTL_MS = 24 * 60 * 60 * 1_000; type MessageRow = { id: string; author_id: string; handle: string; text: string; created_at: number }; type ReceiptRow = { message_id: string; created_at: number; digest: string }; @@ -104,7 +107,7 @@ export class ChatRoom extends DurableObject { if (sender && now - sender.created_at < CHAT_COOLDOWN_MS) return { ok: false, error: "rate_limited", retryAfter: Math.ceil((sender.created_at + CHAT_COOLDOWN_MS - now) / 1_000), }; - // Fifty retained messages exceed the twenty-message global window, so + // Two hundred retained messages exceed the twenty-message global window, so // capping history can never erase a still-relevant global rate entry. const recent = this.ctx.storage.sql.exec<{ count: number; oldest: number | null }>( "SELECT COUNT(*) AS count, MIN(created_at) AS oldest FROM chat_messages WHERE created_at > ?", now - GLOBAL_WINDOW_MS, @@ -165,11 +168,10 @@ export class ChatRoom extends DurableObject { webSocketError(ws: WebSocket) { closeWebSocket(ws, 1011, "WebSocket error"); this.publishPresence(); } async alarm() { - const before = this.history().length; + // Only receipts expire, and they are never shown, so readers hear nothing. this.ctx.storage.transactionSync(() => this.pruneExpired(Date.now())); const cleanup = this.scheduleCleanup(); if (this.env.CHAT_ENABLED !== "true") this.closeReaders(); - else if (this.history().length !== before) this.broadcast({ type: "history", messages: this.history() }); await cleanup; } @@ -178,8 +180,7 @@ export class ChatRoom extends DurableObject { } private pruneExpired(now: number) { - this.ctx.storage.sql.exec("DELETE FROM chat_messages WHERE created_at <= ?", now - CHAT_TTL_MS); - this.ctx.storage.sql.exec("DELETE FROM chat_receipts WHERE created_at <= ?", now - CHAT_TTL_MS); + this.ctx.storage.sql.exec("DELETE FROM chat_receipts WHERE created_at <= ?", now - RECEIPT_TTL_MS); } private pruneCaps() { @@ -190,16 +191,11 @@ export class ChatRoom extends DurableObject { } private scheduleCleanup(): Promise { - const messages = this.ctx.storage.sql.exec<{ oldest: number | null }>( - "SELECT MIN(created_at) AS oldest FROM chat_messages", - ).one().oldest; - // A direct MIN uses the existing receipt timestamp index. MIN over a - // UNION scanned every retained receipt each time someone opened chat. - const receipts = this.ctx.storage.sql.exec<{ oldest: number | null }>( + // A direct MIN uses the existing receipt timestamp index. + const oldest = this.ctx.storage.sql.exec<{ oldest: number | null }>( "SELECT MIN(created_at) AS oldest FROM chat_receipts", ).one().oldest; - const oldest = messages === null ? receipts : receipts === null ? messages : Math.min(messages, receipts); - const desired = oldest === null ? null : oldest + CHAT_TTL_MS; + const desired = oldest === null ? null : oldest + RECEIPT_TTL_MS; // Capture the SQL decision now, then apply decisions in event order. The // durable alarm remains the source of truth after hibernation, and an // older empty read cannot delete a newer post's cleanup alarm. diff --git a/apps/web/src/hooks/use-chat.ts b/apps/web/src/hooks/use-chat.ts index 7a614abe..7a12d03a 100644 --- a/apps/web/src/hooks/use-chat.ts +++ b/apps/web/src/hooks/use-chat.ts @@ -1,15 +1,14 @@ "use client"; import { useCallback, useEffect, useRef, useState, useSyncExternalStore } from "react"; -import { CHAT_MAX_MESSAGES, CHAT_TTL_MS, isChatAuthorId, isChatHandle, parseChatFrame, type ChatBan, type ChatMessage } from "@launchpad/chat"; +import { CHAT_MAX_MESSAGES, isChatAuthorId, isChatHandle, parseChatFrame, type ChatBan, type ChatMessage } from "@launchpad/chat"; const WS_URL = process.env.NEXT_PUBLIC_CHAT_WS_URL ?? "wss://api.xcp.fun/ws/chat"; const MAX_BACKOFF_MS = 30_000; export type ChatStatus = "connecting" | "live" | "reconnecting" | "offline"; function recentMessages(messages: ChatMessage[]): ChatMessage[] { - const cutoff = Date.now() - CHAT_TTL_MS; - return [...new Map(messages.filter((message) => message.createdAt > cutoff).map((message) => [message.id, message])).values()] + return [...new Map(messages.map((message) => [message.id, message])).values()] .sort((a, b) => a.createdAt - b.createdAt || a.id.localeCompare(b.id)) .slice(-CHAT_MAX_MESSAGES); } @@ -50,7 +49,7 @@ export function useChat(active: boolean) { const frame = parseChatFrame(event.data); if (!frame) return; if (frame.count !== undefined) setConnections(frame.count); - // Reconnection history is authoritative: expired/removed messages + // Reconnection history is authoritative: capped/removed messages // must disappear instead of being merged back into the transcript. if (frame.type === "history") { attempt = 0; @@ -64,11 +63,9 @@ export function useChat(active: boolean) { ws.onerror = () => { if (current()) ws.close(); }; }; connect(); - const expiry = setInterval(() => setMessages((previous) => recentMessages(previous)), 30_000); return () => { stopped = true; clearTimeout(timer); - clearInterval(expiry); socket?.close(); }; }, [active, acceptMessage]); diff --git a/packages/chat/src/protocol.ts b/packages/chat/src/protocol.ts index 98b261d8..d4fd4be6 100644 --- a/packages/chat/src/protocol.ts +++ b/packages/chat/src/protocol.ts @@ -1,8 +1,12 @@ /** Shared wire limits: Unicode codepoints, not UTF-16 units or Latin letters. */ export const CHAT_MAX_CODEPOINTS = 280; export const CHAT_MAX_LINES = 3; -export const CHAT_MAX_MESSAGES = 50; -export const CHAT_TTL_MS = 24 * 60 * 60 * 1_000; +/** + * History is kept by count, never by age. A quiet room used to expire every + * message after a day and open to an empty transcript; now the last + * CHAT_MAX_MESSAGES stay until newer posts push them out. + */ +export const CHAT_MAX_MESSAGES = 200; export const CHAT_COOLDOWN_MS = 3_000; export const CHAT_MAX_RAW_BYTES = 4_096; export const CHAT_MAX_CONNECTIONS = 500; @@ -88,7 +92,8 @@ function parseMessage(value: unknown): ChatMessage | null { /** Parse only bounded, valid frames; do not forward unknown fields into UI state. */ export function parseChatFrame(value: unknown): ChatFrame | null { if (typeof value === "string") { - if (value.length > 100_000) return null; + // A full history frame: worst-case escaped text plus envelope per message. + if (value.length > CHAT_MAX_MESSAGES * 2_000) return null; try { value = JSON.parse(value); } catch { return null; } } if (!object(value)) return null; diff --git a/tests/chat-panel.test.tsx b/tests/chat-panel.test.tsx index 736a93f1..50618de1 100644 --- a/tests/chat-panel.test.tsx +++ b/tests/chat-panel.test.tsx @@ -4,7 +4,7 @@ import { createRoot, type Root } from "react-dom/client"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { ChatPanel } from "@/components/chat-panel"; import { LocaleProvider } from "@/lib/i18n/client"; -import { CHAT_COOLDOWN_MS, chatHandle, type ChatMessage } from "@launchpad/chat"; +import { CHAT_COOLDOWN_MS, CHAT_MAX_MESSAGES, chatHandle, type ChatMessage } from "@launchpad/chat"; import { ChatProvider, useChatRoute } from "@/providers/chat-context"; const boundary = vi.hoisted(() => ({ @@ -114,9 +114,9 @@ describe("chat transcript and socket lifecycle", () => { it("deduplicates messages, bounds history, replaces it on reconnect and ignores stale/malformed frames", async () => { vi.useFakeTimers(); await render(); const first = newestSocket(); - await act(() => first.frame({ type: "history", messages: Array.from({ length: 50 }, (_, n) => message(n + 1, `history ${n}`)) })); - await act(() => { first.frame({ type: "message", message: message(51, "latest") }); first.frame({ type: "message", message: message(51, "latest") }); first.frame("x".repeat(100001)); }); - expect(transcript().querySelectorAll("p")).toHaveLength(50); + await act(() => first.frame({ type: "history", messages: Array.from({ length: CHAT_MAX_MESSAGES }, (_, n) => message(n + 1, `history ${n}`)) })); + await act(() => { first.frame({ type: "message", message: message(CHAT_MAX_MESSAGES + 1, "latest") }); first.frame({ type: "message", message: message(CHAT_MAX_MESSAGES + 1, "latest") }); first.frame("x".repeat(CHAT_MAX_MESSAGES * 2_000 + 1)); }); + expect(transcript().querySelectorAll("p")).toHaveLength(CHAT_MAX_MESSAGES); expect(transcript().textContent).not.toContain("history 0"); await act(() => first.close()); await act(async () => vi.advanceTimersByTime(1001)); diff --git a/tests/chat-protocol.test.ts b/tests/chat-protocol.test.ts index 090e16af..fcab4bb8 100644 --- a/tests/chat-protocol.test.ts +++ b/tests/chat-protocol.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { CHAT_MAX_CODEPOINTS, CHAT_MAX_LINES, chatHandle, chatLineCount, normalizeChatPost, normalizeChatText, parseChatFrame, type ChatMessage } from "@launchpad/chat"; +import { CHAT_MAX_CODEPOINTS, CHAT_MAX_MESSAGES, CHAT_MAX_LINES, chatHandle, chatLineCount, normalizeChatPost, normalizeChatText, parseChatFrame, type ChatMessage } from "@launchpad/chat"; const message: ChatMessage = { id: "message-123", authorId: "a".repeat(64), handle: "Quiet-Otter-1234", text: "Hello", createdAt: 1_800_000_000_000 }; const compactMessage: ChatMessage = { ...message, handle: chatHandle(message.authorId) }; @@ -47,7 +47,7 @@ describe("plain-text chat protocol", () => { expect(parseChatFrame({ type: "history", messages: [message] })).toEqual({ type: "history", messages: [compactMessage] }); expect(parseChatFrame({ type: "history", messages: [] })).toEqual({ type: "history", messages: [] }); expect(parseChatFrame({ type: "history", messages: [message, message] })).toBeNull(); - expect(parseChatFrame({ type: "history", messages: Array.from({ length: 51 }, (_, i) => ({ ...message, id: `message-${i}` })) })).toBeNull(); + expect(parseChatFrame({ type: "history", messages: Array.from({ length: CHAT_MAX_MESSAGES + 1 }, (_, i) => ({ ...message, id: `message-${i}` })) })).toBeNull(); expect(parseChatFrame({ type: "message", message: { ...message, createdAt: NaN } })).toBeNull(); expect(parseChatFrame({ type: "message", message: { ...message, text: " noncanonical " } })).toBeNull(); expect(parseChatFrame("x".repeat(100001))).toBeNull(); diff --git a/tests/chat-room.test.ts b/tests/chat-room.test.ts index 17a8ca55..be1c8105 100644 --- a/tests/chat-room.test.ts +++ b/tests/chat-room.test.ts @@ -8,6 +8,7 @@ import { closeWebSocket } from "../apps/api/src/durable/websocket"; // Execute the actual DO class and SQL against SQLite. Only Cloudflare's base // class/transport/alarm boundary is replaced; no message or limit logic is. +const RECEIPT_TTL_MS = 24 * 60 * 60 * 1_000; const source = readFileSync(new URL("../apps/api/src/durable/chat-room.ts", import.meta.url), "utf8") .replace(/^import .*;\r?$/gm, "").replace("export class ChatRoom", "class ChatRoom"); const compiled = transformSync(source, { loader: "ts", target: "es2022" }).code; @@ -214,41 +215,39 @@ describe("shared read-only chat room", () => { expect(receipt.digest).toBe(Buffer.from(bytes).toString("hex")); }); - it("keeps only 50 ordered messages while retrying a capped message without extra stored text", async () => { + it("keeps only 200 ordered messages while retrying a capped message without extra stored text", async () => { const s = setup(); const first = await s.publish(post()); - for (let n = 2; n <= 51; n++) { s.advance(1000); expect((await s.publish(post(n))).ok).toBe(true); } + for (let n = 2; n <= 201; n++) { s.advance(1000); expect((await s.publish(post(n))).ok).toBe(true); } s.hibernate(); const { socket } = await s.connect(); const history = socket.frames[0]; expect(history.type).toBe("history"); if (history.type === "history") { - expect(history.messages).toHaveLength(50); - expect(history.messages[0].text).toBe("Message 2"); expect(history.messages.at(-1)!.text).toBe("Message 51"); + expect(history.messages).toHaveLength(200); + expect(history.messages[0].text).toBe("Message 2"); expect(history.messages.at(-1)!.text).toBe("Message 201"); } expect(await s.publish(post())).toEqual(first); - expect(socket.frames).toHaveLength(1); expect(s.count("chat_messages")).toBe(50); + expect(socket.frames).toHaveLength(1); expect(s.count("chat_messages")).toBe(200); const receipt = s.db.prepare("SELECT * FROM chat_receipts WHERE author_id = ?").get(author())!; expect(receipt).not.toHaveProperty("text"); expect(receipt).not.toHaveProperty("handle"); }); - it("prunes on the 24-hour boundary, broadcasts expiry and retires its cleanup alarm", async () => { - const s = setup(); await s.publish(post()); const firstExpiry = s.now + protocol.CHAT_TTL_MS; + it("expires receipts on the 24-hour boundary but keeps messages, silently retiring its alarm", async () => { + const s = setup(); await s.publish(post()); const firstExpiry = s.now + RECEIPT_TTL_MS; s.advance(4000); await s.publish(post(2)); const { socket } = await s.connect(); expect(s.alarm).toBe(firstExpiry); - s.advance(protocol.CHAT_TTL_MS - 4000); s.hibernate(); await s.fire(); - expect(s.count("chat_messages")).toBe(1); expect(s.count("chat_receipts")).toBe(1); + s.advance(RECEIPT_TTL_MS - 4000); s.hibernate(); await s.fire(); + expect(s.count("chat_messages")).toBe(2); expect(s.count("chat_receipts")).toBe(1); expect(s.alarm).toBe(firstExpiry + 4000); expect(s.alarms.set.mock.calls).toEqual([[firstExpiry], [firstExpiry + 4000]]); - expect(socket.frames.at(-1)?.type).toBe("history"); s.advance(4000); await s.fire(); - expect(socket.frames.at(-1)).toEqual({ type: "history", messages: [], count: 1 }); - expect(s.count("chat_messages")).toBe(0); expect(s.count("chat_receipts")).toBe(0); expect(s.alarm).toBeNull(); - const frames = socket.frames.length; await s.fire(); expect(socket.frames).toHaveLength(frames); + expect(s.count("chat_messages")).toBe(2); expect(s.count("chat_receipts")).toBe(0); expect(s.alarm).toBeNull(); + await s.fire(); expect(socket.frames.filter((frame) => frame.type === "history")).toHaveLength(1); expect(s.alarms.delete).not.toHaveBeenCalled(); }); it("does not rewrite unchanged alarms for reads, cooldown refusals, retries, or hibernation", async () => { const s = setup(); await s.connect(); await s.connect(); await s.fire(); expect(s.alarms.set).not.toHaveBeenCalled(); expect(s.alarms.delete).not.toHaveBeenCalled(); - const first = await s.publish(post()); const expiry = s.now + protocol.CHAT_TTL_MS; + const first = await s.publish(post()); const expiry = s.now + RECEIPT_TTL_MS; expect(s.alarms.set.mock.calls).toEqual([[expiry]]); await s.connect(); s.hibernate(); await s.connect(); expect(await s.publish(post(2, author()))).toEqual({ ok: false, error: "rate_limited", retryAfter: 3 }); @@ -260,7 +259,7 @@ describe("shared read-only chat room", () => { it("keeps a newer post's alarm after a delayed read prunes the previously expired room", async () => { const s = setup(); await s.publish(post()); const expiredAlarm = s.alarm; - s.advance(protocol.CHAT_TTL_MS + 1); + s.advance(RECEIPT_TTL_MS + 1); let finishRead!: (value: number | null) => void; s.alarms.get.mockImplementationOnce(() => new Promise((resolve) => { finishRead = resolve; })); const reading = s.connect(); @@ -271,13 +270,14 @@ describe("shared read-only chat room", () => { finishRead(expiredAlarm); expect((await reading).response.status).toBe(101); expect((await publishing).ok).toBe(true); expect(s.alarms.delete).toHaveBeenCalledTimes(1); - expect(s.alarm).toBe(s.now + protocol.CHAT_TTL_MS); + expect(s.alarm).toBe(s.now + RECEIPT_TTL_MS); expect(s.alarms.set).toHaveBeenCalledTimes(2); }); - it("does not replay stale history when an alarm was delayed", async () => { - const s = setup(); await s.publish(post()); s.advance(protocol.CHAT_TTL_MS + 1); s.hibernate(); - const { socket } = await s.connect(); expect(socket.frames).toEqual([{ type: "history", messages: [], count: 1 }]); + it("serves day-old history to a quiet room after a delayed alarm", async () => { + const s = setup(); const first = await s.publish(post()); s.advance(RECEIPT_TTL_MS + 1); s.hibernate(); + const { socket } = await s.connect(); + if (first.ok) expect(socket.frames).toEqual([{ type: "history", messages: [first.message], count: 1 }]); expect(s.count("chat_receipts")).toBe(0); expect(s.alarm).toBeNull(); expect(s.alarms.delete).toHaveBeenCalledTimes(1); await s.connect(); await s.connect(); @@ -292,13 +292,13 @@ describe("shared read-only chat room", () => { s.db.prepare(`WITH RECURSIVE seed(n) AS (VALUES(1) UNION ALL SELECT n + 1 FROM seed WHERE n < 2048) INSERT INTO chat_receipts (author_id, request_id, message_id, created_at, digest) SELECT printf('%064x', n), printf('request-%05d', n), printf('seed-message-%05d', n), ? + n * 3000, printf('%064x', 0) FROM seed`).run(base); - s.db.prepare(`WITH RECURSIVE seed(n) AS (VALUES(1999) UNION ALL SELECT n + 1 FROM seed WHERE n < 2048) + s.db.prepare(`WITH RECURSIVE seed(n) AS (VALUES(1849) UNION ALL SELECT n + 1 FROM seed WHERE n < 2048) INSERT INTO chat_messages (id, author_id, handle, text, created_at) SELECT printf('seed-message-%05d', n), printf('%064x', n), 'PepeAAAA', 'Message ' || n, ? + n * 3000 FROM seed`).run(base); for (const n of [2049, 2050]) { s.advance(3000); expect((await s.publish(post(n))).ok).toBe(true); } - expect(s.count("chat_messages")).toBe(50); expect(s.count("chat_receipts")).toBe(2048); + expect(s.count("chat_messages")).toBe(200); expect(s.count("chat_receipts")).toBe(2048); expect(s.db.prepare("SELECT request_id FROM chat_receipts ORDER BY created_at LIMIT 1").get()!.request_id).toBe(post(3).requestId); - expect(s.db.prepare("SELECT text FROM chat_messages ORDER BY seq LIMIT 1").get()!.text).toBe("Message 2001"); + expect(s.db.prepare("SELECT text FROM chat_messages ORDER BY seq LIMIT 1").get()!.text).toBe("Message 1851"); expect(s.db.prepare("SELECT text FROM chat_messages ORDER BY seq DESC LIMIT 1").get()!.text).toBe("Message 2050"); }); @@ -380,9 +380,9 @@ describe("shared read-only chat room", () => { expect(await s.listBans()).toHaveLength(protocol.CHAT_MAX_BANS); }); - it("retains bans through history expiry and permits moderation while chat is disabled", async () => { + it("retains bans through receipt expiry and permits moderation while chat is disabled", async () => { const s = setup(); await s.setBan(author(), true); - s.advance(protocol.CHAT_TTL_MS + 1); s.hibernate(); await s.fire(); + s.advance(RECEIPT_TTL_MS + 1); s.hibernate(); await s.fire(); expect(s.count("chat_bans")).toBe(1); s.env.CHAT_ENABLED = "false"; expect(await s.listBans()).toHaveLength(1);