mirror of
https://github.com/Heretek-AI/opencode.git
synced 2026-11-10 10:21:31 -10:00
203 lines
7.7 KiB
TypeScript
203 lines
7.7 KiB
TypeScript
import { describe, expect, test } from "bun:test"
|
|
import type { SessionConfigOption } from "@agentclientprotocol/sdk"
|
|
import { currentValue, selectValues } from "./select-options"
|
|
import {
|
|
durableEvent,
|
|
ephemeralEvent,
|
|
secondModel,
|
|
startSession,
|
|
startWire,
|
|
stepEnded,
|
|
testModel,
|
|
turn,
|
|
type Wire,
|
|
} from "./wire-fixture"
|
|
|
|
describe("acp follows model and agent selections from other clients", () => {
|
|
test("pushes an external model switch with its variant and uses it for the next prompt", async () => {
|
|
await using acp = await startSession({
|
|
onPrompt: ({ sessionID, id }) => turn(sessionID, id, stepEnded(sessionID, "msg_assistant")),
|
|
})
|
|
|
|
acp.server.send(
|
|
durableEvent("session.model.selected", {
|
|
sessionID: acp.sessionId,
|
|
model: { providerID: "test", id: secondModel.id, variant: "low" },
|
|
previous: { providerID: "test", id: "test-model" },
|
|
}),
|
|
)
|
|
const [options] = await optionUpdates(acp, acp.sessionId, 1)
|
|
await acp.prompt(acp.sessionId, "hello")
|
|
|
|
expect(values(options)).toEqual({ model: "test/second-model", effort: "low", mode: "build" })
|
|
expect(await acp.waitForUpdate((item) => item.update.sessionUpdate === "usage_update")).toMatchObject({
|
|
update: { size: secondModel.limit.context },
|
|
})
|
|
})
|
|
|
|
test("shows the default effort when the external switch carries no variant", async () => {
|
|
await using acp = await startSession()
|
|
await acp.request("session/set_config_option", { sessionId: acp.sessionId, configId: "effort", value: "high" })
|
|
|
|
acp.server.send(
|
|
durableEvent("session.model.selected", {
|
|
sessionID: acp.sessionId,
|
|
model: { providerID: "test", id: secondModel.id },
|
|
}),
|
|
)
|
|
|
|
expect(values((await optionUpdates(acp, acp.sessionId, 1))[0])).toEqual({
|
|
model: "test/second-model",
|
|
effort: "default",
|
|
mode: "build",
|
|
})
|
|
})
|
|
|
|
test("pushes an external agent switch as the mode", async () => {
|
|
await using acp = await startSession()
|
|
|
|
acp.server.send(
|
|
durableEvent("session.agent.selected", { sessionID: acp.sessionId, agent: "plan", previous: "build" }),
|
|
)
|
|
|
|
expect(values((await optionUpdates(acp, acp.sessionId, 1))[0])).toEqual({
|
|
model: "test/test-model",
|
|
effort: "default",
|
|
mode: "plan",
|
|
})
|
|
})
|
|
|
|
test("does not echo its own model and mode switches", async () => {
|
|
await using acp = await startSession({
|
|
// The server publishes the selection event before it answers the switch.
|
|
fetch: (request) => {
|
|
if (request.method !== "POST") return undefined
|
|
if (request.path.endsWith("/model"))
|
|
acp.server.send(
|
|
durableEvent("session.model.selected", {
|
|
sessionID: acp.sessionId,
|
|
model: { providerID: "test", id: secondModel.id, variant: "medium" },
|
|
}),
|
|
)
|
|
if (request.path.endsWith("/agent"))
|
|
acp.server.send(durableEvent("session.agent.selected", { sessionID: acp.sessionId, agent: "plan" }))
|
|
return undefined
|
|
},
|
|
})
|
|
const set = (configId: string, value: string) =>
|
|
acp.request("session/set_config_option", { sessionId: acp.sessionId, configId, value })
|
|
|
|
await set("model", "test/second-model/medium")
|
|
await acp.request("session/set_mode", { sessionId: acp.sessionId, modeId: "plan" })
|
|
acp.server.send(durableEvent("session.agent.selected", { sessionID: acp.sessionId, agent: "build" }))
|
|
const updates = await optionUpdates(acp, acp.sessionId, 1)
|
|
|
|
expect(updates.map(values)).toEqual([{ model: "test/second-model", effort: "medium", mode: "build" }])
|
|
})
|
|
|
|
test("ignores selections for other sessions", async () => {
|
|
await using acp = await startSession()
|
|
const other = await acp.newSession()
|
|
|
|
acp.server.send(durableEvent("session.agent.selected", { sessionID: "ses_unattached", agent: "plan" }))
|
|
acp.server.send(durableEvent("session.agent.selected", { sessionID: other.sessionId, agent: "plan" }))
|
|
acp.server.send(
|
|
durableEvent("session.model.selected", {
|
|
sessionID: acp.sessionId,
|
|
model: { providerID: "test", id: secondModel.id },
|
|
}),
|
|
)
|
|
const updates = await optionUpdates(acp, acp.sessionId, 1)
|
|
|
|
expect(updates.map(values)).toEqual([{ model: "test/second-model", effort: "default", mode: "build" }])
|
|
})
|
|
|
|
test("stops following a closed session and follows it once after re-attaching", async () => {
|
|
await using acp = await startSession()
|
|
const other = await acp.newSession()
|
|
await acp.request("session/close", { sessionId: acp.sessionId })
|
|
|
|
acp.server.send(durableEvent("session.agent.selected", { sessionID: acp.sessionId, agent: "plan" }))
|
|
acp.server.send(durableEvent("session.agent.selected", { sessionID: other.sessionId, agent: "plan" }))
|
|
await optionUpdates(acp, other.sessionId, 1)
|
|
expect(configUpdates(acp, acp.sessionId)).toEqual([])
|
|
|
|
const stored = acp.server.sessions.get(acp.sessionId)
|
|
if (!stored) throw new Error(`missing stored session ${acp.sessionId}`)
|
|
acp.server.sessions.set(acp.sessionId, { ...stored, agent: "plan" })
|
|
const resume = () => acp.request("session/resume", { sessionId: acp.sessionId, cwd: "/workspace" })
|
|
const resumed = await resume()
|
|
await resume()
|
|
acp.server.send(
|
|
durableEvent("session.model.selected", {
|
|
sessionID: acp.sessionId,
|
|
model: { providerID: "test", id: secondModel.id },
|
|
}),
|
|
)
|
|
await optionUpdates(acp, acp.sessionId, 1)
|
|
acp.server.send(durableEvent("session.agent.selected", { sessionID: acp.sessionId, agent: "build" }))
|
|
await optionUpdates(acp, acp.sessionId, 2)
|
|
|
|
expect(currentValue(resumed, "mode")).toBe("plan")
|
|
expect(configUpdates(acp, acp.sessionId).map(values)).toEqual([
|
|
{ model: "test/second-model", effort: "default", mode: "plan" },
|
|
{ model: "test/second-model", effort: "default", mode: "build" },
|
|
])
|
|
})
|
|
|
|
test("ends on the latest catalog and selection when both change together", async () => {
|
|
await using acp = await startWire()
|
|
acp.server.catalog.models = [testModel]
|
|
await acp.initialize()
|
|
const { sessionId } = await acp.newSession()
|
|
|
|
acp.server.catalog.models = [testModel, secondModel]
|
|
acp.server.send(
|
|
ephemeralEvent("model.updated", {}),
|
|
durableEvent("session.model.selected", {
|
|
sessionID: sessionId,
|
|
model: { providerID: "test", id: secondModel.id },
|
|
}),
|
|
)
|
|
await acp.until(
|
|
() => configUpdates(acp, sessionId).some((options) => selectValues(options, "model").length === 2),
|
|
"the reloaded catalog",
|
|
)
|
|
acp.server.send(durableEvent("session.agent.selected", { sessionID: sessionId, agent: "plan" }))
|
|
const updates = await acp.until(() => {
|
|
const updates = configUpdates(acp, sessionId)
|
|
return values(updates.at(-1)).mode === "plan" && updates
|
|
}, "the sentinel update")
|
|
|
|
expect(
|
|
updates.slice(-2).map((options) => ({ ...values(options), models: selectValues(options, "model") })),
|
|
).toEqual(
|
|
["build", "plan"].map((mode) => ({
|
|
model: "test/second-model",
|
|
effort: "default",
|
|
mode,
|
|
models: ["test/second-model", "test/test-model"],
|
|
})),
|
|
)
|
|
})
|
|
})
|
|
|
|
function configUpdates(acp: Wire, sessionId: string) {
|
|
return acp.updates.flatMap((item) =>
|
|
item.sessionId === sessionId && item.update.sessionUpdate === "config_option_update"
|
|
? [item.update.configOptions]
|
|
: [],
|
|
)
|
|
}
|
|
|
|
function optionUpdates(acp: Wire, sessionId: string, count: number) {
|
|
return acp.until(() => {
|
|
const updates = configUpdates(acp, sessionId)
|
|
return updates.length >= count && updates
|
|
}, `${count} config_option_update for ${sessionId}`)
|
|
}
|
|
|
|
function values(options: SessionConfigOption[] | undefined) {
|
|
return Object.fromEntries((options ?? []).map((option) => [option.id, option.currentValue]))
|
|
}
|