assertions.ts•6.23 kB
/**
 * @fileoverview Custom test assertions for git tool testing.
 * @module tests/mcp-server/tools/definitions/helpers/assertions
 */
import { expect } from 'vitest';
import type { ContentBlock } from '@modelcontextprotocol/sdk/types.js';
import { McpError, JsonRpcErrorCode } from '@/types-global/errors.js';
/**
 * Assert that a value is a valid McpError with expected properties
 */
export function assertMcpError(
  error: unknown,
  expectedCode: JsonRpcErrorCode,
  messagePattern?: string | RegExp,
): asserts error is McpError {
  expect(error).toBeInstanceOf(McpError);
  const mcpError = error as McpError;
  expect(mcpError.code).toBe(expectedCode);
  if (messagePattern) {
    if (typeof messagePattern === 'string') {
      expect(mcpError.message).toContain(messagePattern);
    } else {
      expect(mcpError.message).toMatch(messagePattern);
    }
  }
}
/**
 * Assert that content blocks contain text content
 */
export function assertTextContent(
  content: ContentBlock[],
  expectedPattern: string | RegExp,
): void {
  expect(content).toHaveLength(1);
  expect(content[0]).toHaveProperty('type', 'text');
  const textContent = (content[0] as { type: 'text'; text: string }).text;
  if (typeof expectedPattern === 'string') {
    expect(textContent).toContain(expectedPattern);
  } else {
    expect(textContent).toMatch(expectedPattern);
  }
}
/**
 * Assert that content blocks contain properly formatted markdown
 */
export function assertMarkdownContent(
  content: ContentBlock[],
  expectedSections: string[],
): void {
  expect(content).toHaveLength(1);
  expect(content[0]).toHaveProperty('type', 'text');
  const textContent = (content[0] as { type: 'text'; text: string }).text;
  // Check for markdown sections
  for (const section of expectedSections) {
    expect(textContent).toContain(section);
  }
}
/**
 * Assert that a tool output has the expected structure
 */
export function assertToolOutput<T extends Record<string, unknown>>(
  output: unknown,
  expectedFields: (keyof T)[],
): asserts output is T {
  expect(output).toBeDefined();
  expect(typeof output).toBe('object');
  expect(output).not.toBeNull();
  for (const field of expectedFields) {
    expect(output).toHaveProperty(field as string);
  }
}
/**
 * Assert that provider was called with expected context
 */
export function assertProviderCalledWithContext(
  providerCall: unknown[],
  expectedWorkingDir: string,
  expectedTenantId: string,
): void {
  expect(providerCall).toHaveLength(2);
  const [_options, context] = providerCall;
  expect(context).toMatchObject({
    workingDirectory: expectedWorkingDir,
    tenantId: expectedTenantId,
  });
  expect(context).toHaveProperty('requestContext');
}
/**
 * Assert that an error contains specific data fields
 */
export function assertErrorData(
  error: McpError,
  expectedData: Record<string, unknown>,
): void {
  expect(error.data).toBeDefined();
  expect(error.data).toMatchObject(expectedData);
}
/**
 * Assert that content is properly escaped/sanitized
 */
export function assertSanitizedContent(content: ContentBlock[]): void {
  expect(content).toHaveLength(1);
  const textContent = (content[0] as { type: 'text'; text: string }).text;
  // Check for common XSS patterns that should be escaped
  expect(textContent).not.toMatch(/<script>/i);
  expect(textContent).not.toMatch(/javascript:/i);
  expect(textContent).not.toMatch(/onerror=/i);
}
/**
 * Assert that response formatter output is LLM-friendly
 * Now supports both JSON and Markdown formats
 */
export function assertLlmFriendlyFormat(
  content: ContentBlock[],
  minLength = 50,
): void {
  expect(content).toHaveLength(1);
  const textContent = (content[0] as { type: 'text'; text: string }).text;
  // Should have meaningful content
  expect(textContent.length).toBeGreaterThan(minLength);
  // Check if it's JSON or Markdown
  const isJsonOnly =
    textContent.trim().startsWith('{') && textContent.trim().endsWith('}');
  if (isJsonOnly) {
    // If it's JSON, it should be formatted (pretty-printed)
    expect(textContent).toMatch(/\n/);
    // Should be valid JSON
    expect(() => JSON.parse(textContent)).not.toThrow();
  } else {
    // If it's Markdown, should have headers
    expect(textContent).toMatch(/^#\s+/m);
  }
}
/**
 * Assert that content blocks contain valid JSON with expected structure
 */
export function assertJsonContent(
  content: ContentBlock[],
  expectedStructure: Record<string, unknown>,
): void {
  expect(content).toHaveLength(1);
  expect(content[0]).toHaveProperty('type', 'text');
  const textContent = (content[0] as { type: 'text'; text: string }).text;
  // Should be valid JSON
  let parsedJson: unknown;
  try {
    parsedJson = JSON.parse(textContent);
  } catch (error) {
    throw new Error(`Content is not valid JSON: ${textContent}`);
  }
  // Should match expected structure
  expect(parsedJson).toMatchObject(expectedStructure);
}
/**
 * Assert that content blocks contain valid JSON and return parsed object
 */
export function parseJsonContent(content: ContentBlock[]): unknown {
  expect(content).toHaveLength(1);
  expect(content[0]).toHaveProperty('type', 'text');
  const textContent = (content[0] as { type: 'text'; text: string }).text;
  try {
    return JSON.parse(textContent);
  } catch (error) {
    throw new Error(`Content is not valid JSON: ${textContent}`);
  }
}
/**
 * Assert that JSON content has specific field with expected value
 */
export function assertJsonField(
  content: ContentBlock[],
  fieldPath: string,
  expectedValue: unknown,
): void {
  const parsed = parseJsonContent(content) as Record<string, unknown>;
  // Support nested paths like "status.current_branch"
  const pathParts = fieldPath.split('.');
  let value: unknown = parsed;
  for (const part of pathParts) {
    if (value && typeof value === 'object' && part in value) {
      value = (value as Record<string, unknown>)[part];
    } else {
      throw new Error(`Field path "${fieldPath}" not found in JSON`);
    }
  }
  if (typeof expectedValue === 'function') {
    // Allow for expect matchers like expect.any(Array)
    expect(value).toEqual(expectedValue);
  } else {
    expect(value).toEqual(expectedValue);
  }
}