Files
gori-agent/test/acp-client.test.ts
T
zenord 7aa610294c feat: pending-finish proposals and QQ image input
Proposal semantics: worker results no longer distinguish success/fail;
only an explicit finish settles a pending proposal. Pending owner input
follows up by resuming the original worker session. Dirty pending blocks
start_next globally; clean pending only blocks its owner.

QQ adapter now downloads image attachments and passes them as ACP image
content blocks; video/file attachments degrade to link text.
2026-08-19 13:36:23 +08:00

91 lines
4.9 KiB
TypeScript

import assert from "node:assert/strict";
import { spawn } from "node:child_process";
import crypto from "node:crypto";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import test from "node:test";
import type { RequestPermissionRequest } from "@agentclientprotocol/sdk";
import { AcpClient, decidePermission } from "../src/acp/client.js";
const request = (rawInput: unknown, overrides: Partial<RequestPermissionRequest["toolCall"]> = {}): RequestPermissionRequest => ({
sessionId: "session",
toolCall: { toolCallId: "call", title: "bash git status", kind: "execute", name: "bash", rawInput, ...overrides },
options: [
{ optionId: "allow", name: "Allow", kind: "allow_once" },
{ optionId: "reject", name: "Reject", kind: "reject_once" }
]
});
test("permission deny and allowlist fail closed", () => {
assert.equal(decidePermission(request("git status"), { mode: "deny", allowedTools: [], allowedCommandPatterns: [] }).outcome.outcome, "selected");
const allowed = decidePermission(request("git status"), { mode: "allowlist", allowedTools: ["bash"], allowedCommandPatterns: ["^git status$"] });
assert.deepEqual(allowed.outcome, { outcome: "selected", optionId: "allow" });
const objectInput = decidePermission(request({ command: "git status", timeout: 60 }), { mode: "allowlist", allowedTools: ["bash"], allowedCommandPatterns: ["^git status$"] });
assert.deepEqual(objectInput.outcome, { outcome: "selected", optionId: "allow" });
const missingDetail = decidePermission(request(undefined), { mode: "allowlist", allowedTools: ["bash"], allowedCommandPatterns: ["^git status$"] });
assert.deepEqual(missingDetail.outcome, { outcome: "selected", optionId: "reject" });
const destructive = decidePermission(request("rm -rf /"), { mode: "allowlist", allowedTools: ["bash"], allowedCommandPatterns: ["^git status$"] });
assert.deepEqual(destructive.outcome, { outcome: "selected", optionId: "reject" });
const unanchored = { mode: "allowlist" as const, allowedTools: ["bash"], allowedCommandPatterns: ["git status"] };
assert.deepEqual(decidePermission(request("git status"), unanchored).outcome, { outcome: "selected", optionId: "allow" });
assert.deepEqual(decidePermission(request("git status; rm -rf /"), unanchored).outcome, { outcome: "selected", optionId: "reject" });
assert.deepEqual(decidePermission(request("git status", { name: "python", title: "bash git status" }), unanchored).outcome, { outcome: "selected", optionId: "reject" });
assert.deepEqual(decidePermission(request("git status", { name: undefined, title: "bash git status" }), unanchored).outcome, { outcome: "selected", optionId: "reject" });
assert.deepEqual(decidePermission(request({ command: "git status", env: { PATH: "/tmp" } }), unanchored).outcome, { outcome: "selected", optionId: "reject" });
});
test("reports activity for every update from the active session", async () => {
const child = spawn(process.execPath, [path.resolve("test/fixtures/fake-acp-agent.mjs")], { stdio: ["pipe", "pipe", "pipe"] });
let activities = 0;
const client = new AcpClient(child, {
initializeTimeoutMs: 1_000,
policy: { mode: "deny", allowedTools: [], allowedCommandPatterns: [] },
onSessionActivity: () => { activities++; }
});
try {
await client.initialize();
await client.newSession(path.resolve("."));
assert.equal(await client.prompt("activity"), "firstsecond");
assert.equal(activities, 2);
} finally {
client.close();
child.kill("SIGTERM");
}
});
test("prompt sends text and image content blocks and records the image capability", async () => {
const logFile = path.join(os.tmpdir(), `gori-acp-client-${crypto.randomUUID()}.log`);
const fixture = path.resolve("test/fixtures/fake-acp-agent.mjs");
const child = spawn(process.execPath, [fixture], {
stdio: ["pipe", "pipe", "pipe"],
env: { ...process.env, FAKE_ACP_LOG: logFile }
});
const client = new AcpClient(child, {
initializeTimeoutMs: 1_000,
policy: { mode: "deny", allowedTools: [], allowedCommandPatterns: [] }
});
try {
await client.initialize();
assert.equal(client.supportsImageInput(), true);
await client.newSession(path.resolve("."));
const image = Buffer.from("fake-png").toString("base64");
const reply = await client.prompt([
{ type: "image", data: image, mimeType: "image/png" },
{ type: "text", text: "hello blocks" }
]);
assert.match(reply, /^reply:.+:hello blocks$/);
const prompts = fs.readFileSync(logFile, "utf8").trim().split("\n").filter(Boolean)
.map((line) => JSON.parse(line) as { method: string; text?: string; images?: number })
.filter((entry) => entry.method === "session/prompt");
assert.equal(prompts.length, 1);
assert.equal(prompts[0]!.text, "hello blocks");
assert.equal(prompts[0]!.images, 1);
} finally {
client.close();
child.kill("SIGTERM");
await fs.promises.rm(logFile, { force: true });
}
});