| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441 |
- import { describe, expect, test } from "bun:test"
- import { replayLocalRows, replaySession } from "@/cli/cmd/run/session-replay"
- import type { SessionMessages } from "@/cli/cmd/run/session.shared"
- function userMessage(id: string, text: string): SessionMessages[number] {
- return {
- info: {
- id,
- sessionID: "session-1",
- role: "user",
- time: {
- created: 1,
- },
- agent: "build",
- model: {
- providerID: "openai",
- modelID: "gpt-5",
- },
- },
- parts: [
- {
- id: `${id}-text`,
- sessionID: "session-1",
- messageID: id,
- type: "text",
- text,
- },
- ],
- }
- }
- function assistantInfo(id: string) {
- return {
- id,
- sessionID: "session-1",
- role: "assistant" as const,
- time: {
- created: 2,
- },
- parentID: "msg-user-1",
- modelID: "gpt-5",
- providerID: "openai",
- mode: "chat",
- agent: "build",
- path: {
- cwd: "/tmp",
- root: "/tmp",
- },
- cost: 0,
- tokens: {
- input: 1,
- output: 1,
- reasoning: 0,
- cache: {
- read: 0,
- write: 0,
- },
- },
- }
- }
- function assistantMessage(id: string, text: string): SessionMessages[number] {
- return {
- info: assistantInfo(id),
- parts: [
- {
- id: `${id}-text`,
- sessionID: "session-1",
- messageID: id,
- type: "text",
- text,
- time: {
- start: 2,
- end: 3,
- },
- },
- ],
- }
- }
- function runningToolMessage(id: string): SessionMessages[number] {
- return {
- info: assistantInfo(id),
- parts: [
- {
- id: `${id}-tool`,
- sessionID: "session-1",
- messageID: id,
- type: "tool",
- callID: `${id}-call`,
- tool: "bash",
- state: {
- status: "running",
- input: {
- command: "pwd",
- },
- time: {
- start: 2,
- },
- },
- },
- ],
- }
- }
- describe("run session replay", () => {
- test("replays persisted user and assistant history into scrollback commits", () => {
- const out = replaySession({
- messages: [
- userMessage("msg-user-1", "Hello, whats the weather today?"),
- assistantMessage("msg-1", "What city or ZIP code should I check?"),
- ],
- permissions: [],
- questions: [],
- thinking: true,
- limits: {},
- })
- expect(out.commits).toEqual([
- expect.objectContaining({
- kind: "user",
- text: "Hello, whats the weather today?",
- phase: "start",
- source: "system",
- messageID: "msg-user-1",
- }),
- expect.objectContaining({
- kind: "assistant",
- text: "What city or ZIP code should I check?",
- phase: "progress",
- source: "assistant",
- messageID: "msg-1",
- }),
- ])
- expect(out.patch).toEqual(
- expect.objectContaining({
- phase: "idle",
- status: "",
- }),
- )
- })
- test("keeps the footer in a running state for resumed active tools", () => {
- const out = replaySession({
- messages: [runningToolMessage("msg-1")],
- permissions: [],
- questions: [],
- thinking: true,
- limits: {},
- })
- expect(out.patch).toEqual(
- expect.objectContaining({
- phase: "running",
- status: "running bash",
- }),
- )
- })
- test("merges failed local rows ahead of later persisted prompts", () => {
- const persisted = {
- kind: "user",
- text: "successful",
- phase: "start",
- source: "system",
- messageID: "msg-user-2",
- } as const
- const failed = {
- kind: "user",
- text: "failed",
- phase: "start",
- source: "system",
- messageID: "msg-user-1",
- } as const
- const error = {
- kind: "error",
- text: "network unavailable",
- phase: "start",
- source: "system",
- messageID: "msg-user-1",
- } as const
- expect(replayLocalRows([userMessage("msg-user-2", "successful")], [persisted], [{ commit: failed }, { commit: error }])).toEqual([
- failed,
- error,
- persisted,
- ])
- })
- test("retains local errors but not duplicate local prompts once a prompt persists", () => {
- const persisted = {
- kind: "user",
- text: "failed after persistence",
- phase: "start",
- source: "system",
- messageID: "msg-user-1",
- } as const
- const error = {
- kind: "error",
- text: "connection closed",
- phase: "start",
- source: "system",
- messageID: "msg-user-1",
- } as const
- expect(replayLocalRows([userMessage("msg-user-1", "failed after persistence")], [persisted], [{ commit: persisted }, { commit: error }])).toEqual([
- persisted,
- error,
- ])
- })
- test("keeps a local turn failure below assistant output already visible for that turn", () => {
- const first = {
- kind: "user",
- text: "start",
- phase: "start",
- source: "system",
- messageID: "msg-user-1",
- } as const
- const answer = {
- kind: "assistant",
- text: "partial answer",
- phase: "progress",
- source: "assistant",
- messageID: "msg-assistant-1",
- } as const
- const error = {
- kind: "error",
- text: "stream failed",
- phase: "start",
- source: "system",
- messageID: "msg-user-1",
- } as const
- const second = {
- kind: "user",
- text: "retry",
- phase: "start",
- source: "system",
- messageID: "msg-user-2",
- } as const
- expect(
- replayLocalRows(
- [userMessage("msg-user-1", "start"), userMessage("msg-user-2", "retry")],
- [first, answer, second],
- [
- {
- commit: error,
- after: { kind: "assistant", text: "partial answer", phase: "progress", messageID: "msg-assistant-1" },
- },
- ],
- ),
- ).toEqual([first, answer, error, second])
- })
- test("keeps a local failure above assistant output received after the failure", () => {
- const first = {
- kind: "user",
- text: "start",
- phase: "start",
- source: "system",
- messageID: "msg-user-1",
- } as const
- const error = {
- kind: "error",
- text: "request failed",
- phase: "start",
- source: "system",
- messageID: "msg-user-1",
- } as const
- const late = {
- kind: "assistant",
- text: "late answer",
- phase: "progress",
- source: "assistant",
- messageID: "msg-assistant-1",
- } as const
- expect(replayLocalRows([userMessage("msg-user-1", "start")], [first, late], [{ commit: error }])).toEqual([
- first,
- error,
- late,
- ])
- })
- test("inserts a local failure between persisted output chunks spanning that failure", () => {
- const first = {
- kind: "user",
- text: "start",
- phase: "start",
- source: "system",
- messageID: "msg-user-1",
- } as const
- const complete = {
- kind: "assistant",
- text: "before after",
- phase: "progress",
- source: "assistant",
- messageID: "msg-assistant-1",
- partID: "part-1",
- } as const
- const error = {
- kind: "error",
- text: "stream failed",
- phase: "start",
- source: "system",
- messageID: "msg-user-1",
- } as const
- expect(
- replayLocalRows([userMessage("msg-user-1", "start")], [first, complete], [
- {
- commit: error,
- after: {
- kind: "assistant",
- text: "before ",
- phase: "progress",
- messageID: "msg-assistant-1",
- partID: "part-1",
- visible: "before ",
- },
- },
- ]),
- ).toEqual([first, { ...complete, text: "before " }, error, { ...complete, text: "after" }])
- })
- test("places an unpersisted failed prompt before live output from that turn", () => {
- const prompt = {
- kind: "user",
- text: "start",
- phase: "start",
- source: "system",
- messageID: "msg-1",
- } as const
- const answer = {
- kind: "assistant",
- text: "partial answer",
- phase: "progress",
- source: "assistant",
- messageID: "msg-2",
- } as const
- const error = {
- kind: "error",
- text: "stream failed",
- phase: "start",
- source: "system",
- messageID: "msg-1",
- } as const
- expect(
- replayLocalRows([], [answer], [
- { commit: prompt },
- {
- commit: error,
- after: { kind: "assistant", text: "partial answer", phase: "progress", messageID: "msg-2" },
- },
- ]),
- ).toEqual([prompt, answer, error])
- })
- test("anchors a failure after the visible start of a tool that later completes", () => {
- const prompt = {
- kind: "user",
- text: "run ls",
- phase: "start",
- source: "system",
- messageID: "msg-user-1",
- } as const
- const running = {
- kind: "tool",
- text: "running bash",
- phase: "start",
- source: "tool",
- messageID: "msg-assistant-1",
- partID: "part-tool-1",
- toolState: "running",
- } as const
- const completed = {
- kind: "tool",
- text: "file.txt",
- phase: "final",
- source: "tool",
- messageID: "msg-assistant-1",
- partID: "part-tool-1",
- toolState: "completed",
- } as const
- const error = {
- kind: "error",
- text: "connection lost",
- phase: "start",
- source: "system",
- messageID: "msg-user-1",
- } as const
- expect(
- replayLocalRows([userMessage("msg-user-1", "run ls")], [prompt, running, completed], [
- {
- commit: error,
- after: {
- kind: "tool",
- text: "running bash",
- phase: "start",
- messageID: "msg-assistant-1",
- partID: "part-tool-1",
- toolState: "running",
- },
- },
- ]),
- ).toEqual([prompt, running, error, completed])
- })
- test("retains an unpersisted local diagnostic before later persisted prompts", () => {
- const first = {
- kind: "user",
- text: "before",
- phase: "start",
- source: "system",
- messageID: "msg-user-1",
- } as const
- const error = {
- kind: "error",
- text: "failed to start new session",
- phase: "start",
- source: "system",
- messageID: "msg-user-2",
- } as const
- const second = {
- kind: "user",
- text: "after",
- phase: "start",
- source: "system",
- messageID: "msg-user-3",
- } as const
- expect(
- replayLocalRows([userMessage("msg-user-1", "before"), userMessage("msg-user-3", "after")], [first, second], [
- { commit: error },
- ]),
- ).toEqual([first, error, second])
- })
- })
|