fix(ci): repair validate and windows checks

This commit is contained in:
777genius 2026-04-17 09:12:49 +03:00
parent f92b77e3af
commit a5c79518fb
4 changed files with 50 additions and 47 deletions

View file

@ -125,13 +125,7 @@ export function hasInstallationInScope(
return installations.some((installation) => installation.scope === scope); return installations.some((installation) => installation.scope === scope);
} }
/** function summarizeInstallationScopes(scopes: InstallScope[]): string | null {
* Build a concise install-status label for plugin badges.
*/
export function getInstallationSummaryLabel(
installations: Pick<InstalledPluginEntry, 'scope'>[]
): string | null {
const scopes = Array.from(new Set(installations.map((installation) => installation.scope)));
if (scopes.length === 0) { if (scopes.length === 0) {
return null; return null;
} }
@ -152,6 +146,16 @@ export function getInstallationSummaryLabel(
} }
} }
/**
* Build a concise install-status label for plugin badges.
*/
export function getInstallationSummaryLabel(
installations: Pick<InstalledPluginEntry, 'scope'>[]
): string | null {
const scopes = Array.from(new Set(installations.map((installation) => installation.scope)));
return summarizeInstallationScopes(scopes);
}
const MCP_SCOPE_PRIORITY: Record<InstalledMcpEntry['scope'], number> = { const MCP_SCOPE_PRIORITY: Record<InstalledMcpEntry['scope'], number> = {
local: 0, local: 0,
project: 1, project: 1,
@ -181,24 +185,7 @@ export function getMcpInstallationSummaryLabel(
installations: Pick<InstalledMcpEntry, 'scope'>[] installations: Pick<InstalledMcpEntry, 'scope'>[]
): string | null { ): string | null {
const scopes = Array.from(new Set(installations.map((installation) => installation.scope))); const scopes = Array.from(new Set(installations.map((installation) => installation.scope)));
if (scopes.length === 0) { return summarizeInstallationScopes(scopes);
return null;
}
if (scopes.length > 1) {
return `Installed in ${scopes.length} scopes`;
}
switch (scopes[0]) {
case 'user':
return 'Installed globally';
case 'project':
return 'Installed in project';
case 'local':
return 'Installed locally';
default:
return 'Installed';
}
} }
/** /**

View file

@ -1,10 +1,20 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import * as fs from 'node:fs/promises'; import * as fs from 'node:fs/promises';
import * as path from 'node:path';
import { McpInstallationStateService } from '@main/services/extensions/state/McpInstallationStateService'; import { McpInstallationStateService } from '@main/services/extensions/state/McpInstallationStateService';
const TEST_ROOT = path.parse(process.cwd()).root || path.sep;
const MOCK_HOME_PATH = path.join(TEST_ROOT, 'tmp', 'mock-home');
const PROJECT_A_PATH = path.join(TEST_ROOT, 'tmp', 'project-a');
const PROJECT_B_PATH = path.join(TEST_ROOT, 'tmp', 'project-b');
function normalizeMockPath(filePath: unknown): string {
return String(filePath).replaceAll('\\', '/');
}
vi.mock('@main/utils/pathDecoder', () => ({ vi.mock('@main/utils/pathDecoder', () => ({
getHomeDir: () => '/tmp/mock-home', getHomeDir: () => MOCK_HOME_PATH,
})); }));
vi.mock('node:fs/promises'); vi.mock('node:fs/promises');
@ -25,14 +35,14 @@ describe('McpInstallationStateService', () => {
describe('getInstalled', () => { describe('getInstalled', () => {
it('includes local scope from the current project entry in ~/.claude.json', async () => { it('includes local scope from the current project entry in ~/.claude.json', async () => {
mockedFs.readFile.mockImplementation(async (filePath) => { mockedFs.readFile.mockImplementation(async (filePath) => {
const normalizedPath = String(filePath); const normalizedPath = normalizeMockPath(filePath);
if (normalizedPath === '/tmp/mock-home/.claude.json') { if (normalizedPath === normalizeMockPath(path.join(MOCK_HOME_PATH, '.claude.json'))) {
return JSON.stringify({ return JSON.stringify({
mcpServers: { mcpServers: {
context7: { command: 'npx -y @upstash/context7-mcp' }, context7: { command: 'npx -y @upstash/context7-mcp' },
}, },
projects: { projects: {
'/tmp/project-a': { [PROJECT_A_PATH]: {
mcpServers: { mcpServers: {
stripe: { url: 'https://mcp.stripe.com' }, stripe: { url: 'https://mcp.stripe.com' },
}, },
@ -41,7 +51,7 @@ describe('McpInstallationStateService', () => {
}); });
} }
if (normalizedPath === '/tmp/project-a/.mcp.json') { if (normalizedPath === normalizeMockPath(path.join(PROJECT_A_PATH, '.mcp.json'))) {
return JSON.stringify({ return JSON.stringify({
mcpServers: { mcpServers: {
paypal: { url: 'https://mcp.paypal.com/mcp' }, paypal: { url: 'https://mcp.paypal.com/mcp' },
@ -52,7 +62,7 @@ describe('McpInstallationStateService', () => {
throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' }); throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' });
}); });
const entries = await service.getInstalled('/tmp/project-a'); const entries = await service.getInstalled(PROJECT_A_PATH);
expect(entries).toEqual([ expect(entries).toEqual([
{ name: 'context7', scope: 'user', transport: 'stdio' }, { name: 'context7', scope: 'user', transport: 'stdio' },
@ -63,8 +73,8 @@ describe('McpInstallationStateService', () => {
it('caches results within TTL for the same project path', async () => { it('caches results within TTL for the same project path', async () => {
mockedFs.readFile.mockImplementation(async (filePath) => { mockedFs.readFile.mockImplementation(async (filePath) => {
const normalizedPath = String(filePath); const normalizedPath = normalizeMockPath(filePath);
if (normalizedPath === '/tmp/mock-home/.claude.json') { if (normalizedPath === normalizeMockPath(path.join(MOCK_HOME_PATH, '.claude.json'))) {
return JSON.stringify({ return JSON.stringify({
mcpServers: { mcpServers: {
context7: { command: 'npx -y @upstash/context7-mcp' }, context7: { command: 'npx -y @upstash/context7-mcp' },
@ -72,7 +82,7 @@ describe('McpInstallationStateService', () => {
}); });
} }
if (normalizedPath === '/tmp/project-a/.mcp.json') { if (normalizedPath === normalizeMockPath(path.join(PROJECT_A_PATH, '.mcp.json'))) {
return JSON.stringify({ return JSON.stringify({
mcpServers: { mcpServers: {
'repo-a-server': { url: 'https://repo-a.example.com/mcp' }, 'repo-a-server': { url: 'https://repo-a.example.com/mcp' },
@ -83,27 +93,27 @@ describe('McpInstallationStateService', () => {
throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' }); throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' });
}); });
await service.getInstalled('/tmp/project-a'); await service.getInstalled(PROJECT_A_PATH);
await service.getInstalled('/tmp/project-a'); await service.getInstalled(PROJECT_A_PATH);
expect(mockedFs.readFile).toHaveBeenCalledTimes(2); expect(mockedFs.readFile).toHaveBeenCalledTimes(2);
}); });
it('caches results independently per project path', async () => { it('caches results independently per project path', async () => {
mockedFs.readFile.mockImplementation(async (filePath) => { mockedFs.readFile.mockImplementation(async (filePath) => {
const normalizedPath = String(filePath); const normalizedPath = normalizeMockPath(filePath);
if (normalizedPath === '/tmp/mock-home/.claude.json') { if (normalizedPath === normalizeMockPath(path.join(MOCK_HOME_PATH, '.claude.json'))) {
return JSON.stringify({ return JSON.stringify({
mcpServers: { mcpServers: {
context7: { command: 'npx -y @upstash/context7-mcp' }, context7: { command: 'npx -y @upstash/context7-mcp' },
}, },
projects: { projects: {
'/tmp/project-a': { [PROJECT_A_PATH]: {
mcpServers: { mcpServers: {
stripe: { url: 'https://mcp.stripe.com' }, stripe: { url: 'https://mcp.stripe.com' },
}, },
}, },
'/tmp/project-b': { [PROJECT_B_PATH]: {
mcpServers: { mcpServers: {
github: { command: 'uvx github-mcp' }, github: { command: 'uvx github-mcp' },
}, },
@ -112,7 +122,7 @@ describe('McpInstallationStateService', () => {
}); });
} }
if (normalizedPath === '/tmp/project-a/.mcp.json') { if (normalizedPath === normalizeMockPath(path.join(PROJECT_A_PATH, '.mcp.json'))) {
return JSON.stringify({ return JSON.stringify({
mcpServers: { mcpServers: {
'repo-a-server': { url: 'https://repo-a.example.com/mcp' }, 'repo-a-server': { url: 'https://repo-a.example.com/mcp' },
@ -120,7 +130,7 @@ describe('McpInstallationStateService', () => {
}); });
} }
if (normalizedPath === '/tmp/project-b/.mcp.json') { if (normalizedPath === normalizeMockPath(path.join(PROJECT_B_PATH, '.mcp.json'))) {
return JSON.stringify({ return JSON.stringify({
mcpServers: { mcpServers: {
'repo-b-server': { command: 'uvx repo-b-mcp' }, 'repo-b-server': { command: 'uvx repo-b-mcp' },
@ -131,8 +141,8 @@ describe('McpInstallationStateService', () => {
throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' }); throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' });
}); });
const projectAEntries = await service.getInstalled('/tmp/project-a'); const projectAEntries = await service.getInstalled(PROJECT_A_PATH);
const projectBEntries = await service.getInstalled('/tmp/project-b'); const projectBEntries = await service.getInstalled(PROJECT_B_PATH);
expect(projectAEntries).toEqual([ expect(projectAEntries).toEqual([
{ name: 'context7', scope: 'user', transport: 'stdio' }, { name: 'context7', scope: 'user', transport: 'stdio' },

View file

@ -9,7 +9,7 @@ const MOCK_CLAUDE_BASE_PATH = path.join(TEST_ROOT, 'tmp', 'mock-claude');
const PROJECT_A_PATH = path.join(TEST_ROOT, 'tmp', 'project-a'); const PROJECT_A_PATH = path.join(TEST_ROOT, 'tmp', 'project-a');
const PROJECT_B_PATH = path.join(TEST_ROOT, 'tmp', 'project-b'); const PROJECT_B_PATH = path.join(TEST_ROOT, 'tmp', 'project-b');
function normalizeMockPath(filePath: string | URL): string { function normalizeMockPath(filePath: unknown): string {
return String(filePath).replaceAll('\\', '/'); return String(filePath).replaceAll('\\', '/');
} }

View file

@ -257,9 +257,15 @@ describe('PluginDetailDialog project context', () => {
}); });
const scopeSelect = host.querySelector('[data-testid="scope-select"]') as HTMLSelectElement; const scopeSelect = host.querySelector('[data-testid="scope-select"]') as HTMLSelectElement;
const projectOption = scopeSelect.querySelector(
'option[value="project"]'
) as HTMLOptionElement | null;
const localOption = scopeSelect.querySelector(
'option[value="local"]'
) as HTMLOptionElement | null;
expect(scopeSelect).not.toBeNull(); expect(scopeSelect).not.toBeNull();
expect(scopeSelect.querySelector('option[value="project"]')?.disabled).toBe(true); expect(projectOption?.disabled).toBe(true);
expect(scopeSelect.querySelector('option[value="local"]')?.disabled).toBe(true); expect(localOption?.disabled).toBe(true);
await act(async () => { await act(async () => {
root.unmount(); root.unmount();