Skip to content
Source
Sign inSign up

userMessage.test.ts

236 lines7.7 KB

1import { describe, expect, it } from "vite-plus/test";
2​
3import { ComposerContextId, GuestMessageId, ScheduledTaskId } from "@t3tools/contracts";
4import { ATTACHMENT_ONLY_BOOTSTRAP_PROMPT } from "@t3tools/shared/composerContextReferences";
5​
6import {
7 resolveUserMessageFileCards,
8 resolveUserMessageGrouping,
9 resolveUserMessagePresentation,
10} from "./userMessage.ts";
11​
12describe("resolveUserMessageGrouping", () => {
13 const own = { role: "user", text: "hello" };
14​
15 it("groups only adjacent own messages", () => {
16 expect(resolveUserMessageGrouping(own, own, own)).toEqual({
17 groupedWithPrevious: true,
18 groupedWithNext: true,
19 continuesSteer: false,
20 });
21 expect(resolveUserMessageGrouping(own, undefined, own)).toMatchObject({
22 groupedWithPrevious: false,
23 groupedWithNext: true,
24 });
25 for (const boundary of [
26 undefined,
27 { ...own, role: "assistant" },
28 { ...own, createdBy: "agent" as const },
29 { ...own, createdBy: "agent" as const, creationSource: "server" as const },
30 { ...own, scheduledTaskId: ScheduledTaskId.make("task") },
31 {
32 ...own,
33 context: {
34 guest: { guestMessageId: GuestMessageId.make("guest"), name: "Sam", text: "hi" },
35 },
36 },
37 ]) {
38 expect(resolveUserMessageGrouping(own, boundary, boundary)).toEqual({
39 groupedWithPrevious: false,
40 groupedWithNext: false,
41 continuesSteer: false,
42 });
43 expect(resolveUserMessageGrouping(boundary, own, own)).toEqual({
44 groupedWithPrevious: false,
45 groupedWithNext: false,
46 continuesSteer: false,
47 });
48 }
49 });
50​
51 it("shows one label for consecutive steers, including promoted queued messages", () => {
52 for (const inputIntent of ["steer", "promoted_queued_to_steer"] as const) {
53 const steer = { ...own, inputIntent };
54 expect(
55 resolveUserMessageGrouping(steer, { ...own, inputIntent: "steer" }, undefined)
56 .continuesSteer,
57 ).toBe(true);
58 expect(
59 resolveUserMessageGrouping(
60 steer,
61 { ...own, inputIntent: "promoted_queued_to_steer" },
62 undefined,
63 ).continuesSteer,
64 ).toBe(true);
65 expect(resolveUserMessageGrouping(steer, own, undefined).continuesSteer).toBe(false);
66 expect(resolveUserMessageGrouping(own, steer, undefined).continuesSteer).toBe(false);
67 }
68 });
69});
70​
71describe("resolveUserMessageFileCards", () => {
72 const file = {
73 type: "file" as const,
74 id: "report",
75 name: "report.pdf",
76 mimeType: "application/pdf",
77 sizeBytes: 1024,
78 };
79 const record = {
80 ...file,
81 version: 1 as const,
82 kind: "file" as const,
83 contextId: ComposerContextId.make("report"),
84 label: file.name,
85 attachmentId: file.id,
86 };
87 const context = { version: 1 as const, records: [record] };
88 const reference = "[report.pdf](t3-context://v1/file/report)";
89​
90 it("shows file cards for bare attachments and bound file references", () => {
91 for (const text of ["", " \n", reference]) {
92 expect(resolveUserMessageFileCards({ text, attachments: [file], context })).toEqual([file]);
93 }
94 expect(
95 resolveUserMessageFileCards({
96 text: `${reference}\n${reference}`,
97 attachments: [file],
98 context,
99 }),
100 ).toEqual([file]);
101 });
102​
103 it("keeps words and unavailable or non-file references in the bubble", () => {
104 for (const text of [
105 `read ${reference}`,
106 "[missing.pdf](t3-context://v1/file/missing)",
107 "[report.pdf](t3-context://v1/mention/report)",
108 ]) {
109 expect(resolveUserMessageFileCards({ text, attachments: [file], context })).toBeNull();
110 }
111 expect(resolveUserMessageFileCards({ text: reference, attachments: [file] })).toBeNull();
112 expect(
113 resolveUserMessageFileCards({
114 text: reference,
115 attachments: [file],
116 context: {
117 version: 1,
118 records: [
119 {
120 version: 1,
121 kind: "file",
122 contextId: record.contextId,
123 label: file.name,
124 payload: {},
125 },
126 ],
127 },
128 }),
129 ).toBeNull();
130 expect(
131 resolveUserMessageFileCards({
132 text: reference,
133 attachments: [{ ...file, id: "other" }],
134 context,
135 }),
136 ).toBeNull();
137 });
138​
139 it("keeps media, unknown attachments, and empty messages out of the file cards", () => {
140 const media = [
141 { ...file, type: "image" as const, name: "photo.png", mimeType: "image/png" },
142 { ...file, name: "photo.png", mimeType: "application/octet-stream" },
143 { ...file, name: "clip.mp4", mimeType: "video/mp4" },
144 { ...file, name: "clip.mov", mimeType: "application/octet-stream" },
145 { ...file, name: "voice.mp3", mimeType: "audio/mpeg" },
146 { ...file, type: "future" },
147 ];
148 expect(resolveUserMessageFileCards({ text: "", attachments: [] })).toBeNull();
149 for (const attachment of media) {
150 expect(resolveUserMessageFileCards({ text: "", attachments: [attachment] })).toBeNull();
151 expect(resolveUserMessageFileCards({ text: "", attachments: [file, attachment] })).toBeNull();
152 }
153 });
154});
155​
156describe("resolveUserMessagePresentation", () => {
157 const legacyText = "[Triggered by schedule task: Daily audit]\n\nCheck for crashes.\n";
158​
159 it("preserves user-written and assistant-quoted schedule headers", () => {
160 for (const message of [
161 { role: "user", createdBy: "user" as const },
162 { role: "user" },
163 { role: "assistant", createdBy: "agent" as const },
164 ]) {
165 expect(resolveUserMessagePresentation({ ...message, text: legacyText })).toEqual({
166 text: legacyText,
167 attribution: null,
168 scheduledTaskId: undefined,
169 });
170 }
171 });
172​
173 it("hides the prompt that stands in for an attachment-only message", () => {
174 for (const text of [
175 ATTACHMENT_ONLY_BOOTSTRAP_PROMPT,
176 `Ultrathink:\n${ATTACHMENT_ONLY_BOOTSTRAP_PROMPT}`,
177 ]) {
178 expect(resolveUserMessagePresentation({ role: "user", text })).toEqual({
179 text: "",
180 attribution: null,
181 scheduledTaskId: undefined,
182 });
183 }
184 });
185​
186 it("shows a guest's own words and name, not the attributed text the agent reads", () => {
187 expect(
188 resolveUserMessagePresentation({
189 role: "user",
190 createdBy: "user",
191 text: "Message from guest Sam, sent by the owner:\n\nWhy?",
192 context: {
193 guest: {
194 guestMessageId: GuestMessageId.make("guest-message:one"),
195 name: "Sam",
196 text: "Why?",
197 },
198 },
199 }),
200 ).toEqual({ text: "Why?", attribution: "guest", scheduledTaskId: undefined, guestName: "Sam" });
201 });
202​
203 it("attributes older server-sent restart continuations to T3 Code, not another agent", () => {
204 const text = "Note: the T3 server restarted.";
205 expect(
206 resolveUserMessagePresentation({
207 role: "user",
208 createdBy: "agent",
209 creationSource: "server",
210 text,
211 }),
212 ).toMatchObject({ attribution: "t3code" });
213 for (const creationSource of ["mcp", "provider"] as const) {
214 expect(
215 resolveUserMessagePresentation({ role: "user", createdBy: "agent", creationSource, text }),
216 ).toMatchObject({ attribution: "agent" });
217 }
218 });
219​
220 it("recovers the triggering automation from older scheduler message ids", () => {
221 for (const trigger of ["scheduled", "manual"]) {
222 expect(
223 resolveUserMessagePresentation({
224 id: `scheduled-task-message:task:daily-audit:1788661140000:${trigger}`,
225 role: "user",
226 createdBy: "user",
227 text: legacyText,
228 }),
229 ).toEqual({
230 text: "Check for crashes.\n",
231 attribution: "automation",
232 scheduledTaskId: "task:daily-audit",
233 });
234 }
235 });
236});