Files
reverse-engineering-anything/tests/boundary/filesystem/lmStudioClientConfiguration.test.ts
T
Christopher Pruijsen 398eb0d1f1 feat(setup): register LM Studio during setup (#8611)
* feat(setup): register LM Studio during setup

Register rea in mcp.json in the LM Studio home through a new lm_studio
client. The home follows LM Studio's own lookup, read-only: the path in
~/.lmstudio-home-pointer, else an existing ~/.cache/lm-studio, else
~/.lmstudio. An unreadable or relative pointer reports a path error
instead of a guessed home.

LM Studio parses mcp.json with strict JSON.parse, so comments, trailing
commas and a BOM are rejected before any backup or write. The entry
carries command, args and env with no type or timeout. Local models have
small context windows, so the registration selects
REA_MCP_INPUT_SCHEMA_PROFILE=compact unless a profile is set explicitly,
and doctor reports an entry without a profile as stale. One predicate now
owns that default for Kimi Code and LM Studio.

* fix(doctor): resolve provider budgets for LM Studio as setup does
2026-11-04 19:52:16 +08:00

202 lines
7.2 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { access, mkdir, readFile, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { readClientRegistrationStatuses } from "../../../src/application/ClientRegistrationStatus.js";
import { currentClientRegistrationCommand } from "../../../src/application/ClientRegistrationIdentity.js";
import { configureClientConfiguration } from "../../../src/application/SetupClientConfiguration.js";
import { supportedClients } from "../../../src/application/SupportedClients.js";
import { systemUninstallHost } from "../../../src/application/Uninstall.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { clearClientLocationEnvironment } from "../../fixtures/clientEnvironment.js";
const command = currentClientRegistrationCommand();
beforeEach(clearClientLocationEnvironment);
afterEach(() => vi.unstubAllEnvs());
const lmStudioClient = (home: string) =>
supportedClients(home, process.platform, {}).find(
({ name }) => name === "lm_studio",
);
const exists = async (path: string): Promise<boolean> => {
try {
await access(path);
return true;
} catch {
return false;
}
};
const expectedRegistration = (profile = "compact") => ({
command: command[0],
args: command.slice(1),
env: { REA_MCP_INPUT_SCHEMA_PROFILE: profile },
});
const doctorState = async (home: string) =>
(
await readClientRegistrationStatuses(home, undefined, { environment: {} })
).find(({ client }) => client === "lm_studio");
/** An installed LM Studio home; the pointer and cache layouts are opt-in. */
const installedHome = async (layout: "default" | "pointer" | "cache") => {
const home = await createTestTempDirectory("rea-lm-studio-");
let directory = join(home, ".lmstudio");
if (layout === "pointer") {
directory = await createTestTempDirectory("rea-lm-studio-target-");
await writeFile(join(home, ".lmstudio-home-pointer"), `${directory}\n`);
}
if (layout === "cache") directory = join(home, ".cache", "lm-studio");
await mkdir(directory, { recursive: true });
const client = lmStudioClient(home);
if (client === undefined) throw new Error("missing LM Studio client");
return { home, directory, client };
};
describe("LM Studio home resolution", () => {
it.each(["default", "pointer", "cache"] as const)(
"resolves mcp.json in the %s home",
async (layout) => {
const { directory, client } = await installedHome(layout);
expect(client).toEqual({
name: "lm_studio",
displayName: "LM Studio",
configPath: join(directory, "mcp.json"),
markerPath: directory,
format: "lm_studio",
});
},
);
it("prefers the home pointer over an existing cache directory", async () => {
const { home, directory } = await installedHome("pointer");
await mkdir(join(home, ".cache", "lm-studio"), { recursive: true });
expect(lmStudioClient(home)?.markerPath).toBe(directory);
});
it("reports a relative home pointer instead of guessing a home", async () => {
const home = await createTestTempDirectory("rea-lm-studio-relative-");
await writeFile(join(home, ".lmstudio-home-pointer"), "lmstudio\n");
expect(lmStudioClient(home)).toMatchObject({
configPath: join(home, ".lmstudio-home-pointer"),
configPathError: expect.stringContaining("absolute"),
});
});
});
describe("LM Studio registration lifecycle", () => {
it.each(["default", "pointer", "cache"] as const)(
"registers rea with the compact profile in the %s home",
async (layout) => {
const { home, directory, client } = await installedHome(layout);
const pointer = join(home, ".lmstudio-home-pointer");
const pointerBefore = (await exists(pointer))
? await readFile(pointer, "utf8")
: undefined;
expect(
(await configureClientConfiguration(client, {}, command)).status,
).toBe("configured");
expect(
JSON.parse(await readFile(join(directory, "mcp.json"), "utf8")),
).toEqual({ mcpServers: { rea: expectedRegistration() } });
expect(await doctorState(home)).toMatchObject({
state: "aligned",
command,
});
expect(
(await exists(pointer)) ? await readFile(pointer, "utf8") : undefined,
).toBe(pointerBefore);
},
);
it("preserves sibling servers, backs up, is idempotent, and uninstalls only rea", async () => {
const { home, client } = await installedHome("default");
const original = `{
"mcpServers": {
"other": { "command": "other-server", "args": ["--stdio"], "cwd": "/srv" }
}
}
`;
await writeFile(client.configPath, original);
expect(await configureClientConfiguration(client, {}, command)).toEqual({
status: "configured",
backupPath: `${client.configPath}.rea.backup`,
});
const text = await readFile(client.configPath, "utf8");
expect(JSON.parse(text)).toEqual({
mcpServers: {
other: { command: "other-server", args: ["--stdio"], cwd: "/srv" },
rea: expectedRegistration(),
},
});
expect(text).not.toContain('"type"');
expect(text).not.toContain('"timeout"');
expect(await readFile(`${client.configPath}.rea.backup`, "utf8")).toBe(
original,
);
expect(await configureClientConfiguration(client, {}, command)).toEqual({
status: "unchanged",
});
expect(await readFile(client.configPath, "utf8")).toBe(text);
expect(await doctorState(home)).toMatchObject({ state: "aligned" });
expect((await systemUninstallHost(home).removeClient(client)).status).toBe(
"removed",
);
expect(JSON.parse(await readFile(client.configPath, "utf8"))).toEqual({
mcpServers: {
other: { command: "other-server", args: ["--stdio"], cwd: "/srv" },
},
});
});
it("keeps an explicit full profile and reports an omitted profile stale", async () => {
const { home, client } = await installedHome("default");
await writeFile(
client.configPath,
JSON.stringify({
mcpServers: { rea: { command: command[0], args: command.slice(1) } },
}),
);
expect(await doctorState(home)).toMatchObject({ state: "stale" });
expect(
(
await configureClientConfiguration(
client,
{ REA_MCP_INPUT_SCHEMA_PROFILE: "full" },
command,
)
).status,
).toBe("configured");
expect(
JSON.parse(await readFile(client.configPath, "utf8")).mcpServers.rea,
).toEqual(expectedRegistration("full"));
expect(await configureClientConfiguration(client, {}, command)).toEqual({
status: "unchanged",
});
expect(await doctorState(home)).toMatchObject({ state: "aligned" });
});
it.each([
'{"mcpServers": {',
'{/* comment */ "mcpServers": {}}',
'{"mcpServers": {},}',
'{"mcpServers": {}}',
])("rejects invalid strict JSON %j without writes", async (original) => {
const { client } = await installedHome("default");
await writeFile(client.configPath, original);
expect(await configureClientConfiguration(client, {}, command)).toEqual({
status: "failed",
reason: "readback",
});
expect(await readFile(client.configPath, "utf8")).toBe(original);
expect(await exists(`${client.configPath}.rea.backup`)).toBe(false);
});
});