feat(pro): add plugin marketplace with catalog, install, and update support

Marketplace backend (agor-pro/src/marketplace.rs): fetch catalog from
GitHub, download+verify+extract plugins, install/uninstall/update with
SHA-256 checksum verification and path traversal protection. 6 Tauri
plugin commands.

PluginMarketplace.svelte: Browse/Installed tabs, search, plugin cards
with permission badges, one-click install/uninstall/update.

Plugin catalog repo: agents-orchestrator/agor-plugins (3 seed plugins).
Plugin scaffolding: scripts/plugin-init.sh.
7 marketplace vitest tests, 3 Rust tests.
This commit is contained in:
Hibryda 2026-03-17 02:20:10 +01:00
parent a98d061b04
commit 5300c09157
8 changed files with 1109 additions and 0 deletions

View file

@ -0,0 +1,99 @@
// SPDX-License-Identifier: LicenseRef-Commercial
// Marketplace tests — catalog fetch, install, uninstall, update flows.
import { describe, it, expect, vi, beforeEach } from 'vitest';
vi.mock('@tauri-apps/api/core', () => ({
invoke: vi.fn(),
}));
describe('Marketplace Bridge', async () => {
const { invoke } = await import('@tauri-apps/api/core');
const mockInvoke = vi.mocked(invoke);
beforeEach(() => {
mockInvoke.mockReset();
});
it('exports all marketplace functions', async () => {
const bridge = await import('../../src/lib/commercial/pro-bridge');
expect(typeof bridge.proMarketplaceFetchCatalog).toBe('function');
expect(typeof bridge.proMarketplaceInstalled).toBe('function');
expect(typeof bridge.proMarketplaceInstall).toBe('function');
expect(typeof bridge.proMarketplaceUninstall).toBe('function');
expect(typeof bridge.proMarketplaceCheckUpdates).toBe('function');
expect(typeof bridge.proMarketplaceUpdate).toBe('function');
});
it('proMarketplaceFetchCatalog calls correct plugin command', async () => {
const { proMarketplaceFetchCatalog } = await import('../../src/lib/commercial/pro-bridge');
mockInvoke.mockResolvedValueOnce([
{ id: 'hello-world', name: 'Hello World', version: '1.0.0', author: 'Test',
description: 'Test plugin', downloadUrl: 'https://example.com/hw.tar.gz',
checksumSha256: 'abc', permissions: ['palette'], tags: ['example'] },
]);
const result = await proMarketplaceFetchCatalog();
expect(mockInvoke).toHaveBeenCalledWith('plugin:agor-pro|pro_marketplace_fetch_catalog');
expect(result).toHaveLength(1);
expect(result[0].id).toBe('hello-world');
});
it('proMarketplaceInstalled returns installed plugins', async () => {
const { proMarketplaceInstalled } = await import('../../src/lib/commercial/pro-bridge');
mockInvoke.mockResolvedValueOnce([
{ id: 'hello-world', name: 'Hello World', version: '1.0.0', author: 'Test',
description: 'Test', permissions: ['palette'],
installPath: '/home/.config/agor/plugins/hello-world',
hasUpdate: false, latestVersion: null },
]);
const result = await proMarketplaceInstalled();
expect(result[0].installPath).toContain('hello-world');
expect(result[0].hasUpdate).toBe(false);
});
it('proMarketplaceInstall calls with pluginId', async () => {
const { proMarketplaceInstall } = await import('../../src/lib/commercial/pro-bridge');
mockInvoke.mockResolvedValueOnce({
id: 'git-stats', name: 'Git Stats', version: '1.0.0', author: 'Test',
description: 'Git stats', permissions: ['palette'],
installPath: '/home/.config/agor/plugins/git-stats',
hasUpdate: false, latestVersion: null,
});
const result = await proMarketplaceInstall('git-stats');
expect(mockInvoke).toHaveBeenCalledWith('plugin:agor-pro|pro_marketplace_install', { pluginId: 'git-stats' });
expect(result.id).toBe('git-stats');
});
it('proMarketplaceUninstall calls with pluginId', async () => {
const { proMarketplaceUninstall } = await import('../../src/lib/commercial/pro-bridge');
mockInvoke.mockResolvedValueOnce(undefined);
await proMarketplaceUninstall('hello-world');
expect(mockInvoke).toHaveBeenCalledWith('plugin:agor-pro|pro_marketplace_uninstall', { pluginId: 'hello-world' });
});
it('proMarketplaceCheckUpdates returns plugins with update flags', async () => {
const { proMarketplaceCheckUpdates } = await import('../../src/lib/commercial/pro-bridge');
mockInvoke.mockResolvedValueOnce([
{ id: 'hello-world', name: 'Hello World', version: '1.0.0', author: 'Test',
description: 'Test', permissions: ['palette'],
installPath: '/home/.config/agor/plugins/hello-world',
hasUpdate: true, latestVersion: '2.0.0' },
]);
const result = await proMarketplaceCheckUpdates();
expect(result[0].hasUpdate).toBe(true);
expect(result[0].latestVersion).toBe('2.0.0');
});
it('proMarketplaceUpdate calls with pluginId', async () => {
const { proMarketplaceUpdate } = await import('../../src/lib/commercial/pro-bridge');
mockInvoke.mockResolvedValueOnce({
id: 'hello-world', name: 'Hello World', version: '2.0.0', author: 'Test',
description: 'Updated', permissions: ['palette'],
installPath: '/home/.config/agor/plugins/hello-world',
hasUpdate: false, latestVersion: null,
});
const result = await proMarketplaceUpdate('hello-world');
expect(mockInvoke).toHaveBeenCalledWith('plugin:agor-pro|pro_marketplace_update', { pluginId: 'hello-world' });
expect(result.version).toBe('2.0.0');
});
});