import { describe, expect, it } from "vite-plus/test";import { ASSISTANT_CITATION_CONTEXT_LENGTH, EnvironmentId, MessageId, ThreadId,} from "@t3tools/contracts";import { formatAssistantCitationHref, parseAssistantCitationHref,} from "@t3tools/shared/assistantCitations"; import { type AssistantTextSelector, captureAssistantTextSelection, createAssistantTextSelector, findAssistantCitationText,} from "./assistantTextSelection"; function selector( text: string, overrides: Partial<Omit<AssistantTextSelector, "text">> = {},): AssistantTextSelector { return { text, start: 0, end: text.replace(/\s+/g, " ").length, prefix: "", suffix: "", ...overrides, };} function roundTripSelector(selector: AssistantTextSelector) { return parseAssistantCitationHref( formatAssistantCitationHref({ version: 1, environmentId: EnvironmentId.make("environment"), threadId: ThreadId.make("thread"), messageId: MessageId.make("assistant"), ...selector, }), );} class SelectionNode { parentElement: SelectionNode | null = null; childNodes: SelectionNode[] = []; constructor( readonly tagName: string, readonly data = "", readonly attributes: Record<string, string> = {}, ) {} get nodeType() { return this.tagName === "#text" ? 3 : 1; } get length() { return this.data.length; } get firstChild(): SelectionNode | null { return this.childNodes[0] ?? null; } get lastChild(): SelectionNode | null { return this.childNodes.at(-1) ?? null; } get previousSibling(): SelectionNode | null { const siblings = this.parentElement?.childNodes; return siblings?.[siblings.indexOf(this) - 1] ?? null; } get nextSibling(): SelectionNode | null { const siblings = this.parentElement?.childNodes; return siblings?.[siblings.indexOf(this) + 1] ?? null; } append(...children: SelectionNode[]) { for (const child of children) child.parentElement = this; this.childNodes.push(...children); return this; } contains(node: SelectionNode): boolean { return node === this || this.childNodes.some((child) => child.contains(node)); } matches(selector: string) { return selector.split(", ").some((part) => { if (!part.startsWith("[")) return part === this.tagName.toLowerCase(); const [attribute, value] = part.slice(1, -1).split("="); return ( Object.hasOwn(this.attributes, attribute!) && (value === undefined || this.attributes[attribute!] === value) ); }); } closest(selector: string): SelectionNode | null { return this.matches(selector) ? this : (this.parentElement?.closest(selector) ?? null); }} function textNode(text: string) { return new SelectionNode("#text", text);} function assistantSource(...children: SelectionNode[]) { return new SelectionNode("DIV", "", { "data-assistant-citation-source": "assistant" }).append( ...children, );} function nativeSelection( start: [SelectionNode, number], end: [SelectionNode, number], backwards = false,) { let root = start[0]; while (root.parentElement !== null) root = root.parentElement; // Give every DOM boundary its own position, including element boundaries // immediately before text offset 0 and after its final character. const positions = new Map<SelectionNode, number[]>(); let position = 0; const index = (node: SelectionNode) => { const offsets = [position++]; positions.set(node, offsets); if (node.nodeType === 3) { for (let offset = 0; offset < node.length; offset++) offsets.push(position++); } else { for (const child of node.childNodes) { index(child); offsets.push(position++); } } }; index(root); const at = (node: SelectionNode, offset: number) => positions.get(node)![offset]!; class SelectionRange { constructor( public startContainer: SelectionNode, public startOffset: number, public endContainer: SelectionNode, public endOffset: number, ) {} get collapsed() { return this.startContainer === this.endContainer && this.startOffset === this.endOffset; } get commonAncestorContainer() { let ancestor = this.startContainer; while (!ancestor.contains(this.endContainer)) ancestor = ancestor.parentElement!; return ancestor; } cloneRange() { return new SelectionRange( this.startContainer, this.startOffset, this.endContainer, this.endOffset, ); } setStart(node: SelectionNode, offset: number) { this.startContainer = node; this.startOffset = offset; } setEnd(node: SelectionNode, offset: number) { this.endContainer = node; this.endOffset = offset; } intersectsNode(node: SelectionNode) { const parent = node.parentElement; if (parent === null) return true; const offset = parent.childNodes.indexOf(node); return ( at(parent, offset) < at(this.endContainer, this.endOffset) && at(parent, offset + 1) > at(this.startContainer, this.startOffset) ); } } const range = new SelectionRange(...start, ...end); const anchor = backwards ? end : start; const focus = backwards ? start : end; return { isCollapsed: range.collapsed, rangeCount: 1, anchorNode: anchor[0], anchorOffset: anchor[1], focusNode: focus[0], focusOffset: focus[1], getRangeAt: () => range, } as unknown as Selection;} function capture(viewport: SelectionNode, selection: Selection) { return captureAssistantTextSelection(viewport as unknown as HTMLElement, selection);} describe("captureAssistantTextSelection", () => { it.each([ { boundary: "next text", backwards: false }, { boundary: "next block", backwards: false }, { boundary: "parent", backwards: false }, { boundary: "next text", backwards: true }, { boundary: "next block", backwards: true }, { boundary: "parent", backwards: true }, ])( "captures a paragraph ending at $boundary, backwards=$backwards", ({ boundary, backwards }) => { const quote = textNode("A paragraph π cafe\u0301."); const source = assistantSource(new SelectionNode("P").append(quote)); const nextText = textNode("Another response."); const nextBlock = assistantSource(new SelectionNode("P").append(nextText)); const viewport = new SelectionNode("MAIN").append(source, nextBlock); const end: [SelectionNode, number] = boundary === "next text" ? [nextText, 0] : boundary === "next block" ? [nextBlock, 0] : [viewport, 1]; const selection = nativeSelection([quote, 0], end, backwards); const captured = capture(viewport, selection); expect(captured?.source).toBe(source); expect(captured?.selector).toEqual(selector(quote.data)); expect(captured?.range.endContainer).toBe(quote); expect(captured?.range.endOffset).toBe(quote.length); expect(selection.getRangeAt(0).endContainer).toBe(end[0]); expect(selection.getRangeAt(0).endOffset).toBe(end[1]); }, ); it("finds the source when both endpoints are parent boundaries", () => { const quote = textNode("Only this response."); const source = assistantSource(new SelectionNode("P").append(quote)); const viewport = new SelectionNode("MAIN").append(source, assistantSource(textNode("Next."))); expect(capture(viewport, nativeSelection([viewport, 0], [viewport, 1]))?.selector).toEqual( selector(quote.data), ); }); it("captures the final paragraph when Chromium extends to the timestamp paragraph boundary", () => { const quote = textNode("The final paragraph."); const source = assistantSource( new SelectionNode("P").append(textNode("Earlier answer.")), new SelectionNode("P").append(quote), ); const timestampText = textNode("1:50 AM"); const timestamp = new SelectionNode("P").append(timestampText); new SelectionNode("MAIN").append( new SelectionNode("DIV").append(source, new SelectionNode("DIV").append(timestamp)), ); const selection = nativeSelection([quote, 0], [timestamp, 0]); selection.toString = () => `${quote.data}\n\n\n`; expect(capture(source, selection)?.selector).toEqual( selector(quote.data, { start: "Earlier answer. ".length, end: "Earlier answer. The final paragraph.".length, prefix: "Earlier answer. ", }), ); expect(capture(source, nativeSelection([quote, 0], [timestampText, 1]))).toBeNull(); }); it("does not attribute an empty starting endpoint to the previous response", () => { const previous = textNode("Previous response."); const quote = textNode("Selected response."); const source = assistantSource(quote); const viewport = new SelectionNode("MAIN").append(assistantSource(previous), source); const captured = capture( viewport, nativeSelection([previous, previous.length], [quote, quote.length]), ); expect(captured?.source).toBe(source); expect(captured?.selector).toEqual(selector(quote.data)); }); it.each([false, true])( "rejects actual overlap with another response, backwards=%s", (backwards) => { const first = textNode("Repeated quote."); const second = textNode(first.data); const viewport = new SelectionNode("MAIN").append( assistantSource(first), assistantSource(second), ); expect(capture(viewport, nativeSelection([first, 0], [second, 1], backwards))).toBeNull(); }, ); it("accepts a control endpoint only when none of its text is selected", () => { const quote = textNode("A paragraph."); const control = textNode("Copy"); const source = assistantSource( new SelectionNode("P").append(quote), new SelectionNode("BUTTON").append(control), ); expect(capture(source, nativeSelection([quote, 0], [control, 0]))?.selector).toEqual( selector(quote.data), ); expect(capture(source, nativeSelection([quote, 0], [control, 1]))).toBeNull(); }); it.each([ new SelectionNode("BUTTON"), new SelectionNode("SPAN", "", { role: "button" }), new SelectionNode("DIV", "", { contenteditable: "true" }), new SelectionNode("SPAN", "", { hidden: "" }), new SelectionNode("SPAN", "", { "aria-hidden": "true" }), ])("rejects selected text at excluded endpoints: %j", (excluded) => { const quote = textNode("A paragraph."); const control = textNode("Excluded"); const source = assistantSource(quote, excluded.append(control)); expect(capture(source, nativeSelection([quote, 0], [control, 1]))).toBeNull(); expect(capture(source, nativeSelection([control, 0], [control, control.length]))).toBeNull(); }); it("keeps exact multiline Unicode text and omits interior controls and hidden text", () => { const first = textNode("Before π "); const code = textNode("first line\r\n second line\tπ"); const source = assistantSource( new SelectionNode("P").append(first, new SelectionNode("EM").append(textNode("cafe\u0301"))), new SelectionNode("BUTTON").append(textNode("Copy")), new SelectionNode("SPAN", "", { hidden: "" }).append(textNode("Hidden")), new SelectionNode("PRE").append(new SelectionNode("CODE").append(code)), ); expect( capture(source, nativeSelection([first, 0], [code, code.length], true))?.selector, ).toEqual(selector("Before π cafe\u0301\nfirst line\r\n second line\tπ")); }); it("rejects a source outside the supplied viewport", () => { const quote = textNode("Outside."); assistantSource(quote); expect( capture(new SelectionNode("MAIN"), nativeSelection([quote, 0], [quote, quote.length])), ).toBeNull(); });}); describe("createAssistantTextSelector", () => { it("keeps exact selected whitespace while normalizing its UTF-16 positions", () => { const text = "Before \n quote\t \n after"; expect( createAssistantTextSelector(text, "Before \n".length, "Before \n quote\t".length), ).toEqual({ text: " quote\t", start: 6, end: 13, prefix: "Before", suffix: "after", }); }); it.each([ 0, ASSISTANT_CITATION_CONTEXT_LENGTH - 2, ASSISTANT_CITATION_CONTEXT_LENGTH - 1, ASSISTANT_CITATION_CONTEXT_LENGTH, ])("keeps complete context code points with %i neighboring UTF-16 units", (paddingLength) => { const padding = "x".repeat(paddingLength); const text = `π${padding}quote${padding}π`; const start = text.indexOf("quote"); const captured = createAssistantTextSelector(text, start, start + 5); const keepEmoji = paddingLength + 2 <= ASSISTANT_CITATION_CONTEXT_LENGTH; expect(captured).toEqual({ text: "quote", start, end: start + 5, prefix: `${keepEmoji ? "π" : ""}${padding}`, suffix: `${padding}${keepEmoji ? "π" : ""}`, }); expect(roundTripSelector(captured!)).toMatchObject(captured!); }); it("preserves multiline quotes and their matching location through a citation URL", () => { const quote = "selected\r\n code π"; const prefix = "x".repeat(ASSISTANT_CITATION_CONTEXT_LENGTH - 1); const suffix = "y".repeat(ASSISTANT_CITATION_CONTEXT_LENGTH - 1); const text = `${quote} elsewhere\nπ${prefix}${quote}${suffix}π`; const rawStart = text.lastIndexOf(quote); const captured = createAssistantTextSelector(text, rawStart, rawStart + quote.length); const start = "selected code π elsewhere π".length + prefix.length; const end = start + "selected code π".length; expect(captured).toEqual({ text: quote, start, end, prefix, suffix }); const parsed = roundTripSelector(captured!); expect(parsed).toMatchObject(captured!); expect(findAssistantCitationText(text, parsed!)).toEqual({ start, end }); expect(findAssistantCitationText(`Inserted paragraph.\n${text}`, parsed!)).toEqual({ start: "Inserted paragraph. ".length + start, end: "Inserted paragraph. ".length + end, }); }); it.each(["prefix", "suffix"])( "does not relocate to a replacement-character decoy after clipping the %s", (side) => { const prefix = "x".repeat(ASSISTANT_CITATION_CONTEXT_LENGTH - (side === "prefix" ? 1 : 0)); const suffix = "y".repeat(ASSISTANT_CITATION_CONTEXT_LENGTH - (side === "suffix" ? 1 : 0)); const occurrence = (character: string) => `${side === "prefix" ? character : ""}${prefix}quote${suffix}${side === "suffix" ? character : ""}`; const text = `${occurrence("π")} / ${occurrence("\uFFFD")}`; const start = text.indexOf("quote"); const captured = createAssistantTextSelector(text, start, start + 5); const parsed = roundTripSelector(captured!); expect(parsed).toMatchObject(captured!); expect(findAssistantCitationText(text, captured!)).toBeNull(); expect(findAssistantCitationText(text, parsed!)).toBeNull(); }, ); it.each(["", " \n\t\r\n"])("rejects empty or whitespace-only selections: %j", (text) => { expect(createAssistantTextSelector(text, 0, text.length)).toBeNull(); });}); describe("findAssistantCitationText", () => { it("resolves an exact selection with its saved position and context", () => { expect( findAssistantCitationText( "Before the selected text after.", selector("selected text", { start: 11, end: 24, prefix: "Before the ", suffix: " after." }), ), ).toEqual({ start: 11, end: 24 }); }); it("finds a unique quote when insertion before it shifts its offsets", () => { expect( findAssistantCitationText( "An inserted paragraph. Before the selected text after.", selector("selected text", { start: 11, end: 24, prefix: "Before the ", suffix: " after." }), ), ).toEqual({ start: 34, end: 47 }); }); it("still finds a unique quote after its surrounding text changes", () => { expect( findAssistantCitationText( "New selected text nearby.", selector("selected text", { start: 11, end: 24, prefix: "Before the ", suffix: " after." }), ), ).toEqual({ start: 4, end: 17 }); }); it("uses both context sides when each side alone matches several repeated quotes", () => { const text = "left quote one; other quote two; left quote two"; expect( findAssistantCitationText( text, selector("quote", { start: 5, end: 10, prefix: "left ", suffix: " two" }), ), ).toEqual({ start: 38, end: 43 }); }); it("does not trust stale offsets that now point at another occurrence", () => { expect( findAssistantCitationText( "wrong quote here; right quote there", selector("quote", { start: 6, end: 11, prefix: "right ", suffix: " there" }), ), ).toEqual({ start: 24, end: 29 }); }); it.each([ { prefix: "first ", suffix: "", expected: { start: 6, end: 11 } }, { prefix: "", suffix: " last", expected: { start: 14, end: 19 } }, ])( "allows a single context side to disambiguate: $prefix / $suffix", ({ expected, ...context }) => { expect( findAssistantCitationText("first quote / quote last", selector("quote", context)), ).toEqual(expected); }, ); it("does not guess between quotes without context, even at the saved position", () => { expect(findAssistantCitationText("quote / quote", selector("quote"))).toBeNull(); }); it("does not use distance or saved offsets to break a context tie", () => { expect( findAssistantCitationText( "same quote end / same quote end", selector("quote", { start: 5, end: 10, prefix: "same ", suffix: " end" }), ), ).toBeNull(); }); it("rejects repeated quotes when neither occurrence matches all supplied context", () => { expect( findAssistantCitationText( "left quote wrong / wrong quote right", selector("quote", { prefix: "left ", suffix: " right" }), ), ).toBeNull(); }); it("counts overlapping occurrences when checking ambiguity", () => { expect(findAssistantCitationText("banana", selector("ana", { start: 1, end: 4 }))).toBeNull(); }); it("matches multiline code after indentation, tabs, and line endings change", () => {Lines 1β500 of 563. Load more
import { describe, expect, it } from "vite-plus/test"; import { ASSISTANT_CITATION_CONTEXT_LENGTH, EnvironmentId, MessageId, ThreadId, } from "@t3tools/contracts"; import { formatAssistantCitationHref, parseAssistantCitationHref, } from "@t3tools/shared/assistantCitations"; import { type AssistantTextSelector, captureAssistantTextSelection, createAssistantTextSelector, findAssistantCitationText, } from "./assistantTextSelection"; function selector( text: string, overrides: Partial<Omit<AssistantTextSelector, "text">> = {}, ): AssistantTextSelector { return { text, start: 0, end: text.replace(/\s+/g, " ").length, prefix: "", suffix: "", ...overrides, }; } function roundTripSelector(selector: AssistantTextSelector) { return parseAssistantCitationHref( formatAssistantCitationHref({ version: 1, environmentId: EnvironmentId.make("environment"), threadId: ThreadId.make("thread"), messageId: MessageId.make("assistant"), ...selector, }), ); } class SelectionNode { parentElement: SelectionNode | null = null; childNodes: SelectionNode[] = []; constructor( readonly tagName: string, readonly data = "", readonly attributes: Record<string, string> = {}, ) {} get nodeType() { return this.tagName === "#text" ? 3 : 1; } get length() { return this.data.length; } get firstChild(): SelectionNode | null { return this.childNodes[0] ?? null; } get lastChild(): SelectionNode | null { return this.childNodes.at(-1) ?? null; } get previousSibling(): SelectionNode | null { const siblings = this.parentElement?.childNodes; return siblings?.[siblings.indexOf(this) - 1] ?? null; } get nextSibling(): SelectionNode | null { const siblings = this.parentElement?.childNodes; return siblings?.[siblings.indexOf(this) + 1] ?? null; } append(...children: SelectionNode[]) { for (const child of children) child.parentElement = this; this.childNodes.push(...children); return this; } contains(node: SelectionNode): boolean { return node === this || this.childNodes.some((child) => child.contains(node)); } matches(selector: string) { return selector.split(", ").some((part) => { if (!part.startsWith("[")) return part === this.tagName.toLowerCase(); const [attribute, value] = part.slice(1, -1).split("="); return ( Object.hasOwn(this.attributes, attribute!) && (value === undefined || this.attributes[attribute!] === value) ); }); } closest(selector: string): SelectionNode | null { return this.matches(selector) ? this : (this.parentElement?.closest(selector) ?? null); } } function textNode(text: string) { return new SelectionNode("#text", text); } function assistantSource(...children: SelectionNode[]) { return new SelectionNode("DIV", "", { "data-assistant-citation-source": "assistant" }).append( ...children, ); } function nativeSelection( start: [SelectionNode, number], end: [SelectionNode, number], backwards = false, ) { let root = start[0]; while (root.parentElement !== null) root = root.parentElement; // Give every DOM boundary its own position, including element boundaries // immediately before text offset 0 and after its final character. const positions = new Map<SelectionNode, number[]>(); let position = 0; const index = (node: SelectionNode) => { const offsets = [position++]; positions.set(node, offsets); if (node.nodeType === 3) { for (let offset = 0; offset < node.length; offset++) offsets.push(position++); } else { for (const child of node.childNodes) { index(child); offsets.push(position++); } } }; index(root); const at = (node: SelectionNode, offset: number) => positions.get(node)![offset]!; class SelectionRange { constructor( public startContainer: SelectionNode, public startOffset: number, public endContainer: SelectionNode, public endOffset: number, ) {} get collapsed() { return this.startContainer === this.endContainer && this.startOffset === this.endOffset; } get commonAncestorContainer() { let ancestor = this.startContainer; while (!ancestor.contains(this.endContainer)) ancestor = ancestor.parentElement!; return ancestor; } cloneRange() { return new SelectionRange( this.startContainer, this.startOffset, this.endContainer, this.endOffset, ); } setStart(node: SelectionNode, offset: number) { this.startContainer = node; this.startOffset = offset; } setEnd(node: SelectionNode, offset: number) { this.endContainer = node; this.endOffset = offset; } intersectsNode(node: SelectionNode) { const parent = node.parentElement; if (parent === null) return true; const offset = parent.childNodes.indexOf(node); return ( at(parent, offset) < at(this.endContainer, this.endOffset) && at(parent, offset + 1) > at(this.startContainer, this.startOffset) ); } } const range = new SelectionRange(...start, ...end); const anchor = backwards ? end : start; const focus = backwards ? start : end; return { isCollapsed: range.collapsed, rangeCount: 1, anchorNode: anchor[0], anchorOffset: anchor[1], focusNode: focus[0], focusOffset: focus[1], getRangeAt: () => range, } as unknown as Selection; } function capture(viewport: SelectionNode, selection: Selection) { return captureAssistantTextSelection(viewport as unknown as HTMLElement, selection); } describe("captureAssistantTextSelection", () => { it.each([ { boundary: "next text", backwards: false }, { boundary: "next block", backwards: false }, { boundary: "parent", backwards: false }, { boundary: "next text", backwards: true }, { boundary: "next block", backwards: true }, { boundary: "parent", backwards: true }, ])( "captures a paragraph ending at $boundary, backwards=$backwards", ({ boundary, backwards }) => { const quote = textNode("A paragraph π cafe\u0301."); const source = assistantSource(new SelectionNode("P").append(quote)); const nextText = textNode("Another response."); const nextBlock = assistantSource(new SelectionNode("P").append(nextText)); const viewport = new SelectionNode("MAIN").append(source, nextBlock); const end: [SelectionNode, number] = boundary === "next text" ? [nextText, 0] : boundary === "next block" ? [nextBlock, 0] : [viewport, 1]; const selection = nativeSelection([quote, 0], end, backwards); const captured = capture(viewport, selection); expect(captured?.source).toBe(source); expect(captured?.selector).toEqual(selector(quote.data)); expect(captured?.range.endContainer).toBe(quote); expect(captured?.range.endOffset).toBe(quote.length); expect(selection.getRangeAt(0).endContainer).toBe(end[0]); expect(selection.getRangeAt(0).endOffset).toBe(end[1]); }, ); it("finds the source when both endpoints are parent boundaries", () => { const quote = textNode("Only this response."); const source = assistantSource(new SelectionNode("P").append(quote)); const viewport = new SelectionNode("MAIN").append(source, assistantSource(textNode("Next."))); expect(capture(viewport, nativeSelection([viewport, 0], [viewport, 1]))?.selector).toEqual( selector(quote.data), ); }); it("captures the final paragraph when Chromium extends to the timestamp paragraph boundary", () => { const quote = textNode("The final paragraph."); const source = assistantSource( new SelectionNode("P").append(textNode("Earlier answer.")), new SelectionNode("P").append(quote), ); const timestampText = textNode("1:50 AM"); const timestamp = new SelectionNode("P").append(timestampText); new SelectionNode("MAIN").append( new SelectionNode("DIV").append(source, new SelectionNode("DIV").append(timestamp)), ); const selection = nativeSelection([quote, 0], [timestamp, 0]); selection.toString = () => `${quote.data}\n\n\n`; expect(capture(source, selection)?.selector).toEqual( selector(quote.data, { start: "Earlier answer. ".length, end: "Earlier answer. The final paragraph.".length, prefix: "Earlier answer. ", }), ); expect(capture(source, nativeSelection([quote, 0], [timestampText, 1]))).toBeNull(); }); it("does not attribute an empty starting endpoint to the previous response", () => { const previous = textNode("Previous response."); const quote = textNode("Selected response."); const source = assistantSource(quote); const viewport = new SelectionNode("MAIN").append(assistantSource(previous), source); const captured = capture( viewport, nativeSelection([previous, previous.length], [quote, quote.length]), ); expect(captured?.source).toBe(source); expect(captured?.selector).toEqual(selector(quote.data)); }); it.each([false, true])( "rejects actual overlap with another response, backwards=%s", (backwards) => { const first = textNode("Repeated quote."); const second = textNode(first.data); const viewport = new SelectionNode("MAIN").append( assistantSource(first), assistantSource(second), ); expect(capture(viewport, nativeSelection([first, 0], [second, 1], backwards))).toBeNull(); }, ); it("accepts a control endpoint only when none of its text is selected", () => { const quote = textNode("A paragraph."); const control = textNode("Copy"); const source = assistantSource( new SelectionNode("P").append(quote), new SelectionNode("BUTTON").append(control), ); expect(capture(source, nativeSelection([quote, 0], [control, 0]))?.selector).toEqual( selector(quote.data), ); expect(capture(source, nativeSelection([quote, 0], [control, 1]))).toBeNull(); }); it.each([ new SelectionNode("BUTTON"), new SelectionNode("SPAN", "", { role: "button" }), new SelectionNode("DIV", "", { contenteditable: "true" }), new SelectionNode("SPAN", "", { hidden: "" }), new SelectionNode("SPAN", "", { "aria-hidden": "true" }), ])("rejects selected text at excluded endpoints: %j", (excluded) => { const quote = textNode("A paragraph."); const control = textNode("Excluded"); const source = assistantSource(quote, excluded.append(control)); expect(capture(source, nativeSelection([quote, 0], [control, 1]))).toBeNull(); expect(capture(source, nativeSelection([control, 0], [control, control.length]))).toBeNull(); }); it("keeps exact multiline Unicode text and omits interior controls and hidden text", () => { const first = textNode("Before π "); const code = textNode("first line\r\n second line\tπ"); const source = assistantSource( new SelectionNode("P").append(first, new SelectionNode("EM").append(textNode("cafe\u0301"))), new SelectionNode("BUTTON").append(textNode("Copy")), new SelectionNode("SPAN", "", { hidden: "" }).append(textNode("Hidden")), new SelectionNode("PRE").append(new SelectionNode("CODE").append(code)), ); expect( capture(source, nativeSelection([first, 0], [code, code.length], true))?.selector, ).toEqual(selector("Before π cafe\u0301\nfirst line\r\n second line\tπ")); }); it("rejects a source outside the supplied viewport", () => { const quote = textNode("Outside."); assistantSource(quote); expect( capture(new SelectionNode("MAIN"), nativeSelection([quote, 0], [quote, quote.length])), ).toBeNull(); }); }); describe("createAssistantTextSelector", () => { it("keeps exact selected whitespace while normalizing its UTF-16 positions", () => { const text = "Before \n quote\t \n after"; expect( createAssistantTextSelector(text, "Before \n".length, "Before \n quote\t".length), ).toEqual({ text: " quote\t", start: 6, end: 13, prefix: "Before", suffix: "after", }); }); it.each([ 0, ASSISTANT_CITATION_CONTEXT_LENGTH - 2, ASSISTANT_CITATION_CONTEXT_LENGTH - 1, ASSISTANT_CITATION_CONTEXT_LENGTH, ])("keeps complete context code points with %i neighboring UTF-16 units", (paddingLength) => { const padding = "x".repeat(paddingLength); const text = `π${padding}quote${padding}π`; const start = text.indexOf("quote"); const captured = createAssistantTextSelector(text, start, start + 5); const keepEmoji = paddingLength + 2 <= ASSISTANT_CITATION_CONTEXT_LENGTH; expect(captured).toEqual({ text: "quote", start, end: start + 5, prefix: `${keepEmoji ? "π" : ""}${padding}`, suffix: `${padding}${keepEmoji ? "π" : ""}`, }); expect(roundTripSelector(captured!)).toMatchObject(captured!); }); it("preserves multiline quotes and their matching location through a citation URL", () => { const quote = "selected\r\n code π"; const prefix = "x".repeat(ASSISTANT_CITATION_CONTEXT_LENGTH - 1); const suffix = "y".repeat(ASSISTANT_CITATION_CONTEXT_LENGTH - 1); const text = `${quote} elsewhere\nπ${prefix}${quote}${suffix}π`; const rawStart = text.lastIndexOf(quote); const captured = createAssistantTextSelector(text, rawStart, rawStart + quote.length); const start = "selected code π elsewhere π".length + prefix.length; const end = start + "selected code π".length; expect(captured).toEqual({ text: quote, start, end, prefix, suffix }); const parsed = roundTripSelector(captured!); expect(parsed).toMatchObject(captured!); expect(findAssistantCitationText(text, parsed!)).toEqual({ start, end }); expect(findAssistantCitationText(`Inserted paragraph.\n${text}`, parsed!)).toEqual({ start: "Inserted paragraph. ".length + start, end: "Inserted paragraph. ".length + end, }); }); it.each(["prefix", "suffix"])( "does not relocate to a replacement-character decoy after clipping the %s", (side) => { const prefix = "x".repeat(ASSISTANT_CITATION_CONTEXT_LENGTH - (side === "prefix" ? 1 : 0)); const suffix = "y".repeat(ASSISTANT_CITATION_CONTEXT_LENGTH - (side === "suffix" ? 1 : 0)); const occurrence = (character: string) => `${side === "prefix" ? character : ""}${prefix}quote${suffix}${side === "suffix" ? character : ""}`; const text = `${occurrence("π")} / ${occurrence("\uFFFD")}`; const start = text.indexOf("quote"); const captured = createAssistantTextSelector(text, start, start + 5); const parsed = roundTripSelector(captured!); expect(parsed).toMatchObject(captured!); expect(findAssistantCitationText(text, captured!)).toBeNull(); expect(findAssistantCitationText(text, parsed!)).toBeNull(); }, ); it.each(["", " \n\t\r\n"])("rejects empty or whitespace-only selections: %j", (text) => { expect(createAssistantTextSelector(text, 0, text.length)).toBeNull(); }); }); describe("findAssistantCitationText", () => { it("resolves an exact selection with its saved position and context", () => { expect( findAssistantCitationText( "Before the selected text after.", selector("selected text", { start: 11, end: 24, prefix: "Before the ", suffix: " after." }), ), ).toEqual({ start: 11, end: 24 }); }); it("finds a unique quote when insertion before it shifts its offsets", () => { expect( findAssistantCitationText( "An inserted paragraph. Before the selected text after.", selector("selected text", { start: 11, end: 24, prefix: "Before the ", suffix: " after." }), ), ).toEqual({ start: 34, end: 47 }); }); it("still finds a unique quote after its surrounding text changes", () => { expect( findAssistantCitationText( "New selected text nearby.", selector("selected text", { start: 11, end: 24, prefix: "Before the ", suffix: " after." }), ), ).toEqual({ start: 4, end: 17 }); }); it("uses both context sides when each side alone matches several repeated quotes", () => { const text = "left quote one; other quote two; left quote two"; expect( findAssistantCitationText( text, selector("quote", { start: 5, end: 10, prefix: "left ", suffix: " two" }), ), ).toEqual({ start: 38, end: 43 }); }); it("does not trust stale offsets that now point at another occurrence", () => { expect( findAssistantCitationText( "wrong quote here; right quote there", selector("quote", { start: 6, end: 11, prefix: "right ", suffix: " there" }), ), ).toEqual({ start: 24, end: 29 }); }); it.each([ { prefix: "first ", suffix: "", expected: { start: 6, end: 11 } }, { prefix: "", suffix: " last", expected: { start: 14, end: 19 } }, ])( "allows a single context side to disambiguate: $prefix / $suffix", ({ expected, ...context }) => { expect( findAssistantCitationText("first quote / quote last", selector("quote", context)), ).toEqual(expected); }, ); it("does not guess between quotes without context, even at the saved position", () => { expect(findAssistantCitationText("quote / quote", selector("quote"))).toBeNull(); }); it("does not use distance or saved offsets to break a context tie", () => { expect( findAssistantCitationText( "same quote end / same quote end", selector("quote", { start: 5, end: 10, prefix: "same ", suffix: " end" }), ), ).toBeNull(); }); it("rejects repeated quotes when neither occurrence matches all supplied context", () => { expect( findAssistantCitationText( "left quote wrong / wrong quote right", selector("quote", { prefix: "left ", suffix: " right" }), ), ).toBeNull(); }); it("counts overlapping occurrences when checking ambiguity", () => { expect(findAssistantCitationText("banana", selector("ana", { start: 1, end: 4 }))).toBeNull(); }); it("matches multiline code after indentation, tabs, and line endings change", () => {