diff --git a/esbuild.js b/esbuild.js index 6fc0c24729..8d4ed7afd1 100644 --- a/esbuild.js +++ b/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 } + }) }, }, ], diff --git a/src/core/webview/packageManagerMessageHandler.ts b/src/core/webview/packageManagerMessageHandler.ts index aca8ddf8b3..5bcf230e11 100644 --- a/src/core/webview/packageManagerMessageHandler.ts +++ b/src/core/webview/packageManagerMessageHandler.ts @@ -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() - 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, }) diff --git a/src/services/package-manager/GitFetcher.ts b/src/services/package-manager/GitFetcher.ts index bc14c19836..441a57467b 100644 --- a/src/services/package-manager/GitFetcher.ts +++ b/src/services/package-manager/GitFetcher.ts @@ -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 { + 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 { 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 }) diff --git a/src/services/package-manager/MetadataScanner.ts b/src/services/package-manager/MetadataScanner.ts index 0e7277ec36..7f71715eda 100644 --- a/src/services/package-manager/MetadataScanner.ts +++ b/src/services/package-manager/MetadataScanner.ts @@ -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 diff --git a/src/services/package-manager/PackageManagerManager.ts b/src/services/package-manager/PackageManagerManager.ts index 9a3edad0a5..cb44cbd3ff 100644 --- a/src/services/package-manager/PackageManagerManager.ts +++ b/src/services/package-manager/PackageManagerManager.ts @@ -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 = new Map() + public isFetching = false + + // Concurrency control + private activeSourceOperations = new Set() // Track active git operations per source + private isMetadataScanActive = false // Track active metadata scanning + private pendingOperations: Array<() => Promise> = [] // 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): Promise { + 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, diff --git a/src/services/package-manager/__tests__/GitFetcher.test.ts b/src/services/package-manager/__tests__/GitFetcher.test.ts index 78cd5e8862..28de6d30bf 100644 --- a/src/services/package-manager/__tests__/GitFetcher.test.ts +++ b/src/services/package-manager/__tests__/GitFetcher.test.ts @@ -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) => { diff --git a/src/services/package-manager/__tests__/MetadataScanner.test.ts b/src/services/package-manager/__tests__/MetadataScanner.test.ts index c1b2965622..bdaec05d21 100644 --- a/src/services/package-manager/__tests__/MetadataScanner.test.ts +++ b/src/services/package-manager/__tests__/MetadataScanner.test.ts @@ -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") diff --git a/src/services/package-manager/__tests__/PackageManagerManager.test.ts b/src/services/package-manager/__tests__/PackageManagerManager.test.ts index 26e8ca06d4..e89b9288b8 100644 --- a/src/services/package-manager/__tests__/PackageManagerManager.test.ts +++ b/src/services/package-manager/__tests__/PackageManagerManager.test.ts @@ -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) }) }) diff --git a/src/services/package-manager/__tests__/PackageManagerSourceValidation.test.ts b/src/services/package-manager/__tests__/PackageManagerSourceValidation.test.ts index 03f8c0580e..f489d495ac 100644 --- a/src/services/package-manager/__tests__/PackageManagerSourceValidation.test.ts +++ b/src/services/package-manager/__tests__/PackageManagerSourceValidation.test.ts @@ -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", () => { diff --git a/src/services/package-manager/index.ts b/src/services/package-manager/index.ts index 55274b3b38..ef47f44106 100644 --- a/src/services/package-manager/index.ts +++ b/src/services/package-manager/index.ts @@ -1,3 +1,4 @@ -export * from "./GitFetcher"; -export * from "./PackageManagerManager"; -export * from "./types"; \ No newline at end of file +export * from "./GitFetcher" +export * from "./PackageManagerManager" +export * from "./types" +export * from "./PackageManagerSourceValidation" diff --git a/tsconfig.json b/tsconfig.json index 1d70336fc1..a497695b6d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -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"]