|
|
@@ -0,0 +1,122 @@
|
|
|
+import { describe, expect, mock, test } from "bun:test"
|
|
|
+import type { SessionMessageInfo } from "@opencode-ai/client/promise"
|
|
|
+import { normalizeSessionMessages } from "@/utils/session-message"
|
|
|
+
|
|
|
+mock.module("@opencode-ai/session-ui/message-part", () => ({
|
|
|
+ renderable: () => true,
|
|
|
+ groupParts: (refs: Array<{ messageID: string; part: { id: string } }>) =>
|
|
|
+ refs.map((ref) => ({
|
|
|
+ type: "part" as const,
|
|
|
+ key: ref.part.id,
|
|
|
+ ref: { messageID: ref.messageID, partID: ref.part.id },
|
|
|
+ })),
|
|
|
+}))
|
|
|
+
|
|
|
+const { Timeline, TimelineRow } = await import("./rows")
|
|
|
+
|
|
|
+describe("current session timeline rows", () => {
|
|
|
+ test("derives turns and tagged rows from chronological current messages", () => {
|
|
|
+ const source = [
|
|
|
+ { id: "msg_1", type: "user", text: "first", time: { created: 1 } },
|
|
|
+ {
|
|
|
+ id: "msg_2",
|
|
|
+ type: "assistant",
|
|
|
+ agent: "build",
|
|
|
+ model: { id: "model", providerID: "provider" },
|
|
|
+ content: [{ type: "text", text: "answer" }],
|
|
|
+ time: { created: 2, completed: 3 },
|
|
|
+ },
|
|
|
+ { id: "msg_3", type: "user", text: "second", time: { created: 4 } },
|
|
|
+ {
|
|
|
+ id: "msg_4",
|
|
|
+ type: "assistant",
|
|
|
+ agent: "build",
|
|
|
+ model: { id: "model", providerID: "provider" },
|
|
|
+ content: [{ type: "reasoning", text: "working" }],
|
|
|
+ time: { created: 5 },
|
|
|
+ },
|
|
|
+ ] satisfies SessionMessageInfo[]
|
|
|
+ const normalized = normalizeSessionMessages("ses_1", source)
|
|
|
+ const messages = new Map(normalized.messages.map((message) => [message.id, message]))
|
|
|
+
|
|
|
+ const result = Timeline.constructSessionMessageRows(
|
|
|
+ source,
|
|
|
+ (messageID) => messages.get(messageID),
|
|
|
+ (messageID) => normalized.parts.get(messageID) ?? [],
|
|
|
+ true,
|
|
|
+ "busy",
|
|
|
+ true,
|
|
|
+ )
|
|
|
+
|
|
|
+ expect(result.activeMessageID).toBe("msg_3")
|
|
|
+ expect(result.rows.map(TimelineRow.key)).toEqual([
|
|
|
+ "user-message:msg_1",
|
|
|
+ "assistant-part:msg_1:msg_2:text:0",
|
|
|
+ "turn-gap:msg_3",
|
|
|
+ "user-message:msg_3",
|
|
|
+ "assistant-part:msg_3:msg_4:reasoning:0",
|
|
|
+ ])
|
|
|
+ })
|
|
|
+
|
|
|
+ test("renders a current shell message as a standalone turn", () => {
|
|
|
+ const source = [
|
|
|
+ {
|
|
|
+ id: "msg_shell",
|
|
|
+ type: "shell",
|
|
|
+ shellID: "shell_1",
|
|
|
+ command: "pwd",
|
|
|
+ status: "exited",
|
|
|
+ exit: 0,
|
|
|
+ output: { output: "/repo", cursor: 5, size: 5, truncated: false },
|
|
|
+ time: { created: 1, completed: 2 },
|
|
|
+ },
|
|
|
+ ] satisfies SessionMessageInfo[]
|
|
|
+ const normalized = normalizeSessionMessages("ses_1", source)
|
|
|
+ const messages = new Map(normalized.messages.map((message) => [message.id, message]))
|
|
|
+
|
|
|
+ const result = Timeline.constructSessionMessageRows(
|
|
|
+ source,
|
|
|
+ (messageID) => messages.get(messageID),
|
|
|
+ (messageID) => normalized.parts.get(messageID) ?? [],
|
|
|
+ true,
|
|
|
+ "idle",
|
|
|
+ true,
|
|
|
+ )
|
|
|
+
|
|
|
+ expect(result.activeMessageID).toBe("msg_shell")
|
|
|
+ expect(result.rows.map(TimelineRow.key)).toEqual([
|
|
|
+ "user-message:msg_shell",
|
|
|
+ "assistant-part:msg_shell:msg_shell:tool",
|
|
|
+ ])
|
|
|
+ })
|
|
|
+
|
|
|
+ test("associates assistants with a projected parent missing from the source page", () => {
|
|
|
+ const source = [
|
|
|
+ { id: "msg_user", type: "user", text: "question", time: { created: 1 } },
|
|
|
+ {
|
|
|
+ id: "msg_assistant",
|
|
|
+ type: "assistant",
|
|
|
+ agent: "build",
|
|
|
+ model: { id: "model", providerID: "provider" },
|
|
|
+ content: [{ type: "text", text: "answer" }],
|
|
|
+ time: { created: 2, completed: 3 },
|
|
|
+ },
|
|
|
+ ] satisfies SessionMessageInfo[]
|
|
|
+ const normalized = normalizeSessionMessages("ses_1", source)
|
|
|
+ const messages = new Map(normalized.messages.map((message) => [message.id, message]))
|
|
|
+
|
|
|
+ const result = Timeline.constructSessionMessageRows(
|
|
|
+ [source[1]!],
|
|
|
+ (messageID) => messages.get(messageID),
|
|
|
+ (messageID) => normalized.parts.get(messageID) ?? [],
|
|
|
+ true,
|
|
|
+ "idle",
|
|
|
+ true,
|
|
|
+ )
|
|
|
+
|
|
|
+ expect(result.rows.map(TimelineRow.key)).toEqual([
|
|
|
+ "user-message:msg_user",
|
|
|
+ "assistant-part:msg_user:msg_assistant:text:0",
|
|
|
+ ])
|
|
|
+ })
|
|
|
+})
|