Upgrade the runtime and native plugin API to OpenCode 2.0.24. Restore session-scoped learning, credential renewal, typed project queries and controlled frontend actions from the legacy Agent. Replace host Shell execution with an approved Landlock/seccomp executor and persistent per-session workspaces. Add typed Bocha search and Tianditu geocoding bridges using the owning session credential. Require a working kernel sandbox before startup and persist workspace files in deployments. Validation completed before this commit: TypeScript checks, 150 Bun tests, Docker build and UID 10001 sandbox execution, native Shell approval and session recovery. Tianditu succeeded; Bocha currently reports exhausted account quota. Unrelated investigation-report contract work is excluded.
30 lines
2.3 KiB
TypeScript
30 lines
2.3 KiB
TypeScript
import { describe, expect, it } from "bun:test";
|
|
import { createBusinessStageTracker } from "../../src/routes/chatBusinessStages.js";
|
|
import { streamPromptResponse } from "../../src/routes/chatStream.js";
|
|
import type { OpencodeRuntimeAdapter } from "../../src/runtime/opencode.js";
|
|
|
|
describe("business stages and final answer", () => {
|
|
it("keeps a stage running while parallel actions remain, then preserves errors", () => {
|
|
const events: Record<string, unknown>[] = [];
|
|
const track = createBusinessStageTracker((_event, data) => events.push(data));
|
|
const part = (id: string, status: string) => ({ id, type: "tool", tool: "query_project_data", state: { status } }) as Parameters<typeof track>[0];
|
|
track(part("a", "running")); track(part("b", "running")); track(part("a", "completed"));
|
|
expect(events.at(-1)?.status).toBe("running");
|
|
track(part("b", "error"));
|
|
expect(events.at(-1)).toMatchObject({ id: "business-data", status: "error" });
|
|
});
|
|
it("buffers final_answer until the runtime succeeds and does not end the run at the tool", async () => {
|
|
const output: string[] = [];
|
|
const events = [
|
|
{ type: "message.part.updated", properties: { sessionID: "s", part: { id: "f", type: "tool", tool: "final_answer", state: { status: "completed", input: { answer: "最终结论" }, output: JSON.stringify({ answer: "最终结论" }) } } } },
|
|
{ type: "message.part.updated", properties: { sessionID: "s", part: { id: "q", type: "tool", tool: "query_project_data", state: { status: "completed", input: { source: "scada_devices" }, output: "{}" } } } },
|
|
{ type: "session.idle", properties: { sessionID: "s" } },
|
|
];
|
|
const runtime = { prompt: async () => undefined, subscribeEvents: async () => ({ async *[Symbol.asyncIterator]() { yield* events; } }), messages: async () => { throw new Error("final answer needs no text fallback"); } } as unknown as OpencodeRuntimeAdapter;
|
|
const result = await streamPromptResponse({ runtime, sessionId: "s", clientSessionId: "c", message: "test", write: (event, data) => { output.push(event); if (event === "final_answer") expect(data.content).toBe("最终结论"); } });
|
|
expect(result.failed).toBeFalse();
|
|
expect(output.indexOf("tool_call")).toBeLessThan(output.indexOf("final_answer"));
|
|
expect(output.at(-1)).toBe("done");
|
|
});
|
|
});
|