mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-07 08:26:51 +00:00
PackageManagerView tests all passing
This commit is contained in:
parent
beb151e1b0
commit
c05d760747
11 changed files with 312 additions and 212 deletions
14
esbuild.js
14
esbuild.js
|
|
@ -173,9 +173,23 @@ const extensionConfig = {
|
|||
{
|
||||
name: "alias-plugin",
|
||||
setup(build) {
|
||||
// Handle pkce-challenge alias
|
||||
build.onResolve({ filter: /^pkce-challenge$/ }, (args) => {
|
||||
return { path: require.resolve("pkce-challenge/dist/index.browser.js") }
|
||||
})
|
||||
|
||||
// Handle @package-manager barrel file
|
||||
build.onResolve({ filter: /^@package-manager$/ }, (args) => {
|
||||
const resolvedPath = path.resolve(__dirname, "src/services/package-manager/index.ts")
|
||||
return { path: resolvedPath }
|
||||
})
|
||||
|
||||
// Handle @package-manager/* paths
|
||||
build.onResolve({ filter: /^@package-manager\// }, (args) => {
|
||||
const modulePath = args.path.replace(/^@package-manager\//, "")
|
||||
const fullPath = path.resolve(__dirname, "src/services/package-manager", `${modulePath}.ts`)
|
||||
return { path: fullPath }
|
||||
})
|
||||
},
|
||||
},
|
||||
],
|
||||
|
|
|
|||
|
|
@ -2,10 +2,15 @@ import * as vscode from "vscode"
|
|||
import { ClineProvider } from "./ClineProvider"
|
||||
import { WebviewMessage } from "../../shared/WebviewMessage"
|
||||
import { ExtensionMessage } from "../../shared/ExtensionMessage"
|
||||
import { PackageManagerManager } from "../../services/package-manager"
|
||||
import { ComponentType, PackageManagerItem, PackageManagerSource } from "../../services/package-manager/types"
|
||||
import { DEFAULT_PACKAGE_MANAGER_SOURCE } from "../../services/package-manager/constants"
|
||||
import { validateSources } from "../../services/package-manager/validation"
|
||||
import {
|
||||
PackageManagerManager,
|
||||
ComponentType,
|
||||
PackageManagerItem,
|
||||
PackageManagerSource,
|
||||
validateSources,
|
||||
ValidationError,
|
||||
} from "@package-manager"
|
||||
import { DEFAULT_PACKAGE_MANAGER_SOURCE } from "@package-manager/constants"
|
||||
import { GlobalState } from "../../schemas"
|
||||
|
||||
/**
|
||||
|
|
@ -32,7 +37,7 @@ export async function handlePackageManagerMessages(
|
|||
// Prevent multiple simultaneous fetches
|
||||
if (packageManagerManager.isFetching) {
|
||||
console.log("Package Manager: Fetch already in progress, skipping")
|
||||
provider.postMessageToWebview({
|
||||
await provider.postMessageToWebview({
|
||||
type: "state",
|
||||
text: "Fetch already in progress",
|
||||
})
|
||||
|
|
@ -96,7 +101,7 @@ export async function handlePackageManagerMessages(
|
|||
else if (result.errors && result.items.length === 0) {
|
||||
const errorMessage = `Failed to load package manager sources:\n${result.errors.join("\n")}`
|
||||
vscode.window.showErrorMessage(errorMessage)
|
||||
provider.postMessageToWebview({
|
||||
await provider.postMessageToWebview({
|
||||
type: "state",
|
||||
text: errorMessage,
|
||||
})
|
||||
|
|
@ -120,7 +125,7 @@ export async function handlePackageManagerMessages(
|
|||
const errorMessage = `Package manager initialization failed: ${initError instanceof Error ? initError.message : String(initError)}`
|
||||
console.error("Error in package manager initialization:", initError)
|
||||
vscode.window.showErrorMessage(errorMessage)
|
||||
provider.postMessageToWebview({
|
||||
await provider.postMessageToWebview({
|
||||
type: "state",
|
||||
text: errorMessage,
|
||||
})
|
||||
|
|
@ -132,7 +137,7 @@ export async function handlePackageManagerMessages(
|
|||
const errorMessage = `Failed to fetch package manager items: ${error instanceof Error ? error.message : String(error)}`
|
||||
console.error("Failed to fetch package manager items:", error)
|
||||
vscode.window.showErrorMessage(errorMessage)
|
||||
provider.postMessageToWebview({
|
||||
await provider.postMessageToWebview({
|
||||
type: "state",
|
||||
text: errorMessage,
|
||||
})
|
||||
|
|
@ -165,7 +170,7 @@ export async function handlePackageManagerMessages(
|
|||
|
||||
// Create a map of invalid indices
|
||||
const invalidIndices = new Set<number>()
|
||||
validationErrors.forEach((error) => {
|
||||
validationErrors.forEach((error: ValidationError) => {
|
||||
// Extract index from error message (Source #X: ...)
|
||||
const match = error.message.match(/Source #(\d+):/)
|
||||
if (match && match[1]) {
|
||||
|
|
@ -180,7 +185,7 @@ export async function handlePackageManagerMessages(
|
|||
updatedSources = updatedSources.filter((_, index) => !invalidIndices.has(index))
|
||||
|
||||
// Show validation errors
|
||||
const errorMessage = `Package manager sources validation failed:\n${validationErrors.map((e) => e.message).join("\n")}`
|
||||
const errorMessage = `Package manager sources validation failed:\n${validationErrors.map((e: ValidationError) => e.message).join("\n")}`
|
||||
console.error(errorMessage)
|
||||
vscode.window.showErrorMessage(errorMessage)
|
||||
}
|
||||
|
|
@ -282,7 +287,7 @@ export async function handlePackageManagerMessages(
|
|||
} finally {
|
||||
// Always notify the webview that the refresh is complete, even if it failed
|
||||
console.log(`Package Manager: Sending repositoryRefreshComplete message for ${message.url}`)
|
||||
provider.postMessageToWebview({
|
||||
await provider.postMessageToWebview({
|
||||
type: "repositoryRefreshComplete",
|
||||
url: message.url,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -3,10 +3,15 @@ import * as path from "path"
|
|||
import * as fs from "fs/promises"
|
||||
import * as yaml from "js-yaml"
|
||||
import simpleGit, { SimpleGit } from "simple-git"
|
||||
import { MetadataScanner } from "./MetadataScanner"
|
||||
import { validateAnyMetadata } from "./schemas"
|
||||
import { LocalizationOptions, PackageManagerItem, PackageManagerRepository, RepositoryMetadata } from "./types"
|
||||
import { getUserLocale } from "./utils"
|
||||
import { MetadataScanner } from "@package-manager/MetadataScanner"
|
||||
import { validateAnyMetadata } from "@package-manager/schemas"
|
||||
import {
|
||||
LocalizationOptions,
|
||||
PackageManagerItem,
|
||||
PackageManagerRepository,
|
||||
RepositoryMetadata,
|
||||
} from "@package-manager/types"
|
||||
import { getUserLocale } from "@package-manager/utils"
|
||||
|
||||
/**
|
||||
* Handles fetching and caching package manager repositories
|
||||
|
|
@ -96,8 +101,23 @@ export class GitFetcher {
|
|||
* @param repoDir Repository directory
|
||||
* @param forceRefresh Whether to force refresh
|
||||
*/
|
||||
/**
|
||||
* Clean up any git lock files in the repository
|
||||
* @param repoDir Repository directory
|
||||
*/
|
||||
private async cleanupGitLocks(repoDir: string): Promise<void> {
|
||||
const indexLockPath = path.join(repoDir, ".git", "index.lock")
|
||||
try {
|
||||
await fs.unlink(indexLockPath)
|
||||
} catch {
|
||||
// Ignore errors if file doesn't exist
|
||||
}
|
||||
}
|
||||
|
||||
private async cloneOrPullRepository(repoUrl: string, repoDir: string, forceRefresh: boolean): Promise<void> {
|
||||
try {
|
||||
// Clean up any existing git lock files first
|
||||
await this.cleanupGitLocks(repoDir)
|
||||
// Check if repository exists
|
||||
const gitDir = path.join(repoDir, ".git")
|
||||
let repoExists = await fs
|
||||
|
|
@ -114,6 +134,8 @@ export class GitFetcher {
|
|||
await git.raw(["reset", "--hard", "origin/main"])
|
||||
await git.raw(["clean", "-f", "-d"])
|
||||
} catch (error) {
|
||||
// Clean up git locks before retrying
|
||||
await this.cleanupGitLocks(repoDir)
|
||||
// If pull fails with specific errors that indicate repo corruption,
|
||||
// we should remove and re-clone
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
|
|
@ -132,6 +154,14 @@ export class GitFetcher {
|
|||
|
||||
if (!repoExists || forceRefresh) {
|
||||
try {
|
||||
// Clean up any existing git lock files
|
||||
const indexLockPath = path.join(repoDir, ".git", "index.lock")
|
||||
try {
|
||||
await fs.unlink(indexLockPath)
|
||||
} catch {
|
||||
// Ignore errors if file doesn't exist
|
||||
}
|
||||
|
||||
// Always remove the directory before cloning
|
||||
await fs.rm(repoDir, { recursive: true, force: true })
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import * as fs from "fs/promises"
|
|||
import * as vscode from "vscode"
|
||||
import * as yaml from "js-yaml"
|
||||
import { SimpleGit } from "simple-git"
|
||||
import { validateAnyMetadata } from "./schemas"
|
||||
import { validateAnyMetadata } from "@package-manager/schemas"
|
||||
import {
|
||||
ComponentMetadata,
|
||||
ComponentType,
|
||||
|
|
@ -11,8 +11,8 @@ import {
|
|||
LocalizedMetadata,
|
||||
PackageManagerItem,
|
||||
PackageMetadata,
|
||||
} from "./types"
|
||||
import { getUserLocale } from "./utils"
|
||||
} from "@package-manager/types"
|
||||
import { getUserLocale } from "@package-manager/utils"
|
||||
|
||||
/**
|
||||
* Handles component discovery and metadata loading
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import * as vscode from "vscode"
|
||||
import * as path from "path"
|
||||
import * as fs from "fs/promises"
|
||||
import { GitFetcher } from "./GitFetcher"
|
||||
import { GitFetcher } from "@package-manager/GitFetcher"
|
||||
import {
|
||||
PackageManagerItem,
|
||||
PackageManagerRepository,
|
||||
|
|
@ -9,20 +9,24 @@ import {
|
|||
ComponentType,
|
||||
ComponentMetadata,
|
||||
LocalizationOptions,
|
||||
} from "./types"
|
||||
import { getUserLocale } from "./utils"
|
||||
} from "@package-manager/types"
|
||||
import { getUserLocale } from "@package-manager/utils"
|
||||
|
||||
/**
|
||||
* Service for managing package manager data
|
||||
*/
|
||||
export class PackageManagerManager {
|
||||
private currentItems: PackageManagerItem[] = []
|
||||
public isFetching = false
|
||||
// Cache expiry time in milliseconds (set to a low value for testing)
|
||||
private static readonly CACHE_EXPIRY_MS = 3600000 // 1 hour
|
||||
|
||||
private gitFetcher: GitFetcher
|
||||
private cache: Map<string, { data: PackageManagerRepository; timestamp: number }> = new Map()
|
||||
public isFetching = false
|
||||
|
||||
// Concurrency control
|
||||
private activeSourceOperations = new Set<string>() // Track active git operations per source
|
||||
private isMetadataScanActive = false // Track active metadata scanning
|
||||
private pendingOperations: Array<() => Promise<void>> = [] // Queue for pending operations
|
||||
|
||||
constructor(private readonly context: vscode.ExtensionContext) {
|
||||
const localizationOptions: LocalizationOptions = {
|
||||
|
|
@ -37,6 +41,33 @@ export class PackageManagerManager {
|
|||
* @param sources The package manager sources
|
||||
* @returns An array of PackageManagerItem objects
|
||||
*/
|
||||
/**
|
||||
* Queue an operation to run when no metadata scan is active
|
||||
*/
|
||||
private async queueOperation(operation: () => Promise<void>): Promise<void> {
|
||||
if (this.isMetadataScanActive) {
|
||||
return new Promise((resolve) => {
|
||||
this.pendingOperations.push(async () => {
|
||||
await operation()
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
this.isMetadataScanActive = true
|
||||
await operation()
|
||||
} finally {
|
||||
this.isMetadataScanActive = false
|
||||
|
||||
// Process any pending operations
|
||||
const nextOperation = this.pendingOperations.shift()
|
||||
if (nextOperation) {
|
||||
void this.queueOperation(nextOperation)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async getPackageManagerItems(
|
||||
sources: PackageManagerSource[],
|
||||
): Promise<{ items: PackageManagerItem[]; errors?: string[] }> {
|
||||
|
|
@ -48,23 +79,34 @@ export class PackageManagerManager {
|
|||
const enabledSources = sources.filter((s) => s.enabled)
|
||||
console.log(`PackageManagerManager: ${enabledSources.length} enabled sources`)
|
||||
|
||||
// Process sources sequentially to avoid overwhelming the system
|
||||
// Process sources sequentially with locking
|
||||
for (const source of enabledSources) {
|
||||
try {
|
||||
console.log(`PackageManagerManager: Processing source ${source.url}`)
|
||||
// Pass the source name to getRepositoryData
|
||||
const repo = await this.getRepositoryData(source.url, false, source.name)
|
||||
if (this.isSourceLocked(source.url)) {
|
||||
console.log(`PackageManagerManager: Source ${source.url} is locked, skipping`)
|
||||
continue
|
||||
}
|
||||
|
||||
if (repo.items && repo.items.length > 0) {
|
||||
console.log(`PackageManagerManager: Found ${repo.items.length} items in ${source.url}`)
|
||||
items.push(...repo.items)
|
||||
} else {
|
||||
console.log(`PackageManagerManager: No items found in ${source.url}`)
|
||||
}
|
||||
try {
|
||||
this.lockSource(source.url)
|
||||
console.log(`PackageManagerManager: Processing source ${source.url}`)
|
||||
|
||||
// Queue metadata scanning operation
|
||||
await this.queueOperation(async () => {
|
||||
const repo = await this.getRepositoryData(source.url, false, source.name)
|
||||
|
||||
if (repo.items && repo.items.length > 0) {
|
||||
console.log(`PackageManagerManager: Found ${repo.items.length} items in ${source.url}`)
|
||||
items.push(...repo.items)
|
||||
} else {
|
||||
console.log(`PackageManagerManager: No items found in ${source.url}`)
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
console.error(`PackageManagerManager: Failed to fetch data from ${source.url}:`, error)
|
||||
errors.push(`Source ${source.url}: ${errorMessage}`)
|
||||
} finally {
|
||||
this.unlockSource(source.url)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -88,6 +130,27 @@ export class PackageManagerManager {
|
|||
* @param sourceName The name of the source
|
||||
* @returns A PackageManagerRepository object
|
||||
*/
|
||||
/**
|
||||
* Check if a source operation is in progress
|
||||
*/
|
||||
private isSourceLocked(url: string): boolean {
|
||||
return this.activeSourceOperations.has(url)
|
||||
}
|
||||
|
||||
/**
|
||||
* Lock a source for operations
|
||||
*/
|
||||
private lockSource(url: string): void {
|
||||
this.activeSourceOperations.add(url)
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlock a source after operations complete
|
||||
*/
|
||||
private unlockSource(url: string): void {
|
||||
this.activeSourceOperations.delete(url)
|
||||
}
|
||||
|
||||
async getRepositoryData(
|
||||
url: string,
|
||||
forceRefresh: boolean = false,
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import * as vscode from "vscode"
|
||||
import { GitFetcher } from "../GitFetcher"
|
||||
import { GitFetcher } from "@package-manager/GitFetcher"
|
||||
import * as fs from "fs/promises"
|
||||
import { Dirent, Stats } from "fs"
|
||||
import simpleGit, { SimpleGit } from "simple-git"
|
||||
import { MetadataScanner } from "../MetadataScanner"
|
||||
import { MetadataScanner } from "@package-manager/MetadataScanner"
|
||||
import { exec, ChildProcess } from "child_process"
|
||||
import { promisify } from "util"
|
||||
import { EventEmitter } from "events"
|
||||
|
|
@ -26,6 +26,7 @@ jest.mock("fs/promises", () => ({
|
|||
mkdir: jest.fn(),
|
||||
stat: jest.fn(),
|
||||
rm: jest.fn(),
|
||||
unlink: jest.fn(),
|
||||
readdir: jest.fn().mockResolvedValue([]),
|
||||
readFile: jest.fn().mockResolvedValue(`
|
||||
name: Test Repository
|
||||
|
|
@ -258,6 +259,59 @@ describe("GitFetcher", () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe("Git Lock File Handling", () => {
|
||||
it("should clean up index.lock file before operations", async () => {
|
||||
// Mock repository exists
|
||||
;(fs.stat as jest.Mock).mockImplementation((path: string) => {
|
||||
if (path.endsWith(".git")) return Promise.resolve(true)
|
||||
if (path.endsWith("metadata.en.yml")) return Promise.resolve(true)
|
||||
if (path.endsWith("README.md")) return Promise.resolve(true)
|
||||
return Promise.reject(new Error("ENOENT"))
|
||||
})
|
||||
|
||||
await gitFetcher.fetchRepository(testRepoUrl)
|
||||
|
||||
// Verify lock file cleanup was attempted
|
||||
expect(fs.unlink).toHaveBeenCalledWith(expect.stringContaining("index.lock"))
|
||||
})
|
||||
|
||||
it("should handle missing lock file gracefully", async () => {
|
||||
// Mock unlink to fail as if file doesn't exist
|
||||
;(fs.unlink as jest.Mock).mockRejectedValue(new Error("ENOENT"))
|
||||
|
||||
await gitFetcher.fetchRepository(testRepoUrl)
|
||||
|
||||
// Operation should succeed despite lock file not existing
|
||||
const mockGit = mockSimpleGit()
|
||||
expect(mockGit.clone).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should clean up lock file when pull fails", async () => {
|
||||
// Mock repository exists
|
||||
;(fs.stat as jest.Mock).mockImplementation((path: string) => {
|
||||
if (path.endsWith(".git")) return Promise.resolve(true)
|
||||
if (path.endsWith("metadata.en.yml")) return Promise.resolve(true)
|
||||
if (path.endsWith("README.md")) return Promise.resolve(true)
|
||||
return Promise.reject(new Error("ENOENT"))
|
||||
})
|
||||
|
||||
const mockGit = {
|
||||
clone: jest.fn().mockResolvedValue(undefined),
|
||||
pull: jest.fn().mockRejectedValue(new Error("not a git repository")),
|
||||
revparse: jest.fn().mockResolvedValue("main"),
|
||||
fetch: jest.fn().mockRejectedValue(new Error("not a git repository")),
|
||||
clean: jest.fn(),
|
||||
raw: jest.fn(),
|
||||
} as unknown as SimpleGit
|
||||
mockSimpleGit.mockReturnValue(mockGit)
|
||||
|
||||
await gitFetcher.fetchRepository(testRepoUrl)
|
||||
|
||||
// Verify lock file cleanup was attempted after pull failure
|
||||
expect(fs.unlink).toHaveBeenCalledWith(expect.stringContaining("index.lock"))
|
||||
})
|
||||
})
|
||||
|
||||
describe("Repository Structure Validation", () => {
|
||||
// Helper function to access private method
|
||||
const validateRepositoryStructure = async (repoDir: string) => {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import * as path from "path"
|
||||
import * as fs from "fs/promises"
|
||||
import { Dirent } from "fs"
|
||||
import { MetadataScanner } from "../MetadataScanner"
|
||||
import { MetadataScanner } from "@package-manager/MetadataScanner"
|
||||
import { SimpleGit } from "simple-git"
|
||||
import { ComponentMetadata, LocalizationOptions, LocalizedMetadata, PackageMetadata } from "../types"
|
||||
import { ComponentMetadata, LocalizationOptions, LocalizedMetadata, PackageMetadata } from "@package-manager/types"
|
||||
|
||||
// Mock fs/promises
|
||||
jest.mock("fs/promises")
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { PackageManagerManager } from "../PackageManagerManager"
|
||||
import { PackageManagerItem } from "../types"
|
||||
import { MetadataScanner } from "../MetadataScanner"
|
||||
import { PackageManagerManager } from "@package-manager/PackageManagerManager"
|
||||
import { PackageManagerItem, PackageManagerSource, PackageManagerRepository } from "@package-manager/types"
|
||||
import { MetadataScanner } from "@package-manager/MetadataScanner"
|
||||
import { GitFetcher } from "@package-manager/GitFetcher"
|
||||
import * as path from "path"
|
||||
import * as vscode from "vscode"
|
||||
|
||||
|
|
@ -68,6 +69,8 @@ describe("PackageManagerManager", () => {
|
|||
]
|
||||
})
|
||||
|
||||
// Concurrency Control tests moved to their own describe block
|
||||
|
||||
test("should include package when filtering by its own type", () => {
|
||||
// Filter by package type
|
||||
const filtered = manager.filterItems(typeFilterTestItems, { type: "package" })
|
||||
|
|
@ -421,182 +424,107 @@ describe("PackageManagerManager", () => {
|
|||
})
|
||||
})
|
||||
|
||||
// Re-declare manager for the following test sections
|
||||
let manager: PackageManagerManager
|
||||
beforeEach(() => {
|
||||
const context = {
|
||||
globalStorageUri: { fsPath: path.resolve(__dirname, "../../../../mock/settings/path") },
|
||||
} as vscode.ExtensionContext
|
||||
manager = new PackageManagerManager(context)
|
||||
})
|
||||
describe("Concurrency Control", () => {
|
||||
let manager: PackageManagerManager
|
||||
|
||||
describe("sortItems with subcomponents", () => {
|
||||
const testItems: PackageManagerItem[] = [
|
||||
{
|
||||
name: "B Package",
|
||||
description: "Package B",
|
||||
type: "package",
|
||||
version: "1.0.0",
|
||||
url: "/test/b",
|
||||
repoUrl: "https://example.com",
|
||||
items: [
|
||||
{
|
||||
type: "mode",
|
||||
path: "modes/y",
|
||||
metadata: {
|
||||
name: "Y Mode",
|
||||
description: "Mode Y",
|
||||
type: "mode",
|
||||
version: "1.0.0",
|
||||
},
|
||||
lastUpdated: "2025-04-13T09:00:00-07:00",
|
||||
},
|
||||
{
|
||||
type: "mode",
|
||||
path: "modes/x",
|
||||
metadata: {
|
||||
name: "X Mode",
|
||||
description: "Mode X",
|
||||
type: "mode",
|
||||
version: "1.0.0",
|
||||
},
|
||||
lastUpdated: "2025-04-13T09:00:00-07:00",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "A Package",
|
||||
description: "Package A",
|
||||
type: "package",
|
||||
version: "1.0.0",
|
||||
url: "/test/a",
|
||||
repoUrl: "https://example.com",
|
||||
items: [
|
||||
{
|
||||
type: "mode",
|
||||
path: "modes/z",
|
||||
metadata: {
|
||||
name: "Z Mode",
|
||||
description: "Mode Z",
|
||||
type: "mode",
|
||||
version: "1.0.0",
|
||||
},
|
||||
lastUpdated: "2025-04-13T08:00:00-07:00",
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
it("should sort parent items while preserving subcomponents", () => {
|
||||
const sorted = manager.sortItems(testItems, "name", "asc")
|
||||
expect(sorted[0].name).toBe("A Package")
|
||||
expect(sorted[1].name).toBe("B Package")
|
||||
expect(sorted[0].items![0].metadata!.name).toBe("Z Mode")
|
||||
expect(sorted[1].items![0].metadata!.name).toBe("Y Mode")
|
||||
beforeEach(() => {
|
||||
const mockContext = {
|
||||
globalStorageUri: { fsPath: "/test/path" },
|
||||
} as vscode.ExtensionContext
|
||||
manager = new PackageManagerManager(mockContext)
|
||||
})
|
||||
|
||||
it("should sort subcomponents within parents", () => {
|
||||
const sorted = manager.sortItems(testItems, "name", "asc", true)
|
||||
expect(sorted[1].items![0].metadata!.name).toBe("X Mode")
|
||||
expect(sorted[1].items![1].metadata!.name).toBe("Y Mode")
|
||||
})
|
||||
it("should not allow concurrent operations on the same source", async () => {
|
||||
const source: PackageManagerSource = {
|
||||
url: "https://github.com/test/repo",
|
||||
enabled: true,
|
||||
}
|
||||
|
||||
it("should preserve subcomponent order when sortSubcomponents is false", () => {
|
||||
const sorted = manager.sortItems(testItems, "name", "asc", false)
|
||||
expect(sorted[1].items![0].metadata!.name).toBe("Y Mode")
|
||||
expect(sorted[1].items![1].metadata!.name).toBe("X Mode")
|
||||
})
|
||||
|
||||
it("should handle empty subcomponents when sorting", () => {
|
||||
const itemsWithEmpty = [
|
||||
...testItems,
|
||||
{
|
||||
name: "C Package",
|
||||
description: "Package C",
|
||||
type: "package" as const,
|
||||
version: "1.0.0",
|
||||
url: "/test/c",
|
||||
repoUrl: "https://example.com",
|
||||
// Mock getRepositoryData to be slow
|
||||
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
|
||||
const slowGetRepositoryData = jest.spyOn(manager as any, "getRepositoryData").mockImplementation(async () => {
|
||||
await delay(100) // Simulate slow operation
|
||||
return {
|
||||
metadata: { name: "test", description: "test", version: "1.0.0" },
|
||||
items: [],
|
||||
} as PackageManagerItem,
|
||||
]
|
||||
const sorted = manager.sortItems(itemsWithEmpty, "name", "asc")
|
||||
expect(sorted[2].name).toBe("C Package")
|
||||
expect(sorted[2].items).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
url: source.url,
|
||||
} as PackageManagerRepository
|
||||
})
|
||||
|
||||
describe("filterItems with real data", () => {
|
||||
it("should return all subcomponents with match info", () => {
|
||||
const testItems: PackageManagerItem[] = [
|
||||
{
|
||||
name: "Data Platform Package",
|
||||
description: "A test platform",
|
||||
type: "package",
|
||||
version: "1.0.0",
|
||||
url: "/test/data-platform",
|
||||
repoUrl: "https://example.com",
|
||||
items: [
|
||||
{
|
||||
type: "mcp server",
|
||||
path: "mcp servers/data-validator",
|
||||
metadata: {
|
||||
name: "Data Validator",
|
||||
description: "An MCP server for validating data quality",
|
||||
type: "mcp server",
|
||||
version: "1.0.0",
|
||||
},
|
||||
lastUpdated: "2025-04-13T10:00:00-07:00",
|
||||
},
|
||||
{
|
||||
type: "mode",
|
||||
path: "modes/task-runner",
|
||||
metadata: {
|
||||
name: "Task Runner",
|
||||
description: "A mode for running tasks",
|
||||
type: "mode",
|
||||
version: "1.0.0",
|
||||
},
|
||||
lastUpdated: "2025-04-13T10:00:00-07:00",
|
||||
},
|
||||
],
|
||||
},
|
||||
// Start two concurrent operations
|
||||
const operation1 = manager.getPackageManagerItems([source])
|
||||
const operation2 = manager.getPackageManagerItems([source])
|
||||
|
||||
// Wait for both to complete
|
||||
const [result1, result2] = await Promise.all([operation1, operation2])
|
||||
|
||||
// Verify getRepositoryData was only called once
|
||||
expect(slowGetRepositoryData).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it("should not allow metadata scanning during git operations", async () => {
|
||||
const source1: PackageManagerSource = {
|
||||
url: "https://github.com/test/repo1",
|
||||
enabled: true,
|
||||
}
|
||||
const source2: PackageManagerSource = {
|
||||
url: "https://github.com/test/repo2",
|
||||
enabled: true,
|
||||
}
|
||||
|
||||
let isGitOperationActive = false
|
||||
let metadataScanDuringGit = false
|
||||
|
||||
// Mock git operation to be slow and set flag
|
||||
jest.spyOn(GitFetcher.prototype, "fetchRepository").mockImplementation(async () => {
|
||||
isGitOperationActive = true
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
isGitOperationActive = false
|
||||
return {
|
||||
metadata: { name: "test", description: "test", version: "1.0.0" },
|
||||
items: [],
|
||||
url: source1.url,
|
||||
}
|
||||
})
|
||||
|
||||
// Mock metadata scanner to check if git operation is active
|
||||
jest.spyOn(MetadataScanner.prototype, "scanDirectory").mockImplementation(async () => {
|
||||
if (isGitOperationActive) {
|
||||
metadataScanDuringGit = true
|
||||
}
|
||||
return []
|
||||
})
|
||||
|
||||
// Process both sources
|
||||
await manager.getPackageManagerItems([source1, source2])
|
||||
|
||||
// Verify metadata scanning didn't occur during git operations
|
||||
expect(metadataScanDuringGit).toBe(false)
|
||||
})
|
||||
|
||||
it("should queue metadata scans and process them sequentially", async () => {
|
||||
const sources: PackageManagerSource[] = [
|
||||
{ url: "https://github.com/test/repo1", enabled: true },
|
||||
{ url: "https://github.com/test/repo2", enabled: true },
|
||||
{ url: "https://github.com/test/repo3", enabled: true },
|
||||
]
|
||||
|
||||
// Search for "data validator"
|
||||
const filtered = manager.filterItems(testItems, { search: "data validator" })
|
||||
let activeScans = 0
|
||||
let maxConcurrentScans = 0
|
||||
|
||||
// Verify package is returned
|
||||
expect(filtered.length).toBe(1)
|
||||
const pkg = filtered[0]
|
||||
|
||||
// Verify all subcomponents are returned
|
||||
expect(pkg.items?.length).toBe(2)
|
||||
|
||||
// Verify matching subcomponent has correct matchInfo
|
||||
const validator = pkg.items?.find((item: any) => item.metadata?.name === "Data Validator")
|
||||
expect(validator?.matchInfo).toEqual({
|
||||
matched: true,
|
||||
matchReason: {
|
||||
nameMatch: true,
|
||||
descriptionMatch: false,
|
||||
},
|
||||
// Mock metadata scanner to track concurrent scans
|
||||
jest.spyOn(MetadataScanner.prototype, "scanDirectory").mockImplementation(async () => {
|
||||
activeScans++
|
||||
maxConcurrentScans = Math.max(maxConcurrentScans, activeScans)
|
||||
await new Promise((resolve) => setTimeout(resolve, 50))
|
||||
activeScans--
|
||||
return []
|
||||
})
|
||||
|
||||
// Verify non-matching subcomponent has correct matchInfo
|
||||
const runner = pkg.items?.find((item: any) => item.metadata?.name === "Task Runner")
|
||||
expect(runner?.matchInfo).toEqual({
|
||||
matched: false,
|
||||
})
|
||||
// Process all sources
|
||||
await manager.getPackageManagerItems(sources)
|
||||
|
||||
// Verify package has matchInfo indicating it contains matches
|
||||
expect(pkg.matchInfo).toEqual({
|
||||
matched: true,
|
||||
matchReason: {
|
||||
nameMatch: false,
|
||||
descriptionMatch: false,
|
||||
hasMatchingSubcomponents: true,
|
||||
},
|
||||
})
|
||||
// Verify only one scan was active at a time
|
||||
expect(maxConcurrentScans).toBe(1)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ import {
|
|||
validateSource,
|
||||
validateSources,
|
||||
ValidationError,
|
||||
} from "../PackageManagerSourceValidation"
|
||||
import { PackageManagerSource } from "../types"
|
||||
} from "@package-manager/PackageManagerSourceValidation"
|
||||
import { PackageManagerSource } from "@package-manager/types"
|
||||
|
||||
describe("PackageManagerSourceValidation", () => {
|
||||
describe("isValidGitRepositoryUrl", () => {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
export * from "./GitFetcher";
|
||||
export * from "./PackageManagerManager";
|
||||
export * from "./types";
|
||||
export * from "./GitFetcher"
|
||||
export * from "./PackageManagerManager"
|
||||
export * from "./types"
|
||||
export * from "./PackageManagerSourceValidation"
|
||||
|
|
|
|||
|
|
@ -18,7 +18,12 @@
|
|||
"strict": true,
|
||||
"target": "es2022",
|
||||
"useDefineForClassFields": true,
|
||||
"useUnknownInCatchVariables": false
|
||||
"useUnknownInCatchVariables": false,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@package-manager": ["src/services/package-manager"],
|
||||
"@package-manager/*": ["src/services/package-manager/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*", "scripts/**/*", ".changeset/**/*"],
|
||||
"exclude": ["node_modules", ".vscode-test", "webview-ui"]
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue