bug fixes, and performance improvements, and drawer

This commit is contained in:
Zoe
2026-09-27 18:44:51 -05:00
parent d48d8163a7
commit 2b9f8d64d7
52 changed files with 4913 additions and 778 deletions
+84
View File
@@ -0,0 +1,84 @@
import { describe, expect, test } from "bun:test";
import { collectCitationSources } from "../app/utils/citations";
import { createMarkdownParser, getNodeChildren, getNodeTag } from "../app/utils/markdown";
import type { MarkdownAstNode } from "../app/utils/markdown";
import { readSearchResponse } from "../server/utils/search";
const citations = (nodes: MarkdownAstNode[]): MarkdownAstNode[] => nodes.flatMap(node =>
getNodeTag(node) === "source-cite" ? [node] : citations(getNodeChildren(node)),
);
describe("citation parsing", () => {
test("keeps citation markers as text unless explicitly enabled", async () => {
const content = "Claim [[cite:1234]] and [[cite:s7,s9]].";
for (const options of [{}, { citations: false }]) {
for (const streaming of [false, true]) {
const tree = await createMarkdownParser(options)(content, { streaming });
expect(citations(tree.nodes)).toEqual([]);
expect(getNodeChildren(tree.nodes[0] ?? "")).toEqual([content]);
}
}
});
test("renders single and grouped references as dedicated nodes", async () => {
const tree = await createMarkdownParser({ citations: true })("Claim [[cite:s7]]. Another [[cite:s7,s9]].");
expect(citations(tree.nodes)).toEqual([
["source-cite", { ids: "s7" }],
["source-cite", { ids: "s7,s9" }],
]);
});
test("does not interpret code, escaped references, or link labels", async () => {
for (const content of [
"`[[cite:s7]]`",
"```text\n[[cite:s7]]\n```",
"\\[[cite:s7]]",
"[Claim [[cite:s7]]](https://example.com)",
]) {
const tree = await createMarkdownParser({ citations: true })(content);
expect(citations(tree.nodes)).toEqual([]);
}
});
test("streaming never auto-completes a citation", async () => {
const parse = createMarkdownParser({ citations: true });
const content = "Claim [[cite:s7,s9]]";
for (let length = 1; length < content.length; length++) {
expect(citations((await parse(content.slice(0, length), { streaming: true })).nodes)).toEqual([]);
}
expect(citations((await parse(content, { streaming: true })).nodes)).toHaveLength(1);
expect(citations((await parse(content)).nodes)).toHaveLength(1);
});
});
describe("citation sources", () => {
test("IDs survive storage, including duplicate URLs and multiple searches", () => {
const first = readSearchResponse({ results: [
{ title: "One", url: "https://example.com" },
{ title: "Two", url: "https://example.com" },
] });
const second = readSearchResponse({ results: [{ title: "Three", url: "https://example.com" }] });
expect(first.ok && second.ok).toBe(true);
if (!first.ok || !second.ok) {
return;
}
const parts = [first.data, second.data].map(output => ({
toolCall: { toolName: "search", output: { type: "json", value: JSON.stringify(output) } },
}));
const sources = collectCitationSources(parts);
expect(sources.size).toBe(3);
expect(sources.get(first.data.results[0]?.sourceId ?? "")?.title).toBe("One");
expect(collectCitationSources(JSON.parse(JSON.stringify(parts)))).toEqual(sources);
expect(sources.has("invented")).toBe(false);
expect(collectCitationSources([]).size).toBe(0);
});
test("ignores legacy results, non-search tools, and unsafe URLs", () => {
const output = { type: "json", value: JSON.stringify({ results: [
{ title: "Legacy", link: "https://example.com" },
{ sourceId: "bad", title: "Bad", link: "javascript:alert(1)" },
] }) };
expect(collectCitationSources([{ toolCall: { toolName: "search", output } }]).size).toBe(0);
expect(collectCitationSources([{ toolCall: { toolName: "python", output } }]).size).toBe(0);
});
});
+25
View File
@@ -0,0 +1,25 @@
import { describe, expect, test } from 'bun:test';
import { getComposerHeight } from '../app/utils/composer-size';
describe('composer height stability', () => {
test('subpixel noise never oscillates around the two-line minimum', () => {
const heights = [47.99999, 48, 48.01].map(height => getComposerHeight(height, 24, 0, 0, true));
expect(heights).toEqual([48, 48, 48]);
});
test('grows and shrinks for actual lines, capped at ten lines', () => {
expect([24, 48, 72, 264, 72, 24].map(height => getComposerHeight(height, 24, 0, 0, true)))
.toEqual([48, 48, 72, 240, 72, 48]);
});
test('accounts for padding and borders according to box sizing', () => {
expect(getComposerHeight(80, 24, 8, 2, true)).toBe(82);
expect(getComposerHeight(80, 24, 8, 2, false)).toBe(72);
expect(getComposerHeight(10, 24, 8, 2, true)).toBe(58);
});
test('respects fractional typography rather than hardcoding a 24px line', () => {
expect(getComposerHeight(40, 20.25, 0, 0, true)).toBe(41);
expect(getComposerHeight(1000, 20.25, 0, 0, true)).toBe(203);
});
});
+52
View File
@@ -0,0 +1,52 @@
import type { MarkdownTree } from '../app/utils/markdown';
import { expect, test } from 'bun:test';
import { splitParagraphBreaks } from '../app/utils/markdown';
test('paragraph splitting preserves nested formatting without mutating a cached parser tree', () => {
const tree = { nodes: [
['p', {}, 'before', ['br', {}], ['strong', {}, 'after']],
['blockquote', {}, 'one', ['br', {}], ['em', {}, 'two']],
] } as MarkdownTree;
const original = structuredClone(tree);
const result = splitParagraphBreaks(tree);
expect(result.nodes).toEqual([
['p', {}, 'before'],
['p', {}, ['strong', {}, 'after']],
['blockquote', {}, ['p', {}, 'one'], ['p', {}, ['em', {}, 'two']]],
]);
expect(tree).toEqual(original);
expect(splitParagraphBreaks(tree)).toEqual(result);
});
test('br-free trees return by reference with no allocation', () => {
const tree = { nodes: [
['p', {}, 'plain ', ['strong', {}, 'text']],
['ul', {}, ['li', {}, 'one'], ['li', {}, 'two']],
] } as MarkdownTree;
expect(splitParagraphBreaks(tree)).toBe(tree);
});
test('single newlines still split into sibling paragraphs', () => {
const tree = { nodes: [
['p', {}, 'line one', ['br', {}], 'line two'],
] } as MarkdownTree;
expect(splitParagraphBreaks(tree).nodes).toEqual([
['p', {}, 'line one'],
['p', {}, 'line two'],
]);
});
test('nested changes rebuild only the affected path', () => {
const tree = { nodes: [
['blockquote', {}, ['p', {}, 'kept']],
['p', {}, 'split', ['br', {}], 'me'],
] } as MarkdownTree;
const result = splitParagraphBreaks(tree);
expect(result).not.toBe(tree);
expect(result.nodes[0]).toBe(tree.nodes[0]);
expect(result.nodes).toEqual([
['blockquote', {}, ['p', {}, 'kept']],
['p', {}, 'split'],
['p', {}, 'me'],
]);
});
+190
View File
@@ -0,0 +1,190 @@
import { describe, expect, test } from "bun:test";
import {
buildFocusedMessageTree,
buildMessageTree,
getActiveMessage,
getCurrentChildIndex,
getMessageCount,
getNextActiveChildId,
} from "../utils/message";
describe("message focus and versioning", () => {
test("reproduces and fixes the 3/2 focus bug after deleting root message and regenerating", () => {
// Step 1: Generate initial assistant message (A)
const msgA = {
id: "msg_a",
userId: "user_1",
topicId: "topic_1",
role: "assistant" as const,
content: "Response 1",
parentMessageId: null,
generationId: null,
activeChildId: null,
deleted: false,
createdAt: new Date(),
updatedAt: new Date(),
parts: [],
generation: null,
attachments: [],
};
let tree = buildMessageTree([msgA]);
expect(tree.length).toBe(1);
expect(getMessageCount(tree[0]!)).toBe(1);
expect(getCurrentChildIndex(tree[0]!)).toBe(0);
expect(getActiveMessage(tree[0]!)?.id).toBe("msg_a");
// Step 2: Regenerate the message -> creates child B, activeChildId points to B
const msgB = {
...msgA,
id: "msg_b",
content: "Response 2",
parentMessageId: "msg_a",
};
msgA.activeChildId = "msg_b";
tree = buildMessageTree([msgA, msgB]);
expect(tree.length).toBe(1);
expect(tree[0]!.children.length).toBe(1);
expect(getMessageCount(tree[0]!)).toBe(2);
// Variant 2 of 2: index should be 1 (displayed as 1 + 1 = 2)
expect(getCurrentChildIndex(tree[0]!)).toBe(1);
expect(getActiveMessage(tree[0]!)?.id).toBe("msg_b");
// Step 3: Switch focus to first, root message (A)
const prevId = getNextActiveChildId(tree[0]!, -1);
expect(prevId).toBeNull();
msgA.activeChildId = null;
tree = buildMessageTree([msgA, msgB]);
expect(getMessageCount(tree[0]!)).toBe(2);
// Variant 1 of 2: index should be 0 (displayed as 0 + 1 = 1)
expect(getCurrentChildIndex(tree[0]!)).toBe(0);
expect(getActiveMessage(tree[0]!)?.id).toBe("msg_a");
// Step 4: Delete the root message (A)
// Root is soft-deleted, active child switches to B
msgA.deleted = true;
msgA.activeChildId = "msg_b";
tree = buildMessageTree([msgA, msgB]);
expect(tree.length).toBe(1);
expect(tree[0]!.deleted).toBe(true);
expect(getMessageCount(tree[0]!)).toBe(1);
expect(getCurrentChildIndex(tree[0]!)).toBe(0);
expect(getActiveMessage(tree[0]!)?.id).toBe("msg_b");
// Step 5: Regenerate the message -> creates child C with parentMessageId: msg_a
const msgC = {
...msgA,
id: "msg_c",
content: "Response 3",
parentMessageId: "msg_a",
deleted: false,
};
msgA.activeChildId = "msg_c";
tree = buildMessageTree([msgA, msgB, msgC]);
expect(tree.length).toBe(1);
expect(tree[0]!.children.length).toBe(2);
expect(getMessageCount(tree[0]!)).toBe(2);
// Crucial test: When C (second child) is active, index must be 1 (displayed as 2/2, NOT 3/2!)
expect(getCurrentChildIndex(tree[0]!)).toBe(1);
expect(getActiveMessage(tree[0]!)?.id).toBe("msg_c");
// Verify switching focus back to B works seamlessly
const backToB = getNextActiveChildId(tree[0]!, -1);
expect(backToB).toBe("msg_b");
msgA.activeChildId = backToB;
tree = buildMessageTree([msgA, msgB, msgC]);
expect(getCurrentChildIndex(tree[0]!)).toBe(0);
expect(getActiveMessage(tree[0]!)?.id).toBe("msg_b");
// Verify switching forward to C works seamlessly
const forwardToC = getNextActiveChildId(tree[0]!, 1);
expect(forwardToC).toBe("msg_c");
msgA.activeChildId = forwardToC;
tree = buildMessageTree([msgA, msgB, msgC]);
expect(getCurrentChildIndex(tree[0]!)).toBe(1);
expect(getActiveMessage(tree[0]!)?.id).toBe("msg_c");
});
test("buildFocusedMessageTree excludes soft-deleted root messages", () => {
const root = {
id: "root",
userId: "user_1",
topicId: "topic_1",
role: "assistant" as const,
content: "Root",
parentMessageId: null,
generationId: null,
activeChildId: "child_1",
deleted: true,
createdAt: new Date(),
updatedAt: new Date(),
parts: [],
generation: null,
attachments: [],
};
const child1 = {
...root,
id: "child_1",
content: "Child 1",
parentMessageId: "root",
activeChildId: null,
deleted: false,
};
const tree = buildMessageTree([root, child1]);
const focused = buildFocusedMessageTree(tree);
expect(focused.length).toBe(1);
expect(focused[0]!.id).toBe("child_1");
expect(focused[0]!.deleted).toBe(false);
});
test("buildMessageTree filters out deleted roots with no children", () => {
const deletedEmptyRoot = {
id: "deleted_root",
userId: "user_1",
topicId: "topic_1",
role: "assistant" as const,
content: "Empty",
parentMessageId: null,
generationId: null,
activeChildId: null,
deleted: true,
createdAt: new Date(),
updatedAt: new Date(),
parts: [],
generation: null,
attachments: [],
};
const tree = buildMessageTree([deletedEmptyRoot]);
expect(tree.length).toBe(0);
});
test("getNextActiveChildId clamps to bounds and handles null fallback", () => {
const root = {
id: "root",
deleted: true,
activeChildId: null as string | null,
children: [
{ id: "child_a" },
{ id: "child_b" },
],
};
// If activeChildId is null and root is deleted, going left stays at child_a
expect(getNextActiveChildId(root, -1)).toBe("child_a");
// Going right moves to child_b
expect(getNextActiveChildId(root, 1)).toBe("child_b");
// When at child_b, going right clamps to child_b
root.activeChildId = "child_b";
expect(getNextActiveChildId(root, 1)).toBe("child_b");
});
});
+42
View File
@@ -0,0 +1,42 @@
import { describe, expect, test } from "bun:test";
import { addPendingRename, cancelPendingRename, completeRename } from "../server/utils/renames";
import { provisionalTopicTitle } from "../utils/topic-title";
describe("rename attempt ownership", () => {
test("an old completion cannot remove a newer attempt", () => {
addPendingRename("topic", "old");
const current = addPendingRename("topic", "new");
completeRename("topic", "old");
cancelPendingRename("topic", "new");
expect(current.signal.aborted).toBe(true);
});
test("an old cancellation cannot abort a newer attempt", () => {
const current = addPendingRename("topic", "new");
cancelPendingRename("topic", "old");
expect(current.signal.aborted).toBe(false);
completeRename("topic", "new");
});
test("cancellation is safe after completion or restart", () => {
cancelPendingRename("missing", "old");
const current = addPendingRename("topic", "new");
completeRename("topic", "new");
cancelPendingRename("topic", "new");
expect(current.signal.aborted).toBe(false);
});
});
describe("provisional titles", () => {
test("uses the first meaningful line and normalizes whitespace", () => {
expect(provisionalTopicTitle({ content: "\n Help me debug this\nMore details" })).toBe("Help me debug this");
});
test("names attachment-only conversations", () => {
expect(provisionalTopicTitle({ content: "", files: [{ name: "photo.jpg" }] })).toBe("photo.jpg");
expect(provisionalTopicTitle({ content: "", fileIds: ["image"] })).toBe("Attached files");
});
test("bounds title length and handles empty input", () => {
expect(provisionalTopicTitle({ content: "a".repeat(200) })).toHaveLength(100);
expect(provisionalTopicTitle()).toBe("New Topic");
});
});
+131
View File
@@ -0,0 +1,131 @@
import { describe, expect, test } from 'bun:test';
import { createHash } from 'node:crypto';
import { topicEvents } from '../server/utils/events';
import { createTopicSnapshotStream } from '../server/utils/topic-snapshot-stream';
import { appendTextDelta, serializeTopicSnapshot } from '../utils/topic-sync';
const snapshot = () => ({
id: 'topic',
name: 'Example',
messages: [{
id: 'message',
activeChildId: null as string | null,
updatedAt: new Date('2026-09-27T00:00:00Z'),
generation: { status: 'pending', tokens: null as unknown },
parts: [{ id: 'part', type: 'text', content: 'hello', finished: false }],
}],
});
const fingerprint = (value: unknown) => createHash('sha256').update(serializeTopicSnapshot(value)).digest('hex');
const readEvent = async (reader: ReadableStreamDefaultReader<string>) => {
const { value } = await reader.read();
return JSON.parse(value!.slice(6));
};
describe('topic snapshot synchronization', () => {
test('matches database Dates and hydrated JSON regardless of object key order', () => {
const topic = snapshot();
const hydrated = JSON.parse(JSON.stringify(topic));
expect(fingerprint({ messages: hydrated.messages, name: hydrated.name, id: hydrated.id })).toBe(fingerprint(topic));
});
test('detects older-message, part, generation, variant, and topic changes without relying on count', () => {
const original = fingerprint(snapshot());
for (const change of [
(topic: ReturnType<typeof snapshot>) => topic.name = 'Renamed',
(topic: ReturnType<typeof snapshot>) => topic.messages[0]!.activeChildId = 'variant',
(topic: ReturnType<typeof snapshot>) => topic.messages[0]!.parts[0]!.content = 'edited',
(topic: ReturnType<typeof snapshot>) => topic.messages[0]!.parts[0]!.finished = true,
(topic: ReturnType<typeof snapshot>) => topic.messages[0]!.generation.tokens = { output: 123 },
]) {
const topic = snapshot();
change(topic);
expect(fingerprint(topic)).not.toBe(original);
}
});
test('matching state receives a small ready event, then live changes', async () => {
const topic = snapshot();
const subscription = createTopicSnapshotStream('match', fingerprint(topic));
const result = await subscription.open(async () => topic);
expect(result.ok).toBe(true);
if (!result.ok) return;
const reader = result.data.getReader();
try {
expect(await readEvent(reader)).toEqual({ type: 'synced', payload: { id: 'match' } });
await topicEvents.emit('match', { type: 'topic_updated', payload: { name: 'New' } });
expect((await readEvent(reader)).payload).toEqual({ name: 'New' });
} finally {
await reader.cancel();
}
});
test('stale or missing fingerprints receive a full replacement snapshot', async () => {
for (const offered of [undefined, fingerprint(snapshot())]) {
const topic = snapshot();
topic.messages[0]!.parts[0]!.content = 'changed';
const subscription = createTopicSnapshotStream('stale', offered);
const result = await subscription.open(async () => topic);
expect(result.ok).toBe(true);
if (!result.ok) continue;
const reader = result.data.getReader();
try {
const event = await readEvent(reader);
expect(event.type).toBe('initial_state');
expect(event.payload.messages[0].parts[0].content).toBe('changed');
} finally {
await reader.cancel();
}
}
});
test('captures events during the DB query and includes text not yet persisted', async () => {
const id = 'query-race';
const initial = snapshot();
await topicEvents.emit(id, {
type: 'text-start', payload: { messageId: 'message', part: initial.messages[0]!.parts[0] },
});
// This delta predates subscription and has not reached the database yet.
await topicEvents.emit(id, {
type: 'text-delta', payload: { messageId: 'message', partId: 'part', content: ' world', offset: 5 },
});
const pending = Promise.withResolvers<ReturnType<typeof snapshot>>();
const subscription = createTopicSnapshotStream(id, fingerprint(initial));
const opening = subscription.open(() => pending.promise);
await topicEvents.emit(id, {
type: 'text-delta', payload: { messageId: 'message', partId: 'part', content: '!', offset: 11 },
});
pending.resolve(snapshot());
const result = await opening;
expect(result.ok).toBe(true);
if (!result.ok) return;
const reader = result.data.getReader();
try {
const state = await readEvent(reader);
expect(state.type).toBe('initial_state');
const text = state.payload.messages[0].parts[0].content;
expect(text).toBe('hello world!');
const replay = await readEvent(reader);
expect(replay.replay).toBe(true);
expect(appendTextDelta(text, replay.payload.content, replay.payload.offset)).toBe(text);
} finally {
await reader.cancel();
topicEvents.clearLiveText(id, 'message');
}
});
test('cancellation during snapshot loading does not establish a stream', async () => {
const pending = Promise.withResolvers<ReturnType<typeof snapshot>>();
const subscription = createTopicSnapshotStream('cancelled', undefined);
const opening = subscription.open(() => pending.promise);
subscription.close();
pending.resolve(snapshot());
expect((await opening).ok).toBe(false);
});
test('text offsets handle partial overlaps, duplicates, and gaps', () => {
expect(appendTextDelta('hello w', ' world', 5)).toBe('hello world');
expect(appendTextDelta('hello world', ' world', 5)).toBe('hello world');
expect(appendTextDelta('hello', '!', 5)).toBe('hello!');
expect(appendTextDelta('hello', '!', 10)).toBe('hello');
});
});