Files
Michael Ramos c2950e709f fix: pre-release QA findings for 0.27.9 (#1405)
Fixes from the 0.27.9 pre-release review. Servers: an unreadable rendered-HTML root falls back to the startup snapshot on both runtimes with a once-per-process warning instead of hanging (Pi) or answering 500 (Bun); the version diff is recomputed against current bytes on reload and carried through the in-app Refresh instead of being dropped, with no history write on a GET. Client: a Refresh action on the compact touch shell; HtmlSurfaceControls renders Refresh independently of the eye; the dead HtmlSurfaceActions removed. Threading: one linear, cycle-safe reply resolution shared by the annotations panel, its sort, and the export (5,000-chain tests), PATCH ingest on both runtimes rejects self-references and cycles, nothing is ever dropped from feedback. WebMCP and viewer hygiene: bounded tombstone and request memories, per-instance minted ids, nudge id caps, waiter cleanup on unmount, a shared retry epoch for diagram blocks. Docs: HTML Refresh documented, the WebMCP design pointer fixed, marketing pages updated.

AI-assisted (Claude) under maintainer direction.
2026-08-27 15:23:28 -07:00

40 lines
2.0 KiB
TypeScript

/**
* The panel's thread ordering must stay linear: 2,000 threaded comments took
* 4.5 s and 5,000 over a minute per render when the sort comparator walked
* each reply chain with a linear parent lookup. POST /api/external-annotations
* has no depth or count limit, so a buggy tool could freeze the tab.
*
* Exercises the same helpers the panel renders from (threadReplies plus the
* shared root-timestamp resolution) without a DOM.
*/
import { describe, expect, test } from 'bun:test';
import { resolveThreadRootTimestamps } from '@plannotator/core/annotation-threads';
import { AnnotationType, type Annotation } from '../types';
import { threadReplies } from './AnnotationPanel';
function comment(id: string, extra: Partial<Annotation> = {}): Annotation {
return { id, blockId: 'b', startOffset: 0, endOffset: 1, type: AnnotationType.COMMENT, text: `t${id}`, originalText: 'x', createdA: Number(id), author: 'a', ...extra };
}
describe('AnnotationPanel threading on a deep chain', () => {
test('5,000 chained replies thread and sort in well under 100 ms, in order, dropping nothing', () => {
const anns: Annotation[] = [comment('0')];
for (let i = 1; i < 5000; i++) anns.push(comment(String(i), { inReplyTo: String(i - 1) }));
const sorted = [...anns].sort((a, b) => a.createdA - b.createdA);
const start = performance.now();
const threaded = threadReplies(sorted);
const rootTs = resolveThreadRootTimestamps(sorted);
const entries = threaded.map(({ annotation, isReply }) => ({ ts: annotation.createdA, threadTs: rootTs.get(annotation.id)!, annotation, isReply }));
entries.sort((a, b) => (a.threadTs !== b.threadTs ? a.threadTs - b.threadTs : a.ts - b.ts));
const elapsed = performance.now() - start;
expect(entries.length).toBe(5000);
expect(entries[0].annotation.id).toBe('0');
expect(entries[0].isReply).toBe(false);
expect(entries[4999].annotation.id).toBe('4999');
expect(entries[4999].isReply).toBe(true);
expect(elapsed).toBeLessThan(100);
});
});