浏览代码

fix(cli): update mini tests for v2 types (#36779)

Aiden Cline 1 月之前
父节点
当前提交
6963f2f6da

+ 13 - 13
packages/opencode/test/cli/run/permission.shared.test.ts

@@ -1,5 +1,5 @@
 import { describe, expect, test } from "bun:test"
-import type { PermissionRequest } from "@opencode-ai/sdk/v2"
+import type { PermissionV2Request } from "@opencode-ai/client/promise"
 import {
   createPermissionBodyState,
   permissionAlwaysLines,
@@ -10,14 +10,14 @@ import {
   permissionRun,
 } from "@opencode-ai/cli/mini/permission.shared"
 
-function req(input: Partial<PermissionRequest> = {}): PermissionRequest {
+function req(input: Partial<PermissionV2Request> = {}): PermissionV2Request {
   return {
     id: "perm-1",
     sessionID: "session-1",
-    permission: "read",
-    patterns: [],
+    action: "read",
+    resources: [],
     metadata: {},
-    always: [],
+    save: [],
     ...input,
   }
 }
@@ -80,7 +80,7 @@ describe("run permission shared", () => {
     expect(
       permissionInfo(
         req({
-          permission: "bash",
+          action: "bash",
           metadata: {
             input: {
               command: "git status --short",
@@ -96,7 +96,7 @@ describe("run permission shared", () => {
     expect(
       permissionInfo(
         req({
-          permission: "task",
+          action: "task",
           metadata: {
             description: "investigate stream",
             subagent_type: "general",
@@ -111,8 +111,8 @@ describe("run permission shared", () => {
     expect(
       permissionInfo(
         req({
-          permission: "external_directory",
-          patterns: ["/tmp/work/**/*.ts", "/tmp/work/**/*.tsx"],
+          action: "external_directory",
+          resources: ["/tmp/work/**/*.ts", "/tmp/work/**/*.tsx"],
         }),
       ),
     ).toMatchObject({
@@ -120,22 +120,22 @@ describe("run permission shared", () => {
       lines: ["- /tmp/work/**/*.ts", "- /tmp/work/**/*.tsx"],
     })
 
-    expect(permissionInfo(req({ permission: "doom_loop" }))).toMatchObject({
+    expect(permissionInfo(req({ action: "doom_loop" }))).toMatchObject({
       title: "Continue after repeated failures",
     })
 
-    expect(permissionInfo(req({ permission: "custom_tool" }))).toMatchObject({
+    expect(permissionInfo(req({ action: "custom_tool" }))).toMatchObject({
       title: "Call tool custom_tool",
       lines: ["Tool: custom_tool"],
     })
   })
 
   test("formats always-allow copy for wildcard and explicit patterns", () => {
-    expect(permissionAlwaysLines(req({ permission: "bash", always: ["*"] }))).toEqual([
+    expect(permissionAlwaysLines(req({ action: "bash", save: ["*"] }))).toEqual([
       "This will allow bash until OpenCode is restarted.",
     ])
 
-    expect(permissionAlwaysLines(req({ always: ["src/**/*.ts", "src/**/*.tsx"] }))).toEqual([
+    expect(permissionAlwaysLines(req({ save: ["src/**/*.ts", "src/**/*.tsx"] }))).toEqual([
       "This will allow the following patterns until OpenCode is restarted.",
       "- src/**/*.ts",
       "- src/**/*.tsx",

+ 0 - 556
packages/opencode/test/cli/run/session-data.test.ts

@@ -1,556 +0,0 @@
-import { describe, expect, test } from "bun:test"
-import type { Event } from "@opencode-ai/sdk/v2"
-import { createSessionData, reduceSessionData } from "@opencode-ai/cli/mini/session-data"
-import type { StreamCommit } from "@opencode-ai/cli/mini/types"
-
-function reduce(data: ReturnType<typeof createSessionData>, event: unknown, thinking = true) {
-  return reduceSessionData({
-    data,
-    event: event as Event,
-    sessionID: "session-1",
-    thinking,
-    limits: {},
-  })
-}
-
-function assistant(id: string, extra: Record<string, unknown> = {}) {
-  return {
-    type: "message.updated",
-    properties: {
-      sessionID: "session-1",
-      info: {
-        id,
-        role: "assistant",
-        providerID: "openai",
-        modelID: "gpt-5",
-        tokens: {
-          input: 1,
-          output: 1,
-          reasoning: 0,
-          cache: { read: 0, write: 0 },
-        },
-        ...extra,
-      },
-    },
-  }
-}
-
-function user(id: string) {
-  return {
-    type: "message.updated",
-    properties: {
-      sessionID: "session-1",
-      info: {
-        id,
-        role: "user",
-      },
-    },
-  }
-}
-
-function text(input: { id: string; messageID: string; text: string; time?: Record<string, number> }) {
-  return {
-    type: "message.part.updated",
-    properties: {
-      part: {
-        id: input.id,
-        messageID: input.messageID,
-        sessionID: "session-1",
-        type: "text",
-        text: input.text,
-        ...(input.time ? { time: input.time } : {}),
-      },
-    },
-  }
-}
-
-function reasoning(input: { id: string; messageID: string; text: string; time?: Record<string, number> }) {
-  return {
-    type: "message.part.updated",
-    properties: {
-      part: {
-        id: input.id,
-        messageID: input.messageID,
-        sessionID: "session-1",
-        type: "reasoning",
-        text: input.text,
-        ...(input.time ? { time: input.time } : {}),
-      },
-    },
-  }
-}
-
-function delta(messageID: string, partID: string, value: string) {
-  return {
-    type: "message.part.delta",
-    properties: {
-      sessionID: "session-1",
-      messageID,
-      partID,
-      field: "text",
-      delta: value,
-    },
-  }
-}
-
-function tool(input: { id: string; messageID: string; tool: string; state: Record<string, unknown>; callID?: string }) {
-  return {
-    type: "message.part.updated",
-    properties: {
-      part: {
-        id: input.id,
-        messageID: input.messageID,
-        sessionID: "session-1",
-        type: "tool",
-        tool: input.tool,
-        ...(input.callID ? { callID: input.callID } : {}),
-        state: input.state,
-      },
-    },
-  }
-}
-
-function shellInfo(id: string, status: "running" | "exited", completed?: number) {
-  return {
-    id,
-    status,
-    command: "pwd",
-    cwd: "/tmp/demo",
-    shell: "/bin/sh",
-    file: `/tmp/${id}.log`,
-    ...(status === "exited" ? { exit: 0 } : {}),
-    metadata: {},
-    time: { started: 1, ...(completed === undefined ? {} : { completed }) },
-  }
-}
-
-function shellStarted(id = "call-1") {
-  return {
-    type: "session.shell.started",
-    properties: { sessionID: "session-1", shell: shellInfo(id, "running") },
-  }
-}
-
-function shellEnded(id = "call-1") {
-  const output = "/tmp/demo\n"
-  return {
-    type: "session.shell.ended",
-    properties: {
-      sessionID: "session-1",
-      shell: shellInfo(id, "exited", 2),
-      output: { output, cursor: Buffer.byteLength(output), size: Buffer.byteLength(output), truncated: false },
-    },
-  }
-}
-
-describe("run session data", () => {
-  test("buffers delayed assistant text until the role is known", () => {
-    let data = createSessionData()
-    data = reduce(data, delta("msg-1", "txt-1", "hello")).data
-    data = reduce(data, assistant("msg-1")).data
-
-    const out = reduce(
-      data,
-      text({
-        id: "txt-1",
-        messageID: "msg-1",
-        text: "",
-        time: { end: 1 },
-      }),
-    )
-
-    expect(out.commits).toEqual([
-      expect.objectContaining({
-        kind: "assistant",
-        text: "hello",
-        partID: "txt-1",
-      }),
-    ])
-  })
-
-  test("keeps leading whitespace buffered until real assistant content arrives", () => {
-    let data = createSessionData()
-    data = reduce(data, assistant("msg-1")).data
-    data = reduce(data, text({ id: "txt-1", messageID: "msg-1", text: "", time: { start: 1 } })).data
-
-    let out = reduce(data, delta("msg-1", "txt-1", " "))
-    expect(out.commits).toEqual([])
-
-    out = reduce(out.data, delta("msg-1", "txt-1", "Found"))
-    expect(out.commits).toEqual([
-      expect.objectContaining({
-        kind: "assistant",
-        text: " Found",
-      }),
-    ])
-  })
-
-  test("drops delayed text once the message resolves to a user role", () => {
-    let data = createSessionData()
-    data = reduce(data, text({ id: "txt-user-1", messageID: "msg-user-1", text: "HELLO", time: { end: 1 } })).data
-
-    const out = reduce(data, user("msg-user-1"))
-
-    expect(out.commits).toEqual([])
-    expect(out.data.ids.has("txt-user-1")).toBe(true)
-  })
-
-  test("suppresses reasoning commits when thinking is disabled", () => {
-    const out = reduce(
-      createSessionData(),
-      reasoning({
-        id: "reason-1",
-        messageID: "msg-1",
-        text: "hidden",
-        time: { end: 1 },
-      }),
-      false,
-    )
-
-    expect(out.commits).toEqual([])
-    expect(out.data.ids.has("reason-1")).toBe(true)
-  })
-
-  test("keeps permission precedence over queued questions", () => {
-    let data = createSessionData()
-    data = reduce(data, {
-      type: "permission.asked",
-      properties: {
-        id: "perm-1",
-        sessionID: "session-1",
-        permission: "read",
-        patterns: ["/tmp/file.txt"],
-        metadata: {},
-        always: [],
-      },
-    }).data
-
-    const ask = reduce(data, {
-      type: "question.asked",
-      properties: {
-        id: "question-1",
-        sessionID: "session-1",
-        questions: [
-          {
-            question: "Mode?",
-            header: "Mode",
-            options: [{ label: "chunked", description: "Incremental output" }],
-            multiple: false,
-          },
-        ],
-      },
-    })
-
-    expect(ask.footer).toEqual({
-      patch: { status: "awaiting permission" },
-      view: {
-        type: "permission",
-        request: expect.objectContaining({ id: "perm-1" }),
-      },
-    })
-
-    expect(
-      reduce(ask.data, {
-        type: "permission.replied",
-        properties: {
-          sessionID: "session-1",
-          requestID: "perm-1",
-          reply: "reject",
-        },
-      }).footer,
-    ).toEqual({
-      patch: { status: "awaiting answer" },
-      view: {
-        type: "question",
-        request: expect.objectContaining({ id: "question-1" }),
-      },
-    })
-  })
-
-  test("refreshes the active permission view when tool input arrives later", () => {
-    const data = reduce(createSessionData(), {
-      type: "permission.asked",
-      properties: {
-        id: "perm-1",
-        sessionID: "session-1",
-        permission: "bash",
-        patterns: ["src/**/*.ts"],
-        metadata: {},
-        always: [],
-        tool: {
-          messageID: "msg-1",
-          callID: "call-1",
-        },
-      },
-    }).data
-
-    const out = reduce(
-      data,
-      tool({
-        id: "tool-1",
-        messageID: "msg-1",
-        callID: "call-1",
-        tool: "bash",
-        state: {
-          status: "running",
-          input: {
-            command: "git status --short",
-          },
-        },
-      }),
-    )
-
-    expect(out.footer).toEqual({
-      view: {
-        type: "permission",
-        request: expect.objectContaining({
-          id: "perm-1",
-          metadata: expect.objectContaining({
-            input: {
-              command: "git status --short",
-            },
-          }),
-        }),
-      },
-    })
-  })
-
-  test("strips bash echo only from the first assistant flush", () => {
-    let data = createSessionData()
-    data = reduce(data, assistant("msg-1")).data
-    data = reduce(
-      data,
-      tool({
-        id: "tool-1",
-        messageID: "msg-1",
-        tool: "bash",
-        state: {
-          status: "completed",
-          input: {
-            command: "printf hi",
-          },
-          output: "echoed\n",
-          time: { start: 1, end: 2 },
-        },
-      }),
-    ).data
-
-    const first = reduce(
-      data,
-      text({
-        id: "txt-1",
-        messageID: "msg-1",
-        text: "echoed\nanswer",
-      }),
-    )
-
-    expect(first.commits).toEqual([
-      expect.objectContaining({
-        kind: "assistant",
-        text: "answer",
-      }),
-    ])
-
-    expect(reduce(first.data, delta("msg-1", "txt-1", "\nechoed\nagain")).commits).toEqual([
-      expect.objectContaining({
-        kind: "assistant",
-        text: "\nechoed\nagain",
-      }),
-    ])
-  })
-
-  test("renders direct shell mode from first-class shell events", () => {
-    let data = createSessionData()
-    const started = reduce(data, shellStarted())
-
-    expect(started.commits).toEqual([
-      expect.objectContaining({
-        kind: "tool",
-        phase: "start",
-        partID: "shell:call-1",
-        tool: "bash",
-        shell: {
-          callID: "call-1",
-          command: "pwd",
-        },
-      }),
-    ])
-
-    data = started.data
-    const ended = reduce(data, shellEnded())
-
-    expect(ended.commits).toEqual([
-      expect.objectContaining({
-        kind: "tool",
-        phase: "progress",
-        partID: "shell:call-1",
-        tool: "bash",
-        text: "/tmp/demo\n",
-        toolState: "completed",
-        shell: {
-          callID: "call-1",
-          command: "pwd",
-        },
-      }),
-    ])
-  })
-
-  test("suppresses legacy bash part updates once shell events claim the call", () => {
-    let data = reduce(createSessionData(), shellStarted()).data
-
-    expect(
-      reduce(
-        data,
-        tool({
-          id: "tool-1",
-          messageID: "msg-1",
-          callID: "call-1",
-          tool: "bash",
-          state: {
-            status: "running",
-            input: {
-              command: "pwd",
-            },
-            time: { start: 1 },
-          },
-        }),
-      ).commits,
-    ).toEqual([])
-
-    data = reduce(data, shellEnded()).data
-
-    expect(
-      reduce(
-        data,
-        tool({
-          id: "tool-1",
-          messageID: "msg-1",
-          callID: "call-1",
-          tool: "bash",
-          state: {
-            status: "completed",
-            input: {
-              command: "pwd",
-            },
-            output: "/tmp/demo\n",
-            title: "",
-            metadata: {
-              output: "/tmp/demo\n",
-            },
-            time: { start: 1, end: 2 },
-          },
-        }),
-      ).commits,
-    ).toEqual([])
-  })
-
-  test("suppresses shell events when the legacy bash part claimed the call first", () => {
-    let data = reduce(
-      createSessionData(),
-      tool({
-        id: "tool-1",
-        messageID: "msg-1",
-        callID: "call-1",
-        tool: "bash",
-        state: {
-          status: "running",
-          input: {
-            command: "pwd",
-          },
-          time: { start: 1 },
-        },
-      }),
-    ).data
-
-    expect(
-      reduce(data, shellStarted()).commits,
-    ).toEqual([])
-
-    data = reduce(
-      data,
-      tool({
-        id: "tool-1",
-        messageID: "msg-1",
-        callID: "call-1",
-        tool: "bash",
-        state: {
-          status: "completed",
-          input: {
-            command: "pwd",
-          },
-          output: "/tmp/demo\n",
-          title: "",
-          metadata: {
-            output: "/tmp/demo\n",
-          },
-          time: { start: 1, end: 2 },
-        },
-      }),
-    ).data
-
-    expect(
-      reduce(data, shellEnded()).commits,
-    ).toEqual([])
-  })
-
-  test("synthesizes a glob start before an error when the running update is missed", () => {
-    expect(
-      reduce(
-        createSessionData(),
-        tool({
-          id: "tool-1",
-          messageID: "msg-1",
-          tool: "glob",
-          state: {
-            status: "error",
-            input: {
-              pattern: "**/*tool*",
-              path: "/tmp/demo/run",
-            },
-            error: "No such file or directory: '/tmp/demo/run'",
-          },
-        }),
-      ).commits,
-    ).toEqual([
-      expect.objectContaining({
-        kind: "tool",
-        tool: "glob",
-        phase: "start",
-        partID: "tool-1",
-        text: "running glob",
-        toolState: "running",
-      }),
-      expect.objectContaining({
-        kind: "tool",
-        tool: "glob",
-        phase: "final",
-        partID: "tool-1",
-        text: "No such file or directory: '/tmp/demo/run'",
-        toolState: "error",
-        toolError: "No such file or directory: '/tmp/demo/run'",
-      }),
-    ])
-  })
-
-  test("surfaces session errors as error commits", () => {
-    const out = reduce(createSessionData(), {
-      type: "session.error",
-      properties: {
-        sessionID: "session-1",
-        error: {
-          name: "UnknownError",
-          data: {
-            message: "permission denied",
-          },
-        },
-      },
-    })
-
-    expect(out.commits).toEqual([
-      expect.objectContaining({
-        kind: "error",
-        text: "permission denied",
-      }),
-    ])
-  })
-})

+ 37 - 133
packages/opencode/test/cli/run/session.shared.test.ts

@@ -1,5 +1,5 @@
 import { afterEach, describe, expect, mock, spyOn, test } from "bun:test"
-import { OpenCode } from "@opencode-ai/client/promise"
+import { OpenCode, type SessionMessageUser } from "@opencode-ai/client/promise"
 import {
   createSession,
   resolveCurrentSession,
@@ -9,12 +9,6 @@ import {
   type SessionMessages,
 } from "@opencode-ai/cli/mini/session.shared"
 
-type Message = SessionMessages[number]
-type Part = Message["parts"][number]
-type TextPart = Extract<Part, { type: "text" }>
-type AgentPart = Extract<Part, { type: "agent" }>
-type FilePart = Extract<Part, { type: "file" }>
-
 const model = {
   providerID: "openai",
   modelID: "gpt-5",
@@ -24,107 +18,30 @@ afterEach(() => {
   mock.restore()
 })
 
-function userMessage(id: string, parts: Message["parts"], variant = "high"): Message {
-  return {
-    info: {
-      id,
-      sessionID: "session-1",
-      role: "user",
-      time: {
-        created: 1,
-      },
-      agent: "build",
-      model: {
-        ...model,
-        variant,
-      },
-    },
-    parts,
-  }
-}
-
-function assistantMessage(id: string, parts: Message["parts"]): Message {
-  return {
-    info: {
-      id,
-      sessionID: "session-1",
-      role: "assistant",
-      time: {
-        created: 1,
-      },
-      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,
-        },
-      },
-    },
-    parts,
-  }
-}
-
-function textPart(id: string, messageID: string, text: string, input: Partial<TextPart> = {}): TextPart {
+function userMessage(id: string, text: string, input: Partial<SessionMessageUser> = {}): SessionMessageUser {
   return {
     id,
-    sessionID: "session-1",
-    messageID,
-    type: "text",
+    type: "user",
     text,
-    synthetic: input.synthetic,
-  }
-}
-
-function agentPart(id: string, messageID: string, name: string, source?: AgentPart["source"]): AgentPart {
-  return {
-    id,
-    sessionID: "session-1",
-    messageID,
-    type: "agent",
-    name,
-    source,
-  }
-}
-
-function filePart(id: string, messageID: string, url: string, input: Partial<FilePart> = {}): FilePart {
-  return {
-    id,
-    sessionID: "session-1",
-    messageID,
-    type: "file",
-    mime: input.mime ?? "text/plain",
-    filename: input.filename,
-    url,
-    source: input.source,
+    time: { created: 1 },
+    ...input,
   }
 }
 
 describe("run session shared", () => {
-  test("builds user prompt text from text, file, and agent parts", () => {
+  test("builds user prompts from projected text and attachments", () => {
     const msgs: SessionMessages = [
-      assistantMessage("msg-assistant-1", [textPart("txt-assistant-1", "msg-assistant-1", "ignore me")]),
-      userMessage("msg-user-1", [
-        textPart("txt-user-1", "msg-user-1", "look @scan"),
-        textPart("txt-user-2", "msg-user-1", "hidden", { synthetic: true }),
-        agentPart("agent-user-1", "msg-user-1", "scan", {
-          start: 5,
-          end: 10,
-          value: "@scan",
-        }),
-        filePart("file-user-1", "msg-user-1", "file:///tmp/note.ts"),
-      ]),
+      userMessage("msg-user-1", "look @scan @note.ts", {
+        agents: [{ name: "scan", mention: { start: 5, end: 10, text: "@scan" } }],
+        files: [
+          {
+            data: "",
+            mime: "text/plain",
+            source: { type: "uri", uri: "file:///tmp/note.ts" },
+            mention: { start: 11, end: 19, text: "@note.ts" },
+          },
+        ],
+      }),
     ]
 
     const out = createSession(msgs)
@@ -132,15 +49,6 @@ describe("run session shared", () => {
     expect(out.turns).toHaveLength(1)
     expect(out.turns[0]?.prompt.text).toBe("look @scan @note.ts")
     expect(out.turns[0]?.prompt.parts).toEqual([
-      {
-        type: "agent",
-        name: "scan",
-        source: {
-          start: 5,
-          end: 10,
-          value: "@scan",
-        },
-      },
       {
         type: "file",
         mime: "text/plain",
@@ -156,44 +64,40 @@ describe("run session shared", () => {
           },
         },
       },
+      {
+        type: "agent",
+        name: "scan",
+        source: {
+          start: 5,
+          end: 10,
+          value: "@scan",
+        },
+      },
     ])
   })
 
-  test("reuses existing mentions when file and agent parts have no source", () => {
+  test("leaves attachment sources undefined when projected mentions are absent", () => {
     const out = createSession([
-      userMessage("msg-user-1", [
-        textPart("txt-user-1", "msg-user-1", "look @scan @note.ts"),
-        agentPart("agent-user-1", "msg-user-1", "scan"),
-        filePart("file-user-1", "msg-user-1", "file:///tmp/note.ts"),
-      ]),
+      userMessage("msg-user-1", "look @scan @note.ts", {
+        agents: [{ name: "scan" }],
+        files: [{ data: "", mime: "text/plain", source: { type: "uri", uri: "file:///tmp/note.ts" } }],
+      }),
     ])
 
     expect(out.turns[0]?.prompt).toEqual({
       text: "look @scan @note.ts",
       parts: [
-        {
-          type: "agent",
-          name: "scan",
-          source: {
-            start: 5,
-            end: 10,
-            value: "@scan",
-          },
-        },
         {
           type: "file",
           mime: "text/plain",
           filename: undefined,
           url: "file:///tmp/note.ts",
-          source: {
-            type: "file",
-            path: "file:///tmp/note.ts",
-            text: {
-              start: 11,
-              end: 19,
-              value: "@note.ts",
-            },
-          },
+          source: undefined,
+        },
+        {
+          type: "agent",
+          name: "scan",
+          source: undefined,
         },
       ],
     })

+ 2 - 5
packages/opencode/test/cli/run/stream-v2.transport.test.ts

@@ -513,11 +513,8 @@ describe("V2 mini transport", () => {
         request: {
           id: "per_1",
           sessionID: "ses_1",
-          permission: "read",
-          patterns: ["/tmp/file"],
-          metadata: {},
-          always: [],
-          tool: undefined,
+          action: "read",
+          resources: ["/tmp/file"],
         },
       },
     })

+ 12 - 28
packages/opencode/test/cli/run/variant.shared.test.ts

@@ -11,7 +11,7 @@ import {
   pickVariant,
   resolveVariant,
 } from "@opencode-ai/cli/mini/variant.shared"
-import type { SessionMessages } from "@opencode-ai/cli/mini/session.shared"
+import type { RunSession } from "@opencode-ai/cli/mini/session.shared"
 import type { RunProvider } from "@opencode-ai/cli/mini/types"
 import { testEffect } from "../../lib/effect"
 
@@ -79,25 +79,6 @@ const providers: RunProvider[] = [
   },
 ]
 
-function userMessage(
-  id: string,
-  input: { providerID: string; modelID: string; variant?: string },
-): SessionMessages[number] {
-  return {
-    info: {
-      id,
-      sessionID: "session-1",
-      role: "user",
-      time: {
-        created: 1,
-      },
-      agent: "build",
-      model: input,
-    },
-    parts: [],
-  }
-}
-
 const it = testEffect(AppNodeBuilder.build(FSUtil.node))
 
 function remap(root: string, file: string) {
@@ -147,14 +128,17 @@ describe("run variant shared", () => {
     expect(formatModelLabel(model, "high", providers)).toBe("GPT-5 · OpenAI · high")
   })
 
-  test("picks the latest matching variant from raw session messages", () => {
-    const msgs: SessionMessages = [
-      userMessage("msg-1", { providerID: "openai", modelID: "gpt-5", variant: "high" }),
-      userMessage("msg-2", { providerID: "anthropic", modelID: "sonnet", variant: "max" }),
-      userMessage("msg-3", { providerID: "openai", modelID: "gpt-5", variant: "minimal" }),
-    ]
-
-    expect(pickVariant(model, msgs)).toBe("minimal")
+  test("picks the latest matching variant from session history", () => {
+    const session: RunSession = {
+      first: false,
+      turns: [
+        { prompt: { text: "one", parts: [] }, provider: "openai", model: "gpt-5", variant: "high" },
+        { prompt: { text: "two", parts: [] }, provider: "anthropic", model: "sonnet", variant: "max" },
+        { prompt: { text: "three", parts: [] }, provider: "openai", model: "gpt-5", variant: "minimal" },
+      ],
+    }
+
+    expect(pickVariant(model, session)).toBe("minimal")
   })
 
   it.live("reads and writes saved variants through a runtime-backed app fs layer", () =>