1import {
2 type ComposerContextId,
3 EnvironmentId,
4 MessageId,
5 OrchestrationMessageContext,
6 ThreadId,
7 type PreviewAnnotationPayload,
8} from "@t3tools/contracts";
9import * as Schema from "effect/Schema";
10import { upgradeLegacyContextMessage } from "@t3tools/shared/composerContextLegacy";
11
12import {
13 formatInlineContextReference,
14 removeInlineContextReference,
15} from "./composerContextReferences";
16import { describe, expect, it } from "vite-plus/test";
17
18import {
19 asKnownContextRecord,
20 attachmentContextRecord,
21 buildMessageContext,
22 composerContextImportLookupIds,
23 isPullRequestSummaryContext,
24 isSameComposerContextPayload,
25 pullRequestContextDisplayState,
26 pullRequestContextKindLabel,
27 previewAnnotationContextLabel,
28 previewAnnotationContextRecord,
29 previewAnnotationFromRecord,
30 resolveUserMessageContext,
31 selectedMessageContextFragment,
32 reviewCommentContextLabel,
33 reviewCommentContextRecord,
34 reviewCommentFromRecord,
35 terminalContextRecord,
36 terminalContextReference,
37 terminalContextDraftFromRecord,
38 uploadedAttachmentContextRecord,
39} from "./composerContextRecords";
40
41const decodeMessageContext = Schema.decodeUnknownSync(OrchestrationMessageContext);
42
43const annotation: PreviewAnnotationPayload = {
44 id: "ann_1",
45 pageUrl: "http://localhost:3000/checkout",
46 pageTitle: "Checkout",
47 comment: " Make this bigger ",
48 elements: [
49 {
50 id: "el_1",
51 rect: { x: 0, y: 0, width: 10, height: 10 },
52 element: {
53 pageUrl: "http://localhost:3000/checkout",
54 pageTitle: "Checkout",
55 tagName: "BUTTON",
56 selector: "#pay",
57 htmlPreview: '<button id="pay">Pay</button>',
58 componentName: "Button",
59 source: null,
60 stack: [],
61 styles: "color: red;",
62 pickedAt: "2026-01-01T00:00:00.000Z",
63 },
64 },
65 ],
66 regions: [],
67 strokes: [],
68 styleChanges: [
69 { targetId: "el_1", selector: "#pay", property: "font-size", previousValue: "", value: "20px" },
70 ],
71 screenshot: null,
72 createdAt: "2026-01-01T00:00:00.000Z",
73};
74
75describe("composerContextRecords", () => {
76 it("copies only ready or persisted server-side attachment IDs", () => {
77 const environmentId = EnvironmentId.make("env");
78 const image = {
79 type: "image" as const,
80 id: "local-image",
81 name: "shot.png",
82 mimeType: "image/png",
83 sizeBytes: 1,
84 file: new File(["x"], "shot.png"),
85 previewUrl: "blob:shot",
86 };
87 expect(uploadedAttachmentContextRecord(image, undefined)).toBeNull();
88 expect(
89 uploadedAttachmentContextRecord(image, { status: "uploading", environmentId, progress: 0.5 }),
90 ).toBeNull();
91 expect(
92 uploadedAttachmentContextRecord(image, {
93 status: "failed",
94 environmentId,
95 reason: "offline",
96 attachmentId: "unfinished",
97 }),
98 ).toBeNull();
99 expect(
100 uploadedAttachmentContextRecord(image, {
101 status: "ready",
102 environmentId,
103 attachmentId: "uploaded-image",
104 }),
105 ).toMatchObject({ attachmentId: "uploaded-image", contextId: "image_local-image" });
106 const file = {
107 type: "file" as const,
108 id: "local-file",
109 name: "notes.txt",
110 mimeType: "text/plain",
111 sizeBytes: 1,
112 file: null,
113 };
114 expect(uploadedAttachmentContextRecord(file, undefined)).toBeNull();
115 expect(
116 uploadedAttachmentContextRecord(
117 { ...file, uploadedAttachmentId: "persisted-file", uploadEnvironmentId: environmentId },
118 undefined,
119 ),
120 ).toMatchObject({ attachmentId: "persisted-file", contextId: "file_local-file" });
121 });
122 it("does not bind an annotation screenshot to a same-ID file", () => {
123 const context = buildMessageContext({
124 terminalContexts: [],
125 reviewComments: [],
126 previewAnnotations: [annotation],
127 attachments: [
128 {
129 attachment: {
130 type: "file",
131 id: annotation.id,
132 name: "notes.txt",
133 mimeType: "text/plain",
134 sizeBytes: 1,
135 file: null,
136 },
137 attachmentId: "uploaded-file",
138 },
139 ],
140 })!;
141 expect(context.records.map((record) => record.kind)).toEqual(["preview-annotation", "file"]);
142 expect(context.records[0]).not.toHaveProperty("screenshotContextId");
143 });
144 it.each(["x", "terminal_x"])(
145 "preserves canonical terminal IDs across repeated imports: %s",
146 (id) => {
147 const threadId = ThreadId.make("t1");
148 const record = terminalContextRecord({
149 id,
150 threadId,
151 terminalId: "default",
152 terminalLabel: "Terminal",
153 lineStart: 1,
154 lineEnd: 1,
155 text: "output",
156 createdAt: "2026-01-01T00:00:00.000Z",
157 });
158 const restored = terminalContextRecord(terminalContextDraftFromRecord(record, threadId));
159 expect(restored).toEqual(record);
160 expect(terminalContextRecord(terminalContextDraftFromRecord(restored, threadId))).toEqual(
161 record,
162 );
163 },
164 );
165
166 it.each(["x", "review-comment_x"])("preserves canonical review IDs across imports: %s", (id) => {
167 const record = reviewCommentContextRecord({
168 id,
169 sectionId: "s",
170 sectionTitle: "Review",
171 filePath: "a.ts",
172 startIndex: 0,
173 endIndex: 0,
174 rangeLabel: "L1",
175 text: "Review",
176 diff: "",
177 });
178 expect(reviewCommentContextRecord(reviewCommentFromRecord(record))).toEqual(record);
179 });
180
181 it.each(["x", "preview-annotation_x"])(
182 "preserves canonical annotation IDs across imports: %s",
183 (id) => {
184 const record = previewAnnotationContextRecord({ ...annotation, id });
185 expect(previewAnnotationContextRecord(previewAnnotationFromRecord(record)).contextId).toBe(
186 record.contextId,
187 );
188 },
189 );
190 it("builds distinct records for producer IDs that differ by a kind prefix", () => {
191 const context = buildMessageContext({
192 terminalContexts: [],
193 reviewComments: [],
194 previewAnnotations: [],
195 attachments: ["x", "image_x"].map((id) => ({
196 attachment: {
197 type: "image" as const,
198 id,
199 name: `${id}.png`,
200 mimeType: "image/png",
201 sizeBytes: 1,
202 file: new File(["x"], `${id}.png`, { type: "image/png" }),
203 previewUrl: `blob:${id}`,
204 },
205 attachmentId: `uploaded-${id}`,
206 })),
207 })!;
208 expect(decodeMessageContext(context).records.map((record) => record.contextId)).toEqual([
209 "image_x",
210 "image_image_x",
211 ]);
212 });
213 it("scopes colliding producer ids and links the annotation to its screenshot record", () => {
214 const id = "same.id:1";
215 const context = buildMessageContext({
216 terminalContexts: [
217 {
218 id,
219 threadId: ThreadId.make("t1"),
220 terminalId: "default",
221 terminalLabel: "Terminal",
222 lineStart: 1,
223 lineEnd: 1,
224 text: "output",
225 createdAt: "2026-01-01T00:00:00.000Z",
226 },
227 ],
228 reviewComments: [
229 {
230 id,
231 sectionId: "s",
232 sectionTitle: "Review",
233 filePath: "file.ts",
234 startIndex: 0,
235 endIndex: 0,
236 rangeLabel: "L1",
237 text: "Review",
238 diff: "",
239 },
240 ],
241 previewAnnotations: [{ ...annotation, id }],
242 attachments: [
243 {
244 attachment: {
245 type: "image",
246 id,
247 name: "shot.png",
248 mimeType: "image/png",
249 sizeBytes: 1,
250 file: new File(["x"], "shot.png"),
251 previewUrl: "blob:shot",
252 },
253 attachmentId: "uploaded-image",
254 },
255 {
256 attachment: {
257 type: "file",
258 id,
259 name: "file.txt",
260 mimeType: "text/plain",
261 sizeBytes: 1,
262 file: null,
263 },
264 attachmentId: "uploaded-file",
265 },
266 ],
267 })!;
268 expect(decodeMessageContext(context).records).toHaveLength(5);
269 const preview = context.records.find((record) => record.kind === "preview-annotation");
270 const image = context.records.find((record) => record.kind === "image");
271 expect(preview).toMatchObject({ screenshotContextId: image!.contextId });
272 expect(image).toMatchObject({ attachmentId: "uploaded-image" });
273 });
274
275 it("restores multiple edits on one target without parsing display strings", () => {
276 const styleChanges = [
277 ...annotation.styleChanges,
278 {
279 targetId: "el_1",
280 selector: "#pay",
281 property: "content",
282 previousValue: "a → b",
283 value: "first\nsecond → third",
284 },
285 ];
286 const restored = previewAnnotationFromRecord(
287 previewAnnotationContextRecord({ ...annotation, styleChanges }),
288 );
289 expect(restored.styleChanges).toEqual(styleChanges);
290 expect(restored.elements[0]?.id).toBe("el_1");
291 });
292 it.each([
293 ["+181", "a.ts L181"],
294 ["+181 to +183", "a.ts L181 to L183"],
295 ["-63", "a.ts L63 (before)"],
296 ["L4", "a.ts L4"],
297 ])("presents review range %s consistently as %s", (rangeLabel, expected) => {
298 expect(
299 reviewCommentContextLabel({
300 id: "review-1",
301 sectionId: "file:src/a.ts",
302 sectionTitle: "File comment",
303 filePath: "src/a.ts",
304 startIndex: 0,
305 endIndex: 0,
306 rangeLabel,
307 text: "",
308 diff: "",
309 }),
310 ).toBe(expected);
311 });
312
313 it("distinguishes a PR summary from a comment on its diff", () => {
314 const summary = {
315 id: "review-1",
316 sectionId: "pull-request:42",
317 sectionTitle: "PR #42",
318 filePath: "PR #42",
319 startIndex: 0,
320 endIndex: 0,
321 rangeLabel: "Improve context chips",
322 text: "Pull request details",
323 diff: "",
324 pullRequest: {
325 number: 42,
326 title: "Improve context chips",
327 url: "https://github.com/pingdotgg/t3code/pull/42",
328 headBranch: "feat/context-chips",
329 baseBranch: "main",
330 state: "open" as const,
331 isDraft: false,
332 },
333 };
334
335 expect(isPullRequestSummaryContext(summary)).toBe(true);
336 expect(reviewCommentContextLabel(summary)).toBe("#42");
337 expect(pullRequestContextDisplayState(summary)).toBe("open");
338 expect(pullRequestContextKindLabel(summary)).toBe("Open pull request");
339 expect(
340 pullRequestContextDisplayState({
341 ...summary,
342 pullRequest: { ...summary.pullRequest, isDraft: true },
343 }),
344 ).toBe("draft");
345 expect(
346 isPullRequestSummaryContext({
347 ...summary,
348 pullRequest: undefined,
349 filePath: "src/a.ts",
350 rangeLabel: "+12",
351 diff: "+const answer = 42;",
352 }),
353 ).toBe(false);
354 });
355
356 it("builds a preview annotation record with element details and readable style changes", () => {
357 expect(previewAnnotationContextLabel(annotation)).toBe("Make this bigger");
358 expect(previewAnnotationContextRecord(annotation, { screenshotContextId: "ann_1" })).toEqual({
359 version: 1,
360 contextId: "preview-annotation_ann_1",
361 kind: "preview-annotation",
362 label: "Make this bigger",
363 annotationId: "ann_1",
364 pageUrl: "http://localhost:3000/checkout",
365 pageTitle: "Checkout",
366 comment: "Make this bigger",
367 targetSummary: "1 selected element",
368 styleChanges: ["font-size: (unset) → 20px"],
369 styleChangeDetails: annotation.styleChanges,
370 elementIds: ["el_1"],
371 screenshotContextId: "image_ann_1",
372 elements: [
373 {
374 pageUrl: "http://localhost:3000/checkout",
375 pageTitle: "Checkout",
376 tagName: "button",
377 selector: "#pay",
378 htmlPreview: '<button id="pay">Pay</button>',
379 componentName: "Button",
380 source: null,
381 styles: "color: red;",
382 },
383 ],
384 });
385 });
386
387 it("removes an expired terminal chip by its kind-scoped id, not the producer id", () => {
388 const context = {
389 id: "term-1",
390 threadId: ThreadId.make("t"),
391 createdAt: "2026-01-01T00:00:00.000Z",
392 terminalId: "default",
393 terminalLabel: "Terminal 1",
394 lineStart: 3,
395 lineEnd: 4,
396 text: "boom",
397 };
398 const reference = terminalContextReference(context);
399 expect(reference.contextId).toBe("terminal_term-1");
400
401 const prompt = `prose ${formatInlineContextReference(reference)} tail`;
402 // The send path drops expired excerpts; the raw producer id matches nothing and would
403 // leave the chip behind.
404 expect(removeInlineContextReference(prompt, context.id).prompt).toBe(prompt);
405 expect(removeInlineContextReference(prompt, reference.contextId).prompt).toBe("prose tail");
406 });
407
408 it("clamps an oversized review selection so the record still encodes", () => {
409 const build = (diffLength: number) =>
410 reviewCommentContextRecord({
411 id: "rc-big",
412 sectionId: "file:a/b.ts",
413 sectionTitle: "File comment",
414 filePath: "a/b.ts",
415 startIndex: 0,
416 endIndex: 1,
417 rangeLabel: "L1",
418 text: "Why?",
419 diff: "d".repeat(diffLength),
420 });
421
422 // At the limit the diff is untouched; one character over it is clamped, and both encode.
423 const atLimit = build(32_000);
424 expect(atLimit.diff).toHaveLength(32_000);
425 const overLimit = build(32_001);
426 expect(overLimit.diff.length).toBeLessThanOrEqual(32_000);
427 expect(overLimit.diff.endsWith("… truncated …")).toBe(true);
428 expect(() => decodeMessageContext({ version: 1, records: [atLimit] })).not.toThrow();
429 expect(() => decodeMessageContext({ version: 1, records: [overLimit] })).not.toThrow();
430 });
431
432 it("keeps a region-only annotation's target summary and screenshot across a round trip", () => {
433 const regionOnly: PreviewAnnotationPayload = {
434 ...annotation,
435 elements: [],
436 styleChanges: [],
437 regions: [{ id: "rg_1", rect: { x: 1, y: 2, width: 3, height: 4 } }],
438 };
439 const record = previewAnnotationContextRecord(regionOnly, { screenshotContextId: "ann_1" });
440 expect(record.targetSummary).toBe("1 marked region");
441 expect(record.screenshotContextId).toBe("image_ann_1");
442
443 // Re-encoding what a paste rebuilt must not empty the summary or drop the screenshot.
444 const reencoded = previewAnnotationContextRecord(previewAnnotationFromRecord(record), {
445 screenshotContextId: "ann_1",
446 });
447 expect(reencoded.targetSummary).toBe("1 marked region");
448 expect(reencoded.screenshotContextId).toBe("image_ann_1");
449 });
450
451 it("treats colliding ids with different payloads as distinct records", () => {
452 const base = terminalContextRecord({
453 id: "term-1",
454 threadId: ThreadId.make("t"),
455 createdAt: "2026-01-01T00:00:00.000Z",
456 terminalId: "default",
457 terminalLabel: "Terminal 1",
458 lineStart: 1,
459 lineEnd: 2,
460 text: "A",
461 });
462 expect(isSameComposerContextPayload(base, { ...base })).toBe(true);
463 // Labels are display text, never identity.
464 expect(isSameComposerContextPayload(base, { ...base, label: "different" })).toBe(true);
465 expect(isSameComposerContextPayload(base, { ...base, text: "B" })).toBe(false);
466 });
467
468 it("finds the destination collision for different legacy terminal messages", () => {
469 const legacy = (text: string) =>
470 asKnownContextRecord(
471 upgradeLegacyContextMessage(
472 `Inspect this\n\n<terminal_context>\n- Terminal 1 line 1:\n 1 | ${text}\n</terminal_context>`,
473 ).records[0],
474 )!;
475 const first = legacy("A");
476 const second = legacy("B");
477 if (first.kind !== "terminal" || second.kind !== "terminal") {
478 throw new Error("Expected legacy terminal records");
479 }
480 const destinationId = terminalContextReference(
481 terminalContextDraftFromRecord(first, ThreadId.make("t")),
482 ).contextId;
483
484 expect(destinationId).toBe("terminal_legacy_terminal_1");
485 expect(composerContextImportLookupIds(second)[0]).toBe(destinationId);
486 expect(isSameComposerContextPayload(first, second)).toBe(false);
487 });
488
489 it("treats an imported legacy id and its canonical reconstruction as the same excerpt", () => {
490 const draft = {
491 id: "term-1",
492 threadId: ThreadId.make("t"),
493 createdAt: "2026-01-01T00:00:00.000Z",
494 terminalId: "default",
495 terminalLabel: "Terminal 1",
496 lineStart: 1,
497 lineEnd: 2,
498 text: "A",
499 };
500 const canonical = terminalContextRecord(draft);
501 // An import carries the id it was sent with; the draft rebuilds the folded
502 // canonical form. Same payload either way, so no duplicate entry may form.
503 const imported = { ...canonical, contextId: "legacy_terminal_1" as ComposerContextId };
504
505 expect(isSameComposerContextPayload(canonical, imported)).toBe(true);
506 expect(isSameComposerContextPayload(canonical, { ...canonical, text: "B" })).toBe(false);
507 });
508
509 it("compares nested annotation element and source payloads", () => {
510 const base = previewAnnotationContextRecord(annotation);
511 expect(isSameComposerContextPayload(base, { ...base, label: "Different display label" })).toBe(
512 true,
513 );
514 expect(
515 isSameComposerContextPayload(base, {
516 ...base,
517 elements: base.elements!.map((element) => ({
518 ...element,
519 htmlPreview: '<button id="pay">Changed</button>',
520 })),
521 }),
522 ).toBe(false);
523 expect(
524 isSameComposerContextPayload(base, {
525 ...base,
526 elements: base.elements!.map((element) => ({
527 ...element,
528 source: {
529 functionName: "Checkout",
530 fileName: "src/Checkout.tsx",
531 lineNumber: 20,
532 columnNumber: 4,
533 },
534 })),
535 }),
536 ).toBe(false);
537 });
538
539 it("builds terminal and review records and a message context in draft order", () => {
540 const terminal = terminalContextRecord({
541 id: "term-1",
542 threadId: ThreadId.make("t"),
543 createdAt: "2026-01-01T00:00:00.000Z",
544 terminalId: "default",
545 terminalLabel: "Terminal 1",
546 lineStart: 3,
547 lineEnd: 4,
548 text: "\nboom\n",
549 });
550 expect(terminal).toMatchObject({
551 kind: "terminal",
552 label: "Terminal 1 lines 3-4",
553 text: "boom",
554 });
555 const review = reviewCommentContextRecord({
556 id: "rc-1",
557 sectionId: "file:a/b.ts",
558 sectionTitle: "File comment",
559 filePath: "a/b.ts",
560 startIndex: 3,
561 endIndex: 3,
562 rangeLabel: "L4",
563 text: "Why?",
564 diff: "const x = 1;",
565 fenceLanguage: "ts",
566 });
567 expect(review).toMatchObject({ kind: "review-comment", label: "b.ts L4", fenceLanguage: "ts" });
568 const context = buildMessageContext({
569 terminalContexts: [],
570 reviewComments: [
571 {
572 id: "rc-1",
573 sectionId: "s",
574 sectionTitle: "t",
575 filePath: "a/b.ts",
576 startIndex: 0,
577 endIndex: 0,
578 rangeLabel: "L1",
579 text: "",
580 diff: "",
581 },
582 ],
583 previewAnnotations: [annotation],
584 });
585 expect(context?.records.map((record) => record.contextId)).toEqual([
586 "review-comment_rc-1",
587 "preview-annotation_ann_1",
588 ]);
589 expect(
590 buildMessageContext({ terminalContexts: [], reviewComments: [], previewAnnotations: [] }),
591 ).toBeUndefined();
592 });
593
594 it("resolves structured context directly and upgrades legacy text otherwise", () => {
595 const structured = resolveUserMessageContext({
596 text: "hi [b.ts L4](t3-context://v1/review-comment/rc-1)",
597 context: {
598 version: 1,
599 records: [
600 reviewCommentContextRecord({
601 id: "rc-1",
602 sectionId: "s",
603 sectionTitle: "t",
604 filePath: "a/b.ts",
605 startIndex: 3,
606 endIndex: 3,
607 rangeLabel: "L4",
608 text: "",
609 diff: "",
610 }),
611 ],
612 },
613 });
614 expect(structured.recordsById.get("review-comment_rc-1")?.kind).toBe("review-comment");
615 const legacy = resolveUserMessageContext({
616 text: "hi\n\n<terminal_context>\n- T line 1:\n 1 | x\n</terminal_context>",
617 });
618 expect(legacy.text).toBe("hi\n\n[T line 1](t3-context://v1/terminal/legacy_terminal_1)");
619 expect(legacy.recordsById.get("legacy_terminal_1")?.kind).toBe("terminal");
620 });
621});
622
623describe("attachment context records", () => {
624 it("binds image and file records to the given attachment id", () => {
625 const image = attachmentContextRecord({
626 attachment: {
627 type: "image",
628 id: "img-1",
629 name: "shot.png",
630 mimeType: "image/png",
631 sizeBytes: 10,
632 previewUrl: "blob:x",
633 file: new File(["x"], "shot.png", { type: "image/png" }),
634 },
635 attachmentId: "pending-abc",
636 });
637 expect(image).toEqual({
638 version: 1,
639 contextId: "image_img-1",
640 kind: "image",
641 label: "shot.png",
642 attachmentId: "pending-abc",
643 name: "shot.png",
644 mimeType: "image/png",
645 sizeBytes: 10,
646 });
647 const file = attachmentContextRecord({
648 attachment: {
649 type: "file",
650 id: "file-1",
651 name: "notes.txt",
652 mimeType: "text/plain",
653 sizeBytes: 3,
654 file: null,
655 uploadedAttachmentId: "pending-def",
656 },
657 attachmentId: "pending-def",
658 });
659 expect(file).toMatchObject({
660 kind: "file",
661 contextId: "file_file-1",
662 attachmentId: "pending-def",
663 });
664 const context = buildMessageContext({
665 terminalContexts: [],
666 reviewComments: [],
667 previewAnnotations: [],
668 attachments: [
669 {
670 attachment: {
671 type: "file",
672 id: "file-1",
673 name: "n",
674 mimeType: "text/plain",
675 sizeBytes: 1,
676 file: null,
677 },
678 attachmentId: "file-1",
679 },
680 ],
681 });
682 expect(context?.records.map((record) => record.kind)).toEqual(["file"]);
683 });
684});
685
686describe("producer ids that do not fit the grammar", () => {
687 it("folds review comment ids and keeps the raw id in the draft shape", () => {
688 const record = reviewCommentContextRecord({
689 id: "pull-request-finding:42",
690 sectionId: "s",
691 sectionTitle: "t",
692 filePath: "a/b.ts",
693 startIndex: 0,
694 endIndex: 0,
695 rangeLabel: "L1",
696 text: "",
697 diff: "",
698 });
699 expect(record.contextId).toMatch(/^review-comment_pull-request-finding-42-[0-9a-f]{16}$/);
700 expect(
701 resolveUserMessageContext({
702 text: `[b.ts L1](t3-context://v1/review-comment/${record.contextId})`,
703 context: { version: 1, records: [record] },
704 }).recordsById.has(record.contextId),
705 ).toBe(true);
706 });
707});
708
709describe("selectedMessageContextFragment", () => {
710 const terminal = terminalContextRecord({
711 id: "term-1",
712 threadId: ThreadId.make("t"),
713 createdAt: "2026-01-01T00:00:00.000Z",
714 terminalId: "default",
715 terminalLabel: "Terminal 1",
716 lineStart: 1,
717 lineEnd: 1,
718 text: "A",
719 });
720 const review = reviewCommentContextRecord({
721 id: "rc-1",
722 sectionId: "s",
723 sectionTitle: "t",
724 filePath: "a/b.ts",
725 startIndex: 3,
726 endIndex: 3,
727 rangeLabel: "L4",
728 text: "",
729 diff: "",
730 });
731 const input = {
732 records: [terminal, review],
733 environmentId: EnvironmentId.make("env"),
734 threadId: ThreadId.make("t"),
735 messageId: MessageId.make("msg-1"),
736 };
737
738 it("carries only records for chips inside the selection", () => {
739 const fragment = selectedMessageContextFragment({
740 ...input,
741 markdown: `see [b.ts L4](t3-context://v1/review-comment/${review.contextId})`,
742 });
743
744 expect(fragment).toContain(review.contextId);
745 expect(fragment).not.toContain(terminal.contextId);
746 });
747
748 it("returns null when no selected chip has backing records", () => {
749 expect(selectedMessageContextFragment({ ...input, markdown: "just prose" })).toBeNull();
750 expect(
751 selectedMessageContextFragment({
752 ...input,
753 records: [],
754 markdown: `[b.ts L4](t3-context://v1/review-comment/${review.contextId})`,
755 }),
756 ).toBeNull();
757 });
758});