mirror of
https://github.com/anomalyco/opencode.git
synced 2026-07-24 04:01:10 -04:00
260 lines
9.6 KiB
TypeScript
260 lines
9.6 KiB
TypeScript
import { describe, expect, test } from "bun:test"
|
|
import { makeACPFixture, makeSession, secondModel, type FixtureContext, type FixtureRequest } from "./service-fixture"
|
|
|
|
describe("acp service prompt routing and usage", () => {
|
|
test("routes slash commands, skills, and compact through their session endpoints", async () => {
|
|
await using fixture = makeACPFixture({
|
|
fetch(request, context) {
|
|
if (request.method === "POST" && request.path === "/api/session") {
|
|
return Response.json({ data: makeSession("ses_routes") })
|
|
}
|
|
if (request.method === "GET" && request.path === "/api/session/ses_routes") {
|
|
return Response.json({ data: makeSession("ses_routes") })
|
|
}
|
|
if (request.method === "POST" && request.path === "/api/session/ses_routes/command") {
|
|
const id = requestID(request)
|
|
completeTurn(context, "ses_routes", {
|
|
id: `evt_${id}`,
|
|
type: "session.input.promoted",
|
|
data: { sessionID: "ses_routes", inputID: id },
|
|
})
|
|
return Response.json({ data: {} })
|
|
}
|
|
if (request.method === "POST" && request.path === "/api/session/ses_routes/skill") {
|
|
const id = requestID(request)
|
|
completeTurn(context, "ses_routes", {
|
|
id: id.replace(/^msg_/, "evt_"),
|
|
type: "session.skill.activated",
|
|
data: { sessionID: "ses_routes", skill: "verify" },
|
|
})
|
|
return new Response(null, { status: 204 })
|
|
}
|
|
if (request.method === "POST" && request.path === "/api/session/ses_routes/compact") {
|
|
const id = requestID(request)
|
|
completeTurn(context, "ses_routes", {
|
|
id: `evt_${id}`,
|
|
type: "session.compaction.admitted",
|
|
data: { sessionID: "ses_routes", inputID: id },
|
|
})
|
|
return Response.json({ data: {} })
|
|
}
|
|
return undefined
|
|
},
|
|
})
|
|
const session = await fixture.service.newSession({ cwd: "/workspace", mcpServers: [] })
|
|
|
|
const commandResult = await fixture.service.prompt({
|
|
sessionId: session.sessionId,
|
|
prompt: [{ type: "text", text: "/review now" }],
|
|
})
|
|
const skillResult = await fixture.service.prompt({
|
|
sessionId: session.sessionId,
|
|
prompt: [{ type: "text", text: "/verify" }],
|
|
})
|
|
const compactResult = await fixture.service.prompt({
|
|
sessionId: session.sessionId,
|
|
prompt: [{ type: "text", text: "/compact" }],
|
|
})
|
|
|
|
expect([commandResult.stopReason, skillResult.stopReason, compactResult.stopReason]).toEqual([
|
|
"end_turn",
|
|
"end_turn",
|
|
"end_turn",
|
|
])
|
|
const command = fixture.requests.find((request) => request.path === "/api/session/ses_routes/command")
|
|
const skill = fixture.requests.find((request) => request.path === "/api/session/ses_routes/skill")
|
|
const compact = fixture.requests.find((request) => request.path === "/api/session/ses_routes/compact")
|
|
expect(command?.body).toMatchObject({
|
|
id: expect.any(String),
|
|
command: "review",
|
|
arguments: "now",
|
|
files: [],
|
|
delivery: "steer",
|
|
})
|
|
expect(skill?.body).toMatchObject({ id: expect.any(String), skill: "verify" })
|
|
expect(compact?.body).toMatchObject({ id: expect.any(String) })
|
|
expect(fixture.requests.some((request) => request.path === "/api/session/ses_routes/prompt")).toBe(false)
|
|
})
|
|
|
|
test("returns turn usage and publishes current context usage with cumulative session cost", async () => {
|
|
const assistantTokens = {
|
|
input: 100,
|
|
output: 40,
|
|
reasoning: 7,
|
|
cache: { read: 11, write: 13 },
|
|
}
|
|
await using fixture = makeACPFixture({
|
|
fetch(request, context) {
|
|
if (request.method === "POST" && request.path === "/api/session") {
|
|
return Response.json({ data: makeSession("ses_usage") })
|
|
}
|
|
if (request.method === "POST" && request.path === "/api/session/ses_usage/model") {
|
|
return new Response(null, { status: 204 })
|
|
}
|
|
if (request.method === "POST" && request.path === "/api/session/ses_usage/prompt") {
|
|
const id = requestID(request)
|
|
context.send({
|
|
id: `evt_${id}`,
|
|
type: "session.input.promoted",
|
|
data: { sessionID: "ses_usage", inputID: id },
|
|
})
|
|
context.send({
|
|
id: "evt_step",
|
|
type: "session.step.ended",
|
|
data: {
|
|
sessionID: "ses_usage",
|
|
assistantMessageID: "msg_assistant",
|
|
finish: "stop",
|
|
cost: 0.5,
|
|
tokens: assistantTokens,
|
|
},
|
|
})
|
|
context.send({
|
|
id: "evt_done",
|
|
type: "session.execution.succeeded",
|
|
data: { sessionID: "ses_usage" },
|
|
})
|
|
return Response.json({ data: {} })
|
|
}
|
|
if (request.method === "GET" && request.path === "/api/session/ses_usage/message/msg_assistant") {
|
|
return Response.json({
|
|
data: {
|
|
id: "msg_assistant",
|
|
type: "assistant",
|
|
agent: "build",
|
|
model: { providerID: "test", id: secondModel.id },
|
|
content: [{ type: "text", text: "done" }],
|
|
finish: "stop",
|
|
tokens: assistantTokens,
|
|
time: { created: 1, completed: 2 },
|
|
},
|
|
})
|
|
}
|
|
if (request.method === "GET" && request.path === "/api/session/ses_usage") {
|
|
return Response.json({
|
|
data: makeSession("ses_usage", {
|
|
model: { providerID: "test", id: secondModel.id },
|
|
cost: 3.5,
|
|
tokens: { input: 120, output: 50, reasoning: 8, cache: { read: 30, write: 4 } },
|
|
}),
|
|
})
|
|
}
|
|
return undefined
|
|
},
|
|
})
|
|
const session = await fixture.service.newSession({ cwd: "/workspace", mcpServers: [] })
|
|
await fixture.service.setSessionConfigOption({
|
|
sessionId: session.sessionId,
|
|
configId: "model",
|
|
value: "test/second-model",
|
|
})
|
|
|
|
const response = await fixture.service.prompt({
|
|
sessionId: session.sessionId,
|
|
prompt: [{ type: "text", text: "hello" }],
|
|
})
|
|
|
|
expect(response).toEqual({
|
|
stopReason: "end_turn",
|
|
usage: {
|
|
inputTokens: 100,
|
|
outputTokens: 40,
|
|
thoughtTokens: 7,
|
|
cachedReadTokens: 11,
|
|
cachedWriteTokens: 13,
|
|
totalTokens: 171,
|
|
},
|
|
_meta: {},
|
|
})
|
|
expect(fixture.updates.filter((item) => item.update.sessionUpdate === "usage_update")).toEqual([
|
|
{
|
|
sessionId: "ses_usage",
|
|
update: {
|
|
sessionUpdate: "usage_update",
|
|
used: 171,
|
|
size: 200_000,
|
|
cost: { amount: 3.5, currency: "USD" },
|
|
},
|
|
},
|
|
])
|
|
})
|
|
|
|
test("does not fail a completed prompt when the usage refresh fails", async () => {
|
|
await using fixture = makeACPFixture({
|
|
fetch(request, context) {
|
|
if (request.method === "POST" && request.path === "/api/session") {
|
|
return Response.json({ data: makeSession("ses_usage_failure") })
|
|
}
|
|
if (request.method === "POST" && request.path === "/api/session/ses_usage_failure/prompt") {
|
|
const id = requestID(request)
|
|
context.send({
|
|
id: `evt_${id}`,
|
|
type: "session.input.promoted",
|
|
data: { sessionID: "ses_usage_failure", inputID: id },
|
|
})
|
|
context.send({
|
|
id: "evt_step_failure",
|
|
type: "session.step.ended",
|
|
data: {
|
|
sessionID: "ses_usage_failure",
|
|
assistantMessageID: "msg_usage_failure",
|
|
finish: "stop",
|
|
cost: 0,
|
|
tokens: { input: 3, output: 2, reasoning: 0, cache: { read: 0, write: 0 } },
|
|
},
|
|
})
|
|
context.send({
|
|
id: "evt_done_failure",
|
|
type: "session.execution.succeeded",
|
|
data: { sessionID: "ses_usage_failure" },
|
|
})
|
|
return Response.json({ data: {} })
|
|
}
|
|
if (request.method === "GET" && request.path === "/api/session/ses_usage_failure/message/msg_usage_failure") {
|
|
return Response.json({
|
|
data: {
|
|
id: "msg_usage_failure",
|
|
type: "assistant",
|
|
agent: "build",
|
|
model: { providerID: "test", id: "test-model" },
|
|
content: [],
|
|
finish: "stop",
|
|
tokens: { input: 3, output: 2, reasoning: 0, cache: { read: 0, write: 0 } },
|
|
time: { created: 1, completed: 2 },
|
|
},
|
|
})
|
|
}
|
|
if (request.method === "GET" && request.path === "/api/session/ses_usage_failure") {
|
|
return new Response(null, { status: 500 })
|
|
}
|
|
return undefined
|
|
},
|
|
})
|
|
const session = await fixture.service.newSession({ cwd: "/workspace", mcpServers: [] })
|
|
|
|
const response = await fixture.service.prompt({
|
|
sessionId: session.sessionId,
|
|
prompt: [{ type: "text", text: "hello" }],
|
|
})
|
|
|
|
expect(response.stopReason).toBe("end_turn")
|
|
expect(fixture.updates.some((item) => item.update.sessionUpdate === "usage_update")).toBe(false)
|
|
})
|
|
})
|
|
|
|
function requestID(request: FixtureRequest) {
|
|
if (!request.body || typeof request.body !== "object") throw new Error(`missing body for ${request.path}`)
|
|
const id = Reflect.get(request.body, "id")
|
|
if (typeof id !== "string") throw new Error(`missing prompt id for ${request.path}`)
|
|
return id
|
|
}
|
|
|
|
function completeTurn(context: FixtureContext, sessionID: string, start: unknown) {
|
|
context.send(start)
|
|
context.send({
|
|
id: `evt_done_${sessionID}`,
|
|
type: "session.execution.succeeded",
|
|
data: { sessionID },
|
|
})
|
|
}
|