Files
2026-07-13 13:36:53 +08:00

331 lines
9.8 KiB
TypeScript

import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import { join } from "path";
import { mkdirSync, readFileSync, rmSync, writeFileSync, existsSync } from "fs";
/**
* Tests for setToolsetEnabled / getToolsets — the YAML line-by-line
* state machine that reads/writes platform_toolsets.cli in config.yaml.
*
* Regression target: when platform_toolsets: exists but has no cli:
* subsection, and that section is the last top-level block in the file,
* setToolsetEnabled silently returns true without writing the cli:
* block. The stale file means toolsets appear disabled on every read.
*/
const { TEST_HOME } = vi.hoisted(() => {
// eslint-disable-next-line @typescript-eslint/no-require-imports
const path = require("path");
// eslint-disable-next-line @typescript-eslint/no-require-imports
const os = require("os");
return {
TEST_HOME: path.join(os.tmpdir(), `hermes-toolset-test-${Date.now()}`),
};
});
vi.mock("../src/main/installer", () => ({
HERMES_HOME: TEST_HOME,
HERMES_PYTHON: "/usr/bin/python3",
HERMES_REPO: "/dev/null",
hermesCliArgs: (args: string[] = []) => ["/dev/null", ...args],
getEnhancedPath: () => process.env.PATH || "",
}));
vi.mock("../src/main/utils", () => {
const actual =
vi.importActual<typeof import("../src/main/utils")>("../src/main/utils");
return actual;
});
vi.mock("../src/shared/i18n", () => ({
t: (key: string) => key.split(".").pop() || key,
}));
vi.mock("../src/main/locale", () => ({
getAppLocale: () => "en",
}));
import {
getPlatformToolsets,
getToolsets,
setMessagingPlatformToolsetEnabled,
setToolsetEnabled,
} from "../src/main/tools";
const CONFIG_FILE = join(TEST_HOME, "config.yaml");
beforeEach(() => {
mkdirSync(TEST_HOME, { recursive: true });
});
afterEach(() => {
vi.resetModules();
if (existsSync(TEST_HOME)) {
rmSync(TEST_HOME, { recursive: true, force: true });
}
});
function writeConfig(content: string): void {
writeFileSync(CONFIG_FILE, content);
}
function readConfig(): string {
return readFileSync(CONFIG_FILE, "utf-8");
}
describe("setToolsetEnabled — platform_toolsets without cli subsection", () => {
it("writes cli subsection when platform_toolsets block exists but has no cli (last block)", () => {
writeConfig("model:\n default: gpt-4o\nplatform_toolsets:\n");
const result = setToolsetEnabled("web", true);
expect(result).toBe(true);
const after = readConfig();
expect(after).toContain("cli:");
expect(after).toContain(" - web");
});
it("writes cli subsection when platform_toolsets block has sibling keys but no cli (last block)", () => {
writeConfig(
"model:\n default: gpt-4o\nplatform_toolsets:\n other_key: value\n",
);
const result = setToolsetEnabled("web", true);
expect(result).toBe(true);
const after = readConfig();
expect(after).toContain("cli:");
expect(after).toContain(" - web");
});
it("writes cli subsection when platform_toolsets is the ONLY top-level block", () => {
writeConfig("platform_toolsets:\n");
const result = setToolsetEnabled("browser", true);
expect(result).toBe(true);
const after = readConfig();
expect(after).toContain("cli:");
expect(after).toContain(" - browser");
});
it("preserves existing non-cli siblings when inserting cli", () => {
writeConfig(
[
"model:",
" default: gpt-4o",
"platform_toolsets:",
" some_config: true",
"",
].join("\n"),
);
const result = setToolsetEnabled("terminal", true);
expect(result).toBe(true);
const after = readConfig();
expect(after).toContain("cli:");
expect(after).toContain(" - terminal");
expect(after).toContain("some_config: true");
});
it("reads back the enabled toolset after writing (round-trip)", () => {
writeConfig("model:\n default: gpt-4o\nplatform_toolsets:\n");
setToolsetEnabled("web", true);
setToolsetEnabled("file", true);
// Reimport so getToolsets reads the freshly-written file (no cache pollution)
const toolsets = getToolsets();
const webTs = toolsets.find((t) => t.key === "web");
const fileTs = toolsets.find((t) => t.key === "file");
const memTs = toolsets.find((t) => t.key === "memory");
expect(webTs?.enabled).toBe(true);
expect(fileTs?.enabled).toBe(true);
expect(memTs?.enabled).toBe(false);
});
it("round-trips disable after enable when cli was initially missing", () => {
writeConfig("model:\n default: gpt-4o\nplatform_toolsets:\n");
// Enable → should create cli section
setToolsetEnabled("web", true);
let toolsets = getToolsets();
expect(toolsets.find((t) => t.key === "web")?.enabled).toBe(true);
// Disable → should remove web from cli
setToolsetEnabled("web", false);
toolsets = getToolsets();
expect(toolsets.find((t) => t.key === "web")?.enabled).toBe(false);
});
});
describe("setToolsetEnabled — no config file", () => {
it("returns false when config.yaml does not exist", () => {
const result = setToolsetEnabled("web", true);
expect(result).toBe(false);
});
});
describe("setToolsetEnabled — no platform_toolsets section (C1)", () => {
it("appends platform_toolsets.cli when the key is totally absent", () => {
writeConfig("model:\n default: gpt-4o\n");
const result = setToolsetEnabled("vision", true);
expect(result).toBe(true);
const after = readConfig();
expect(after).toContain("platform_toolsets:");
expect(after).toContain(" cli:");
expect(after).toContain(" - vision");
expect(after).toContain("model:");
expect(after).toContain("default: gpt-4o");
});
it("appends only once on repeated calls", () => {
writeConfig("model:\n default: gpt-4o\n");
setToolsetEnabled("web", true);
setToolsetEnabled("vision", true);
const after = readConfig();
const n = after.split("platform_toolsets:").length - 1;
expect(n).toBe(1);
});
it("reads back the appended toolset (round-trip)", () => {
writeConfig("model:\n default: gpt-4o\n");
setToolsetEnabled("memory", true);
const toolsets = getToolsets();
expect(toolsets.find((t) => t.key === "memory")?.enabled).toBe(true);
});
});
describe("setToolsetEnabled — existing cli subsection (C2a)", () => {
it("replaces in-place when enabling a new toolset alongside existing ones", () => {
writeConfig(
[
"model:",
" default: gpt-4o",
"platform_toolsets:",
" cli:",
" - web",
" - terminal",
"",
].join("\n"),
);
setToolsetEnabled("vision", true);
const after = readConfig();
expect(after).toContain(" - web");
expect(after).toContain(" - terminal");
expect(after).toContain(" - vision");
});
it("removes a toolset from the list when disabling an existing one", () => {
writeConfig(
[
"model:",
" default: gpt-4o",
"platform_toolsets:",
" cli:",
" - web",
" - terminal",
" - vision",
"",
].join("\n"),
);
setToolsetEnabled("terminal", false);
const after = readConfig();
expect(after).toContain(" - web");
expect(after).toContain(" - vision");
expect(after).not.toContain(" - terminal");
});
it("round-trips enable after disable on an existing cli list", () => {
writeConfig(["platform_toolsets:", " cli:", " - web", ""].join("\n"));
setToolsetEnabled("web", false);
let toolsets = getToolsets();
expect(toolsets.find((t) => t.key === "web")?.enabled).toBe(false);
setToolsetEnabled("web", true);
toolsets = getToolsets();
expect(toolsets.find((t) => t.key === "web")?.enabled).toBe(true);
});
});
describe("setToolsetEnabled — disable all (C2d)", () => {
it("handles empty cli list gracefully when last toolset is disabled", () => {
writeConfig(["platform_toolsets:", " cli:", " - web", ""].join("\n"));
const result = setToolsetEnabled("web", false);
expect(result).toBe(true);
const after = readConfig();
expect(after).toContain("platform_toolsets:");
expect(after).toContain(" cli:");
});
});
describe("setMessagingPlatformToolsetEnabled", () => {
it("writes a messaging platform subsection without changing cli", () => {
writeConfig(
[
"platform_toolsets:",
" cli:",
" - web",
" telegram:",
" - memory",
"",
].join("\n"),
);
const result = setMessagingPlatformToolsetEnabled(
"telegram",
"browser",
true,
);
expect(result).toBe(true);
const after = readConfig();
expect(after).toContain(" cli:");
expect(after).toContain(" - web");
expect(after).toContain(" telegram:");
expect(after).toContain(" - browser");
expect(after).toContain(" - memory");
});
it("starts from safe messaging defaults when the platform has no explicit list", () => {
writeConfig(["platform_toolsets:", " cli:", " - web", ""].join("\n"));
const result = setMessagingPlatformToolsetEnabled(
"telegram",
"browser",
true,
);
expect(result).toBe(true);
const toolsets = getPlatformToolsets();
expect(toolsets.telegram).toContain("browser");
expect(toolsets.telegram).toContain("web");
expect(toolsets.telegram).toContain("memory");
expect(toolsets.telegram).not.toContain("terminal");
});
it("respects an explicit empty messaging platform toolset list", () => {
writeConfig(["platform_toolsets:", " telegram: []", ""].join("\n"));
const toolsets = getPlatformToolsets();
expect(toolsets.telegram).toEqual([]);
setMessagingPlatformToolsetEnabled("telegram", "browser", true);
expect(getPlatformToolsets().telegram).toEqual(["browser"]);
});
});