Files
rea/src/application/Setup.planning.test.ts
T
morluto 08561e0eb7 feat(clients): register Grok Build and Grok Bot during setup (#1134)
* feat(clients): register Grok Build and Grok Bot during setup

Detect both clients and install the local REA MCP registration the same
way setup already handles the other agents. Grok Build edits only the
REA tables in its TOML config and sets startup_timeout_sec to 30. Grok
Bot writes the mcpServers.rea entry into mcp.json. That file is the
registration to add to the signed-in account; setup does not call the
account API.

Filed by Grok.

* fix(clients): clear a disabled Grok Build registration during setup

Grok Build keeps a server suppressed when its name stays in
disabled_mcp_servers, and setup was treating that file as already aligned.
Setup now removes only that name, doctor reports the registration stale until
it is gone, and uninstall keeps comments in an inline table whose only server
was REA. Split the lifecycle suite and reflow the get-started page so static
checks pass.

* fix(verify): check packed Hopper search as literal-only

The package verifier still called a regex probe that main deleted. The
native bridge refuses regex and joins string hits with typed records, so
the restored probe stubs that inventory and the check asserts the literal
matches and the regex refusal. The new README translations also name
Grok Build and Grok Bot.

* fix(clients): stop treating a Grok Bot data directory as registered

Grok Bot stores connectors in the signed-in account and runs them on its
hosted computer. Setup no longer writes mcp.json or reports that file as
aligned. Doctor and setup tell the caller to add the server from the
Grok Bot chat.

* fix(clients): drop multiline Grok disable-list entries

Decode disabled_mcp_servers strings with the document parser so a
multiline basic or literal string that loads as rea is removed.

---------

Co-authored-by: Grok <eugene.ray@gmail.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: morluto <morluto@users.noreply.github.com>
2026-10-09 06:08:29 +08:00

290 lines
9.1 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { runSetup } from "./Setup.js";
import type { SetupProgressEvent } from "./SetupTypes.js";
import { FakeSetupHost, options } from "./Setup.fixture.js";
describe("setup workflow", () => {
it("returns a complete plan without mutation", async () => {
const host = new FakeSetupHost();
host.clients = [{ name: "cursor", configPath: "/cursor.json" }];
const result = await runSetup(
{ ...options(false), clientIds: ["cursor"] },
host,
);
expect(result.status).toBe("needs_confirmation");
expect(result.plannedActions.map(({ kind }) => kind)).toEqual([
"configure_client",
"install_skill",
]);
expect(host.hopperInstalls).toBe(0);
expect(host.configurations).toBe(0);
expect(result.remediation).toBe(
"Review the setup plan, then rerun interactively or with --yes.",
);
});
it("reports ready after an accepted Linux plan installs healthy Hopper", async () => {
const host = new FakeSetupHost("linux");
host.distribution = {
id: "ubuntu",
versionId: "24.04",
packageFamily: "deb",
supported: true,
};
const result = await runSetup(
{ ...options(false), structured: false },
host,
() => Promise.resolve(true),
);
expect(result.status).toBe("ready");
expect(result.appliedActions).toEqual([
"installed_hopper",
"installed_skill",
]);
expect(host.hopperInstalls).toBe(1);
expect(result.remediation).toBeUndefined();
});
it("requires first-run activation after installing Hopper on macOS", async () => {
const host = new FakeSetupHost();
const result = await runSetup(
{ ...options(false), structured: false },
host,
() => Promise.resolve(true),
);
expect(result.status).toBe("needs_human");
expect(result.remediation).toContain("choose its demo mode");
});
it("declines an interactive plan without mutation", async () => {
const host = new FakeSetupHost();
const result = await runSetup(
{ ...options(false), structured: false },
host,
() => Promise.resolve(false),
);
expect(result.status).toBe("cancelled");
expect(result.appliedActions).toEqual([]);
expect(host.hopperInstalls).toBe(0);
expect(host.doctorCalls).toBe(1);
});
it("treats an empty interactive selection as cancellation", async () => {
const host = new FakeSetupHost();
host.clients = [{ name: "codex", configPath: "/codex.toml" }];
const result = await runSetup(
{ ...options(false), structured: false },
host,
() => Promise.resolve({ approved: false, selectedActionIds: [] }),
);
expect(result.status).toBe("ready");
expect(result.plannedActions).toEqual([]);
expect(result.appliedActions).toEqual([]);
expect(host.configurations).toBe(0);
expect(host.skillInstalls).toBe(0);
expect(host.hopperInstalls).toBe(0);
expect(host.doctorCalls).toBe(2);
});
it("requires the Hopper flag for unattended setup", async () => {
const host = new FakeSetupHost();
const result = await runSetup(options(true), host);
expect(result.status).toBe("ready");
expect(host.hopperInstalls).toBe(0);
expect(result.appliedActions).toEqual([]);
});
it("returns an approved planning outcome for explicit dry runs", async () => {
const host = new FakeSetupHost();
host.hopper = "/Applications/Hopper";
host.skill = "unchanged";
host.clients = [{ name: "codex", configPath: "/codex.toml" }];
const result = await runSetup(
{ ...options(false), dryRun: true, clientIds: ["codex"] },
host,
);
expect(result.status).toBe("planned");
expect(result.plannedActions.map(({ id }) => id)).toEqual([
"configure_client:codex",
]);
expect(host.hopperInstalls).toBe(0);
expect(host.configurations).toBe(0);
});
it("installs Hopper when unattended authorization is explicit", async () => {
const host = new FakeSetupHost("linux");
host.distribution = {
id: "ubuntu",
versionId: "24.04",
packageFamily: "deb",
supported: true,
};
const result = await runSetup(options(true, true), host);
expect(result.appliedActions).toEqual(["installed_hopper"]);
expect(host.hopperInstalls).toBe(1);
expect(result.status).toBe("ready");
expect(result.remediation).toBeUndefined();
});
it("reuses existing Hopper without selecting every detected client", async () => {
const host = new FakeSetupHost();
host.hopper = "/Applications/Hopper";
host.clients = [
{ name: "claude", configPath: "/claude.json" },
{ name: "cursor", configPath: "/cursor.json" },
];
const result = await runSetup(options(true), host);
expect(result.status).toBe("ready");
expect(result.appliedActions).toEqual([]);
expect(host.hopperInstalls).toBe(0);
});
});
describe("setup workflow lifecycle and provider planning", () => {
it("reports append-only lifecycle progress for each selected action", async () => {
const host = new FakeSetupHost();
host.hopper = "/Applications/Hopper";
host.clients = [
{
name: "codex",
displayName: "Codex",
configPath: "/codex.toml",
},
];
host.productRegistrations = [
{
client: "codex",
config_path: "/codex.toml",
command: ["rea", "mcp"],
state: "aligned",
remediation: null,
},
];
const progress: SetupProgressEvent[] = [];
await runSetup(
{ ...options(true), onProgress: (event) => progress.push(event) },
host,
);
expect(progress).toEqual([
{ actionId: "configure_client:codex", label: "Codex", state: "started" },
{
actionId: "configure_client:codex",
label: "Codex",
state: "completed",
},
{
actionId: "install_skill",
label: "REA reverse-engineering skill",
state: "started",
},
{
actionId: "install_skill",
label: "REA reverse-engineering skill",
state: "completed",
},
]);
});
it("plans and propagates BYO Ghidra without installing software", async () => {
const host = new FakeSetupHost("linux");
host.distribution = {
id: "ubuntu",
versionId: "24.04",
packageFamily: "deb",
supported: true,
};
host.ghidra = "/opt/ghidra_12.1.4_PUBLIC";
host.javaHome = "/usr/lib/jvm/jdk-21";
host.doctorHealthy = true;
host.skill = "unchanged";
host.clients = [{ name: "cursor", configPath: "/cursor.json" }];
const plan = await runSetup(
{ ...options(false), clientIds: ["cursor"] },
host,
);
expect(plan.plannedActions.map(({ kind }) => kind)).toEqual([
"configure_client",
]);
expect(plan.plannedActions[0]?.detail).toContain(
"GHIDRA_INSTALL_DIR=/opt/ghidra_12.1.4_PUBLIC",
);
expect(plan.plannedActions[0]?.detail).toContain(
"JAVA_HOME=/usr/lib/jvm/jdk-21",
);
expect(host.hopperInstalls).toBe(0);
expect(host.configurations).toBe(0);
const applied = await runSetup(
{ ...options(true), clientIds: ["cursor"] },
host,
);
expect(applied.status).toBe("ready");
expect(host.hopperInstalls).toBe(0);
expect(host.configuredProviderEnvironments).toContainEqual({
GHIDRA_INSTALL_DIR: "/opt/ghidra_12.1.4_PUBLIC",
JAVA_HOME: "/usr/lib/jvm/jdk-21",
});
});
it("reinstalls existing Hopper when explicitly requested", async () => {
const host = new FakeSetupHost();
host.hopper = "/Applications/Hopper";
host.skill = "unchanged";
host.clients = [{ name: "codex", configPath: "/codex.toml" }];
host.clientResults.set("codex", { status: "unchanged" });
const result = await runSetup(
{ ...options(true, true), clientIds: ["codex"] },
host,
);
expect(result.plannedActions.map(({ kind }) => kind)).toEqual([
"install_hopper",
"configure_client",
]);
expect(result.appliedActions).toEqual(["installed_hopper"]);
expect(host.hopperInstalls).toBe(1);
expect(host.hopperReplaceRequests).toEqual([true]);
expect(host.configurations).toBe(0);
expect(host.checkedHopperPaths).toEqual(["/manual/Hopper"]);
});
it("does not write a file when Grok Bot is selected", async () => {
const host = new FakeSetupHost();
host.doctorHealthy = true;
host.skill = "unchanged";
host.clients = [
{
name: "grok_bot",
displayName: "Grok Bot",
configPath: "/home/user/.grokbot",
format: "unsupported",
},
];
const planned = await runSetup(
{ ...options(true), clientIds: ["grok_bot"], dryRun: true },
host,
);
expect(planned.status).toBe("planned");
expect(planned.plannedActions).toEqual([]);
expect(planned.remediation).toContain("hosted computer");
expect(host.configurations).toBe(0);
const applied = await runSetup(
{ ...options(true), clientIds: ["grok_bot"] },
host,
);
expect(applied.status).toBe("needs_human");
expect(applied.appliedActions).toEqual([]);
expect(applied.remediation).toContain("hosted computer");
expect(host.configurations).toBe(0);
});
});