| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223 |
- import { afterEach, describe, expect, mock, spyOn, test } from "bun:test"
- import { OpenCode, type SessionMessageUser } from "@opencode-ai/client/promise"
- import {
- createSession,
- resolveCurrentSession,
- sessionHistory,
- sessionVariant,
- type RunSession,
- type SessionMessages,
- } from "../../src/mini/session.shared"
- const model = {
- providerID: "openai",
- modelID: "gpt-5",
- }
- afterEach(() => {
- mock.restore()
- })
- function userMessage(id: string, text: string, input: Partial<SessionMessageUser> = {}): SessionMessageUser {
- return {
- id,
- type: "user",
- text,
- time: { created: 1 },
- ...input,
- }
- }
- describe("run session shared", () => {
- test("builds user prompts from projected text and attachments", () => {
- const msgs: SessionMessages = [
- 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)
- expect(out.first).toBe(false)
- expect(out.turns).toHaveLength(1)
- expect(out.turns[0]?.prompt.text).toBe("look @scan @note.ts")
- expect(out.turns[0]?.prompt.parts).toEqual([
- {
- 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",
- },
- },
- },
- {
- type: "agent",
- name: "scan",
- source: {
- start: 5,
- end: 10,
- value: "@scan",
- },
- },
- ])
- })
- test("leaves attachment sources undefined when projected mentions are absent", () => {
- const out = createSession([
- 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: "file",
- mime: "text/plain",
- filename: undefined,
- url: "file:///tmp/note.ts",
- source: undefined,
- },
- {
- type: "agent",
- name: "scan",
- source: undefined,
- },
- ],
- })
- })
- test("dedupes consecutive history entries, drops blanks, and copies prompt parts", () => {
- const parts = [
- {
- type: "agent" as const,
- name: "scan",
- source: {
- start: 0,
- end: 5,
- value: "@scan",
- },
- },
- ]
- const session: RunSession = {
- first: false,
- turns: [
- { prompt: { text: "one", parts }, provider: "openai", model: "gpt-5", variant: "high" },
- { prompt: { text: "one", parts: structuredClone(parts) }, provider: "openai", model: "gpt-5", variant: "high" },
- { prompt: { text: " ", parts: [] }, provider: "openai", model: "gpt-5", variant: "high" },
- { prompt: { text: "two", parts: [] }, provider: "openai", model: "gpt-5", variant: undefined },
- ],
- }
- const out = sessionHistory(session)
- expect(out.map((item) => item.text)).toEqual(["one", "two"])
- expect(out[0]?.parts).toEqual(parts)
- expect(out[0]?.parts).not.toBe(parts)
- expect(out[0]?.parts[0]).not.toBe(parts[0])
- })
- test("returns the latest matching variant for the active model", () => {
- 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: undefined },
- ],
- }
- expect(sessionVariant(session, model)).toBeUndefined()
- session.turns.push({
- prompt: { text: "four", parts: [] },
- provider: "openai",
- model: "gpt-5",
- variant: "minimal",
- })
- expect(sessionVariant(session, model)).toBe("minimal")
- })
- test("restores current prompt history from stored text and file references", async () => {
- const client = OpenCode.make({ baseUrl: "https://opencode.test" })
- spyOn(client.message, "list").mockImplementation(() =>
- Promise.resolve({
- data: [
- {
- id: "msg_prompt",
- type: "user",
- text: "Review @note.ts",
- files: [
- {
- data: "",
- mime: "text/plain",
- name: "note.ts",
- source: { type: "uri", uri: "file:///tmp/note.ts" },
- mention: { start: 7, end: 15, text: "@note.ts" },
- },
- ],
- agents: [],
- time: { created: 1 },
- },
- ],
- cursor: {},
- }),
- )
- spyOn(client.session, "get").mockImplementation(() =>
- Promise.resolve({
- id: "ses_1",
- title: "Session",
- projectID: "proj_1",
- location: { directory: "/tmp" },
- time: { created: 1, updated: 1 },
- cost: 0,
- tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
- model: { providerID: "openai", id: "gpt-5", variant: "high" },
- }),
- )
- const controller = new AbortController()
- const out = await resolveCurrentSession(client, "ses_1", controller.signal)
- expect(out.model).toEqual({ providerID: "openai", modelID: "gpt-5" })
- expect(out.variant).toBe("high")
- expect(out.turns[0]?.prompt).toEqual({
- text: "Review @note.ts",
- parts: [
- {
- type: "file",
- url: "file:///tmp/note.ts",
- mime: "text/plain",
- filename: "note.ts",
- source: {
- type: "file",
- path: "note.ts",
- text: { start: 7, end: 15, value: "@note.ts" },
- },
- },
- ],
- })
- expect(client.message.list).toHaveBeenCalledWith(
- { sessionID: "ses_1", limit: 200, order: "desc" },
- { signal: controller.signal },
- )
- expect(client.session.get).toHaveBeenCalledWith({ sessionID: "ses_1" }, { signal: controller.signal })
- })
- })
|