Open History to see the last commit
1import { describe, expect, it } from "vite-plus/test";
2
3import {
4 COMPOSER_CONTEXT_CLIPBOARD_MIME,
5 decodeComposerContextFragment,
6 encodeComposerContextFragment,
7 encodeComposerContextClipboardHtml,
8 decodeComposerContextClipboardHtml,
9} from "./composerContextClipboard.ts";
10
11describe("composerContextClipboard", () => {
12 it("preserves rich HTML while carrying context metadata", () => {
13 const fragment = encodeComposerContextFragment({
14 version: 1,
15 source: { environmentId: "env" as never },
16 records: [],
17 })!;
18 const rich = '<p><strong>Important</strong> <a href="https://example.com">link</a></p>';
19 const html = encodeComposerContextClipboardHtml("Important link", fragment, rich);
20 expect(html).toContain(rich);
21 expect(decodeComposerContextClipboardHtml(html)).toEqual(JSON.parse(fragment));
22 });
23 it("round-trips selections larger than two million characters", () => {
24 const fragment = {
25 version: 1 as const,
26 source: { environmentId: "env" as never },
27 records: Array.from({ length: 32 }, (_, index) => ({
28 version: 1 as const,
29 contextId: `terminal-${index}` as never,
30 kind: "terminal" as const,
31 label: "Build",
32 terminalId: "build",
33 terminalLabel: "Build",
34 lineStart: 1,
35 lineEnd: 1,
36 text: "x".repeat(64_000),
37 })),
38 };
39 const encoded = encodeComposerContextFragment(fragment);
40 expect(encoded?.length).toBeGreaterThan(2_000_000);
41 expect(decodeComposerContextFragment(encoded)).toEqual(fragment);
42 expect(
43 encodeComposerContextFragment({
44 ...fragment,
45 records: Array.from({ length: 200 }, () => ({
46 ...fragment.records[0]!,
47 text: "\u0000".repeat(64_000),
48 })),
49 }),
50 ).toBeNull();
51 });
52 it("round-trips native/browser HTML without interpreting captured content as markup", () => {
53 const raw = JSON.stringify({
54 version: 1,
55 source: { environmentId: "env-1" },
56 records: [
57 {
58 version: 1,
59 kind: "terminal",
60 contextId: "terminal",
61 label: 'Build "failure"',
62 terminalId: "main",
63 terminalLabel: "Main",
64 lineStart: 1,
65 lineEnd: 1,
66 text: "<script> & café",
67 },
68 ],
69 });
70 const html = encodeComposerContextClipboardHtml("<script> & café", raw);
71 expect(html).toContain("<script> & café");
72 expect(html).not.toContain("<script>");
73 expect(decodeComposerContextClipboardHtml(html)).toEqual(JSON.parse(raw));
74 expect(
75 decodeComposerContextClipboardHtml('<pre data-t3-context-fragment="%ZZ">bad</pre>'),
76 ).toBeNull();
77 expect(decodeComposerContextClipboardHtml("<p>Ordinary clipboard</p>")).toBeNull();
78 });
79 it("round-trips context whose text contains an apostrophe", () => {
80 const raw = JSON.stringify({
81 version: 1,
82 source: { environmentId: "env-1" },
83 records: [
84 {
85 version: 1,
86 kind: "terminal",
87 contextId: "terminal",
88 label: "Build",
89 terminalId: "main",
90 terminalLabel: "Main",
91 lineStart: 1,
92 lineEnd: 1,
93 text: "error: can't find module",
94 },
95 ],
96 });
97 const html = encodeComposerContextClipboardHtml("error: can't find module", raw);
98 expect(decodeComposerContextClipboardHtml(html)).toEqual(JSON.parse(raw));
99 expect(
100 decodeComposerContextClipboardHtml(
101 `<pre data-t3-context-fragment='${encodeURIComponent(raw).replaceAll("'", "%27")}'>x</pre>`,
102 ),
103 ).toEqual(JSON.parse(raw));
104 });
105 it("accepts a large non-ASCII fragment through the HTML attribute", () => {
106 // CJK code units expand to nine encoded characters despite fitting the fragment limit.
107 const raw = JSON.stringify({
108 version: 1,
109 source: { environmentId: "env-1" },
110 records: [
111 {
112 version: 1,
113 kind: "terminal",
114 contextId: "terminal",
115 label: "ビルド",
116 terminalId: "main",
117 terminalLabel: "Main",
118 lineStart: 1,
119 lineEnd: 1,
120 text: "日本語".repeat(21_000),
121 },
122 ],
123 });
124
125 const html = encodeComposerContextClipboardHtml("output", raw);
126 expect(html.length).toBeGreaterThan(raw.length * 8);
127 expect(decodeComposerContextClipboardHtml(html)).toEqual(JSON.parse(raw));
128 });
129
130 it("round-trips a fragment and drops records it cannot decode", () => {
131 const encoded = encodeComposerContextFragment({
132 version: 1,
133 source: { environmentId: "env-1" as never, threadId: "thread-1" as never },
134 records: [
135 { version: 1, contextId: "ctx-1" as never, kind: "skill", label: "$x", name: "x" },
136 { version: 1, contextId: "ctx-2" as never, kind: "image", label: "bad" } as never,
137 ],
138 });
139 const decoded = decodeComposerContextFragment(encoded);
140 expect(decoded?.source.threadId).toBe("thread-1");
141 expect(decoded?.records.map((record) => record.contextId)).toEqual(["ctx-1"]);
142 });
143
144 it("rejects garbage, other versions, and oversized payloads", () => {
145 expect(decodeComposerContextFragment(null)).toBeNull();
146 expect(decodeComposerContextFragment("not json")).toBeNull();
147 expect(
148 decodeComposerContextFragment(
149 JSON.stringify({ version: 2, source: { environmentId: "e" }, records: [] }),
150 ),
151 ).toBeNull();
152 expect(decodeComposerContextFragment("x".repeat(2_000_001))).toBeNull();
153 expect(COMPOSER_CONTEXT_CLIPBOARD_MIME).toMatch(/^web application\//);
154 });
155});