fix(cli): update mini tests for v2 types (#36779)
This commit is contained in:
parent
547e0148c7
commit
6963f2f6da
5 changed files with 64 additions and 735 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
}),
|
||||
])
|
||||
})
|
||||
})
|
||||
|
|
@ -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,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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"],
|
||||
},
|
||||
},
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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" }),
|
||||
]
|
||||
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, msgs)).toBe("minimal")
|
||||
expect(pickVariant(model, session)).toBe("minimal")
|
||||
})
|
||||
|
||||
it.live("reads and writes saved variants through a runtime-backed app fs layer", () =>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue