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

767 lines
24 KiB
JavaScript
Raw Normal View History

const test = require("node:test");
const assert = require("node:assert/strict");
const { EventEmitter } = require("node:events");
const {
GROK_MCP_MODE_DISALLOWED_LOCAL_TOOLS,
buildGrokCliArgs,
buildGrokMcpServerTomlSection,
createLineBuffer,
formatGrokErrorForUser,
listGrokModels,
mergeWorkspaceGrokMcpToml,
parseGrokModelsOutput,
resetGrokMcpMergeRefcountsForTests,
resolveGrokPermissionFlags,
resolveGrokSpawnSpec,
resolveGrokToolIntegrationFlags,
resolveGrokTurnPrompt,
extractGrokAcpPromptUsage,
emitGrokUsage,
normalizeGrokPlanUpdate,
parseGrokModelSelection,
shouldReportGrokProcessExitFailure,
runGrokTurn,
spawnGrokProcess,
stripGrokMcpServerSection,
translateGrokStreamEvent,
} = require("./grokDriver.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("resolveGrokPermissionFlags maps observer to plan and others to always-approve", () => {
assert.deepEqual(resolveGrokPermissionFlags("observer"), ["--permission-mode", "plan"]);
assert.deepEqual(resolveGrokPermissionFlags("confirm"), ["--always-approve"]);
assert.deepEqual(resolveGrokPermissionFlags("auto"), ["--always-approve"]);
});
test("buildGrokCliArgs uses streaming-json and optional model/resume/cwd", () => {
assert.deepEqual(
buildGrokCliArgs({
prompt: "hi",
model: "grok-4.5",
cwd: "/repo",
resumeSessionId: "sess-1",
permissionMode: "observer",
toolIntegrationMode: "skills",
}),
[
"--no-auto-update",
"-p",
"hi",
"--output-format",
"streaming-json",
"-m",
"grok-4.5",
"--cwd",
"/repo",
"-r",
"sess-1",
"--permission-mode",
"plan",
],
);
const autoArgs = buildGrokCliArgs({
prompt: "go",
permissionMode: "auto",
toolIntegrationMode: "skills",
});
assert.ok(autoArgs.includes("--always-approve"));
assert.ok(autoArgs.includes("--no-auto-update"));
assert.ok(!autoArgs.includes("-m"));
});
test("buildGrokCliArgs passes a selected reasoning effort separately from the model", () => {
assert.deepEqual(parseGrokModelSelection("grok-4.6/xhigh"), {
model: "grok-4.6",
effort: "xhigh",
});
assert.deepEqual(parseGrokModelSelection("provider/model"), {
model: "provider/model",
effort: undefined,
});
const args = buildGrokCliArgs({
prompt: "hi",
model: "grok-4.6/xhigh",
permissionMode: "auto",
toolIntegrationMode: "skills",
});
const modelIdx = args.indexOf("-m");
const effortIdx = args.indexOf("--reasoning-effort");
assert.equal(args[modelIdx + 1], "grok-4.6");
assert.equal(args[effortIdx + 1], "xhigh");
});
test("resolveGrokToolIntegrationFlags locks local side-effect tools only in MCP mode", () => {
assert.deepEqual(resolveGrokToolIntegrationFlags("skills"), []);
assert.deepEqual(resolveGrokToolIntegrationFlags("mcp"), [
"--disallowed-tools",
GROK_MCP_MODE_DISALLOWED_LOCAL_TOOLS.join(","),
]);
// Default/unknown → MCP lockdown (align with Claude MCP-mode empty local tools).
assert.deepEqual(resolveGrokToolIntegrationFlags(undefined), [
"--disallowed-tools",
GROK_MCP_MODE_DISALLOWED_LOCAL_TOOLS.join(","),
]);
assert.ok(GROK_MCP_MODE_DISALLOWED_LOCAL_TOOLS.includes("run_terminal_command"));
assert.ok(GROK_MCP_MODE_DISALLOWED_LOCAL_TOOLS.includes("search_replace"));
assert.ok(GROK_MCP_MODE_DISALLOWED_LOCAL_TOOLS.includes("write"));
});
test("buildGrokCliArgs applies MCP-mode local-tool lockdown via real builder", () => {
const mcpArgs = buildGrokCliArgs({
prompt: "list sessions",
permissionMode: "auto",
toolIntegrationMode: "mcp",
});
const denyIdx = mcpArgs.indexOf("--disallowed-tools");
assert.ok(denyIdx >= 0, "MCP mode must pass --disallowed-tools");
const denied = String(mcpArgs[denyIdx + 1] || "");
assert.match(denied, /run_terminal_command/);
assert.match(denied, /search_replace/);
assert.match(denied, /write/);
// MCP meta-tools must not appear in the deny list (Netcatty remote path).
assert.doesNotMatch(denied, /mcp|netcatty/i);
const skillsArgs = buildGrokCliArgs({
prompt: "list sessions",
permissionMode: "auto",
toolIntegrationMode: "skills",
});
assert.ok(!skillsArgs.includes("--disallowed-tools"), "skills mode must not apply MCP lockdown");
});
test("createLineBuffer rejects and releases an unterminated oversized message", () => {
const lines = [];
const lineBuffer = createLineBuffer((line) => lines.push(line), 8);
lineBuffer.push(Buffer.from("12345678"));
assert.throws(
() => lineBuffer.push(Buffer.from("9")),
(error) => error?.code === "GROK_LINE_LIMIT",
);
lineBuffer.flush();
assert.deepEqual(lines, []);
});
test("formatGrokErrorForUser maps auth failures without over-matching bare login strings", () => {
assert.match(
formatGrokErrorForUser("Not authenticated"),
/not logged in/i,
);
assert.equal(
formatGrokErrorForUser("Failed to run login form validation"),
"Failed to run login form validation",
);
});
test("resolveGrokSpawnSpec matches prepareCommandForSpawn for cmd shims and exes", () => {
const { prepareCommandForSpawn } = require("../../ai/shellUtils.cjs");
// On win32, .cmd needs shell (or native-exe rewrite). Elsewhere shell stays false.
const shim = "C:\\Users\\me\\AppData\\Roaming\\npm\\grok.cmd";
const expected = prepareCommandForSpawn(shim, ["agent", "stdio"]);
const actual = resolveGrokSpawnSpec(shim, ["agent", "stdio"]);
assert.deepEqual(actual, expected);
if (process.platform === "win32") {
assert.equal(actual.shell, true);
assert.equal(actual.args.length, 0);
} else {
assert.equal(actual.shell, false);
}
const exePath = process.platform === "win32" ? "C:\\Tools\\grok.exe" : "/usr/bin/grok";
const exe = resolveGrokSpawnSpec(exePath, ["-p", "hi"]);
assert.equal(exe.shell, false);
assert.equal(exe.command, exePath);
assert.deepEqual(exe.args, ["-p", "hi"]);
});
test("spawnGrokProcess forwards shell from prepareCommandForSpawn into spawnImpl", () => {
const calls = [];
const fakeChild = {
stdout: { on() {} },
stderr: { on() {} },
stdin: null,
on() {},
kill() {},
};
const shim = "C:\\Users\\me\\AppData\\Roaming\\npm\\grok.cmd";
const child = spawnGrokProcess(
(command, args, options) => {
calls.push({ command, args, options });
return fakeChild;
},
shim,
["agent", "stdio"],
{ cwd: "D:\\repo", windowsHide: true },
);
assert.equal(child, fakeChild);
assert.equal(calls.length, 1);
assert.equal(calls[0].options.cwd, "D:\\repo");
assert.equal(calls[0].options.windowsHide, true);
assert.equal(calls[0].options.shell, process.platform === "win32");
if (process.platform === "win32") {
assert.match(String(calls[0].command), /grok\.cmd/i);
assert.deepEqual(calls[0].args, []);
} else {
assert.equal(calls[0].command, shim);
assert.deepEqual(calls[0].args, ["agent", "stdio"]);
}
});
test("extractGrokAcpPromptUsage maps live Grok _meta.usage and cachedReadTokens", () => {
const promptResult = {
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,
},
},
};
const extracted = extractGrokAcpPromptUsage(promptResult);
assert.equal(extracted.cachedReadTokens, 2560);
const calls = [];
emitGrokUsage({ usage: (u) => calls.push(u) }, extracted);
assert.deepEqual(calls[0], {
inputTokens: 27144,
cachedInputTokens: 2560,
outputTokens: 29,
reasoningTokens: 24,
totalTokens: 27173,
});
});
test("resolveGrokTurnPrompt seeds history only when resume falls back to session/new", () => {
const seed = "[Conversation context replay]\nUSER: earlier";
const turn = "latest question";
assert.equal(
resolveGrokTurnPrompt({
turnPrompt: turn,
historySeed: seed,
resumeSessionId: "old-sess",
establishMethod: "new",
}),
`${seed}\n\n${turn}`,
);
// Successful resume/load must not inject seed (avoids stacked prior replies).
assert.equal(
resolveGrokTurnPrompt({
turnPrompt: turn,
historySeed: seed,
resumeSessionId: "old-sess",
establishMethod: "resume",
}),
turn,
);
assert.equal(
resolveGrokTurnPrompt({
turnPrompt: turn,
historySeed: seed,
resumeSessionId: "old-sess",
establishMethod: "load",
}),
turn,
);
// No resume attempt → never seed (first-turn replay is handled upstream).
assert.equal(
resolveGrokTurnPrompt({
turnPrompt: turn,
historySeed: seed,
resumeSessionId: undefined,
establishMethod: "new",
}),
turn,
);
assert.equal(
resolveGrokTurnPrompt({
turnPrompt: turn,
historySeed: "",
resumeSessionId: "old-sess",
establishMethod: "new",
}),
turn,
);
});
test("translateGrokStreamEvent maps thought, text, tools, usage, end", () => {
const emitter = makeEmitter();
const state = {};
translateGrokStreamEvent({ type: "thought", data: "plan" }, emitter, state);
translateGrokStreamEvent({ type: "text", data: "Hi" }, emitter, state);
translateGrokStreamEvent({
type: "tool_call",
toolCallId: "c1",
toolName: "read_file",
status: "in_progress",
rawInput: { path: "a.ts" },
}, emitter, state);
translateGrokStreamEvent({
type: "tool_call_update",
toolCallId: "c1",
status: "completed",
rawOutput: { lines: 2 },
}, emitter, state);
translateGrokStreamEvent({
type: "usage",
usage: {
input_tokens: 10,
output_tokens: 3,
cache_read_input_tokens: 1,
reasoning_tokens: 2,
total_tokens: 16,
},
}, emitter, state);
translateGrokStreamEvent({
type: "end",
stopReason: "end_turn",
sessionId: "s1",
usage: { input_tokens: 10, output_tokens: 3, total_tokens: 13 },
}, emitter, state);
assert.deepEqual(emitter.calls, [
["reasoning", "plan"],
["reasoningEnd"],
["text", "Hi"],
["toolCall", "read_file", { path: "a.ts" }, "c1"],
["toolResult", "c1", "{\"lines\":2}", "read_file"],
["usage", {
inputTokens: 10,
cachedInputTokens: 1,
outputTokens: 3,
reasoningTokens: 2,
totalTokens: 16,
}],
["sessionId", "s1"],
["usage", {
inputTokens: 10,
cachedInputTokens: 0,
outputTokens: 3,
reasoningTokens: 0,
totalTokens: 13,
}],
]);
assert.equal(state.sessionId, "s1");
assert.equal(state.streamedAssistantText, true);
});
test("translateGrokStreamEvent maps error events to emitError and stop", () => {
const emitter = makeEmitter();
const state = {};
const stop = translateGrokStreamEvent(
{ type: "error", message: "Couldn't start session" },
emitter,
state,
);
assert.equal(stop, true);
assert.equal(state.failed, true);
assert.deepEqual(emitter.calls, [["error", "Couldn't start session"]]);
});
test("buildGrokMcpServerTomlSection escapes paths and env", () => {
const section = buildGrokMcpServerTomlSection({
name: "netcatty-remote-hosts",
command: "C:\\Program Files\\node.exe",
args: ["mcp.cjs", "--flag"],
env: [{ name: "TOKEN", value: 'a"b' }],
});
assert.match(section, /\[mcp_servers\.netcatty-remote-hosts\]/);
assert.match(section, /command = "C:\\\\Program Files\\\\node\.exe"/);
assert.match(section, /args = \["mcp\.cjs", "--flag"\]/);
assert.match(section, /TOKEN = "a\\"b"/);
assert.match(section, /enabled = true/);
});
test("stripGrokMcpServerSection removes only the named server block", () => {
const input = [
"[ui]",
"compact_mode = true",
"",
"[mcp_servers.other]",
'command = "echo"',
"",
"[mcp_servers.netcatty-remote-hosts]",
'command = "node"',
"enabled = true",
"",
"[mcp_servers.other.nested]",
"x = 1",
].join("\n");
const stripped = stripGrokMcpServerSection(input, "netcatty-remote-hosts");
assert.match(stripped, /\[mcp_servers\.other\]/);
assert.match(stripped, /\[ui\]/);
assert.doesNotMatch(stripped, /netcatty-remote-hosts/);
});
test("mergeWorkspaceGrokMcpToml upserts netcatty without dropping other servers", () => {
resetGrokMcpMergeRefcountsForTests();
const path = require("node:path");
const repo = path.join("repo-fixture");
const grokDir = path.join(repo, ".grok");
const configPath = path.join(grokDir, "config.toml");
const original = [
"[mcp_servers.other]",
'command = "echo"',
"enabled = true",
"",
].join("\n");
const files = new Map();
files.set(configPath, original);
const handle = mergeWorkspaceGrokMcpToml(repo, [{
name: "netcatty-remote-hosts",
command: "node",
args: ["mcp.cjs"],
env: [{ name: "TOKEN", value: "x" }],
}], {
existsSync: (p) => files.has(p) || p === grokDir,
readFileSync: (p) => files.get(p),
writeFileSync: (p, data) => { files.set(p, data); },
mkdirSync: () => {},
unlinkSync: (p) => { files.delete(p); },
});
const written = files.get(configPath);
assert.match(written, /\[mcp_servers\.other\]/);
assert.match(written, /\[mcp_servers\.netcatty-remote-hosts\]/);
assert.match(written, /TOKEN = "x"/);
handle.restore();
assert.equal(files.get(configPath), original);
});
test("parseGrokModelsOutput reads default and bullet list", () => {
const parsed = parseGrokModelsOutput([
"You are logged in with grok.com.",
"",
"Default model: grok-4.5",
"",
"Available models:",
" * grok-4.5 (default)",
" * grok-code-fast",
].join("\n"));
assert.equal(parsed.currentModelId, "grok-4.5");
assert.deepEqual(parsed.models, [
{
id: "grok-4.5",
name: "grok-4.5",
thinkingLevels: ["high", "medium", "low"],
defaultThinkingLevel: "high",
},
{ id: "grok-code-fast", name: "grok-code-fast" },
]);
});
test("runGrokTurn streams fixture lines and emits done", async () => {
const emitter = makeEmitter();
const child = new EventEmitter();
child.stdout = new EventEmitter();
child.stderr = new EventEmitter();
child.pid = 4242;
child.kill = () => {};
const spawnImpl = (bin, args) => {
assert.equal(bin, "/usr/bin/grok");
assert.ok(args.includes("streaming-json"));
assert.ok(args.includes("--always-approve"));
queueMicrotask(() => {
child.stdout.emit("data", Buffer.from(
[
'{"type":"thought","data":"thinking"}',
'{"type":"text","data":"hello"}',
'{"type":"end","sessionId":"sess-xyz","stopReason":"end_turn"}',
"",
].join("\n"),
));
child.emit("close", 0);
});
return child;
};
const result = await runGrokTurn({
prompt: "hi",
binPath: "/usr/bin/grok",
cwd: "/repo",
permissionMode: "auto",
injectedMcpServers: [],
emitter,
spawnImpl,
mergeMcp: () => ({ restore() {} }),
});
assert.equal(result.sessionId, "sess-xyz");
assert.ok(emitter.calls.some((c) => c[0] === "text" && c[1] === "hello"));
assert.ok(emitter.calls.some((c) => c[0] === "done"));
assert.ok(emitter.calls.some((c) => c[0] === "sessionId" && c[1] === "sess-xyz"));
});
test("runGrokTurn reports error when process dies after partial text without end", async () => {
// Mid-response crash: text already streamed, no end → must not emitDone.
const emitter = makeEmitter();
const child = new EventEmitter();
child.stdout = new EventEmitter();
child.stderr = new EventEmitter();
child.pid = 99;
child.kill = () => {};
const spawnImpl = () => {
queueMicrotask(() => {
child.stdout.emit("data", Buffer.from('{"type":"text","data":"partial…"}\n'));
child.emit("close", 1);
});
return child;
};
await runGrokTurn({
prompt: "write a lot",
binPath: "/usr/bin/grok",
permissionMode: "auto",
injectedMcpServers: [],
emitter,
spawnImpl,
mergeMcp: () => ({ restore() {} }),
});
assert.ok(emitter.calls.some((c) => c[0] === "text" && c[1] === "partial…"));
assert.ok(emitter.calls.some((c) => c[0] === "error"), "partial stream + exit 1 must emitError");
assert.ok(!emitter.calls.some((c) => c[0] === "done"), "must not emitDone on mid-turn crash");
});
test("runGrokTurn fails when process is signal-killed mid-turn (code=null)", async () => {
// Node close(null, "SIGTERM") — previously skipped because code was not a nonzero number.
const emitter = makeEmitter();
const child = new EventEmitter();
child.stdout = new EventEmitter();
child.stderr = new EventEmitter();
child.pid = 98;
child.kill = () => {};
const spawnImpl = () => {
queueMicrotask(() => {
child.stdout.emit("data", Buffer.from('{"type":"text","data":"partial…"}\n'));
child.emit("close", null, "SIGTERM");
});
return child;
};
await runGrokTurn({
prompt: "write a lot",
binPath: "/usr/bin/grok",
permissionMode: "auto",
injectedMcpServers: [],
emitter,
spawnImpl,
mergeMcp: () => ({ restore() {} }),
});
assert.ok(emitter.calls.some((c) => c[0] === "text" && c[1] === "partial…"));
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"));
});
test("runGrokTurn fails when process exits 0 after partial text without end", async () => {
// Quiet CLI death must not look like a successful turn.
const emitter = makeEmitter();
const child = new EventEmitter();
child.stdout = new EventEmitter();
child.stderr = new EventEmitter();
child.pid = 97;
child.kill = () => {};
const spawnImpl = () => {
queueMicrotask(() => {
child.stdout.emit("data", Buffer.from('{"type":"text","data":"partial…"}\n'));
child.emit("close", 0);
});
return child;
};
await runGrokTurn({
prompt: "write a lot",
binPath: "/usr/bin/grok",
permissionMode: "auto",
injectedMcpServers: [],
emitter,
spawnImpl,
mergeMcp: () => ({ restore() {} }),
});
assert.ok(emitter.calls.some((c) => c[0] === "text" && c[1] === "partial…"));
assert.ok(emitter.calls.some((c) => c[0] === "error"), "exit 0 without end must emitError");
assert.ok(!emitter.calls.some((c) => c[0] === "done"));
});
test("translateGrokStreamEvent emits toolResult when rawOutput present without status", () => {
const emitter = makeEmitter();
const state = {};
translateGrokStreamEvent({
type: "tool_call_update",
toolCallId: "t1",
toolName: "read",
rawOutput: { content: "file body" },
}, emitter, state);
assert.ok(emitter.calls.some((c) => c[0] === "toolCall" && c[3] === "t1"));
assert.ok(emitter.calls.some((c) => c[0] === "toolResult" && c[1] === "t1"));
});
test("normalizeGrokPlanUpdate maps to shared { text, completed } activity shape", () => {
assert.deepEqual(
normalizeGrokPlanUpdate([
{ content: "Explore", status: "completed" },
{ text: "Edit", status: "pending" },
"Ship it",
]),
{
items: [
{ text: "Explore", completed: true },
{ text: "Edit", completed: false },
{ text: "Ship it", completed: false },
],
status: "running",
},
);
assert.deepEqual(
normalizeGrokPlanUpdate([
{ content: "A", status: "done" },
{ content: "B", completed: true },
]),
{
items: [
{ text: "A", completed: true },
{ text: "B", completed: true },
],
status: "completed",
},
);
assert.equal(normalizeGrokPlanUpdate([]), null);
});
test("translateGrokStreamEvent plan uses text/completed and running|completed status", () => {
const emitter = makeEmitter();
translateGrokStreamEvent({
type: "plan",
entries: [
{ content: "Step one", status: "completed" },
{ content: "Step two", status: "in_progress" },
],
}, emitter, {});
const planCall = emitter.calls.find((c) => c[0] === "planUpdate");
assert.ok(planCall);
assert.equal(planCall[1], "grok-plan");
assert.deepEqual(planCall[2], [
{ text: "Step one", completed: true },
{ text: "Step two", completed: false },
]);
assert.equal(planCall[3], "running");
assert.notEqual(planCall[3], "updated");
});
test("shouldReportGrokProcessExitFailure fails any incomplete close (incl exit 0)", () => {
assert.equal(shouldReportGrokProcessExitFailure({ turnCompleted: false }, null, null, "SIGTERM"), true);
assert.equal(shouldReportGrokProcessExitFailure({ turnCompleted: false }, null, 143, null), true);
// Exit 0 without protocol completion is still a failure (CLI can die quietly).
assert.equal(shouldReportGrokProcessExitFailure({ turnCompleted: false }, null, 0, null), true);
assert.equal(shouldReportGrokProcessExitFailure({ turnCompleted: true }, null, null, "SIGTERM"), false);
assert.equal(shouldReportGrokProcessExitFailure({ turnCompleted: true }, null, 1, null), false);
assert.equal(shouldReportGrokProcessExitFailure({ turnCompleted: false }, { aborted: true }, null, "SIGKILL"), false);
assert.equal(shouldReportGrokProcessExitFailure({ turnCompleted: false, failed: true }, null, 1, null), false);
});
test("runGrokTurn ignores exit code 1 after end event (Windows teardown)", async () => {
const emitter = makeEmitter();
const child = new EventEmitter();
child.stdout = new EventEmitter();
child.stderr = new EventEmitter();
child.pid = 100;
child.kill = () => {};
const spawnImpl = () => {
queueMicrotask(() => {
child.stdout.emit("data", Buffer.from(
[
'{"type":"text","data":"done"}',
'{"type":"end","sessionId":"s-end","stopReason":"end_turn"}',
"",
].join("\n"),
));
child.emit("close", 1);
});
return child;
};
await runGrokTurn({
prompt: "hi",
binPath: "/usr/bin/grok",
permissionMode: "auto",
injectedMcpServers: [],
emitter,
spawnImpl,
mergeMcp: () => ({ restore() {} }),
});
assert.ok(emitter.calls.some((c) => c[0] === "done"));
assert.ok(!emitter.calls.some((c) => c[0] === "error"));
});
test("runGrokTurn reports missing CLI clearly", async () => {
const emitter = makeEmitter();
const result = await runGrokTurn({
prompt: "hi",
binPath: "",
emitter,
});
assert.equal(result.sessionId, null);
assert.match(String(emitter.calls[0]?.[1] || ""), /not found/i);
});
test("listGrokModels parses spawn stdout", async () => {
const child = new EventEmitter();
child.stdout = new EventEmitter();
child.stderr = new EventEmitter();
child.pid = 1;
child.kill = () => {};
const spawnImpl = (_bin, args) => {
assert.deepEqual(args, ["--no-auto-update", "models"]);
queueMicrotask(() => {
child.stdout.emit("data", Buffer.from("Default model: grok-4.5\n* grok-4.5 (default)\n"));
child.emit("close", 0);
});
return child;
};
const result = await listGrokModels({
binPath: "/usr/bin/grok",
spawnImpl,
});
assert.equal(result.currentModelId, "grok-4.5");
assert.equal(result.models[0].id, "grok-4.5");
});