fix(agent): stabilize sandboxed streaming workflows

This commit is contained in:
2026-08-26 12:26:29 +08:00
parent 80cfc1f2ab
commit 4ec010455b
12 changed files with 916 additions and 34 deletions
+136 -1
View File
@@ -90,6 +90,136 @@ describe("OpencodeRuntimeAdapter.ensureClient", () => {
});
});
describe("OpencodeRuntimeAdapter.subscribeEvents", () => {
it("subscribes to the conversation workspace directory", async () => {
const calls: Array<Record<string, unknown> | undefined> = [];
const stream = (async function* () {
return;
})();
const client = {
event: {
subscribe: async (input?: Record<string, unknown>) => {
calls.push(input);
return { stream };
},
},
} as unknown as OpencodeClient;
const runtime = Object.assign(Object.create(OpencodeRuntimeAdapter.prototype), {
ensureClient: async () => client,
}) as OpencodeRuntimeAdapter;
await expect(
runtime.subscribeEvents("/tmp/conversation-workspace-1"),
).resolves.toBe(stream);
expect(calls).toEqual([
{ directory: "/tmp/conversation-workspace-1" },
]);
});
});
describe("OpencodeRuntimeAdapter.prompt", () => {
it("forwards the final answer schema", async () => {
const calls: unknown[] = [];
const client = {
session: {
prompt: async (input: unknown) => {
calls.push(input);
return { data: {} };
},
},
} as unknown as OpencodeClient;
const runtime = Object.assign(Object.create(OpencodeRuntimeAdapter.prototype), {
ensureClient: async () => client,
}) as OpencodeRuntimeAdapter;
await runtime.prompt(
"session-1",
"分析供水分区",
{ providerID: "deepseek", modelID: "deepseek-v4-flash" },
{
format: {
type: "json_schema",
schema: {
type: "object",
properties: { answer: { type: "string" } },
required: ["answer"],
},
},
},
);
expect(calls).toEqual([
{
sessionID: "session-1",
model: {
providerID: "deepseek",
modelID: "deepseek-v4-flash",
},
format: {
type: "json_schema",
schema: {
type: "object",
properties: { answer: { type: "string" } },
required: ["answer"],
},
},
parts: [{ type: "text", text: "分析供水分区" }],
},
]);
});
});
describe("OpencodeRuntimeAdapter interaction replies", () => {
it("replies to permissions in the conversation workspace directory", async () => {
const calls: unknown[] = [];
const client = {
permission: {
reply: async (input: unknown) => {
calls.push(input);
return { data: true };
},
},
} as unknown as OpencodeClient;
const runtime = Object.assign(Object.create(OpencodeRuntimeAdapter.prototype), {
ensureClient: async () => client,
}) as OpencodeRuntimeAdapter;
await runtime.replyPermission({
requestId: "permission-1",
sessionId: "session-1",
directory: "/tmp/conversation-workspace-1",
reply: "once",
});
expect(calls).toEqual([
{
requestID: "permission-1",
directory: "/tmp/conversation-workspace-1",
reply: "once",
},
]);
});
it("fails when OpenCode returns no permission reply data", async () => {
const client = {
permission: {
reply: async () => ({ data: undefined }),
},
} as unknown as OpencodeClient;
const runtime = Object.assign(Object.create(OpencodeRuntimeAdapter.prototype), {
ensureClient: async () => client,
}) as OpencodeRuntimeAdapter;
await expect(
runtime.replyPermission({
requestId: "permission-1",
directory: "/tmp/conversation-workspace-1",
reply: "once",
}),
).rejects.toThrow("permission.reply returned no data");
});
});
describe("OpencodeRuntimeAdapter.createSession", () => {
it("creates a real chat session inside a dedicated conversation workspace", async () => {
const workspaceRoot = await mkdtemp(join(tmpdir(), "tjwater-conversations-"));
@@ -225,11 +355,16 @@ describe("OpencodeRuntimeAdapter.warmup", () => {
await runtime.replyQuestion({
requestId: "question-1",
sessionId: "session-1",
directory: "/tmp/conversation-workspace-1",
answers: [["继续"]],
});
expect(calls).toEqual([
{ requestID: "question-1", answers: [["继续"]] },
{
requestID: "question-1",
directory: "/tmp/conversation-workspace-1",
answers: [["继续"]],
},
]);
});
});