mirror of
https://github.com/anomalyco/opencode.git
synced 2026-07-23 10:45:33 -04:00
925c2423de
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.
224 lines
6.4 KiB
TypeScript
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 })
|
|
})
|
|
})
|