Files
opencode/packages/tui/test/mini/session.shared.test.ts
Simon Klee 925c2423de mini: add reconnect, forms, and shared targets (#37811)
Centralize session target resolution for mini and noninteractive
run paths. Recover from transport drops, replace questions with
forms, and keep tool/catalog state location-scoped with live
progress and theme discovery.
2026-07-19 22:45:10 +02:00

224 lines
6.4 KiB
TypeScript

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