247 lines
7.2 KiB
TypeScript
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-]*$/);
|
|
}
|
|
});
|
|
});
|