feat(opencode): migrate agent runtime to v2

This commit is contained in:
2026-08-04 16:56:04 +08:00
parent 07016451d6
commit 764a1f4e82
33 changed files with 1662 additions and 1057 deletions
+193
View File
@@ -0,0 +1,193 @@
import { describe, expect, it } from "bun:test";
import { type OpenCodeClient } from "@opencode-ai/client";
import { OpencodeRuntimeAdapter } from "../../src/runtime/opencode.js";
const createRuntimeAdapter = (client: unknown) =>
Object.assign(new OpencodeRuntimeAdapter(), {
clientPromise: Promise.resolve(client as OpenCodeClient),
}) as OpencodeRuntimeAdapter;
const createEventStream = (events: unknown[]) => ({
async *[Symbol.asyncIterator]() {
for (const event of events) {
yield event;
}
},
});
describe("OpencodeRuntimeAdapter V2 interaction responses", () => {
it("replies through the top-level V2 permission API", async () => {
const runtime = createRuntimeAdapter({
permission: {
reply: async (parameters: Record<string, unknown>) => {
expect(parameters).toEqual({
sessionID: "session-1",
requestID: "request-1",
reply: "once",
message: "approved",
});
},
},
});
await expect(
runtime.replyPermission({
requestId: "request-1",
sessionId: "session-1",
reply: "once",
message: "approved",
}),
).resolves.toBeUndefined();
});
it("replies through the top-level V2 question API", async () => {
const runtime = createRuntimeAdapter({
question: {
reply: async (parameters: Record<string, unknown>) => {
expect(parameters).toEqual({
sessionID: "session-1",
requestID: "request-1",
answers: [["A"]],
});
},
},
});
await expect(
runtime.replyQuestion({
requestId: "request-1",
sessionId: "session-1",
answers: [["A"]],
}),
).resolves.toBeUndefined();
});
it("rejects through the top-level V2 question API", async () => {
const runtime = createRuntimeAdapter({
question: {
reject: async (parameters: Record<string, unknown>) => {
expect(parameters).toEqual({
sessionID: "session-1",
requestID: "request-1",
});
},
},
});
await expect(
runtime.rejectQuestion({
requestId: "request-1",
sessionId: "session-1",
}),
).resolves.toBeUndefined();
});
it("maps frontend question answers to the V2 Form API", async () => {
const runtime = createRuntimeAdapter({
form: {
get: async (parameters: Record<string, unknown>) => {
expect(parameters).toEqual({ sessionID: "session-1", formID: "frm_1" });
return {
fields: [
{
key: "scope",
type: "string",
title: "范围",
options: [{ value: "urban", label: "城区" }],
},
{ key: "confirmed", type: "boolean", title: "确认" },
],
};
},
reply: async (parameters: Record<string, unknown>) => {
expect(parameters).toEqual({
sessionID: "session-1",
formID: "frm_1",
answer: { scope: "urban", confirmed: true },
});
},
},
});
await expect(
runtime.replyQuestion({
requestId: "frm_1",
sessionId: "session-1",
answers: [["城区"], ["是"]],
}),
).resolves.toBeUndefined();
});
it("cancels V2 Form requests by form ID", async () => {
const runtime = createRuntimeAdapter({
form: {
cancel: async (parameters: Record<string, unknown>) => {
expect(parameters).toEqual({ sessionID: "session-1", formID: "frm_1" });
},
},
});
await expect(
runtime.rejectQuestion({ requestId: "frm_1", sessionId: "session-1" }),
).resolves.toBeUndefined();
});
it("normalizes V2 Form and execution events for the chat stream", async () => {
const runtime = createRuntimeAdapter({
event: {
subscribe: () =>
createEventStream([
{
type: "form.created",
data: {
form: {
id: "frm_1",
sessionID: "session-1",
title: "分析参数",
fields: [
{
key: "scope",
type: "string",
title: "范围",
description: "选择分析范围",
options: [{ value: "urban", label: "城区" }],
},
],
},
},
},
{
type: "session.execution.succeeded",
data: { sessionID: "session-1" },
},
]),
},
});
const events = [];
for await (const event of await runtime.subscribeEvents()) {
events.push(event);
}
expect(events).toEqual([
{
type: "question.asked",
sessionId: "session-1",
request: {
id: "frm_1",
questions: [
{
header: "范围",
question: "选择分析范围",
options: [{ label: "城区", description: "" }],
multiple: false,
custom: false,
},
],
},
},
{ type: "session.execution.succeeded", sessionId: "session-1" },
]);
});
});