Files
rea/tests/boundary/filesystem/traeClientConfiguration.test.ts
Iven Kooandmorluto bfa805decf feat(clients): register Trae during setup (#1573)
* feat(clients): register Trae during setup

- SupportedClients: trae definition (VS Code-style user directory,
  User/mcp.json, format json)
- Trae documents mcpServers as the top-level server table with
  command/args/env and stdio as the default transport; it has no
  personal skills directory, so setup skips the skill copy
- tests: traeClientConfiguration.test.ts (per-platform user directory
  resolution, upsert preserving unrelated settings, fresh/empty
  configs, uninstall)

Verified: tsc --noEmit clean; vitest run
tests/boundary/filesystem/traeClientConfiguration.test.ts 6/6.

* fix(clients): install Trae's personal REA skill

Review follow-up (#1573): the Trae Skills documentation lists global
skills under ~/.trae/skills on macOS/Linux and %userprofile%/.trae/skills
on Windows, so the definition now carries that skillPath and setup
installs the bundled skill copy there. The installation docs no longer
claim Trae has no personal skills directory, and the bundled REA
skill's supported client list includes trae.

Verified: tsc --noEmit clean; vitest run
tests/boundary/filesystem/traeClientConfiguration.test.ts 7/7.

* docs(clients): qualify Trae configuration path evidence

---------

Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
2026-10-12 22:53:30 +08:00

174 lines
6.0 KiB
TypeScript

import { 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 { npxRegistrationCommand } from "../../../src/application/ClientRegistrationIdentity.js";
import { configureClientConfiguration } from "../../../src/application/SetupClientConfiguration.js";
import { skillDestinations } from "../../../src/application/SetupSkill.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";
/**
* The registration command setup writes on the host running the tests. Windows
* wraps it in `cmd.exe /d /c`, so the expected entry follows the host too.
*/
const command = npxRegistrationCommand(process.platform);
beforeEach(clearClientLocationEnvironment);
afterEach(() => vi.unstubAllEnvs());
const traePath = (
home: string,
platform: NodeJS.Platform = "linux",
env: Parameters<typeof supportedClients>[2] = {},
) => supportedClients(home, platform, env).find(({ name }) => name === "trae");
describe("Trae configuration paths", () => {
it("resolves the VS Code-style user directory per platform", () => {
expect(traePath("/home/a", "linux", {})?.configPath).toBe(
join("/home/a", ".config", "Trae", "User", "mcp.json"),
);
expect(
traePath("/home/a", "linux", { XDG_CONFIG_HOME: "/xdg" })?.configPath,
).toBe(join("/xdg", "Trae", "User", "mcp.json"));
expect(traePath("/Users/a", "darwin")?.configPath).toBe(
join(
"/Users/a",
"Library",
"Application Support",
"Trae",
"User",
"mcp.json",
),
);
expect(
traePath("C:\\Users\\a", "win32", {
APPDATA: "C:\\Users\\a\\AppData\\Roaming",
})?.configPath,
).toBe(join("C:\\Users\\a\\AppData\\Roaming", "Trae", "User", "mcp.json"));
expect(
traePath("C:\\Users\\a", "win32", {
APPDATA: "C:\\Users\\a\\AppData\\Roaming",
})?.markerPath,
).toBe(join("C:\\Users\\a\\AppData\\Roaming", "Trae", "User"));
});
it("resolves the registration format as the plain mcpServers JSON dialect", () => {
expect(traePath("/home/a")?.format).toBe("json");
});
it("resolves the personal skills directory under the home root", () => {
expect(
skillDestinations("/home/a", ["trae"]).map(({ path }) => path),
).toEqual([
join("/home/a", ".trae", "skills", "reverse-engineer-anything"),
]);
expect(
skillDestinations("C:\\Users\\a", ["trae"], {}, "win32").map(
({ path }) => path,
),
).toEqual([
join("C:\\Users\\a", ".trae", "skills", "reverse-engineer-anything"),
]);
});
});
describe("Trae JSON registration", () => {
const traeClient = async () => {
const home = await createTestTempDirectory("rea-trae-");
const client = traePath(home, process.platform);
if (client?.markerPath === undefined)
throw new Error("missing Trae client");
await mkdir(client.markerPath, { recursive: true });
return { home, client };
};
it("upserts rea into mcpServers while preserving unrelated settings", async () => {
const { home, client } = await traeClient();
await writeFile(
client.configPath,
`{
"mcpServers": {
"other": { "command": "other-server" }
}
}`,
);
expect(
(await configureClientConfiguration(client, {}, command)).status,
).toBe("configured");
const parsed = JSON.parse(await readFile(client.configPath, "utf8"));
expect(parsed.mcpServers.other).toEqual({ command: "other-server" });
expect(parsed.mcpServers.rea).toEqual({
command: command[0],
args: command.slice(1),
});
expect(await configureClientConfiguration(client, {}, command)).toEqual({
status: "unchanged",
});
expect(
(
await readClientRegistrationStatuses(home, undefined, {
platform: process.platform,
environment: {},
})
).find(({ client }) => client === "trae"),
).toMatchObject({ state: "aligned", command });
});
it("creates the mcpServers table in an existing mcp.json without servers", async () => {
const { client } = await traeClient();
await writeFile(
client.configPath,
JSON.stringify({ unrelated: { setting: true } }, null, 2),
);
expect(
(await configureClientConfiguration(client, {}, command)).status,
).toBe("configured");
const parsed = JSON.parse(await readFile(client.configPath, "utf8"));
expect(parsed.unrelated).toEqual({ setting: true });
expect(parsed.mcpServers.rea).toEqual({
command: command[0],
args: command.slice(1),
});
});
it("registers into an empty mcp.json", async () => {
const { client } = await traeClient();
await writeFile(client.configPath, "");
expect(
(await configureClientConfiguration(client, {}, command)).status,
).toBe("configured");
const parsed = JSON.parse(await readFile(client.configPath, "utf8"));
expect(parsed.mcpServers.rea).toEqual({
command: command[0],
args: command.slice(1),
});
});
it("removes only the rea entry on uninstall", async () => {
const { home, client } = await traeClient();
await writeFile(
client.configPath,
JSON.stringify(
{ mcpServers: { other: { command: "other-server" } } },
null,
2,
),
);
await configureClientConfiguration(client, {}, command);
expect((await systemUninstallHost(home).removeClient(client)).status).toBe(
"removed",
);
const parsed = JSON.parse(await readFile(client.configPath, "utf8"));
expect(parsed.mcpServers.other).toEqual({ command: "other-server" });
expect(parsed.mcpServers.rea).toBeUndefined();
});
});