@nospt/plugin-dev-ai-hub 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dev/index.tsx +17 -217
- package/dist/api/DevAiHubClient.esm.js +2 -0
- package/dist/api/DevAiHubClient.esm.js.map +1 -1
- package/dist/components/AssetCard/AssetCard.esm.js +60 -134
- package/dist/components/AssetCard/AssetCard.esm.js.map +1 -1
- package/dist/components/AssetCard/AssetCard.module.css.esm.js +8 -0
- package/dist/components/AssetCard/AssetCard.module.css.esm.js.map +1 -0
- package/dist/components/AssetDetailPanel/AssetDetailPanel.esm.js +175 -281
- package/dist/components/AssetDetailPanel/AssetDetailPanel.esm.js.map +1 -1
- package/dist/components/AssetDetailPanel/AssetDetailPanel.module.css.esm.js +8 -0
- package/dist/components/AssetDetailPanel/AssetDetailPanel.module.css.esm.js.map +1 -0
- package/dist/components/AssetFilters/AssetFilters.esm.js +94 -134
- package/dist/components/AssetFilters/AssetFilters.esm.js.map +1 -1
- package/dist/components/AssetFilters/AssetFilters.module.css.esm.js +8 -0
- package/dist/components/AssetFilters/AssetFilters.module.css.esm.js.map +1 -0
- package/dist/components/AssetInstallDialog/AssetInstallDialog.esm.js +70 -126
- package/dist/components/AssetInstallDialog/AssetInstallDialog.esm.js.map +1 -1
- package/dist/components/AssetInstallDialog/AssetInstallDialog.module.css.esm.js +8 -0
- package/dist/components/AssetInstallDialog/AssetInstallDialog.module.css.esm.js.map +1 -0
- package/dist/components/DevAiHubPage/DevAiHubPage.esm.js +64 -189
- package/dist/components/DevAiHubPage/DevAiHubPage.esm.js.map +1 -1
- package/dist/components/DevAiHubPage/DevAiHubPage.module.css.esm.js +8 -0
- package/dist/components/DevAiHubPage/DevAiHubPage.module.css.esm.js.map +1 -0
- package/dist/components/McpConfigPage/McpConfigPage.esm.js +200 -0
- package/dist/components/McpConfigPage/McpConfigPage.esm.js.map +1 -0
- package/dist/components/McpConfigPage/McpConfigPage.module.css.esm.js +8 -0
- package/dist/components/McpConfigPage/McpConfigPage.module.css.esm.js.map +1 -0
- package/dist/components/McpConfigPage/index.esm.js +6 -0
- package/dist/components/McpConfigPage/index.esm.js.map +1 -0
- package/dist/components/ToolIcon/ToolIcon.esm.js +25 -13
- package/dist/components/ToolIcon/ToolIcon.esm.js.map +1 -1
- package/dist/index.d.ts +10 -5
- package/dist/index.esm.js +1 -1
- package/dist/node_modules_dist/style-inject/dist/style-inject.es.esm.js +29 -0
- package/dist/node_modules_dist/style-inject/dist/style-inject.es.esm.js.map +1 -0
- package/dist/plugin.esm.js +22 -4
- package/dist/plugin.esm.js.map +1 -1
- package/dist/routes.esm.js +2 -1
- package/dist/routes.esm.js.map +1 -1
- package/package.json +24 -14
- package/package.json-prepack +24 -14
- package/src/components/AssetCard/AssetCard.module.css +153 -0
- package/src/components/AssetCard/AssetCard.tsx +79 -148
- package/src/components/AssetDetailPanel/AssetDetailPanel.module.css +207 -0
- package/src/components/AssetDetailPanel/AssetDetailPanel.tsx +228 -292
- package/src/components/AssetFilters/AssetFilters.module.css +67 -0
- package/src/components/AssetFilters/AssetFilters.tsx +136 -178
- package/src/components/AssetInstallDialog/AssetInstallDialog.module.css +72 -0
- package/src/components/AssetInstallDialog/AssetInstallDialog.tsx +95 -135
- package/src/components/DevAiHubPage/DevAiHubPage.module.css +105 -0
- package/src/components/DevAiHubPage/DevAiHubPage.tsx +73 -222
- package/src/components/McpConfigDialog/McpConfigDialog.module.css +111 -0
- package/src/components/McpConfigDialog/McpConfigDialog.tsx +178 -208
- package/src/components/McpConfigPage/McpConfigPage.module.css +121 -0
- package/src/components/McpConfigPage/McpConfigPage.tsx +242 -0
- package/src/components/McpConfigPage/index.ts +1 -0
- package/src/components/ToolIcon/ToolIcon.tsx +31 -15
- package/src/css-modules.d.ts +4 -0
- package/src/index.ts +1 -1
- package/src/plugin.tsx +21 -1
- package/src/routes.ts +1 -0
- package/tests/e2e/.auth.json +14 -0
- package/tests/e2e/asset-detail.spec.ts +90 -0
- package/tests/e2e/asset-filtering.spec.ts +145 -0
- package/tests/e2e/asset-installation.spec.ts +74 -0
- package/tests/e2e/dev-ai-hub-page.spec.ts +89 -0
- package/tests/e2e/fixtures/base.ts +61 -0
- package/tests/e2e/fixtures/mock-api.ts +221 -0
- package/tests/e2e/mcp-config.spec.ts +88 -0
- package/dist/components/McpConfigDialog/McpConfigDialog.esm.js +0 -274
- package/dist/components/McpConfigDialog/McpConfigDialog.esm.js.map +0 -1
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { test, expect } from './fixtures/base';
|
|
2
|
+
import { DEV_ASSETS } from './fixtures/mock-api';
|
|
3
|
+
|
|
4
|
+
const PAGE_URL = '/dev-ai-hub';
|
|
5
|
+
|
|
6
|
+
// First mock asset: TypeScript Best Practices (claude-code + github-copilot)
|
|
7
|
+
const ASSET = DEV_ASSETS[0];
|
|
8
|
+
|
|
9
|
+
test.describe('Asset Install Dialog', () => {
|
|
10
|
+
test.beforeEach(async ({ page }) => {
|
|
11
|
+
await page.goto(PAGE_URL);
|
|
12
|
+
await expect(page.getByText(ASSET.name)).toBeVisible();
|
|
13
|
+
await page.getByRole('button', { name: 'Install in editor' }).first().click();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test('sets installId query param in the URL', async ({ page }) => {
|
|
17
|
+
await expect(page).toHaveURL(/installId=/);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('dialog header shows "Install: <asset-name>"', async ({ page }) => {
|
|
21
|
+
// AssetInstallDialog renders "Install: {asset.name}" in DialogHeader
|
|
22
|
+
await expect(page.getByText(`Install: ${ASSET.name}`)).toBeVisible();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('subtitle explains the workflow', async ({ page }) => {
|
|
26
|
+
await expect(
|
|
27
|
+
page.getByText('Copy the content and place the file at the path shown for your tool.'),
|
|
28
|
+
).toBeVisible();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('shows a section for each compatible tool', async ({ page }) => {
|
|
32
|
+
// ASSET.tools = ['claude-code', 'github-copilot'] — scope to dialog to avoid filter button ambiguity
|
|
33
|
+
const dialog = page.getByRole('dialog');
|
|
34
|
+
await expect(dialog.getByText('Claude Code', { exact: true })).toBeVisible();
|
|
35
|
+
await expect(dialog.getByText('GitHub Copilot', { exact: true })).toBeVisible();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('each tool section shows an "Install path" label', async ({ page }) => {
|
|
39
|
+
const labels = page.getByText('Install path');
|
|
40
|
+
await expect(labels.first()).toBeVisible();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('"Copy Content" button is present', async ({ page }) => {
|
|
44
|
+
await expect(page.getByRole('button', { name: 'Copy Content' }).first()).toBeVisible();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('"Download" button is present', async ({ page }) => {
|
|
48
|
+
await expect(page.getByRole('button', { name: 'Download' }).first()).toBeVisible();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('"Copy Content" button label changes to "Copied!" after click', async ({ page }) => {
|
|
52
|
+
await page.getByRole('button', { name: 'Copy Content' }).first().click();
|
|
53
|
+
await expect(page.getByRole('button', { name: 'Copied!' }).first()).toBeVisible();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('"Close" button dismisses the dialog and clears installId', async ({ page }) => {
|
|
57
|
+
// Dialog has an X icon button (aria-label="Close") and a footer "Close" text button — use last()
|
|
58
|
+
await page.getByRole('button', { name: 'Close' }).last().click();
|
|
59
|
+
await expect(page).not.toHaveURL(/installId=/);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// ── Skill asset (tools: all) — verify universal install paths appear ──────
|
|
63
|
+
|
|
64
|
+
test('skill asset (tools: all) shows install path for Claude Code', async ({ page }) => {
|
|
65
|
+
// Close this dialog and open Git Commit (skill, tools: ['all'])
|
|
66
|
+
await page.getByRole('button', { name: 'Close' }).last().click();
|
|
67
|
+
|
|
68
|
+
// Git Commit is the 3rd card (index 2) in the mock data order
|
|
69
|
+
await page.getByRole('button', { name: 'Install in editor' }).nth(2).click();
|
|
70
|
+
|
|
71
|
+
await expect(page.getByText('Install: Git Commit')).toBeVisible();
|
|
72
|
+
await expect(page.getByRole('dialog').getByText('Claude Code', { exact: true })).toBeVisible();
|
|
73
|
+
});
|
|
74
|
+
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { test, expect } from './fixtures/base';
|
|
2
|
+
import { DEV_ASSETS, DEV_STATS } from './fixtures/mock-api';
|
|
3
|
+
|
|
4
|
+
const PAGE_URL = '/dev-ai-hub';
|
|
5
|
+
|
|
6
|
+
test.describe('Dev AI Hub — main page', () => {
|
|
7
|
+
test.beforeEach(async ({ page }) => {
|
|
8
|
+
await page.goto(PAGE_URL);
|
|
9
|
+
// Wait for the asset grid to finish loading
|
|
10
|
+
await expect(page.getByText(DEV_ASSETS[0].name)).toBeVisible();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test('renders the four stats cards', async ({ page }) => {
|
|
14
|
+
for (const label of ['Instructions', 'Agents', 'Skills', 'Workflows']) {
|
|
15
|
+
await expect(page.getByText(label).first()).toBeVisible();
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test('stats cards display correct counts from mock data', async ({ page }) => {
|
|
20
|
+
// The mock has 2 instructions, 2 agents, 1 skill, 1 workflow
|
|
21
|
+
const byType = DEV_STATS.byType;
|
|
22
|
+
await expect(page.getByText(String(byType.instruction)).first()).toBeVisible();
|
|
23
|
+
await expect(page.getByText(String(byType.agent)).first()).toBeVisible();
|
|
24
|
+
await expect(page.getByText(String(byType.skill)).first()).toBeVisible();
|
|
25
|
+
await expect(page.getByText(String(byType.workflow)).first()).toBeVisible();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('clicking a stats card filters assets by type', async ({ page }) => {
|
|
29
|
+
// Stats cards are plain div[role="button"] (not ToggleButton); they come first in DOM order
|
|
30
|
+
await page.getByRole('button', { name: /Skills/i }).first().click();
|
|
31
|
+
await expect(page.getByText('1 asset found')).toBeVisible();
|
|
32
|
+
await expect(page.getByText('Git Commit', { exact: true })).toBeVisible();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('clicking an active stats card removes the type filter', async ({ page }) => {
|
|
36
|
+
await page.getByRole('button', { name: /Skills/i }).first().click();
|
|
37
|
+
await expect(page.getByText('1 asset found')).toBeVisible();
|
|
38
|
+
|
|
39
|
+
// Click it again to deselect
|
|
40
|
+
await page.getByRole('button', { name: /Skills/i }).first().click();
|
|
41
|
+
await expect(page.getByText(`${DEV_STATS.total} assets found`)).toBeVisible();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('renders all mock assets', async ({ page }) => {
|
|
45
|
+
for (const asset of DEV_ASSETS) {
|
|
46
|
+
await expect(page.getByText(asset.name, { exact: true })).toBeVisible();
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('cards display type labels', async ({ page }) => {
|
|
51
|
+
await expect(page.getByText('Instruction').first()).toBeVisible();
|
|
52
|
+
await expect(page.getByText('Agent').first()).toBeVisible();
|
|
53
|
+
await expect(page.getByText('Skill').first()).toBeVisible();
|
|
54
|
+
await expect(page.getByText('Workflow').first()).toBeVisible();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('popular assets (≥5 installs) show 🔥 icon', async ({ page }) => {
|
|
58
|
+
// TypeScript Best Practices (14), Code Review Agent (8), Git Commit (22), Feature Development Workflow (5) are popular
|
|
59
|
+
const fireEmojis = page.getByText('🔥');
|
|
60
|
+
await expect(fireEmojis.first()).toBeVisible();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('assets with 0 installs do not show an install count', async ({ page }) => {
|
|
64
|
+
// Product Manager has installCount: 0 — the count row is entirely omitted
|
|
65
|
+
const card = page.locator('text=Product Manager').locator('..');
|
|
66
|
+
await expect(card).not.toContainText('0');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('"View details" button updates the URL with assetId', async ({ page }) => {
|
|
70
|
+
await page.getByRole('button', { name: 'View details' }).first().click();
|
|
71
|
+
await expect(page).toHaveURL(/assetId=/);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('"Install in editor" button updates the URL with installId', async ({ page }) => {
|
|
75
|
+
await page.getByRole('button', { name: 'Install in editor' }).first().click();
|
|
76
|
+
await expect(page).toHaveURL(/installId=/);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('shows empty state when no assets match the search', async ({ page }) => {
|
|
80
|
+
await page.getByRole('searchbox', { name: 'Search assets' }).fill('zzznoresultszzz');
|
|
81
|
+
await expect(page.getByText('No assets found')).toBeVisible();
|
|
82
|
+
await expect(page.getByText('Try adjusting your filters or search terms.')).toBeVisible();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('pagination is hidden when all assets fit one page (PAGE_SIZE=24)', async ({ page }) => {
|
|
86
|
+
// 6 mock assets vs PAGE_SIZE=24 — TablePagination is not rendered
|
|
87
|
+
await expect(page.getByRole('button', { name: /next/i })).not.toBeVisible();
|
|
88
|
+
});
|
|
89
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pre-seed Backstage's guest-auth localStorage keys so every page load
|
|
3
|
+
* auto-signs in as Guest (legacy token path) without a running auth backend.
|
|
4
|
+
*
|
|
5
|
+
* Keys come from @backstage/core-components SignInPage/providers internals:
|
|
6
|
+
* PROVIDER_STORAGE_KEY → '@backstage/core:SignInPage:provider'
|
|
7
|
+
* enableLegacyGuestToken (guestProvider.tsx)
|
|
8
|
+
*
|
|
9
|
+
* API calls to the backend are intercepted via page.route() and served with
|
|
10
|
+
* the canonical mock data from mock-api.ts, keeping tests predictable and
|
|
11
|
+
* independent of a running backend.
|
|
12
|
+
*/
|
|
13
|
+
import { test as base, expect } from '@playwright/test';
|
|
14
|
+
import {
|
|
15
|
+
MOCK_ASSETS_FULL,
|
|
16
|
+
MOCK_PROVIDER,
|
|
17
|
+
MOCK_STATS,
|
|
18
|
+
buildListResponse,
|
|
19
|
+
} from './mock-api';
|
|
20
|
+
|
|
21
|
+
export const test = base.extend<object>({
|
|
22
|
+
page: async ({ page }, use) => {
|
|
23
|
+
await page.addInitScript(() => {
|
|
24
|
+
localStorage.setItem('@backstage/core:SignInPage:provider', 'guest');
|
|
25
|
+
localStorage.setItem('enableLegacyGuestToken', 'true');
|
|
26
|
+
// Mock clipboard so Copy buttons work without browser permissions
|
|
27
|
+
Object.defineProperty(navigator, 'clipboard', {
|
|
28
|
+
value: { writeText: async () => {} },
|
|
29
|
+
configurable: true,
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
await page.route('**/api/dev-ai-hub/**', async route => {
|
|
34
|
+
const url = new URL(route.request().url());
|
|
35
|
+
const path = url.pathname.replace(/^.*\/api\/dev-ai-hub/, '');
|
|
36
|
+
const method = route.request().method();
|
|
37
|
+
|
|
38
|
+
if (method === 'GET' && path === '/assets') {
|
|
39
|
+
await route.fulfill({ json: buildListResponse(url.searchParams) });
|
|
40
|
+
} else if (method === 'GET' && /^\/assets\/[^/]+$/.test(path)) {
|
|
41
|
+
const id = decodeURIComponent(path.split('/')[2]);
|
|
42
|
+
const asset = MOCK_ASSETS_FULL.find(a => a.id === id);
|
|
43
|
+
if (asset) await route.fulfill({ json: asset });
|
|
44
|
+
else await route.fulfill({ status: 404, json: { error: 'Not found' } });
|
|
45
|
+
} else if (method === 'GET' && path === '/stats') {
|
|
46
|
+
await route.fulfill({ json: MOCK_STATS });
|
|
47
|
+
} else if (method === 'GET' && path === '/providers') {
|
|
48
|
+
await route.fulfill({ json: [MOCK_PROVIDER] });
|
|
49
|
+
} else if (method === 'POST' && path.endsWith('/track-install')) {
|
|
50
|
+
await route.fulfill({ status: 204, body: '' });
|
|
51
|
+
} else {
|
|
52
|
+
await route.continue();
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
57
|
+
await use(page);
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
export { expect };
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical mock data for e2e tests.
|
|
3
|
+
*
|
|
4
|
+
* DEV_ASSETS / DEV_STATS are used directly in test assertions.
|
|
5
|
+
* MOCK_ASSETS_FULL / buildListResponse / MOCK_PROVIDER / MOCK_STATS are used
|
|
6
|
+
* by the base fixture to intercept real API calls with predictable responses.
|
|
7
|
+
*/
|
|
8
|
+
import type {
|
|
9
|
+
AiAsset,
|
|
10
|
+
AiAssetListResponse,
|
|
11
|
+
AiHubProvider,
|
|
12
|
+
AiHubStats,
|
|
13
|
+
} from '@nospt/plugin-dev-ai-hub-common';
|
|
14
|
+
|
|
15
|
+
// ── Summary data used in test assertions ──────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
export const DEV_ASSETS = [
|
|
18
|
+
{ id: 'mock-1', name: 'TypeScript Best Practices', type: 'instruction', installCount: 14, tools: ['claude-code', 'github-copilot'] },
|
|
19
|
+
{ id: 'mock-2', name: 'Code Review Agent', type: 'agent', installCount: 8, tools: ['claude-code'] },
|
|
20
|
+
{ id: 'mock-3', name: 'Git Commit', type: 'skill', installCount: 22, tools: ['all'] },
|
|
21
|
+
{ id: 'mock-4', name: 'Feature Development Workflow', type: 'workflow', installCount: 5, tools: ['github-copilot', 'cursor'] },
|
|
22
|
+
{ id: 'mock-5', name: 'Security Guidelines', type: 'instruction', installCount: 3, tools: ['google-gemini', 'claude-code'] },
|
|
23
|
+
{ id: 'mock-6', name: 'Product Manager', type: 'agent', installCount: 0, tools: ['github-copilot'] },
|
|
24
|
+
] as const;
|
|
25
|
+
|
|
26
|
+
export const DEV_STATS = {
|
|
27
|
+
total: DEV_ASSETS.length,
|
|
28
|
+
byType: { instruction: 2, agent: 2, skill: 1, workflow: 1 },
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const FIRST_ASSET = DEV_ASSETS[0].name;
|
|
32
|
+
|
|
33
|
+
// ── Full asset data for API route interception ────────────────────────────────
|
|
34
|
+
|
|
35
|
+
export const MOCK_ASSETS_FULL: AiAsset[] = [
|
|
36
|
+
{
|
|
37
|
+
id: 'mock-1',
|
|
38
|
+
providerId: 'mock-provider',
|
|
39
|
+
name: 'TypeScript Best Practices',
|
|
40
|
+
description: 'Coding standards and best practices for TypeScript projects.',
|
|
41
|
+
type: 'instruction',
|
|
42
|
+
tools: ['claude-code', 'github-copilot'],
|
|
43
|
+
tags: ['typescript', 'best-practices'],
|
|
44
|
+
author: 'Platform Team',
|
|
45
|
+
version: '1.0.0',
|
|
46
|
+
content: '# TypeScript Best Practices\n\nAlways use strict mode. Prefer `interface` over `type` for object shapes. Use explicit return types on public functions.',
|
|
47
|
+
yamlRaw: 'name: TypeScript Best Practices\ntype: instruction',
|
|
48
|
+
yamlPath: 'instructions/typescript-best-practices.yaml',
|
|
49
|
+
mdPath: 'instructions/typescript-best-practices.md',
|
|
50
|
+
repoUrl: 'https://github.com/example/ai-assets',
|
|
51
|
+
branch: 'main',
|
|
52
|
+
installCount: 14,
|
|
53
|
+
syncedAt: new Date(Date.now() - 5 * 60 * 1000).toISOString(),
|
|
54
|
+
createdAt: '2026-03-10T10:00:00Z',
|
|
55
|
+
updatedAt: '2026-03-10T12:00:00Z',
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
id: 'mock-2',
|
|
59
|
+
providerId: 'mock-provider',
|
|
60
|
+
name: 'Code Review Agent',
|
|
61
|
+
description: 'Specialized agent for performing thorough and constructive code reviews.',
|
|
62
|
+
type: 'agent',
|
|
63
|
+
tools: ['claude-code'],
|
|
64
|
+
tags: ['code-review', 'quality'],
|
|
65
|
+
author: 'Platform Team',
|
|
66
|
+
version: '1.2.0',
|
|
67
|
+
content: '# Code Review Agent\n\nYou are an expert code reviewer. Focus on correctness, performance, security, and maintainability.',
|
|
68
|
+
yamlRaw: 'name: Code Review Agent\ntype: agent',
|
|
69
|
+
yamlPath: 'agents/code-review.yaml',
|
|
70
|
+
mdPath: 'agents/code-review.md',
|
|
71
|
+
repoUrl: 'https://github.com/example/ai-assets',
|
|
72
|
+
branch: 'main',
|
|
73
|
+
installCount: 8,
|
|
74
|
+
syncedAt: new Date(Date.now() - 5 * 60 * 1000).toISOString(),
|
|
75
|
+
createdAt: '2024-02-01T10:00:00Z',
|
|
76
|
+
updatedAt: '2024-03-10T09:00:00Z',
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
id: 'mock-3',
|
|
80
|
+
providerId: 'mock-provider',
|
|
81
|
+
name: 'Git Commit',
|
|
82
|
+
description: 'Skill for creating well-structured, conventional git commit messages.',
|
|
83
|
+
type: 'skill',
|
|
84
|
+
tools: ['all'],
|
|
85
|
+
tags: ['git', 'commits', 'conventional-commits'],
|
|
86
|
+
author: 'Platform Team',
|
|
87
|
+
version: '1.0.0',
|
|
88
|
+
content: '# Git Commit Skill\n\nCreate commit messages following the Conventional Commits specification.',
|
|
89
|
+
yamlRaw: 'name: Git Commit\ntype: skill',
|
|
90
|
+
yamlPath: 'skills/git-commit/skill.yaml',
|
|
91
|
+
mdPath: 'skills/git-commit/SKILL.md',
|
|
92
|
+
repoUrl: 'https://github.com/example/ai-assets',
|
|
93
|
+
branch: 'main',
|
|
94
|
+
installCount: 22,
|
|
95
|
+
syncedAt: new Date(Date.now() - 5 * 60 * 1000).toISOString(),
|
|
96
|
+
createdAt: '2024-01-20T10:00:00Z',
|
|
97
|
+
updatedAt: '2024-02-15T14:00:00Z',
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
id: 'mock-4',
|
|
101
|
+
providerId: 'mock-provider',
|
|
102
|
+
name: 'Feature Development Workflow',
|
|
103
|
+
description: 'Step-by-step workflow for planning, implementing and shipping a new feature.',
|
|
104
|
+
type: 'workflow',
|
|
105
|
+
tools: ['github-copilot', 'cursor'],
|
|
106
|
+
tags: ['workflow', 'feature', 'development'],
|
|
107
|
+
author: 'Platform Team',
|
|
108
|
+
version: '2.0.0',
|
|
109
|
+
content: '# Feature Development Workflow\n\n## Step 1: Planning\n\n## Step 2: Implementation\n\n## Step 3: Testing\n\n## Step 4: Review',
|
|
110
|
+
yamlRaw: 'name: Feature Development Workflow\ntype: workflow',
|
|
111
|
+
yamlPath: 'workflows/feature-development.yaml',
|
|
112
|
+
mdPath: 'workflows/feature-development.md',
|
|
113
|
+
repoUrl: 'https://github.com/example/ai-assets',
|
|
114
|
+
branch: 'main',
|
|
115
|
+
installCount: 5,
|
|
116
|
+
syncedAt: new Date(Date.now() - 5 * 60 * 1000).toISOString(),
|
|
117
|
+
createdAt: '2024-03-01T10:00:00Z',
|
|
118
|
+
updatedAt: '2024-03-20T16:00:00Z',
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
id: 'mock-5',
|
|
122
|
+
providerId: 'mock-provider',
|
|
123
|
+
name: 'Security Guidelines',
|
|
124
|
+
description: 'Security best practices and guidelines for backend development.',
|
|
125
|
+
type: 'instruction',
|
|
126
|
+
tools: ['google-gemini', 'claude-code'],
|
|
127
|
+
tags: ['security', 'backend', 'best-practices'],
|
|
128
|
+
author: 'Security Team',
|
|
129
|
+
version: '1.1.0',
|
|
130
|
+
content: '# Security Guidelines\n\nNever store secrets in code. Always validate user input. Use parameterized queries.',
|
|
131
|
+
yamlRaw: 'name: Security Guidelines\ntype: instruction',
|
|
132
|
+
yamlPath: 'instructions/security-guidelines.yaml',
|
|
133
|
+
mdPath: 'instructions/security-guidelines.md',
|
|
134
|
+
repoUrl: 'https://github.com/example/ai-assets',
|
|
135
|
+
branch: 'main',
|
|
136
|
+
installCount: 3,
|
|
137
|
+
syncedAt: new Date(Date.now() - 5 * 60 * 1000).toISOString(),
|
|
138
|
+
createdAt: '2024-02-10T10:00:00Z',
|
|
139
|
+
updatedAt: '2024-03-05T11:00:00Z',
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
id: 'mock-6',
|
|
143
|
+
providerId: 'mock-provider',
|
|
144
|
+
name: 'Product Manager',
|
|
145
|
+
description: 'Data-driven product decision assistant for roadmap and prioritization.',
|
|
146
|
+
type: 'agent',
|
|
147
|
+
tools: ['github-copilot'],
|
|
148
|
+
tags: ['product', 'roadmap', 'prioritization'],
|
|
149
|
+
author: 'Product Team',
|
|
150
|
+
version: '1.0.0',
|
|
151
|
+
content: '# Product Manager Agent\n\nYou are a data-driven product manager. Help prioritize features using impact/effort frameworks.',
|
|
152
|
+
yamlRaw: 'name: Product Manager\ntype: agent',
|
|
153
|
+
yamlPath: 'agents/product-manager.yaml',
|
|
154
|
+
mdPath: 'agents/product-manager.md',
|
|
155
|
+
repoUrl: 'https://github.com/example/ai-assets',
|
|
156
|
+
branch: 'main',
|
|
157
|
+
installCount: 0,
|
|
158
|
+
syncedAt: new Date(Date.now() - 5 * 60 * 1000).toISOString(),
|
|
159
|
+
createdAt: '2024-03-15T10:00:00Z',
|
|
160
|
+
updatedAt: '2024-03-15T10:00:00Z',
|
|
161
|
+
},
|
|
162
|
+
];
|
|
163
|
+
|
|
164
|
+
export const MOCK_PROVIDER: AiHubProvider = {
|
|
165
|
+
id: 'mock-provider',
|
|
166
|
+
type: 'github',
|
|
167
|
+
target: 'https://github.com/example/ai-assets.git',
|
|
168
|
+
branch: 'main',
|
|
169
|
+
lastSync: new Date(Date.now() - 5 * 60 * 1000).toISOString(),
|
|
170
|
+
status: 'idle',
|
|
171
|
+
assetCount: MOCK_ASSETS_FULL.length,
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
export const MOCK_STATS: AiHubStats = {
|
|
175
|
+
totalAssets: MOCK_ASSETS_FULL.length,
|
|
176
|
+
byType: { instruction: 2, agent: 2, skill: 1, workflow: 1 },
|
|
177
|
+
byTool: {
|
|
178
|
+
'claude-code': MOCK_ASSETS_FULL.filter(a => a.tools.includes('claude-code') || a.tools.includes('all')).length,
|
|
179
|
+
'github-copilot': MOCK_ASSETS_FULL.filter(a => a.tools.includes('github-copilot') || a.tools.includes('all')).length,
|
|
180
|
+
'google-gemini': MOCK_ASSETS_FULL.filter(a => a.tools.includes('google-gemini') || a.tools.includes('all')).length,
|
|
181
|
+
'cursor': MOCK_ASSETS_FULL.filter(a => a.tools.includes('cursor') || a.tools.includes('all')).length,
|
|
182
|
+
},
|
|
183
|
+
byProvider: { 'mock-provider': MOCK_ASSETS_FULL.length },
|
|
184
|
+
lastSync: MOCK_PROVIDER.lastSync,
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
/** Applies the same filter logic the backend uses, for use in page.route() handlers. */
|
|
188
|
+
export function buildListResponse(params: URLSearchParams): AiAssetListResponse {
|
|
189
|
+
let items = [...MOCK_ASSETS_FULL];
|
|
190
|
+
|
|
191
|
+
const type = params.get('type');
|
|
192
|
+
const tool = params.get('tool');
|
|
193
|
+
const search = params.get('search');
|
|
194
|
+
const tags = params.get('tags')?.split(',').filter(Boolean);
|
|
195
|
+
const provider = params.get('provider');
|
|
196
|
+
const page = Number(params.get('page') ?? '1');
|
|
197
|
+
const pageSize = Number(params.get('pageSize') ?? '24');
|
|
198
|
+
|
|
199
|
+
if (type) items = items.filter(a => a.type === type);
|
|
200
|
+
if (tool) items = items.filter(a => a.tools.includes(tool as any) || a.tools.includes('all'));
|
|
201
|
+
if (search) {
|
|
202
|
+
const q = search.toLowerCase();
|
|
203
|
+
items = items.filter(a =>
|
|
204
|
+
a.name.toLowerCase().includes(q) ||
|
|
205
|
+
a.description.toLowerCase().includes(q) ||
|
|
206
|
+
a.tags.some(t => t.toLowerCase().includes(q)),
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
if (tags?.length) items = items.filter(a => tags.every(t => a.tags.includes(t)));
|
|
210
|
+
if (provider) items = items.filter(a => a.providerId === provider);
|
|
211
|
+
|
|
212
|
+
const totalCount = items.length;
|
|
213
|
+
const start = (page - 1) * pageSize;
|
|
214
|
+
|
|
215
|
+
return {
|
|
216
|
+
items: items.slice(start, start + pageSize) as any,
|
|
217
|
+
totalCount,
|
|
218
|
+
page,
|
|
219
|
+
pageSize,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { test, expect } from './fixtures/base';
|
|
2
|
+
|
|
3
|
+
// McpConfigPage is registered at /mcp-config in dev/index.tsx
|
|
4
|
+
const PAGE_URL = '/mcp-config';
|
|
5
|
+
|
|
6
|
+
test.describe('MCP Configuration Page', () => {
|
|
7
|
+
test.beforeEach(async ({ page }) => {
|
|
8
|
+
await page.goto(PAGE_URL);
|
|
9
|
+
await expect(page.getByText('Configure MCP Server')).toBeVisible();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test('renders the page heading', async ({ page }) => {
|
|
13
|
+
await expect(page.getByText('Configure MCP Server')).toBeVisible();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test('renders the subtitle', async ({ page }) => {
|
|
17
|
+
await expect(
|
|
18
|
+
page.getByText('Connect your AI tool to the Dev AI Hub via Model Context Protocol.'),
|
|
19
|
+
).toBeVisible();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test('shows tabs for all three supported tools', async ({ page }) => {
|
|
23
|
+
// TOOL_CONFIGS defines Claude Code, GitHub Copilot, Google Gemini
|
|
24
|
+
await expect(page.getByRole('tab', { name: /Claude Code/i })).toBeVisible();
|
|
25
|
+
await expect(page.getByRole('tab', { name: /GitHub Copilot/i })).toBeVisible();
|
|
26
|
+
await expect(page.getByRole('tab', { name: /Google Gemini/i })).toBeVisible();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('"Claude Code" tab is selected by default', async ({ page }) => {
|
|
30
|
+
await expect(page.getByRole('tab', { name: /Claude Code/i })).toHaveAttribute('aria-selected', 'true');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('Claude Code panel shows .mcp.json config snippet', async ({ page }) => {
|
|
34
|
+
await expect(page.getByText('.mcp.json', { exact: true })).toBeVisible();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('switching to GitHub Copilot tab shows .vscode/settings.json snippet', async ({ page }) => {
|
|
38
|
+
await page.getByRole('tab', { name: /GitHub Copilot/i }).click();
|
|
39
|
+
await expect(page.getByText('.vscode/settings.json', { exact: true })).toBeVisible();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('switching to Google Gemini tab shows gemini-config.json snippet', async ({ page }) => {
|
|
43
|
+
await page.getByRole('tab', { name: /Google Gemini/i }).click();
|
|
44
|
+
await expect(page.getByText('gemini-config.json')).toBeVisible();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('shows the "MCP Endpoint" section', async ({ page }) => {
|
|
48
|
+
await expect(page.getByText('MCP Endpoint')).toBeVisible();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('"Copy URL" button is present', async ({ page }) => {
|
|
52
|
+
await expect(page.getByRole('button', { name: 'Copy URL' })).toBeVisible();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('"Copy config" button is present', async ({ page }) => {
|
|
56
|
+
await expect(page.getByRole('button', { name: 'Copy config' })).toBeVisible();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('proactive suggestions switch is unchecked by default', async ({ page }) => {
|
|
60
|
+
const toggle = page.getByRole('switch', { name: 'Proactive suggestions' });
|
|
61
|
+
await expect(toggle).toBeVisible();
|
|
62
|
+
await expect(toggle).not.toBeChecked();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('enabling proactive toggle appends ?proactive=true to the MCP URL', async ({ page }) => {
|
|
66
|
+
await page.getByRole('switch', { name: 'Proactive suggestions' }).click({ force: true });
|
|
67
|
+
await expect(page.getByText(/proactive=true/).first()).toBeVisible();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('switching tabs updates the tool param in the MCP URL', async ({ page }) => {
|
|
71
|
+
const urlBefore = await page.getByText(/\/mcp\?/).first().textContent();
|
|
72
|
+
expect(urlBefore).toContain('tool=claude-code');
|
|
73
|
+
|
|
74
|
+
await page.getByRole('tab', { name: /GitHub Copilot/i }).click();
|
|
75
|
+
const urlAfter = await page.getByText(/\/mcp\?/).first().textContent();
|
|
76
|
+
expect(urlAfter).toContain('tool=github-copilot');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('hint text references ?tool= and suggest_assets', async ({ page }) => {
|
|
80
|
+
await expect(page.getByText(/Omit \?tool=/)).toBeVisible();
|
|
81
|
+
await expect(page.getByText(/suggest_assets/)).toBeVisible();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('"Copy URL" button label changes to "Copied!" after click', async ({ page }) => {
|
|
85
|
+
await page.getByRole('button', { name: 'Copy URL' }).click();
|
|
86
|
+
await expect(page.getByRole('button', { name: 'Copied!' })).toBeVisible();
|
|
87
|
+
});
|
|
88
|
+
});
|