Files
NetMesh/electron/bridges/aiBridge/sdk/grokAcpDriver.test.cjs

1898 lines
63 KiB
JavaScript
Raw Permalink Normal View History

const test = require("node:test");
const assert = require("node:assert/strict");
const { EventEmitter } = require("node:events");
const path = require("node:path");
const {
ACP_PROTOCOL_VERSION,
authenticateGrokAcp,
buildGrokAcpInitializeParams,
buildGrokAcpPermissionResponse,
buildGrokAcpPromptParams,
buildGrokAcpSessionNewParams,
buildGrokAcpSessionResumeOrLoadParams,
buildGrokAcpSpawnArgs,
createJsonRpcClient,
establishGrokAcpSession,
handleGrokAcpMessage,
listGrokAcpModels,
parseGrokAcpAgentCapabilities,
parseGrokAcpModelCatalog,
planGrokAcpSessionEstablish,
resolveGrokAcpCwd,
runGrokAcpTurn,
selectGrokAcpAuthMethodId,
toAcpMcpEnvPairs,
toAcpMcpServers,
translateGrokAcpUpdate,
} = require("./grokAcpDriver.cjs");
const {
GROK_MCP_MODE_DISALLOWED_LOCAL_TOOLS,
shouldReportGrokProcessExitFailure,
} = require("./grokDriver.cjs");
const { getDriver, listBackends } = require("./index.cjs");
function makeEmitter() {
const calls = [];
return {
calls,
text: (value) => calls.push(["text", value]),
reasoning: (value) => calls.push(["reasoning", value]),
reasoningEnd: () => calls.push(["reasoningEnd"]),
toolCall: (name, args, id) => calls.push(["toolCall", name, args, id]),
toolResult: (id, result, name) => calls.push(["toolResult", id, result, name]),
sessionId: (id) => calls.push(["sessionId", id]),
planUpdate: (itemId, items, status) => calls.push(["planUpdate", itemId, items, status]),
usage: (usage) => calls.push(["usage", usage]),
emitDone: () => calls.push(["done"]),
emitError: (message) => calls.push(["error", message]),
};
}
test("buildGrokAcpSpawnArgs uses agent stdio, no-auto-update, and always-approve for non-observer", () => {
const args = buildGrokAcpSpawnArgs({
model: "grok-4.5",
permissionMode: "auto",
toolIntegrationMode: "skills",
});
assert.deepEqual(args, [
"--no-auto-update",
"agent",
"--always-approve",
"-m",
"grok-4.5",
"stdio",
]);
const observer = buildGrokAcpSpawnArgs({
permissionMode: "observer",
toolIntegrationMode: "skills",
});
assert.deepEqual(observer, ["--no-auto-update", "agent", "stdio"]);
});
test("buildGrokAcpSpawnArgs passes a selected reasoning effort separately from the model", () => {
assert.deepEqual(buildGrokAcpSpawnArgs({
model: "grok-4.6/high",
permissionMode: "auto",
toolIntegrationMode: "skills",
}), [
"--no-auto-update",
"agent",
"--always-approve",
"-m",
"grok-4.6",
"--reasoning-effort",
"high",
"stdio",
]);
});
test("parseGrokAcpModelCatalog exposes only each model's advertised reasoning efforts", () => {
const catalog = parseGrokAcpModelCatalog({
_meta: {
modelState: {
currentModelId: "grok-4.6",
availableModels: [
{
modelId: "grok-4.6",
name: "Grok 4.6",
description: "Latest",
_meta: {
supportsReasoningEffort: true,
reasoningEffort: "high",
reasoningEfforts: [
{ id: "deep", value: "xhigh", label: "Extra High" },
{ id: "high", value: "high", label: "High" },
{ id: "medium", value: "medium", label: "Medium" },
{ id: "low", value: "low", label: "Low" },
],
},
},
{
modelId: "ocx-fast",
name: "OCX Fast",
_meta: { supportsReasoningEffort: false },
},
],
},
},
});
assert.deepEqual(catalog, {
currentModelId: "grok-4.6/high",
models: [
{
id: "grok-4.6",
name: "Grok 4.6",
description: "Latest",
thinkingLevels: ["xhigh", "high", "medium", "low"],
defaultThinkingLevel: "high",
},
{ id: "ocx-fast", name: "OCX Fast" },
],
});
});
test("parseGrokAcpModelCatalog fills known legacy models without overriding explicit no-reasoning metadata", () => {
const catalog = parseGrokAcpModelCatalog({
_meta: {
modelState: {
currentModelId: "grok-4.5",
availableModels: [
{ modelId: "grok-4.5", name: "Grok 4.5" },
{
modelId: "grok-4.6",
name: "Grok 4.6",
_meta: { supportsReasoningEffort: false },
},
],
},
},
});
assert.deepEqual(catalog.models, [
{
id: "grok-4.5",
name: "Grok 4.5",
thinkingLevels: ["high", "medium", "low"],
defaultThinkingLevel: "high",
},
{ id: "grok-4.6", name: "Grok 4.6" },
]);
assert.equal(catalog.currentModelId, "grok-4.5/high");
});
test("parseGrokAcpModelCatalog uses a model-specific fallback when advertised options are empty", () => {
const catalog = parseGrokAcpModelCatalog({
_meta: {
modelState: {
currentModelId: "grok-4.5",
availableModels: [{
modelId: "grok-4.5",
name: "Grok 4.5",
_meta: {
supportsReasoningEffort: true,
reasoningEfforts: [],
},
}],
},
},
});
assert.equal(catalog.currentModelId, "grok-4.5/high");
assert.deepEqual(catalog.models[0].thinkingLevels, ["high", "medium", "low"]);
assert.ok(!catalog.models[0].thinkingLevels.includes("xhigh"));
});
test("listGrokAcpModels reads reasoning metadata from the initialize response", async () => {
const { EventEmitter } = require("node:events");
const child = new EventEmitter();
child.stdout = new EventEmitter();
child.stderr = new EventEmitter();
child.stdin = new EventEmitter();
child.stdin.end = () => {};
child.exitCode = null;
child.killed = false;
child.pid = 42;
child.kill = () => { child.killed = true; };
child.stdin.write = (line, callback) => {
const request = JSON.parse(String(line));
assert.equal(request.method, "initialize");
queueMicrotask(() => {
child.stdout.emit("data", Buffer.from(`${JSON.stringify({
jsonrpc: "2.0",
id: request.id,
result: {
_meta: {
modelState: {
currentModelId: "grok-4.6",
availableModels: [{
modelId: "grok-4.6",
name: "Grok 4.6",
_meta: {
supportsReasoningEffort: true,
reasoningEffort: "high",
reasoningEfforts: [
{ id: "high", value: "high" },
{ id: "low", value: "low" },
],
},
}],
},
},
},
})}\n`));
callback?.();
});
return true;
};
const signals = [];
const catalog = await listGrokAcpModels({
binPath: "/usr/bin/grok",
spawnImpl: (bin, args) => {
assert.equal(bin, "/usr/bin/grok");
assert.deepEqual(args, ["--no-auto-update", "agent", "stdio"]);
return child;
},
forceKillImpl: (_child, signal) => {
signals.push(signal);
if (signal === "SIGTERM") {
queueMicrotask(() => {
child.exitCode = 0;
child.emit("close", 0);
});
}
},
});
assert.equal(catalog.currentModelId, "grok-4.6/high");
assert.deepEqual(catalog.models[0].thinkingLevels, ["high", "low"]);
assert.equal(catalog.models[0].defaultThinkingLevel, "high");
assert.deepEqual(signals, ["SIGTERM"]);
});
test("listGrokAcpModels force-kills a model probe that ignores timeout shutdown", async () => {
const child = new EventEmitter();
child.stdout = new EventEmitter();
child.stderr = new EventEmitter();
child.stdin = new EventEmitter();
child.stdin.write = () => true;
child.stdin.end = () => {};
child.exitCode = null;
child.killed = false;
child.pid = 43;
const signals = [];
const keepAlive = setTimeout(() => {}, 50);
let catalog;
try {
catalog = await listGrokAcpModels({
binPath: "/usr/bin/grok",
spawnImpl: () => child,
timeoutMs: 2,
abortGraceMs: 2,
forceKillImpl: (_child, signal) => signals.push(signal),
});
await new Promise((resolve) => setTimeout(resolve, 10));
} finally {
clearTimeout(keepAlive);
}
assert.deepEqual(catalog, { currentModelId: null, models: [] });
assert.deepEqual(signals, ["SIGTERM", "SIGKILL"]);
});
test("listGrokAcpModels force-kills a model probe that ignores abort shutdown", async () => {
const child = new EventEmitter();
child.stdout = new EventEmitter();
child.stderr = new EventEmitter();
child.stdin = new EventEmitter();
child.stdin.write = () => true;
child.stdin.end = () => {};
child.exitCode = null;
child.killed = false;
child.pid = 44;
const signals = [];
const controller = new AbortController();
const pending = listGrokAcpModels({
binPath: "/usr/bin/grok",
spawnImpl: () => child,
signal: controller.signal,
timeoutMs: 100,
abortGraceMs: 2,
forceKillImpl: (_child, signal) => signals.push(signal),
});
controller.abort();
const catalog = await pending;
await new Promise((resolve) => setTimeout(resolve, 10));
assert.deepEqual(catalog, { currentModelId: null, models: [] });
assert.deepEqual(signals, ["SIGTERM", "SIGKILL"]);
});
test("registry Grok model discovery enriches legacy fallback reasoning levels", async () => {
const grokModule = require("./grokDriver.cjs");
const grokAcpModule = require("./grokAcpDriver.cjs");
const originalAcp = grokAcpModule.listGrokAcpModels;
const originalLegacy = grokModule.listGrokModels;
grokAcpModule.listGrokAcpModels = async () => ({ currentModelId: null, models: [] });
grokModule.listGrokModels = async () => ({
currentModelId: "grok-4.6",
models: [grokModule.applyGrokReasoningFallback({ id: "grok-4.6", name: "Grok 4.6" })],
});
try {
const catalog = await getDriver("grok").listModels({});
assert.equal(catalog.currentModelId, "grok-4.6/high");
assert.deepEqual(catalog.models[0].thinkingLevels, ["xhigh", "high", "medium", "low"]);
assert.equal(catalog.models[0].defaultThinkingLevel, "high");
} finally {
grokAcpModule.listGrokAcpModels = originalAcp;
grokModule.listGrokModels = originalLegacy;
}
});
test("registry Grok model discovery keeps ACP current model when its list is incomplete", async () => {
const grokModule = require("./grokDriver.cjs");
const grokAcpModule = require("./grokAcpDriver.cjs");
const originalAcp = grokAcpModule.listGrokAcpModels;
const originalLegacy = grokModule.listGrokModels;
grokAcpModule.listGrokAcpModels = async () => ({ currentModelId: "grok-4.6", models: [] });
grokModule.listGrokModels = async () => ({ currentModelId: "grok-4.5", models: [] });
try {
const catalog = await getDriver("grok").listModels({});
assert.equal(catalog.currentModelId, "grok-4.6/high");
assert.deepEqual(catalog.models, [{
id: "grok-4.6",
name: "grok-4.6",
thinkingLevels: ["xhigh", "high", "medium", "low"],
defaultThinkingLevel: "high",
}]);
} finally {
grokAcpModule.listGrokAcpModels = originalAcp;
grokModule.listGrokModels = originalLegacy;
}
});
test("buildGrokAcpSpawnArgs places global MCP lockdown before agent subcommand", () => {
const args = buildGrokAcpSpawnArgs({
model: "grok-4.5",
permissionMode: "auto",
toolIntegrationMode: "mcp",
});
const agentIdx = args.indexOf("agent");
const denyIdx = args.indexOf("--disallowed-tools");
const noUpdateIdx = args.indexOf("--no-auto-update");
const alwaysIdx = args.indexOf("--always-approve");
const modelIdx = args.indexOf("-m");
assert.ok(agentIdx >= 0, "must include agent subcommand");
assert.ok(denyIdx >= 0, "MCP mode must pass --disallowed-tools on ACP spawn");
// Live grok rejects --disallowed-tools AFTER `agent` (unexpected argument).
assert.ok(noUpdateIdx < agentIdx, "--no-auto-update must be before agent");
assert.ok(denyIdx < agentIdx, "--disallowed-tools must be before agent");
assert.ok(alwaysIdx > agentIdx, "--always-approve is agent-local (after agent)");
assert.ok(modelIdx > agentIdx, "-m is agent-local (after agent)");
assert.equal(args[args.length - 1], "stdio");
assert.deepEqual(args, [
"--no-auto-update",
"--disallowed-tools",
GROK_MCP_MODE_DISALLOWED_LOCAL_TOOLS.join(","),
"agent",
"--always-approve",
"-m",
"grok-4.5",
"stdio",
]);
const denied = String(args[denyIdx + 1] || "");
assert.match(denied, /run_terminal_command/);
assert.match(denied, /search_replace/);
assert.match(denied, /write/);
assert.doesNotMatch(denied, /mcp|netcatty/i);
// Skills mode: no lockdown list; global flags still precede agent.
const skills = buildGrokAcpSpawnArgs({
permissionMode: "auto",
toolIntegrationMode: "skills",
});
assert.ok(!skills.includes("--disallowed-tools"));
assert.ok(skills.indexOf("--no-auto-update") < skills.indexOf("agent"));
});
test("toAcpMcpEnvPairs keeps Grok session/new pair-array shape", () => {
// Live Grok rejects plain object env (Invalid params / McpServer enum).
assert.deepEqual(
toAcpMcpEnvPairs([{ name: "NETCATTY_MCP_PORT", value: "9" }]),
[{ name: "NETCATTY_MCP_PORT", value: "9" }],
);
// If a plain object sneaks in, still emit pairs (not a map).
assert.deepEqual(
toAcpMcpEnvPairs({ NETCATTY_MCP_PORT: "9", NETCATTY_MCP_TOKEN: "t" }),
[
{ name: "NETCATTY_MCP_PORT", value: "9" },
{ name: "NETCATTY_MCP_TOKEN", value: "t" },
],
);
assert.deepEqual(toAcpMcpEnvPairs(undefined), []);
});
test("toAcpMcpServers maps injectMcp env as name/value pairs for session/new", () => {
assert.deepEqual(
toAcpMcpServers([{
name: "netcatty-remote-hosts",
command: "node",
args: ["mcp.cjs"],
env: [{ name: "NETCATTY_MCP_PORT", value: "9" }, { name: "NETCATTY_MCP_TOKEN", value: "t" }],
}]),
[{
name: "netcatty-remote-hosts",
type: "stdio",
command: "node",
args: ["mcp.cjs"],
env: [
{ name: "NETCATTY_MCP_PORT", value: "9" },
{ name: "NETCATTY_MCP_TOKEN", value: "t" },
],
}],
);
// Must never emit object-map env (Grok session/new rejects it).
const mapped = toAcpMcpServers([{
name: "x",
command: "node",
args: [],
env: { A: "1" },
}]);
assert.ok(Array.isArray(mapped[0].env));
assert.equal(mapped[0].type, "stdio");
assert.deepEqual(mapped[0].env, [{ name: "A", value: "1" }]);
});
test("buildGrokAcpSessionNewParams injects MCP servers and MCP-mode rules", () => {
const params = buildGrokAcpSessionNewParams({
cwd: "/repo",
permissionMode: "auto",
toolIntegrationMode: "mcp",
injectedMcpServers: [{
name: "netcatty-remote-hosts",
command: "node",
args: ["mcp.cjs"],
env: [{ name: "NETCATTY_MCP_PORT", value: "1" }],
}],
});
assert.equal(params.cwd, resolveGrokAcpCwd("/repo"));
assert.equal(params.mcpServers[0].name, "netcatty-remote-hosts");
assert.equal(params.mcpServers[0].type, "stdio");
assert.ok(Array.isArray(params.mcpServers[0].env));
assert.deepEqual(params.mcpServers[0].env, [{ name: "NETCATTY_MCP_PORT", value: "1" }]);
// Explicitly forbid the broken object-map shape in the shipped builder output.
assert.equal(typeof params.mcpServers[0].env.NETCATTY_MCP_PORT, "undefined");
assert.equal(params._meta.yoloMode, true);
assert.match(String(params._meta.rules || ""), /netcatty-remote-hosts|MCP mode/i);
assert.match(String(params._meta.rules || ""), /run_terminal_command|search_replace|write/);
// Soft rules list the same local tools as the hard CLI deny list.
for (const tool of GROK_MCP_MODE_DISALLOWED_LOCAL_TOOLS) {
assert.match(String(params._meta.rules || ""), new RegExp(tool));
}
const skills = buildGrokAcpSessionNewParams({
cwd: "/repo",
permissionMode: "auto",
toolIntegrationMode: "skills",
injectedMcpServers: [],
});
assert.equal(skills._meta.yoloMode, true);
assert.equal(skills._meta.rules, undefined);
});
test("planGrokAcpSessionEstablish prefers resume then load then new", () => {
assert.deepEqual(planGrokAcpSessionEstablish({ resumeSessionId: null }), ["new"]);
assert.deepEqual(planGrokAcpSessionEstablish({
resumeSessionId: "s1",
agentCapabilities: { resume: true, loadSession: true, hasCapabilityInfo: true },
}), ["resume", "load", "new"]);
assert.deepEqual(planGrokAcpSessionEstablish({
resumeSessionId: "s1",
agentCapabilities: { resume: true, loadSession: false, hasCapabilityInfo: true },
}), ["resume", "new"]);
assert.deepEqual(planGrokAcpSessionEstablish({
resumeSessionId: "s1",
agentCapabilities: { resume: false, loadSession: true, hasCapabilityInfo: true },
}), ["load", "new"]);
// Unknown capabilities: try resume + load then new (Grok versions vary).
assert.deepEqual(planGrokAcpSessionEstablish({
resumeSessionId: "s1",
agentCapabilities: { resume: false, loadSession: false, hasCapabilityInfo: false },
}), ["resume", "load", "new"]);
});
test("parseGrokAcpAgentCapabilities reads loadSession and sessionCapabilities.resume", () => {
assert.deepEqual(parseGrokAcpAgentCapabilities({
agentCapabilities: {
loadSession: true,
sessionCapabilities: { resume: {} },
},
}), { loadSession: true, resume: true, hasCapabilityInfo: true });
assert.deepEqual(parseGrokAcpAgentCapabilities({}), {
loadSession: false,
resume: false,
hasCapabilityInfo: false,
});
});
test("selectGrokAcpAuthMethodId follows xAI sample precedence", () => {
assert.deepEqual(
selectGrokAcpAuthMethodId({ authMethods: [] }, {}),
{ methodId: null, required: false },
);
assert.deepEqual(
selectGrokAcpAuthMethodId({
authMethods: [{ id: "xai.api_key" }, { id: "cached_token" }],
}, { XAI_API_KEY: "xai-test" }),
{ methodId: "xai.api_key", required: true },
);
assert.deepEqual(
selectGrokAcpAuthMethodId({
authMethods: [{ id: "xai.api_key" }, { id: "cached_token" }],
}, {}),
{ methodId: "cached_token", required: true },
);
assert.deepEqual(
selectGrokAcpAuthMethodId({ authMethods: [{ id: "other" }] }, {}),
{ methodId: null, required: true },
);
});
test("authenticateGrokAcp skips when no methods; errors when required method missing", async () => {
const calls = [];
const rpc = {
async request(method, params) {
calls.push([method, params]);
return {};
},
};
const skipped = await authenticateGrokAcp(rpc, { authMethods: [] }, {});
assert.equal(skipped.skipped, true);
assert.deepEqual(calls, []);
await assert.rejects(
() => authenticateGrokAcp(rpc, { authMethods: [{ id: "unknown" }] }, {}),
/grok login|XAI_API_KEY/i,
);
const ok = await authenticateGrokAcp(
rpc,
{ authMethods: [{ id: "cached_token" }] },
{},
);
assert.equal(ok.methodId, "cached_token");
assert.equal(calls[0][0], "authenticate");
assert.equal(calls[0][1].methodId, "cached_token");
assert.equal(calls[0][1]._meta.headless, true);
});
test("establishGrokAcpSession tries resume then falls back to load then new", async () => {
const methods = [];
const rpc = {
async request(method, params) {
methods.push([method, params]);
if (method === "session/resume") throw new Error("resume unsupported");
if (method === "session/load") {
// Simulate history replay side-channel; caller keeps acceptUpdates false.
return { sessionId: "loaded-1" };
}
throw new Error(`unexpected ${method}`);
},
};
const result = await establishGrokAcpSession(rpc, {
resumeSessionId: "prev-1",
cwd: "/repo",
injectedMcpServers: [],
agentCapabilities: { resume: true, loadSession: true, hasCapabilityInfo: true },
});
assert.equal(result.method, "load");
assert.equal(result.sessionId, "loaded-1");
assert.deepEqual(methods.map((m) => m[0]), ["session/resume", "session/load"]);
assert.equal(methods[0][1].sessionId, "prev-1");
assert.equal(methods[0][1].cwd, resolveGrokAcpCwd("/repo"));
});
test("establishGrokAcpSession prefers resume when it succeeds", async () => {
const methods = [];
const rpc = {
async request(method, params) {
methods.push(method);
if (method === "session/resume") return {};
throw new Error(`unexpected ${method}`);
},
};
const result = await establishGrokAcpSession(rpc, {
resumeSessionId: "prev-2",
cwd: path.resolve("/repo"),
injectedMcpServers: [],
agentCapabilities: { resume: true, loadSession: true, hasCapabilityInfo: true },
});
assert.equal(result.method, "resume");
assert.equal(result.sessionId, "prev-2");
assert.deepEqual(methods, ["session/resume"]);
});
test("buildGrokAcpSessionResumeOrLoadParams keeps absolute cwd and MCP pairs", () => {
const params = buildGrokAcpSessionResumeOrLoadParams({
sessionId: "s1",
cwd: "relative-dir",
injectedMcpServers: [{
name: "netcatty-remote-hosts",
command: "node",
args: ["x"],
env: { A: "1" },
}],
});
assert.equal(params.sessionId, "s1");
assert.equal(params.cwd, resolveGrokAcpCwd("relative-dir"));
assert.ok(path.isAbsolute(params.cwd));
assert.deepEqual(params.mcpServers[0].env, [{ name: "A", value: "1" }]);
});
test("buildGrokAcpInitializeParams and prompt params follow ACP shapes", () => {
const init = buildGrokAcpInitializeParams();
assert.equal(init.protocolVersion, ACP_PROTOCOL_VERSION);
assert.equal(init.clientInfo.name, "netcatty");
assert.deepEqual(
buildGrokAcpPromptParams("sess-1", "hello"),
{ sessionId: "sess-1", prompt: [{ type: "text", text: "hello" }] },
);
});
test("translateGrokAcpUpdate maps text, thought, tools to canonical emitter events", () => {
const emitter = makeEmitter();
const state = {};
translateGrokAcpUpdate({
sessionUpdate: "agent_thought_chunk",
content: { text: "plan" },
}, emitter, state);
translateGrokAcpUpdate({
sessionUpdate: "agent_message_chunk",
content: { text: "Hi" },
}, emitter, state);
translateGrokAcpUpdate({
sessionUpdate: "tool_call",
toolCallId: "c1",
toolName: "mcp__netcatty-remote-hosts__get_environment",
rawInput: { x: 1 },
}, emitter, state);
translateGrokAcpUpdate({
sessionUpdate: "tool_call_update",
toolCallId: "c1",
status: "completed",
rawOutput: { ok: true },
}, emitter, state);
assert.deepEqual(emitter.calls, [
["reasoning", "plan"],
["reasoningEnd"],
["text", "Hi"],
["toolCall", "mcp__netcatty-remote-hosts__get_environment", { x: 1 }, "c1"],
["toolResult", "c1", "{\"ok\":true}", "mcp__netcatty-remote-hosts__get_environment"],
]);
});
test("translateGrokAcpUpdate plan uses shared { text, completed } shape not content/status", () => {
const emitter = makeEmitter();
translateGrokAcpUpdate({
sessionUpdate: "plan",
entries: [
{ content: "Map APIs", status: "completed" },
{ text: "Write tests", status: "pending" },
],
}, emitter, {});
const planCall = emitter.calls.find((c) => c[0] === "planUpdate");
assert.ok(planCall, "plan must emit planUpdate");
assert.deepEqual(planCall[2], [
{ text: "Map APIs", completed: true },
{ text: "Write tests", completed: false },
]);
assert.equal(planCall[3], "running");
// Adapter only treats top-level "completed" as done; "updated" stuck as running forever.
assert.notEqual(planCall[3], "updated");
});
test("createJsonRpcClient correlates request ids and parses lines", async () => {
const written = [];
const client = createJsonRpcClient({
write: (line) => written.push(line),
onMessage: (message, pending) => {
if (message.id != null && pending.has(message.id)) {
const waiter = pending.get(message.id);
pending.delete(message.id);
waiter.resolve(message.result);
}
},
});
const pending = client.request("initialize", { protocolVersion: 1 });
assert.match(written[0], /"method":"initialize"/);
const sent = JSON.parse(written[0]);
client.handleLine(JSON.stringify({ jsonrpc: "2.0", id: sent.id, result: { ok: true } }));
assert.deepEqual(await pending, { ok: true });
});
test("handleGrokAcpMessage routes session/update notifications", () => {
const emitter = makeEmitter();
const state = { acceptUpdates: true };
const pending = new Map();
handleGrokAcpMessage({
jsonrpc: "2.0",
method: "session/update",
params: {
sessionId: "s1",
update: {
sessionUpdate: "agent_message_chunk",
content: { text: "ok" },
},
},
}, { emitter, state, pending });
assert.equal(state.sessionId, "s1");
assert.deepEqual(emitter.calls, [
["text", "ok"],
["sessionId", "s1"],
]);
});
test("handleGrokAcpMessage suppresses session/load history until prompt accepts updates", () => {
const emitter = makeEmitter();
const state = { acceptUpdates: false };
const pending = new Map();
// Historical replay during session/load must not pollute the current turn.
handleGrokAcpMessage({
jsonrpc: "2.0",
method: "session/update",
params: {
sessionId: "s1",
update: {
sessionUpdate: "agent_message_chunk",
content: { text: "previous turn reply" },
},
},
}, { emitter, state, pending });
assert.equal(state.sessionId, "s1");
assert.deepEqual(emitter.calls, []);
state.acceptUpdates = true;
handleGrokAcpMessage({
jsonrpc: "2.0",
method: "session/update",
params: {
sessionId: "s1",
update: {
sessionUpdate: "agent_message_chunk",
content: { text: "only this turn" },
},
},
}, { emitter, state, pending });
assert.deepEqual(emitter.calls, [["text", "only this turn"]]);
});
test("runGrokAcpTurn drives initialize/authenticate/session/new/prompt via fixture RPC", async () => {
const emitter = makeEmitter();
const methods = [];
const result = await runGrokAcpTurn({
prompt: "hi",
binPath: "/usr/bin/grok",
cwd: "/repo",
permissionMode: "auto",
toolIntegrationMode: "mcp",
env: { XAI_API_KEY: "xai-test" },
injectedMcpServers: [{
name: "netcatty-remote-hosts",
command: "node",
args: ["mcp.cjs"],
env: [{ name: "NETCATTY_MCP_PORT", value: "7" }],
}],
emitter,
rpcClientFactory: ({ emitter: em, state }) => ({
async request(method, params) {
methods.push([method, params]);
if (method === "initialize") {
return {
protocolVersion: ACP_PROTOCOL_VERSION,
authMethods: [{ id: "xai.api_key" }, { id: "cached_token" }],
};
}
if (method === "authenticate") {
assert.equal(params.methodId, "xai.api_key");
assert.equal(params._meta.headless, true);
return {};
}
if (method === "session/new") {
assert.equal(params.cwd, resolveGrokAcpCwd("/repo"));
assert.equal(params.mcpServers[0].name, "netcatty-remote-hosts");
assert.equal(params.mcpServers[0].type, "stdio");
assert.ok(Array.isArray(params.mcpServers[0].env));
assert.deepEqual(
params.mcpServers[0].env,
[{ name: "NETCATTY_MCP_PORT", value: "7" }],
);
return { sessionId: "acp-sess-1" };
}
if (method === "session/prompt") {
// Simulate streamed ACP updates during the prompt
translateGrokAcpUpdate({
sessionUpdate: "agent_message_chunk",
content: { text: "hello-acp" },
}, em, state);
return { stopReason: "end_turn" };
}
throw new Error(`unexpected method ${method}`);
},
}),
});
assert.equal(result.runtime, "acp");
assert.equal(result.sessionId, "acp-sess-1");
assert.deepEqual(
methods.map((m) => m[0]),
["initialize", "authenticate", "session/new", "session/prompt"],
);
assert.ok(emitter.calls.some((c) => c[0] === "text" && c[1] === "hello-acp"));
assert.ok(emitter.calls.some((c) => c[0] === "sessionId" && c[1] === "acp-sess-1"));
assert.ok(emitter.calls.some((c) => c[0] === "done"));
});
test("runGrokAcpTurn seeds history when resume/load fail and session/new is used", async () => {
const emitter = makeEmitter();
const methods = [];
const historySeed = [
"[Conversation context replay: the agent SDK may be starting from a fresh local session, so use these prior turns as context and answer only the latest user request.]",
"USER: earlier question",
"ASSISTANT: earlier answer",
].join("\n");
await runGrokAcpTurn({
prompt: "latest only",
binPath: "/usr/bin/grok",
resumeSessionId: "stale-sess",
historySeed,
permissionMode: "auto",
emitter,
rpcClientFactory: () => ({
async request(method, params) {
methods.push([method, params]);
if (method === "initialize") {
return {
protocolVersion: ACP_PROTOCOL_VERSION,
agentCapabilities: {
loadSession: true,
sessionCapabilities: { resume: {} },
},
authMethods: [],
};
}
if (method === "session/resume") throw new Error("missing session");
if (method === "session/load") throw new Error("cannot load");
if (method === "session/new") return { sessionId: "fresh-sess" };
if (method === "session/prompt") {
assert.equal(params.sessionId, "fresh-sess");
const text = params.prompt?.[0]?.text || "";
assert.match(text, /Conversation context replay/);
assert.match(text, /earlier question/);
assert.match(text, /latest only$/);
return { stopReason: "end_turn" };
}
throw new Error(`unexpected method ${method}`);
},
}),
});
assert.deepEqual(methods.map((m) => m[0]), [
"initialize",
"session/resume",
"session/load",
"session/new",
"session/prompt",
]);
assert.ok(emitter.calls.some((c) => c[0] === "sessionId" && c[1] === "fresh-sess"));
});
test("runGrokAcpTurn does not seed history when session/resume succeeds", async () => {
const emitter = makeEmitter();
const historySeed = "USER: earlier\nASSISTANT: earlier answer";
await runGrokAcpTurn({
prompt: "latest only",
binPath: "/usr/bin/grok",
resumeSessionId: "live-sess",
historySeed,
permissionMode: "auto",
emitter,
rpcClientFactory: () => ({
async request(method, params) {
if (method === "initialize") {
return {
protocolVersion: ACP_PROTOCOL_VERSION,
agentCapabilities: {
loadSession: true,
sessionCapabilities: { resume: {} },
},
authMethods: [],
};
}
if (method === "session/resume") return {};
if (method === "session/prompt") {
const text = params.prompt?.[0]?.text || "";
assert.equal(text, "latest only");
assert.doesNotMatch(text, /earlier/);
return { stopReason: "end_turn" };
}
throw new Error(`unexpected method ${method}`);
},
}),
});
});
test("runGrokAcpTurn prefers session/resume and keeps load history off the emitter", async () => {
const emitter = makeEmitter();
const methods = [];
const result = await runGrokAcpTurn({
prompt: "follow up",
binPath: "/usr/bin/grok",
cwd: "/repo",
resumeSessionId: "prior-sess",
permissionMode: "auto",
toolIntegrationMode: "mcp",
emitter,
rpcClientFactory: ({ emitter: em, state }) => ({
async request(method, params) {
methods.push(method);
if (method === "initialize") {
return {
protocolVersion: ACP_PROTOCOL_VERSION,
agentCapabilities: {
loadSession: true,
sessionCapabilities: { resume: {} },
},
authMethods: [],
};
}
if (method === "session/resume") {
assert.equal(params.sessionId, "prior-sess");
// Resume must not replay; even if a rogue update arrives pre-prompt, suppress it.
handleGrokAcpMessage({
jsonrpc: "2.0",
method: "session/update",
params: {
sessionId: "prior-sess",
update: {
sessionUpdate: "agent_message_chunk",
content: { text: "SHOULD NOT APPEAR" },
},
},
}, { emitter: em, state, pending: new Map() });
return {};
}
if (method === "session/prompt") {
assert.equal(state.acceptUpdates, true);
translateGrokAcpUpdate({
sessionUpdate: "agent_message_chunk",
content: { text: "only this turn" },
}, em, state);
return { stopReason: "end_turn" };
}
throw new Error(`unexpected method ${method}`);
},
}),
});
assert.equal(result.sessionId, "prior-sess");
assert.deepEqual(methods, ["initialize", "session/resume", "session/prompt"]);
assert.deepEqual(
emitter.calls.filter((c) => c[0] === "text"),
[["text", "only this turn"]],
);
assert.ok(!emitter.calls.some((c) => c[0] === "text" && String(c[1]).includes("SHOULD NOT")));
});
test("runGrokAcpTurn falls back to session/load and suppresses history replay text", async () => {
const emitter = makeEmitter();
const methods = [];
const result = await runGrokAcpTurn({
prompt: "follow up",
binPath: "/usr/bin/grok",
resumeSessionId: "prior-load",
permissionMode: "auto",
emitter,
rpcClientFactory: ({ emitter: em, state }) => ({
async request(method) {
methods.push(method);
if (method === "initialize") {
return {
protocolVersion: ACP_PROTOCOL_VERSION,
agentCapabilities: {
loadSession: true,
sessionCapabilities: { resume: {} },
},
authMethods: [],
};
}
if (method === "session/resume") throw new Error("resume not available");
if (method === "session/load") {
assert.equal(state.acceptUpdates, false);
// Load-style history replay must not hit the current bubble.
handleGrokAcpMessage({
jsonrpc: "2.0",
method: "session/update",
params: {
sessionId: "prior-load",
update: {
sessionUpdate: "agent_message_chunk",
content: { text: "previous turn reply" },
},
},
}, { emitter: em, state, pending: new Map() });
return { sessionId: "prior-load" };
}
if (method === "session/prompt") {
translateGrokAcpUpdate({
sessionUpdate: "agent_message_chunk",
content: { text: "fresh reply" },
}, em, state);
return { stopReason: "end_turn" };
}
throw new Error(`unexpected method ${method}`);
},
}),
});
assert.equal(result.sessionId, "prior-load");
assert.deepEqual(methods, [
"initialize",
"session/resume",
"session/load",
"session/prompt",
]);
assert.deepEqual(
emitter.calls.filter((c) => c[0] === "text"),
[["text", "fresh reply"]],
);
});
test("runGrokAcpTurn maps missing auth methods to a clear user error", async () => {
const emitter = makeEmitter();
await runGrokAcpTurn({
prompt: "hi",
binPath: "/usr/bin/grok",
emitter,
rpcClientFactory: () => ({
async request(method) {
if (method === "initialize") {
return {
protocolVersion: ACP_PROTOCOL_VERSION,
authMethods: [{ id: "weird-method" }],
};
}
throw new Error(`unexpected method ${method}`);
},
}),
});
const err = emitter.calls.find((c) => c[0] === "error");
assert.ok(err);
assert.match(String(err[1]), /not logged in|grok login|XAI_API_KEY/i);
});
test("runGrokAcpTurn reports missing CLI clearly", async () => {
const emitter = makeEmitter();
const result = await runGrokAcpTurn({
prompt: "hi",
binPath: "",
emitter,
});
assert.equal(result.sessionId, null);
assert.equal(result.runtime, "acp");
assert.match(String(emitter.calls[0]?.[1] || ""), /not found/i);
});
test("runGrokAcpTurn ignores benign stdin EPIPE after turn completes", async () => {
// Matches processErrorGuards: EPIPE must not crash main or fail a completed turn.
const { EventEmitter } = require("node:events");
const emitter = makeEmitter();
let stdinRef = null;
function makeFakeChild() {
const child = new EventEmitter();
child.stdout = new EventEmitter();
child.stderr = new EventEmitter();
const stdin = new EventEmitter();
stdin.destroyed = false;
stdin.writable = true;
stdin.write = (line, cb) => {
let msg;
try { msg = JSON.parse(String(line).trim()); } catch {
if (typeof cb === "function") cb();
return true;
}
if (msg?.id == null) {
if (typeof cb === "function") cb();
return true;
}
queueMicrotask(() => {
if (msg.method === "initialize") {
child.stdout.emit("data", Buffer.from(`${JSON.stringify({
jsonrpc: "2.0",
id: msg.id,
result: { protocolVersion: ACP_PROTOCOL_VERSION, authMethods: [] },
})}\n`));
} else if (msg.method === "session/new") {
child.stdout.emit("data", Buffer.from(`${JSON.stringify({
jsonrpc: "2.0",
id: msg.id,
result: { sessionId: "s-epipe" },
})}\n`));
} else if (msg.method === "session/prompt") {
child.stdout.emit("data", Buffer.from(`${JSON.stringify({
jsonrpc: "2.0",
method: "session/update",
params: {
sessionId: "s-epipe",
update: { sessionUpdate: "agent_message_chunk", content: { text: "ok" } },
},
})}\n`));
child.stdout.emit("data", Buffer.from(`${JSON.stringify({
jsonrpc: "2.0",
id: msg.id,
result: { stopReason: "end_turn" },
})}\n`));
// Teardown race: peer closed stdin after prompt success.
queueMicrotask(() => {
const err = new Error("write EPIPE");
err.code = "EPIPE";
stdin.emit("error", err);
child.emit("close", 1);
});
} else {
child.stdout.emit("data", Buffer.from(`${JSON.stringify({
jsonrpc: "2.0",
id: msg.id,
result: {},
})}\n`));
}
if (typeof cb === "function") cb();
});
return true;
};
stdin.end = () => {};
child.stdin = stdin;
stdinRef = stdin;
child.pid = 3;
child.killed = false;
child.exitCode = null;
child.kill = () => {
child.killed = true;
queueMicrotask(() => child.emit("close", 1));
};
return child;
}
await runGrokAcpTurn({
prompt: "hi",
binPath: "C:\\fake\\grok.exe",
permissionMode: "auto",
emitter,
spawnImpl: () => makeFakeChild(),
forceKillImpl: (c) => { c.kill(); },
});
assert.ok(stdinRef, "stdin must have been wired");
assert.ok(emitter.calls.some((c) => c[0] === "done"));
assert.ok(!emitter.calls.some((c) => c[0] === "error"), "post-completion EPIPE is benign");
});
test("runGrokAcpTurn forwards usage from session/prompt result", async () => {
const emitter = makeEmitter();
await runGrokAcpTurn({
prompt: "hi",
binPath: "/usr/bin/grok",
permissionMode: "auto",
emitter,
rpcClientFactory: ({ emitter: em, state }) => ({
async request(method) {
if (method === "initialize") {
return { protocolVersion: ACP_PROTOCOL_VERSION, authMethods: [] };
}
if (method === "session/new") return { sessionId: "s-usage" };
if (method === "session/prompt") {
translateGrokAcpUpdate({
sessionUpdate: "agent_message_chunk",
content: { text: "ok" },
}, em, state);
// Live Grok shape under _meta.usage (camelCase + cachedReadTokens).
return {
stopReason: "end_turn",
_meta: {
inputTokens: 27144,
outputTokens: 29,
totalTokens: 27174,
cachedReadTokens: 2560,
reasoningTokens: 24,
usage: {
inputTokens: 27144,
outputTokens: 29,
totalTokens: 27173,
cachedReadTokens: 2560,
reasoningTokens: 24,
},
},
};
}
throw new Error(`unexpected ${method}`);
},
}),
});
const usage = emitter.calls.find((c) => c[0] === "usage");
assert.ok(usage);
assert.deepEqual(usage[1], {
inputTokens: 27144,
cachedInputTokens: 2560,
outputTokens: 29,
reasoningTokens: 24,
totalTokens: 27173,
});
assert.ok(emitter.calls.some((c) => c[0] === "done"));
});
test("runGrokAcpTurn emits usage on prompt response even when process closes same tick", async () => {
// Repro of Token ~1: onPromptComplete resolves promptDone in the same turn as
// the prompt RPC result; Promise.race often returns "closed" and used to skip usage.
const { EventEmitter } = require("node:events");
const emitter = makeEmitter();
function makeFakeChild() {
const child = new EventEmitter();
child.stdout = new EventEmitter();
child.stderr = new EventEmitter();
child.stdin = {
destroyed: false,
write(line) {
let msg;
try { msg = JSON.parse(String(line).trim()); } catch { return; }
if (msg?.id == null) return;
queueMicrotask(() => {
if (msg.method === "initialize") {
child.stdout.emit("data", Buffer.from(`${JSON.stringify({
jsonrpc: "2.0", id: msg.id,
result: { protocolVersion: ACP_PROTOCOL_VERSION, authMethods: [] },
})}\n`));
} else if (msg.method === "session/new") {
child.stdout.emit("data", Buffer.from(`${JSON.stringify({
jsonrpc: "2.0", id: msg.id, result: { sessionId: "s-race" },
})}\n`));
} else if (msg.method === "session/prompt") {
child.stdout.emit("data", Buffer.from(`${JSON.stringify({
jsonrpc: "2.0", method: "session/update",
params: {
sessionId: "s-race",
update: { sessionUpdate: "agent_message_chunk", content: { text: "long reply…" } },
},
})}\n`));
// Prompt result with real Grok usage, then same-tick process close
// (simulates onPromptComplete → promptDone racing the await).
child.stdout.emit("data", Buffer.from(`${JSON.stringify({
jsonrpc: "2.0",
id: msg.id,
result: {
stopReason: "end_turn",
_meta: {
usage: {
inputTokens: 1200,
outputTokens: 800,
totalTokens: 2000,
cachedReadTokens: 100,
reasoningTokens: 50,
},
},
},
})}\n`));
queueMicrotask(() => child.emit("close", 1));
} else {
child.stdout.emit("data", Buffer.from(`${JSON.stringify({
jsonrpc: "2.0", id: msg.id, result: {},
})}\n`));
}
});
},
end() {},
};
child.pid = 7;
child.killed = false;
child.exitCode = null;
child.kill = () => {
child.killed = true;
queueMicrotask(() => child.emit("close", 1));
};
return child;
}
await runGrokAcpTurn({
prompt: "write a long essay",
binPath: "C:\\fake\\grok.exe",
permissionMode: "auto",
emitter,
spawnImpl: () => makeFakeChild(),
forceKillImpl: (c) => { c.kill(); },
});
const usage = emitter.calls.find((c) => c[0] === "usage");
assert.ok(usage, "usage must be emitted even if process closes in the same tick");
assert.deepEqual(usage[1], {
inputTokens: 1200,
cachedInputTokens: 100,
outputTokens: 800,
reasoningTokens: 50,
totalTokens: 2000,
});
assert.ok(emitter.calls.some((c) => c[0] === "done"));
assert.ok(!emitter.calls.some((c) => c[0] === "error"));
});
test("runGrokAcpTurn does not treat post-prompt exit code 1 as failure for tool-only turns", async () => {
// Repro: successful session/prompt (tools only, no assistant text) then
// process teardown with code 1 (Windows taskkill). Must emitDone, not error.
const { EventEmitter } = require("node:events");
const emitter = makeEmitter();
function makeFakeChild() {
const child = new EventEmitter();
child.stdout = new EventEmitter();
child.stderr = new EventEmitter();
child.stdin = {
destroyed: false,
write(line) {
let msg;
try { msg = JSON.parse(String(line).trim()); } catch { return; }
if (msg?.id == null) return;
queueMicrotask(() => {
if (msg.method === "initialize") {
child.stdout.emit("data", Buffer.from(`${JSON.stringify({
jsonrpc: "2.0",
id: msg.id,
result: { protocolVersion: ACP_PROTOCOL_VERSION, authMethods: [] },
})}\n`));
} else if (msg.method === "session/new") {
child.stdout.emit("data", Buffer.from(`${JSON.stringify({
jsonrpc: "2.0",
id: msg.id,
result: { sessionId: "s-tool" },
})}\n`));
} else if (msg.method === "session/prompt") {
child.stdout.emit("data", Buffer.from(`${JSON.stringify({
jsonrpc: "2.0",
method: "session/update",
params: {
sessionId: "s-tool",
update: {
sessionUpdate: "tool_call",
toolCallId: "t1",
toolName: "read_file",
rawInput: {},
},
},
})}\n`));
child.stdout.emit("data", Buffer.from(`${JSON.stringify({
jsonrpc: "2.0",
method: "session/update",
params: {
sessionId: "s-tool",
update: {
sessionUpdate: "tool_call_update",
toolCallId: "t1",
status: "completed",
rawOutput: "ok",
},
},
})}\n`));
child.stdout.emit("data", Buffer.from(`${JSON.stringify({
jsonrpc: "2.0",
id: msg.id,
result: { stopReason: "end_turn" },
})}\n`));
queueMicrotask(() => child.emit("close", 1));
} else {
child.stdout.emit("data", Buffer.from(`${JSON.stringify({
jsonrpc: "2.0",
id: msg.id,
result: {},
})}\n`));
}
});
},
end() {},
};
child.pid = 4242;
child.killed = false;
child.exitCode = null;
child.kill = () => {
child.killed = true;
queueMicrotask(() => child.emit("close", 1));
};
return child;
}
const result = await runGrokAcpTurn({
prompt: "tool only",
binPath: "C:\\fake\\grok.exe",
permissionMode: "auto",
emitter,
spawnImpl: () => makeFakeChild(),
forceKillImpl: (child) => { child.kill(); },
});
assert.equal(result.sessionId, "s-tool");
assert.ok(emitter.calls.some((c) => c[0] === "toolCall"));
assert.ok(emitter.calls.some((c) => c[0] === "done"));
assert.ok(!emitter.calls.some((c) => c[0] === "error"), "post-prompt exit 1 must not emitError");
});
test("runGrokAcpTurn still errors when process dies mid-turn with no completion", async () => {
const { EventEmitter } = require("node:events");
const emitter = makeEmitter();
function makeFakeChild() {
const child = new EventEmitter();
child.stdout = new EventEmitter();
child.stderr = new EventEmitter();
child.stdin = {
destroyed: false,
write(line) {
let msg;
try { msg = JSON.parse(String(line).trim()); } catch { return; }
if (msg?.id == null) return;
queueMicrotask(() => {
if (msg.method === "initialize") {
child.stdout.emit("data", Buffer.from(`${JSON.stringify({
jsonrpc: "2.0",
id: msg.id,
result: { protocolVersion: ACP_PROTOCOL_VERSION, authMethods: [] },
})}\n`));
} else if (msg.method === "session/new") {
child.stdout.emit("data", Buffer.from(`${JSON.stringify({
jsonrpc: "2.0",
id: msg.id,
result: { sessionId: "s-die" },
})}\n`));
} else if (msg.method === "session/prompt") {
// Die before answering prompt — turn never completed.
queueMicrotask(() => child.emit("close", 1));
} else {
child.stdout.emit("data", Buffer.from(`${JSON.stringify({
jsonrpc: "2.0",
id: msg.id,
result: {},
})}\n`));
}
});
},
end() {},
};
child.pid = 1;
child.killed = false;
child.exitCode = null;
child.kill = () => { child.killed = true; };
return child;
}
await runGrokAcpTurn({
prompt: "die mid turn",
binPath: "C:\\fake\\grok.exe",
permissionMode: "auto",
emitter,
spawnImpl: () => makeFakeChild(),
});
assert.ok(emitter.calls.some((c) => c[0] === "error"));
assert.ok(!emitter.calls.some((c) => c[0] === "done"));
});
test("runGrokAcpTurn errors when process dies after partial text without prompt result", async () => {
// Partial assistant text is not success — must not suppress exit failure.
const { EventEmitter } = require("node:events");
const emitter = makeEmitter();
function makeFakeChild() {
const child = new EventEmitter();
child.stdout = new EventEmitter();
child.stderr = new EventEmitter();
child.stdin = {
destroyed: false,
write(line) {
let msg;
try { msg = JSON.parse(String(line).trim()); } catch { return; }
if (msg?.id == null) return;
queueMicrotask(() => {
if (msg.method === "initialize") {
child.stdout.emit("data", Buffer.from(`${JSON.stringify({
jsonrpc: "2.0",
id: msg.id,
result: { protocolVersion: ACP_PROTOCOL_VERSION, authMethods: [] },
})}\n`));
} else if (msg.method === "session/new") {
child.stdout.emit("data", Buffer.from(`${JSON.stringify({
jsonrpc: "2.0",
id: msg.id,
result: { sessionId: "s-partial" },
})}\n`));
} else if (msg.method === "session/prompt") {
child.stdout.emit("data", Buffer.from(`${JSON.stringify({
jsonrpc: "2.0",
method: "session/update",
params: {
sessionId: "s-partial",
update: {
sessionUpdate: "agent_message_chunk",
content: { text: "half…" },
},
},
})}\n`));
// Crash before session/prompt result (turnCompleted stays false).
queueMicrotask(() => child.emit("close", 1));
} else {
child.stdout.emit("data", Buffer.from(`${JSON.stringify({
jsonrpc: "2.0",
id: msg.id,
result: {},
})}\n`));
}
});
},
end() {},
};
child.pid = 2;
child.killed = false;
child.exitCode = null;
child.kill = () => { child.killed = true; };
return child;
}
await runGrokAcpTurn({
prompt: "long answer",
binPath: "C:\\fake\\grok.exe",
permissionMode: "auto",
emitter,
spawnImpl: () => makeFakeChild(),
});
assert.ok(emitter.calls.some((c) => c[0] === "text" && c[1] === "half…"));
assert.ok(emitter.calls.some((c) => c[0] === "error"), "partial text + crash must error");
assert.ok(!emitter.calls.some((c) => c[0] === "done"));
});
test("registry grok backend defaults to ACP path and keeps streaming-json fallback", async () => {
assert.ok(listBackends().includes("grok"));
const driver = getDriver("grok");
assert.equal(typeof driver.runTurn, "function");
// Fallback runtime uses streaming-json builder path (no real binary).
const emitter = makeEmitter();
const fallback = await driver.runTurn({
prompt: "x",
binPath: "",
grokRuntime: "streaming-json",
emitter,
env: {},
});
assert.equal(fallback.sessionId, null);
assert.match(String(emitter.calls[0]?.[1] || ""), /not found/i);
// Default ACP path also surfaces missing CLI without hanging.
const emitter2 = makeEmitter();
const acp = await driver.runTurn({
prompt: "x",
binPath: "",
emitter: emitter2,
env: {},
});
assert.equal(acp.runtime, "acp");
assert.match(String(emitter2.calls[0]?.[1] || ""), /not found/i);
});
test("buildGrokAcpPermissionResponse selects offered optionId, never invents allow-once", () => {
// Live ACP uses optionId values like "allow-once" / "allow-always" / "reject".
const allowOnce = buildGrokAcpPermissionResponse({
options: [
{ optionId: "allow-once", kind: "allow_once", name: "Allow once" },
{ optionId: "allow-always", kind: "allow_always", name: "Allow always" },
{ optionId: "reject", kind: "reject_once", name: "Reject" },
],
}, true);
assert.deepEqual(allowOnce, {
outcome: { outcome: "selected", optionId: "allow-always" },
});
const allowOnlyOnce = buildGrokAcpPermissionResponse({
options: [
{ optionId: "allow-once", name: "Allow once" },
{ optionId: "reject", name: "Reject" },
],
}, true);
assert.deepEqual(allowOnlyOnce, {
outcome: { outcome: "selected", optionId: "allow-once" },
});
const deny = buildGrokAcpPermissionResponse({
options: [
{ optionId: "allow-once", kind: "allow_once" },
{ optionId: "reject", kind: "reject_once" },
],
}, false);
assert.deepEqual(deny, {
outcome: { outcome: "selected", optionId: "reject" },
});
// No options offered: deny cancels; allow falls back to underscore form only as last resort.
assert.deepEqual(
buildGrokAcpPermissionResponse({}, false),
{ outcome: { outcome: "cancelled" } },
);
assert.deepEqual(
buildGrokAcpPermissionResponse({ options: [] }, true),
{ outcome: { outcome: "selected", optionId: "allow_once" } },
);
});
test("handleGrokAcpMessage permission reply uses real optionId from params.options", () => {
const written = [];
const state = {
autoAllowPermissions: true,
writeResponse: (id, result) => written.push([id, result]),
};
handleGrokAcpMessage({
jsonrpc: "2.0",
id: 42,
method: "session/request_permission",
params: {
options: [
{ optionId: "allow-once", kind: "allow_once" },
{ optionId: "reject", kind: "reject_once" },
],
},
}, { emitter: makeEmitter(), state, pending: new Map() });
assert.equal(written.length, 1);
assert.equal(written[0][0], 42);
// Must echo the offered id (hyphen form), not invent underscore "allow_once".
assert.equal(written[0][1].outcome.optionId, "allow-once");
});
test("shouldReportGrokProcessExitFailure treats any incomplete close as failure", () => {
// Node close(null, "SIGTERM") when killed — must fail unfinished turns.
assert.equal(
shouldReportGrokProcessExitFailure({ turnCompleted: false }, null, null, "SIGTERM"),
true,
);
assert.equal(
shouldReportGrokProcessExitFailure({ turnCompleted: false }, null, 1, null),
true,
);
// Exit 0 without session/prompt success is still a failure.
assert.equal(
shouldReportGrokProcessExitFailure({ turnCompleted: false }, null, 0, null),
true,
);
assert.equal(
shouldReportGrokProcessExitFailure({ turnCompleted: true }, null, 1, "SIGTERM"),
false,
);
assert.equal(
shouldReportGrokProcessExitFailure({ turnCompleted: false }, { aborted: true }, null, "SIGTERM"),
false,
);
assert.equal(
shouldReportGrokProcessExitFailure({ turnCompleted: false, failed: true }, null, 1, null),
false,
);
});
test("runGrokAcpTurn fails on session/prompt JSON-RPC error (no silent done)", async () => {
// Regression: async request wrap + promptDone race used to swallow the reject
// and emitDone when the process later exited 0.
const emitter = makeEmitter();
const { EventEmitter } = require("node:events");
const unhandled = [];
const onUnhandled = (err) => unhandled.push(err);
process.on("unhandledRejection", onUnhandled);
function makeFakeChild() {
const child = new EventEmitter();
child.stdout = new EventEmitter();
child.stderr = new EventEmitter();
const stdin = new EventEmitter();
stdin.write = (chunk, cb) => {
const msg = JSON.parse(String(chunk).trim());
queueMicrotask(() => {
if (msg.method === "initialize") {
child.stdout.emit("data", Buffer.from(`${JSON.stringify({
jsonrpc: "2.0",
id: msg.id,
result: { protocolVersion: ACP_PROTOCOL_VERSION, authMethods: [] },
})}\n`));
} else if (msg.method === "session/new") {
child.stdout.emit("data", Buffer.from(`${JSON.stringify({
jsonrpc: "2.0",
id: msg.id,
result: { sessionId: "s-err" },
})}\n`));
} else if (msg.method === "session/prompt") {
child.stdout.emit("data", Buffer.from(`${JSON.stringify({
jsonrpc: "2.0",
id: msg.id,
error: { code: -32000, message: "model overloaded" },
})}\n`));
queueMicrotask(() => child.emit("close", 0));
} else {
child.stdout.emit("data", Buffer.from(`${JSON.stringify({
jsonrpc: "2.0",
id: msg.id,
result: {},
})}\n`));
}
if (typeof cb === "function") cb();
});
return true;
};
stdin.end = () => {};
child.stdin = stdin;
child.pid = 8;
child.killed = false;
child.exitCode = null;
child.kill = () => { child.killed = true; };
return child;
}
try {
await runGrokAcpTurn({
prompt: "hi",
binPath: "C:\\fake\\grok.exe",
permissionMode: "auto",
emitter,
spawnImpl: () => makeFakeChild(),
});
} finally {
process.off("unhandledRejection", onUnhandled);
}
const err = emitter.calls.find((c) => c[0] === "error");
assert.ok(err, "prompt JSON-RPC error must emitError");
assert.match(String(err[1]), /overloaded|error|fail/i);
assert.ok(!emitter.calls.some((c) => c[0] === "done"), "must not emitDone on prompt error");
assert.equal(unhandled.length, 0, "must not leave unhandledRejection from swallowed race");
});
test("runGrokAcpTurn fails when process exits 0 mid-prompt without result", async () => {
const emitter = makeEmitter();
const { EventEmitter } = require("node:events");
function makeFakeChild() {
const child = new EventEmitter();
child.stdout = new EventEmitter();
child.stderr = new EventEmitter();
const stdin = new EventEmitter();
stdin.write = (chunk, cb) => {
const msg = JSON.parse(String(chunk).trim());
queueMicrotask(() => {
if (msg.method === "initialize") {
child.stdout.emit("data", Buffer.from(`${JSON.stringify({
jsonrpc: "2.0",
id: msg.id,
result: { protocolVersion: ACP_PROTOCOL_VERSION, authMethods: [] },
})}\n`));
} else if (msg.method === "session/new") {
child.stdout.emit("data", Buffer.from(`${JSON.stringify({
jsonrpc: "2.0",
id: msg.id,
result: { sessionId: "s-zero" },
})}\n`));
} else if (msg.method === "session/prompt") {
child.stdout.emit("data", Buffer.from(`${JSON.stringify({
jsonrpc: "2.0",
method: "session/update",
params: {
sessionId: "s-zero",
update: { sessionUpdate: "agent_message_chunk", content: { text: "half…" } },
},
})}\n`));
queueMicrotask(() => child.emit("close", 0));
} else {
child.stdout.emit("data", Buffer.from(`${JSON.stringify({
jsonrpc: "2.0",
id: msg.id,
result: {},
})}\n`));
}
if (typeof cb === "function") cb();
});
return true;
};
stdin.end = () => {};
child.stdin = stdin;
child.pid = 9;
child.killed = false;
child.exitCode = null;
child.kill = () => { child.killed = true; };
return child;
}
await runGrokAcpTurn({
prompt: "long",
binPath: "C:\\fake\\grok.exe",
permissionMode: "auto",
emitter,
spawnImpl: () => makeFakeChild(),
});
assert.ok(emitter.calls.some((c) => c[0] === "text" && c[1] === "half…"));
assert.ok(emitter.calls.some((c) => c[0] === "error"), "exit 0 mid-prompt must emitError");
assert.ok(!emitter.calls.some((c) => c[0] === "done"));
});
test("runGrokAcpTurn fails when process is signal-killed mid-turn (code=null)", async () => {
// Node reports close(null, "SIGTERM") for signal kills — must not silently succeed.
const emitter = makeEmitter();
const { EventEmitter } = require("node:events");
function makeFakeChild() {
const child = new EventEmitter();
child.stdout = new EventEmitter();
child.stderr = new EventEmitter();
const stdin = new EventEmitter();
stdin.write = (chunk, cb) => {
const msg = JSON.parse(String(chunk).trim());
queueMicrotask(() => {
if (msg.method === "initialize") {
child.stdout.emit("data", Buffer.from(`${JSON.stringify({
jsonrpc: "2.0",
id: msg.id,
result: { protocolVersion: ACP_PROTOCOL_VERSION, authMethods: [] },
})}\n`));
} else if (msg.method === "session/new") {
child.stdout.emit("data", Buffer.from(`${JSON.stringify({
jsonrpc: "2.0",
id: msg.id,
result: { sessionId: "s-sig" },
})}\n`));
} else if (msg.method === "session/prompt") {
child.stdout.emit("data", Buffer.from(`${JSON.stringify({
jsonrpc: "2.0",
method: "session/update",
params: {
sessionId: "s-sig",
update: { sessionUpdate: "agent_message_chunk", content: { text: "half…" } },
},
})}\n`));
// Crash mid-prompt: signal kill, code=null (Node convention).
queueMicrotask(() => child.emit("close", null, "SIGTERM"));
} else {
child.stdout.emit("data", Buffer.from(`${JSON.stringify({
jsonrpc: "2.0",
id: msg.id,
result: {},
})}\n`));
}
if (typeof cb === "function") cb();
});
return true;
};
stdin.end = () => {};
child.stdin = stdin;
child.pid = 7;
child.killed = false;
child.exitCode = null;
child.kill = () => { child.killed = true; };
return child;
}
await runGrokAcpTurn({
prompt: "long",
binPath: "C:\\fake\\grok.exe",
permissionMode: "auto",
emitter,
spawnImpl: () => makeFakeChild(),
});
assert.ok(emitter.calls.some((c) => c[0] === "text" && c[1] === "half…"));
const err = emitter.calls.find((c) => c[0] === "error");
assert.ok(err, "signal kill mid-turn must emitError");
assert.match(String(err[1]), /SIGTERM|signal/i);
assert.ok(!emitter.calls.some((c) => c[0] === "done"));
});