fix(ci): repair validate and windows checks
This commit is contained in:
parent
f92b77e3af
commit
a5c79518fb
4 changed files with 50 additions and 47 deletions
|
|
@ -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';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -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' },
|
||||||
|
|
|
||||||
|
|
@ -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('\\', '/');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue