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 { 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 }) }) })