mini migrate to v2 (#35526)
This commit is contained in:
@@ -41,34 +41,6 @@ Options:
|
||||
--pure run without external plugins [boolean]"
|
||||
`;
|
||||
|
||||
exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode mini --help 1`] = `
|
||||
"opencode mini
|
||||
|
||||
start the minimal interactive interface
|
||||
|
||||
Commands:
|
||||
opencode mini [project] start the minimal interactive interface [default]
|
||||
opencode mini attach <url> attach to a running opencode server with the minimal interface
|
||||
|
||||
Positionals:
|
||||
project path to start opencode in [string]
|
||||
|
||||
Options:
|
||||
-h, --help show help [boolean]
|
||||
-v, --version show version number [boolean]
|
||||
--print-logs print logs to stderr [boolean]
|
||||
--log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"]
|
||||
--pure run without external plugins [boolean]
|
||||
-m, --model model to use in the format of provider/model [string]
|
||||
--agent agent to use [string]
|
||||
--prompt prompt to use [string]
|
||||
-c, --continue continue the last session [boolean]
|
||||
-s, --session session id to continue [string]
|
||||
--fork fork the session when continuing (use with --continue or --session) [boolean]
|
||||
--no-replay disable session history replay on resume and after resize [boolean]
|
||||
--replay-limit cap visible replay to the newest N messages [number]"
|
||||
`;
|
||||
|
||||
exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode attach --help 1`] = `
|
||||
"opencode attach <url>
|
||||
|
||||
@@ -100,33 +72,25 @@ Positionals:
|
||||
message message to send [array] [default: []]
|
||||
|
||||
Options:
|
||||
-h, --help show help [boolean]
|
||||
-v, --version show version number [boolean]
|
||||
--print-logs print logs to stderr [boolean]
|
||||
--log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"]
|
||||
--pure run without external plugins [boolean]
|
||||
--command the command to run, use message for args [string]
|
||||
-c, --continue continue the last session [boolean]
|
||||
-s, --session session id to continue [string]
|
||||
--fork fork the session before continuing (requires --continue or --session) [boolean]
|
||||
-m, --model model to use in the format of provider/model [string]
|
||||
--agent agent to use [string]
|
||||
--format format: default (formatted) or json (raw JSON events)
|
||||
-h, --help show help [boolean]
|
||||
-v, --version show version number [boolean]
|
||||
--print-logs print logs to stderr [boolean]
|
||||
--log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"]
|
||||
--pure run without external plugins [boolean]
|
||||
-c, --continue continue the last session [boolean]
|
||||
-s, --session session id to continue [string]
|
||||
--fork fork the session before continuing (requires --continue or --session) [boolean]
|
||||
-m, --model model to use in the format of provider/model [string]
|
||||
--agent agent to use [string]
|
||||
--format format: default (formatted) or json (raw JSON events)
|
||||
[string] [choices: "default", "json"] [default: "default"]
|
||||
-f, --file file(s) to attach to message [array]
|
||||
--title title for the session (uses truncated prompt if no value provided) [string]
|
||||
--attach attach to a running opencode server (e.g., http://localhost:4096) [string]
|
||||
-p, --password basic auth password (defaults to OPENCODE_SERVER_PASSWORD) [string]
|
||||
-u, --username basic auth username (defaults to OPENCODE_SERVER_USERNAME or 'opencode')
|
||||
[string]
|
||||
--dir directory to run in, path on remote server if attaching [string]
|
||||
--port port for the local server (defaults to random port if no value provided)
|
||||
[number]
|
||||
--variant model variant (provider-specific reasoning effort, e.g., high, max, minimal)
|
||||
[string]
|
||||
--thinking show thinking blocks [boolean]
|
||||
-i, --interactive run in direct interactive split-footer mode [boolean] [default: false]
|
||||
--auto auto-approve permissions that are not explicitly denied (dangerous!)
|
||||
-f, --file file(s) to attach to message [array]
|
||||
--title title for the session (uses truncated prompt if no value provided) [string]
|
||||
--server connect to a running opencode server [string]
|
||||
--dir directory to run in, or a path on the remote server [string]
|
||||
--variant model variant [string]
|
||||
--thinking show thinking blocks [boolean]
|
||||
--auto auto-approve permissions that are not explicitly denied (dangerous!)
|
||||
[boolean] [default: false]"
|
||||
`;
|
||||
|
||||
@@ -426,31 +390,6 @@ Options:
|
||||
--format Output format [string] [choices: "json", "tsv"] [default: "tsv"]"
|
||||
`;
|
||||
|
||||
exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode mini attach --help 1`] = `
|
||||
"opencode mini attach <url>
|
||||
|
||||
attach to a running opencode server with the minimal interface
|
||||
|
||||
Positionals:
|
||||
url http://localhost:4096 [string] [required]
|
||||
|
||||
Options:
|
||||
-h, --help show help [boolean]
|
||||
-v, --version show version number [boolean]
|
||||
--print-logs print logs to stderr [boolean]
|
||||
--log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"]
|
||||
--pure run without external plugins [boolean]
|
||||
--dir directory on the remote server [string]
|
||||
-p, --password basic auth password (defaults to OPENCODE_SERVER_PASSWORD) [string]
|
||||
-u, --username basic auth username (defaults to OPENCODE_SERVER_USERNAME or 'opencode')
|
||||
[string]
|
||||
-c, --continue continue the last session [boolean]
|
||||
-s, --session session id to continue [string]
|
||||
--fork fork the session when continuing (use with --continue or --session) [boolean]
|
||||
--no-replay disable session history replay on resume and after resize [boolean]
|
||||
--replay-limit cap visible replay to the newest N messages [number]"
|
||||
`;
|
||||
|
||||
exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode mcp list --help 1`] = `
|
||||
"opencode mcp list
|
||||
|
||||
|
||||
@@ -57,7 +57,6 @@ function normalize(text: string): string {
|
||||
const TOP_LEVEL = [
|
||||
"acp",
|
||||
"mcp",
|
||||
"mini",
|
||||
"attach",
|
||||
"run",
|
||||
"debug",
|
||||
@@ -82,7 +81,6 @@ const TOP_LEVEL = [
|
||||
// distinct argv shape, not every leaf. Add new entries when a subcommand
|
||||
// gains user-visible flags that we want to lock in.
|
||||
const SUBCOMMANDS = [
|
||||
["mini", "attach"],
|
||||
["mcp", "list"],
|
||||
["mcp", "add"],
|
||||
["mcp", "auth"],
|
||||
@@ -115,7 +113,7 @@ describe("opencode CLI help-text snapshots", () => {
|
||||
const topLevel = yield* opencode.spawn(["--help"], { env: SNAPSHOT_ENV })
|
||||
expect(topLevel.exitCode).toBe(0)
|
||||
expect(topLevel.stderr.endsWith("\n")).toBe(true)
|
||||
expect(topLevel.stderr).toContain("opencode mini")
|
||||
expect(topLevel.stderr).not.toContain("opencode mini")
|
||||
expect(topLevel.stderr).not.toContain("--mini")
|
||||
expect(topLevel.stderr).not.toContain("--thinking")
|
||||
expect(topLevel.stderr).not.toContain("--variant")
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { afterEach, describe, expect, mock, spyOn, test } from "bun:test"
|
||||
import { OpencodeClient } from "@opencode-ai/sdk/v2"
|
||||
import { loadRunReferences, runProviders, waitForDefaultModel } from "@/cli/cmd/run/catalog.shared"
|
||||
import { OpenCode } from "@opencode-ai/client/promise"
|
||||
import { loadRunReferences, runProviders, waitForDefaultModel } from "@opencode-ai/cli/mini/catalog.shared"
|
||||
|
||||
afterEach(() => {
|
||||
mock.restore()
|
||||
@@ -8,20 +8,12 @@ afterEach(() => {
|
||||
|
||||
describe("run catalog shared", () => {
|
||||
test("resolves the catalog-selected model for the footer", async () => {
|
||||
const client = new OpencodeClient()
|
||||
const selected = spyOn(client.v2.model, "default").mockImplementation(
|
||||
const client = OpenCode.make({ baseUrl: "https://opencode.test" })
|
||||
const selected = spyOn(client.model, "default").mockImplementation(
|
||||
() =>
|
||||
Promise.resolve({
|
||||
data: {
|
||||
location: { directory: "/tmp", project: { id: "proj_1", directory: "/tmp" } },
|
||||
data: {
|
||||
id: "gpt-5",
|
||||
providerID: "openai",
|
||||
},
|
||||
},
|
||||
error: undefined,
|
||||
request: new Request("https://opencode.test"),
|
||||
response: new Response(),
|
||||
location: { directory: "/tmp", project: { id: "proj_1", directory: "/tmp" } },
|
||||
data: { id: "gpt-5", providerID: "openai" },
|
||||
}) as never,
|
||||
)
|
||||
|
||||
@@ -29,17 +21,16 @@ describe("run catalog shared", () => {
|
||||
providerID: "openai",
|
||||
modelID: "gpt-5",
|
||||
})
|
||||
expect(selected).toHaveBeenCalledWith({ location: { directory: "/tmp" } }, { throwOnError: true })
|
||||
expect(selected).toHaveBeenCalledWith({ location: { directory: "/tmp" } })
|
||||
})
|
||||
|
||||
test("loads visible project references from the current reference catalog", async () => {
|
||||
const client = new OpencodeClient()
|
||||
const list = spyOn(client.v2.reference, "list").mockImplementation(
|
||||
const client = OpenCode.make({ baseUrl: "https://opencode.test" })
|
||||
const list = spyOn(client.reference, "list").mockImplementation(
|
||||
() =>
|
||||
Promise.resolve({
|
||||
data: {
|
||||
location: { directory: "/tmp", project: { id: "proj_1", directory: "/tmp" } },
|
||||
data: [
|
||||
location: { directory: "/tmp", project: { id: "proj_1", directory: "/tmp" } },
|
||||
data: [
|
||||
{
|
||||
name: "effect",
|
||||
path: "/repos/effect",
|
||||
@@ -52,17 +43,13 @@ describe("run catalog shared", () => {
|
||||
hidden: true,
|
||||
source: { type: "local", path: "/repos/secret" },
|
||||
},
|
||||
],
|
||||
},
|
||||
error: undefined,
|
||||
request: new Request("https://opencode.test"),
|
||||
response: new Response(),
|
||||
],
|
||||
}) as never,
|
||||
)
|
||||
|
||||
const references = await loadRunReferences(client, "/tmp")
|
||||
|
||||
expect(list).toHaveBeenCalledWith({ location: { directory: "/tmp" } }, { throwOnError: true })
|
||||
expect(list).toHaveBeenCalledWith({ location: { directory: "/tmp" } })
|
||||
expect(references).toMatchObject([{ name: "effect", path: "/repos/effect", description: "Effect v4 sources" }])
|
||||
})
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import type { ToolPart } from "@opencode-ai/sdk/v2"
|
||||
import { entryBody, entryCanStream, entryDone } from "@/cli/cmd/run/entry.body"
|
||||
import type { StreamCommit, ToolSnapshot } from "@/cli/cmd/run/types"
|
||||
import { entryBody, entryCanStream, entryDone } from "@opencode-ai/cli/mini/entry.body"
|
||||
import type { StreamCommit, ToolSnapshot } from "@opencode-ai/cli/mini/types"
|
||||
|
||||
function commit(input: Partial<StreamCommit> & Pick<StreamCommit, "kind" | "text" | "phase" | "source">): StreamCommit {
|
||||
return input
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect, test } from "bun:test"
|
||||
import { createRoot } from "solid-js"
|
||||
import { FOOTER_MENU_ROWS, createFooterMenuState } from "@/cli/cmd/run/footer.menu"
|
||||
import { FOOTER_MENU_ROWS, createFooterMenuState } from "@opencode-ai/cli/mini/footer.menu"
|
||||
|
||||
function mount(count: number, limit = FOOTER_MENU_ROWS) {
|
||||
let dispose!: () => void
|
||||
|
||||
@@ -15,10 +15,10 @@ import {
|
||||
RunSkillSelectBody,
|
||||
RunSubagentSelectBody,
|
||||
RunVariantSelectBody,
|
||||
} from "@/cli/cmd/run/footer.command"
|
||||
import { RunFooterView } from "@/cli/cmd/run/footer.view"
|
||||
import { RunEntryContent } from "@/cli/cmd/run/scrollback.writer"
|
||||
import { RUN_THEME_FALLBACK, type RunTheme } from "@/cli/cmd/run/theme"
|
||||
} from "@opencode-ai/cli/mini/footer.command"
|
||||
import { RunFooterView } from "@opencode-ai/cli/mini/footer.view"
|
||||
import { RunEntryContent } from "@opencode-ai/cli/mini/scrollback.writer"
|
||||
import { RUN_THEME_FALLBACK, type RunTheme } from "@opencode-ai/cli/mini/theme"
|
||||
import type {
|
||||
FooterState,
|
||||
FooterSubagentState,
|
||||
@@ -30,10 +30,10 @@ import type {
|
||||
RunProvider,
|
||||
RunTuiConfig,
|
||||
StreamCommit,
|
||||
} from "@/cli/cmd/run/types"
|
||||
import { RunQuestionBody } from "@/cli/cmd/run/footer.question"
|
||||
import { selectedCommand } from "@/cli/cmd/run/footer.prompt"
|
||||
import { RejectField } from "@/cli/cmd/run/footer.permission"
|
||||
} from "@opencode-ai/cli/mini/types"
|
||||
import { RunQuestionBody } from "@opencode-ai/cli/mini/footer.question"
|
||||
import { selectedCommand } from "@opencode-ai/cli/mini/footer.prompt"
|
||||
import { RejectField } from "@opencode-ai/cli/mini/footer.permission"
|
||||
import { createTuiResolvedConfig } from "../../fixture/tui-runtime"
|
||||
|
||||
const tuiConfig = createTuiResolvedConfig()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { footerWidthPolicy } from "@/cli/cmd/run/footer.width"
|
||||
import { footerWidthPolicy } from "@opencode-ai/cli/mini/footer.width"
|
||||
|
||||
describe("run footer width", () => {
|
||||
test("preserves shared dialog and statusline breakpoints", () => {
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
import { afterEach, describe, expect, mock, spyOn, test } from "bun:test"
|
||||
import { OpencodeClient, type V2Event } from "@opencode-ai/sdk/v2"
|
||||
import { runNonInteractivePrompt } from "@/cli/cmd/run/noninteractive"
|
||||
import { OpenCode, type EventSubscribeOutput } from "@opencode-ai/client/promise"
|
||||
import { runNonInteractivePrompt } from "@opencode-ai/cli/mini/noninteractive"
|
||||
|
||||
type V2Event = EventSubscribeOutput
|
||||
type FormInfo = Extract<V2Event, { type: "form.created" }>["data"]["form"]
|
||||
|
||||
function ok<T>(data: T) {
|
||||
return Promise.resolve({
|
||||
data,
|
||||
error: undefined,
|
||||
request: new Request("https://opencode.test"),
|
||||
response: new Response(),
|
||||
})
|
||||
return Promise.resolve(data)
|
||||
}
|
||||
|
||||
function form(id: string, sessionID: string): FormInfo {
|
||||
@@ -43,8 +39,8 @@ function settled(outcome: "success" | "interrupted" = "success"): V2Event {
|
||||
// Runs one non-interactive prompt against a mocked SDK. `turn` produces the
|
||||
// live events the prompt admission triggers, keyed by the generated message ID.
|
||||
async function run(input: { turn: (inputID: string) => V2Event[]; pendingForms?: FormInfo[]; attached?: boolean }) {
|
||||
const sdk = new OpencodeClient()
|
||||
const values: V2Event[] = [{ id: "evt_connected", created: 0, type: "server.connected", data: {} }]
|
||||
const sdk = OpenCode.make({ baseUrl: "https://opencode.test" })
|
||||
const values: V2Event[] = [{ id: "evt_connected", type: "server.connected", data: {} }]
|
||||
let wake: (() => void) | undefined
|
||||
const stream = (async function* (): AsyncGenerator<V2Event, void, unknown> {
|
||||
while (true) {
|
||||
@@ -58,22 +54,20 @@ async function run(input: { turn: (inputID: string) => V2Event[]; pendingForms?:
|
||||
yield value
|
||||
}
|
||||
})()
|
||||
spyOn(sdk.v2.event, "subscribe").mockImplementation(
|
||||
() => Promise.resolve({ stream }) as ReturnType<typeof sdk.v2.event.subscribe>,
|
||||
)
|
||||
spyOn(sdk.v2.session.permission, "list").mockImplementation(() => ok({ data: [] }) as never)
|
||||
spyOn(sdk.v2.session.question, "list").mockImplementation(() => ok({ data: [] }) as never)
|
||||
spyOn(sdk.v2.session.form, "list").mockImplementation(
|
||||
spyOn(sdk.event, "subscribe").mockImplementation(() => stream)
|
||||
spyOn(sdk.permission, "list").mockImplementation(() => ok([]) as never)
|
||||
spyOn(sdk.question, "list").mockImplementation(() => ok([]) as never)
|
||||
spyOn(sdk.form, "list").mockImplementation(
|
||||
(request) =>
|
||||
ok({ data: input.pendingForms?.filter((item) => item.sessionID === request.sessionID) ?? [] }) as never,
|
||||
ok(input.pendingForms?.filter((item) => item.sessionID === request.sessionID) ?? []) as never,
|
||||
)
|
||||
spyOn(sdk.v2.session.form, "cancel").mockImplementation(() => ok(undefined) as never)
|
||||
spyOn(sdk.v2.session, "prompt").mockImplementation((request) => {
|
||||
spyOn(sdk.form, "cancel").mockImplementation(() => ok(undefined) as never)
|
||||
spyOn(sdk.session, "prompt").mockImplementation((request) => {
|
||||
const messageID = request.id ?? "msg_prompt"
|
||||
values.push(...input.turn(messageID))
|
||||
wake?.()
|
||||
wake = undefined
|
||||
return ok({ data: { admittedSeq: 1, id: messageID, sessionID: "ses_1", timeCreated: 1 } }) as never
|
||||
return ok({ admittedSeq: 1, id: messageID, sessionID: "ses_1", timeCreated: 1 }) as never
|
||||
})
|
||||
await runNonInteractivePrompt({
|
||||
client: sdk,
|
||||
@@ -102,9 +96,9 @@ describe("runNonInteractivePrompt", () => {
|
||||
// which must not leave the consume loop waiting forever.
|
||||
turn: () => [formCreated(form("frm_live", "global")), settled("interrupted")],
|
||||
})
|
||||
expect(sdk.v2.session.form.cancel).toHaveBeenCalledWith({ sessionID: "global", formID: "frm_live" })
|
||||
expect(sdk.v2.session.form.cancel).toHaveBeenCalledWith({ sessionID: "ses_1", formID: "frm_pending" })
|
||||
expect(sdk.v2.session.form.cancel).toHaveBeenCalledWith({ sessionID: "global", formID: "frm_pending_global" })
|
||||
expect(sdk.form.cancel).toHaveBeenCalledWith({ sessionID: "global", formID: "frm_live" })
|
||||
expect(sdk.form.cancel).toHaveBeenCalledWith({ sessionID: "ses_1", formID: "frm_pending" })
|
||||
expect(sdk.form.cancel).toHaveBeenCalledWith({ sessionID: "global", formID: "frm_pending_global" })
|
||||
})
|
||||
|
||||
test("attach mode cancels only session-owned forms", async () => {
|
||||
@@ -113,9 +107,9 @@ describe("runNonInteractivePrompt", () => {
|
||||
pendingForms: [form("frm_pending", "ses_1"), form("frm_pending_global", "global")],
|
||||
turn: (messageID) => [formCreated(form("frm_live", "global")), prompted(messageID), settled()],
|
||||
})
|
||||
expect(sdk.v2.session.form.cancel).toHaveBeenCalledWith({ sessionID: "ses_1", formID: "frm_pending" })
|
||||
expect(sdk.v2.session.form.list).not.toHaveBeenCalledWith({ sessionID: "global" })
|
||||
expect(sdk.v2.session.form.cancel).not.toHaveBeenCalledWith({ sessionID: "global", formID: "frm_live" })
|
||||
expect(sdk.v2.session.form.cancel).not.toHaveBeenCalledWith({ sessionID: "global", formID: "frm_pending_global" })
|
||||
expect(sdk.form.cancel).toHaveBeenCalledWith({ sessionID: "ses_1", formID: "frm_pending" })
|
||||
expect(sdk.form.list).not.toHaveBeenCalledWith({ sessionID: "global" })
|
||||
expect(sdk.form.cancel).not.toHaveBeenCalledWith({ sessionID: "global", formID: "frm_live" })
|
||||
expect(sdk.form.cancel).not.toHaveBeenCalledWith({ sessionID: "global", formID: "frm_pending_global" })
|
||||
})
|
||||
})
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
permissionInfo,
|
||||
permissionReject,
|
||||
permissionRun,
|
||||
} from "@/cli/cmd/run/permission.shared"
|
||||
} from "@opencode-ai/cli/mini/permission.shared"
|
||||
|
||||
function req(input: Partial<PermissionRequest> = {}): PermissionRequest {
|
||||
return {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { realignEditorPromptParts, resolveEditorSlashValue } from "@/cli/cmd/run/prompt.editor"
|
||||
import type { RunPromptPart } from "@/cli/cmd/run/types"
|
||||
import { realignEditorPromptParts, resolveEditorSlashValue } from "@opencode-ai/cli/mini/prompt.editor"
|
||||
import type { RunPromptPart } from "@opencode-ai/cli/mini/types"
|
||||
|
||||
describe("run prompt editor helpers", () => {
|
||||
test("strips the local /editor command from the initial editor text", () => {
|
||||
|
||||
@@ -5,8 +5,8 @@ import {
|
||||
isNewCommand,
|
||||
movePromptHistory,
|
||||
pushPromptHistory,
|
||||
} from "@/cli/cmd/run/prompt.shared"
|
||||
import type { RunPrompt } from "@/cli/cmd/run/types"
|
||||
} from "@opencode-ai/cli/mini/prompt.shared"
|
||||
import type { RunPrompt } from "@opencode-ai/cli/mini/types"
|
||||
|
||||
function prompt(text: string, parts: RunPrompt["parts"] = []): RunPrompt {
|
||||
return { text, parts }
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
questionStoreCustom,
|
||||
questionSubmit,
|
||||
questionSync,
|
||||
} from "@/cli/cmd/run/question.shared"
|
||||
} from "@opencode-ai/cli/mini/question.shared"
|
||||
|
||||
function req(input: Partial<QuestionRequest> = {}): QuestionRequest {
|
||||
return {
|
||||
|
||||
@@ -363,11 +363,11 @@ describe("opencode run (non-interactive subprocess)", () => {
|
||||
)
|
||||
|
||||
cliIt.concurrent(
|
||||
"applies a variant to the configured default model",
|
||||
"applies a variant to the selected model",
|
||||
({ llm, opencode }) =>
|
||||
Effect.gen(function* () {
|
||||
yield* llm.text("variant response")
|
||||
const result = yield* opencode.spawn(["run", "--variant", "default", "use the default model"], {
|
||||
const result = yield* opencode.spawn(["run", "--model", "test/test-model", "--variant", "default", "use the model"], {
|
||||
config: { ...testProviderConfig(llm.url), model: "test/test-model" },
|
||||
})
|
||||
|
||||
|
||||
@@ -1,17 +1,11 @@
|
||||
import { afterEach, describe, expect, mock, spyOn, test } from "bun:test"
|
||||
import { OpencodeClient } from "@opencode-ai/sdk/v2"
|
||||
import { OpenCode } from "@opencode-ai/client/promise"
|
||||
import type { Resolved } from "@opencode-ai/tui/config"
|
||||
import { TuiConfig } from "@/config/tui"
|
||||
import { resolveDiffStyle, resolveModelInfo, resolveRunTuiConfig } from "@/cli/cmd/run/runtime.boot"
|
||||
import { resolveDiffStyle, resolveModelInfo, resolveRunTuiConfig } from "@opencode-ai/cli/mini/runtime.boot"
|
||||
import { createTuiResolvedConfig } from "../../fixture/tui-runtime"
|
||||
|
||||
function ok<T>(data: T) {
|
||||
return Promise.resolve({
|
||||
data,
|
||||
error: undefined,
|
||||
request: new Request("https://opencode.test"),
|
||||
response: new Response(),
|
||||
})
|
||||
return Promise.resolve(data)
|
||||
}
|
||||
|
||||
function provider(id: string, name: string) {
|
||||
@@ -108,23 +102,21 @@ describe("run runtime boot", () => {
|
||||
})
|
||||
|
||||
test("reads footer keybinds from resolved keybind config", async () => {
|
||||
spyOn(TuiConfig, "get").mockResolvedValue(
|
||||
config({
|
||||
leader: "ctrl+g",
|
||||
bindings: {
|
||||
commandList: ["ctrl+p"],
|
||||
variantCycle: ["ctrl+t", "alt+t"],
|
||||
interrupt: ["ctrl+c"],
|
||||
historyPrevious: ["k"],
|
||||
historyNext: ["j"],
|
||||
inputClear: ["ctrl+l"],
|
||||
inputSubmit: ["ctrl+s"],
|
||||
inputNewline: ["alt+return"],
|
||||
},
|
||||
}),
|
||||
)
|
||||
const input = config({
|
||||
leader: "ctrl+g",
|
||||
bindings: {
|
||||
commandList: ["ctrl+p"],
|
||||
variantCycle: ["ctrl+t", "alt+t"],
|
||||
interrupt: ["ctrl+c"],
|
||||
historyPrevious: ["k"],
|
||||
historyNext: ["j"],
|
||||
inputClear: ["ctrl+l"],
|
||||
inputSubmit: ["ctrl+s"],
|
||||
inputNewline: ["alt+return"],
|
||||
},
|
||||
})
|
||||
|
||||
const result = await resolveRunTuiConfig()
|
||||
const result = await resolveRunTuiConfig(input)
|
||||
|
||||
expect(result.keybinds.get("leader")?.[0]?.key).toBe("ctrl+g")
|
||||
expect(result.leader_timeout).toBe(2000)
|
||||
@@ -139,9 +131,7 @@ describe("run runtime boot", () => {
|
||||
})
|
||||
|
||||
test("falls back to default tui keymap config when config load fails", async () => {
|
||||
spyOn(TuiConfig, "get").mockRejectedValue(new Error("boom"))
|
||||
|
||||
const result = await resolveRunTuiConfig()
|
||||
const result = await resolveRunTuiConfig(Promise.reject(new Error("boom")))
|
||||
|
||||
expect(result.keybinds.get("leader")?.[0]?.key).toBe("ctrl+x")
|
||||
expect(result.leader_timeout).toBe(2000)
|
||||
@@ -157,31 +147,23 @@ describe("run runtime boot", () => {
|
||||
})
|
||||
|
||||
test("preserves disabled leader from resolved tui config", async () => {
|
||||
spyOn(TuiConfig, "get").mockResolvedValue(config({ leader: "none" }))
|
||||
|
||||
const result = await resolveRunTuiConfig()
|
||||
const result = await resolveRunTuiConfig(config({ leader: "none" }))
|
||||
|
||||
expect(result.keybinds.get("leader")).toEqual([])
|
||||
})
|
||||
|
||||
test("reads diff style and falls back to auto", async () => {
|
||||
spyOn(TuiConfig, "get").mockResolvedValue(config({ diff_style: "stacked" }))
|
||||
await expect(resolveDiffStyle()).resolves.toBe("stacked")
|
||||
await expect(resolveDiffStyle(config({ diff_style: "stacked" }))).resolves.toBe("stacked")
|
||||
|
||||
mock.restore()
|
||||
spyOn(TuiConfig, "get").mockRejectedValue(new Error("boom"))
|
||||
await expect(resolveDiffStyle()).resolves.toBe("auto")
|
||||
await expect(resolveDiffStyle(Promise.reject(new Error("boom")))).resolves.toBe("auto")
|
||||
})
|
||||
|
||||
test("loads v2 providers and models for model selector data", async () => {
|
||||
const sdk = new OpencodeClient()
|
||||
const sdk = OpenCode.make({ baseUrl: "https://opencode.test" })
|
||||
const providers = [provider("openai", "OpenAI")]
|
||||
const models = [model("gpt-5", "openai", 128000, ["high", "minimal"])]
|
||||
// The generated methods have conditional return types for throwOnError; these mocks represent the successful branch.
|
||||
// @ts-expect-error successful SDK response is valid for both modes at runtime
|
||||
const providerList = spyOn(sdk.v2.provider, "list").mockImplementation(() => ok({ data: providers }))
|
||||
// @ts-expect-error successful SDK response is valid for both modes at runtime
|
||||
spyOn(sdk.v2.model, "list").mockImplementation(() => ok({ data: models }))
|
||||
const providerList = spyOn(sdk.provider, "list").mockImplementation(() => ok({ data: providers }) as never)
|
||||
spyOn(sdk.model, "list").mockImplementation(() => ok({ data: models }) as never)
|
||||
|
||||
await expect(resolveModelInfo(sdk, "/workspace", { providerID: "openai", modelID: "gpt-5" })).resolves.toEqual({
|
||||
providers: [
|
||||
@@ -230,19 +212,15 @@ describe("run runtime boot", () => {
|
||||
directory: "/workspace",
|
||||
},
|
||||
},
|
||||
{ throwOnError: true },
|
||||
)
|
||||
})
|
||||
|
||||
test("loads context limits across v2 providers", async () => {
|
||||
const sdk = new OpencodeClient()
|
||||
const sdk = OpenCode.make({ baseUrl: "https://opencode.test" })
|
||||
const providers = [provider("openai", "OpenAI"), provider("anthropic", "Anthropic")]
|
||||
const models = [model("gpt-5", "openai", 128000, ["high", "minimal"]), model("sonnet", "anthropic", 200000)]
|
||||
// The generated methods have conditional return types for throwOnError; these mocks represent the successful branch.
|
||||
// @ts-expect-error successful SDK response is valid for both modes at runtime
|
||||
spyOn(sdk.v2.provider, "list").mockImplementation(() => ok({ data: providers }))
|
||||
// @ts-expect-error successful SDK response is valid for both modes at runtime
|
||||
spyOn(sdk.v2.model, "list").mockImplementation(() => ok({ data: models }))
|
||||
spyOn(sdk.provider, "list").mockImplementation(() => ok({ data: providers }) as never)
|
||||
spyOn(sdk.model, "list").mockImplementation(() => ok({ data: models }) as never)
|
||||
|
||||
await expect(resolveModelInfo(sdk, "/workspace", { providerID: "openai", modelID: "gpt-5" })).resolves.toEqual({
|
||||
providers: [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { runPromptQueue } from "@/cli/cmd/run/runtime.queue"
|
||||
import type { FooterApi, FooterEvent, RunPrompt, StreamCommit } from "@/cli/cmd/run/types"
|
||||
import { runPromptQueue } from "@opencode-ai/cli/mini/runtime.queue"
|
||||
import type { FooterApi, FooterEvent, RunPrompt, StreamCommit } from "@opencode-ai/cli/mini/types"
|
||||
|
||||
function footer() {
|
||||
const prompts = new Set<(input: RunPrompt) => void>()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { Readable } from "node:stream"
|
||||
import { INTERACTIVE_INPUT_ERROR, resolveInteractiveStdin } from "@/cli/cmd/run/runtime.stdin"
|
||||
import { INTERACTIVE_INPUT_ERROR, resolveInteractiveStdin } from "@opencode-ai/cli/mini/runtime.stdin"
|
||||
|
||||
function stream(isTTY: boolean) {
|
||||
return Object.assign(new Readable({ read() {} }), { isTTY }) as NodeJS.ReadStream
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { afterEach, describe, expect, mock, spyOn, test } from "bun:test"
|
||||
import { OpencodeClient } from "@opencode-ai/sdk/v2"
|
||||
import { runInteractiveMode } from "@/cli/cmd/run/runtime"
|
||||
import type { FooterApi, FooterEvent, RunProvider } from "@/cli/cmd/run/types"
|
||||
import { OpenCode } from "@opencode-ai/client/promise"
|
||||
import { runInteractiveDeferredMode, runInteractiveMode } from "@opencode-ai/cli/mini/runtime"
|
||||
import type { FooterApi, FooterEvent, RunProvider } from "@opencode-ai/cli/mini/types"
|
||||
|
||||
const provider: RunProvider = {
|
||||
id: "openai",
|
||||
@@ -45,12 +45,7 @@ function defer<T>() {
|
||||
}
|
||||
|
||||
function ok<T>(data: T) {
|
||||
return Promise.resolve({
|
||||
data,
|
||||
error: undefined,
|
||||
request: new Request("https://opencode.test"),
|
||||
response: new Response(),
|
||||
})
|
||||
return Promise.resolve(data)
|
||||
}
|
||||
|
||||
function footer(events: FooterEvent[] = []): FooterApi {
|
||||
@@ -110,16 +105,173 @@ afterEach(() => {
|
||||
})
|
||||
|
||||
describe("run interactive runtime", () => {
|
||||
test("resolves the deferred session only after first paint", async () => {
|
||||
const sdk = OpenCode.make({ baseUrl: "https://opencode.test" })
|
||||
const lifecycleStarted = defer<void>()
|
||||
const painted = defer<void>()
|
||||
const api = footer()
|
||||
let resolved = 0
|
||||
api.idle = () => painted.promise
|
||||
spyOn(sdk.provider, "list").mockImplementation(() => ok({ location: { directory: "/tmp" }, data: [] }) as never)
|
||||
spyOn(sdk.model, "list").mockImplementation(() => ok({ location: { directory: "/tmp" }, data: [] }) as never)
|
||||
spyOn(sdk.agent, "list").mockImplementation(() => ok({ location: { directory: "/tmp" }, data: [] }) as never)
|
||||
spyOn(sdk.reference, "list").mockImplementation(() => ok({ location: { directory: "/tmp" }, data: [] }) as never)
|
||||
spyOn(sdk.command, "list").mockImplementation(() => ok({ location: { directory: "/tmp" }, data: [] }) as never)
|
||||
spyOn(sdk.skill, "list").mockImplementation(() => ok({ location: { directory: "/tmp" }, data: [] }) as never)
|
||||
|
||||
const task = runInteractiveDeferredMode(
|
||||
{
|
||||
sdk,
|
||||
directory: "/tmp",
|
||||
resolveAgent: async () => "build",
|
||||
session: async () => {
|
||||
resolved++
|
||||
api.close()
|
||||
return { id: "ses-deferred", title: "Deferred" }
|
||||
},
|
||||
agent: "build",
|
||||
model: { providerID: "openai", modelID: "gpt-5" },
|
||||
variant: undefined,
|
||||
files: [],
|
||||
thinking: false,
|
||||
backgroundSubagents: false,
|
||||
},
|
||||
{
|
||||
createRuntimeLifecycle: async () => {
|
||||
lifecycleStarted.resolve()
|
||||
return {
|
||||
footer: api,
|
||||
onResize: () => () => {},
|
||||
refreshTheme: () => {},
|
||||
resetForReplay: () => Promise.resolve(),
|
||||
close: () => Promise.resolve(),
|
||||
}
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
await lifecycleStarted.promise
|
||||
expect(resolved).toBe(0)
|
||||
painted.resolve()
|
||||
await task
|
||||
expect(resolved).toBe(1)
|
||||
})
|
||||
|
||||
test("restores deferred session history and model after first paint", async () => {
|
||||
const sdk = OpenCode.make({ baseUrl: "https://opencode.test" })
|
||||
const lifecycleStarted = defer<void>()
|
||||
const painted = defer<void>()
|
||||
const events: FooterEvent[] = []
|
||||
const api = footer(events)
|
||||
api.idle = () => painted.promise
|
||||
const event = api.event
|
||||
api.event = (value) => {
|
||||
event(value)
|
||||
if (value.type === "model") api.close()
|
||||
}
|
||||
spyOn(sdk.session, "get").mockImplementation(
|
||||
() =>
|
||||
ok({
|
||||
id: "ses-resume",
|
||||
projectID: "pro-1",
|
||||
title: "Resume",
|
||||
cost: 0,
|
||||
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
|
||||
time: { created: 1, updated: 1 },
|
||||
location: { directory: "/tmp" },
|
||||
model: { providerID: "openai", id: "gpt-5", variant: "high" },
|
||||
}) as never,
|
||||
)
|
||||
spyOn(sdk.message, "list").mockImplementation(
|
||||
() =>
|
||||
ok({
|
||||
data: [{ id: "msg-user", type: "user", text: "previous prompt", time: { created: 1 } }],
|
||||
cursor: {},
|
||||
}) as never,
|
||||
)
|
||||
spyOn(sdk.provider, "list").mockImplementation(
|
||||
() =>
|
||||
ok({
|
||||
location: { directory: "/tmp" },
|
||||
data: [{ id: "openai", name: "OpenAI", request: { headers: {}, body: {} } }],
|
||||
}) as never,
|
||||
)
|
||||
spyOn(sdk.model, "list").mockImplementation(
|
||||
() =>
|
||||
ok({
|
||||
location: { directory: "/tmp" },
|
||||
data: [
|
||||
{
|
||||
id: "gpt-5",
|
||||
providerID: "openai",
|
||||
name: "Little Frank",
|
||||
capabilities: { tools: true, input: ["text"], output: ["text"] },
|
||||
request: { headers: {}, body: {} },
|
||||
variants: [{ id: "high", settings: {}, headers: {}, body: {} }],
|
||||
time: { released: 1 },
|
||||
cost: [{ input: 0, output: 0, cache: { read: 0, write: 0 } }],
|
||||
status: "active",
|
||||
enabled: true,
|
||||
limit: { context: 128000, output: 8192 },
|
||||
},
|
||||
],
|
||||
}) as never,
|
||||
)
|
||||
spyOn(sdk.agent, "list").mockImplementation(() => ok({ location: { directory: "/tmp" }, data: [] }) as never)
|
||||
spyOn(sdk.reference, "list").mockImplementation(() => ok({ location: { directory: "/tmp" }, data: [] }) as never)
|
||||
spyOn(sdk.command, "list").mockImplementation(() => ok({ location: { directory: "/tmp" }, data: [] }) as never)
|
||||
spyOn(sdk.skill, "list").mockImplementation(() => ok({ location: { directory: "/tmp" }, data: [] }) as never)
|
||||
|
||||
const task = runInteractiveDeferredMode(
|
||||
{
|
||||
sdk,
|
||||
directory: "/tmp",
|
||||
resolveAgent: async () => "build",
|
||||
session: async () => ({ id: "ses-resume", title: "Resume", resume: true }),
|
||||
agent: "build",
|
||||
model: undefined,
|
||||
variant: undefined,
|
||||
files: [],
|
||||
thinking: false,
|
||||
backgroundSubagents: false,
|
||||
},
|
||||
{
|
||||
createRuntimeLifecycle: async () => {
|
||||
lifecycleStarted.resolve()
|
||||
return {
|
||||
footer: api,
|
||||
onResize: () => () => {},
|
||||
refreshTheme: () => {},
|
||||
resetForReplay: () => Promise.resolve(),
|
||||
close: () => Promise.resolve(),
|
||||
}
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
await lifecycleStarted.promise
|
||||
expect(sdk.session.get).not.toHaveBeenCalled()
|
||||
painted.resolve()
|
||||
await task
|
||||
|
||||
expect(events).toContainEqual({
|
||||
type: "history",
|
||||
history: [{ text: "previous prompt", parts: [] }],
|
||||
})
|
||||
expect(events).toContainEqual({
|
||||
type: "model",
|
||||
model: "Little Frank · OpenAI · high",
|
||||
selection: { providerID: "openai", modelID: "gpt-5" },
|
||||
})
|
||||
})
|
||||
|
||||
test("waits for provider metadata before eager replay transport bootstrap", async () => {
|
||||
const providersStarted = defer<void>()
|
||||
const providers = defer<void>()
|
||||
const lifecycleModels: unknown[] = []
|
||||
|
||||
const sdk = new OpencodeClient()
|
||||
const legacyProviders = spyOn(sdk.config, "providers").mockRejectedValue(new Error("legacy providers should stay unused"))
|
||||
const legacyAgents = spyOn(sdk.app, "agents").mockRejectedValue(new Error("legacy agents should stay unused"))
|
||||
const legacyCommands = spyOn(sdk.command, "list").mockRejectedValue(new Error("legacy commands should stay unused"))
|
||||
spyOn(sdk.v2.provider, "list").mockImplementation(async () => {
|
||||
const sdk = OpenCode.make({ baseUrl: "https://opencode.test" })
|
||||
spyOn(sdk.provider, "list").mockImplementation(async () => {
|
||||
providersStarted.resolve()
|
||||
await providers.promise
|
||||
return ok({
|
||||
@@ -142,55 +294,56 @@ describe("run interactive runtime", () => {
|
||||
],
|
||||
}) as never
|
||||
})
|
||||
spyOn(sdk.v2.model, "list").mockImplementation(() =>
|
||||
ok({
|
||||
location: {
|
||||
directory: "/tmp",
|
||||
},
|
||||
data: [
|
||||
{
|
||||
id: "gpt-5",
|
||||
providerID: "openai",
|
||||
name: "Little Frank",
|
||||
api: {
|
||||
id: "openai",
|
||||
type: "native",
|
||||
settings: {},
|
||||
},
|
||||
capabilities: {
|
||||
tools: true,
|
||||
input: ["text"],
|
||||
output: ["text"],
|
||||
},
|
||||
request: {
|
||||
headers: {},
|
||||
body: {},
|
||||
},
|
||||
variants: [],
|
||||
time: {
|
||||
released: 1,
|
||||
},
|
||||
cost: [
|
||||
{
|
||||
input: 0,
|
||||
output: 0,
|
||||
cache: {
|
||||
read: 0,
|
||||
write: 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
status: "active",
|
||||
enabled: true,
|
||||
limit: {
|
||||
context: 128000,
|
||||
output: 8192,
|
||||
},
|
||||
spyOn(sdk.model, "list").mockImplementation(
|
||||
() =>
|
||||
ok({
|
||||
location: {
|
||||
directory: "/tmp",
|
||||
},
|
||||
],
|
||||
}) as never,
|
||||
data: [
|
||||
{
|
||||
id: "gpt-5",
|
||||
providerID: "openai",
|
||||
name: "Little Frank",
|
||||
api: {
|
||||
id: "openai",
|
||||
type: "native",
|
||||
settings: {},
|
||||
},
|
||||
capabilities: {
|
||||
tools: true,
|
||||
input: ["text"],
|
||||
output: ["text"],
|
||||
},
|
||||
request: {
|
||||
headers: {},
|
||||
body: {},
|
||||
},
|
||||
variants: [],
|
||||
time: {
|
||||
released: 1,
|
||||
},
|
||||
cost: [
|
||||
{
|
||||
input: 0,
|
||||
output: 0,
|
||||
cache: {
|
||||
read: 0,
|
||||
write: 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
status: "active",
|
||||
enabled: true,
|
||||
limit: {
|
||||
context: 128000,
|
||||
output: 8192,
|
||||
},
|
||||
},
|
||||
],
|
||||
}) as never,
|
||||
)
|
||||
spyOn(sdk.v2.session, "messages").mockImplementation(() =>
|
||||
spyOn(sdk.message, "list").mockImplementation(() =>
|
||||
ok({
|
||||
data: [
|
||||
{
|
||||
@@ -205,9 +358,9 @@ describe("run interactive runtime", () => {
|
||||
cursor: {},
|
||||
}),
|
||||
)
|
||||
spyOn(sdk.v2.session, "get").mockImplementation(() =>
|
||||
ok({
|
||||
data: {
|
||||
spyOn(sdk.session, "get").mockImplementation(
|
||||
() =>
|
||||
ok({
|
||||
id: "ses-1",
|
||||
projectID: "pro-1",
|
||||
title: "Session",
|
||||
@@ -232,13 +385,12 @@ describe("run interactive runtime", () => {
|
||||
providerID: "openai",
|
||||
id: "gpt-5",
|
||||
},
|
||||
},
|
||||
}),
|
||||
}) as never,
|
||||
)
|
||||
spyOn(sdk.v2.agent, "list").mockImplementation(() => ok({ location: { directory: "/tmp" }, data: [] }) as never)
|
||||
spyOn(sdk.v2.reference, "list").mockImplementation(() => ok({ location: { directory: "/tmp" }, data: [] }) as never)
|
||||
spyOn(sdk.v2.command, "list").mockImplementation(() => ok({ location: { directory: "/tmp" }, data: [] }) as never)
|
||||
spyOn(sdk.v2.skill, "list").mockImplementation(() => ok({ location: { directory: "/tmp" }, data: [] }) as never)
|
||||
spyOn(sdk.agent, "list").mockImplementation(() => ok({ location: { directory: "/tmp" }, data: [] }) as never)
|
||||
spyOn(sdk.reference, "list").mockImplementation(() => ok({ location: { directory: "/tmp" }, data: [] }) as never)
|
||||
spyOn(sdk.command, "list").mockImplementation(() => ok({ location: { directory: "/tmp" }, data: [] }) as never)
|
||||
spyOn(sdk.skill, "list").mockImplementation(() => ok({ location: { directory: "/tmp" }, data: [] }) as never)
|
||||
|
||||
const task = runInteractiveMode(
|
||||
{
|
||||
@@ -296,13 +448,10 @@ describe("run interactive runtime", () => {
|
||||
|
||||
expect(lifecycleModels).toEqual([{ providerID: "openai", modelID: "gpt-5" }])
|
||||
expect(transportProviders).toEqual([[provider]])
|
||||
expect(legacyProviders).not.toHaveBeenCalled()
|
||||
expect(legacyAgents).not.toHaveBeenCalled()
|
||||
expect(legacyCommands).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test("defers catalog-selected model resolution until after first paint", async () => {
|
||||
const sdk = new OpencodeClient()
|
||||
const sdk = OpenCode.make({ baseUrl: "https://opencode.test" })
|
||||
const defaultStarted = defer<void>()
|
||||
const releaseDefault = defer<void>()
|
||||
const lifecycleStarted = defer<void>()
|
||||
@@ -320,7 +469,7 @@ describe("run interactive runtime", () => {
|
||||
api.close()
|
||||
}
|
||||
|
||||
spyOn(sdk.v2.model, "default").mockImplementation(async () => {
|
||||
spyOn(sdk.model, "default").mockImplementation(async () => {
|
||||
defaultRequested = true
|
||||
defaultStarted.resolve()
|
||||
await releaseDefault.promise
|
||||
@@ -329,24 +478,12 @@ describe("run interactive runtime", () => {
|
||||
data: { id: "gpt-5", providerID: "openai" },
|
||||
}) as never
|
||||
})
|
||||
spyOn(sdk.v2.provider, "list").mockImplementation(() =>
|
||||
ok({ location: { directory: "/tmp" }, data: [] }) as never,
|
||||
)
|
||||
spyOn(sdk.v2.model, "list").mockImplementation(() =>
|
||||
ok({ location: { directory: "/tmp" }, data: [] }) as never,
|
||||
)
|
||||
spyOn(sdk.v2.agent, "list").mockImplementation(() =>
|
||||
ok({ location: { directory: "/tmp" }, data: [] }) as never,
|
||||
)
|
||||
spyOn(sdk.v2.reference, "list").mockImplementation(() =>
|
||||
ok({ location: { directory: "/tmp" }, data: [] }) as never,
|
||||
)
|
||||
spyOn(sdk.v2.command, "list").mockImplementation(() =>
|
||||
ok({ location: { directory: "/tmp" }, data: [] }) as never,
|
||||
)
|
||||
spyOn(sdk.v2.skill, "list").mockImplementation(() =>
|
||||
ok({ location: { directory: "/tmp" }, data: [] }) as never,
|
||||
)
|
||||
spyOn(sdk.provider, "list").mockImplementation(() => ok({ location: { directory: "/tmp" }, data: [] }) as never)
|
||||
spyOn(sdk.model, "list").mockImplementation(() => ok({ location: { directory: "/tmp" }, data: [] }) as never)
|
||||
spyOn(sdk.agent, "list").mockImplementation(() => ok({ location: { directory: "/tmp" }, data: [] }) as never)
|
||||
spyOn(sdk.reference, "list").mockImplementation(() => ok({ location: { directory: "/tmp" }, data: [] }) as never)
|
||||
spyOn(sdk.command, "list").mockImplementation(() => ok({ location: { directory: "/tmp" }, data: [] }) as never)
|
||||
spyOn(sdk.skill, "list").mockImplementation(() => ok({ location: { directory: "/tmp" }, data: [] }) as never)
|
||||
|
||||
const task = runInteractiveMode(
|
||||
{
|
||||
@@ -402,12 +539,12 @@ describe("run interactive runtime", () => {
|
||||
})
|
||||
|
||||
test("does not start deferred work after the footer closes", async () => {
|
||||
const sdk = new OpencodeClient()
|
||||
const sdk = OpenCode.make({ baseUrl: "https://opencode.test" })
|
||||
const lifecycleStarted = defer<void>()
|
||||
const painted = defer<void>()
|
||||
const api = footer()
|
||||
api.idle = () => painted.promise
|
||||
const defaultModel = spyOn(sdk.v2.model, "default")
|
||||
const defaultModel = spyOn(sdk.model, "default")
|
||||
|
||||
const task = runInteractiveMode(
|
||||
{
|
||||
@@ -444,8 +581,50 @@ describe("run interactive runtime", () => {
|
||||
expect(defaultModel).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test("searches files through the V2 file API", async () => {
|
||||
const sdk = OpenCode.make({ baseUrl: "https://opencode.test" })
|
||||
const api = footer()
|
||||
const find = spyOn(sdk.file, "find").mockImplementation(
|
||||
() =>
|
||||
ok({
|
||||
location: { directory: "/tmp", project: { id: "proj_1", directory: "/tmp" } },
|
||||
data: [{ path: "src/index.ts", type: "file" }],
|
||||
}) as never,
|
||||
)
|
||||
|
||||
await runInteractiveMode(
|
||||
{
|
||||
sdk,
|
||||
directory: "/tmp",
|
||||
sessionID: "ses-files",
|
||||
resume: false,
|
||||
agent: "build",
|
||||
model: undefined,
|
||||
variant: undefined,
|
||||
files: [],
|
||||
thinking: false,
|
||||
backgroundSubagents: false,
|
||||
},
|
||||
{
|
||||
createRuntimeLifecycle: async (input) => {
|
||||
await expect(input.findFiles("index")).resolves.toEqual(["src/index.ts"])
|
||||
api.close()
|
||||
return {
|
||||
footer: api,
|
||||
onResize: () => () => {},
|
||||
refreshTheme: () => {},
|
||||
resetForReplay: () => Promise.resolve(),
|
||||
close: () => Promise.resolve(),
|
||||
}
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
expect(find).toHaveBeenCalledWith({ query: "index", type: "file", location: { directory: "/tmp" } })
|
||||
})
|
||||
|
||||
test("retains last-known-good state across failed coalesced refreshes and retries later", async () => {
|
||||
const sdk = new OpencodeClient()
|
||||
const sdk = OpenCode.make({ baseUrl: "https://opencode.test" })
|
||||
const refreshGate = defer<void>()
|
||||
let providerCalls = 0
|
||||
let modelCalls = 0
|
||||
@@ -453,7 +632,7 @@ describe("run interactive runtime", () => {
|
||||
let referenceCalls = 0
|
||||
const events: FooterEvent[] = []
|
||||
const api = footer(events)
|
||||
spyOn(sdk.v2.provider, "list").mockImplementation(async () => {
|
||||
spyOn(sdk.provider, "list").mockImplementation(async () => {
|
||||
providerCalls++
|
||||
if (providerCalls === 2) {
|
||||
await refreshGate.promise
|
||||
@@ -471,7 +650,7 @@ describe("run interactive runtime", () => {
|
||||
],
|
||||
}) as never
|
||||
})
|
||||
spyOn(sdk.v2.model, "list").mockImplementation(() => {
|
||||
spyOn(sdk.model, "list").mockImplementation(() => {
|
||||
modelCalls++
|
||||
return ok({
|
||||
location: { directory: "/tmp" },
|
||||
@@ -484,9 +663,7 @@ describe("run interactive runtime", () => {
|
||||
capabilities: { tools: true, input: ["text"], output: ["text"] },
|
||||
request: { headers: {}, body: {} },
|
||||
variants:
|
||||
modelCalls >= 4
|
||||
? []
|
||||
: [{ id: modelCalls >= 3 ? "high" : "low", settings: {}, headers: {}, body: {} }],
|
||||
modelCalls >= 4 ? [] : [{ id: modelCalls >= 3 ? "high" : "low", settings: {}, headers: {}, body: {} }],
|
||||
time: { released: 1 },
|
||||
cost: [{ input: 0, output: 0, cache: { read: 0, write: 0 } }],
|
||||
status: "active",
|
||||
@@ -496,7 +673,7 @@ describe("run interactive runtime", () => {
|
||||
],
|
||||
}) as never
|
||||
})
|
||||
spyOn(sdk.v2.agent, "list").mockImplementation(async () => {
|
||||
spyOn(sdk.agent, "list").mockImplementation(async () => {
|
||||
agentCalls++
|
||||
if (agentCalls === 2) throw new Error("agent refresh failed")
|
||||
return ok({
|
||||
@@ -504,7 +681,7 @@ describe("run interactive runtime", () => {
|
||||
data: [{ id: "build", description: agentCalls >= 3 ? "Refreshed agent" : "Agent", mode: "primary" }],
|
||||
}) as never
|
||||
})
|
||||
spyOn(sdk.v2.reference, "list").mockImplementation(() => {
|
||||
spyOn(sdk.reference, "list").mockImplementation(() => {
|
||||
referenceCalls++
|
||||
return ok({
|
||||
location: { directory: "/tmp" },
|
||||
@@ -513,12 +690,10 @@ describe("run interactive runtime", () => {
|
||||
],
|
||||
}) as never
|
||||
})
|
||||
spyOn(sdk.v2.command, "list").mockImplementation(() =>
|
||||
ok({ location: { directory: "/tmp" }, data: [{ name: "check", description: "Check" }] }) as never,
|
||||
)
|
||||
spyOn(sdk.v2.skill, "list").mockImplementation(() =>
|
||||
ok({ location: { directory: "/tmp" }, data: [] }) as never,
|
||||
spyOn(sdk.command, "list").mockImplementation(
|
||||
() => ok({ location: { directory: "/tmp" }, data: [{ name: "check", description: "Check" }] }) as never,
|
||||
)
|
||||
spyOn(sdk.skill, "list").mockImplementation(() => ok({ location: { directory: "/tmp" }, data: [] }) as never)
|
||||
let finalProviders: RunProvider[] = []
|
||||
let finalLimits: Record<string, number> = {}
|
||||
let retainedProviders: RunProvider[] = []
|
||||
|
||||
@@ -2,9 +2,9 @@ import { afterEach, expect, test } from "bun:test"
|
||||
import type { ToolPart } from "@opencode-ai/sdk/v2"
|
||||
import { RGBA, SyntaxStyle } from "@opentui/core"
|
||||
import { MockTreeSitterClient, createTestRenderer, type TestRenderer } from "@opentui/core/testing"
|
||||
import { RunScrollbackStream } from "@/cli/cmd/run/scrollback.surface"
|
||||
import { RUN_THEME_FALLBACK, type RunTheme } from "@/cli/cmd/run/theme"
|
||||
import type { StreamCommit } from "@/cli/cmd/run/types"
|
||||
import { RunScrollbackStream } from "@opencode-ai/cli/mini/scrollback.surface"
|
||||
import { RUN_THEME_FALLBACK, type RunTheme } from "@opencode-ai/cli/mini/theme"
|
||||
import type { StreamCommit } from "@opencode-ai/cli/mini/types"
|
||||
|
||||
type ClaimedCommit = {
|
||||
snapshot: {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import type { Event } from "@opencode-ai/sdk/v2"
|
||||
import { createSessionData, reduceSessionData } from "@/cli/cmd/run/session-data"
|
||||
import type { StreamCommit } from "@/cli/cmd/run/types"
|
||||
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({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { afterEach, describe, expect, mock, spyOn, test } from "bun:test"
|
||||
import { OpencodeClient } from "@opencode-ai/sdk/v2"
|
||||
import { OpenCode } from "@opencode-ai/client/promise"
|
||||
import {
|
||||
createSession,
|
||||
resolveCurrentSession,
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
sessionVariant,
|
||||
type RunSession,
|
||||
type SessionMessages,
|
||||
} from "@/cli/cmd/run/session.shared"
|
||||
} from "@opencode-ai/cli/mini/session.shared"
|
||||
|
||||
type Message = SessionMessages[number]
|
||||
type Part = Message["parts"][number]
|
||||
@@ -252,11 +252,10 @@ describe("run session shared", () => {
|
||||
})
|
||||
|
||||
test("restores current prompt history from stored text and file references", async () => {
|
||||
const client = new OpencodeClient()
|
||||
spyOn(client.v2.session, "messages").mockImplementation(() =>
|
||||
const client = OpenCode.make({ baseUrl: "https://opencode.test" })
|
||||
spyOn(client.message, "list").mockImplementation(() =>
|
||||
Promise.resolve({
|
||||
data: {
|
||||
data: [
|
||||
data: [
|
||||
{
|
||||
id: "msg_prompt",
|
||||
type: "user",
|
||||
@@ -272,32 +271,20 @@ describe("run session shared", () => {
|
||||
agents: [],
|
||||
time: { created: 1 },
|
||||
},
|
||||
],
|
||||
cursor: {},
|
||||
},
|
||||
error: undefined,
|
||||
request: new Request("https://opencode.test"),
|
||||
response: new Response(),
|
||||
],
|
||||
cursor: {},
|
||||
}),
|
||||
)
|
||||
spyOn(client.v2.session, "get").mockImplementation(() =>
|
||||
spyOn(client.session, "get").mockImplementation(() =>
|
||||
Promise.resolve({
|
||||
data: {
|
||||
data: {
|
||||
id: "ses_1",
|
||||
title: "Session",
|
||||
version: "dev",
|
||||
projectID: "proj_1",
|
||||
location: { directory: "/tmp", project: { id: "proj_1", 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" },
|
||||
},
|
||||
},
|
||||
error: undefined,
|
||||
request: new Request("https://opencode.test"),
|
||||
response: new Response(),
|
||||
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" },
|
||||
}),
|
||||
)
|
||||
|
||||
|
||||
@@ -2,12 +2,12 @@ import { afterEach, describe, expect, mock, spyOn, test } from "bun:test"
|
||||
import fs from "fs/promises"
|
||||
import path from "path"
|
||||
import { pathToFileURL } from "node:url"
|
||||
import { OpencodeClient, type V2Event } from "@opencode-ai/sdk/v2"
|
||||
import { createSessionTransport } from "@/cli/cmd/run/stream-v2.transport"
|
||||
import type { FooterApi, FooterEvent, StreamCommit } from "@/cli/cmd/run/types"
|
||||
import { OpenCode, type EventSubscribeOutput, type MessageListOutput, type OpenCodeClient } from "@opencode-ai/client/promise"
|
||||
import { createSessionTransport } from "@opencode-ai/cli/mini/stream-v2.transport"
|
||||
import type { FooterApi, FooterEvent, StreamCommit } from "@opencode-ai/cli/mini/types"
|
||||
import { tmpdir } from "../../fixture/fixture"
|
||||
|
||||
type RunV2Event = V2Event
|
||||
type RunV2Event = EventSubscribeOutput
|
||||
|
||||
function feed() {
|
||||
const values: RunV2Event[] = []
|
||||
@@ -41,16 +41,11 @@ function feed() {
|
||||
}
|
||||
|
||||
function ok<T>(data: T) {
|
||||
return Promise.resolve({
|
||||
data,
|
||||
error: undefined,
|
||||
request: new Request("https://opencode.test"),
|
||||
response: new Response(),
|
||||
})
|
||||
return Promise.resolve(data)
|
||||
}
|
||||
|
||||
function connected(id = "evt_connected") {
|
||||
return { id, created: 0, type: "server.connected", data: {} } satisfies RunV2Event
|
||||
return { id, type: "server.connected", data: {} } satisfies RunV2Event
|
||||
}
|
||||
|
||||
function durable(sessionID: string, seq = 0, version = 1) {
|
||||
@@ -85,9 +80,7 @@ function footer() {
|
||||
return { api, commits, events }
|
||||
}
|
||||
|
||||
type SessionMessages = NonNullable<
|
||||
Awaited<ReturnType<OpencodeClient["v2"]["session"]["messages"]>>["data"]
|
||||
>["data"][number][]
|
||||
type SessionMessages = MessageListOutput["data"]
|
||||
|
||||
function sdk(input: {
|
||||
streams: ReturnType<typeof feed>[]
|
||||
@@ -95,15 +88,10 @@ function sdk(input: {
|
||||
messages?: Record<string, SessionMessages>
|
||||
sessions?: Array<{ id: string; parentID?: string; title?: string; agent?: string; time: { updated: number } }>
|
||||
}) {
|
||||
const client = new OpencodeClient()
|
||||
const client = OpenCode.make({ baseUrl: "https://opencode.test" })
|
||||
let subscription = 0
|
||||
spyOn(client.v2.event, "subscribe").mockImplementation(
|
||||
() =>
|
||||
Promise.resolve({ stream: input.streams[subscription++]?.stream ?? feed().stream }) as ReturnType<
|
||||
typeof client.v2.event.subscribe
|
||||
>,
|
||||
)
|
||||
spyOn(client.v2.session, "messages").mockImplementation((request) =>
|
||||
spyOn(client.event, "subscribe").mockImplementation(() => input.streams[subscription++]?.stream ?? feed().stream)
|
||||
spyOn(client.message, "list").mockImplementation((request) =>
|
||||
ok({
|
||||
data: input.messages?.[request.sessionID] ?? [
|
||||
{
|
||||
@@ -118,14 +106,14 @@ function sdk(input: {
|
||||
cursor: {},
|
||||
}),
|
||||
)
|
||||
spyOn(client.v2.session.permission, "list").mockImplementation(() => ok({ data: [] }))
|
||||
spyOn(client.v2.session.question, "list").mockImplementation(() => ok({ data: [] }))
|
||||
spyOn(client.v2.session, "active").mockImplementation(() => ok({ data: input.active?.() ?? {}, watermarks: {} }))
|
||||
spyOn(client.v2.session, "switchAgent").mockImplementation(() => ok(undefined))
|
||||
spyOn(client.v2.session, "switchModel").mockImplementation(() => ok(undefined))
|
||||
spyOn(client.permission, "list").mockImplementation(() => ok([]))
|
||||
spyOn(client.question, "list").mockImplementation(() => ok([]))
|
||||
spyOn(client.session, "active").mockImplementation(() => ok({ data: input.active?.() ?? {}, watermarks: {} }))
|
||||
spyOn(client.session, "switchAgent").mockImplementation(() => ok(undefined))
|
||||
spyOn(client.session, "switchModel").mockImplementation(() => ok(undefined))
|
||||
// The generated methods have conditional return types for throwOnError; the
|
||||
// minimal shapes below are enough for family discovery and model fallback.
|
||||
spyOn(client.v2.session, "list").mockImplementation((request) => {
|
||||
spyOn(client.session, "list").mockImplementation((request) => {
|
||||
const parentID = request?.parentID
|
||||
return ok({
|
||||
location: { directory: "/tmp", project: { id: "proj_1", directory: "/tmp" } },
|
||||
@@ -139,7 +127,7 @@ function sdk(input: {
|
||||
) ?? [],
|
||||
}) as never
|
||||
})
|
||||
spyOn(client.v2.model, "default").mockImplementation(
|
||||
spyOn(client.model, "default").mockImplementation(
|
||||
() =>
|
||||
ok({
|
||||
location: { directory: "/tmp", project: { id: "proj_1", directory: "/tmp" } },
|
||||
@@ -170,9 +158,7 @@ describe("V2 mini transport", () => {
|
||||
expect(ui.commits.map((item) => item.text)).toEqual(["previous prompt"])
|
||||
|
||||
let admitted = false
|
||||
// The generated method has conditional return types for throwOnError; this mock represents the successful branch.
|
||||
// @ts-expect-error successful SDK response is valid for both modes at runtime
|
||||
spyOn(client.v2.session, "prompt").mockImplementation((request) => {
|
||||
spyOn(client.session, "prompt").mockImplementation((request) => {
|
||||
const messageID = request.id ?? "msg_prompt"
|
||||
const prompt = request.prompt ?? { text: "" }
|
||||
admitted = true
|
||||
@@ -185,7 +171,7 @@ describe("V2 mini transport", () => {
|
||||
delivery: "steer" as const,
|
||||
timeCreated: 2,
|
||||
},
|
||||
})
|
||||
}) as never
|
||||
})
|
||||
|
||||
const turn = transport.runPromptTurn({
|
||||
@@ -250,10 +236,8 @@ describe("V2 mini transport", () => {
|
||||
limits: () => ({}),
|
||||
footer: ui.api,
|
||||
})
|
||||
let request: Parameters<OpencodeClient["v2"]["session"]["prompt"]>[0] | undefined
|
||||
// The generated method has conditional return types for throwOnError; this mock represents the successful branch.
|
||||
// @ts-expect-error successful SDK response is valid for both modes at runtime
|
||||
spyOn(client.v2.session, "prompt").mockImplementation((input) => {
|
||||
let request: Parameters<OpenCodeClient["session"]["prompt"]>[0] | undefined
|
||||
spyOn(client.session, "prompt").mockImplementation((input) => {
|
||||
request = input
|
||||
queueMicrotask(() => {
|
||||
events.push({
|
||||
@@ -282,7 +266,7 @@ describe("V2 mini transport", () => {
|
||||
delivery: "steer" as const,
|
||||
timeCreated: 2,
|
||||
},
|
||||
})
|
||||
}) as never
|
||||
})
|
||||
|
||||
await transport.runPromptTurn({
|
||||
@@ -344,10 +328,10 @@ describe("V2 mini transport", () => {
|
||||
limits: () => ({}),
|
||||
footer: ui.api,
|
||||
})
|
||||
let request: Parameters<OpencodeClient["v2"]["session"]["prompt"]>[0] | undefined
|
||||
let request: Parameters<OpenCodeClient["session"]["prompt"]>[0] | undefined
|
||||
// The generated method has conditional return types for throwOnError; this mock represents the successful branch.
|
||||
// @ts-expect-error successful SDK response is valid for both modes at runtime
|
||||
spyOn(client.v2.session, "prompt").mockImplementation((input) => {
|
||||
spyOn(client.session, "prompt").mockImplementation((input) => {
|
||||
request = input
|
||||
queueMicrotask(() => {
|
||||
events.push({
|
||||
@@ -441,10 +425,10 @@ describe("V2 mini transport", () => {
|
||||
limits: () => ({}),
|
||||
footer: ui.api,
|
||||
})
|
||||
let request: Parameters<OpencodeClient["v2"]["session"]["prompt"]>[0] | undefined
|
||||
let request: Parameters<OpenCodeClient["session"]["prompt"]>[0] | undefined
|
||||
// The generated method has conditional return types for throwOnError; this mock represents the successful branch.
|
||||
// @ts-expect-error successful SDK response is valid for both modes at runtime
|
||||
spyOn(client.v2.session, "prompt").mockImplementation((input) => {
|
||||
spyOn(client.session, "prompt").mockImplementation((input) => {
|
||||
request = input
|
||||
queueMicrotask(() => {
|
||||
events.push({
|
||||
@@ -561,7 +545,7 @@ describe("V2 mini transport", () => {
|
||||
},
|
||||
})
|
||||
let projected = false
|
||||
spyOn(client.v2.session, "messages").mockImplementation(() =>
|
||||
spyOn(client.message, "list").mockImplementation(() =>
|
||||
ok({
|
||||
data: projected
|
||||
? [
|
||||
@@ -589,7 +573,7 @@ describe("V2 mini transport", () => {
|
||||
let admitted = false
|
||||
// The generated method has conditional return types for throwOnError; this mock represents the successful branch.
|
||||
// @ts-expect-error successful SDK response is valid for both modes at runtime
|
||||
spyOn(client.v2.session, "prompt").mockImplementation((request) => {
|
||||
spyOn(client.session, "prompt").mockImplementation((request) => {
|
||||
const messageID = request.id ?? "msg_prompt"
|
||||
const prompt = request.prompt ?? { text: "" }
|
||||
admitted = true
|
||||
@@ -632,7 +616,7 @@ describe("V2 mini transport", () => {
|
||||
return active
|
||||
},
|
||||
})
|
||||
spyOn(client.v2.session, "messages").mockImplementation(() =>
|
||||
spyOn(client.message, "list").mockImplementation(() =>
|
||||
ok({
|
||||
data: projected
|
||||
? [
|
||||
@@ -661,7 +645,7 @@ describe("V2 mini transport", () => {
|
||||
let admitted = false
|
||||
// The generated method has conditional return types for throwOnError; this mock represents the successful branch.
|
||||
// @ts-expect-error successful SDK response is valid for both modes at runtime
|
||||
spyOn(client.v2.session, "prompt").mockImplementation((request) => {
|
||||
spyOn(client.session, "prompt").mockImplementation((request) => {
|
||||
const messageID = request.id ?? "msg_prompt"
|
||||
const prompt = request.prompt ?? { text: "" }
|
||||
admitted = true
|
||||
@@ -701,7 +685,7 @@ describe("V2 mini transport", () => {
|
||||
limits: () => ({}),
|
||||
footer: ui.api,
|
||||
})
|
||||
spyOn(client.v2.session, "messages").mockImplementation(() =>
|
||||
spyOn(client.message, "list").mockImplementation(() =>
|
||||
ok({
|
||||
data: [
|
||||
{
|
||||
@@ -745,7 +729,7 @@ describe("V2 mini transport", () => {
|
||||
const events = feed()
|
||||
events.push(connected())
|
||||
const client = sdk({ streams: [events] })
|
||||
spyOn(client.v2.session, "messages").mockImplementation(() =>
|
||||
spyOn(client.message, "list").mockImplementation(() =>
|
||||
ok({
|
||||
data: [
|
||||
{
|
||||
@@ -842,7 +826,7 @@ describe("V2 mini transport", () => {
|
||||
let admitted = false
|
||||
// The generated method has conditional return types for throwOnError; this mock represents the successful branch.
|
||||
// @ts-expect-error successful SDK response is valid for both modes at runtime
|
||||
spyOn(client.v2.session, "prompt").mockImplementation((request) => {
|
||||
spyOn(client.session, "prompt").mockImplementation((request) => {
|
||||
const messageID = request.id ?? "msg_prompt"
|
||||
const prompt = request.prompt ?? { text: "" }
|
||||
admitted = true
|
||||
@@ -850,7 +834,7 @@ describe("V2 mini transport", () => {
|
||||
data: { admittedSeq: 1, id: messageID, sessionID: "ses_1", prompt, delivery: "steer" as const, timeCreated: 2 },
|
||||
})
|
||||
})
|
||||
const interrupted = spyOn(client.v2.session, "interrupt").mockImplementation(() => ok(undefined))
|
||||
const interrupted = spyOn(client.session, "interrupt").mockImplementation(() => ok(undefined))
|
||||
|
||||
const turn = transport.runPromptTurn({
|
||||
agent: undefined,
|
||||
@@ -886,21 +870,19 @@ describe("V2 mini transport", () => {
|
||||
limits: () => ({}),
|
||||
footer: ui.api,
|
||||
})
|
||||
// The generated method has conditional return types for throwOnError; the test only needs the nested model field.
|
||||
// @ts-expect-error minimal session shape is enough for this lookup
|
||||
spyOn(client.v2.session, "get").mockImplementation(() => ok({ data: { model: undefined } }))
|
||||
spyOn(client.v2.model, "default").mockImplementation(
|
||||
spyOn(client.session, "get").mockImplementation(() => ok({ model: undefined }) as never)
|
||||
spyOn(client.model, "default").mockImplementation(
|
||||
() =>
|
||||
ok({
|
||||
location: { directory: "/tmp", project: { id: "proj_1", directory: "/tmp" } },
|
||||
data: { id: "gpt-5", providerID: "openai" },
|
||||
}) as never,
|
||||
)
|
||||
const switched = spyOn(client.v2.session, "switchModel").mockImplementation(() => ok(undefined))
|
||||
const switched = spyOn(client.session, "switchModel").mockImplementation(() => ok(undefined))
|
||||
let admitted = false
|
||||
// The generated method has conditional return types for throwOnError; this mock represents the successful branch.
|
||||
// @ts-expect-error successful SDK response is valid for both modes at runtime
|
||||
spyOn(client.v2.session, "prompt").mockImplementation((request) => {
|
||||
spyOn(client.session, "prompt").mockImplementation((request) => {
|
||||
const messageID = request.id ?? "msg_prompt"
|
||||
const prompt = request.prompt ?? { text: "" }
|
||||
admitted = true
|
||||
@@ -938,7 +920,7 @@ describe("V2 mini transport", () => {
|
||||
|
||||
expect(switched).toHaveBeenCalledWith(
|
||||
{ sessionID: "ses_1", model: { providerID: "openai", id: "gpt-5", variant: "high" } },
|
||||
expect.objectContaining({ throwOnError: true }),
|
||||
{ signal: undefined },
|
||||
)
|
||||
await transport.close()
|
||||
})
|
||||
@@ -958,7 +940,7 @@ describe("V2 mini transport", () => {
|
||||
let admitted = false
|
||||
// The generated method has conditional return types for throwOnError; this mock represents the successful branch.
|
||||
// @ts-expect-error successful SDK response is valid for both modes at runtime
|
||||
spyOn(client.v2.session, "prompt").mockImplementation((request) => {
|
||||
spyOn(client.session, "prompt").mockImplementation((request) => {
|
||||
const messageID = request.id ?? "msg_prompt"
|
||||
const prompt = request.prompt ?? { text: "" }
|
||||
admitted = true
|
||||
@@ -966,7 +948,7 @@ describe("V2 mini transport", () => {
|
||||
data: { admittedSeq: 1, id: messageID, sessionID: "ses_1", prompt, delivery: "steer" as const, timeCreated: 2 },
|
||||
})
|
||||
})
|
||||
const interrupted = spyOn(client.v2.session, "interrupt").mockImplementation(() => ok(undefined))
|
||||
const interrupted = spyOn(client.session, "interrupt").mockImplementation(() => ok(undefined))
|
||||
const controller = new AbortController()
|
||||
const turn = transport.runPromptTurn({
|
||||
agent: undefined,
|
||||
@@ -1014,8 +996,8 @@ describe("V2 mini transport", () => {
|
||||
limits: () => ({}),
|
||||
footer: ui.api,
|
||||
})
|
||||
let request: Parameters<OpencodeClient["v2"]["session"]["shell"]>[0] | undefined
|
||||
spyOn(client.v2.session, "shell").mockImplementation((input) => {
|
||||
let request: Parameters<OpenCodeClient["session"]["shell"]>[0] | undefined
|
||||
spyOn(client.session, "shell").mockImplementation((input) => {
|
||||
request = input
|
||||
queueMicrotask(() => {
|
||||
events.push({
|
||||
@@ -1094,7 +1076,7 @@ describe("V2 mini transport", () => {
|
||||
})
|
||||
let started = false
|
||||
let aborted = false
|
||||
spyOn(client.v2.session, "shell").mockImplementation(
|
||||
spyOn(client.session, "shell").mockImplementation(
|
||||
(_input, options) =>
|
||||
new Promise((_, reject) => {
|
||||
started = true
|
||||
@@ -1104,7 +1086,7 @@ describe("V2 mini transport", () => {
|
||||
})
|
||||
}) as never,
|
||||
)
|
||||
const interrupted = spyOn(client.v2.session, "interrupt").mockImplementation(() => ok(undefined))
|
||||
const interrupted = spyOn(client.session, "interrupt").mockImplementation(() => ok(undefined))
|
||||
|
||||
const turn = transport.runPromptTurn({
|
||||
agent: undefined,
|
||||
@@ -1135,9 +1117,9 @@ describe("V2 mini transport", () => {
|
||||
limits: () => ({}),
|
||||
footer: ui.api,
|
||||
})
|
||||
let request: Parameters<OpencodeClient["v2"]["session"]["shell"]>[0] | undefined
|
||||
let request: Parameters<OpenCodeClient["session"]["shell"]>[0] | undefined
|
||||
let complete!: () => void
|
||||
spyOn(client.v2.session, "shell").mockImplementation((input) => {
|
||||
spyOn(client.session, "shell").mockImplementation((input) => {
|
||||
request = input
|
||||
return new Promise<void>((resolve) => {
|
||||
complete = resolve
|
||||
@@ -1414,8 +1396,8 @@ describe("V2 mini transport", () => {
|
||||
limits: () => ({}),
|
||||
footer: ui.api,
|
||||
})
|
||||
let request: Parameters<OpencodeClient["v2"]["session"]["command"]>[0] | undefined
|
||||
spyOn(client.v2.session, "command").mockImplementation((input) => {
|
||||
let request: Parameters<OpenCodeClient["session"]["command"]>[0] | undefined
|
||||
spyOn(client.session, "command").mockImplementation((input) => {
|
||||
request = input
|
||||
queueMicrotask(() => {
|
||||
events.push({
|
||||
@@ -1436,14 +1418,12 @@ describe("V2 mini transport", () => {
|
||||
})
|
||||
})
|
||||
return ok({
|
||||
data: {
|
||||
admittedSeq: 1,
|
||||
id: input.id ?? "msg_cmd",
|
||||
sessionID: "ses_1",
|
||||
prompt: { text: "evaluated template" },
|
||||
delivery: "steer" as const,
|
||||
timeCreated: 2,
|
||||
},
|
||||
admittedSeq: 1,
|
||||
id: input.id ?? "msg_cmd",
|
||||
sessionID: "ses_1",
|
||||
prompt: { text: "evaluated template" },
|
||||
delivery: "steer" as const,
|
||||
timeCreated: 2,
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1471,8 +1451,8 @@ describe("V2 mini transport", () => {
|
||||
delivery: "steer",
|
||||
})
|
||||
// Selection rides the command payload; no separate client-side switch.
|
||||
expect(client.v2.session.switchAgent).not.toHaveBeenCalled()
|
||||
expect(client.v2.session.switchModel).not.toHaveBeenCalled()
|
||||
expect(client.session.switchAgent).not.toHaveBeenCalled()
|
||||
expect(client.session.switchModel).not.toHaveBeenCalled()
|
||||
await transport.close()
|
||||
})
|
||||
|
||||
@@ -1488,10 +1468,10 @@ describe("V2 mini transport", () => {
|
||||
limits: () => ({}),
|
||||
footer: ui.api,
|
||||
})
|
||||
let request: Parameters<OpencodeClient["v2"]["session"]["skill"]>[0] | undefined
|
||||
const command = spyOn(client.v2.session, "command")
|
||||
const prompt = spyOn(client.v2.session, "prompt")
|
||||
spyOn(client.v2.session, "skill").mockImplementation((input) => {
|
||||
let request: Parameters<OpenCodeClient["session"]["skill"]>[0] | undefined
|
||||
const command = spyOn(client.session, "command")
|
||||
const prompt = spyOn(client.session, "prompt")
|
||||
spyOn(client.session, "skill").mockImplementation((input) => {
|
||||
request = input
|
||||
queueMicrotask(() => {
|
||||
events.push({
|
||||
@@ -1551,7 +1531,7 @@ describe("V2 mini transport", () => {
|
||||
footer: ui.api,
|
||||
})
|
||||
let sent = false
|
||||
spyOn(client.v2.session, "skill").mockImplementation(() => {
|
||||
spyOn(client.session, "skill").mockImplementation(() => {
|
||||
sent = true
|
||||
return ok(undefined) as never
|
||||
})
|
||||
@@ -1721,20 +1701,18 @@ describe("V2 mini transport", () => {
|
||||
],
|
||||
},
|
||||
})
|
||||
spyOn(client.v2.session, "get").mockImplementation(() =>
|
||||
spyOn(client.session, "get").mockImplementation(() =>
|
||||
ok({
|
||||
data: {
|
||||
id: "ses_child",
|
||||
parentID: "ses_1",
|
||||
projectID: "proj_1",
|
||||
agent: "explore",
|
||||
cost: 0,
|
||||
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
|
||||
time: { created: 1, updated: 1 },
|
||||
title: "Find files",
|
||||
location: { directory: "/tmp", project: { id: "proj_1", directory: "/tmp" } },
|
||||
},
|
||||
}),
|
||||
id: "ses_child",
|
||||
parentID: "ses_1",
|
||||
projectID: "proj_1",
|
||||
agent: "explore",
|
||||
cost: 0,
|
||||
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
|
||||
time: { created: 1, updated: 1 },
|
||||
title: "Find files",
|
||||
location: { directory: "/tmp" },
|
||||
}) as never,
|
||||
)
|
||||
const ui = footer()
|
||||
const transport = await createSessionTransport({
|
||||
@@ -1857,7 +1835,7 @@ describe("V2 mini transport", () => {
|
||||
const hydration = new Promise<void>((resolve) => {
|
||||
releaseHydration = resolve
|
||||
})
|
||||
spyOn(client.v2.session, "messages").mockImplementation(async (request) => {
|
||||
spyOn(client.message, "list").mockImplementation(async (request) => {
|
||||
if (request.sessionID === "ses_child") {
|
||||
childHydrating = true
|
||||
await hydration
|
||||
@@ -1927,7 +1905,7 @@ describe("V2 mini transport", () => {
|
||||
const retry = new Promise<void>((resolve) => {
|
||||
releaseRetry = resolve
|
||||
})
|
||||
spyOn(client.v2.session, "messages").mockImplementation(async (request) => {
|
||||
spyOn(client.message, "list").mockImplementation(async (request) => {
|
||||
if (request.sessionID !== "ses_child") return ok({ data: [], cursor: {} })
|
||||
childRequests++
|
||||
if (childRequests === 1) {
|
||||
@@ -2006,7 +1984,7 @@ describe("V2 mini transport", () => {
|
||||
const hydration = new Promise<void>((resolve) => {
|
||||
releaseHydration = resolve
|
||||
})
|
||||
spyOn(client.v2.session, "messages").mockImplementation(async (request) => {
|
||||
spyOn(client.message, "list").mockImplementation(async (request) => {
|
||||
if (request.sessionID !== "ses_child") return ok({ data: [], cursor: {} })
|
||||
childHydrating = true
|
||||
await hydration
|
||||
@@ -2119,21 +2097,19 @@ describe("V2 mini transport", () => {
|
||||
const gate = new Promise<void>((resolve) => {
|
||||
resolveGet = resolve
|
||||
})
|
||||
spyOn(client.v2.session, "get").mockImplementation(async () => {
|
||||
spyOn(client.session, "get").mockImplementation(async () => {
|
||||
await gate
|
||||
return ok({
|
||||
data: {
|
||||
id: "ses_child",
|
||||
parentID: "ses_1",
|
||||
projectID: "proj_1",
|
||||
agent: "explore",
|
||||
cost: 0,
|
||||
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
|
||||
time: { created: 1, updated: 1 },
|
||||
title: "Find files",
|
||||
location: { directory: "/tmp", project: { id: "proj_1", directory: "/tmp" } },
|
||||
},
|
||||
})
|
||||
id: "ses_child",
|
||||
parentID: "ses_1",
|
||||
projectID: "proj_1",
|
||||
agent: "explore",
|
||||
cost: 0,
|
||||
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
|
||||
time: { created: 1, updated: 1 },
|
||||
title: "Find files",
|
||||
location: { directory: "/tmp" },
|
||||
}) as never
|
||||
})
|
||||
const ui = footer()
|
||||
const transport = await createSessionTransport({
|
||||
@@ -2178,21 +2154,19 @@ describe("V2 mini transport", () => {
|
||||
const gate = new Promise<void>((resolve) => {
|
||||
resolveGet = resolve
|
||||
})
|
||||
spyOn(client.v2.session, "get").mockImplementation(async () => {
|
||||
spyOn(client.session, "get").mockImplementation(async () => {
|
||||
await gate
|
||||
return ok({
|
||||
data: {
|
||||
id: "ses_child",
|
||||
parentID: "ses_1",
|
||||
projectID: "proj_1",
|
||||
agent: "explore",
|
||||
cost: 0,
|
||||
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
|
||||
time: { created: 1, updated: 1 },
|
||||
title: "Find files",
|
||||
location: { directory: "/tmp", project: { id: "proj_1", directory: "/tmp" } },
|
||||
},
|
||||
})
|
||||
id: "ses_child",
|
||||
parentID: "ses_1",
|
||||
projectID: "proj_1",
|
||||
agent: "explore",
|
||||
cost: 0,
|
||||
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
|
||||
time: { created: 1, updated: 1 },
|
||||
title: "Find files",
|
||||
location: { directory: "/tmp" },
|
||||
}) as never
|
||||
})
|
||||
const ui = footer()
|
||||
const transport = await createSessionTransport({
|
||||
@@ -2286,10 +2260,7 @@ describe("V2 mini transport", () => {
|
||||
footer: ui.api,
|
||||
})
|
||||
const states = ui.events.flatMap((event) => (event.type === "stream.subagent" ? [event.state] : []))
|
||||
expect(client.v2.session.list).toHaveBeenCalledWith(
|
||||
{ parentID: "ses_1", limit: 100, order: "desc" },
|
||||
{ throwOnError: true },
|
||||
)
|
||||
expect(client.session.list).toHaveBeenCalledWith({ parentID: "ses_1", limit: 100, order: "desc" })
|
||||
expect(states.at(-1)?.tabs).toMatchObject([
|
||||
{
|
||||
sessionID: "ses_child_old",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { writeSessionOutput } from "@/cli/cmd/run/stream"
|
||||
import type { FooterApi, FooterEvent, StreamCommit } from "@/cli/cmd/run/types"
|
||||
import { writeSessionOutput } from "@opencode-ai/cli/mini/stream"
|
||||
import type { FooterApi, FooterEvent, StreamCommit } from "@opencode-ai/cli/mini/types"
|
||||
|
||||
function footer() {
|
||||
const events: FooterEvent[] = []
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect, test } from "bun:test"
|
||||
import { RGBA, type CliRenderer, type TerminalColors } from "@opentui/core"
|
||||
import { RUN_THEME_FALLBACK, generateSystem, resolveRunTheme, resolveTheme } from "@/cli/cmd/run/theme"
|
||||
import { RUN_THEME_FALLBACK, generateSystem, resolveRunTheme, resolveTheme } from "@opencode-ai/cli/mini/theme"
|
||||
|
||||
const palette = ["#15161e", "#f7768e", "#9ece6a", "#e0af68", "#7aa2f7", "#bb9af7", "#7dcfff", "#c0caf5"] as const
|
||||
|
||||
|
||||
@@ -10,9 +10,9 @@ import {
|
||||
formatModelLabel,
|
||||
pickVariant,
|
||||
resolveVariant,
|
||||
} from "@/cli/cmd/run/variant.shared"
|
||||
import type { SessionMessages } from "@/cli/cmd/run/session.shared"
|
||||
import type { RunProvider } from "@/cli/cmd/run/types"
|
||||
} from "@opencode-ai/cli/mini/variant.shared"
|
||||
import type { SessionMessages } from "@opencode-ai/cli/mini/session.shared"
|
||||
import type { RunProvider } from "@opencode-ai/cli/mini/types"
|
||||
import { testEffect } from "../../lib/effect"
|
||||
|
||||
const model = {
|
||||
|
||||
@@ -4,7 +4,6 @@ import fs from "fs/promises"
|
||||
import path from "path"
|
||||
import yargs from "yargs"
|
||||
import { tmpdir } from "../../fixture/fixture"
|
||||
import { MiniLocalCommand } from "../../../src/cli/cmd/mini"
|
||||
import { TuiThreadCommand, resolveThreadDirectory } from "../../../src/cli/cmd/tui"
|
||||
import { cliIt } from "../../lib/cli-process"
|
||||
|
||||
@@ -38,7 +37,7 @@ describe("tui thread", () => {
|
||||
await check(".")
|
||||
})
|
||||
|
||||
test("resolves a relative mini project from PWD when cwd differs", async () => {
|
||||
test("resolves a relative project from PWD when cwd differs", async () => {
|
||||
await using pwd = await tmpdir({ git: true })
|
||||
await using cwd = await tmpdir({ git: true })
|
||||
|
||||
@@ -46,18 +45,6 @@ describe("tui thread", () => {
|
||||
expect(resolveThreadDirectory(undefined, pwd.path, cwd.path)).toBe(cwd.path)
|
||||
})
|
||||
|
||||
test("parses supported mini --no-replay forms", async () => {
|
||||
for (const option of ["--no-replay", "--no-replay=true", "--noReplay"]) {
|
||||
const args = await yargs([])
|
||||
.command({ ...MiniLocalCommand, handler: () => {} })
|
||||
.exitProcess(false)
|
||||
.parse([option, "--replay-limit", "10"])
|
||||
|
||||
expect(args.replay === false || args.noReplay === true).toBe(true)
|
||||
expect(args.replayLimit).toBe(10)
|
||||
}
|
||||
})
|
||||
|
||||
test("preserves boolean negation for existing options", async () => {
|
||||
const args = await yargs([])
|
||||
.command({ ...TuiThreadCommand, handler: () => {} })
|
||||
@@ -85,24 +72,6 @@ describe("tui thread", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
cliIt.live("routes local sessions through mini", ({ opencode }) =>
|
||||
Effect.gen(function* () {
|
||||
const result = yield* opencode.spawn(["mini"])
|
||||
|
||||
opencode.expectExit(result, 1)
|
||||
expect(result.stderr).toContain("opencode mini requires a TTY stdout")
|
||||
}),
|
||||
)
|
||||
|
||||
cliIt.live("routes attached sessions through mini attach", ({ opencode }) =>
|
||||
Effect.gen(function* () {
|
||||
const result = yield* opencode.spawn(["mini", "attach", "http://127.0.0.1:1"])
|
||||
|
||||
opencode.expectExit(result, 1)
|
||||
expect(result.stderr).toContain("opencode mini requires a TTY stdout")
|
||||
}),
|
||||
)
|
||||
|
||||
cliIt.live("rejects removed attach mini alias", ({ opencode }) =>
|
||||
Effect.gen(function* () {
|
||||
const result = yield* opencode.spawn(["attach", "http://127.0.0.1:1", "--mini"])
|
||||
|
||||
Reference in New Issue
Block a user