Files
fathah--hermes-desktop/tests/preload-api-surface.test.ts
2026-07-13 13:36:53 +08:00

247 lines
7.2 KiB
TypeScript

import { describe, it, expect } from "vitest";
import { readFileSync } from "fs";
import { join } from "path";
const ROOT = join(__dirname, "..");
const preloadSrc = readFileSync(join(ROOT, "src/preload/index.ts"), "utf-8");
const preloadTypes = readFileSync(
join(ROOT, "src/preload/index.d.ts"),
"utf-8",
);
/**
* Extract method names from the hermesAPI object in preload/index.ts.
* Matches lines like ` methodName: (...` or ` methodName: ()`.
*/
function extractPreloadMethods(src: string): string[] {
const methods: string[] = [];
const re = /^\s{2}(\w+)\s*:\s*\(/gm;
let m: RegExpExecArray | null;
while ((m = re.exec(src)) !== null) {
methods.push(m[1]);
}
return [...new Set(methods)];
}
/**
* Extract method names from the HermesAPI interface in index.d.ts.
*/
function extractTypeMethods(src: string): string[] {
const methods: string[] = [];
// Match lines inside `interface HermesAPI { ... }`
const interfaceMatch = src.match(/interface\s+HermesAPI\s*\{([\s\S]*?)^\}/m);
if (!interfaceMatch) return [];
const body = interfaceMatch[1];
const re = /^\s{2}(\w+)\s*[:(]/gm;
let m: RegExpExecArray | null;
while ((m = re.exec(body)) !== null) {
methods.push(m[1]);
}
return [...new Set(methods)];
}
const preloadMethods = extractPreloadMethods(preloadSrc);
const typeMethods = extractTypeMethods(preloadTypes);
describe("Preload API Surface", () => {
it("preload exposes methods", () => {
expect(preloadMethods.length).toBeGreaterThan(30);
});
it("type declarations define methods", () => {
expect(typeMethods.length).toBeGreaterThan(30);
});
it("every preload method has a type declaration", () => {
const missing = preloadMethods.filter((m) => !typeMethods.includes(m));
expect(missing).toEqual([]);
});
it("every type declaration has a preload implementation", () => {
const missing = typeMethods.filter((m) => !preloadMethods.includes(m));
expect(missing).toEqual([]);
});
});
// ─── New APIs exist ─────────────────────────────────────
describe("New APIs from v0.8/v0.9 features", () => {
it("has backup/import APIs", () => {
expect(preloadMethods).toContain("runHermesBackup");
expect(preloadMethods).toContain("runHermesImport");
expect(typeMethods).toContain("runHermesBackup");
expect(typeMethods).toContain("runHermesImport");
});
it("has log viewer API", () => {
expect(preloadMethods).toContain("readLogs");
expect(typeMethods).toContain("readLogs");
});
it("has debug dump API", () => {
expect(preloadMethods).toContain("runHermesDump");
expect(typeMethods).toContain("runHermesDump");
});
it("has MCP server list API", () => {
expect(preloadMethods).toContain("listMcpServers");
expect(typeMethods).toContain("listMcpServers");
expect(preloadMethods).toContain("addMcpServer");
expect(typeMethods).toContain("addMcpServer");
expect(preloadMethods).toContain("removeMcpServer");
expect(typeMethods).toContain("removeMcpServer");
expect(preloadMethods).toContain("setMcpServerEnabled");
expect(typeMethods).toContain("setMcpServerEnabled");
expect(preloadMethods).toContain("testMcpServer");
expect(typeMethods).toContain("testMcpServer");
expect(preloadMethods).toContain("listMcpCatalog");
expect(typeMethods).toContain("listMcpCatalog");
expect(preloadMethods).toContain("installMcpCatalogEntry");
expect(typeMethods).toContain("installMcpCatalogEntry");
});
it("has memory provider discovery API", () => {
expect(preloadMethods).toContain("discoverMemoryProviders");
expect(typeMethods).toContain("discoverMemoryProviders");
});
it("has dashboard transport probe APIs", () => {
expect(preloadMethods).toContain("dashboardStatus");
expect(typeMethods).toContain("dashboardStatus");
expect(preloadMethods).toContain("startDashboard");
expect(typeMethods).toContain("startDashboard");
expect(preloadMethods).toContain("stopDashboard");
expect(typeMethods).toContain("stopDashboard");
expect(preloadMethods).toContain("setConnectionChatTransports");
expect(typeMethods).toContain("setConnectionChatTransports");
});
});
// ─── Legacy APIs still present ──────────────────────────
describe("Legacy APIs preserved (backward compat)", () => {
const requiredMethods = [
// Installation
"checkInstall",
"startInstall",
"onInstallProgress",
// Hermes engine
"getHermesVersion",
"refreshHermesVersion",
"runHermesDoctor",
"runHermesUpdate",
// Config
"getEnv",
"setEnv",
"getConfig",
"setConfig",
"getHermesHome",
"getModelConfig",
"setModelConfig",
// Chat
"sendMessage",
"abortChat",
"onChatChunk",
"onChatReasoningChunk",
"onChatDone",
"onChatSessionStarted",
"onChatToolProgress",
"onChatUsage",
"onChatError",
// Gateway
"startGateway",
"stopGateway",
"restartGateway",
"gatewayStatus",
"getPlatformEnabled",
"setPlatformEnabled",
// Sessions
"listSessions",
"getSessionMessages",
"recordSessionContinuation",
"recordSessionLocalError",
"deleteSessions",
// Profiles
"listProfiles",
"createProfile",
"deleteProfile",
"setActiveProfile",
// Memory
"readMemory",
"addMemoryEntry",
"updateMemoryEntry",
"removeMemoryEntry",
"writeUserProfile",
// Soul
"readSoul",
"writeSoul",
"resetSoul",
// Tools
"getToolsets",
"setToolsetEnabled",
// Skills
"listInstalledSkills",
"listBundledSkills",
"getSkillContent",
"installSkill",
"uninstallSkill",
// Models
"listModels",
"addModel",
"removeModel",
"updateModel",
"onModelLibraryChanged",
// Credential pool
"getCredentialPool",
"setCredentialPool",
// Claw3D
"claw3dStatus",
"claw3dSetup",
// Cron
"listCronJobs",
"createCronJob",
"removeCronJob",
"pauseCronJob",
"resumeCronJob",
"triggerCronJob",
// Shell
"openExternal",
"openTerminal",
];
for (const method of requiredMethods) {
it(`preload has ${method}`, () => {
expect(preloadMethods).toContain(method);
});
it(`type declaration has ${method}`, () => {
expect(typeMethods).toContain(method);
});
}
});
// ─── IPC channel consistency ────────────────────────────
describe("IPC channel consistency", () => {
it("preload invoke calls use quoted string channel names", () => {
const invokeChannels = [
...preloadSrc.matchAll(/ipcRenderer\.invoke\(\s*["']([^"']+)["']/g),
].map((m) => m[1]);
expect(invokeChannels.length).toBeGreaterThan(30);
// Every channel should be kebab-case
for (const ch of invokeChannels) {
expect(ch).toMatch(/^[a-z][a-z0-9-]*$/);
}
});
it("preload on/removeListener calls use quoted string channel names", () => {
const onChannels = [
...preloadSrc.matchAll(/ipcRenderer\.on\(\s*["']([^"']+)["']/g),
].map((m) => m[1]);
expect(onChannels.length).toBeGreaterThan(0);
for (const ch of onChannels) {
expect(ch).toMatch(/^[a-z][a-z0-9-]*$/);
}
});
});