sandbox-agent/server/packages/sandbox-agent/tests/opencode-compat/questions.test.ts
Nathan Flurry 76586f409f
Add ACP permission mode support to the SDK (#224)
* chore: recover hamburg workspace state

* chore: drop workspace context files

* refactor: generalize permissions example

* refactor: parse permissions example flags

* docs: clarify why fs and terminal stay native

* feat: add interactive permission prompt UI to Inspector

Add permission request handling to the Inspector UI so users can
Allow, Always Allow, or Reject tool calls that require permissions
instead of having them auto-cancelled. Wires up SDK
onPermissionRequest/respondPermission through App → ChatPanel →
ChatMessages with proper toolCallId-to-pendingId mapping.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: prevent permission reply from silently escalating "once" to "always"

Remove allow_always from the fallback chain when the user replies "once",
aligning with the ACP spec which says "map by option kind first" with no
fallback for allow_once. Also fix Inspector to use rawSend, revert
hydration guard to accept empty configOptions, and handle respondPermission
errors by rejecting the pending promise.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 21:52:43 -07:00

83 lines
2.4 KiB
TypeScript

/**
* Tests for OpenCode-compatible question endpoints.
*/
import { describe, it, expect, beforeAll, beforeEach, afterEach } from "vitest";
import { createOpencodeClient, type OpencodeClient } from "@opencode-ai/sdk";
import { spawnSandboxAgent, buildSandboxAgent, type SandboxAgentHandle } from "./helpers/spawn";
describe("OpenCode-compatible Question API", () => {
let handle: SandboxAgentHandle;
let client: OpencodeClient;
let sessionId: string;
beforeAll(async () => {
await buildSandboxAgent();
});
beforeEach(async () => {
handle = await spawnSandboxAgent({ opencodeCompat: true });
client = createOpencodeClient({
baseUrl: `${handle.baseUrl}/opencode`,
headers: { Authorization: `Bearer ${handle.token}` },
});
const session = await client.session.create();
sessionId = session.data?.id!;
expect(sessionId).toBeDefined();
});
afterEach(async () => {
await handle?.dispose();
});
const questionPrompt = "question";
async function waitForQuestionRequest(timeoutMs = 10_000) {
const start = Date.now();
while (Date.now() - start < timeoutMs) {
const list = await client.question.list();
const request = list.data?.[0];
if (request) {
return request;
}
await new Promise((r) => setTimeout(r, 200));
}
throw new Error("Timed out waiting for question request");
}
it("should ask a question and accept a reply", async () => {
await client.session.prompt({
sessionID: sessionId,
model: { providerID: "mock", modelID: "mock" },
parts: [{ type: "text", text: questionPrompt }],
});
const asked = await waitForQuestionRequest();
const requestId = asked?.id;
expect(requestId).toBeDefined();
const replyResponse = await client.question.reply({
requestID: requestId,
answers: [["Yes"]],
});
expect(replyResponse.error).toBeUndefined();
});
it("should allow rejecting a question", async () => {
await client.session.prompt({
sessionID: sessionId,
model: { providerID: "mock", modelID: "mock" },
parts: [{ type: "text", text: questionPrompt }],
});
const asked = await waitForQuestionRequest();
const requestId = asked?.id;
expect(requestId).toBeDefined();
const rejectResponse = await client.question.reject({
requestID: requestId,
});
expect(rejectResponse.error).toBeUndefined();
});
});