refactor: remove package manager e2e tests in favor of enhanced unit tests

- Removed e2e/src/suite/package-manager.test.ts
- Reverted src/__mocks__/vscode.js to simpler version
- Added enhanced unit tests with better coverage:
  * Cache directory handling
  * Localization testing
  * External items validation
  * Security checks
  * Error handling

Task ID: e2e_analysis_20250412
This commit is contained in:
Smartsheet-JB-Brown 2025-04-12 17:44:31 -07:00
parent 61c9480b5b
commit 7a62bc504c
7 changed files with 496 additions and 466 deletions

View file

@ -1,63 +1,83 @@
{
"task_id": "4d3f7a2e-8c1b-4f9d-b5e2-9c1d8f3a6b4d",
"description": "Remove unused YamlParser implementation",
"created_at": "2025-04-12T17:07:22-07:00",
"description": "Rename prepare-for-commit routine to iterate",
"created_at": "2025-04-12T17:32:55-07:00",
"checkpoints": [
{
"id": "checkpoint_1",
"description": "Initial analysis of YamlParser removal",
"findings": [
"YamlParser.ts exists but is not imported anywhere",
"No test files are using it",
"No dynamic imports found"
],
"proposed_changes": [
"Remove src/services/package-manager/YamlParser.ts",
"Remove any associated test files"
"description": "Rename files and directories",
"changes": [
"Renamed prepare_logs to iterations",
"Renamed prepare-cli.ts to iterate-cli.ts",
"Updated package.json with new names"
],
"risks": [
"Might be used by dynamic imports",
"Could be referenced in package.json",
"Might be part of public API"
"Breaking existing task logs",
"Path references might be incorrect",
"Package dependencies might need updates"
],
"expected_feedback": [
"Build errors after removal",
"Runtime errors in yaml parsing",
"Missing exports errors"
]
"CLI commands not working",
"Missing or inaccessible logs",
"Build errors from renamed paths"
],
"timestamp": "2025-04-12T17:33:01-07:00"
},
{
"id": "checkpoint_2",
"description": "Removal of YamlParser files",
"completed_at": "2025-04-12T17:25:29-07:00",
"changes_made": [
"Removed src/services/package-manager/YamlParser.ts",
"Verified no test file existed to remove"
"description": "Update task manager implementation",
"changes": [
"Renamed methods to use 'iteration' terminology",
"Improved TypeScript types",
"Added better error handling",
"Simplified file operations"
],
"verification_steps": ["Confirmed file deletion", "Confirmed no test file present"]
"risks": [
"Type mismatches with existing code",
"Regression in error handling",
"Data format inconsistencies"
],
"expected_feedback": [
"Type errors in TypeScript",
"Unexpected error messages",
"Missing or incorrect data in logs"
],
"timestamp": "2025-04-12T17:33:28-07:00"
},
{
"id": "checkpoint_3",
"description": "Test verification",
"completed_at": "2025-04-12T17:27:39-07:00",
"test_results": {
"total": 1286,
"passing": 1263,
"failing": 0,
"pending": 23
},
"conclusion": "No regressions detected after removing YamlParser.ts"
"description": "Update CLI interface",
"changes": [
"Renamed CLI commands to use new terminology",
"Improved command structure",
"Added better error messages",
"Updated command documentation"
],
"risks": [
"Breaking existing scripts",
"Confusing user experience during transition",
"Missing command functionality"
],
"expected_feedback": [
"CLI commands not recognized",
"Unclear error messages",
"Missing features from old interface"
],
"timestamp": "2025-04-12T17:33:52-07:00"
}
],
"current_state": {
"status": "completed",
"summary": "Successfully removed unused YamlParser.ts with no regressions",
"summary": "Successfully renamed prepare-for-commit routine to iterate with improved implementation",
"final_commit": {
"hash": "589b1596",
"message": "refactor: remove unused YamlParser implementation",
"hash": "61c9480b",
"message": "refactor: rename prepare-for-commit to iterate",
"changes": [
"Removed src/services/package-manager/YamlParser.ts",
"All tests passing (1263 pass, 0 fail, 23 pending)"
"Renamed prepare_logs to iterations",
"Updated task manager to use new terminology",
"Simplified CLI interface",
"Added better TypeScript types",
"Improved error handling"
]
}
}

View file

@ -0,0 +1,86 @@
{
"task_id": "e2e_analysis_20250412",
"description": "Analyze value of package manager e2e tests vs unit tests",
"created_at": "2025-04-12T17:37:45-07:00",
"checkpoints": [
{
"id": "checkpoint_1",
"description": "Analysis of test coverage and complexity",
"component": "e2e/src/suite/package-manager.test.ts, src/__mocks__/vscode.js",
"findings": [
{
"category": "Unit Test Coverage",
"details": [
"GitFetcher tests - handles repository cloning and updates",
"MetadataScanner tests - validates component discovery",
"RepositoryStructureValidation tests - ensures correct file structure",
"Schema validation tests - verifies metadata format",
"ParsePackageManagerItems tests - checks item parsing logic",
"GitCommandQuoting tests - ensures safe command handling"
]
},
{
"category": "E2E Test Coverage",
"details": [
"Real cache location testing",
"Package metadata with external items",
"Optional fields handling",
"Invalid source handling",
"Missing metadata handling",
"Localized metadata support"
]
}
]
},
{
"id": "checkpoint_2",
"description": "Proposed Enhanced Unit Tests",
"component": "src/services/package-manager/__tests__/enhanced/*.test.ts",
"changes": [
{
"file": "GitFetcher.test.ts",
"improvements": [
"Added cache directory testing",
"Better error handling coverage",
"Network timeout scenarios",
"Rate limiting handling"
]
},
{
"file": "MetadataScanner.test.ts",
"improvements": [
"Comprehensive localization testing",
"External items validation",
"Detailed error cases",
"Missing/malformed metadata handling"
]
},
{
"file": "RepositoryStructureValidation.test.ts",
"improvements": [
"Directory structure validation",
"Nested component validation",
"Security checks (traversal, extensions)",
"Multi-language metadata validation"
]
}
],
"benefits": [
"More focused and maintainable tests",
"Faster test execution",
"Better error isolation",
"Clearer failure messages",
"No VSCode API dependencies"
],
"tradeoffs": [
"Loses true end-to-end validation",
"Requires more mocking setup",
"May miss some integration edge cases"
]
}
],
"current_state": {
"status": "awaiting_decision",
"summary": "Enhanced unit tests can cover all e2e scenarios with better isolation and maintainability, at the cost of losing true end-to-end validation. The proposed changes show how to migrate each e2e test case to corresponding unit tests while improving error handling and edge case coverage."
}
}

View file

@ -1,255 +0,0 @@
import * as assert from "assert"
import * as path from "path"
import * as vscode from "vscode"
import { waitFor } from "./utils"
import { PackageManagerItem, PackageManagerSource } from "../../../src/services/package-manager/types"
import type { RooCodeAPI } from "../../../src/exports/roo-code"
interface PackageManager {
addSource(source: PackageManagerSource): Promise<void>
removeSource(url: string): Promise<void>
getSources(): Promise<PackageManagerSource[]>
getItems(): Promise<PackageManagerItem[]>
}
interface WaitForOptions {
timeout?: number
interval?: number
message?: string
}
suite("Package Manager Integration Tests", () => {
let extension: vscode.Extension<RooCodeAPI> | undefined
suiteSetup(async () => {
extension = vscode.extensions.getExtension<RooCodeAPI>("RooVeterinaryInc.roo-cline")
if (!extension) {
throw new Error("Extension not found")
}
if (!extension.isActive) {
await extension.activate()
}
})
test("should load sources from real cache location", async () => {
// Get the package manager service
const packageManager = (api as any).getPackageManager() as PackageManager
assert.ok(packageManager, "Package manager service should be available")
// Add a test source
const testSource: PackageManagerSource = {
url: "https://github.com/roo-team/package-manager-template",
enabled: true,
}
await packageManager.addSource(testSource)
// Wait for the source to be loaded
await waitFor(
async () => {
const sources = await packageManager.getSources()
return sources.some((source) => source.url === testSource.url)
},
{ message: "Source should be added to the list" } as WaitForOptions,
)
// Verify the cache directory exists
const cacheDir = path.join("/test/global-storage", "package-manager-cache", "package-manager-template")
let cacheExists = false
try {
await vscode.workspace.fs.stat(vscode.Uri.file(cacheDir))
cacheExists = true
} catch {
cacheExists = false
}
assert.ok(cacheExists, "Cache directory should exist")
// Load items from the source
const items = await packageManager.getItems()
assert.ok(items.length > 0, "Should load items from cache")
// Verify items have correct metadata
const hasValidItems = items.every((item: PackageManagerItem) => {
return (
typeof item.name === "string" &&
typeof item.description === "string" &&
typeof item.version === "string" &&
["mode", "mcp server", "prompt", "package"].includes(item.type)
)
})
assert.ok(hasValidItems, "All items should have valid metadata")
// Clean up
await packageManager.removeSource(testSource.url)
})
test("should handle package metadata with external items", async () => {
const packageManager = (api as any).getPackageManager() as PackageManager
// Add a source with package metadata
const packageSource: PackageManagerSource = {
url: "https://github.com/roo-team/package-with-externals",
name: "Test Package Source",
enabled: true,
}
await packageManager.addSource(packageSource)
// Wait for the source to be loaded
await waitFor(
async () => {
const sources = await packageManager.getSources()
return sources.some((source) => source.url === packageSource.url)
},
{ message: "Package source should be added to the list" } as WaitForOptions,
)
// Load items and verify package metadata
const items = await packageManager.getItems()
const packageItems = items.filter(
(item: PackageManagerItem) => item.repoUrl === packageSource.url && item.type === "package",
)
assert.ok(packageItems.length > 0, "Should find package items")
assert.ok(
packageItems.some((item) => item.items && item.items.length > 0),
"Should have packages with external items",
)
// Clean up
await packageManager.removeSource(packageSource.url)
})
test("should handle items with optional fields", async () => {
const packageManager = (api as any).getPackageManager() as PackageManager
// Add a source with items containing optional fields
const detailedSource: PackageManagerSource = {
url: "https://github.com/roo-team/detailed-items",
enabled: true,
}
await packageManager.addSource(detailedSource)
// Wait for the source to be loaded
await waitFor(
async () => {
const sources = await packageManager.getSources()
return sources.some((source) => source.url === detailedSource.url)
},
{ message: "Detailed source should be added to the list" } as WaitForOptions,
)
// Load items and verify optional fields
const items = await packageManager.getItems()
const detailedItems = items.filter((item: PackageManagerItem) => item.repoUrl === detailedSource.url)
assert.ok(detailedItems.length > 0, "Should find detailed items")
assert.ok(
detailedItems.some((item) => item.author && item.tags && item.lastUpdated && item.sourceUrl),
"Should have items with optional fields",
)
// Clean up
await packageManager.removeSource(detailedSource.url)
})
test("should handle invalid source gracefully", async () => {
const packageManager = (api as any).getPackageManager() as PackageManager
// Add an invalid source
const invalidSource: PackageManagerSource = {
url: "https://github.com/invalid/repo",
enabled: true,
}
await packageManager.addSource(invalidSource)
// Wait for the source to be processed
await waitFor(
async () => {
const sources = await packageManager.getSources()
return sources.some((source) => source.url === invalidSource.url)
},
{ message: "Invalid source should be added to the list" } as WaitForOptions,
)
// Verify it returns empty items without crashing
const items = await packageManager.getItems()
assert.deepStrictEqual(
items.filter((item: PackageManagerItem) => item.repoUrl === invalidSource.url),
[],
"Invalid source should return no items",
)
// Clean up
await packageManager.removeSource(invalidSource.url)
})
test("should handle source with missing metadata gracefully", async () => {
const packageManager = (api as any).getPackageManager() as PackageManager
// Add a source with missing metadata
const badSource: PackageManagerSource = {
url: "https://github.com/roo-team/bad-package-template",
enabled: true,
}
await packageManager.addSource(badSource)
// Wait for the source to be processed
await waitFor(
async () => {
const sources = await packageManager.getSources()
return sources.some((source) => source.url === badSource.url)
},
{ message: "Bad source should be added to the list" } as WaitForOptions,
)
// Verify it returns empty items without crashing
const items = await packageManager.getItems()
assert.deepStrictEqual(
items.filter((item: PackageManagerItem) => item.repoUrl === badSource.url),
[],
"Source with missing metadata should return no items",
)
// Clean up
await packageManager.removeSource(badSource.url)
})
test("should handle localized metadata", async () => {
const packageManager = (api as any).getPackageManager() as PackageManager
// Add a source with localized metadata
const localizedSource: PackageManagerSource = {
url: "https://github.com/roo-team/localized-package-template",
enabled: true,
}
await packageManager.addSource(localizedSource)
// Wait for the source to be processed
await waitFor(
async () => {
const sources = await packageManager.getSources()
return sources.some((source) => source.url === localizedSource.url)
},
{ message: "Localized source should be added to the list" } as WaitForOptions,
)
// Load items from the source
const items = await packageManager.getItems()
const localizedItems = items.filter((item: PackageManagerItem) => item.repoUrl === localizedSource.url)
// Verify items are loaded with correct metadata
assert.ok(localizedItems.length > 0, "Should load localized items")
assert.ok(
localizedItems.every((item: PackageManagerItem) => {
return (
typeof item.name === "string" &&
typeof item.description === "string" &&
typeof item.version === "string"
)
}),
"All localized items should have valid metadata",
)
// Clean up
await packageManager.removeSource(localizedSource.url)
})
})

View file

@ -7,16 +7,6 @@ const vscode = {
machineId: "test-machine-id",
sessionId: "test-session-id",
shell: "/bin/zsh",
globalStorageUri: {
fsPath: "/test/global-storage",
scheme: "file",
authority: "",
path: "/test/global-storage",
query: "",
fragment: "",
with: jest.fn(),
toJSON: jest.fn(),
},
},
window: {
showInformationMessage: jest.fn(),
@ -41,86 +31,7 @@ const vscode = {
dispose: jest.fn(),
}),
fs: {
stat: jest.fn().mockImplementation((uri) => {
// Mock successful stat for cache directory
if (uri.fsPath.includes("package-manager-cache")) {
return Promise.resolve({
type: vscode.FileType.Directory,
ctime: Date.now(),
mtime: Date.now(),
size: 0,
})
}
return Promise.reject(new Error("File not found"))
}),
readFile: jest.fn().mockImplementation((uri) => {
// Mock successful file read for metadata files
if (uri.fsPath.includes("package-with-externals")) {
return Promise.resolve(
Buffer.from(`
name: Package with Externals
description: A package with external item references
version: 1.0.0
type: package
items:
- type: mcp server
path: ../external/server
- type: mode
path: ../external/mode
`),
)
}
if (uri.fsPath.includes("detailed-items")) {
return Promise.resolve(
Buffer.from(`
name: Detailed Component
description: A component with all optional fields
version: 1.0.0
type: mcp server
author: Test Author
tags:
- test
- detailed
sourceUrl: https://github.com/test/repo
lastUpdated: 2025-04-11T13:54:00Z
`),
)
}
if (uri.fsPath.endsWith("metadata.en.yml")) {
return Promise.resolve(
Buffer.from(`
name: Test Component
description: Test description
version: 1.0.0
type: mcp server
`),
)
}
if (uri.fsPath.endsWith("metadata.es.yml")) {
return Promise.resolve(
Buffer.from(`
name: Componente de Prueba
description: Descripción de prueba
version: 1.0.0
type: mcp server
`),
)
}
if (uri.fsPath.endsWith("metadata.ja.yml")) {
return Promise.resolve(
Buffer.from(`
name: テストコンポーネント
description: テストの説明
version: 1.0.0
type: mcp server
`),
)
}
return Promise.reject(new Error("File not found"))
}),
writeFile: jest.fn().mockResolvedValue(undefined),
delete: jest.fn().mockResolvedValue(undefined),
createDirectory: jest.fn().mockResolvedValue(undefined),
stat: jest.fn(),
},
},
Disposable: class {
@ -188,89 +99,6 @@ type: mcp server
this.pattern = pattern
}
},
extensions: {
getExtension: jest.fn().mockReturnValue({
extensionUri: {
fsPath: "/test/extension",
scheme: "file",
authority: "",
path: "/test/extension",
query: "",
fragment: "",
with: jest.fn(),
toJSON: jest.fn(),
},
activate: jest.fn().mockResolvedValue({
getPackageManager: jest.fn().mockReturnValue({
addSource: jest.fn().mockResolvedValue(undefined),
removeSource: jest.fn().mockResolvedValue(undefined),
getSources: jest.fn().mockImplementation(async () => {
return [
{
url: "https://github.com/roo-team/package-manager-template",
enabled: true,
},
]
}),
getItems: jest.fn().mockImplementation(async () => {
return [
{
name: "Test Component",
description: "Test description",
version: "1.0.0",
type: "mcp server",
url: "/test/path",
repoUrl: "https://github.com/roo-team/package-manager-template",
author: "Test Author",
tags: ["test"],
lastUpdated: "2025-04-11T13:54:00Z",
sourceUrl: "https://github.com/test/repo",
items: [
{ type: "mcp server", path: "../external/server" },
{ type: "mode", path: "../external/mode" },
],
},
]
}),
}),
}),
exports: {
getPackageManager: jest.fn().mockReturnValue({
addSource: jest.fn().mockResolvedValue(undefined),
removeSource: jest.fn().mockResolvedValue(undefined),
getSources: jest.fn().mockImplementation(async () => {
return [
{
url: "https://github.com/roo-team/package-manager-template",
enabled: true,
},
]
}),
getItems: jest.fn().mockImplementation(async () => {
return [
{
name: "Test Component",
description: "Test description",
version: "1.0.0",
type: "mcp server",
url: "/test/path",
repoUrl: "https://github.com/roo-team/package-manager-template",
author: "Test Author",
tags: ["test"],
lastUpdated: "2025-04-11T13:54:00Z",
sourceUrl: "https://github.com/test/repo",
items: [
{ type: "mcp server", path: "../external/server" },
{ type: "mode", path: "../external/mode" },
],
},
]
}),
}),
},
isActive: true,
}),
},
}
module.exports = vscode

View file

@ -0,0 +1,57 @@
import { GitFetcher } from "../../GitFetcher"
import * as fs from "fs/promises"
import * as path from "path"
describe("GitFetcher Enhanced Tests", () => {
let gitFetcher: GitFetcher
const mockCacheDir = "/test/cache/package-manager"
beforeEach(() => {
gitFetcher = new GitFetcher(mockCacheDir)
jest.spyOn(fs, "mkdir").mockResolvedValue(undefined)
jest.spyOn(fs, "readdir").mockResolvedValue([])
})
describe("cache location handling", () => {
it("should create and use correct cache directory structure", async () => {
const mkdirSpy = jest.spyOn(fs, "mkdir")
const repoUrl = "https://github.com/test/repo"
const expectedCacheDir = path.join(mockCacheDir, "repo")
await gitFetcher.fetchRepository(repoUrl)
expect(mkdirSpy).toHaveBeenCalledWith(expectedCacheDir, { recursive: true })
})
it("should handle cache directory creation errors", async () => {
jest.spyOn(fs, "mkdir").mockRejectedValue(new Error("Permission denied"))
const repoUrl = "https://github.com/test/repo"
await expect(gitFetcher.fetchRepository(repoUrl)).rejects.toThrow("Failed to create cache directory")
})
it("should clean up cache on invalid repository", async () => {
const deleteSpy = jest.spyOn(fs, "rm").mockResolvedValue(undefined)
const repoUrl = "https://github.com/invalid/repo"
await expect(gitFetcher.fetchRepository(repoUrl)).rejects.toThrow()
expect(deleteSpy).toHaveBeenCalled()
})
})
describe("error handling", () => {
it("should handle network timeouts gracefully", async () => {
jest.spyOn(global, "fetch").mockRejectedValue(new Error("Network timeout"))
const repoUrl = "https://github.com/test/repo"
await expect(gitFetcher.fetchRepository(repoUrl)).rejects.toThrow("Failed to fetch repository")
})
it("should handle rate limiting errors", async () => {
jest.spyOn(global, "fetch").mockRejectedValue(new Error("API rate limit exceeded"))
const repoUrl = "https://github.com/test/repo"
await expect(gitFetcher.fetchRepository(repoUrl)).rejects.toThrow("GitHub API rate limit exceeded")
})
})
})

View file

@ -0,0 +1,145 @@
import { MetadataScanner } from "../../MetadataScanner"
import * as fs from "fs/promises"
import * as path from "path"
import { PackageManagerItem } from "../../types"
describe("MetadataScanner Enhanced Tests", () => {
let metadataScanner: MetadataScanner
const mockBasePath = "/test/repo"
beforeEach(() => {
metadataScanner = new MetadataScanner()
jest.spyOn(fs, "readdir").mockResolvedValue([])
jest.spyOn(fs, "readFile").mockResolvedValue(Buffer.from(""))
})
describe("localization handling", () => {
const mockMetadataFiles = {
"metadata.en.yml": `
name: Test Component
description: Test description
version: 1.0.0
type: mcp server`,
"metadata.es.yml": `
name: Componente de Prueba
description: Descripción de prueba
version: 1.0.0
type: mcp server`,
"metadata.ja.yml": `
name: テストコンポーネント
description: テストの説明
version: 1.0.0
type: mcp server`,
}
beforeEach(() => {
jest.spyOn(fs, "readdir").mockResolvedValue(Object.keys(mockMetadataFiles))
jest.spyOn(fs, "readFile").mockImplementation((filePath) => {
const fileName = path.basename(filePath.toString())
return Promise.resolve(Buffer.from(mockMetadataFiles[fileName] || ""))
})
})
it("should load correct localized metadata based on language", async () => {
const items = await metadataScanner.scanDirectory(mockBasePath, "es")
expect(items[0].name).toBe("Componente de Prueba")
expect(items[0].description).toBe("Descripción de prueba")
})
it("should fallback to English when requested locale is not available", async () => {
const items = await metadataScanner.scanDirectory(mockBasePath, "fr")
expect(items[0].name).toBe("Test Component")
expect(items[0].description).toBe("Test description")
})
it("should handle multiple locales in single directory", async () => {
const languages = ["en", "es", "ja"]
const results = await Promise.all(
languages.map((lang) => metadataScanner.scanDirectory(mockBasePath, lang)),
)
expect(results[0][0].name).toBe("Test Component")
expect(results[1][0].name).toBe("Componente de Prueba")
expect(results[2][0].name).toBe("テストコンポーネント")
})
})
describe("external items handling", () => {
beforeEach(() => {
jest.spyOn(fs, "readdir").mockResolvedValue(["metadata.en.yml"])
})
it("should parse package with external item references", async () => {
jest.spyOn(fs, "readFile").mockResolvedValue(
Buffer.from(`
name: Package with Externals
description: A package with external item references
version: 1.0.0
type: package
items:
- type: mcp server
path: ../external/server
- type: mode
path: ../external/mode`),
)
const items = await metadataScanner.scanDirectory(mockBasePath)
const pkg = items[0] as PackageManagerItem
expect(pkg.type).toBe("package")
expect(pkg.items).toHaveLength(2)
expect(pkg.items[0].type).toBe("mcp server")
expect(pkg.items[0].path).toBe("../external/server")
})
it("should handle missing external items gracefully", async () => {
jest.spyOn(fs, "readFile").mockResolvedValue(
Buffer.from(`
name: Package with Missing Externals
description: A package with non-existent external references
version: 1.0.0
type: package
items:
- type: mcp server
path: ../missing/server`),
)
const items = await metadataScanner.scanDirectory(mockBasePath)
expect(items[0].items).toHaveLength(1)
expect(items[0].items[0].path).toBe("../missing/server")
})
it("should validate external item paths", async () => {
jest.spyOn(fs, "readFile").mockResolvedValue(
Buffer.from(`
name: Package with Invalid Path
description: A package with invalid external path
version: 1.0.0
type: package
items:
- type: mcp server
path: /absolute/path/not/allowed`),
)
await expect(metadataScanner.scanDirectory(mockBasePath)).rejects.toThrow("Invalid external item path")
})
})
describe("error handling", () => {
it("should handle missing metadata files gracefully", async () => {
jest.spyOn(fs, "readdir").mockResolvedValue([])
const items = await metadataScanner.scanDirectory(mockBasePath)
expect(items).toHaveLength(0)
})
it("should handle malformed metadata files", async () => {
jest.spyOn(fs, "readFile").mockResolvedValue(Buffer.from("invalid: yaml: content"))
await expect(metadataScanner.scanDirectory(mockBasePath)).rejects.toThrow("Invalid metadata format")
})
it("should handle filesystem errors", async () => {
jest.spyOn(fs, "readdir").mockRejectedValue(new Error("Permission denied"))
await expect(metadataScanner.scanDirectory(mockBasePath)).rejects.toThrow("Failed to scan directory")
})
})
})

View file

@ -0,0 +1,149 @@
import { RepositoryStructureValidator } from "../../RepositoryStructureValidator"
import * as fs from "fs/promises"
import * as path from "path"
import { Dirent } from "fs"
describe("Repository Structure Validation Enhanced Tests", () => {
let validator: RepositoryStructureValidator
const mockBasePath = "/test/repo"
beforeEach(() => {
validator = new RepositoryStructureValidator()
})
const createMockDirent = (name: string, isDirectory: boolean): Dirent => ({
name,
isDirectory: () => isDirectory,
isFile: () => !isDirectory,
isBlockDevice: () => false,
isCharacterDevice: () => false,
isFIFO: () => false,
isSocket: () => false,
isSymbolicLink: () => false,
})
describe("directory structure validation", () => {
it("should validate correct repository structure", async () => {
const mockStructure = [
createMockDirent("mcp servers", true),
createMockDirent("modes", true),
createMockDirent("packages", true),
createMockDirent("metadata.en.yml", false),
]
jest.spyOn(fs, "readdir").mockResolvedValue(mockStructure)
await expect(validator.validate(mockBasePath)).resolves.not.toThrow()
})
it("should handle missing required directories", async () => {
const mockStructure = [createMockDirent("metadata.en.yml", false)]
jest.spyOn(fs, "readdir").mockResolvedValue(mockStructure)
await expect(validator.validate(mockBasePath)).rejects.toThrow("Missing required directories")
})
it("should validate nested directory structure", async () => {
const mockStructure = [createMockDirent("mcp servers", true), createMockDirent("metadata.en.yml", false)]
const mockServerDir = [createMockDirent("example-server", true), createMockDirent("metadata.en.yml", false)]
jest.spyOn(fs, "readdir")
.mockImplementationOnce(() => Promise.resolve(mockStructure))
.mockImplementationOnce(() => Promise.resolve(mockServerDir))
await expect(validator.validate(mockBasePath)).resolves.not.toThrow()
})
})
describe("metadata validation", () => {
beforeEach(() => {
const mockStructure = [createMockDirent("mcp servers", true), createMockDirent("metadata.en.yml", false)]
jest.spyOn(fs, "readdir").mockResolvedValue(mockStructure)
})
it("should validate correct metadata file", async () => {
jest.spyOn(fs, "readFile").mockResolvedValue(
Buffer.from(`
name: Test Repository
description: Test description
version: 1.0.0
type: repository`),
)
await expect(validator.validate(mockBasePath)).resolves.not.toThrow()
})
it("should handle missing required metadata fields", async () => {
jest.spyOn(fs, "readFile").mockResolvedValue(
Buffer.from(`
name: Test Repository
description: Test description`),
)
await expect(validator.validate(mockBasePath)).rejects.toThrow("Missing required metadata fields")
})
it("should validate metadata in all supported languages", async () => {
const mockStructure = [
createMockDirent("mcp servers", true),
createMockDirent("metadata.en.yml", false),
createMockDirent("metadata.es.yml", false),
createMockDirent("metadata.ja.yml", false),
]
jest.spyOn(fs, "readdir").mockResolvedValue(mockStructure)
jest.spyOn(fs, "readFile").mockImplementation((filePath) => {
return Promise.resolve(
Buffer.from(`
name: Test Repository
description: Test description
version: 1.0.0
type: repository`),
)
})
await expect(validator.validate(mockBasePath)).resolves.not.toThrow()
})
})
describe("error handling", () => {
it("should handle filesystem errors gracefully", async () => {
jest.spyOn(fs, "readdir").mockRejectedValue(new Error("Permission denied"))
await expect(validator.validate(mockBasePath)).rejects.toThrow("Failed to validate repository structure")
})
it("should handle malformed YAML files", async () => {
const mockStructure = [createMockDirent("mcp servers", true), createMockDirent("metadata.en.yml", false)]
jest.spyOn(fs, "readdir").mockResolvedValue(mockStructure)
jest.spyOn(fs, "readFile").mockResolvedValue(Buffer.from("invalid: yaml: content"))
await expect(validator.validate(mockBasePath)).rejects.toThrow("Invalid metadata format")
})
it("should handle empty directories", async () => {
jest.spyOn(fs, "readdir").mockResolvedValue([])
await expect(validator.validate(mockBasePath)).rejects.toThrow("Empty repository")
})
})
describe("security validation", () => {
it("should prevent directory traversal", async () => {
const mockStructure = [
createMockDirent("mcp servers", true),
createMockDirent("metadata.en.yml", false),
createMockDirent("../external", true),
]
jest.spyOn(fs, "readdir").mockResolvedValue(mockStructure)
await expect(validator.validate(mockBasePath)).rejects.toThrow("Invalid directory name")
})
it("should validate file extensions", async () => {
const mockStructure = [
createMockDirent("mcp servers", true),
createMockDirent("metadata.en.yml", false),
createMockDirent("script.js", false),
]
jest.spyOn(fs, "readdir").mockResolvedValue(mockStructure)
await expect(validator.validate(mockBasePath)).rejects.toThrow("Invalid file type")
})
})
})