|
1 | 1 | import { describe, expect, it } from "vitest"; |
2 | | -import { computeMessageDurationStart, normalizeCompactToolLabel } from "./MessagesTimeline.logic"; |
| 2 | +import { |
| 3 | + computeMessageDurationStart, |
| 4 | + deriveMessagesTimelineRows, |
| 5 | + normalizeCompactToolLabel, |
| 6 | + resolveAssistantMessageCopyState, |
| 7 | +} from "./MessagesTimeline.logic"; |
3 | 8 |
|
4 | 9 | describe("computeMessageDurationStart", () => { |
5 | 10 | it("returns message createdAt when there is no preceding user message", () => { |
@@ -143,3 +148,120 @@ describe("normalizeCompactToolLabel", () => { |
143 | 148 | expect(normalizeCompactToolLabel("Read file completed")).toBe("Read file"); |
144 | 149 | }); |
145 | 150 | }); |
| 151 | + |
| 152 | +describe("resolveAssistantMessageCopyState", () => { |
| 153 | + it("returns enabled copy state for completed assistant messages", () => { |
| 154 | + expect( |
| 155 | + resolveAssistantMessageCopyState({ |
| 156 | + showCopyButton: true, |
| 157 | + text: "Ship it", |
| 158 | + streaming: false, |
| 159 | + }), |
| 160 | + ).toEqual({ |
| 161 | + text: "Ship it", |
| 162 | + visible: true, |
| 163 | + }); |
| 164 | + }); |
| 165 | + |
| 166 | + it("hides copy while an assistant message is still streaming", () => { |
| 167 | + expect( |
| 168 | + resolveAssistantMessageCopyState({ |
| 169 | + showCopyButton: true, |
| 170 | + text: "Still streaming", |
| 171 | + streaming: true, |
| 172 | + }), |
| 173 | + ).toEqual({ |
| 174 | + text: "Still streaming", |
| 175 | + visible: false, |
| 176 | + }); |
| 177 | + }); |
| 178 | + |
| 179 | + it("hides copy for empty completed assistant messages", () => { |
| 180 | + expect( |
| 181 | + resolveAssistantMessageCopyState({ |
| 182 | + showCopyButton: true, |
| 183 | + text: " ", |
| 184 | + streaming: false, |
| 185 | + }), |
| 186 | + ).toEqual({ |
| 187 | + text: null, |
| 188 | + visible: false, |
| 189 | + }); |
| 190 | + }); |
| 191 | + |
| 192 | + it("hides copy for non-terminal assistant messages", () => { |
| 193 | + expect( |
| 194 | + resolveAssistantMessageCopyState({ |
| 195 | + showCopyButton: false, |
| 196 | + text: "Interim thought", |
| 197 | + streaming: false, |
| 198 | + }), |
| 199 | + ).toEqual({ |
| 200 | + text: "Interim thought", |
| 201 | + visible: false, |
| 202 | + }); |
| 203 | + }); |
| 204 | +}); |
| 205 | + |
| 206 | +describe("deriveMessagesTimelineRows", () => { |
| 207 | + it("only enables assistant copy for the terminal assistant message in a turn", () => { |
| 208 | + const rows = deriveMessagesTimelineRows({ |
| 209 | + timelineEntries: [ |
| 210 | + { |
| 211 | + id: "user-1-entry", |
| 212 | + kind: "message", |
| 213 | + createdAt: "2026-01-01T00:00:00Z", |
| 214 | + message: { |
| 215 | + id: "user-1" as never, |
| 216 | + role: "user", |
| 217 | + text: "Write a poem", |
| 218 | + turnId: null, |
| 219 | + createdAt: "2026-01-01T00:00:00Z", |
| 220 | + streaming: false, |
| 221 | + }, |
| 222 | + }, |
| 223 | + { |
| 224 | + id: "assistant-thought-entry", |
| 225 | + kind: "message", |
| 226 | + createdAt: "2026-01-01T00:00:10Z", |
| 227 | + message: { |
| 228 | + id: "assistant-thought" as never, |
| 229 | + role: "assistant", |
| 230 | + text: "I should ground this first.", |
| 231 | + turnId: "turn-1" as never, |
| 232 | + createdAt: "2026-01-01T00:00:10Z", |
| 233 | + completedAt: "2026-01-01T00:00:11Z", |
| 234 | + streaming: false, |
| 235 | + }, |
| 236 | + }, |
| 237 | + { |
| 238 | + id: "assistant-final-entry", |
| 239 | + kind: "message", |
| 240 | + createdAt: "2026-01-01T00:00:20Z", |
| 241 | + message: { |
| 242 | + id: "assistant-final" as never, |
| 243 | + role: "assistant", |
| 244 | + text: "Here is the poem.", |
| 245 | + turnId: "turn-1" as never, |
| 246 | + createdAt: "2026-01-01T00:00:20Z", |
| 247 | + completedAt: "2026-01-01T00:00:30Z", |
| 248 | + streaming: false, |
| 249 | + }, |
| 250 | + }, |
| 251 | + ], |
| 252 | + completionDividerBeforeEntryId: "assistant-final-entry", |
| 253 | + isWorking: false, |
| 254 | + activeTurnStartedAt: null, |
| 255 | + }); |
| 256 | + |
| 257 | + const assistantRows = rows.filter( |
| 258 | + (row): row is Extract<(typeof rows)[number], { kind: "message" }> => |
| 259 | + row.kind === "message" && row.message.role === "assistant", |
| 260 | + ); |
| 261 | + |
| 262 | + expect(assistantRows).toHaveLength(2); |
| 263 | + expect(assistantRows[0]?.showAssistantCopyButton).toBe(false); |
| 264 | + expect(assistantRows[1]?.showAssistantCopyButton).toBe(true); |
| 265 | + expect(assistantRows[1]?.showCompletionDivider).toBe(true); |
| 266 | + }); |
| 267 | +}); |
0 commit comments