refactor: Introduce McpPrimitiveType and McpListAction enums for better type safety

- Added McpPrimitiveType enum to categorize sidebar row types.
- Introduced McpListAction enum to standardize action names in capability menus.
- Updated references in tests and flows to use new enums instead of string literals for improved maintainability and clarity.
- Refactored related tests to ensure compatibility with the new enum structure.
This commit is contained in:
Bob Qiu committed 2026-10-19 15:50:18 +08:00
1 parent 460dc8da7a
commit e7e37bddcb
20 files changed
+110 -78

No files matched your search

@@ -17,7 +17,7 @@ Every test injects only the `user` fixture (`async ({ user }) => {...}`) — nev
| WebSocket request | `webSocketRequestFlow` | `flows/web-socket-request.flow.ts` | `pages/web-socket-request.page.ts` | `models/websocket-request.ts` |
| Socket.IO request | `socketIoRequestFlow` | `flows/socket-io-request.flow.ts` | `pages/socket-io-request.page.ts` | `models/socket-io-request.ts` |
| Event Stream (SSE) request | `eventStreamRequestFlow` | `flows/event-stream-request.flow.ts` | `pages/event-stream-request.page.ts` | `models/event-stream-request.ts` |
| MCP client | `mcpClientFlow` | `flows/mcp-client.flow.ts` | `pages/mcp-client.page.ts` | `models/mcp-client.ts` |
| MCP client | `mcpClientFlow` | `flows/mcp-client.flow.ts` | `pages/mcp-client.page.ts` | `models/mcp-client.ts`; `enums/mcp-capability.ts` (`McpPrimitiveType`, `McpListAction`) |
| Mock Server (self-hosted only, `tests/mock-server/`) | `mockServerFlow` | `flows/mock-server.flow.ts` | `pages/mock-server.page.ts` | `models/mock-server.ts` (`MockServer`, positional ctor), `models/mock-route.ts` (`MockRoute` literal, `MockRouteHistoryEntry`); `enums/mock-body-type.ts` (`MockBodyType`) |
| Environment | `environmentFlow` | `flows/environment.flow.ts` | `pages/environment.page.ts` | `models/environment.ts` |
| Cookie | `cookieFlow` | `flows/cookie.flow.ts` | `pages/cookie.page.ts` | `models/cookie.ts` |
@@ -557,11 +557,11 @@ getListError(client: McpClient, type: McpCapabilityType): Promise<McpCapabilityE
toggleCapability(client: McpClient, type: McpCapabilityType): Promise<void> // collapse/expand the capability item
refresh(client: McpClient, type: McpCapabilityType): Promise<void> // actions menu -> Refresh (re-runs the list request, re-derives entries + error)
loadMore(client: McpClient, type: McpCapabilityType): Promise<void> // actions menu -> Load More (only offered while the last page carried a nextCursor)
getListAction(client: McpClient, type: McpCapabilityType): Promise<string> // "Refresh" or "Load More" — what the actions menu currently offers
getListAction(client: McpClient, type: McpCapabilityType): Promise<McpListAction> // McpListAction.Refresh or .LoadMore — what the actions menu currently offers
filterCapabilities(client: McpClient, text: string): Promise<void> // opens the client, types into the sidebar capability filter (narrows primitives by name) — wraps McpClientPage.setCapabilityFilter
```
`McpClientPage` (`pageManager.mcpClientPage`) additionally exposes plain-value readers for the same sidebar, none of which have a Flow wrapper: `getPrimitives(type: McpPrimitiveType): Promise<string[]>` (row names of one kind; `McpPrimitiveType` adds `"resourceTemplates"`; **does not wait**, so an empty list is a valid answer — poll it with `expect.poll`, unlike `getTools()` which waits for >0), `getCapabilityErrorCount(type): Promise<number>` (0 or 1 — use `expect.poll(...).toBe(0|1)` for "panel hidden/shown"), `hasCapability(type): Promise<boolean>`, `hasCaret(type): Promise<boolean>` (no caret on an empty capability item), `getCapabilityBounds(type)` / `getCapabilityErrorBounds(type)` (`{ y, height }`, for "panel does not overlap the next row"), `isCapabilityErrorScrollable(type): Promise<boolean>`.
`McpClientPage` (`pageManager.mcpClientPage`) additionally exposes plain-value readers for the same sidebar, none of which have a Flow wrapper: `getPrimitives(type: McpPrimitiveType): Promise<string[]>` (row names of one kind; `McpPrimitiveType` (`enums/mcp-capability.ts`) adds `ResourceTemplates`; every `type` argument here and in the Flow methods above takes the enum (`McpPrimitiveType.Tools`), never a `"tools"` string literal, and list-action assertions use `McpListAction.LoadMore`/`.Refresh`; **does not wait**, so an empty list is a valid answer — poll it with `expect.poll`, unlike `getTools()` which waits for >0), `getCapabilityErrorCount(type): Promise<number>` (0 or 1 — use `expect.poll(...).toBe(0|1)` for "panel hidden/shown"), `hasCapability(type): Promise<boolean>`, `hasCaret(type): Promise<boolean>` (no caret on an empty capability item), `getCapabilityBounds(type)` / `getCapabilityErrorBounds(type)` (`{ y, height }`, for "panel does not overlap the next row"), `isCapabilityErrorScrollable(type): Promise<boolean>`.
**Scripting what the MCP server answers (`misc/mcp-list-server.ts`).** `misc/mcp-server.ts`'s regular `/mcp` endpoint only serves valid tools. For malformed / partial / error / paginated list responses, every test gets its own endpoint: `const url = await scriptMcpListServer(key, config)` (key unique per test, e.g. `faker.string.alphanumeric(12)`; returns the URL to give `new McpClient(name, url)`). `config` maps a list method (`"tools/list" | "resources/list" | "resources/templates/list" | "prompts/list"`) to `{ result: <any JSON, sent verbatim> }`, `{ error: { code, message } }` (a JSON-RPC error), or `{ pages: { "": <first page>, "<cursor>": <page for params.cursor> } }`. Methods left out answer with a valid empty list. `config` also takes the flag `serverRequestReusingToolsListId: true`, which makes the `tools/list` reply an SSE stream: the result, then a server-initiated `roots/list` request reusing the `tools/list` request's JSON-RPC id (for testing id-collision handling). The `McpListMethod` type names the four list methods. Requests without an `id` **or without a `method`** get a bare 202. Call `scriptMcpListServer` again with the **same key** to switch behavior mid-test (e.g. between two Refresh clicks). Real examples: `tests/mcp-client/error-response/` (one spec per INS-3647 test-plan case; `helpers.ts` there holds `connectTo(user, config)` = script + create client + connect, plus `validTool` / `wholeFailureTitle` / `skippedTitle`).
@@ -1579,6 +1579,8 @@ ContextMenuItem: Collection |
CollaboratorRole: Owner | Admin | Member; // enums/collaborator-role.ts — InvitePage's per-row role-menu selector, OrganizationFlow.invite()'s Collaborator.role field
DeleteMode: Local | Full; // enums/delete-mode.ts
SyncStatus: Synced | Unsynced; // enums/sync-status.ts
McpPrimitiveType: Tools | Resources | Prompts | ResourceTemplates; // enums/mcp-capability.ts — values "tools"/"resources"/"prompts"/"resourceTemplates"; `McpCapabilityType` (same file, a type alias) = every member except ResourceTemplates
McpListAction: Refresh | LoadMore; // enums/mcp-capability.ts — "Refresh" / "Load More", the list action a capability item's menu offers
ExportFormat: InsomniaV5 | Har; // enums/export-format.ts
ApiSpecExportFormat: Yaml | Json; // enums/spec-format.ts or export-format.ts — see ExportPage
ImportSource: File | Url | Curl | Clipboard | Mcp;
@@ -0,0 +1,16 @@
/** The primitive kinds a sidebar row can belong to (the prefix of its `data-key`). */
export enum McpPrimitiveType {
Tools = "tools",
Resources = "resources",
Prompts = "prompts",
ResourceTemplates = "resourceTemplates",
}
/** The capability items of the MCP sidebar; Resource Templates are listed under `resources`. */
export type McpCapabilityType = Exclude<McpPrimitiveType, McpPrimitiveType.ResourceTemplates>;
/** The list action a capability item's menu offers. */
export enum McpListAction {
Refresh = "Refresh",
LoadMore = "Load More",
}
@@ -1,11 +1,12 @@
import { expect } from "@playwright/test";
import { type McpCapabilityType, McpListAction } from "../enums/mcp-capability";
import { DEFAULT_TIMEOUT } from "../misc/constants";
import { McpClient } from "../models/mcp-client";
import type { Project } from "../models/project";
import type { ResponseView } from "../models/response";
import type { OAuth2Tokens } from "../pages/auth-tab.page";
import type { McpCapabilityError, McpCapabilityType } from "../pages/mcp-client.page";
import type { McpCapabilityError } from "../pages/mcp-client.page";
import type { PageManager } from "../pages/page-manager";
import { BaseFlow } from "./base.flow";
import type { FlowContext } from "./flow-context";
@@ -223,7 +224,7 @@ export class McpClientFlow extends BaseFlow {
*/
async refresh(client: McpClient, type: McpCapabilityType): Promise<void> {
await this.open(client);
await this.pageManager.mcpClientPage.clickCapabilityAction(type, "Refresh");
await this.pageManager.mcpClientPage.clickCapabilityAction(type, McpListAction.Refresh);
}
/**
@@ -235,7 +236,7 @@ export class McpClientFlow extends BaseFlow {
*/
async loadMore(client: McpClient, type: McpCapabilityType): Promise<void> {
await this.open(client);
await this.pageManager.mcpClientPage.clickCapabilityAction(type, "Load More");
await this.pageManager.mcpClientPage.clickCapabilityAction(type, McpListAction.LoadMore);
}
/**
@@ -245,7 +246,7 @@ export class McpClientFlow extends BaseFlow {
* @param type - Which capability item's actions menu to read
* @returns `Refresh`, or `Load More` while a `nextCursor` is pending
*/
async getListAction(client: McpClient, type: McpCapabilityType): Promise<string> {
async getListAction(client: McpClient, type: McpCapabilityType): Promise<McpListAction> {
await this.open(client);
return this.pageManager.mcpClientPage.getCapabilityActionName(type);
}
@@ -1,13 +1,10 @@
import { expect, type Locator } from '@playwright/test';
import type { McpListAction} from '../enums/mcp-capability';
import { type McpCapabilityType, type McpPrimitiveType } from '../enums/mcp-capability';
import { ACTION_TIMEOUT, DEFAULT_TIMEOUT } from '../misc/constants';
import { RequestPage } from './request.page';
/** The capability items of the MCP sidebar; Resource Templates are listed under `resources`. */
export type McpCapabilityType = 'tools' | 'resources' | 'prompts';
/** The primitive kinds a sidebar row can belong to (the prefix of its `data-key`). */
export type McpPrimitiveType = McpCapabilityType | 'resourceTemplates';
/** The error panel embedded in a capability item's row. */
export interface McpCapabilityError {
title: string;
@@ -245,7 +242,7 @@ export class McpClientPage extends RequestPage {
* @param type - Which capability item's menu to open
* @param action - `Refresh`, or `Load More` once the server returned a `nextCursor`
*/
async clickCapabilityAction(type: McpCapabilityType, action: 'Refresh' | 'Load More'): Promise<void> {
async clickCapabilityAction(type: McpCapabilityType, action: McpListAction): Promise<void> {
const capabilityItem = this.getCapabilityItem(type);
await capabilityItem.hover({ position: { x: 60, y: 10 } });
await capabilityItem.locator(`[data-testid="Dropdown-${type}"]`).click();
@@ -258,12 +255,12 @@ export class McpClientPage extends RequestPage {
* Opens the menu to read it and closes it again.
* @param type - Which capability item's menu to read
*/
async getCapabilityActionName(type: McpCapabilityType): Promise<string> {
async getCapabilityActionName(type: McpCapabilityType): Promise<McpListAction> {
const capabilityItem = this.getCapabilityItem(type);
await capabilityItem.hover({ position: { x: 60, y: 10 } });
await capabilityItem.locator(`[data-testid="Dropdown-${type}"]`).click();
const item = this.page.locator(this.ACTIONS_MENU).locator('[role^="menuitem"]').first();
const name = (await item.innerText()).trim();
const name = (await item.innerText()).trim() as McpListAction;
await this.page.keyboard.press('Escape');
await expect(item).toBeHidden();
return name;
@@ -1,5 +1,6 @@
import { faker } from '@faker-js/faker';
import { McpPrimitiveType } from '../../../enums/mcp-capability';
import { ProjectType } from '../../../enums/project-types';
import { expect, test } from '../../../misc/fixtures';
import { scriptMcpListServer } from '../../../misc/mcp-list-server';
@@ -19,6 +20,6 @@ test('Verify Invalid Entries With the Same Name and Issue Are All Listed', async
const client = await mcpClientFlow.create(project, new McpClient(faker.string.alphanumeric(10), url));
await mcpClientFlow.connect(client);
const error = await mcpClientFlow.getListError(client, 'tools');
const error = await mcpClientFlow.getListError(client, McpPrimitiveType.Tools);
expect(error.entries.map(entry => entry.label)).toEqual(['dup', 'dup']);
});
@@ -1,5 +1,6 @@
import { faker } from '@faker-js/faker';
import { McpPrimitiveType } from '../../../enums/mcp-capability';
import { ProjectType } from '../../../enums/project-types';
import { expect, test } from '../../../misc/fixtures';
import { scriptMcpListServer } from '../../../misc/mcp-list-server';
@@ -20,7 +21,7 @@ test('Verify an Entry Without a Usable Name Is Labeled by Its Position and Shows
const client = await mcpClientFlow.create(project, new McpClient(faker.string.alphanumeric(10), url));
await mcpClientFlow.connect(client);
const error = await mcpClientFlow.getListError(client, 'tools');
const error = await mcpClientFlow.getListError(client, McpPrimitiveType.Tools);
expect(error.entries.map(entry => entry.label)).toEqual(['index 1', 'index 2', 'index 3', 'index 4']);
expect(error.entries[3].reason).toMatch(/\(And \d+ more\)$/);
});
@@ -37,11 +38,11 @@ test('Verify a Long Error Panel Scrolls Inside Its Own Area Without Overlapping
const client = await mcpClientFlow.create(project, new McpClient(faker.string.alphanumeric(10), url));
await mcpClientFlow.connect(client);
const error = await mcpClientFlow.getListError(client, 'tools');
const error = await mcpClientFlow.getListError(client, McpPrimitiveType.Tools);
expect(error.entries).toHaveLength(12);
expect(await mcpClientPage.isCapabilityErrorScrollable('tools')).toBe(true);
expect(await mcpClientPage.isCapabilityErrorScrollable(McpPrimitiveType.Tools)).toBe(true);
const panelBox = await mcpClientPage.getCapabilityErrorBounds('tools');
const nextCapabilityBox = await mcpClientPage.getCapabilityBounds('prompts');
const panelBox = await mcpClientPage.getCapabilityErrorBounds(McpPrimitiveType.Tools);
const nextCapabilityBox = await mcpClientPage.getCapabilityBounds(McpPrimitiveType.Prompts);
expect(panelBox.y + panelBox.height).toBeLessThanOrEqual(nextCapabilityBox.y + 1);
});
@@ -1,5 +1,6 @@
import { faker } from '@faker-js/faker';
import { McpPrimitiveType } from '../../../enums/mcp-capability';
import { ProjectType } from '../../../enums/project-types';
import { expect, test } from '../../../misc/fixtures';
import { scriptMcpListServer } from '../../../misc/mcp-list-server';
@@ -19,18 +20,18 @@ test('Verify the Error Panel of a Partially Invalid List Follows Expand and Coll
const project = await workspaceFlow.create(new Project(faker.string.alphanumeric(10), ProjectType.Local));
const client = await mcpClientFlow.create(project, new McpClient(faker.string.alphanumeric(10), url));
await mcpClientFlow.connect(client);
await mcpClientFlow.getListError(client, 'tools');
await expect.poll(() => mcpClientPage.hasCapability('prompts')).toBe(true);
await mcpClientFlow.getListError(client, McpPrimitiveType.Tools);
await expect.poll(() => mcpClientPage.hasCapability(McpPrimitiveType.Prompts)).toBe(true);
const panelBox = await mcpClientPage.getCapabilityErrorBounds('tools');
const nextCapabilityBox = await mcpClientPage.getCapabilityBounds('prompts');
const panelBox = await mcpClientPage.getCapabilityErrorBounds(McpPrimitiveType.Tools);
const nextCapabilityBox = await mcpClientPage.getCapabilityBounds(McpPrimitiveType.Prompts);
expect(panelBox.y + panelBox.height).toBeLessThanOrEqual(nextCapabilityBox.y + 1);
await mcpClientFlow.toggleCapability(client, 'tools');
await expect.poll(() => mcpClientPage.getCapabilityErrorCount('tools')).toBe(0);
await mcpClientFlow.toggleCapability(client, McpPrimitiveType.Tools);
await expect.poll(() => mcpClientPage.getCapabilityErrorCount(McpPrimitiveType.Tools)).toBe(0);
await mcpClientFlow.toggleCapability(client, 'tools');
await expect.poll(() => mcpClientPage.getCapabilityErrorCount('tools')).toBe(1);
await mcpClientFlow.toggleCapability(client, McpPrimitiveType.Tools);
await expect.poll(() => mcpClientPage.getCapabilityErrorCount(McpPrimitiveType.Tools)).toBe(1);
});
test('Verify the Error Panel of an Empty List Stays Visible Without a Collapse Caret', async ({ user }) => {
@@ -42,9 +43,9 @@ test('Verify the Error Panel of an Empty List Stays Visible Without a Collapse C
const project = await workspaceFlow.create(new Project(faker.string.alphanumeric(10), ProjectType.Local));
const client = await mcpClientFlow.create(project, new McpClient(faker.string.alphanumeric(10), url));
await mcpClientFlow.connect(client);
await mcpClientFlow.getListError(client, 'tools');
await mcpClientFlow.getListError(client, McpPrimitiveType.Tools);
expect(await mcpClientPage.hasCaret('tools')).toBe(false);
await mcpClientFlow.toggleCapability(client, 'tools');
await expect.poll(() => mcpClientPage.getCapabilityErrorCount('tools')).toBe(1);
expect(await mcpClientPage.hasCaret(McpPrimitiveType.Tools)).toBe(false);
await mcpClientFlow.toggleCapability(client, McpPrimitiveType.Tools);
await expect.poll(() => mcpClientPage.getCapabilityErrorCount(McpPrimitiveType.Tools)).toBe(1);
});
@@ -1,5 +1,6 @@
import { faker } from '@faker-js/faker';
import { McpPrimitiveType } from '../../../enums/mcp-capability';
import { ProjectType } from '../../../enums/project-types';
import { expect, test } from '../../../misc/fixtures';
import { scriptMcpListServer } from '../../../misc/mcp-list-server';
@@ -15,7 +16,7 @@ test('Verify a JSON-RPC Error Whose Message Is a JSON String Is Shown Readably',
const client = await mcpClientFlow.create(project, new McpClient(faker.string.alphanumeric(10), url));
await mcpClientFlow.connect(client);
const error = await user.flowManager.mcpClientFlow.getListError(client, 'tools');
const error = await user.flowManager.mcpClientFlow.getListError(client, McpPrimitiveType.Tools);
expect(error.title).toContain('boom');
expect(error.title).not.toContain('[object Object]');
@@ -1,5 +1,6 @@
import { faker } from '@faker-js/faker';
import { McpPrimitiveType } from '../../../enums/mcp-capability';
import { ProjectType } from '../../../enums/project-types';
import { expect, test } from '../../../misc/fixtures';
import { scriptMcpListServer } from '../../../misc/mcp-list-server';
@@ -21,7 +22,7 @@ for (const rpcError of jsonRpcErrors) {
const client = await mcpClientFlow.create(project, new McpClient(faker.string.alphanumeric(10), url));
await mcpClientFlow.connect(client);
const error = await user.flowManager.mcpClientFlow.getListError(client, 'tools');
const error = await user.flowManager.mcpClientFlow.getListError(client, McpPrimitiveType.Tools);
expect(error.title).toContain(rpcError.message);
expect(error.entries).toEqual([]);
});
@@ -1,5 +1,6 @@
import { faker } from '@faker-js/faker';
import { McpPrimitiveType } from '../../../enums/mcp-capability';
import { ProjectType } from '../../../enums/project-types';
import { DEFAULT_TIMEOUT, expect, test } from '../../../misc/fixtures';
import { scriptMcpListServer } from '../../../misc/mcp-list-server';
@@ -32,13 +33,13 @@ test('Verify Load More Whose Next Page Has an Invalid Entry Lists the Skipped En
const project = await workspaceFlow.create(new Project(faker.string.alphanumeric(10), ProjectType.Local));
const client = await mcpClientFlow.create(project, new McpClient(faker.string.alphanumeric(10), url));
await mcpClientFlow.connect(client);
await expect.poll(() => mcpClientPage.getPrimitives('resources'), { timeout: DEFAULT_TIMEOUT }).toEqual(['item-1']);
await expect.poll(() => mcpClientPage.getPrimitives(McpPrimitiveType.Resources), { timeout: DEFAULT_TIMEOUT }).toEqual(['item-1']);
await mcpClientFlow.loadMore(client, 'resources');
await mcpClientFlow.loadMore(client, McpPrimitiveType.Resources);
await expect
.poll(() => mcpClientPage.getPrimitives('resources'), { timeout: DEFAULT_TIMEOUT })
.poll(() => mcpClientPage.getPrimitives(McpPrimitiveType.Resources), { timeout: DEFAULT_TIMEOUT })
.toEqual(['item-1', 'item-2']);
const error = await mcpClientFlow.getListError(client, 'resources');
const error = await mcpClientFlow.getListError(client, McpPrimitiveType.Resources);
expect(error.entries.map(entry => entry.label)).toEqual(['bad-resource']);
});
@@ -1,5 +1,6 @@
import { faker } from '@faker-js/faker';
import { McpListAction, McpPrimitiveType } from '../../../enums/mcp-capability';
import { ProjectType } from '../../../enums/project-types';
import { DEFAULT_TIMEOUT, expect, test } from '../../../misc/fixtures';
import { scriptMcpListServer } from '../../../misc/mcp-list-server';
@@ -8,19 +9,19 @@ import { Project } from '../../../models/project';
const loadMoreVariants = [
{
type: 'tools',
type: McpPrimitiveType.Tools,
method: 'tools/list',
entry: (name: string) => ({ name, inputSchema: { type: 'object' } }),
badEntry: { name: 'bad-tool' },
},
{
type: 'prompts',
type: McpPrimitiveType.Prompts,
method: 'prompts/list',
entry: (name: string) => ({ name }),
badEntry: { name: 'bad-prompt', arguments: 'x' },
},
{
type: 'resources',
type: McpPrimitiveType.Resources,
method: 'resources/list',
entry: (name: string) => ({ name, uri: `file:///${name}.txt` }),
badEntry: { name: 'bad-resource' },
@@ -30,8 +31,8 @@ const loadMoreVariants = [
// Open app bugs: listTools() / listPrompts() do not forward the cursor, so Load More re-requests the first page.
// Resources forwards it and passes.
const knownBugs: Partial<Record<(typeof loadMoreVariants)[number]['type'], string>> = {
tools: 'INS-4060',
prompts: 'INS-4061',
[McpPrimitiveType.Tools]: 'INS-4060',
[McpPrimitiveType.Prompts]: 'INS-4061',
};
for (const { type, method, entry, badEntry } of loadMoreVariants) {
@@ -51,12 +52,12 @@ for (const { type, method, entry, badEntry } of loadMoreVariants) {
const client = await mcpClientFlow.create(project, new McpClient(faker.string.alphanumeric(10), url));
await mcpClientFlow.connect(client);
await expect.poll(() => mcpClientPage.getPrimitives(type), { timeout: DEFAULT_TIMEOUT }).toEqual(['item-1']);
expect(await mcpClientFlow.getListAction(client, type)).toBe('Load More');
expect(await mcpClientFlow.getListAction(client, type)).toBe(McpListAction.LoadMore);
await mcpClientFlow.loadMore(client, type);
await expect
.poll(() => mcpClientPage.getPrimitives(type), { timeout: DEFAULT_TIMEOUT })
.toEqual(['item-1', 'item-2']);
expect(await mcpClientFlow.getListAction(client, type)).toBe('Refresh');
expect(await mcpClientFlow.getListAction(client, type)).toBe(McpListAction.Refresh);
});
}
@@ -1,5 +1,6 @@
import { faker } from '@faker-js/faker';
import { McpPrimitiveType } from '../../../enums/mcp-capability';
import { ProjectType } from '../../../enums/project-types';
import { DEFAULT_TIMEOUT, expect, test } from '../../../misc/fixtures';
import { scriptMcpListServer } from '../../../misc/mcp-list-server';
@@ -31,14 +32,14 @@ for (const [variant, { tools, skipped }] of Object.entries(partialInvalidTools))
const client = await mcpClientFlow.create(project, new McpClient(faker.string.alphanumeric(10), url));
await mcpClientFlow.connect(client);
const error = await mcpClientFlow.getListError(client, 'tools');
const error = await mcpClientFlow.getListError(client, McpPrimitiveType.Tools);
expect(error.title).toMatch(skippedTitle(2));
expect(error.entries.map(entry => entry.label)).toEqual(skipped);
for (const entry of error.entries) {
expect(entry.reason).not.toBe('');
}
await expect
.poll(() => mcpClientPage.getPrimitives('tools'), {
.poll(() => mcpClientPage.getPrimitives(McpPrimitiveType.Tools), {
timeout: DEFAULT_TIMEOUT,
})
.toEqual(['valid-tool']);
@@ -1,5 +1,6 @@
import { faker } from '@faker-js/faker';
import { McpPrimitiveType } from '../../../enums/mcp-capability';
import { ProjectType } from '../../../enums/project-types';
import { DEFAULT_TIMEOUT, expect, test } from '../../../misc/fixtures';
import { type McpListConfig, scriptMcpListServer } from '../../../misc/mcp-list-server';
@@ -19,14 +20,14 @@ test('Verify Refresh Clears the Error Once the Server Returns Valid Data', async
);
const client = await mcpClientFlow.create(project, new McpClient(faker.string.alphanumeric(10), url));
await mcpClientFlow.connect(client);
await mcpClientFlow.getListError(client, 'tools');
await mcpClientFlow.getListError(client, McpPrimitiveType.Tools);
await scriptMcpListServer(key, valid);
await mcpClientFlow.refresh(client, 'tools');
await expect.poll(() => mcpClientPage.getCapabilityErrorCount('tools')).toBe(0);
await expect.poll(() => mcpClientPage.getPrimitives('tools'), { timeout: DEFAULT_TIMEOUT }).toEqual(['valid-tool']);
await mcpClientFlow.refresh(client, McpPrimitiveType.Tools);
await expect.poll(() => mcpClientPage.getCapabilityErrorCount(McpPrimitiveType.Tools)).toBe(0);
await expect.poll(() => mcpClientPage.getPrimitives(McpPrimitiveType.Tools), { timeout: DEFAULT_TIMEOUT }).toEqual(['valid-tool']);
await scriptMcpListServer(key, invalid);
await mcpClientFlow.refresh(client, 'tools');
expect((await mcpClientFlow.getListError(client, 'tools')).title).toBe(wholeFailureTitle('tools/list'));
await mcpClientFlow.refresh(client, McpPrimitiveType.Tools);
expect((await mcpClientFlow.getListError(client, McpPrimitiveType.Tools)).title).toBe(wholeFailureTitle('tools/list'));
});
@@ -1,5 +1,6 @@
import { faker } from '@faker-js/faker';
import { McpPrimitiveType } from '../../../enums/mcp-capability';
import { ProjectType } from '../../../enums/project-types';
import { DEFAULT_TIMEOUT, expect, test } from '../../../misc/fixtures';
import { scriptMcpListServer } from '../../../misc/mcp-list-server';
@@ -23,7 +24,7 @@ test('Verify Refresh on Resources Clears a Resource Templates Error Once the Ser
const project = await workspaceFlow.create(new Project(faker.string.alphanumeric(10), ProjectType.Local));
const client = await mcpClientFlow.create(project, new McpClient(faker.string.alphanumeric(10), url));
await mcpClientFlow.connect(client);
await mcpClientFlow.getListError(client, 'resources');
await mcpClientFlow.getListError(client, McpPrimitiveType.Resources);
await scriptMcpListServer(key, {
'resources/list': resources,
@@ -31,7 +32,7 @@ test('Verify Refresh on Resources Clears a Resource Templates Error Once the Ser
result: { resourceTemplates: [{ uriTemplate: 'file:///{path}', name: 'valid-template' }] },
},
});
await mcpClientFlow.refresh(client, 'resources');
await mcpClientFlow.refresh(client, McpPrimitiveType.Resources);
await expect.poll(() => mcpClientPage.getCapabilityErrorCount('resources'), { timeout: DEFAULT_TIMEOUT }).toBe(0);
await expect.poll(() => mcpClientPage.getCapabilityErrorCount(McpPrimitiveType.Resources), { timeout: DEFAULT_TIMEOUT }).toBe(0);
});
@@ -1,5 +1,6 @@
import { faker } from '@faker-js/faker';
import { McpPrimitiveType } from '../../../enums/mcp-capability';
import { ProjectType } from '../../../enums/project-types';
import { DEFAULT_TIMEOUT, expect, test } from '../../../misc/fixtures';
import { scriptMcpListServer } from '../../../misc/mcp-list-server';
@@ -26,20 +27,20 @@ test('Verify Invalid resources/list and prompts/list Are Reported Like tools/lis
const client = await mcpClientFlow.create(project, new McpClient(faker.string.alphanumeric(10), url));
await mcpClientFlow.connect(client);
const resourcesError = await mcpClientFlow.getListError(client, 'resources');
const resourcesError = await mcpClientFlow.getListError(client, McpPrimitiveType.Resources);
expect(resourcesError.title).toMatch(skippedTitle(2));
expect(resourcesError.entries.map(entry => entry.label)).toEqual(['no-uri-resource', 'numeric-uri-resource']);
await expect
.poll(() => mcpClientPage.getPrimitives('resources'), {
.poll(() => mcpClientPage.getPrimitives(McpPrimitiveType.Resources), {
timeout: DEFAULT_TIMEOUT,
})
.toEqual(['valid-resource']);
expect(await mcpClientFlow.getListError(client, 'prompts')).toEqual({
expect(await mcpClientFlow.getListError(client, McpPrimitiveType.Prompts)).toEqual({
title: wholeFailureTitle('prompts/list'),
entries: [],
});
expect(await mcpClientPage.getPrimitives('prompts')).toEqual([]);
expect(await mcpClientPage.getPrimitives(McpPrimitiveType.Prompts)).toEqual([]);
});
test('Verify a prompts/list Whose prompts Is an Object Is a Whole-Response Error', async ({ user }) => {
@@ -51,7 +52,7 @@ test('Verify a prompts/list Whose prompts Is an Object Is a Whole-Response Error
const client = await mcpClientFlow.create(project, new McpClient(faker.string.alphanumeric(10), url));
await mcpClientFlow.connect(client);
expect(await user.flowManager.mcpClientFlow.getListError(client, 'prompts')).toEqual({
expect(await user.flowManager.mcpClientFlow.getListError(client, McpPrimitiveType.Prompts)).toEqual({
title: wholeFailureTitle('prompts/list'),
entries: [],
});
@@ -70,11 +71,11 @@ test('Verify a Prompt with Bad arguments Is Skipped While Valid Prompts Are List
const client = await mcpClientFlow.create(project, new McpClient(faker.string.alphanumeric(10), url));
await mcpClientFlow.connect(client);
const error = await mcpClientFlow.getListError(client, 'prompts');
const error = await mcpClientFlow.getListError(client, McpPrimitiveType.Prompts);
expect(error.title).toMatch(skippedTitle(1));
expect(error.entries.map(entry => entry.label)).toEqual(['bad-args-prompt']);
await expect
.poll(() => user.pageManager.mcpClientPage.getPrimitives('prompts'), {
.poll(() => user.pageManager.mcpClientPage.getPrimitives(McpPrimitiveType.Prompts), {
timeout: DEFAULT_TIMEOUT,
})
.toEqual(['valid-prompt']);
@@ -1,5 +1,6 @@
import { faker } from '@faker-js/faker';
import { McpPrimitiveType } from '../../../enums/mcp-capability';
import { ProjectType } from '../../../enums/project-types';
import { DEFAULT_TIMEOUT, expect, test } from '../../../misc/fixtures';
import { scriptMcpListServer } from '../../../misc/mcp-list-server';
@@ -21,6 +22,6 @@ test('Verify a Server-Initiated Request Reusing the tools/list Request Id Does N
const client = await mcpClientFlow.create(project, new McpClient(faker.string.alphanumeric(10), url));
await mcpClientFlow.connect(client);
await expect.poll(() => mcpClientPage.getPrimitives('tools'), { timeout: DEFAULT_TIMEOUT }).toEqual(['valid-tool']);
expect(await mcpClientPage.getCapabilityErrorCount('tools')).toBe(0);
await expect.poll(() => mcpClientPage.getPrimitives(McpPrimitiveType.Tools), { timeout: DEFAULT_TIMEOUT }).toEqual(['valid-tool']);
expect(await mcpClientPage.getCapabilityErrorCount(McpPrimitiveType.Tools)).toBe(0);
});
@@ -1,5 +1,6 @@
import { faker } from '@faker-js/faker';
import { McpPrimitiveType } from '../../../enums/mcp-capability';
import { ProjectType } from '../../../enums/project-types';
import { DEFAULT_TIMEOUT, expect, test } from '../../../misc/fixtures';
import { scriptMcpListServer } from '../../../misc/mcp-list-server';
@@ -22,13 +23,13 @@ test('Verify Resources and Resource Templates Errors Share One Capability Item P
const client = await mcpClientFlow.create(project, new McpClient(faker.string.alphanumeric(10), url));
await mcpClientFlow.connect(client);
const error = await mcpClientFlow.getListError(client, 'resources');
await expect.poll(() => mcpClientPage.getCapabilityErrorCount('resources')).toBe(1);
const error = await mcpClientFlow.getListError(client, McpPrimitiveType.Resources);
await expect.poll(() => mcpClientPage.getCapabilityErrorCount(McpPrimitiveType.Resources)).toBe(1);
expect(error.title).toContain(wholeFailureTitle('resources/list'));
expect(error.title).toContain('skipped because they do not match the MCP schema');
expect(error.entries.map(entry => entry.label)).toEqual(['no-uri-template']);
await expect
.poll(() => mcpClientPage.getPrimitives('resourceTemplates'), {
.poll(() => mcpClientPage.getPrimitives(McpPrimitiveType.ResourceTemplates), {
timeout: DEFAULT_TIMEOUT,
})
.toEqual(['valid-template']);
@@ -1,5 +1,6 @@
import { faker } from '@faker-js/faker';
import { McpPrimitiveType } from '../../../enums/mcp-capability';
import { ProjectType } from '../../../enums/project-types';
import { expect, test } from '../../../misc/fixtures';
import { scriptMcpListServer } from '../../../misc/mcp-list-server';
@@ -21,11 +22,11 @@ for (const [variant, result] of Object.entries(wholeInvalidTools)) {
const client = await mcpClientFlow.create(project, new McpClient(faker.string.alphanumeric(10), url));
await mcpClientFlow.connect(client);
expect(await mcpClientFlow.getListError(client, 'tools')).toEqual({
expect(await mcpClientFlow.getListError(client, McpPrimitiveType.Tools)).toEqual({
title: wholeFailureTitle('tools/list'),
entries: [],
});
expect(await user.pageManager.mcpClientPage.getPrimitives('tools')).toEqual([]);
expect(await user.pageManager.mcpClientPage.getPrimitives(McpPrimitiveType.Tools)).toEqual([]);
});
}
@@ -39,8 +40,8 @@ test('Verify a tools/list Result That Is Not an Object Shows an Error and Lists
// The MCP SDK rejects a non-object `result` before the pane's own
// validation runs, so the title is the SDK's ZodError, not the agreed
// "does not match the MCP schema" wording.
const error = await user.flowManager.mcpClientFlow.getListError(client, 'tools');
const error = await user.flowManager.mcpClientFlow.getListError(client, McpPrimitiveType.Tools);
expect(error.title).not.toBe('');
expect(error.entries).toEqual([]);
expect(await user.pageManager.mcpClientPage.getPrimitives('tools')).toEqual([]);
expect(await user.pageManager.mcpClientPage.getPrimitives(McpPrimitiveType.Tools)).toEqual([]);
});
@@ -1,5 +1,6 @@
import { faker } from "@faker-js/faker";
import { McpPrimitiveType } from "../../enums/mcp-capability";
import { ProjectType } from "../../enums/project-types";
import { DEFAULT_TIMEOUT,expect, MCP_SERVER, test } from "../../misc/fixtures";
import { McpClient } from "../../models/mcp-client";
@@ -38,7 +39,7 @@ test("Verify Create MCP Client", async ({ user }) => {
);
await mcpClientFlow.connect(mcpClient);
const tools = await mcpClientFlow.getTools(mcpClient);
expect(await user.pageManager.mcpClientPage.getCapabilityErrorCount("tools")).toBe(0);
expect(await user.pageManager.mcpClientPage.getCapabilityErrorCount(McpPrimitiveType.Tools)).toBe(0);
expect(tools).toEqual(
expect.arrayContaining([
"read_wiki_structure",
@@ -1,5 +1,6 @@
import { faker } from "@faker-js/faker";
import { McpPrimitiveType } from "../../enums/mcp-capability";
import { ProjectType } from "../../enums/project-types";
import { DEFAULT_TIMEOUT, expect, MCP_SERVER, test } from "../../misc/fixtures";
import { scriptMcpListServer } from "../../misc/mcp-list-server";
@@ -39,9 +40,9 @@ test("Verify Loaded Pages Are Kept After Switching to Another MCP Client and Bac
new McpClient(faker.string.alphanumeric(10), url),
);
await mcpClientFlow.connect(client);
await mcpClientFlow.loadMore(client, "resources");
await mcpClientFlow.loadMore(client, McpPrimitiveType.Resources);
await expect
.poll(() => mcpClientPage.getPrimitives("resources"), {
.poll(() => mcpClientPage.getPrimitives(McpPrimitiveType.Resources), {
timeout: DEFAULT_TIMEOUT,
})
.toEqual(["item-1", "item-2"]);
@@ -50,10 +51,10 @@ test("Verify Loaded Pages Are Kept After Switching to Another MCP Client and Bac
project,
new McpClient(faker.string.alphanumeric(10), MCP_SERVER),
);
await mcpClientFlow.getListAction(client, "resources");
await mcpClientFlow.getListAction(client, McpPrimitiveType.Resources);
await expect
.poll(() => mcpClientPage.getPrimitives("resources"), {
.poll(() => mcpClientPage.getPrimitives(McpPrimitiveType.Resources), {
timeout: DEFAULT_TIMEOUT,
})
.toEqual(["item-1", "item-2"]);