Files
wehub-resource-sync b3a7f98e5a
CI / E2E Cloudflare (4/8) (push) Failing after 0s
CI / E2E Cloudflare (8/8) (push) Failing after 1s
CI / Lint (push) Failing after 1s
Auto Extract / Extract (push) Failing after 4s
CI / Version Check (push) Failing after 9s
CI / Integration Tests (push) Failing after 1s
CI / E2E tests (1/8) (push) Failing after 1s
CI / E2E tests (2/8) (push) Failing after 2s
CI / E2E tests (3/8) (push) Failing after 2s
CI / Browser Tests (push) Failing after 1s
CI / E2E tests (5/8) (push) Failing after 1s
CI / E2E Cloudflare (2/8) (push) Failing after 2s
CI / Typecheck (push) Failing after 1s
CI / Changeset Validation (push) Failing after 2s
CI / E2E Cloudflare (5/8) (push) Failing after 1s
CI / E2E Cloudflare (6/8) (push) Failing after 1s
CI / E2E Cloudflare (7/8) (push) Failing after 1s
CodeQL / Analyze (javascript-typescript) (push) Failing after 1s
Format / Format (push) Failing after 0s
CodeQL / Analyze (actions) (push) Failing after 4s
CI / E2E tests (4/8) (push) Failing after 1s
CI / E2E tests (6/8) (push) Failing after 1s
CI / E2E tests (7/8) (push) Failing after 2s
CI / E2E tests (8/8) (push) Failing after 1s
CI / E2E Cloudflare (1/8) (push) Failing after 1s
CI / E2E Cloudflare (3/8) (push) Failing after 2s
Preview Releases / Publish Preview (push) Failing after 0s
zizmor / Run zizmor (push) Failing after 1s
Release / Release (push) Failing after 2s
CI / Smoke Tests (push) Failing after 5m36s
CI / Tests (push) Failing after 6m36s
Release / Sync Templates (push) Has been skipped
CI / E2E Tests (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:23:53 +08:00

163 lines
5.2 KiB
TypeScript

/**
* MCP tool input schema tests.
*
* The MCP SDK validates `arguments` against each tool's `inputSchema`
* (Zod) before the handler runs. These tests pin down what happens at
* that boundary: missing required fields, wrong types, invalid enum
* values, out-of-range numeric inputs, etc.
*
* The expected behavior is consistent: invalid arguments produce a
* structured error response (`isError: true`) with a message that names
* the offending field. We assert specifically that errors at this layer
* remain user-friendly across the omnibus fix.
*/
import { Role } from "@emdash-cms/auth";
import type { Kysely } from "kysely";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import type { Database } from "../../../src/database/types.js";
import { connectMcpHarness, extractText, type McpHarness } from "../../utils/mcp-runtime.js";
import { setupTestDatabaseWithCollections, teardownTestDatabase } from "../../utils/test-db.js";
const ADMIN_ID = "user_admin";
describe("MCP input schema validation", () => {
let db: Kysely<Database>;
let harness: McpHarness;
beforeEach(async () => {
db = await setupTestDatabaseWithCollections();
harness = await connectMcpHarness({ db, userId: ADMIN_ID, userRole: Role.ADMIN });
});
afterEach(async () => {
if (harness) await harness.cleanup();
await teardownTestDatabase(db);
});
it("content_create rejects missing required collection argument", async () => {
const result = await harness.client.callTool({
name: "content_create",
arguments: { data: { title: "T" } } as unknown as Record<string, unknown>,
});
expect(result.isError).toBe(true);
});
it("content_create rejects wrong-type for data field (string instead of object)", async () => {
const result = await harness.client.callTool({
name: "content_create",
arguments: { collection: "post", data: "not-an-object" } as unknown as Record<
string,
unknown
>,
});
expect(result.isError).toBe(true);
});
it("content_create with status enum value outside the enum is rejected", async () => {
const result = await harness.client.callTool({
name: "content_create",
arguments: {
collection: "post",
data: { title: "T" },
status: "weird-status",
} as unknown as Record<string, unknown>,
});
expect(result.isError).toBe(true);
});
it("content_list rejects out-of-range limit (e.g. negative)", async () => {
const result = await harness.client.callTool({
name: "content_list",
arguments: { collection: "post", limit: -5 },
});
expect(result.isError).toBe(true);
});
it("content_list rejects non-integer limit", async () => {
const result = await harness.client.callTool({
name: "content_list",
arguments: { collection: "post", limit: 5.7 },
});
expect(result.isError).toBe(true);
});
it("content_list rejects order outside enum", async () => {
const result = await harness.client.callTool({
name: "content_list",
arguments: { collection: "post", order: "sideways" } as unknown as Record<string, unknown>,
});
expect(result.isError).toBe(true);
});
it("schema_create_collection rejects supports value outside enum", async () => {
const result = await harness.client.callTool({
name: "schema_create_collection",
arguments: {
slug: "x",
label: "X",
supports: ["drafts", "garbage"],
} as unknown as Record<string, unknown>,
});
expect(result.isError).toBe(true);
});
it("schema_create_field rejects type outside enum", async () => {
const result = await harness.client.callTool({
name: "schema_create_field",
arguments: {
collection: "post",
slug: "x",
label: "X",
type: "magic",
} as unknown as Record<string, unknown>,
});
expect(result.isError).toBe(true);
});
it("content_get rejects missing id", async () => {
const result = await harness.client.callTool({
name: "content_get",
arguments: { collection: "post" } as unknown as Record<string, unknown>,
});
expect(result.isError).toBe(true);
});
it("content_schedule rejects missing scheduledAt", async () => {
const result = await harness.client.callTool({
name: "content_schedule",
arguments: { collection: "post", id: "01ANY" } as unknown as Record<string, unknown>,
});
expect(result.isError).toBe(true);
});
it("media_list with limit > 100 is rejected by inputSchema", async () => {
const result = await harness.client.callTool({
name: "media_list",
arguments: { limit: 500 },
});
expect(result.isError).toBe(true);
});
it("revision_list with limit > 50 is rejected by inputSchema", async () => {
const result = await harness.client.callTool({
name: "revision_list",
arguments: { collection: "post", id: "01x", limit: 500 },
});
expect(result.isError).toBe(true);
});
it("input validation error messages name the offending field", async () => {
const result = await harness.client.callTool({
name: "schema_create_collection",
arguments: { slug: "Has-Caps", label: "Bad" },
});
expect(result.isError).toBe(true);
// Ideally the error names the field "slug" or shows the regex /
// pattern violation. Today the SDK error usually does — pin that
// behavior so it doesn't regress.
expect(extractText(result)).toMatch(/slug|pattern|regex|invalid/i);
});
});