From 0f532a88fae914661a9e4638b3d7548903d869aa Mon Sep 17 00:00:00 2001 From: Power-Maverick Date: Tue, 25 Aug 2026 17:58:58 -0400 Subject: [PATCH] feat: implement tool maturity feature with verified badge and filtering - Added maturity property to ToolDetail interface for tool maturity tracking. - Introduced utility functions for handling tool maturity: isVerifiedTool, compareVerifiedFirst, and renderVerifiedBadge. - Updated toolsSidebarManagement to load tools with maturity information and filter based on verified status. - Enhanced UI to display verified badges for tools and apply sorting/filtering based on maturity. - Created styles for verified tools and badges in both light and dark themes. - Added e2e tests for verifying tool maturity functionality, including badge display and filtering. - Included unit tests for tool maturity utilities and registry manager maturity mapping. - Documented local testing setup for tool maturity feature. --- docs/TOOL_MATURITY_LOCAL_TESTING.md | 26 +++++++ src/common/types/tool.ts | 3 + src/main/index.ts | 3 +- src/main/managers/toolRegistryManager.ts | 22 +++++- src/main/managers/toolsManager.ts | 1 + src/main/utilities/mockRegistry.ts | 3 + src/renderer/icons/dark/verified.svg | 5 ++ src/renderer/icons/light/verified.svg | 3 + src/renderer/index.html | 9 +++ src/renderer/modules/marketplaceManagement.ts | 44 +++++++++-- .../modules/toolsSidebarManagement.ts | 46 ++++++++++-- src/renderer/styles.scss | 46 ++++++++++++ src/renderer/types/index.ts | 1 + src/renderer/utils/toolMaturity.ts | 15 ++++ tests/e2e/data/maturity-registry.json | 59 +++++++++++++++ tests/e2e/fixtures.ts | 53 ++++++++++++-- tests/e2e/toolMaturity.spec.ts | 73 +++++++++++++++++++ .../main/managers/toolRegistryManager.test.ts | 15 ++++ tests/unit/renderer/toolMaturity.test.ts | 28 +++++++ 19 files changed, 435 insertions(+), 20 deletions(-) create mode 100644 docs/TOOL_MATURITY_LOCAL_TESTING.md create mode 100644 src/renderer/icons/dark/verified.svg create mode 100644 src/renderer/icons/light/verified.svg create mode 100644 src/renderer/utils/toolMaturity.ts create mode 100644 tests/e2e/data/maturity-registry.json create mode 100644 tests/e2e/toolMaturity.spec.ts create mode 100644 tests/unit/main/managers/toolRegistryManager.test.ts create mode 100644 tests/unit/renderer/toolMaturity.test.ts diff --git a/docs/TOOL_MATURITY_LOCAL_TESTING.md b/docs/TOOL_MATURITY_LOCAL_TESTING.md new file mode 100644 index 00000000..a4859bdd --- /dev/null +++ b/docs/TOOL_MATURITY_LOCAL_TESTING.md @@ -0,0 +1,26 @@ +# Tool Maturity UI Local Testing + +## Setup + +Use registry data containing `maturity: "Verified"`, no maturity value, and an unrecognized value such as `maturity: "Community Recommended"`. + +The built-in Supabase source reads `tool_maturity(status)` through the `tools` relationship. A missing `tool_maturity` row is treated as Unverified. Because the desktop app uses an anonymous key, the backend must grant public read access only to `tool_maturity.tool_id` and `tool_maturity.status`; review requests, reviewer identities, CSP snapshots, and change reasons must remain restricted. + +```bash +pnpm run typecheck +pnpm run lint +pnpm run test:unit -- tests/unit/renderer/toolMaturity.test.ts +pnpm exec playwright test tests/e2e/toolMaturity.spec.ts +pnpm run build +pnpm run dev +``` + +## Manual Checks + +1. Open Marketplace and confirm Verified tools appear before other tools while each selected sort remains the order within those groups. +2. Confirm only `Verified` tools show the checkmark badge in standard and compact modes, in light and dark themes. +3. Hover and inspect the badge with a screen reader to verify its short explanation is available. +4. Enable **Verified only** in Marketplace and Installed; confirm only Verified tools remain and the existing empty state appears when none match. +5. In Installed, select **Maturity (Verified first)** and confirm Verified tools appear first, followed by name. +6. Change a tool's registry maturity, wait at least 30 seconds, then reload either list through navigation or a filter change. Confirm both views reflect the new status without restarting the app. +7. Confirm missing, `Unverified`, and unknown maturity values show no badge. diff --git a/src/common/types/tool.ts b/src/common/types/tool.ts index 8804eeca..b57152a7 100644 --- a/src/common/types/tool.ts +++ b/src/common/types/tool.ts @@ -78,6 +78,7 @@ export interface Tool { marketplaceSourceId?: string; marketplaceSourceLabel?: string; marketplaceSourceType?: "builtin" | "private"; + maturity?: string; } /** @@ -112,6 +113,7 @@ export interface ToolRegistryEntry { marketplaceSourceId?: string; marketplaceSourceLabel?: string; marketplaceSourceType?: "builtin" | "private"; + maturity?: string; } /** @@ -149,6 +151,7 @@ export interface ToolManifest { marketplaceSourceId?: string; marketplaceSourceLabel?: string; marketplaceSourceType?: "builtin" | "private"; + maturity?: string; } /** diff --git a/src/main/index.ts b/src/main/index.ts index 570c07e5..1daa7d03 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -137,8 +137,9 @@ class ToolBoxApp { this.connectionsManager = new ConnectionsManager(); this.api = new ToolBoxUtilityManager(); // Pass Supabase credentials and Azure Blob base URL from environment variables + const testToolsDirectory = process.env.PPTB_TEST_MODE === "1" ? process.env.PPTB_TEST_TOOLS_DIRECTORY : undefined; this.toolManager = new ToolManager( - path.join(app.getPath("userData"), "tools"), + testToolsDirectory || path.join(app.getPath("userData"), "tools"), process.env.SUPABASE_URL, process.env.SUPABASE_ANON_KEY, this.installIdManager, diff --git a/src/main/managers/toolRegistryManager.ts b/src/main/managers/toolRegistryManager.ts index f6bed5e6..f46522b6 100644 --- a/src/main/managers/toolRegistryManager.ts +++ b/src/main/managers/toolRegistryManager.ts @@ -35,6 +35,14 @@ interface SupabaseAnalyticsRow { mau?: number; // Monthly Active Users } +interface SupabaseMaturityRow { + status?: string; +} + +export function getSupabaseMaturityStatus(relation: SupabaseMaturityRow | SupabaseMaturityRow[] | undefined): string | undefined { + return (Array.isArray(relation) ? relation[0] : relation)?.status; +} + function getOptionalAnalyticsNumber(value: number | null | undefined): number | undefined { return typeof value === "number" ? value : undefined; } @@ -81,6 +89,7 @@ interface SupabaseTool { website?: string; min_api?: string; // Minimum ToolBox API version required max_api?: string; // Maximum ToolBox API version tested + tool_maturity?: SupabaseMaturityRow | SupabaseMaturityRow[]; tool_categories?: SupabaseCategoryRow[]; tool_contributors?: SupabaseContributorRow[]; tool_analytics?: SupabaseAnalyticsRow | SupabaseAnalyticsRow[]; // sometimes array depending on RLS / joins @@ -190,9 +199,10 @@ export class ToolRegistryManager extends EventEmitter { // Initialize Supabase client const url = supabaseUrl || SUPABASE_URL; const key = supabaseKey || SUPABASE_ANON_KEY; + const useTestRegistry = process.env.PPTB_TEST_MODE === "1" && !!process.env.PPTB_TEST_REGISTRY_PATH; // Validate Supabase credentials and create client - if (!url || !key || url === "" || key === "") { + if (useTestRegistry || !url || !key || url === "" || key === "") { logWarn("[ToolRegistry] Supabase credentials not configured. Set SUPABASE_URL and SUPABASE_ANON_KEY environment variables."); logWarn("[ToolRegistry] Falling back to local registry.json file."); this.useLocalFallback = true; @@ -347,6 +357,7 @@ export class ToolRegistryManager extends EventEmitter { features: tool.features, license: tool.license, status: (tool.status as "active" | "deprecated" | "archived" | undefined) || "active", + maturity: tool.maturity, marketplaceSourceId: source.id, marketplaceSourceLabel: source.label, marketplaceSourceType: source.type, @@ -385,6 +396,7 @@ export class ToolRegistryManager extends EventEmitter { "min_api", "max_api", // embedded relations + "tool_maturity(status)", "tool_categories(categories(name))", "tool_contributors(contributors(name,profile_url))", "tool_analytics(downloads,rating,mau)", @@ -444,6 +456,7 @@ export class ToolRegistryManager extends EventEmitter { minAPI: tool.min_api, // Include min API version from database maxAPI: tool.max_api, // Include max API version from database npmPackageName: tool.packagename || undefined, // npm package name for pre-release detection + maturity: getSupabaseMaturityStatus(tool.tool_maturity), } as ToolRegistryEntry; }); @@ -460,6 +473,10 @@ export class ToolRegistryManager extends EventEmitter { * Azure Blob is tried first (when configured), then the local registry.json. */ private async fetchFallbackRegistry(): Promise { + if (process.env.PPTB_TEST_MODE === "1" && process.env.PPTB_TEST_REGISTRY_PATH) { + return this.fetchLocalRegistry(); + } + if (this.azureBlobBaseUrl) { try { const tools = await this.fetchAzureBlobRegistry(); @@ -529,6 +546,7 @@ export class ToolRegistryManager extends EventEmitter { features: tool.features, license: tool.license, status: (tool.status as "active" | "deprecated" | "archived" | undefined) || "active", + maturity: tool.maturity, })); logInfo(`[ToolRegistry] Fetched ${tools.length} tools from Azure Blob registry`); @@ -627,6 +645,7 @@ export class ToolRegistryManager extends EventEmitter { status: (tool.status as "active" | "deprecated" | "archived" | undefined) || "active", minAPI: tool.minAPI, maxAPI: tool.maxAPI, + maturity: tool.maturity, })); } @@ -874,6 +893,7 @@ export class ToolRegistryManager extends EventEmitter { marketplaceSourceId: tool.marketplaceSourceId, marketplaceSourceLabel: tool.marketplaceSourceLabel, marketplaceSourceType: tool.marketplaceSourceType, + maturity: tool.maturity, }; // Save to manifest file diff --git a/src/main/managers/toolsManager.ts b/src/main/managers/toolsManager.ts index 976b37cc..60d0aba1 100644 --- a/src/main/managers/toolsManager.ts +++ b/src/main/managers/toolsManager.ts @@ -95,6 +95,7 @@ export class ToolManager extends EventEmitter { marketplaceSourceId: manifest.marketplaceSourceId, marketplaceSourceLabel: manifest.marketplaceSourceLabel, marketplaceSourceType: manifest.marketplaceSourceType, + maturity: manifest.maturity, }; const cached = this.analyticsCache.get(tool.id); diff --git a/src/main/utilities/mockRegistry.ts b/src/main/utilities/mockRegistry.ts index b20b081f..b9efc1d3 100644 --- a/src/main/utilities/mockRegistry.ts +++ b/src/main/utilities/mockRegistry.ts @@ -32,6 +32,7 @@ export interface OfflineMockRegistryTool { status?: string; minAPI?: string; maxAPI?: string; + maturity?: string; } export interface OfflineMockRegistryLoadResult { @@ -40,7 +41,9 @@ export interface OfflineMockRegistryLoadResult { } function resolveOfflineMockRegistryPath(): string | null { + const testRegistryPath = process.env.PPTB_TEST_MODE === "1" ? process.env.PPTB_TEST_REGISTRY_PATH : undefined; const candidatePaths = [ + ...(testRegistryPath ? [testRegistryPath] : []), // Bundled layout: dist/main/data/registry.json (most common runtime path) path.join(__dirname, "..", "data", "registry.json"), // Defensive fallback for alternate build layouts diff --git a/src/renderer/icons/dark/verified.svg b/src/renderer/icons/dark/verified.svg new file mode 100644 index 00000000..d643653b --- /dev/null +++ b/src/renderer/icons/dark/verified.svg @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/src/renderer/icons/light/verified.svg b/src/renderer/icons/light/verified.svg new file mode 100644 index 00000000..af33cb81 --- /dev/null +++ b/src/renderer/icons/light/verified.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/renderer/index.html b/src/renderer/index.html index 54867f3d..893916bf 100644 --- a/src/renderer/index.html +++ b/src/renderer/index.html @@ -90,6 +90,7 @@ +
@@ -120,6 +121,10 @@ MCP Enabled +