Skip to main content
Glama

hypertool-mcp

installer.test.tsโ€ข20.8 kB
/** * Tests for Persona Installer * * @fileoverview Comprehensive tests for persona installation functionality */ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; import { promises as fs } from "fs"; import { join } from "path"; import { tmpdir } from "os"; import { installPersona, analyzeSource, checkPersonaExists, getStandardPersonasDir, listInstalledPersonas, uninstallPersona, SourceType, type InstallOptions, } from "./installer.js"; import { packPersona } from "./archive.js"; import { PersonaErrorCode } from "./types.js"; import { isPersonaError } from "./errors.js"; import { clearDiscoveryCache } from "./discovery.js"; // Test utilities let tempDir: string; let testPersonaDir: string; let testArchivePath: string; let mockPersonasDir: string; /** * Create a test persona directory structure */ async function createTestPersona( dir: string, name: string = "test-persona", config: any = { name, description: "A test persona for installer tests", version: "1.0", toolsets: [ { name: "development", toolIds: ["git.status", "npm.run"], }, ], defaultToolset: "development", } ): Promise<void> { await fs.mkdir(dir, { recursive: true }); // Create persona.yaml const configContent = Object.entries(config) .map(([key, value]) => { if (typeof value === "string") { return `${key}: "${value}"`; } else if (key === "toolsets") { return `toolsets:\n${(value as any[]) .map( (toolset) => ` - name: ${toolset.name}\n toolIds:\n${toolset.toolIds .map((id: string) => ` - ${id}`) .join("\n")}` ) .join("\n")}`; } else { return `${key}: ${value}`; } }) .join("\n"); await fs.writeFile(join(dir, "persona.yaml"), configContent, "utf8"); // Create assets directory const assetsDir = join(dir, "assets"); await fs.mkdir(assetsDir, { recursive: true }); await fs.writeFile( join(assetsDir, "README.md"), `# ${name}\n\nThis is a test persona.`, "utf8" ); // Create optional mcp.json const mcpConfig = { mcpServers: { "test-server": { command: "node", args: ["./test-server.js"], }, }, }; await fs.writeFile( join(dir, "mcp.json"), JSON.stringify(mcpConfig, null, 2), "utf8" ); } /** * Create invalid persona directory */ async function createInvalidPersona(dir: string): Promise<void> { await fs.mkdir(dir, { recursive: true }); await fs.writeFile( join(dir, "invalid.txt"), "This is not a valid persona", "utf8" ); } beforeEach(async () => { // Create temporary directory for tests tempDir = join(tmpdir(), `persona-installer-test-${Date.now()}`); await fs.mkdir(tempDir, { recursive: true }); testPersonaDir = join(tempDir, "test-persona"); testArchivePath = join(tempDir, "test-persona.htp"); mockPersonasDir = join(tempDir, "personas"); // Override persona directory to use temp directory for tests process.env.HYPERTOOL_PERSONA_DIR = mockPersonasDir; await createTestPersona(testPersonaDir); await fs.mkdir(mockPersonasDir, { recursive: true }); }); afterEach(async () => { // Clear discovery cache to prevent test pollution clearDiscoveryCache(); // Clean up environment variable delete process.env.HYPERTOOL_PERSONA_DIR; // Clean up temporary directory try { await fs.rm(tempDir, { recursive: true, force: true }); } catch { // Ignore cleanup errors } }); describe("analyzeSource", () => { it("should analyze folder source correctly", async () => { const sourceInfo = await analyzeSource(testPersonaDir); expect(sourceInfo.accessible).toBe(true); expect(sourceInfo.type).toBe(SourceType.FOLDER); expect(sourceInfo.personaName).toBe("test-persona"); expect(sourceInfo.path).toBe(testPersonaDir); }); it("should analyze archive source correctly", async () => { // Create test archive first await packPersona(testPersonaDir, testArchivePath); const sourceInfo = await analyzeSource(testArchivePath); expect(sourceInfo.accessible).toBe(true); expect(sourceInfo.type).toBe(SourceType.ARCHIVE); expect(sourceInfo.personaName).toBe("test-persona"); expect(sourceInfo.path).toBe(testArchivePath); }); it("should handle non-existent source", async () => { const nonExistentPath = join(tempDir, "does-not-exist"); const sourceInfo = await analyzeSource(nonExistentPath); expect(sourceInfo.accessible).toBe(false); expect(sourceInfo.type).toBe(SourceType.FOLDER); // Default expect(sourceInfo.personaName).toBeUndefined(); }); it("should reject non-.htp file", async () => { const textFile = join(tempDir, "test.txt"); await fs.writeFile(textFile, "test content", "utf8"); await expect(analyzeSource(textFile)).rejects.toThrow(); try { await analyzeSource(textFile); } catch (error) { expect(isPersonaError(error)).toBe(true); if (isPersonaError(error)) { expect(error.code).toBe(PersonaErrorCode.FILE_SYSTEM_ERROR); } } }); it("should extract persona name from folder name if config unavailable", async () => { const customDir = join(tempDir, "custom-persona-name"); await fs.mkdir(customDir, { recursive: true }); // Don't create persona.yaml, should fallback to folder name const sourceInfo = await analyzeSource(customDir); expect(sourceInfo.accessible).toBe(true); expect(sourceInfo.type).toBe(SourceType.FOLDER); expect(sourceInfo.personaName).toBe("custom-persona-name"); }); }); describe("checkPersonaExists", () => { it("should return false for non-existent persona", async () => { const exists = await checkPersonaExists("non-existent", mockPersonasDir); expect(exists).toBe(false); }); it("should return true for existing persona directory", async () => { // Install a persona first const personaPath = join(mockPersonasDir, "existing-persona"); await createTestPersona(personaPath, "existing-persona"); const exists = await checkPersonaExists( "existing-persona", mockPersonasDir ); expect(exists).toBe(true); }); it("should return false for directory that exists but is not a valid persona", async () => { // Create directory without valid persona structure const invalidPath = join(mockPersonasDir, "invalid-persona"); await fs.mkdir(invalidPath, { recursive: true }); await fs.writeFile(join(invalidPath, "random.txt"), "content", "utf8"); const exists = await checkPersonaExists("invalid-persona", mockPersonasDir); expect(exists).toBe(false); }); }); describe("installPersona - from folder", () => { it("should install persona from folder successfully", async () => { const result = await installPersona(testPersonaDir, { installDir: mockPersonasDir, }); expect(result.success).toBe(true); expect(result.personaName).toBe("test-persona"); expect(result.installPath).toBe(join(mockPersonasDir, "test-persona")); expect(result.wasOverwrite).toBe(false); expect(result.errors).toEqual([]); // Verify files were copied const installedPath = join(mockPersonasDir, "test-persona"); await expect( fs.access(join(installedPath, "persona.yaml")) ).resolves.toBeUndefined(); await expect( fs.access(join(installedPath, "mcp.json")) ).resolves.toBeUndefined(); await expect( fs.access(join(installedPath, "assets", "README.md")) ).resolves.toBeUndefined(); }); it("should reject invalid persona source", async () => { const invalidDir = join(tempDir, "invalid-persona"); await createInvalidPersona(invalidDir); const result = await installPersona(invalidDir, { installDir: mockPersonasDir, }); expect(result.success).toBe(false); expect(result.errors).toHaveLength(1); expect(result.errors[0]).toContain("invalid"); }); it("should reject existing persona without force", async () => { // Install once await installPersona(testPersonaDir, { installDir: mockPersonasDir }); // Try to install again const result = await installPersona(testPersonaDir, { installDir: mockPersonasDir, }); expect(result.success).toBe(false); expect(result.errors).toHaveLength(1); expect(result.errors[0]).toContain("already exists"); }); it("should overwrite existing persona with force", async () => { // Install once const firstResult = await installPersona(testPersonaDir, { installDir: mockPersonasDir, }); expect(firstResult.success).toBe(true); // Modify the original await fs.writeFile( join(testPersonaDir, "assets", "new-file.txt"), "new content", "utf8" ); // Install again with force const result = await installPersona(testPersonaDir, { installDir: mockPersonasDir, force: true, }); expect(result.success).toBe(true); expect(result.wasOverwrite).toBe(true); // Verify new file exists const newFile = join( mockPersonasDir, "test-persona", "assets", "new-file.txt" ); const content = await fs.readFile(newFile, "utf8"); expect(content).toBe("new content"); }); it("should create backup when requested", async () => { // Install once await installPersona(testPersonaDir, { installDir: mockPersonasDir }); // Install again with backup const result = await installPersona(testPersonaDir, { installDir: mockPersonasDir, force: true, backup: true, }); expect(result.success).toBe(true); expect(result.backupPath).toBeDefined(); expect(result.backupPath).toContain("test-persona.backup."); // Verify backup exists if (result.backupPath) { await expect(fs.access(result.backupPath)).resolves.toBeUndefined(); await expect( fs.access(join(result.backupPath, "persona.yaml")) ).resolves.toBeUndefined(); } }); it("should skip validation when requested", async () => { const invalidDir = join(tempDir, "skip-validation"); await createInvalidPersona(invalidDir); // Use folder name as persona name (no persona.yaml needed for skip validation test) const result = await installPersona(invalidDir, { installDir: mockPersonasDir, skipValidation: true, }); // Should succeed despite being invalid because validation is skipped expect(result.success).toBe(true); // No warnings should be present since validation was skipped entirely expect(result.warnings.length).toBe(0); }); it("should handle non-existent source", async () => { const nonExistentPath = join(tempDir, "does-not-exist"); const result = await installPersona(nonExistentPath, { installDir: mockPersonasDir, }); expect(result.success).toBe(false); expect(result.errors).toHaveLength(1); expect(result.errors[0]).toContain("File system error"); }); }); describe("installPersona - from archive", () => { beforeEach(async () => { // Create test archive await packPersona(testPersonaDir, testArchivePath); }); it("should install persona from archive successfully", async () => { const result = await installPersona(testArchivePath, { installDir: mockPersonasDir, }); expect(result.success).toBe(true); expect(result.personaName).toBe("test-persona"); expect(result.installPath).toBe(join(mockPersonasDir, "test-persona")); expect(result.wasOverwrite).toBe(false); expect(result.errors).toEqual([]); // Verify files were extracted const installedPath = join(mockPersonasDir, "test-persona"); await expect( fs.access(join(installedPath, "persona.yaml")) ).resolves.toBeUndefined(); await expect( fs.access(join(installedPath, "mcp.json")) ).resolves.toBeUndefined(); await expect( fs.access(join(installedPath, "assets", "README.md")) ).resolves.toBeUndefined(); }); it("should handle corrupted archive", async () => { // Create corrupted archive const corruptedArchive = join(tempDir, "corrupted.htp"); await fs.writeFile(corruptedArchive, "not a valid archive", "utf8"); const result = await installPersona(corruptedArchive, { installDir: mockPersonasDir, }); expect(result.success).toBe(false); expect(result.errors).toHaveLength(1); expect(result.errors[0]).toContain("extract"); // Verify no partial installation const wouldBeInstallPath = join(mockPersonasDir, "test-persona"); await expect(fs.access(wouldBeInstallPath)).rejects.toThrow(); }); it("should clean up on extraction failure", async () => { // Create archive with invalid content that will fail validation after extraction const invalidPersonaDir = join(tempDir, "invalid-for-archive"); await createInvalidPersona(invalidPersonaDir); // Force create a persona.yaml with proper name but invalid structure await fs.writeFile( join(invalidPersonaDir, "persona.yaml"), "name: invalid-for-archive\ninvalid-yaml: [unclosed", "utf8" ); const invalidArchive = join(tempDir, "invalid.htp"); // Force pack even invalid content await packPersona(invalidPersonaDir, invalidArchive, { force: true }); const result = await installPersona(invalidArchive, { installDir: mockPersonasDir, }); expect(result.success).toBe(false); // Verify cleanup - install directory should not exist const wouldBeInstallPath = join(mockPersonasDir, "invalid-for-archive"); await expect(fs.access(wouldBeInstallPath)).rejects.toThrow(); }); }); describe("listInstalledPersonas", () => { it("should return empty list when no personas installed", async () => { const personas = await listInstalledPersonas(mockPersonasDir); expect(personas).toEqual([]); }); it("should list installed personas", async () => { // Install a few personas await installPersona(testPersonaDir, { installDir: mockPersonasDir }); const secondPersonaDir = join(tempDir, "second-persona"); await createTestPersona(secondPersonaDir, "second-persona"); await installPersona(secondPersonaDir, { installDir: mockPersonasDir }); const personas = await listInstalledPersonas(mockPersonasDir); expect(personas).toHaveLength(2); const names = personas.map((p) => p.name); expect(names).toContain("test-persona"); expect(names).toContain("second-persona"); }); it("should handle non-existent install directory", async () => { const nonExistentDir = join(tempDir, "does-not-exist"); const personas = await listInstalledPersonas(nonExistentDir); expect(personas).toEqual([]); }); }); describe("uninstallPersona", () => { it("should uninstall existing persona", async () => { // Install first await installPersona(testPersonaDir, { installDir: mockPersonasDir }); // Verify it exists const existsBefore = await checkPersonaExists( "test-persona", mockPersonasDir ); expect(existsBefore).toBe(true); // Uninstall const result = await uninstallPersona("test-persona", mockPersonasDir); expect(result.success).toBe(true); expect(result.personaName).toBe("test-persona"); expect(result.installPath).toBe(join(mockPersonasDir, "test-persona")); // Verify it's gone const existsAfter = await checkPersonaExists( "test-persona", mockPersonasDir ); expect(existsAfter).toBe(false); }); it("should create backup during uninstall", async () => { // Install first await installPersona(testPersonaDir, { installDir: mockPersonasDir }); // Uninstall with backup const result = await uninstallPersona( "test-persona", mockPersonasDir, true ); expect(result.success).toBe(true); expect(result.backupPath).toBeDefined(); expect(result.backupPath).toContain("test-persona.backup."); // Verify backup exists but original is gone if (result.backupPath) { await expect(fs.access(result.backupPath)).resolves.toBeUndefined(); } const existsAfter = await checkPersonaExists( "test-persona", mockPersonasDir ); expect(existsAfter).toBe(false); }); it("should handle non-existent persona", async () => { const result = await uninstallPersona("does-not-exist", mockPersonasDir); expect(result.success).toBe(false); expect(result.errors).toHaveLength(1); expect(result.errors[0]).toContain("not installed"); }); }); describe("getStandardPersonasDir", () => { it("should return expected standard path", () => { // Ensure environment variable is not set for this test const originalEnv = process.env.HYPERTOOL_PERSONA_DIR; delete process.env.HYPERTOOL_PERSONA_DIR; try { // This test runs without HYPERTOOL_PERSONA_DIR set, so should return default path const standardDir = getStandardPersonasDir(); expect(standardDir).toContain(".toolprint"); expect(standardDir).toContain("hypertool-mcp"); expect(standardDir).toContain("personas"); expect(standardDir).toContain( process.env.HOME || process.env.USERPROFILE || "" ); } finally { // Restore original environment if it was set if (originalEnv) { process.env.HYPERTOOL_PERSONA_DIR = originalEnv; } } }); it("should respect environment variable when set", () => { const customDir = "/tmp/custom-personas"; process.env.HYPERTOOL_PERSONA_DIR = customDir; try { const standardDir = getStandardPersonasDir(); expect(standardDir).toBe(customDir); } finally { delete process.env.HYPERTOOL_PERSONA_DIR; } }); }); describe("Error conditions and edge cases", () => { it("should handle permission errors gracefully", async () => { // This is platform-specific and might not work on all systems const restrictedDir = join(tempDir, "restricted"); await fs.mkdir(restrictedDir, { recursive: true }); try { // Try to make directory read-only await fs.chmod(restrictedDir, 0o444); const result = await installPersona(testPersonaDir, { installDir: restrictedDir, }); // Should fail due to permissions expect(result.success).toBe(false); } finally { // Restore permissions for cleanup try { await fs.chmod(restrictedDir, 0o755); } catch { // Ignore } } }); it("should handle disk space issues during installation", async () => { // This is hard to simulate reliably, so we'll skip it // In a real scenario, you might mock fs operations to simulate ENOSPC }); it("should handle concurrent installation attempts", async () => { // Install the persona first await installPersona(testPersonaDir, { installDir: mockPersonasDir }); // Now try to install it again concurrently (without force) - all should fail const promises = [ installPersona(testPersonaDir, { installDir: mockPersonasDir }), installPersona(testPersonaDir, { installDir: mockPersonasDir }), installPersona(testPersonaDir, { installDir: mockPersonasDir }), ]; const results = await Promise.all(promises); // All should fail since persona already exists const successCount = results.filter((r) => r.success).length; expect(successCount).toBe(0); // All should fail with "already exists" errors const failureCount = results.filter((r) => !r.success).length; expect(failureCount).toBe(3); }); it("should handle very long persona names", async () => { const longName = "a".repeat(200); // Very long name const longPersonaDir = join(tempDir, longName); await createTestPersona(longPersonaDir, longName); const result = await installPersona(longPersonaDir, { installDir: mockPersonasDir, }); // Should either succeed or fail gracefully (depending on filesystem limits) if (!result.success) { expect(result.errors.length).toBeGreaterThan(0); } }); it("should handle special characters in persona names", async () => { // Note: persona names should be validated to be hyphen-delimited lowercase // but the installer should handle edge cases gracefully const specialName = "test-persona-with-special-chars-123"; const specialPersonaDir = join(tempDir, specialName); await createTestPersona(specialPersonaDir, specialName); const result = await installPersona(specialPersonaDir, { installDir: mockPersonasDir, }); expect(result.success).toBe(true); expect(result.personaName).toBe(specialName); }); });

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/toolprint/hypertool-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server