test/acp-adapter.test.js
Rama main · réplica de lectura del repositorio original.
var test = require("node:test");
var assert = require("node:assert");
var createAcpAdapter = require("../lib/yoke/adapters/acp").createAcpAdapter;
var modelValues = require("../lib/yoke/adapters/acp").modelValues;
var acpUserInput = require("../lib/yoke/acp-user-input");
var createAcpQueryHandle = require("../lib/yoke/acp-query-handle").createAcpQueryHandle;
var getProfile = require("../lib/yoke/acp-agent-profiles").getAcpAgentProfile;
function FakeManager(executablePath, opts) {
this.executablePath = executablePath;
this.opts = opts;
this.started = false;
this.handlers = [];
this.calls = [];
FakeManager.instances.push(this);
}
FakeManager.instances = [];
FakeManager.prototype.start = function() { this.started = true; return Promise.resolve(); };
FakeManager.prototype.stop = function() { this.started = false; };
FakeManager.prototype.addRequestHandler = function(method, fn) {
this.requestHandlers = this.requestHandlers || {};
this.requestHandlers[method] = fn;
};
FakeManager.prototype.addHandler = function(fn) {
var entry = { sessionId: null, fn: fn };
this.handlers.push(entry);
return entry;
};
FakeManager.prototype.removeHandler = function(entry) {
var index = this.handlers.indexOf(entry);
if (index !== -1) this.handlers.splice(index, 1);
};
FakeManager.prototype.notify = function(method, params) { this.calls.push({ method: method, params: params, notification: true }); };
FakeManager.prototype.respond = function(id, result) { this.calls.push({ id: id, result: result, response: true }); };
FakeManager.prototype.respondError = function(id, code, message) { this.calls.push({ id: id, error: { code: code, message: message }, response: true }); };
FakeManager.prototype.send = function(method, params) {
this.calls.push({ method: method, params: params });
if (method === "initialize") {
return Promise.resolve({
protocolVersion: 1,
agentCapabilities: { loadSession: true, sessionCapabilities: { resume: {} } },
});
}
if (method === "session/new") {
return Promise.resolve(this.sessionResult || {
sessionId: "session-1",
configOptions: [{
id: "model",
category: "model",
currentValue: "provider/default",
options: [
{ value: "provider/default", name: "Default" },
{ value: "provider/fast", name: "Fast" },
],
}, {
id: "mode",
category: "mode",
currentValue: "auto-approve",
options: [
{ value: "default", name: "Default" },
{ value: "auto-approve", name: "Auto approve" },
],
}],
});
}
if (method === "session/prompt") {
var entry = this.handlers[0];
entry.fn({
method: "session/update",
params: {
sessionId: "session-1",
update: { sessionUpdate: "agent_message_chunk", content: { type: "text", text: "hello" } },
},
});
return Promise.resolve({ stopReason: "end_turn" });
}
if (method === "session/set_config_option" && params.configId === "model" && this.rejectModelChange) {
return Promise.reject(new Error("model rejected"));
}
return Promise.resolve({});
};
function adapterOptions(profile) {
return {
cwd: process.cwd(),
_profile: profile,
_binaryPath: "/contract/" + profile.binaryName,
_AcpProcessManagerCtor: FakeManager,
_fetchModels: function() { return Promise.resolve([]); },
_openCodeAgentNames: [],
_openCodeResolvedConfig: {
permission: "ask",
agent: {
build: { permission: "ask" },
plan: { permission: "ask" },
},
},
};
}
test("OpenCode profile uses its official ACP entry point", function() {
assert.deepStrictEqual(getProfile("opencode").args, ["acp"]);
});
test("new ACP profiles use their official supervised entry points", function() {
assert.deepStrictEqual(getProfile("kimi").args, ["acp"]);
assert.deepStrictEqual(getProfile("grok").args, ["--no-auto-update", "--permission-mode", "default", "agent", "stdio"]);
assert.deepStrictEqual(getProfile("copilot").args, ["--acp"]);
assert.deepStrictEqual(getProfile("qwen").args, ["--acp", "--approval-mode", "default"]);
assert.deepStrictEqual(getProfile("junie").args, ["--acp", "true"]);
});
test("new ACP vendor factories satisfy the shared YOKE contract", function() {
var vendors = ["kimi", "grok", "copilot", "qwen", "junie"];
for (var i = 0; i < vendors.length; i++) {
var adapter = require("../lib/yoke").createAdapter({ vendor: vendors[i], cwd: process.cwd() });
assert.strictEqual(adapter.vendor, vendors[i]);
assert.strictEqual(typeof adapter.createQuery, "function");
}
});
test("shared ACP initialization advertises form elicitation and grouped model options", async function() {
FakeManager.instances = [];
var adapter = createAcpAdapter("opencode", adapterOptions(getProfile("opencode")));
var ready = await adapter.init();
var initialize = FakeManager.instances[0].calls.find(function(call) { return call.method === "initialize"; });
assert.deepStrictEqual(initialize.params.clientCapabilities.elicitation, { form: {} });
assert.strictEqual(ready.capabilities.elicitation, true);
assert.deepStrictEqual(adapter.userInputCapability, { mode: "fallback", native: false, nativeElicitation: true, source: "acp_elicitation" });
assert.deepStrictEqual(modelValues([{
id: "model",
options: [
{ id: "empty-group", name: "Empty", options: [] },
{ id: "provider-group", name: "Provider", options: [{ value: "provider/fast" }, { id: "provider/deep" }] },
],
}]), ["provider/fast", "provider/deep"]);
await adapter.shutdown();
});
test("Kimi ACP does not claim static effort levels for model-specific thinking options", async function() {
FakeManager.instances = [];
var adapter = createAcpAdapter("kimi", adapterOptions(getProfile("kimi")));
var ready = await adapter.init();
assert.strictEqual(ready.capabilities.effort, false);
FakeManager.instances[0].sessionResult = {
sessionId: "session-1",
configOptions: [{ id: "thinking", category: "thought_level", currentValue: "off", options: [{ value: "off" }, { value: "high" }] }],
};
var handle = await adapter.createQuery({ cwd: process.cwd(), model: "auto" });
handle.pushMessage("effort test");
for await (var event of handle) if (event.yokeType === "result") break;
assert.strictEqual(FakeManager.instances[0].calls.some(function(call) {
return call.method === "session/set_config_option" && call.params.configId === "thinking";
}), false);
handle.close();
await adapter.shutdown();
});
test("all six shared ACP production adapters replace the stdio bridge config on the next resumed query", async function () {
var vendors = ["opencode", "kimi", "grok", "copilot", "qwen", "junie"];
for (var i = 0; i < vendors.length; i++) {
FakeManager.instances = [];
var profile = getProfile(vendors[i]);
var adapter = createAcpAdapter(vendors[i], adapterOptions(profile));
await adapter.init();
var freshDescriptor = { name: "clay-tools", command: process.execPath, args: ["bridge.js", "--session", "51", "--query-generation", "3"] };
var resumedDescriptor = { name: "clay-tools", command: process.execPath, args: ["bridge.js", "--session", "51", "--query-generation", "4"] };
var fresh = await adapter.createQuery({ cwd: process.cwd(), adapterOptions: { ACP: { mcpServers: [freshDescriptor] } } });
fresh.pushMessage("fresh");
for await (var freshEvent of fresh) { if (freshEvent.yokeType === "result") break; }
fresh.close();
var resumed = await adapter.createQuery({ cwd: process.cwd(), resumeSessionId: "session-1", adapterOptions: { ACP: { mcpServers: [resumedDescriptor] } } });
resumed.pushMessage("resumed");
for await (var resumedEvent of resumed) { if (resumedEvent.yokeType === "result") break; }
resumed.close();
var manager = FakeManager.instances[0];
var freshCall = manager.calls.find(function (call) { return call.method === "session/new"; });
var resumeCall = manager.calls.find(function (call) { return call.method === "session/resume"; });
assert.deepEqual(freshCall.params.mcpServers, [freshDescriptor], vendors[i] + " fresh catalog");
assert.deepEqual(resumeCall.params.mcpServers, [resumedDescriptor], vendors[i] + " replacement catalog");
await adapter.shutdown();
}
});
test("ACP createQuery honors disabled skill discovery before prompt metadata is built", async function() {
FakeManager.instances = [];
var profile = getProfile("opencode");
var adapter = createAcpAdapter("opencode", adapterOptions(profile));
await adapter.init();
var handle = await adapter.createQuery({ cwd: process.cwd(), skillOptions: { disable: true } });
handle.pushMessage("disabled skills");
for await (var event of handle) { if (event.yokeType === "result") break; }
var promptCall = FakeManager.instances[0].calls.find(function(call) { return call.method === "session/prompt"; });
assert.doesNotMatch(promptCall.params.prompt[0].text, /Available shared skills/);
handle.close();
await adapter.shutdown();
});
["kimi", "copilot", "junie"].forEach(function(vendor) {
test(vendor + " ACP profile replaces an exposed unsafe mode before prompting", async function() {
FakeManager.instances = [];
var adapter = createAcpAdapter(vendor, adapterOptions(getProfile(vendor)));
await adapter.init();
var handle = await adapter.createQuery({ cwd: process.cwd(), model: "auto" });
handle.pushMessage("permission mode test");
for await (var event of handle) {
if (event.yokeType === "result") break;
}
assert.ok(FakeManager.instances[0].calls.some(function(call) {
return call.method === "session/set_config_option" && call.params.configId === "mode" && call.params.value === "default";
}));
handle.close();
await adapter.shutdown();
});
});
test("OpenCode derives session resume support from the ACP handshake", async function() {
FakeManager.instances = [];
var options = adapterOptions(getProfile("opencode"));
options._openCodeAgentNames = ["custom"];
options.env = {
OPENCODE_CONFIG_CONTENT: JSON.stringify({
theme: "clay",
agent: { custom: { model: "provider/custom", permission: "allow" } },
}),
};
var adapter = createAcpAdapter("opencode", options);
var ready = await adapter.init();
assert.strictEqual(ready.capabilities.sessionResume, true);
assert.deepStrictEqual(FakeManager.instances[0].opts.args, ["acp"]);
var enforced = JSON.parse(FakeManager.instances[0].opts.env.OPENCODE_CONFIG_CONTENT);
assert.strictEqual(enforced.permission, "ask");
assert.strictEqual(enforced.theme, "clay");
assert.deepStrictEqual(enforced.agent.custom, { model: "provider/custom", permission: "ask" });
assert.strictEqual(enforced.agent.build.permission, "ask");
assert.strictEqual(enforced.agent.plan.permission, "ask");
await adapter.shutdown();
});
test("ACP adapter forwards the scoped environment to its process manager", async function() {
FakeManager.instances = [];
var adapter = createAcpAdapter("opencode", adapterOptions(getProfile("opencode")));
await adapter.init({ env: { PROJECT_TOKEN: "scoped" } });
assert.strictEqual(FakeManager.instances[0].opts.env.PROJECT_TOKEN, "scoped");
await adapter.shutdown();
});
test("OpenCode rejects late configuration that restores permissive agent rules", async function() {
FakeManager.instances = [];
var options = adapterOptions(getProfile("opencode"));
options._openCodeResolvedConfig = {
permission: "ask",
agent: {
build: { permission: "ask" },
managed: { permission: { edit: "allow", bash: "ask" } },
},
};
var adapter = createAcpAdapter("opencode", options);
await assert.rejects(adapter.init(), /unsafe resolved permissions: managed/);
assert.strictEqual(FakeManager.instances.length, 0);
});
test("OpenCode rejects late configuration that replaces the global ask rule", async function() {
FakeManager.instances = [];
var options = adapterOptions(getProfile("opencode"));
options._openCodeResolvedConfig = { permission: "allow", agent: {} };
var adapter = createAcpAdapter("opencode", options);
await assert.rejects(adapter.init(), /does not preserve global ask permissions/);
assert.strictEqual(FakeManager.instances.length, 0);
});
test("OpenCode accepts normalized safe recursive ask/deny permissions", function() {
var validate = require("../lib/yoke/acp-agent-profiles").validateOpenCodeConfig;
assert.doesNotThrow(function() {
validate({ permission: { "*": "ask", edit: "deny", shell: { read: "ask" } }, agent: {
build: { permission: { edit: "ask", shell: "deny" } },
} });
});
assert.doesNotThrow(function() { validate({ permission: "ask", agent: {} }); });
assert.throws(function() { validate({ permission: {}, agent: {} }); }, /global ask permissions/);
assert.throws(function() { validate({ permission: { "*": "allow" }, agent: {} }); }, /global ask permissions/);
assert.throws(function() { validate({ permission: { "*": "ask" }, agent: { build: { permission: { shell: "allow" } } } }); }, /unsafe resolved permissions: build/);
assert.throws(function() { validate({ permission: { "*": "ask" }, agent: { build: { permission: {} } } }); }, /unsafe resolved permissions: build/);
assert.throws(function() { validate({ permission: { "*": "ask" }, agent: { build: null } }); }, /malformed resolved configuration: build/);
});
test("ACP shutdown cancels in-flight initialization without poisoning retry", async function() {
FakeManager.instances = [];
var releaseModels;
var fetchCount = 0;
var options = adapterOptions(getProfile("opencode"));
options._fetchModels = function() {
fetchCount++;
if (fetchCount > 1) return Promise.resolve([]);
return new Promise(function(resolve) { releaseModels = resolve; });
};
var adapter = createAcpAdapter("opencode", options);
var initPromise = adapter.init();
var rejected = assert.rejects(initPromise, /adapter is shutting down/);
await new Promise(function(resolve) { setImmediate(resolve); });
var shutdownPromise = adapter.shutdown();
releaseModels([]);
await rejected;
assert.strictEqual(await shutdownPromise, true);
assert.strictEqual(FakeManager.instances.length, 0);
await adapter.init();
assert.strictEqual(FakeManager.instances.length, 1);
assert.strictEqual(FakeManager.instances[0].started, true);
await adapter.shutdown();
});
test("shared ACP adapter streams standard updates through the YOKE contract", async function() {
FakeManager.instances = [];
var adapter = createAcpAdapter("opencode", adapterOptions(getProfile("opencode")));
await adapter.init();
var handle = await adapter.createQuery({
cwd: process.cwd(),
model: "provider/default",
adapterOptions: { ACP: { mcpServers: [{ name: "clay-tools", command: "node", args: ["bridge.js"], env: [] }] } },
});
assert.strictEqual(handle.pushMessage("hello"), true);
handle.endInput();
assert.strictEqual(handle.pushMessage("must not queue"), false);
var events = [];
for await (var event of handle) {
events.push(event);
}
handle.close();
assert.ok(events.some(function(event) { return event.yokeType === "text_delta" && event.text === "hello"; }));
assert.ok(events.some(function(event) { return event.yokeType === "result" && event.sessionId === "session-1"; }));
assert.ok(FakeManager.instances[0].calls.some(function(call) {
return call.method === "session/set_config_option" && call.params.configId === "model";
}));
assert.strictEqual(FakeManager.instances[0].calls.some(function(call) {
return call.method === "session/set_config_option" && call.params.configId === "mode";
}), false);
var newSessionCall = FakeManager.instances[0].calls.find(function(call) { return call.method === "session/new"; });
assert.strictEqual(newSessionCall.params.mcpServers[0].name, "clay-tools");
await adapter.shutdown();
});
test("ACP vendor drivers extend YOKE without replacing the shared defaults", async function() {
FakeManager.instances = [];
var flags = {
registered: false,
initialized: false,
sessionReady: false,
opened: false,
supervised: false,
prompted: false,
stopped: false,
cancelled: false,
shutdown: false,
};
var driver = Object.assign({}, getProfile("opencode"), {
buildProcessOptions: function(ctx, base) {
base.args = base.args.concat(["--vendor-extension"]);
return base;
},
buildInitializeParams: function(ctx, base) {
base._meta = { vendorExtension: true };
return base;
},
registerRequestHandlers: function(ctx) {
ctx.acp.addRequestHandler("vendor/token", function() { return { token: "test" }; });
flags.registered = true;
},
onInitialize: function() { flags.initialized = true; },
extendCapabilities: function() { return { effort: true, sessionListing: true }; },
extendReadyResult: function(ctx, next) {
var result = next();
result.vendorReady = true;
return result;
},
supportedModels: function() { return ["vendor/rich-model"]; },
buildSessionParams: function(ctx, base) {
base._meta = { operation: ctx.operation };
return base;
},
afterSessionOpen: function() { flags.sessionReady = true; },
openSession: function(ctx, next) { flags.opened = true; return next(); },
ensureSafePermissionMode: function(ctx, next) { flags.supervised = true; return next(); },
setModel: function(ctx) {
return ctx.acp.send("vendor/set_model", { sessionId: ctx.state.sessionId, model: ctx.model });
},
buildPromptParams: function(ctx, base) {
base._meta = { vendorPrompt: true };
return base;
},
prompt: function(ctx, next) { flags.prompted = true; return next(); },
normalizeUpdate: function(ctx, next) {
var events = next();
events.push({ yokeType: "runtime_specific", vendor: ctx.vendor, eventType: "vendor/extra" });
return events;
},
buildResult: function(ctx, next) {
var event = next();
event.vendorUsage = { credits: 2 };
return event;
},
createToolServer: function(ctx) { return { definition: ctx.definition, vendor: ctx.vendor }; },
listSessions: function() { return [{ sessionId: "vendor-session" }]; },
stopTask: function() { flags.stopped = true; },
getContextUsage: function() { return { vendorTokens: 9 }; },
cancel: function() { flags.cancelled = true; },
onShutdown: function() { flags.shutdown = true; },
});
var adapter = createAcpAdapter("opencode", adapterOptions(driver));
var ready = await adapter.init();
var manager = FakeManager.instances[0];
assert.strictEqual(ready.capabilities.effort, true);
assert.strictEqual(ready.capabilities.sessionListing, true);
assert.strictEqual(ready.vendorReady, true);
assert.deepStrictEqual(await adapter.supportedModels(), ["vendor/rich-model"]);
assert.strictEqual(flags.registered, true);
assert.strictEqual(flags.initialized, true);
assert.ok(manager.requestHandlers["vendor/token"]);
assert.deepStrictEqual(manager.opts.args, ["acp", "--vendor-extension"]);
assert.deepStrictEqual(manager.calls[0].params._meta, { vendorExtension: true });
assert.deepStrictEqual(adapter.createToolServer({ name: "tool" }), {
definition: { name: "tool" },
vendor: "opencode",
});
assert.deepStrictEqual(await adapter.listSessions(), [{ sessionId: "vendor-session" }]);
var handle = await adapter.createQuery({ cwd: process.cwd(), model: "provider/default" });
handle.pushMessage("extension test");
var events = [];
for await (var event of handle) {
events.push(event);
if (event.yokeType === "result") break;
}
handle.close();
assert.strictEqual(flags.sessionReady, true);
assert.strictEqual(flags.opened, true);
assert.strictEqual(flags.supervised, true);
assert.strictEqual(flags.prompted, true);
var newCall = manager.calls.find(function(call) { return call.method === "session/new"; });
var promptCall = manager.calls.find(function(call) { return call.method === "session/prompt"; });
assert.deepStrictEqual(newCall.params._meta, { operation: "new" });
assert.deepStrictEqual(promptCall.params._meta, { vendorPrompt: true });
assert.ok(manager.calls.some(function(call) { return call.method === "vendor/set_model"; }));
assert.ok(events.some(function(event) { return event.eventType === "vendor/extra"; }));
assert.deepStrictEqual(events.find(function(event) { return event.yokeType === "result"; }).vendorUsage, { credits: 2 });
await handle.stopTask("task-1");
assert.strictEqual(flags.stopped, true);
assert.deepStrictEqual(await handle.getContextUsage(), { vendorTokens: 9 });
var cancelledHandle = await adapter.createQuery({ cwd: process.cwd() });
cancelledHandle.abort();
assert.strictEqual(flags.cancelled, true);
await adapter.shutdown();
assert.strictEqual(flags.shutdown, true);
});
test("shared ACP permission routing returns the nested selected outcome", async function() {
var manager = new FakeManager("/contract/acp", {});
manager.started = true;
var seenTool = null;
var handle = createAcpQueryHandle(manager, {
vendor: "opencode",
cwd: process.cwd(),
driver: {
mapPermissionRequest: function(ctx, next) {
var mapped = next();
mapped.toolName = "VendorExecute";
mapped.input = { command: "vendor-command" };
return mapped;
},
buildPermissionResponse: function(ctx, next) {
var result = next();
result._meta = { vendorPermission: true };
return result;
},
},
canUseTool: function(toolName, input) {
seenTool = { toolName: toolName, input: input };
return Promise.resolve({ behavior: "allow" });
},
});
handle.pushMessage("permission test");
await new Promise(function(resolve) { setImmediate(resolve); });
manager.handlers[0].fn({
id: 42,
method: "session/request_permission",
params: {
sessionId: "session-1",
toolCall: { toolCallId: "tool-1", kind: "execute", title: "Run" },
options: [
{ optionId: "allow-forever", kind: "allow_always" },
{ optionId: "allow", kind: "allow_once" },
{ optionId: "deny-forever", kind: "reject_always" },
{ optionId: "deny", kind: "reject_once" },
],
},
});
await new Promise(function(resolve) { setImmediate(resolve); });
var response = manager.calls.find(function(call) { return call.response && call.id === 42; });
assert.deepStrictEqual(seenTool, { toolName: "VendorExecute", input: { command: "vendor-command" } });
assert.deepStrictEqual(response.result, {
outcome: { outcome: "selected", optionId: "allow" },
_meta: { vendorPermission: true },
});
handle.abort();
});
test("shared ACP preserves modern tool names and raw input for permission routing", async function() {
var manager = new FakeManager("/contract/acp", {});
manager.started = true;
var seenTool = null;
var handle = createAcpQueryHandle(manager, {
vendor: "qwen",
cwd: process.cwd(),
canUseTool: function(toolName, input) {
seenTool = { toolName: toolName, input: input };
return Promise.resolve({ behavior: "allow" });
},
});
handle.pushMessage("permission metadata");
await new Promise(function(resolve) { setImmediate(resolve); });
manager.handlers[0].fn({ method: "session/update", params: { sessionId: "session-1", update: {
sessionUpdate: "tool_call", toolCallId: "tool-modern", name: "execute_command", title: "Run", rawInput: { command: "pwd" },
} } });
manager.handlers[0].fn({ id: 43, method: "session/request_permission", params: {
sessionId: "session-1", toolCall: { toolCallId: "tool-modern", title: "Run" },
options: [{ optionId: "allow", kind: "allow_once" }, { optionId: "deny", kind: "reject_once" }],
} });
await new Promise(function(resolve) { setImmediate(resolve); });
assert.deepStrictEqual(seenTool, { toolName: "execute_command", input: { command: "pwd" } });
assert.strictEqual(manager.calls.find(function(call) { return call.id === 43; }).result.outcome.optionId, "allow");
handle.abort();
});
test("shared ACP forwards dynamic command catalogs", function() {
var normalizer = require("../lib/yoke/acp-event-normalizer");
var state = normalizer.createEventState({ vendor: "opencode" });
assert.deepStrictEqual(normalizer.normalizeAcpUpdate({
sessionUpdate: "available_commands_update",
availableCommands: [{ name: "compact", description: "Compact context" }, { name: "review", description: "Review changes" }],
}, state), [{ yokeType: "commands_changed", commandNames: ["compact", "review"] }]);
});
test("shared ACP maps form elicitation into the query user-input lifecycle", async function() {
var manager = new FakeManager("/contract/acp", {});
manager.started = true;
var request = null;
var handle = createAcpQueryHandle(manager, {
vendor: "opencode",
cwd: process.cwd(),
onUserInputRequest: function(input, respond) {
request = input;
respond({ choice: "safe", enabled: "true" });
},
});
handle.pushMessage("ask me");
await new Promise(function(resolve) { setImmediate(resolve); });
manager.handlers[0].fn({ id: 44, method: "elicitation/create", params: {
sessionId: "session-1", mode: "form", message: "Choose a mode", requestedSchema: {
type: "object", properties: {
choice: { type: "string", description: "Mode", oneOf: [{ const: "safe", title: "Safe" }, { const: "fast", title: "Fast" }] },
enabled: { type: "boolean", description: "Enable it" },
},
},
} });
await new Promise(function(resolve) { setImmediate(resolve); });
assert.strictEqual(request.source, "acp_elicitation");
assert.deepStrictEqual(request.questions[0].options.map(function(option) { return option.label; }), ["safe", "fast"]);
assert.deepStrictEqual(manager.calls.find(function(call) { return call.id === 44; }).result, { action: "accept", content: { choice: "safe", enabled: true } });
handle.abort();
});
test("shared ACP routes raw elicitation callbacks with their native signature", async function() {
FakeManager.instances = [];
var adapter = createAcpAdapter("opencode", adapterOptions(getProfile("opencode")));
await adapter.init();
var manager = FakeManager.instances[0];
var seen = null;
var handle = await adapter.createQuery({
cwd: process.cwd(),
onElicitation: function(request, opts) {
seen = { request: request, signal: opts.signal };
return { action: "accept", content: { choice: "safe" } };
},
});
handle.pushMessage("ask raw");
await new Promise(function(resolve) { setImmediate(resolve); });
manager.handlers[0].fn({ id: 45, method: "elicitation/create", params: {
sessionId: "session-1", mode: "form", serverName: "Server", message: "Choose",
requestedSchema: { type: "object", properties: { choice: { enum: ["safe", "fast"] } } },
} });
await new Promise(function(resolve) { setImmediate(resolve); });
assert.strictEqual(seen.request.serverName, "Server");
assert.ok(seen.signal);
assert.deepStrictEqual(manager.calls.filter(function(call) { return call.id === 45; }), [{
id: 45, result: { action: "accept", content: { choice: "safe" } }, response: true,
}]);
handle.abort();
await adapter.shutdown();
});
test("shared ACP query abort cancels raw elicitation once and ignores late settlement", async function() {
var manager = new FakeManager("/contract/acp", {});
manager.started = true;
var deferred = [];
var callbackCalls = 0;
var handle = createAcpQueryHandle(manager, {
vendor: "opencode", cwd: process.cwd(),
onElicitation: function() {
callbackCalls++;
return new Promise(function(resolve, reject) {
deferred.push({ resolve: resolve, reject: reject });
});
},
});
handle.pushMessage("wait raw");
await new Promise(function(resolve) { setImmediate(resolve); });
manager.handlers[0].fn({ id: 451, method: "elicitation/create", params: {
sessionId: "session-1", mode: "form", message: "Wait",
} });
manager.handlers[0].fn({ id: 453, method: "elicitation/create", params: {
sessionId: "session-1", mode: "form", message: "Wait again",
} });
await new Promise(function(resolve) { setImmediate(resolve); });
assert.strictEqual(callbackCalls, 2);
handle.abort();
await new Promise(function(resolve) { setImmediate(resolve); });
assert.deepStrictEqual(manager.calls.filter(function(call) { return call.id === 451 || call.id === 453; }), [
{ id: 451, result: { action: "cancel" }, response: true },
{ id: 453, result: { action: "cancel" }, response: true },
]);
deferred[0].resolve({ action: "accept", content: { late: true } });
deferred[1].reject(new Error("late rejection"));
await new Promise(function(resolve) { setImmediate(resolve); });
assert.strictEqual(manager.calls.filter(function(call) { return call.id === 451; }).length, 1);
assert.strictEqual(manager.calls.filter(function(call) { return call.id === 453; }).length, 1);
});
test("shared ACP does not invoke raw elicitation after its signal is already aborted", function() {
var manager = new FakeManager("/contract/acp", {});
var controller = new AbortController();
var calls = 0;
controller.abort();
acpUserInput.handleElicitation(manager, {
id: 454, params: { mode: "form", message: "Too late" },
}, null, function() { calls++; }, controller.signal, "opencode", "OpenCode");
assert.strictEqual(calls, 0);
assert.deepStrictEqual(manager.calls, [{ id: 454, result: { action: "cancel" }, response: true }]);
});
test("shared ACP query abort cancels pending structured elicitation once", async function() {
var manager = new FakeManager("/contract/acp", {});
manager.started = true;
var lateRespond = null;
var handle = createAcpQueryHandle(manager, {
vendor: "opencode", cwd: process.cwd(),
onUserInputRequest: function(request, respond) { lateRespond = respond; },
});
handle.pushMessage("wait structured");
await new Promise(function(resolve) { setImmediate(resolve); });
manager.handlers[0].fn({ id: 452, method: "elicitation/create", params: {
sessionId: "session-1", mode: "form", message: "Wait",
} });
await new Promise(function(resolve) { setImmediate(resolve); });
handle.abort();
await new Promise(function(resolve) { setImmediate(resolve); });
assert.deepStrictEqual(manager.calls.filter(function(call) { return call.id === 452; }), [{
id: 452, result: { action: "cancel" }, response: true,
}]);
assert.strictEqual(lateRespond({ response: "late" }), false);
assert.strictEqual(manager.calls.filter(function(call) { return call.id === 452; }).length, 1);
});
test("shared ACP elicitation cancellation and missing handlers each respond once", async function() {
var manager = new FakeManager("/contract/acp", {});
manager.started = true;
var handle = createAcpQueryHandle(manager, {
vendor: "opencode", cwd: process.cwd(),
onUserInputRequest: function(request, respond) { respond.cancel("No answer"); },
onElicitation: function() { assert.fail("structured handler must take precedence"); },
});
handle.pushMessage("cancel");
await new Promise(function(resolve) { setImmediate(resolve); });
manager.handlers[0].fn({ id: 46, method: "elicitation/create", params: {
sessionId: "session-1", mode: "form", message: "Choose", requestedSchema: null,
} });
await new Promise(function(resolve) { setImmediate(resolve); });
assert.deepStrictEqual(manager.calls.filter(function(call) { return call.id === 46; }).map(function(call) { return call.result; }), [{ action: "cancel" }]);
handle.abort();
var noHandlerManager = new FakeManager("/contract/acp", {});
noHandlerManager.started = true;
var noHandler = createAcpQueryHandle(noHandlerManager, { vendor: "opencode", cwd: process.cwd() });
noHandler.pushMessage("decline");
await new Promise(function(resolve) { setImmediate(resolve); });
noHandlerManager.handlers[0].fn({ id: 47, method: "elicitation/create", params: {
sessionId: "session-1", mode: "form", message: "Choose",
} });
assert.deepStrictEqual(noHandlerManager.calls.filter(function(call) { return call.id === 47; }).map(function(call) { return call.result; }), [{ action: "cancel" }]);
noHandler.abort();
});
[
"yolo",
"auto_edit",
].forEach(function(unsafeMode) {
test("shared ACP sessions fail closed for unsafe mode " + unsafeMode, async function() {
var manager = new FakeManager("/contract/acp", {});
manager.started = true;
manager.sessionResult = {
sessionId: "unsafe-session",
configOptions: [{
id: "mode",
category: "mode",
currentValue: unsafeMode,
options: [{ value: unsafeMode, name: "Unsafe" }],
}],
};
var handle = createAcpQueryHandle(manager, { vendor: "opencode", cwd: process.cwd() });
handle.pushMessage("must not run");
var errors = [];
for await (var event of handle) {
if (event.yokeType === "error") errors.push(event.text);
}
assert.strictEqual(errors.length, 1);
assert.match(errors[0], /unsafe permission mode/);
assert.strictEqual(manager.calls.some(function(call) { return call.method === "session/prompt"; }), false);
});
});
test("shared ACP model changes propagate agent rejection", async function() {
var manager = new FakeManager("/contract/acp", {});
manager.started = true;
manager.rejectModelChange = true;
var handle = createAcpQueryHandle(manager, {
vendor: "opencode",
driver: getProfile("opencode"),
cwd: process.cwd(),
model: "auto",
});
handle.pushMessage("model test");
var sawResult = false;
for await (var event of handle) {
if (event.yokeType === "result") {
sawResult = true;
break;
}
}
assert.strictEqual(sawResult, true);
await assert.rejects(handle.setModel("provider/fast"), /model rejected/);
handle.close();
});