Skip to content

feat(testing-lib) Implementing a testing suite for MCP and Evals - #69

Merged
gabrypavanello merged 35 commits into
mainfrom
001-testing-library
Jan 7, 2026
Merged

feat(testing-lib) Implementing a testing suite for MCP and Evals#69
gabrypavanello merged 35 commits into
mainfrom
001-testing-library

Conversation

@gabrypavanello

@gabrypavanello gabrypavanello commented Jan 7, 2026

Copy link
Copy Markdown
Contributor

Note

Introduces a comprehensive testing library and wires up example tests across the repo.

  • Adds @mcp-apps-kit/testing package with server/client test utilities (startTestServer, createTestClient), behavior assertions (expectToolResult), declarative suites (defineTestSuite/runTestSuite), property-based testing (generators, forAllInputs), LLM evaluation (createLLMEvaluator, criteria), UI testing (createMockHost, createTestEnvironment, TestEnvironmentBuilder), Vitest/Jest adapters, structured types, debug loggers, and error types
  • Provides docs in packages/testing/README.md and extensive unit/integration tests under packages/testing/tests
  • Updates examples/minimal to be testable: exports app, guards server start in tests, adds Vitest config/setup, and new tests for v1/v2 tools, versioning, integration, advanced features, and React UI widgets
  • Enhances tooling/config: adds testing deps/scripts to examples/minimal/package.json, extends eslint.config.js to include packages/testing, and ignores .cursor/ in .gitignore

Written by Cursor Bugbot for commit da29196. Configure here.

- Created package directory structure
- Added package.json with dependencies and peer dependencies
- Configured TypeScript, tsup, and Vitest
- Added README with package overview

Completed tasks: T001-T006
- Created shared types and interfaces (types.ts)
- Implemented error types (TestingError, ConnectionError, TimeoutError, etc.)
- Added debug logger using debug package convention
- Created debug module index
- Set up main package index with type exports

Completed tasks: T007-T011
- Implemented TestClient wrapper around MCP SDK
- Added startTestServer for App instances and external processes
- Implemented ServerStartupError with diagnostic details
- Created server module index exports
- Added unit and contract tests (stubs for now, to be expanded)

Completed tasks: T012-T019
- Implemented expectToolResult standalone matcher with all assertions
- Added toMatchObject, toMatchSchema, toHaveNoError, toHaveError, toContainText
- Implemented defineTestSuite for test case definition
- Implemented runTestSuite for executing test suites
- Created behavior and eval module indexes
- Added comprehensive unit, contract, and integration tests

Completed tasks: T020-T031
- Implemented framework-agnostic core matchers
- Added setupVitestMatchers and setupJestMatchers adapters
- Added TypeScript type augmentations for both frameworks
- Created matchers module index
- Added comprehensive unit and integration tests

Completed tasks: T032-T039
- Implemented generators wrapper around fast-check with lazy loading
- Added generators.fromSchema for Zod schema generation
- Implemented forAllInputs property test runner
- Added PropertyFailureError with shrunk input details
- Created property module index
- Added unit and integration tests with graceful handling of missing dependencies

Completed tasks: T040-T046
- Implemented createMockHost wrapping MockAdapter from @mcp-apps-kit/ui
- Added emitToolResult, setTheme, emitToolCancelled methods
- Added getToolCallHistory and clearHistory for assertions
- Implemented createTestEnvironment coordinating server and client
- Implemented TestEnvironmentBuilder with fluent API
- Created ui module index
- Added unit tests with graceful handling of missing dependencies

Completed tasks: T047-T054
- Implemented LLMProvider interface
- Added OpenAI and Anthropic providers with lazy loading
- Implemented createLLMEvaluator factory
- Added env var validation for API keys
- Implemented built-in criteria (accuracy, relevance, safety, completeness)
- Added evaluateWithPrompt for custom evaluation
- Created llm module index
- Added unit tests with graceful handling of missing dependencies

Completed tasks: T055-T063
- Updated main index.ts with all exports
- Verified subpath exports for /vitest and /jest in package.json
- Wrote comprehensive README with examples
- Fixed build errors (exported extractResultData, fixed type imports)
- Build verified and working
- Package already included in monorepo workspace (packages/*)

Note: T069 (quickstart validation) and T070 (full test suite) require
runtime dependencies and can be validated during integration testing.

Completed tasks: T064-T068
- Fixed import paths in contract/integration tests (wrong relative depth)
- Fixed zod-fast-check version (^0.10.1 instead of ^1.0.0)
- Improved error handling in test-server test to prevent unhandled errors
- All 62 tests now passing across 14 test files
- Created comprehensive test suite for minimal example
- Tests for v1 and v2 greet tools
- Versioning tests
- Integration tests
- Fixed TestClient to use client.request() with CallToolResultSchema
- Updated to extract structuredContent from MCP SDK results

Note: Some tests still failing - need to verify structuredContent extraction
- Fixed TestClient to properly use client.request() with CallToolResultSchema
  to access structuredContent from MCP SDK responses
- Fixed type issues for lazy-loaded modules (fast-check, zod-fast-check, mock)
- Added @types/debug for proper TypeScript support
- Fixed type import path for MCP SDK Client
- Fixed vitest adapter type augmentation
- Simplified minimal example tests to be more robust
- All 83 tests passing (62 in testing package + 21 in minimal example)
- Add comprehensive advanced-features.test.ts covering:
  - defineTestSuite / runTestSuite (declarative test definitions)
  - Suite hooks (beforeEach/afterEach)
  - Suite skip flag
  - Property-based testing with built-in generators
  - All assertion methods (toHaveNoError, toContainText, toMatchSchema, toMatchObject)
  - Client options (history tracking, listTools, timeout)
- Remove zod-fast-check dependency (incompatible with Zod v4)
- Remove generators.fromSchema() function
- Add fast-check as devDependency for property testing
- All 98 tests passing (62 in testing package + 36 in minimal example)
- Add `port` and `version` options to TestEnvironmentOptions
- Update createTestEnvironment to support versioned apps (e.g., /v1/mcp)
- Add TestEnvironmentBuilder.withPort() and .withVersion() methods
- Update advanced-features.test.ts to use createTestEnvironment
- Add tests for createTestEnvironment and TestEnvironmentBuilder
- All 103 tests passing (62 in testing package + 41 in minimal example)
- Implement standalone MockHost that works with or without @mcp-apps-kit/ui
- Add MockHost methods: setTheme, getTheme, simulateToolCall
- Add event handlers: onToolResult, onTeardown, onToolCancelled
- Track tool call history with timestamps
- Add comprehensive tests for all MockHost functionality
- All 112 tests passing (62 in testing package + 50 in minimal example)
- Add UI component tests for GreetingWidgetV1 and GreetingWidgetV2
- Test rendering, user interactions, tool calls, and error handling
- Mock @mcp-apps-kit/ui-react hooks and @mcp-apps-kit/ui exports
- Add @testing-library/react, @testing-library/jest-dom, jsdom dependencies
- Configure jsdom environment via @vitest-environment directive
- All 64 tests passing in minimal example
- Add table of contents and structured sections
- Document all major features with code examples
- Add Test Environment section (createTestEnvironment, builder, manual setup)
- Add Assertions & Matchers section with all available matchers
- Add Test Suites section with hooks and skip flags
- Add Property-Based Testing section with generators
- Add UI Widget Testing section with @testing-library/react
- Add Mock Host Environment section
- Add LLM Evaluation section with criteria
- Add Framework Integration section (Vitest/Jest)
- Add complete API Reference tables
- Follow style from @mcp-apps-kit/core README
- Add peerDependenciesMeta to mark fast-check, openai, anthropic,
  vitest, and jest as optional
- Update README install section with clearer table format
- Clarify that optional deps won't be auto-installed
- Move fast-check, openai, @anthropic-ai/sdk to dependencies
- Users no longer need to install these separately
- Keep vitest/jest as optional peers (user chooses test framework)
- Simplify README install section
- Change validation condition to ensure the response text includes the input name and is not an error.
- Remove unused Anthropic client initialization code to streamline the provider implementation.
- Fix property test predicate in advanced-features.test.ts to properly check for errors and parse JSON response
- Change integration.test.ts to use truly sequential calls instead of Promise.all
- Move versioning.test.ts to tests/integration/ directory with updated imports
- Various testing library improvements
- Add TypeScript files from the testing package to ESLint configuration for linting.
- Update test files to improve formatting and consistency, including removing unnecessary whitespace and ensuring proper indentation.
- Refactor test assertions for better readability and maintainability.
- Introduce optional stop method in the App interface to allow graceful server shutdown.
- Update startTestServerFromApp function to track if app.start() was used and call app.stop() if available during server stop.
- Ensure proper handling of dynamic and fixed port scenarios for server shutdown.
- Update test files to utilize structuredContent for typed data assertions, improving reliability and readability.
- Refactor property tests to filter out whitespace-only strings and ensure proper input validation.
- Introduce lazy loading for fast-check generators, allowing for better performance and compatibility with ESM.
- Improve error handling in property tests to provide detailed failure information, enhancing debugging capabilities.
- Update README documentation to reflect changes in testing utilities and usage examples.
- Mark @anthropic-ai/sdk, fast-check, and openai as optional peer dependencies in package.json.
- Revise README to clarify optional dependencies and their installation instructions.
- Introduce new resource and prompt matchers for improved testing capabilities.
- Implement lazy loading for fast-check and LLM evaluation modules to optimize performance.
- Expand test client functionality with methods for listing and retrieving prompts.
- Add checks for empty or missing content in Anthropic responses to prevent runtime errors.
- Update error messages for clarity regarding unexpected response types from Anthropic.
- Implement timeout cleanup in startTestServerFromApp to prevent memory leaks.
- Refactor lazy-loader to allow retrying of loading on failure and improve handling of concurrent calls.
@gabrypavanello gabrypavanello self-assigned this Jan 7, 2026
@coderabbitai

coderabbitai Bot commented Jan 7, 2026

Copy link
Copy Markdown
Contributor

Caution

Review failed

The pull request is closed.

📝 Walkthrough

Summary by CodeRabbit

Release Notes

  • New Features

    • Introduced comprehensive testing library with support for behavior testing, property-based testing, UI widget testing, and LLM evaluation of MCP applications
    • Added test suite examples for minimal application demonstrating greet tool testing, integration testing, and advanced features
    • Integrated testing infrastructure with Vitest configuration and setup utilities
  • Tests

    • Added extensive test coverage for greet tools (v1/v2), integration scenarios, and UI components
    • Included LLM evaluation tests for MCP tool call verification
  • Chores

    • Updated ESLint configuration for TypeScript testing code
    • Enhanced project dependencies with testing and evaluation frameworks
    • Updated environment configuration for testing setup

✏️ Tip: You can customize this high-level summary in your review settings.

Walkthrough

Adds a new testing workspace package @mcp-apps-kit/testing (types, server/client test helpers, behavior/property/LLM/UI testing, matchers/adapters, reporters, utilities), integrates extensive Vitest-based tests into the minimal example, and updates ESLint and .gitignore for testing paths and editor cursor artifacts.

Changes

Cohort / File(s) Summary
Repository config
.
/.gitignore, eslint.config.js
Added .cursor/ to .gitignore; included packages/testing/**/*.ts and ./packages/testing/tsconfig.json in ESLint server-side configuration.
Testing package manifest & build
packages/testing/
packages/testing/package.json, packages/testing/tsconfig.json, packages/testing/tsup.config.ts, packages/testing/vitest.config.ts
New package manifest, TS project config, tsup build config (entries: index/vitest/jest), and local Vitest config merging root config.
Public API & types aggregation
packages/testing/src/index.ts, packages/testing/src/types.ts
New central public entry exporting wide API surface and comprehensive type declarations for testing, eval, UI, property, LLM, and server utilities.
Errors
packages/testing/src/errors.ts
New structured TestingError hierarchy (TestingError, ConnectionError, TimeoutError, ServerStartupError, AssertionError, PropertyFailureError, ConfigurationError).
Debug & utils
packages/testing/src/debug/*, packages/testing/src/utils/*
Added createDebugLogger plus named loggers; lazy-loader, cached client factory, and isModuleAvailable helpers for optional runtime imports.
Server: test server & client
packages/testing/src/server/*
New startTestServer (App or external command, readyPattern, timeout) and createTestClient (StreamableHTTP transport, timeout/retries, history, normalization, disconnect).
Behavior testing
packages/testing/src/eval/behavior/*
Added defineTestSuite, runTestSuite, expectToolResult, and supporting matchers/utilities (deepMatch, extractResultData).
Property testing
packages/testing/src/eval/property/*
Lazy fast-check integration: generators (string, integer, boolean, array, object, oneOf, optional), resolve helpers, ensureFastCheckLoaded, and forAllInputs runner with failure/shrink reporting.
LLM evaluation (core) & criteria
packages/testing/src/eval/llm/*
createLLMEvaluator, criteria helpers, and provider interface exported; provider factories implemented in providers/* (OpenAI, Anthropic) with lazy SDK loading.
MCP evaluation framework
packages/testing/src/eval/mcp/*
Full MCPEval framework: createMCPEval, evaluator, session, retry/resilience, error-injection, cost-utils, batch runner, reporter, provider dispatch and helpers (detect/getDefaultModel).
Matchers & adapters
packages/testing/src/matchers/*, packages/testing/src/adapters/*
Core matchers (schema/success/error/text/object), resource/prompt matchers, and Vitest/Jest adapter setup with TypeScript augmentations.
UI testing
packages/testing/src/ui/*
createMockHost (lazy UI adapter integration, events, history) and createTestEnvironment/TestEnvironmentBuilder for server+client lifecycle.
Eval reporting & logging
packages/testing/src/eval/reporter/*
EvalReporter and MCPEvalReporter implementations, global results collector, and utilities to print batch/global summaries.
Examples & tests (minimal example)
examples/minimal/*, examples/minimal/tests/*
Exported app from examples/minimal/src/index.ts (deferred start under test env), added vitest config and .env.example, updated package.json scripts/devDeps, and many new Vitest test suites (unit, integration, UI, eval).
Testing package tests
packages/testing/tests/*
Comprehensive unit, integration, and contract tests for matchers, server utilities, behavior/property testing, LLM evals, UI utilities, lazy-loader, adapters, and reporter.

Sequence Diagram(s)

sequenceDiagram
    participant Test as Test Suite
    participant Env as TestEnvironmentBuilder
    participant Server as Test Server
    participant Client as Test Client
    participant App as MCP App

    Test->>Env: build() / createTestEnvironment(opts)
    Env->>Server: startTestServer(app or external)
    Server->>App: app.start({port, transport:'http'}) / spawn cmd
    Server-->>Env: TestServer{url,mcpUrl,stop}
    Env->>Client: createTestClient(mcpUrl, options)
    Client-->>Env: TestClient{callTool, listTools, history, disconnect}
    Env-->>Test: {server, client, cleanup}

    Test->>Client: callTool('greet', {name:'Alice'})
    Client->>Server: HTTP POST /mcp (CallTool)
    Server->>App: route to tool implementation
    App-->>Server: ToolResult
    Server-->>Client: ToolResult
    Client-->>Test: ToolResult (with structuredContent)

    Test->>Test: expectToolResult(result).toMatchSchema(...)
    Test->>Env: cleanup()
    Env->>Client: disconnect()
    Env->>Server: stop()
Loading
sequenceDiagram
    participant Suite as Test Runner
    participant Client as Test Client
    participant Tool as App/tool
    participant Matchers as expectToolResult

    Suite->>Suite: iterate TestCases (skip/only)
    Suite->>Client: callTool(toolName, input)
    Client->>Tool: invoke tool
    Tool-->>Client: return ToolResult
    Suite->>Matchers: expectToolResult(result)
    Matchers-->>Suite: pass / throw (assert)
    Suite->>Suite: record case result (time/status)
    Suite->>Suite: run afterEach()
    Suite-->>Test Runner: aggregated TestSuiteResult
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat(testing-lib) Implementing a testing suite for MCP and Evals' clearly and concisely describes the main change: introducing a comprehensive testing library for MCP and Evals functionality.
Description check ✅ Passed The description is well-structured and directly relates to the changeset. It clearly outlines the new testing package, updated examples, and enhanced tooling configuration.
Docstring Coverage ✅ Passed Docstring coverage is 94.12% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

📜 Recent review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b384b50 and b5334b8.

📒 Files selected for processing (2)
  • packages/testing/src/eval/mcp/error-injection.ts
  • packages/testing/src/eval/mcp/index.ts

Comment @coderabbitai help to get the list of available commands and usage tips.

…versions

- Change vitest specifier from ^4.0.0 to ^3 || ^4 to allow compatibility with both major versions.
*/

import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, screen, waitFor, fireEvent } from "@testing-library/react";
* using mock clients to simulate MCP tool calls.
*/

import { describe, it, expect, vi, beforeEach } from "vitest";
@gabrypavanello

Copy link
Copy Markdown
Contributor Author

@cursor review

@claude

claude Bot commented Jan 7, 2026

Copy link
Copy Markdown
Contributor

Code Review - Testing Library PR

Overview

This is an impressive and comprehensive addition to the mcp-apps-kit ecosystem! The testing library adds ~9500 lines of well-structured code that provides a complete testing solution for MCP applications. The implementation quality is very high overall.

✅ Strengths

1. Excellent Architecture & Design

  • Modular structure: Clean separation between server/client, matchers, property testing, LLM evaluation, and UI testing
  • Lazy loading pattern: Brilliant use of optional peer dependencies with createLazyLoader to keep bundle size minimal
  • Type safety: Strong TypeScript throughout with proper type exports and minimal any usage
  • Error handling: Comprehensive custom error classes with proper error codes and context

2. Outstanding Documentation

  • The 711-line README is exceptional - clear examples, comprehensive API reference, and usage patterns
  • Code has good inline documentation with JSDoc comments
  • Example tests in examples/minimal/tests/ demonstrate all features effectively

3. Developer Experience

  • Fluent API with TestEnvironmentBuilder is intuitive
  • createTestEnvironment simplifies common setup patterns
  • Framework-agnostic matchers (expectToolResult) work with any test runner
  • Native Vitest/Jest integrations via subpath exports

4. Security Considerations

  • API keys properly sourced from environment variables
  • No hardcoded credentials
  • Proper error messages when credentials are missing
  • Client factory pattern prevents credential leaks between tests

5. Test Coverage

  • 17 test files covering unit, integration, and contract tests
  • Tests demonstrate the library's own features effectively
  • Good mix of positive and negative test cases

@claude

claude Bot commented Jan 7, 2026

Copy link
Copy Markdown
Contributor

🔍 Issues & Recommendations

High Priority

1. Strict TypeScript Violations ⚠️

The project mandates "no any types" per CLAUDE.md, but there are instances in:

packages/testing/src/adapters/jest.ts:19-20

// eslint-disable-next-line @typescript-eslint/no-explicit-any
declare const expect: any;

Recommendation: Use proper type definition:

import type { Expect } from '@jest/expect';
declare const expect: Expect;

2. Unused Variables ⚠️

Per CLAUDE.md: "No unused variables: Remove or prefix with _"

Run: pnpm -C packages/testing lint to identify and fix these.

3. Missing Test Execution in CI

Ensure:

  • pnpm -C packages/testing test passes
  • pnpm -C examples/minimal test passes
  • These are added to the CI workflow

Medium Priority

4. Error Handling in LLM Providers

packages/testing/src/eval/llm/providers/anthropic.ts:92-100

Add validation after JSON parsing:

parsed = JSON.parse(cleanText);
if (\!parsed.criteria || \!Array.isArray(parsed.criteria) || \!parsed.overall) {
  throw new Error("Invalid evaluation response structure");
}

5. Test Client Timeout Cleanup

packages/testing/src/server/test-client.ts:42-68

Wrap in try-finally to prevent timer leaks:

try {
  const result = await Promise.race([...]);
  return toolResult;
} finally {
  if (timeoutId \!== undefined) {
    clearTimeout(timeoutId);
  }
}

6. Package.json Peer Dependency Version

Update peer dependency: "fast-check": "^3.0.0 || ^4.0.0" to support both versions.

@claude

claude Bot commented Jan 7, 2026

Copy link
Copy Markdown
Contributor

🎯 Adherence to CLAUDE.md Guidelines

Guideline Status Notes
Strict TypeScript, no any ⚠️ Minor violations 1-2 instances in jest adapter, needs fixing
No unused variables ✅ Likely pass Should verify with linter
Public API in index.ts ✅ Perfect Clean exports structure
Tests mirror source ✅ Excellent tests/unit/, tests/integration/ structure
Zod ^4.0.0 ✅ Correct Listed in dependencies
Node >= 20 ✅ Correct Specified in engines
80% coverage target ❓ Unknown Should run pnpm test:coverage

🔒 Security Assessment

Overall: GOOD

  • No credential leaking
  • Proper environment variable usage
  • No command injection vulnerabilities (child_process spawn with array args)
  • Client factory pattern prevents credential sharing
  • No secrets in code or examples

One consideration: External server startup (test-server.ts:151) passes through all env vars - generally fine for testing but be aware sensitive vars are passed to child processes.

📊 Performance Considerations

  • Lazy loading: Excellent choice - only loads dependencies when features are used
  • Client caching: Good pattern in createCachedClientFactory
  • Test parallelization: Consider documenting that tests can run in parallel if they use different ports

Recommendation: Add parallel test execution guidance to README.

@claude

claude Bot commented Jan 7, 2026

Copy link
Copy Markdown
Contributor

✅ Final Verdict

APPROVE with minor changes

This is high-quality work that significantly enhances the mcp-apps-kit ecosystem. The architecture is solid, documentation is excellent, and the API design is intuitive.

Required Changes:

  1. ✅ Fix any type usage in jest adapter
  2. ✅ Run linter and fix any unused variables
  3. ✅ Ensure tests pass and are added to CI

Recommended Changes:

  1. ⚠️ Add LLM response validation
  2. ⚠️ Fix timeout cleanup in try-finally
  3. ⚠️ Update fast-check peer dependency version range
  4. ⚠️ Increase default property test runs to 100

Nice-to-Have:

  1. 💡 Add parallel test execution guidance to README
  2. 💡 Document test coverage target and how to check it

Excellent work! This testing library will be a valuable addition to the project. The code quality is high and the developer experience is well thought out. 🎉

Files reviewed: 68 changed files with focus on:

  • Core architecture (types.ts, index.ts)
  • Test client/server implementation
  • LLM provider implementations
  • Error handling
  • Security patterns
  • Documentation quality

Low Priority Items:

  • Property testing runs (examples/minimal/tests/advanced-features.test.ts:322): Consider increasing from 5 to 100
  • Debug logger namespaces are consistent - keep following this pattern
  • Test environment cleanup looks good - just verify all await env.cleanup() calls are present

Suggested Future Additions (not blockers):

  1. Snapshot testing utilities
  2. Performance benchmarking
  3. Coverage reporting for tool calls
  4. Mock server for client testing
  5. Contract testing for API compatibility

@claude

claude Bot commented Jan 7, 2026

Copy link
Copy Markdown
Contributor

Code Review: Testing Library Implementation

This is an excellent PR that adds comprehensive testing capabilities to the MCP AppsKit. Overall score: 9.5/10

Key Strengths

  • Modular architecture with clean separation of concerns
  • Smart lazy loading pattern for optional dependencies
  • Framework agnostic core with Vitest/Jest adapters
  • Strong TypeScript type safety throughout
  • 17 test files with comprehensive coverage
  • 711-line detailed README with examples

Issues Found

1. Retry Logic Incomplete (Medium Priority)

Location: packages/testing/src/server/test-client.ts:37

Function is named callToolWithRetry but never retries on failure. Either implement the retry logic using the retries option or rename the function.

2. Timeout Memory Leak Risk (Minor)

Location: packages/testing/src/server/test-client.ts:47-63

Timeout may not be cleared if promise rejects for reasons other than timeout. Wrap in try/finally to ensure cleanup.

3. Port Collision Risk (Minor)

Location: packages/testing/src/server/test-server.ts:40-61

Race condition between finding available port and using it in app.start().

Recommendations

Before Merge (High Priority):

  1. Implement retry logic or remove misleading parameter
  2. Fix timeout cleanup to prevent memory leaks
  3. Verify test coverage meets 80% target

Post Merge (Medium Priority):
4. Add troubleshooting section to README
5. Consider connection pooling for performance
6. Update fast-check peer dependency to support v4

Conclusion

This is a production-ready, high-quality addition demonstrating excellent software engineering practices. The few issues identified are minor and don't block merging.

Recommendation: APPROVE with minor fixes

Great work! This testing library will significantly improve the developer experience for MCP application testing.


Reviewed by: Claude Code | 2026-01-07

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 14

🤖 Fix all issues with AI agents
In @examples/minimal/tests/greet-v1.test.ts:
- Line 17: Replace the hardcoded await new Promise(setTimeout...) by waiting for
an actual readiness signal: modify the test to either call a startTestServer
variant that resolves only when the server is listening (adjust or implement
startTestServer to resolve after server.listen callback), or poll a readiness
endpoint (e.g., GET /health) until it returns success with a timeout, or add
retry logic around the first client call (with exponential backoff and overall
timeout); update the test in greet-v1.test.ts to use startTestServer (or the new
startTestServerReady) or the polling helper instead of the 100ms sleep so the
test proceeds only once the server is truly ready.

In @examples/minimal/tests/greet-v2.test.ts:
- Around line 14-31: The test uses an arbitrary delay after calling
startTestServer — remove the setTimeout and make startTestServer guarantee
readiness before resolving (update the startTestServer implementation to resolve
only after the server's listen/ready callback completes), then keep the test
using startTestServer(...) and createTestClient(...) without the delay; if you
prefer not to change startTestServer, implement a small waitForServer(url) retry
helper and call await waitForServer(`http://localhost:${testPort}/v2/mcp`) after
startTestServer instead of using setTimeout.

In @examples/minimal/tests/integration.test.ts:
- Around line 1-85: This test file (the one containing describe("Minimal Example
Integration") and importing app) must be relocated into the project's
tests/integration test folder and renamed to follow conventions (e.g.,
minimal-example.test.ts or integration.test.ts); move the file contents into
that new test directory, update any relative imports if they break (the import
of app and testing helpers), and ensure the test runner configuration includes
the tests/integration folder so the moved test is discovered.

In @examples/minimal/tests/integration/versioning.test.ts:
- Line 17: The cast "as unknown" on the app passed to startTestServer defeats
type safety; remove the unnecessary cast and ensure app is typed to match
startTestServer's parameter (App | ExternalServerOptions). Locate the call to
startTestServer (symbol: startTestServer) and the app variable (symbol: app),
then either declare/annotate app as App or adapt the argument to the expected
union (e.g., pass an ExternalServerOptions object or wrap app appropriately) so
the type error is resolved without using "as unknown" before assigning to
mainServer.

In @examples/minimal/tests/ui-widget.test.tsx:
- Line 11: The import list includes an unused symbol "fireEvent" which should be
removed to clean up the test file; update the import statement that currently
imports render, screen, waitFor, fireEvent from "@testing-library/react" to drop
"fireEvent" so only the used imports remain (e.g., render, screen, waitFor).

In @packages/testing/package.json:
- Around line 63-91: Update the overly restrictive peerDependencies in
package.json: change the "openai" constraint from ^4.0.0 to ^6.0.0, "fast-check"
from ^3.0.0 to ^4.0.0, and "jest" from ^29.0.0 to ^30.0.0 while leaving "zod"
(^4.0.0) and "vitest" (^3 || ^4) as-is; ensure these edits are made under the
"peerDependencies" section so consumers can install current major versions.

In @packages/testing/src/adapters/jest.ts:
- Around line 7-16: Replace the dynamic type import for ZodType with the
standard direct type import used elsewhere: change the line declaring `type
ZodType = import("zod").ZodType;` to a direct type import `import type { ZodType
} from "zod";`, ensuring the `ZodType` symbol is imported in the same style as
other matcher/type files to maintain consistency.

In @packages/testing/src/eval/behavior/runner.ts:
- Around line 61-79: The passed++ is incremented before suite.afterEach runs
inside the finally, so if suite.afterEach throws the test is incorrectly counted
as passed; move the passed++ increment to after the finally completes (i.e.,
after running/awaiting suite.afterEach) or wrap the suite.afterEach call in its
own try-catch and only increment passed++ when no errors occur; adjust
references around runTestCase, the try/catch/finally block, the suite.afterEach
invocation, and the passed/failed counters accordingly so passed++ occurs only
after afterEach succeeds.

In @packages/testing/src/eval/llm/evaluator.ts:
- Around line 38-40: The switch default currently throws a generic Error for
unsupported providers (throw new Error(`Unsupported provider:
${config.provider}`)); replace this with the project's custom error type (e.g.,
import and throw ConfigurationError or create/throw UnsupportedProviderError) so
the code uses the consistent error taxonomy; update the import list to include
the chosen error class and throw new ConfigurationError(`Unsupported provider:
${config.provider}`) (or new UnsupportedProviderError(...)) in place of the
generic Error, keeping the message intact to preserve context.

In @packages/testing/src/eval/llm/providers/anthropic.ts:
- Around line 179-183: The inner variable named result in the
parsed.criteria.find callback shadows the outer result parameter of evaluate(),
so rename the inner variable to a non-conflicting name (e.g., parsedCriterion or
parsedItem) inside the options.criteria.map block to avoid shadowing; update
references in the callback and downstream use in the creation of
CriterionResult[] so the logic and types (CriterionResult[],
parsed.criteria.find, options.criteria.map) remain identical but without name
collision.

In @packages/testing/src/utils/lazy-loader.ts:
- Around line 138-147: The pendingPromise created in the lazy loader (the block
that sets pendingPromise = createClient(apiKey).then(...)) lacks rejection
handling so a failed createClient leaves pendingPromise set and subsequent calls
reuse the rejected promise; fix by attaching a .catch handler to that promise
which clears pendingPromise (and also resets cachedClient and cachedApiKey if
appropriate) before rethrowing the error so state is clean for retries; update
the promise chain around createClient, pendingPromise, cachedClient, and
cachedApiKey accordingly.

In @packages/testing/tests/integration/behavior-testing.test.ts:
- Line 8: The import line currently imports beforeEach from vitest but that
symbol is never used and is shadowed by local constants named beforeEach in this
test file; remove beforeEach from the named imports in the vitest import
statement (i.e., change `import { describe, it, expect, vi, beforeEach } from
"vitest";` to exclude beforeEach) so the code no longer imports an unused symbol
while leaving the other imports intact.

In @packages/testing/tests/integration/property-testing.test.ts:
- Around line 13-35: The test currently swallows the absence of fast-check by
returning inside a try-catch; instead detect availability of fast-check up front
and use Vitest's conditional skip (e.g., test.skip or conditionally call it) so
the test is explicitly skipped when missing; specifically, replace the try-catch
around generators.string and forAllInputs with a pre-check (attempt to
require/import fast-check or check that generators and forAllInputs are
present), and if unavailable call the test skip mechanism before executing the
assertions, and apply the same change to the second test that also wraps
fast-check usage (the block using generators.string and forAllInputs).

In @packages/testing/tests/unit/eval/evaluator.test.ts:
- Around line 1-11: The test file must be moved to mirror the source's nested
eval/llm structure under the unit tests (place it in the tests/unit location
matching the src eval llm hierarchy) and then update the module imports so they
resolve from the new relative location (ensure imports referencing
createLLMEvaluator, criteria and ConfigurationError point to the correct
relative paths after the move); keep the test logic unchanged, only relocate the
file and fix the import paths.
🧹 Nitpick comments (27)
examples/minimal/tests/ui-widget.test.tsx (2)

1-251: Reorganize test file to follow subdirectory structure.

Per coding guidelines, test files should be organized under tests/ with unit/, integration/, and contract/ subdirectories. This UI widget test file should be moved to an appropriate subdirectory, likely tests/integration/ui-widget.test.tsx since it tests React components with mocked dependencies.

Based on learnings, tests should mirror the source structure within these subdirectories.


176-251: Consider adding comprehensive test coverage for V2 to match V1.

The GreetingWidgetV2 test suite has fewer test cases than V1. Consider adding tests for:

  • Error handling on greet failure (similar to V1 lines 140-156)
  • Cancel button functionality (similar to V1 lines 125-138)
  • Unwrapped result format support (similar to V1 lines 76-86)
  • Theme application (similar to V1 lines 166-173)

These scenarios would ensure both widgets have equivalent test coverage for shared functionality.

examples/minimal/tests/advanced-features.test.ts (2)

316-337: Property test predicate uses type assertion.

The type assertion on line 332 (result.structuredContent as { message: string } | undefined) is acceptable given the external API, but consider using a type guard or Zod schema validation for stronger type safety in property tests.

💡 Optional: Use schema validation for type-safe access
+import { z } from "zod";
+
+const greetResultSchema = z.object({ message: z.string() });
+
 await forAllInputs(
   generators.string({ minLength: 1, maxLength: 20 }),
   async (name: string) => {
     if (name.trim().length === 0) {
       return true;
     }
     const result = await env.client.callTool("greet", { name });
     if (result.isError) {
       return false;
     }
-    const data = result.structuredContent as { message: string } | undefined;
-    return typeof data?.message === "string" && data.message.includes(name);
+    const parsed = greetResultSchema.safeParse(result.structuredContent);
+    return parsed.success && parsed.data.message.includes(name);
   },
   { numRuns: 5 }
 );

391-448: Consider server readiness checks instead of fixed delays.

The setTimeout(resolve, 100) pattern (lines 394, 420, 435) for waiting on server startup can be flaky under load. While acceptable for examples, consider using a readiness check or the createTestEnvironment helper which handles this internally.

packages/testing/tests/unit/utils/lazy-loader.test.ts (1)

103-168: Good test coverage, consider adding failure scenario test.

The tests cover the happy paths well. Consider adding a test for when createClient rejects to verify error handling behavior (especially relevant given the issue identified in the implementation).

💡 Optional: Add test for client creation failure
it("should allow retry after client creation fails", async () => {
  const mockClient = { id: "client1" };
  const createClient = vi
    .fn()
    .mockRejectedValueOnce(new Error("Network error"))
    .mockResolvedValueOnce(mockClient);

  const factory = createCachedClientFactory(createClient);

  // First call fails
  await expect(factory.get("api-key-123")).rejects.toThrow("Network error");

  // Second call should retry and succeed
  const result = await factory.get("api-key-123");
  expect(result).toBe(mockClient);
  expect(createClient).toHaveBeenCalledTimes(2);
});
packages/testing/tests/unit/ui/test-environment.test.ts (1)

22-28: Consider verifying the fluent API actually applies configuration.

The test confirms the fluent API returns a builder instance, but doesn't verify that withServerUrl and withClientOptions actually store the configuration. While this is acceptable for a unit test focused on API structure, consider adding assertions or integration tests that verify the built environment reflects the configured values.

💡 Enhanced test example
  it("should support fluent API", () => {
    const builder = new TestEnvironmentBuilder()
      .withServerUrl("http://localhost:3000")
      .withClientOptions({ timeout: 5000 });

    expect(builder).toBeDefined();
+   // Could verify internal state or add integration test
+   // that builds and checks the resulting environment
  });
packages/testing/tests/unit/server/test-client.test.ts (1)

31-53: Strengthen option tests to verify actual TestClient behavior.

The current option tests only verify that plain objects can have properties assigned, but they never pass these options to createTestClient or verify that the options affect behavior. This means bugs where options are silently ignored would not be caught.

Consider either:

  1. Moving these to integration tests where you can verify the options actually work with a test server, or
  2. Enhancing these tests to verify the options are at least accepted by createTestClient (even if the full behavior can't be tested without a server)
Example: Verify options are accepted
  describe("options", () => {
-   it("should accept trackHistory option", async () => {
-     // We can't fully test this without a server, but we can verify the option is accepted
-     const options = { trackHistory: true };
-     expect(options.trackHistory).toBe(true);
-   });
+   it("should accept trackHistory option", async () => {
+     // Verify the function accepts the option without throwing
+     await expect(
+       createTestClient("http://localhost:59998/mcp", { trackHistory: true })
+     ).rejects.toThrow(ConnectionError); // Expected to fail connection, but option should be accepted
+   });

-   it("should accept timeout option", async () => {
-     const options = { timeout: 5000 };
-     expect(options.timeout).toBe(5000);
-   });
+   it("should accept timeout option", async () => {
+     await expect(
+       createTestClient("http://localhost:59998/mcp", { timeout: 5000 })
+     ).rejects.toThrow(ConnectionError);
+   });

-   it("should accept retries option", async () => {
-     const options = { retries: 3 };
-     expect(options.retries).toBe(3);
-   });
+   it("should accept retries option", async () => {
+     await expect(
+       createTestClient("http://localhost:59998/mcp", { retries: 3 })
+     ).rejects.toThrow(ConnectionError);
+   });

    it("should have default timeout of 30000ms", () => {
-     // Verify the default from types
-     const defaultOptions = { timeout: 30000 };
-     expect(defaultOptions.timeout).toBe(30000);
+     // This can only be properly verified with a real server in integration tests
+     // For now, we document the expected default
+     expect(30000).toBe(30000);
    });
  });
packages/testing/tests/contract/test-server.test.ts (1)

20-28: Consider removing empty placeholder tests or converting them to TODOs.

These placeholder tests currently don't validate anything and will always pass. Consider either:

  1. Implementing them as part of this PR (if integration tests are ready)
  2. Removing them and tracking the work elsewhere (e.g., GitHub issue)
  3. Using it.todo() to mark them as pending work
♻️ Suggested refactor using Vitest's todo feature
-  it("createTestClient should return a TestClient", async () => {
-    // This test requires a running server
-    // Will be implemented with integration tests
-  });
+  it.todo("createTestClient should return a TestClient when connected to a running server");

-  it("startTestServer should return a TestServer", async () => {
-    // This test requires @mcp-apps-kit/core or external server
-    // Will be implemented with integration tests
-  });
+  it.todo("startTestServer should return a TestServer when starting an app or external process");
packages/testing/tests/unit/eval/generators.test.ts (1)

11-19: Consider using dynamic import for consistency.

The current implementation uses require() to detect fast-check availability, which is inconsistent with the ESM pattern used throughout the rest of the codebase (e.g., await import("zod") in other test files).

♻️ Refactor to use dynamic import pattern
 // Detect whether fast-check is available
 let isFastCheckAvailable = false;
 try {
-  // eslint-disable-next-line @typescript-eslint/no-require-imports
-  require("fast-check");
+  await import("fast-check");
   isFastCheckAvailable = true;
 } catch {
   isFastCheckAvailable = false;
 }

Note: This would require wrapping the detection in an async IIFE or moving it into a beforeAll hook:

let isFastCheckAvailable = false;

beforeAll(async () => {
  try {
    await import("fast-check");
    isFastCheckAvailable = true;
  } catch {
    isFastCheckAvailable = false;
  }
});
packages/testing/tests/unit/eval/evaluator.test.ts (1)

13-31: Consider using Vitest lifecycle hooks for environment isolation.

The test directly manipulates process.env.OPENAI_API_KEY within a try-finally block. While the current approach works, using Vitest's beforeEach/afterEach hooks would provide better test isolation and make the intent clearer.

♻️ Optional refactor using lifecycle hooks
 describe("createLLMEvaluator", () => {
+  let originalKey: string | undefined;
+
+  beforeEach(() => {
+    originalKey = process.env.OPENAI_API_KEY;
+  });
+
+  afterEach(() => {
+    if (originalKey) {
+      process.env.OPENAI_API_KEY = originalKey;
+    } else {
+      delete process.env.OPENAI_API_KEY;
+    }
+  });
+
   it("should throw ConfigurationError if API key is not set", () => {
-    // Temporarily remove API key
-    const originalKey = process.env.OPENAI_API_KEY;
     delete process.env.OPENAI_API_KEY;
-
-    try {
-      expect(() => {
-        createLLMEvaluator({
-          provider: "openai",
-          model: "gpt-4o-mini",
-        });
-      }).toThrow(ConfigurationError);
-    } finally {
-      // Restore API key
-      if (originalKey) {
-        process.env.OPENAI_API_KEY = originalKey;
-      }
-    }
+
+    expect(() => {
+      createLLMEvaluator({
+        provider: "openai",
+        model: "gpt-4o-mini",
+      });
+    }).toThrow(ConfigurationError);
   });
packages/testing/tests/unit/ui/mock-host.test.ts (1)

8-53: Extract repetitive try-catch pattern into a test helper.

All three tests use identical try-catch blocks to handle missing @mcp-apps-kit/ui dependency. This repetition reduces maintainability and clarity. Consider extracting this pattern into a helper function or using Vitest's conditional test features.

♻️ Proposed refactor using a helper function
+/**
+ * Helper to run tests that depend on @mcp-apps-kit/ui
+ * Skips gracefully if the dependency is not available
+ */
+function testWithUI(name: string, fn: () => void | Promise<void>) {
+  it(name, async () => {
+    try {
+      await fn();
+    } catch (error) {
+      if (error instanceof Error && error.message.includes("@mcp-apps-kit/ui")) {
+        // Test skipped - dependency not available
+        return;
+      }
+      throw error;
+    }
+  });
+}
+
 describe("createMockHost", () => {
-  it("should create a mock host with default options", () => {
-    // This test requires @mcp-apps-kit/ui to be installed
-    try {
-      const host = createMockHost();
-      expect(host).toBeDefined();
-      expect(typeof host.emitToolResult).toBe("function");
-      expect(typeof host.getToolCallHistory).toBe("function");
-    } catch (error) {
-      // Skip test if @mcp-apps-kit/ui is not available
-      if (error instanceof Error && error.message.includes("@mcp-apps-kit/ui")) {
-        // Test skipped - dependency not available
-        return;
-      }
-      throw error;
-    }
+  testWithUI("should create a mock host with default options", () => {
+    const host = createMockHost();
+    expect(host).toBeDefined();
+    expect(typeof host.emitToolResult).toBe("function");
+    expect(typeof host.getToolCallHistory).toBe("function");
   });
 
-  it("should track tool call history", () => {
-    // This test requires @mcp-apps-kit/ui to be installed
-    try {
-      const host = createMockHost();
-      const history = host.getToolCallHistory();
-      expect(Array.isArray(history)).toBe(true);
-    } catch (error) {
-      if (error instanceof Error && error.message.includes("@mcp-apps-kit/ui")) {
-        return;
-      }
-      throw error;
-    }
+  testWithUI("should track tool call history", () => {
+    const host = createMockHost();
+    const history = host.getToolCallHistory();
+    expect(Array.isArray(history)).toBe(true);
   });
 
-  it("should clear history", () => {
-    // This test requires @mcp-apps-kit/ui to be installed
-    try {
-      const host = createMockHost();
-      host.clearHistory();
-      expect(host.getToolCallHistory()).toHaveLength(0);
-    } catch (error) {
-      if (error instanceof Error && error.message.includes("@mcp-apps-kit/ui")) {
-        return;
-      }
-      throw error;
-    }
+  testWithUI("should clear history", () => {
+    const host = createMockHost();
+    host.clearHistory();
+    expect(host.getToolCallHistory()).toHaveLength(0);
   });
 });
packages/testing/src/eval/llm/evaluator.ts (1)

29-29: Use the LLMProvider interface type directly instead of ReturnType.

The type annotation ReturnType<typeof createOpenAIProvider> is indirect and assumes both providers return compatible types. Since both createOpenAIProvider and createAnthropicProvider should implement the same LLMProvider interface, use that type explicitly for clarity and to ensure consistency if provider signatures diverge in the future.

♻️ Proposed refactor
-  let provider: ReturnType<typeof createOpenAIProvider>;
+  let provider: LLMProvider;

You'll need to import the LLMProvider type:

-import type { LLMEvaluator, LLMEvaluatorConfig } from "../../types";
+import type { LLMEvaluator, LLMEvaluatorConfig, LLMProvider } from "../../types";
packages/testing/tests/integration/vitest-integration.test.ts (1)

29-38: Consider static import for Zod.

The dynamic import of Zod on line 30 is unusual since Zod is listed as a dependency in the library context. Unless there's a specific reason for lazy-loading (e.g., testing optional dependency behavior), a static import at the top of the file would be more conventional and clearer.

♻️ Proposed refactor (if no lazy-loading needed)
 import { describe, it, expect, beforeAll } from "vitest";
 import { setupVitestMatchers } from "../../src/adapters/vitest";
 import type { ToolResult } from "../../src/types";
+import { z } from "zod";

 // ... rest of file ...

   it("should work with toMatchToolSchema", async () => {
-    const { z } = await import("zod");
     const result: ToolResult = {
       content: [{ type: "text", text: '{"message":"Hello"}' }],
       isError: false,
     };
     const schema = z.object({ message: z.string() });

     expect(result).toMatchToolSchema(schema);
   });
examples/minimal/tests/greet-v2.test.ts (1)

38-47: Validate structuredContent with a schema instead of type assertion.

The type assertion as { message: string; fullName: string } on line 44 bypasses TypeScript's type safety. Since you're already using Zod in this file (line 64-68), validate the structure with a schema to ensure the data actually conforms to expectations at runtime.

♻️ Proposed refactor

Define the schema once at the module level:

+const GreetResponseSchema = z.object({
+  message: z.string(),
+  fullName: z.string(),
+  timestamp: z.string(),
+});
+
 describe("Greet Tool V2", () => {

Then use it to validate instead of asserting:

     it("should greet a user by name only", async () => {
       const result = await env.client.callTool("greet", { name: "Alice" });

       expectToolResult(result).toHaveNoError();

-      // Use structuredContent for typed data assertions
-      const data = result.structuredContent as { message: string; fullName: string };
+      // Validate and parse structuredContent
+      const data = GreetResponseSchema.parse(result.structuredContent);
       expect(data.message).toContain("Alice");
       expect(data.fullName).toBe("Alice");
     });

Apply the same pattern to lines 58, 85, and 91.

examples/minimal/tests/integration.test.ts (2)

16-16: Remove artificial delay from test setup.

The 100ms timeout appears unnecessary and slows down test execution. The test client has built-in retry and timeout mechanisms (configured on line 20-21) that should handle connection timing without explicit delays.

⚡ Proposed fix to remove unnecessary delay
     const server = await startTestServer(app, { port: testPort });
-    await new Promise((resolve) => setTimeout(resolve, 100));

     const client = await createTestClient(`http://localhost:${testPort}/v1/mcp`, {

80-80: Avoid type assertion that bypasses type safety.

The as assertion silences TypeScript's validation of required fields. Consider using a test helper that creates invalid inputs explicitly, or use expect().rejects pattern without the assertion.

🔒 Proposed fix using proper error testing
   it("should handle errors gracefully", async () => {
-    // Test with invalid input (missing required field)
-    try {
-      await env.client.callTool("greet", {} as { name: string });
-    } catch (error) {
-      expect(error).toBeDefined();
-    }
+    // Test with invalid input (missing required field)
+    await expect(
+      env.client.callTool("greet", {})
+    ).rejects.toBeDefined();
   });
packages/testing/src/matchers/resource.ts (1)

156-184: Consider clarifying null/undefined equivalence behavior.

Lines 159-161 treat null and undefined as equivalent in matching. While this may be intentional for flexibility, it could mask bugs where the distinction between null and undefined matters (e.g., explicit null in JSON vs. missing field). Consider documenting this behavior or making it stricter if the distinction is important for your use cases.

packages/testing/src/eval/llm/providers/anthropic.ts (1)

194-196: Consider making the overall pass threshold configurable.

The overall pass threshold is hardcoded to 0.7, while individual criterion thresholds are configurable via criterion.threshold. This inconsistency may surprise users who expect to control both thresholds. Consider adding an overallThreshold option to EvalOptions.

packages/testing/tests/unit/server/test-server.test.ts (1)

129-144: Test doesn't verify actual behavior.

This test only validates the options object structure but never calls startTestServer with these options. It doesn't actually test that environment variables are passed through to the external command. Consider either:

  1. Adding a real integration test that verifies env var passthrough
  2. Removing this test if it's not providing value
  3. At minimum, documenting why this limited validation is sufficient
packages/testing/src/server/test-client.ts (1)

72-82: Local type shadows imported ContentBlock type.

Line 73 defines a local ContentBlock type that shadows the ContentBlock type imported from ../types (via the ToolResult type). While this works because TypeScript resolves to the local scope, it's confusing to have two different ContentBlock types. Consider renaming the local type or using the imported type directly.

♻️ Proposed fix
       // Build content blocks from the result
-      type ContentBlock = { type: string; text?: string; data?: string; mimeType?: string };
-      const contentBlocks = (result.content ?? []).map((block: ContentBlock) => {
+      type RawContentBlock = { type: string; text?: string; data?: string; mimeType?: string };
+      const contentBlocks = (result.content ?? []).map((block: RawContentBlock) => {
         if (block.type === "text") {
           return { type: "text" as const, text: block.text };
         }
packages/testing/src/eval/behavior/matchers.ts (1)

189-192: Consider documenting the null/undefined equivalence behavior.

The function treats null and undefined as equivalent (both match each other). This is reasonable for a flexible "partial match" semantic, but could surprise users expecting strict equality. Consider adding a JSDoc note about this behavior.

packages/testing/src/server/test-server.ts (1)

212-228: Potential double-resolve in stop() method.

If the process exits normally after SIGTERM but before the 5-second timeout, both the exit listener (line 220) and the timeout callback (line 225) may call resolve(). While calling resolve() twice on a Promise is harmless (subsequent calls are ignored), this could be cleaner.

♻️ Suggested improvement
     async stop(): Promise<void> {
       serverLogger("Stopping external server");
       return new Promise((resolve) => {
         if (childProcess.killed) {
           resolve();
           return;
         }
+        let resolved = false;
         childProcess.once("exit", () => {
+          if (!resolved) {
+            resolved = true;
             resolve();
+          }
         });
         childProcess.kill("SIGTERM");
         setTimeout(() => {
-          if (!childProcess.killed) childProcess.kill("SIGKILL");
-          resolve();
+          if (!resolved) {
+            resolved = true;
+            if (!childProcess.killed) childProcess.kill("SIGKILL");
+            resolve();
+          }
         }, 5000);
       });
     },
packages/testing/src/ui/test-environment.ts (2)

12-16: Consider extracting the shared App interface.

This App interface is duplicated from test-server.ts (same fields minus stop). Consider extracting it to a shared location to avoid drift.


50-51: Arbitrary delay may cause flaky tests.

The 100ms sleep after startTestServer is fragile. Since startTestServer already handles startup completion via its timeout mechanism, this additional delay shouldn't be necessary. If it is needed, consider increasing the default timeout in startTestServer or using a more robust readiness check.

♻️ Consider removing the arbitrary delay
     server = await startTestServer(options.app as App, { port });
-
-    // Wait a bit for server to be ready
-    await new Promise((resolve) => setTimeout(resolve, 100));
   } else if (options.serverUrl) {
packages/testing/src/eval/llm/providers/openai.ts (1)

85-88: Consider validating the parsed JSON structure.

The response is cast to a specific shape without runtime validation. If the LLM returns valid JSON with an unexpected structure (e.g., missing criteria or overall fields), this will throw cryptic errors at lines 93-94 or 106.

♻️ Add defensive checks or Zod validation
       // Parse response
-      const parsed = JSON.parse(content) as {
-        criteria: Array<{ name: string; score: number; explanation: string }>;
-        overall: { score: number };
-      };
+      const parsed = JSON.parse(content) as Record<string, unknown>;
+      
+      if (!parsed.criteria || !Array.isArray(parsed.criteria)) {
+        throw new Error("Invalid response structure: missing criteria array");
+      }
+      if (!parsed.overall || typeof (parsed.overall as Record<string, unknown>).score !== "number") {
+        throw new Error("Invalid response structure: missing overall.score");
+      }
+      
+      const typedParsed = parsed as {
+        criteria: Array<{ name: string; score: number; explanation: string }>;
+        overall: { score: number };
+      };
packages/testing/src/adapters/vitest.ts (1)

15-16: Consider using standard import type syntax.

The inline type ZodType = import("zod").ZodType works but is unconventional. A regular import type { ZodType } from "zod" at the top of the file would be clearer.

♻️ Use standard import type
 import type { ToolResult } from "../types";
+import type { ZodType } from "zod";
 
-// ZodType is the modern replacement for ZodSchema in Zod v4+
-type ZodType = import("zod").ZodType;
packages/testing/src/types.ts (1)

17-22: Consider using a discriminated union for ContentBlock.

The current interface allows combinations that may be invalid (e.g., type: "text" with data and mimeType but no text). A discriminated union would provide stronger type safety.

♻️ Alternative with discriminated union
export type ContentBlock =
  | { type: "text"; text: string }
  | { type: "image"; data: string; mimeType: string }
  | { type: "resource"; data?: string; mimeType?: string };

This is optional and depends on whether the MCP SDK's actual types support this level of strictness.

beforeAll(async () => {
const testPort = 3001;
const server = await startTestServer(app, { port: testPort });
await new Promise((resolve) => setTimeout(resolve, 100));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Replace hardcoded sleep with proper readiness check.

The hardcoded 100ms sleep after starting the test server is a test smell that can lead to flaky tests. If the server takes longer to start (e.g., under load or on slower CI machines), tests may fail intermittently.

Recommended approaches

Consider one of these alternatives:

  1. Add a readiness endpoint to the server and poll it
  2. Have startTestServer return only when the server is ready to accept connections
  3. Add retry logic to the first client operation with appropriate timeout
  beforeAll(async () => {
    const testPort = 3001;
    const server = await startTestServer(app, { port: testPort });
-   await new Promise((resolve) => setTimeout(resolve, 100));
+   // startTestServer should already ensure server is ready,
+   // or implement a readiness check here

    const client = await createTestClient(`http://localhost:${testPort}/v1/mcp`, {
      trackHistory: true,
    });

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In @examples/minimal/tests/greet-v1.test.ts at line 17, Replace the hardcoded
await new Promise(setTimeout...) by waiting for an actual readiness signal:
modify the test to either call a startTestServer variant that resolves only when
the server is listening (adjust or implement startTestServer to resolve after
server.listen callback), or poll a readiness endpoint (e.g., GET /health) until
it returns success with a timeout, or add retry logic around the first client
call (with exponential backoff and overall timeout); update the test in
greet-v1.test.ts to use startTestServer (or the new startTestServerReady) or the
polling helper instead of the 100ms sleep so the test proceeds only once the
server is truly ready.

Comment on lines +14 to +31
beforeAll(async () => {
const testPort = 3002;
const server = await startTestServer(app, { port: testPort });
await new Promise((resolve) => setTimeout(resolve, 100));

const client = await createTestClient(`http://localhost:${testPort}/v2/mcp`, {
trackHistory: true,
});

env = {
server,
client,
async cleanup() {
await client.disconnect();
await server.stop();
},
};
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Remove the arbitrary delay after server startup.

The setTimeout on line 17 suggests the server may not be fully ready immediately after startTestServer resolves. If startTestServer doesn't wait for the server to be ready, this should be fixed in the test server implementation rather than worked around with an arbitrary delay. Arbitrary delays make tests slower and can still cause flakiness if the delay is insufficient under load.

🔧 Recommended approach

If startTestServer doesn't guarantee server readiness, verify in the test server implementation (packages/testing/src/server/test-server.ts) that it waits for the server to start listening before resolving. Then remove the setTimeout:

   beforeAll(async () => {
     const testPort = 3002;
     const server = await startTestServer(app, { port: testPort });
-    await new Promise((resolve) => setTimeout(resolve, 100));

     const client = await createTestClient(`http://localhost:${testPort}/v2/mcp`, {
       trackHistory: true,
     });

Alternatively, if there's a genuine need to wait, add a readiness check:

const server = await startTestServer(app, { port: testPort });
// Wait for server to be ready by attempting connection with retry
await waitForServer(`http://localhost:${testPort}/v2/mcp`);
🤖 Prompt for AI Agents
In @examples/minimal/tests/greet-v2.test.ts around lines 14 - 31, The test uses
an arbitrary delay after calling startTestServer — remove the setTimeout and
make startTestServer guarantee readiness before resolving (update the
startTestServer implementation to resolve only after the server's listen/ready
callback completes), then keep the test using startTestServer(...) and
createTestClient(...) without the delay; if you prefer not to change
startTestServer, implement a small waitForServer(url) retry helper and call
await waitForServer(`http://localhost:${testPort}/v2/mcp`) after startTestServer
instead of using setTimeout.

Comment on lines +1 to +85
/**
* Integration tests for minimal example
*/

import { describe, it, expect, beforeAll, afterAll } from "vitest";
import { expectToolResult, startTestServer, createTestClient } from "@mcp-apps-kit/testing";
import type { TestEnvironment } from "@mcp-apps-kit/testing";
import { app } from "../src/index.js";

describe("Minimal Example Integration", () => {
let env: TestEnvironment;

beforeAll(async () => {
const testPort = 3004;
const server = await startTestServer(app, { port: testPort });
await new Promise((resolve) => setTimeout(resolve, 100));

const client = await createTestClient(`http://localhost:${testPort}/v1/mcp`, {
trackHistory: true,
timeout: 10000,
});

env = {
server,
client,
async cleanup() {
await client.disconnect();
await server.stop();
},
};
});

afterAll(async () => {
await env.cleanup();
});

it("should start server and connect client", () => {
expect(env.server).toBeDefined();
expect(env.client).toBeDefined();
expect(env.server.url).toBeTruthy();
});

it("should list available tools", async () => {
const tools = await env.client.listTools();
expect(tools.length).toBeGreaterThan(0);
expect(tools.some((t) => t.name === "greet")).toBe(true);
});

it("should track call history when enabled", async () => {
env.client.clearHistory();

await env.client.callTool("greet", { name: "HistoryTest" });

const history = env.client.getCallHistory();
expect(history.length).toBe(1);
expect(history[0]?.name).toBe("greet");
expect(history[0]?.args).toEqual({ name: "HistoryTest" });
});

it("should handle multiple sequential calls", async () => {
env.client.clearHistory();

const result1 = await env.client.callTool("greet", { name: "First" });
const result2 = await env.client.callTool("greet", { name: "Second" });
const result3 = await env.client.callTool("greet", { name: "Third" });

const results = [result1, result2, result3];
expect(results).toHaveLength(3);
results.forEach((result) => {
expectToolResult(result).toHaveNoError();
});

const history = env.client.getCallHistory();
expect(history.length).toBe(3);
});

it("should handle errors gracefully", async () => {
// Test with invalid input (missing required field)
try {
await env.client.callTool("greet", {} as { name: string });
} catch (error) {
expect(error).toBeDefined();
}
});
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Relocate test to follow project structure guidelines.

Based on coding guidelines, test files should mirror source structure with subdirectories for unit/, integration/, and contract/ tests. This integration test should be moved to tests/integration/integration.test.ts or tests/integration/minimal-example.test.ts.

As per coding guidelines, test files in tests/ should be organized in subdirectories.

🤖 Prompt for AI Agents
In @examples/minimal/tests/integration.test.ts around lines 1 - 85, This test
file (the one containing describe("Minimal Example Integration") and importing
app) must be relocated into the project's tests/integration test folder and
renamed to follow conventions (e.g., minimal-example.test.ts or
integration.test.ts); move the file contents into that new test directory,
update any relative imports if they break (the import of app and testing
helpers), and ensure the test runner configuration includes the
tests/integration folder so the moved test is discovered.


beforeAll(async () => {
const testPort = 3003;
mainServer = await startTestServer(app as unknown, { port: testPort });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Investigate the as unknown type cast.

The cast to unknown defeats type safety. Since startTestServer accepts App | ExternalServerOptions according to its signature, and app should be of type App, this cast appears unnecessary. If there's a type mismatch, it should be resolved properly rather than worked around with a cast to unknown.

♻️ Suggested fix
-mainServer = await startTestServer(app as unknown, { port: testPort });
+mainServer = await startTestServer(app, { port: testPort });

If this causes a type error, the underlying type mismatch should be resolved rather than hidden with a cast.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
mainServer = await startTestServer(app as unknown, { port: testPort });
mainServer = await startTestServer(app, { port: testPort });
🤖 Prompt for AI Agents
In @examples/minimal/tests/integration/versioning.test.ts at line 17, The cast
"as unknown" on the app passed to startTestServer defeats type safety; remove
the unnecessary cast and ensure app is typed to match startTestServer's
parameter (App | ExternalServerOptions). Locate the call to startTestServer
(symbol: startTestServer) and the app variable (symbol: app), then either
declare/annotate app as App or adapt the argument to the expected union (e.g.,
pass an ExternalServerOptions object or wrap app appropriately) so the type
error is resolved without using "as unknown" before assigning to mainServer.

*/

import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, screen, waitFor, fireEvent } from "@testing-library/react";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Remove unused import fireEvent.

The fireEvent import is not used anywhere in this test file. All user interactions are handled via userEvent instead.

🧹 Proposed fix
-import { render, screen, waitFor, fireEvent } from "@testing-library/react";
+import { render, screen, waitFor } from "@testing-library/react";
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { render, screen, waitFor, fireEvent } from "@testing-library/react";
import { render, screen, waitFor } from "@testing-library/react";
🤖 Prompt for AI Agents
In @examples/minimal/tests/ui-widget.test.tsx at line 11, The import list
includes an unused symbol "fireEvent" which should be removed to clean up the
test file; update the import statement that currently imports render, screen,
waitFor, fireEvent from "@testing-library/react" to drop "fireEvent" so only the
used imports remain (e.g., render, screen, waitFor).

Comment on lines +179 to +183
const criteriaResults: CriterionResult[] = options.criteria.map((criterion) => {
const result = parsed.criteria.find((c) => c.name === criterion.name);
if (!result) {
throw new Error(`Missing evaluation result for criterion: ${criterion.name}`);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Variable shadowing: result shadows outer parameter.

The variable result in the find callback shadows the outer result parameter passed to evaluate(). This is confusing and could lead to bugs if the wrong variable is referenced. Consider renaming the inner variable.

🐛 Proposed fix
       const criteriaResults: CriterionResult[] = options.criteria.map((criterion) => {
-        const result = parsed.criteria.find((c) => c.name === criterion.name);
-        if (!result) {
+        const criterionMatch = parsed.criteria.find((c) => c.name === criterion.name);
+        if (!criterionMatch) {
           throw new Error(`Missing evaluation result for criterion: ${criterion.name}`);
         }
 
         const threshold = criterion.threshold ?? 0.7;
         return {
           name: criterion.name,
-          score: result.score,
-          pass: result.score >= threshold,
-          explanation: result.explanation,
+          score: criterionMatch.score,
+          pass: criterionMatch.score >= threshold,
+          explanation: criterionMatch.explanation,
         };
       });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const criteriaResults: CriterionResult[] = options.criteria.map((criterion) => {
const result = parsed.criteria.find((c) => c.name === criterion.name);
if (!result) {
throw new Error(`Missing evaluation result for criterion: ${criterion.name}`);
}
const criteriaResults: CriterionResult[] = options.criteria.map((criterion) => {
const criterionMatch = parsed.criteria.find((c) => c.name === criterion.name);
if (!criterionMatch) {
throw new Error(`Missing evaluation result for criterion: ${criterion.name}`);
}
const threshold = criterion.threshold ?? 0.7;
return {
name: criterion.name,
score: criterionMatch.score,
pass: criterionMatch.score >= threshold,
explanation: criterionMatch.explanation,
};
});
🤖 Prompt for AI Agents
In @packages/testing/src/eval/llm/providers/anthropic.ts around lines 179 - 183,
The inner variable named result in the parsed.criteria.find callback shadows the
outer result parameter of evaluate(), so rename the inner variable to a
non-conflicting name (e.g., parsedCriterion or parsedItem) inside the
options.criteria.map block to avoid shadowing; update references in the callback
and downstream use in the creation of CriterionResult[] so the logic and types
(CriterionResult[], parsed.criteria.find, options.criteria.map) remain identical
but without name collision.

Comment on lines +138 to +147
// Start creating client and cache the promise
pendingPromise = createClient(apiKey).then((client) => {
cachedClient = client;
cachedApiKey = apiKey;
pendingPromise = null;
return client;
});

return pendingPromise;
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Missing error handling for failed client creation.

If createClient rejects, pendingPromise is never cleared, causing subsequent calls with the same API key to return the rejected promise. Add error handling to clear state on failure.

🐛 Proposed fix
       // Start creating client and cache the promise
-      pendingPromise = createClient(apiKey).then((client) => {
-        cachedClient = client;
-        cachedApiKey = apiKey;
-        pendingPromise = null;
-        return client;
-      });
+      pendingPromise = createClient(apiKey)
+        .then((client) => {
+          cachedClient = client;
+          cachedApiKey = apiKey;
+          pendingPromise = null;
+          return client;
+        })
+        .catch((error) => {
+          // Clear state on failure to allow retry
+          pendingPromise = null;
+          cachedApiKey = null;
+          throw error;
+        });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Start creating client and cache the promise
pendingPromise = createClient(apiKey).then((client) => {
cachedClient = client;
cachedApiKey = apiKey;
pendingPromise = null;
return client;
});
return pendingPromise;
},
// Start creating client and cache the promise
pendingPromise = createClient(apiKey)
.then((client) => {
cachedClient = client;
cachedApiKey = apiKey;
pendingPromise = null;
return client;
})
.catch((error) => {
// Clear state on failure to allow retry
pendingPromise = null;
cachedApiKey = null;
throw error;
});
return pendingPromise;
},
🤖 Prompt for AI Agents
In @packages/testing/src/utils/lazy-loader.ts around lines 138 - 147, The
pendingPromise created in the lazy loader (the block that sets pendingPromise =
createClient(apiKey).then(...)) lacks rejection handling so a failed
createClient leaves pendingPromise set and subsequent calls reuse the rejected
promise; fix by attaching a .catch handler to that promise which clears
pendingPromise (and also resets cachedClient and cachedApiKey if appropriate)
before rethrowing the error so state is clean for retries; update the promise
chain around createClient, pendingPromise, cachedClient, and cachedApiKey
accordingly.

* using mock clients to simulate MCP tool calls.
*/

import { describe, it, expect, vi, beforeEach } from "vitest";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Remove unused import beforeEach.

The beforeEach import from vitest is never used. The local constants named beforeEach defined on lines 33 and 231 shadow this import. Remove it from the import statement.

♻️ Proposed fix
-import { describe, it, expect, vi, beforeEach } from "vitest";
+import { describe, it, expect, vi } from "vitest";
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { describe, it, expect, vi, beforeEach } from "vitest";
import { describe, it, expect, vi } from "vitest";
🤖 Prompt for AI Agents
In @packages/testing/tests/integration/behavior-testing.test.ts at line 8, The
import line currently imports beforeEach from vitest but that symbol is never
used and is shadowed by local constants named beforeEach in this test file;
remove beforeEach from the named imports in the vitest import statement (i.e.,
change `import { describe, it, expect, vi, beforeEach } from "vitest";` to
exclude beforeEach) so the code no longer imports an unused symbol while leaving
the other imports intact.

Comment on lines +13 to +35
it("should run property tests with generators", async () => {
// This test requires fast-check to be installed
// Will be skipped or fail gracefully if not available
try {
const stringGen = generators.string({ minLength: 1, maxLength: 10 });

await forAllInputs(
stringGen,
(input) => {
// Property: string length should be within bounds
return input.length >= 1 && input.length <= 10;
},
{ numRuns: 10 }
);
} catch (error) {
// If fast-check is not available, skip the test
if (error instanceof Error && error.message.includes("fast-check")) {
// Test skipped - fast-check not available
return;
}
throw error;
}
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Replace silent test skipping with explicit skip mechanisms.

The try-catch pattern that silently returns when fast-check is unavailable (lines 28-32) causes the test to pass without actually running any assertions. This masks whether fast-check is properly installed and could hide real failures. Use Vitest's conditional skip mechanisms instead.

♻️ Proposed refactor
+import { vi } from "vitest";
+
+// Check if fast-check is available
+let hasFastCheck = false;
+try {
+  await import("fast-check");
+  hasFastCheck = true;
+} catch {
+  // fast-check not available
+}
+
 describe("Property Testing Integration", () => {
-  it("should run property tests with generators", async () => {
-    // This test requires fast-check to be installed
-    // Will be skipped or fail gracefully if not available
-    try {
-      const stringGen = generators.string({ minLength: 1, maxLength: 10 });
+  it.skipIf(!hasFastCheck)("should run property tests with generators", async () => {
+    const stringGen = generators.string({ minLength: 1, maxLength: 10 });

-      await forAllInputs(
-        stringGen,
-        (input) => {
-          // Property: string length should be within bounds
-          return input.length >= 1 && input.length <= 10;
-        },
-        { numRuns: 10 }
-      );
-    } catch (error) {
-      // If fast-check is not available, skip the test
-      if (error instanceof Error && error.message.includes("fast-check")) {
-        // Test skipped - fast-check not available
-        return;
-      }
-      throw error;
-    }
+    await forAllInputs(
+      stringGen,
+      (input) => {
+        // Property: string length should be within bounds
+        return input.length >= 1 && input.length <= 10;
+      },
+      { numRuns: 10 }
+    );
   });

Apply the same pattern to the second test at lines 37-60.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In @packages/testing/tests/integration/property-testing.test.ts around lines 13
- 35, The test currently swallows the absence of fast-check by returning inside
a try-catch; instead detect availability of fast-check up front and use Vitest's
conditional skip (e.g., test.skip or conditionally call it) so the test is
explicitly skipped when missing; specifically, replace the try-catch around
generators.string and forAllInputs with a pre-check (attempt to require/import
fast-check or check that generators and forAllInputs are present), and if
unavailable call the test skip mechanism before executing the assertions, and
apply the same change to the second test that also wraps fast-check usage (the
block using generators.string and forAllInputs).

Comment on lines +1 to +11
/**
* Unit tests for LLM evaluator
*
* Note: These tests require OpenAI or Anthropic SDKs to be installed.
* They will be skipped if the dependencies are not available.
*/

import { describe, it, expect } from "vitest";
import { createLLMEvaluator, criteria } from "../../../src/eval/llm";
import { ConfigurationError } from "../../../src/errors";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Test file structure should mirror source directory.

The test file is located at tests/unit/eval/evaluator.test.ts but the source file is at src/eval/llm/evaluator.ts. According to project conventions, the test should be at tests/unit/eval/llm/evaluator.test.ts to mirror the source structure.

Based on learnings, test files should mirror the source structure with unit/, integration/, and contract/ subdirectories.

📁 Suggested file relocation

Move this test file to:

packages/testing/tests/unit/eval/llm/evaluator.test.ts

And update the import paths accordingly:

-import { createLLMEvaluator, criteria } from "../../../src/eval/llm";
-import { ConfigurationError } from "../../../src/errors";
+import { createLLMEvaluator, criteria } from "../../../../src/eval/llm";
+import { ConfigurationError } from "../../../../src/errors";
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/**
* Unit tests for LLM evaluator
*
* Note: These tests require OpenAI or Anthropic SDKs to be installed.
* They will be skipped if the dependencies are not available.
*/
import { describe, it, expect } from "vitest";
import { createLLMEvaluator, criteria } from "../../../src/eval/llm";
import { ConfigurationError } from "../../../src/errors";
/**
* Unit tests for LLM evaluator
*
* Note: These tests require OpenAI or Anthropic SDKs to be installed.
* They will be skipped if the dependencies are not available.
*/
import { describe, it, expect } from "vitest";
import { createLLMEvaluator, criteria } from "../../../../src/eval/llm";
import { ConfigurationError } from "../../../../src/errors";
🤖 Prompt for AI Agents
In @packages/testing/tests/unit/eval/evaluator.test.ts around lines 1 - 11, The
test file must be moved to mirror the source's nested eval/llm structure under
the unit tests (place it in the tests/unit location matching the src eval llm
hierarchy) and then update the module imports so they resolve from the new
relative location (ensure imports referencing createLLMEvaluator, criteria and
ConfigurationError point to the correct relative paths after the move); keep the
test logic unchanged, only relocate the file and fix the import paths.


// Build evaluation result
const criteriaResults: CriterionResult[] = options.criteria.map((criterion) => {
const result = parsed.criteria.find((c) => c.name === criterion.name);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OpenAI provider lacks JSON structure validation before use

Medium Severity

The OpenAI provider parses JSON and casts it directly to the expected type without runtime validation. If the LLM returns valid JSON with an unexpected structure (e.g., {"message": "I cannot evaluate"}), parsed.criteria will be undefined, and calling .find() on it will throw a confusing TypeError: Cannot read properties of undefined (reading 'find'). The Anthropic provider in the same codebase has extensive validation (checking Array.isArray(parsedObj.criteria), validating each criterion's properties) that provides clear error messages. The same validation pattern is missing from the OpenAI provider.

Fix in Cursor Fix in Web

cachedApiKey = apiKey;
pendingPromise = null;
return client;
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cached client factory never clears rejected promises

Low Severity

In createCachedClientFactory, the pendingPromise is only set to null in the .then() success callback. If createClient rejects (e.g., due to a transient network issue during module import), the rejected promise remains cached, and all subsequent calls with the same API key will return the same rejected promise instead of retrying. This is inconsistent with createLazyLoader in the same file, which correctly clears state.loadPromise = null in the catch block to allow retry attempts.

Fix in Cursor Fix in Web

}

// Create test client
client = await createTestClient(mcpUrl, options.clientOptions);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Server not stopped if client creation fails

Medium Severity

In createTestEnvironment, if createTestClient at line 74 throws an error (e.g., connection refused, network timeout), the server started at line 48 is never stopped. The function throws the error without calling server.stop(), leaving an orphaned server process running. This causes resource leaks, port conflicts in subsequent tests, and potential test flakiness. The server creation and client creation need to be wrapped in try-catch with cleanup on failure.

Fix in Cursor Fix in Web

…documentation

- Add OpenAI and dotenv as dependencies in the minimal example.
- Implement LLM evaluation tests using OpenAI, including setup instructions in the README.
- Update test setup to load environment variables from a .env file.
- Enhance testing documentation with detailed instructions for running LLM evaluation tests.
- Introduce new test files for LLM evaluation and improve overall test structure.
…ns and error handling

- Introduce comprehensive evaluation tests for the MCP framework, covering basic tool evaluation, multi-turn conversations, batch evaluation, and multi-criteria judging.
- Implement error handling tests to validate LLM responses under error injection scenarios.
- Update README documentation to include new features and usage examples for multi-turn conversations and batch evaluations.
- Refactor test setup to support token usage tracking and improved result reporting.
- Enhance the testing framework with new utility functions for session management and batch processing.
- Enhance the README with instructions for setting up the OpenAI API key and running LLM-based evaluation tests.
- Refactor test files for improved readability by consolidating code and removing unnecessary whitespace.
- Update various test cases to ensure consistent formatting and maintainability.
- Improve error handling in test scenarios to provide clearer feedback on failures.
- Introduce utility functions for parsing and formatting costs in dollars, improving cost handling in batch evaluations and session management.
- Update error injection state management to allow resetting, enhancing testing scenarios for retry logic.
- Refactor cost aggregation logic to ensure consistent dollar normalization across evaluations.
- Improve documentation for error injection methods to clarify usage and deprecate older patterns.
- Upgrade @anthropic-ai/sdk from version 0.20.9 to 0.71.2 with updated specifier to '>=0.30.0'.
- Introduce json-schema-to-ts@3.1.1 and ts-algebra@2.0.0 as new dependencies.
- Remove deprecated web-streams-polyfill@3.3.3 and update its version to 4.0.0-beta.3.
- Add optional peer dependency for zod with version 4.2.1.
* Calculate delay for a retry attempt
*/
function calculateDelay(attempt: number, config: Required<Omit<RetryConfig, "retryOn">>): number {
let delay = config.delay;
@claude

claude Bot commented Jan 7, 2026

Copy link
Copy Markdown
Contributor

Comprehensive Code Review: Testing Library Implementation

Overview

This is an excellent and comprehensive PR that introduces a full-featured testing library for MCP applications. The implementation is well-architected, thoroughly documented, and follows best practices. The scope is ambitious (86 files, 13,815 additions) but well-executed.

🎯 Strengths

1. Architecture & Design

  • Excellent modular structure: Clear separation between server utilities, client, matchers, eval framework, and UI testing
  • Lazy loading pattern: Smart use of optional dependencies with helpful error messages (packages/testing/src/utils/lazy-loader.ts:181)
  • Type safety: Strong TypeScript usage with proper type exports and no any types
  • Plugin architecture: MCP eval with provider abstraction (OpenAI/Anthropic) is well designed

2. Documentation 📚

  • Outstanding README: Comprehensive, well-organized with clear examples (packages/testing/README.md:942 lines)
  • Inline documentation: Good JSDoc comments throughout the codebase
  • Example tests: Excellent real-world examples in examples/minimal/tests/

3. Testing Strategy 🧪

  • Multiple testing paradigms: Behavior testing, property-based testing, LLM evaluation, UI testing
  • Test coverage: Good unit, integration, and contract tests for the testing library itself
  • Real-world examples: The minimal example demonstrates all features comprehensively

4. Error Handling 🛡️

  • Custom error classes: Well-structured error hierarchy (ConnectionError, TimeoutError, etc.)
  • Helpful error messages: Clear guidance when optional dependencies are missing
  • Proper cleanup: Timeout cleanup to prevent memory leaks (packages/testing/src/server/test-client.ts:66-68)

🔍 Areas for Improvement

1. Security Considerations 🔒

API Key Handling

The library reads API keys from environment variables which is correct, but consider adding guidance:

packages/testing/src/eval/mcp/providers/index.ts:35-37

return process.env.OPENAI_API_KEY;
return process.env.ANTHROPIC_API_KEY;

Recommendation: Add a security note in the README warning users:

  • Never commit API keys to version control
  • Use .env files (already shown in examples - good!)
  • Consider rate limiting to prevent accidental cost overruns in CI/CD

Process Spawning

packages/testing/src/server/test-server.ts:150-153

const childProcess = spawn(command, args, {
  env: { ...process.env, ...env },
  stdio: ["ignore", "pipe", "pipe"],
});

This is generally safe but document that users should validate command and args when accepting user input.

2. Potential Bugs 🐛

Timeout Cleanup Edge Case

packages/testing/src/server/test-server.ts:89-100

The timeout is cleared after app.start() resolves, but if the promise is rejected, the timeout continues:

const timeoutPromise = new Promise<never>((_, reject) => {
  timeoutId = setTimeout(() => {
    reject(new ServerStartupError(...));
  }, timeout);
});

Recommendation: Use a try-finally block to ensure timeout is always cleared:

try {
  await Promise.race([startPromise, timeoutPromise]);
} finally {
  if (timeoutId !== undefined) {
    clearTimeout(timeoutId);
  }
}

Process Kill Safety

packages/testing/src/server/test-server.ts:195-204

The stop() method doesn't verify the process actually terminated:

async stop(): Promise<void> {
  if (childProcess && !childProcess.killed) {
    childProcess.kill();
  }
}

Recommendation: Add a timeout and force kill if needed:

async stop(): Promise<void> {
  if (childProcess && !childProcess.killed) {
    childProcess.kill('SIGTERM');
    // Wait for graceful shutdown
    await new Promise((resolve) => {
      const timeout = setTimeout(() => {
        childProcess.kill('SIGKILL');
        resolve(undefined);
      }, 5000);
      childProcess.once('exit', () => {
        clearTimeout(timeout);
        resolve(undefined);
      });
    });
  }
}

3. Performance Considerations

Rate Limiting Implementation

The retry/rate limiting logic is good (packages/testing/src/eval/mcp/retry.ts:317), but consider:

Recommendation:

  • Add token bucket algorithm for smoother rate limiting
  • Consider per-provider rate limits (OpenAI vs Anthropic have different limits)
  • Document cost estimation accuracy (marked as "approximate")

Test Server Port Finding

packages/testing/src/server/test-server.ts:40-61

The findAvailablePort() opens and closes a server which creates a race condition:

server.listen(0, () => {
  const port = addr.port;
  server.close((err) => {
    resolve(port);
  });
});

Between closing and starting the actual server, another process could grab the port.

Recommendation: Document this known limitation or consider:

  1. Keeping the server open and passing the server instance
  2. Retrying if the port is taken
  3. Using a port range with retry logic

4. Code Quality 🎨

Consistency in Error Handling

Some places use Error while others use custom errors. For example:

packages/testing/src/server/test-server.ts:56

reject(new Error("Failed to detect available port"));

Recommendation: Use ServerStartupError for consistency.

Type Casting

packages/testing/src/server/test-client.ts:86

const structuredContent = (result as { structuredContent?: unknown }).structuredContent;

Recommendation: Define a proper interface for the MCP result type to avoid casting.

5. Testing Coverage 🧪

The testing library itself has good test coverage, but consider:

Missing test scenarios:

  1. Concurrent test server starts (race conditions)
  2. Network failures during tool calls
  3. Malformed MCP responses
  4. Memory leak tests for long-running sessions
  5. Edge cases in cost calculation (very small/large numbers)

Recommendation: Add integration tests that specifically test:

  • Multiple simultaneous client connections
  • Server restart scenarios
  • Tool call cancellation

📝 Minor Issues

1. TypeScript

  • Good use of strict mode and proper type exports ✅
  • Some places could benefit from readonly modifiers for immutability

2. Documentation Gaps

  • The .cursor/ addition to .gitignore is fine but unrelated to the testing library
  • Consider adding troubleshooting section to README for common issues

3. Dependencies

packages/testing/package.json:70

"fast-check": "^3.0.0"

Consider updating to ^4.0.0 as you're already using it in examples/minimal:
examples/minimal/package.json:40

"fast-check": "^4.5.3"

🎯 Recommendations

High Priority

  1. ✅ Fix timeout cleanup edge case in test-server.ts:89-100
  2. ✅ Improve process termination handling in test-server.ts:195-204
  3. ⚠️ Add security guidance for API keys in production CI/CD

Medium Priority

  1. 📚 Add troubleshooting section to README
  2. 🧪 Add integration tests for concurrent operations
  3. 🔄 Align fast-check versions across packages

Low Priority

  1. 🎨 Consistent custom error usage throughout
  2. 📝 Add cost estimation accuracy disclaimer

🌟 Exemplary Practices

  • Lazy loading: Excellent implementation preventing dependency bloat
  • Test organization: Clear separation of unit/integration/contract tests
  • Documentation: README is comprehensive and example-driven
  • Error messages: Helpful guidance when features aren't available
  • Resilience: Retry logic, rate limiting, and timeout handling

📊 Summary

Category Score Notes
Architecture ⭐⭐⭐⭐⭐ Excellent modular design
Code Quality ⭐⭐⭐⭐ Very good, minor improvements needed
Documentation ⭐⭐⭐⭐⭐ Outstanding README and examples
Testing ⭐⭐⭐⭐ Good coverage, could add more edge cases
Security ⭐⭐⭐⭐ Solid, needs CI/CD guidance
Performance ⭐⭐⭐⭐ Good, rate limiting could be refined

✅ Conclusion

This PR is ready to merge with the recommended fixes for the timeout cleanup and process termination issues. The testing library is well-designed, thoroughly documented, and will be a valuable addition to the MCP AppsKit ecosystem.

The scope is large but justified—this provides comprehensive testing capabilities that the project needs. Great work! 🎉

Recommended Action:

  1. Fix the two high-priority issues (timeout cleanup, process kill)
  2. Merge
  3. Address medium/low priority items in follow-up PRs

@claude

claude Bot commented Jan 7, 2026

Copy link
Copy Markdown
Contributor

Code Review: Testing Library Implementation

This is an excellent and comprehensive testing library addition! The implementation is well-structured, thoroughly tested, and follows best practices. Here's my detailed review:


Strengths

1. Architecture & Design

  • Clean separation of concerns: Server, client, matchers, eval, and UI testing are well-isolated
  • Lazy loading pattern: Excellent use of lazy loaders for optional dependencies (packages/testing/src/utils/lazy-loader.ts) - reduces bundle size and provides helpful error messages
  • Flexible API: Multiple entry points (simple createTestEnvironment, builder pattern, manual setup) cater to different use cases
  • Protocol abstraction: Works with any MCP-compliant server

2. TypeScript Quality

  • Strong typing: Minimal use of any type (only 5 occurrences, all justified with ESLint disables and comments)
  • Proper type exports: Clean separation with export type for type-only exports
  • Type safety: Good use of Zod for runtime validation alongside TypeScript

3. Testing

  • Comprehensive coverage: 17 test files covering unit, integration, and contract tests
  • Good examples: The examples/minimal/tests directory provides excellent real-world usage patterns
  • Self-hosting: Testing library tests itself - dogfooding at its finest!

4. Developer Experience

  • Excellent documentation: README.md is thorough with clear examples
  • Helpful error messages: Custom error classes with codes and context (packages/testing/src/errors.ts)
  • Debug logging: Consistent debug logger pattern across all modules
  • Multiple test framework support: Vitest and Jest adapters via subpath exports

5. Feature Completeness

  • Basic tool testing ✅
  • Property-based testing ✅
  • LLM evaluation (both output quality and MCP tool usage) ✅
  • UI widget testing with mock host ✅
  • Batch evaluation ✅
  • Multi-turn conversations ✅
  • Error injection ✅
  • Token usage tracking ✅

🔍 Areas for Consideration

1. Console Logging (Low Priority)

Found 60+ console.log/warn/error statements across 6 files:

  • packages/testing/src/eval/mcp/reporter.ts (53 occurrences)
  • packages/testing/src/eval/mcp/evaluator.ts
  • packages/testing/src/eval/mcp/session.ts
  • packages/testing/src/eval/behavior/runner.ts
  • packages/testing/src/eval/reporter/reporter.ts
  • packages/testing/src/ui/mock-host.ts

Recommendation: Consider using the debug logger pattern consistently instead of console.* for better control over logging output. However, for reporters and summary output, console.* is appropriate.

2. Environment Variables (Medium Priority)

API keys are accessed from environment variables in multiple places:

  • packages/testing/src/eval/llm/providers/anthropic.ts:46
  • packages/testing/src/eval/llm/providers/openai.ts:44
  • packages/testing/src/eval/mcp/evaluator.ts:732, 739
  • packages/testing/src/eval/mcp/providers/index.ts:35, 37, 102

Current implementation is GOOD:

  • ✅ Keys are read from environment (not hardcoded)
  • ✅ Tests properly skip when keys are missing (describeEval helper)
  • ✅ Clear documentation in .env.example

Minor suggestion: Consider adding a runtime check or warning when eval tests run without keys to make it more obvious why tests are skipped.

3. Peer Dependencies (Good as-is)

The package correctly marks all optional dependencies as peer dependencies:

  • fast-check, openai, @anthropic-ai/sdk, vitest, jest
  • All marked as optional with peerDependenciesMeta
  • Lazy loading provides clear error messages when missing

This is the correct approach - no changes needed.

4. Test Guard Pattern (Excellent)

The minimal example properly guards server startup during tests:

// Only start server if not in test environment
if (process.env.NODE_ENV !== "test" && !process.env.VITEST) {
  app.start({ port });
}

This is the right pattern for testable applications.


🔒 Security

No security concerns found:

  • ✅ No hardcoded secrets
  • ✅ API keys properly sourced from environment
  • ✅ No unsafe dynamic imports or eval
  • ✅ Proper error handling prevents information leakage
  • ✅ No TODOs, FIXMEs, or security-related comments

📊 Metrics

  • Total TypeScript files: 67
  • Test files: 17 (unit, integration, contract)
  • Lines added: ~13,800
  • Lines deleted: 20
  • Any types: 5 (all justified with comments)
  • Console statements: 60 (mostly in reporters - acceptable)

🎯 Recommendations

Must Address (None)

All code quality issues are acceptable for this PR.

Should Consider (Optional)

  1. Consistency: Replace console.* with debug loggers where appropriate (not in reporters)
  2. Documentation: Add a note about why eval tests are skipped when API keys are missing
  3. Future enhancement: Consider adding a coverage report CI step to maintain 80% target

Nice to Have

  1. Add examples of custom LLM criteria in the README
  2. Consider adding a troubleshooting section for common test setup issues

Verdict

LGTM - Excellent work! 🎉

This is a production-ready testing library that demonstrates:

  • Strong TypeScript practices
  • Thoughtful API design
  • Comprehensive testing (testing the tests!)
  • Excellent documentation
  • No security concerns
  • Clean dependency management

The code follows the project's CLAUDE.md guidelines and maintains high quality standards. The minor suggestions above are purely optional improvements that don't block merging.

Recommendation: ✅ Approve and Merge


Great job on this substantial contribution! The testing library will be a valuable addition to the MCP AppsKit ecosystem.

- Eliminate the deprecated resetErrorInjectionState function from error-injection.ts, encouraging the use of wrappedClient.reset() instead.
- Update index.ts to remove the export of resetErrorInjectionState, streamlining the error injection API.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 Fix all issues with AI agents
In @examples/minimal/README.md:
- Around line 88-91: The fenced code block containing
"OPENAI_API_KEY=sk-your-actual-api-key" is missing a language identifier; update
the opening triple-backtick for that block to include a language (e.g., "bash")
so it matches the other block and satisfies markdown linting/consistency.

In @packages/testing/src/eval/llm/providers/openai.ts:
- Around line 84-109: The code blindly casts JSON.parse(content) to the expected
shape and then assumes parsed.criteria and parsed.overall exist; add runtime
validation after parsing to guard against malformed responses by checking parsed
is an object, parsed.criteria is an array, each parsed.criteria item has name
(string), score (number) and explanation (string), and parsed.overall exists
with a numeric score before using them in the loop that builds CriteriaResults
and before computing overallScore; if any check fails, throw a clear Error that
includes the raw content (or a snippet) and a message like "Invalid LLM
response: missing or malformed 'criteria'/'overall'". Ensure you reference and
validate parsed (the variable from JSON.parse), parsed.criteria, parsed.overall,
and the loop over options.criteria/CriteriaResults to avoid TypeError at
runtime.

In @packages/testing/src/eval/mcp/providers/anthropic.ts:
- Around line 70-71: Remove the unused eslint-disable directive preceding the
declaration of the content array: delete the line "// eslint-disable-next-line
@typescript-eslint/no-explicit-any" and leave the declaration "const content:
any[] = [];" as-is (or replace the any type with a more specific type if
desired) so the file no longer contains the unnecessary eslint-disable comment.

In @packages/testing/src/eval/mcp/session.ts:
- Around line 117-119: The end() method calls this.reset() which will break if
end is extracted or used as a callback; change its implementation so it doesn't
rely on dynamic this: either convert end into a bound arrow property (e.g., end
= () => { this.reset(); }) or move reset into a closure/local function and call
that directly from end; update the declaration of end() in the same object/class
where reset() is defined to use one of these approaches so calling end without
the original this still invokes reset.

In @packages/testing/src/eval/reporter/reporter.ts:
- Around line 32-42: The ESLint no-console warning comes from the console calls
in the defaultOutput object; suppress it by disabling the rule only for this
block (e.g., add a block-level suppression like /* eslint-disable no-console */
immediately before the defaultOutput definition and /* eslint-enable no-console
*/ immediately after) so the EvalReporterOutput defaultOutput (log/warn/error)
can intentionally use console without triggering pipeline warnings.

In @packages/testing/src/eval/reporter/types.ts:
- Around line 54-70: The EvalReportSummary type is used as the return type for
EvalReporter methods printSummary() and getSummary() but isn't exported from the
package public API; export it from the eval/reporter exports in your public
index (add EvalReportSummary to the exported symbols alongside EvalReporter) so
consumers can import it, and optionally convert the declaration from "export
interface EvalReportSummary" to "export type EvalReportSummary = { ... }" to
mark it as a type-only export per TypeScript guidelines.
🧹 Nitpick comments (11)
packages/testing/README.md (1)

724-730: Add language specifier to fenced code block.

The example output block at line 724 is missing a language specifier. For console output, use text or console to satisfy linting and improve syntax highlighting.

📝 Suggested fix
-```
+```text
 [MCP EVAL] Please greet Alice
   Tools: ✓ greet({"name":"Alice"})
   Response: Hello, Alice!
   Duration: 1406ms
   Judge: [PASS] (100%) - The agent successfully greeted Alice with a friendly message.
</details>

</blockquote></details>
<details>
<summary>packages/testing/src/server/test-client.ts (1)</summary><blockquote>

`73-82`: **Consider extracting inline type to improve maintainability.**

The local `ContentBlock` type definition shadows the similarly-named interface from `../types`. While this inline definition serves its purpose for SDK response mapping, consider adding a brief comment explaining why this differs from the shared `ContentBlock` type to prevent confusion during maintenance.

</blockquote></details>
<details>
<summary>packages/testing/src/eval/mcp/providers/openai.ts (1)</summary><blockquote>

`165-171`: **Silent failure in `safeParseJSON` may hide tool argument parsing issues.**

When tool arguments fail to parse, returning an empty object silently could mask issues with malformed tool calls from the LLM. Consider logging the parse failure for debugging purposes.


<details>
<summary>🔧 Suggested improvement</summary>

```diff
+import { llmLogger } from "../../../debug";
+
 function safeParseJSON(str: string): Record<string, unknown> {
   try {
     return JSON.parse(str) as Record<string, unknown>;
-  } catch {
+  } catch (error) {
+    llmLogger("Failed to parse tool arguments: %s, error: %O", str, error);
     return {};
   }
 }
examples/minimal/tests/eval.test.ts (1)

394-399: Skip block may be redundant if describeEval already handles provider key checks.

The describeEval wrapper likely already skips tests when no provider keys are available. This explicit skip block creates a duplicate test entry that could be confusing in test output.

🔧 Consider removing redundant skip block

If describeEval internally checks hasAnyProviderKey() and skips appropriately, this block can be removed. Otherwise, consider consolidating the skip logic in one place.

packages/testing/src/eval/llm/providers/anthropic.ts (1)

86-176: Consider extracting JSON validation to a separate function.

The runtime validation logic (lines 103-163) is comprehensive but makes the evaluate method quite long. Extracting this to a helper function would improve readability.

🔧 Suggested extraction
interface ParsedEvalResponse {
  criteria: Array<{ name: string; score: number; explanation: string }>;
  overall: { score: number };
}

function validateParsedResponse(rawParsed: unknown, textPreview: string): ParsedEvalResponse {
  if (typeof rawParsed !== "object" || rawParsed === null) {
    throw new Error(`Expected parsed response to be an object, got ${typeof rawParsed}. Response preview: ${textPreview}`);
  }
  // ... rest of validation
  return rawParsed as ParsedEvalResponse;
}
packages/testing/src/eval/mcp/providers/index.ts (1)

53-62: Unreachable default case returns same value as explicit case.

The default branch on line 59-60 is unreachable because ProviderType is exhaustively handled. While it provides a safety net, consider using TypeScript's exhaustive check pattern instead:

♻️ Suggested improvement
 export function getDefaultModel(provider: ProviderType): string {
   switch (provider) {
     case "openai":
       return "gpt-4o-mini";
     case "anthropic":
       return "claude-3-haiku-20240307";
-    default:
-      return "gpt-4o-mini";
+    default: {
+      const _exhaustive: never = provider;
+      return _exhaustive;
+    }
   }
 }
packages/testing/src/eval/mcp/providers/anthropic.ts (1)

193-198: Hardcoded max_tokens: 512 may be insufficient for complex JSON responses.

createJSONCompletion uses a fixed 512 token limit while createCompletion uses the configurable maxTokens from config. Multi-criteria judge responses with detailed explanations could be truncated.

♻️ Suggested fix
       const response: any = await anthropic.messages.create({
         model,
-        max_tokens: 512,
+        max_tokens: Math.min(maxTokens, 1024), // Cap for JSON responses
         system: jsonSystemPrompt,
         messages: anthropicMessages,
       });
packages/testing/src/eval/mcp/reporter.ts (2)

268-273: Fragile private field access via bracket notation.

Accessing defaultReporter?.["verbose"] relies on implementation details. If the field is renamed or the class structure changes, this will silently break.

♻️ Suggested improvement
+let currentVerboseSetting: boolean | null = null;
+
 export function getReporter(verbose: boolean = true): MCPEvalReporter {
-  if (defaultReporter?.["verbose"] !== verbose) {
+  if (currentVerboseSetting !== verbose) {
     defaultReporter = new MCPEvalReporter(verbose);
+    currentVerboseSetting = verbose;
   }
   return defaultReporter;
 }

353-357: Duplicate cost parsing logic.

The cost string parsing (replace(/[$¢]/g, "") then parseFloat) appears in both printSummary (line 355-356) and getSummary (line 463-464). The module already imports parseCostToDollars from ./cost-utils—consider using it here for consistency.

♻️ Suggested fix in printSummary
+import { parseCostToDollars } from "./cost-utils";
+
         if (result.usage.estimatedCost) {
           hasCost = true;
-          const costStr = result.usage.estimatedCost.replace(/[$¢]/g, "");
-          totalCost += parseFloat(costStr) || 0;
+          totalCost += parseCostToDollars(result.usage.estimatedCost);
         }

Also applies to: 462-465

packages/testing/src/eval/mcp/batch.ts (2)

165-170: Duplicate sleep utility function.

This sleep helper is identical to the one in retry.ts. Consider extracting to a shared utility module to avoid duplication.


200-200: Minor: failures array built redundantly.

The failures array is populated during sequential execution (line 307) but then rebuilt from results.filter() (line 353) for the final result. The in-loop population is only used for stopOnFailure logging. This is fine but the final rebuild makes the in-loop tracking partially redundant.

Also applies to: 307-307, 353-354

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between da29196 and b384b50.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (28)
  • examples/minimal/.env.example
  • examples/minimal/README.md
  • examples/minimal/package.json
  • examples/minimal/tests/eval.test.ts
  • examples/minimal/tests/setup.ts
  • packages/testing/README.md
  • packages/testing/package.json
  • packages/testing/src/eval/index.ts
  • packages/testing/src/eval/llm/providers/anthropic.ts
  • packages/testing/src/eval/llm/providers/openai.ts
  • packages/testing/src/eval/mcp/batch.ts
  • packages/testing/src/eval/mcp/cost-utils.ts
  • packages/testing/src/eval/mcp/error-injection.ts
  • packages/testing/src/eval/mcp/evaluator.ts
  • packages/testing/src/eval/mcp/index.ts
  • packages/testing/src/eval/mcp/providers/anthropic.ts
  • packages/testing/src/eval/mcp/providers/index.ts
  • packages/testing/src/eval/mcp/providers/openai.ts
  • packages/testing/src/eval/mcp/providers/types.ts
  • packages/testing/src/eval/mcp/reporter.ts
  • packages/testing/src/eval/mcp/retry.ts
  • packages/testing/src/eval/mcp/session.ts
  • packages/testing/src/eval/reporter/index.ts
  • packages/testing/src/eval/reporter/reporter.ts
  • packages/testing/src/eval/reporter/types.ts
  • packages/testing/src/index.ts
  • packages/testing/src/server/test-client.ts
  • packages/testing/src/types.ts
✅ Files skipped from review due to trivial changes (1)
  • packages/testing/src/eval/mcp/index.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • examples/minimal/package.json
  • examples/minimal/tests/setup.ts
🧰 Additional context used
📓 Path-based instructions (6)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use strict TypeScript with no any types - use unknown and narrow instead

Files:

  • packages/testing/src/eval/mcp/cost-utils.ts
  • packages/testing/src/eval/mcp/providers/types.ts
  • packages/testing/src/eval/mcp/providers/openai.ts
  • packages/testing/src/eval/reporter/types.ts
  • packages/testing/src/eval/mcp/providers/index.ts
  • packages/testing/src/eval/mcp/providers/anthropic.ts
  • packages/testing/src/eval/index.ts
  • packages/testing/src/eval/reporter/reporter.ts
  • packages/testing/src/eval/reporter/index.ts
  • packages/testing/src/eval/mcp/session.ts
  • packages/testing/src/server/test-client.ts
  • packages/testing/src/eval/mcp/batch.ts
  • examples/minimal/tests/eval.test.ts
  • packages/testing/src/eval/mcp/error-injection.ts
  • packages/testing/src/eval/llm/providers/openai.ts
  • packages/testing/src/eval/mcp/retry.ts
  • packages/testing/src/eval/mcp/evaluator.ts
  • packages/testing/src/types.ts
  • packages/testing/src/index.ts
  • packages/testing/src/eval/llm/providers/anthropic.ts
  • packages/testing/src/eval/mcp/reporter.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Remove unused variables or prefix with underscore (_)

Files:

  • packages/testing/src/eval/mcp/cost-utils.ts
  • packages/testing/src/eval/mcp/providers/types.ts
  • packages/testing/src/eval/mcp/providers/openai.ts
  • packages/testing/src/eval/reporter/types.ts
  • packages/testing/src/eval/mcp/providers/index.ts
  • packages/testing/src/eval/mcp/providers/anthropic.ts
  • packages/testing/src/eval/index.ts
  • packages/testing/src/eval/reporter/reporter.ts
  • packages/testing/src/eval/reporter/index.ts
  • packages/testing/src/eval/mcp/session.ts
  • packages/testing/src/server/test-client.ts
  • packages/testing/src/eval/mcp/batch.ts
  • examples/minimal/tests/eval.test.ts
  • packages/testing/src/eval/mcp/error-injection.ts
  • packages/testing/src/eval/llm/providers/openai.ts
  • packages/testing/src/eval/mcp/retry.ts
  • packages/testing/src/eval/mcp/evaluator.ts
  • packages/testing/src/types.ts
  • packages/testing/src/index.ts
  • packages/testing/src/eval/llm/providers/anthropic.ts
  • packages/testing/src/eval/mcp/reporter.ts
**/*.ts

📄 CodeRabbit inference engine (CLAUDE.md)

Use export type for type-only exports

Files:

  • packages/testing/src/eval/mcp/cost-utils.ts
  • packages/testing/src/eval/mcp/providers/types.ts
  • packages/testing/src/eval/mcp/providers/openai.ts
  • packages/testing/src/eval/reporter/types.ts
  • packages/testing/src/eval/mcp/providers/index.ts
  • packages/testing/src/eval/mcp/providers/anthropic.ts
  • packages/testing/src/eval/index.ts
  • packages/testing/src/eval/reporter/reporter.ts
  • packages/testing/src/eval/reporter/index.ts
  • packages/testing/src/eval/mcp/session.ts
  • packages/testing/src/server/test-client.ts
  • packages/testing/src/eval/mcp/batch.ts
  • examples/minimal/tests/eval.test.ts
  • packages/testing/src/eval/mcp/error-injection.ts
  • packages/testing/src/eval/llm/providers/openai.ts
  • packages/testing/src/eval/mcp/retry.ts
  • packages/testing/src/eval/mcp/evaluator.ts
  • packages/testing/src/types.ts
  • packages/testing/src/index.ts
  • packages/testing/src/eval/llm/providers/anthropic.ts
  • packages/testing/src/eval/mcp/reporter.ts
**/index.ts

📄 CodeRabbit inference engine (CLAUDE.md)

Export public API only in index.ts files

Files:

  • packages/testing/src/eval/mcp/providers/index.ts
  • packages/testing/src/eval/index.ts
  • packages/testing/src/eval/reporter/index.ts
  • packages/testing/src/index.ts
**/tests/**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Mirror test file structure to source: tests in tests/ directory with unit/, integration/, and contract/ subdirectories

Files:

  • examples/minimal/tests/eval.test.ts
{packages/core,examples}/**/*.ts

📄 CodeRabbit inference engine (CLAUDE.md)

{packages/core,examples}/**/*.ts: Always use defineTool and defineUI for type inference when defining tools and UI components
Use Koa-style async/await middleware pattern with await next() to chain middleware execution
Use AppError and ErrorCode from @mcp-apps-kit/core for error handling
Implement plugins using the Plugin interface with hooks: onInit, onStart, onShutdown, beforeToolCall, afterToolCall, onToolError
Use app.events.on() and app.events.once() for event subscription with event types like app:init, tool:call, app:start
Use Zod schemas with defineTool for input/output validation
Colocate UI definitions near tool definitions using defineUI with html property pointing to compiled UI assets

Files:

  • examples/minimal/tests/eval.test.ts
🧠 Learnings (11)
📓 Common learnings
Learnt from: CR
Repo: AndurilCode/mcp-apps-kit PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-29T20:58:59.376Z
Learning: Applies to **/tests/**/*.{test,spec}.{ts,tsx} : Mirror test file structure to source: tests in `tests/` directory with `unit/`, `integration/`, and `contract/` subdirectories
Learnt from: CR
Repo: AndurilCode/mcp-apps-kit PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-29T20:58:59.376Z
Learning: Applies to {packages/core,examples}/**/*.ts : Use `AppError` and `ErrorCode` from `mcp-apps-kit/core` for error handling
📚 Learning: 2025-12-29T20:58:59.376Z
Learnt from: CR
Repo: AndurilCode/mcp-apps-kit PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-29T20:58:59.376Z
Learning: Applies to {packages/core,examples}/**/*.ts : Always use `defineTool` and `defineUI` for type inference when defining tools and UI components

Applied to files:

  • packages/testing/src/eval/mcp/providers/types.ts
  • packages/testing/src/eval/reporter/types.ts
  • examples/minimal/README.md
  • packages/testing/src/types.ts
  • packages/testing/src/index.ts
📚 Learning: 2025-12-29T20:58:59.376Z
Learnt from: CR
Repo: AndurilCode/mcp-apps-kit PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-29T20:58:59.376Z
Learning: Applies to **/*.ts : Use `export type` for type-only exports

Applied to files:

  • packages/testing/src/eval/reporter/types.ts
  • packages/testing/src/types.ts
📚 Learning: 2025-12-29T20:58:59.376Z
Learnt from: CR
Repo: AndurilCode/mcp-apps-kit PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-29T20:58:59.376Z
Learning: Applies to **/tests/**/*.{test,spec}.{ts,tsx} : Mirror test file structure to source: tests in `tests/` directory with `unit/`, `integration/`, and `contract/` subdirectories

Applied to files:

  • packages/testing/package.json
  • examples/minimal/README.md
  • packages/testing/src/types.ts
  • packages/testing/src/index.ts
📚 Learning: 2025-12-29T20:58:59.376Z
Learnt from: CR
Repo: AndurilCode/mcp-apps-kit PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-29T20:58:59.376Z
Learning: Zod version must be ^4.0.0

Applied to files:

  • packages/testing/package.json
📚 Learning: 2025-12-29T20:58:59.376Z
Learnt from: CR
Repo: AndurilCode/mcp-apps-kit PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-29T20:58:59.376Z
Learning: Applies to {packages/core,examples}/**/*.ts : Use `AppError` and `ErrorCode` from `mcp-apps-kit/core` for error handling

Applied to files:

  • packages/testing/package.json
  • packages/testing/src/eval/mcp/error-injection.ts
  • packages/testing/src/index.ts
📚 Learning: 2025-12-29T20:58:59.376Z
Learnt from: CR
Repo: AndurilCode/mcp-apps-kit PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-29T20:58:59.376Z
Learning: Applies to **/index.ts : Export public API only in `index.ts` files

Applied to files:

  • packages/testing/src/eval/reporter/index.ts
  • packages/testing/src/index.ts
📚 Learning: 2025-12-29T20:58:59.376Z
Learnt from: CR
Repo: AndurilCode/mcp-apps-kit PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-29T20:58:59.376Z
Learning: Run full checks before PR: `pnpm build && pnpm test && pnpm lint && pnpm typecheck`

Applied to files:

  • examples/minimal/README.md
📚 Learning: 2025-12-29T20:58:59.376Z
Learnt from: CR
Repo: AndurilCode/mcp-apps-kit PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-29T20:58:59.376Z
Learning: Applies to {packages/core,examples}/**/*.ts : Colocate UI definitions near tool definitions using `defineUI` with `html` property pointing to compiled UI assets

Applied to files:

  • examples/minimal/README.md
  • packages/testing/src/types.ts
📚 Learning: 2025-12-29T20:58:59.376Z
Learnt from: CR
Repo: AndurilCode/mcp-apps-kit PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-29T20:58:59.376Z
Learning: Applies to {packages/core,examples}/**/*.ts : Use Zod schemas with `defineTool` for input/output validation

Applied to files:

  • packages/testing/src/types.ts
  • packages/testing/src/index.ts
📚 Learning: 2025-12-29T20:58:59.376Z
Learnt from: CR
Repo: AndurilCode/mcp-apps-kit PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-29T20:58:59.376Z
Learning: Applies to {packages/core,examples}/**/*.ts : Implement plugins using the `Plugin` interface with hooks: `onInit`, `onStart`, `onShutdown`, `beforeToolCall`, `afterToolCall`, `onToolError`

Applied to files:

  • packages/testing/src/types.ts
🧬 Code graph analysis (8)
packages/testing/src/eval/mcp/providers/types.ts (3)
packages/testing/src/eval/mcp/index.ts (6)
  • ProviderTool (80-80)
  • ProviderToolCall (82-82)
  • ProviderMessage (79-79)
  • ProviderResponse (81-81)
  • ProviderConfig (78-78)
  • LLMProvider (77-77)
packages/testing/src/eval/mcp/providers/index.ts (6)
  • ProviderTool (16-16)
  • ProviderToolCall (18-18)
  • ProviderMessage (15-15)
  • ProviderResponse (17-17)
  • ProviderConfig (14-14)
  • LLMProvider (13-13)
packages/ui-react-builder/src/vite-plugin.ts (1)
  • config (502-514)
packages/testing/src/eval/reporter/types.ts (1)
packages/testing/src/types.ts (2)
  • EvaluationResult (336-346)
  • EvalOptions (302-311)
packages/testing/src/eval/mcp/providers/anthropic.ts (3)
packages/testing/src/eval/mcp/providers/index.ts (7)
  • createAnthropicProvider (22-22)
  • ProviderConfig (14-14)
  • LLMProvider (13-13)
  • ProviderMessage (15-15)
  • ProviderTool (16-16)
  • ProviderResponse (17-17)
  • ProviderToolCall (18-18)
packages/testing/src/eval/mcp/providers/types.ts (6)
  • ProviderConfig (52-56)
  • LLMProvider (61-72)
  • ProviderMessage (28-33)
  • ProviderTool (10-14)
  • ProviderResponse (38-47)
  • ProviderToolCall (19-23)
packages/testing/src/index.ts (1)
  • llmLogger (123-123)
packages/testing/src/eval/reporter/reporter.ts (2)
packages/testing/src/eval/reporter/types.ts (3)
  • EvalReporterOutput (26-30)
  • EvalReportEntry (35-52)
  • EvalReporterOptions (10-21)
packages/testing/src/types.ts (3)
  • CriterionResult (316-325)
  • EvaluationResult (336-346)
  • EvalOptions (302-311)
packages/testing/src/eval/mcp/error-injection.ts (3)
packages/testing/src/eval/mcp/index.ts (5)
  • ToolErrorConfig (51-51)
  • ErrorInjectionConfig (52-52)
  • ErrorInjectionState (53-53)
  • wrapWithErrorInjection (49-49)
  • resetErrorInjectionState (50-50)
packages/testing/src/index.ts (4)
  • ToolErrorConfig (94-94)
  • ErrorInjectionConfig (95-95)
  • TestClient (27-27)
  • ToolResult (24-24)
packages/testing/src/types.ts (2)
  • TestClient (77-109)
  • ToolResult (27-38)
packages/testing/src/eval/llm/providers/openai.ts (5)
packages/testing/src/utils/lazy-loader.ts (2)
  • createLazyLoader (47-85)
  • createCachedClientFactory (109-158)
packages/testing/src/eval/llm/providers/index.ts (1)
  • LLMProvider (12-22)
packages/testing/src/index.ts (6)
  • llmLogger (123-123)
  • ConfigurationError (110-110)
  • EvalOptions (47-47)
  • EvaluationResult (50-50)
  • CriteriaResults (49-49)
  • CustomEvalOptions (51-51)
packages/testing/src/errors.ts (1)
  • ConfigurationError (175-187)
packages/testing/src/types.ts (4)
  • EvalOptions (302-311)
  • EvaluationResult (336-346)
  • CriteriaResults (331-331)
  • CustomEvalOptions (373-378)
packages/testing/src/eval/mcp/evaluator.ts (9)
packages/testing/src/eval/mcp/index.ts (21)
  • ToolCallRecord (22-22)
  • JudgeCriterion (24-24)
  • JudgeOptions (25-25)
  • JudgeResult (23-23)
  • TokenUsage (27-27)
  • MCPEvalResult (21-21)
  • ProviderMessage (79-79)
  • MCPEvalConfig (18-18)
  • ProviderType (76-76)
  • RetryConfig (44-44)
  • ToolErrorConfig (51-51)
  • MCPEvaluator (17-17)
  • MCPSession (38-38)
  • BatchEvalCase (60-60)
  • MCPEval (16-16)
  • detectProvider (75-75)
  • getProviderApiKey (72-72)
  • getDefaultModel (74-74)
  • wrapWithErrorInjection (49-49)
  • ProviderTool (80-80)
  • createSession (38-38)
packages/testing/src/eval/index.ts (3)
  • ToolCallRecord (19-19)
  • MCPEvalResult (19-19)
  • MCPEvalConfig (19-19)
packages/testing/src/eval/mcp/providers/index.ts (6)
  • ProviderMessage (15-15)
  • ProviderType (27-27)
  • detectProvider (101-109)
  • getProviderApiKey (32-41)
  • getDefaultModel (53-62)
  • ProviderTool (16-16)
packages/testing/src/eval/mcp/providers/types.ts (2)
  • ProviderMessage (28-33)
  • ProviderTool (10-14)
packages/testing/src/eval/mcp/retry.ts (1)
  • RetryConfig (12-25)
packages/testing/src/eval/mcp/error-injection.ts (2)
  • ToolErrorConfig (12-21)
  • wrapWithErrorInjection (72-144)
packages/testing/src/eval/mcp/session.ts (2)
  • MCPSession (14-32)
  • createSession (57-121)
packages/testing/src/eval/mcp/batch.ts (1)
  • BatchEvalCase (21-42)
packages/testing/src/types.ts (1)
  • TestClient (77-109)
packages/testing/src/eval/llm/providers/anthropic.ts (5)
packages/testing/src/utils/lazy-loader.ts (2)
  • createLazyLoader (47-85)
  • createCachedClientFactory (109-158)
packages/testing/src/eval/llm/providers/index.ts (1)
  • LLMProvider (12-22)
packages/testing/src/index.ts (6)
  • llmLogger (123-123)
  • ConfigurationError (110-110)
  • EvalOptions (47-47)
  • EvaluationResult (50-50)
  • CriteriaResults (49-49)
  • CustomEvalOptions (51-51)
packages/testing/src/errors.ts (1)
  • ConfigurationError (175-187)
packages/testing/src/types.ts (4)
  • EvalOptions (302-311)
  • EvaluationResult (336-346)
  • CriteriaResults (331-331)
  • CustomEvalOptions (373-378)
🪛 GitHub Actions: PR Check
packages/testing/src/eval/mcp/providers/anthropic.ts

[warning] 70-70: Unused eslint-disable directive (no problems were reported from '@typescript-eslint/no-explicit-any')

packages/testing/src/eval/reporter/reporter.ts

[warning] 34-40: Unexpected console statement (no-console)

packages/testing/src/eval/mcp/evaluator.ts

[warning] 774-774: Unexpected console statement (no-console).

packages/testing/src/eval/mcp/reporter.ts

[warning] 62-101: Multiple unexpected console statements detected by eslint (no-console)

🪛 markdownlint-cli2 (0.18.1)
examples/minimal/README.md

89-89: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

packages/testing/README.md

724-724: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: claude-review
🔇 Additional comments (43)
examples/minimal/.env.example (1)

1-3: Environment template looks good.

Clear placeholder format with helpful guidance comments. The example value won't accidentally work as a real API key.

examples/minimal/README.md (2)

69-99: Testing section is clear and well-structured.

The new Testing section properly documents test execution and LLM evaluation setup with helpful environment configuration steps and a note about auto-skipping tests when the API key is missing. This makes it easy for contributors to get started.


100-122: Project structure accurately reflects new testing setup.

The updated structure properly documents the new tests/ directory and vitest.config.ts configuration, making it clear to users where test files are located and how the testing framework is configured.

packages/testing/package.json (3)

1-62: LGTM! Well-structured package configuration.

The package metadata, dual ESM/CJS exports, and build configuration follow modern Node.js best practices. The three entry points (root, vitest, jest) are properly configured with types and dual module formats.


63-67: Dependencies and configuration are correct.

The core dependencies are appropriate:

  • Zod ^4.0.0 matches the required version per project learnings
  • MCP SDK and debug are necessary for the testing utilities
  • Node >=20 and sideEffects: false are good choices for a modern package

Also applies to: 92-98


68-74: Outdated peer dependency version constraints.

The version constraints for openai, fast-check, and jest are behind current major versions. This issue was already identified in a previous review.

Likely an incorrect or invalid review comment.

packages/testing/README.md (1)

1-941: LGTM! Comprehensive and well-structured documentation.

The README provides excellent coverage of the testing library's features including test environment setup, assertions/matchers, test suites, property-based testing, UI widget testing, LLM evaluation, and framework integration. The examples are clear and the API reference tables are helpful.

packages/testing/src/server/test-client.ts (2)

37-122: LGTM! Well-implemented retry and timeout handling.

The callToolWithRetry function correctly:

  • Clears the timeout timer on both success and error paths to prevent leaks
  • Only retries on TimeoutError, preserving other error semantics
  • Tracks call history with duration and timestamp metadata
  • Uses defensive copying when returning history

124-205: LGTM! Clean client interface implementation.

The returned client object properly exposes all required methods with appropriate type handling for SDK responses. The disconnect method correctly closes the transport.

packages/testing/src/eval/llm/providers/openai.ts (2)

41-50: LGTM! Proper API key validation and error handling.

The provider correctly validates the API key at creation time and throws a descriptive ConfigurationError if missing.


151-188: LGTM! Well-structured evaluation prompt builder.

The buildEvaluationPrompt function cleanly constructs the evaluation prompt with context, criteria, and expected JSON response format.

packages/testing/src/types.ts (1)

1-549: LGTM! Well-organized and comprehensive type definitions.

The types file provides excellent coverage of all testing utilities with:

  • Clear section organization using comment headers
  • Proper JSDoc documentation for all interfaces
  • No use of any types (uses unknown appropriately where needed)
  • Consistent naming conventions

The use of unknown for app in TestEnvironmentOptions (line 166) is a good choice to avoid circular dependencies with @mcp-apps-kit/core.

packages/testing/src/eval/reporter/index.ts (1)

1-8: LGTM! Clean public API exports following best practices.

The index file correctly:

  • Uses export type for type-only exports (EvalReporterOptions, EvalReportEntry)
  • Exposes only the public API surface
  • Keeps the module boundary clean

As per coding guidelines for **/index.ts files.

packages/testing/src/eval/reporter/types.ts (1)

1-52: LGTM! Well-defined reporter types.

The reporter types provide a clean contract for:

  • Configurable output with verbose mode, colors, and custom streams
  • Structured report entries with comprehensive metadata
  • Output abstraction allowing custom logging implementations
packages/testing/src/eval/mcp/cost-utils.ts (2)

23-37: LGTM! Robust cost parsing with good edge case handling.

The function correctly handles:

  • Undefined/empty input (returns 0)
  • Both dollar ($) and cent (¢) formats
  • NaN parsing results (returns 0)
  • Cent-to-dollar conversion

The JSDoc examples are helpful for understanding expected behavior.


39-48: LGTM! Clean formatting utility.

Simple and effective dollar formatting with configurable precision.

packages/testing/src/eval/mcp/providers/openai.ts (2)

27-35: LGTM on lazy loading and provider initialization.

The lazy loading pattern for the OpenAI SDK and the fallback handling for different export styles (default, OpenAI, or the module itself) is well implemented for handling various bundler configurations.


42-129: LGTM on createCompletion implementation.

The message and tool conversion logic properly handles all message roles including tool responses with tool_call_id. The finish reason determination correctly falls back to tool_calls when tool calls are present even if the API doesn't explicitly return that reason.

examples/minimal/tests/eval.test.ts (3)

28-35: Good practice clearing and printing global collector.

Clearing previous results before tests and printing summary after ensures clean test runs and useful output aggregation.


41-99: LGTM on basic evaluation tests.

The tests properly verify tool calls with correct arguments, token usage tracking, and LLM judgment. Good coverage of single and multiple tool call scenarios.


280-341: LGTM on error injection tests.

Good coverage of both global mock errors and per-run error injection. The tests properly verify that errors are handled gracefully and the LLM reports failures to users.

packages/testing/src/eval/index.ts (1)

1-23: LGTM on module exports.

The barrel file properly uses export type for type-only exports as per coding guidelines, and cleanly organizes the public API surface for the evaluation module.

packages/testing/src/eval/reporter/reporter.ts (2)

47-54: LGTM on color detection.

Proper handling of NO_COLOR standard, CI environments, and TTY detection for determining whether to use ANSI colors.


59-279: LGTM on EvalReporter implementation.

The reporter provides good functionality including:

  • Configurable verbose mode with full/truncated output
  • Per-criterion formatting with pass/fail icons and scores
  • Summary aggregation with failed test listing
  • Immutable returns via spread operators in getEntries() and getSummary()
packages/testing/src/eval/mcp/providers/types.ts (1)

1-77: LGTM on provider type definitions.

Well-structured provider-agnostic types with:

  • Clear JSDoc documentation
  • Strict typing without any (uses Record<string, unknown>)
  • Proper separation of concerns between tool definitions, messages, responses, and provider configuration
  • Extensible name literal union for future providers
packages/testing/src/eval/mcp/session.ts (1)

57-105: LGTM on session state management.

Good implementation patterns:

  • Internal state encapsulated in closure
  • Immutable returns via spread operators in getters
  • Proper aggregation of token usage and estimated costs across turns
packages/testing/src/eval/llm/providers/anthropic.ts (2)

43-52: LGTM on API key validation.

Proper error handling with ConfigurationError when API key is missing, following the project's error handling patterns as per learnings.


248-282: LGTM on prompt construction.

The buildEvaluationPrompt function creates well-structured prompts with clear JSON output format instructions, including context information when available.

packages/testing/src/eval/mcp/providers/index.ts (1)

1-109: LGTM!

The provider factory implementation is well-structured with clear separation of concerns. The lazy provider detection pattern is appropriate for a testing library, and the re-exports follow the index.ts public API pattern correctly.

packages/testing/src/eval/mcp/providers/anthropic.ts (1)

29-44: LGTM on lazy loading and SDK instantiation.

The dynamic import pattern with fallback for different SDK export shapes handles version compatibility well. The eslint-disable blocks are appropriately scoped and justified in comments.

packages/testing/src/eval/mcp/error-injection.ts (2)

72-144: LGTM!

The error injection wrapper correctly implements:

  • Call count tracking before incrementing (allowing errorCount to work as "fail N times then succeed")
  • Probability-based error injection with configurable thresholds
  • Clean delegation pattern for read-only methods
  • Reset capability via the returned interface

186-204: Good deprecation pattern.

The deprecated function properly uses underscore prefix for the unused parameter and provides clear migration guidance in the JSDoc. Consider removing it in a future major version.

packages/testing/src/eval/mcp/evaluator.ts (4)

586-621: Closure mutates totalUsage after result is returned.

The judge() method modifies totalUsage (lines 588-591) via closure after the result object is already returned. This means result.usage won't reflect judge token costs if accessed before judge() is called, but subsequent calls will accumulate. This is likely intentional for multi-call scenarios but could surprise users.

Consider documenting this behavior or returning a fresh usage object from judge().


766-794: LGTM on describeEval implementation.

The auto-skip pattern for missing LLM provider keys is a good DX improvement. The console.warn on line 774 is appropriate for alerting users when no test framework is detected—this is intentional diagnostic output, not debug logging.


818-851: Random port allocation for test isolation.

The port range 3100-3999 is reasonable for parallel test execution. The 100ms delay after server start (line 826) is a simple readiness wait—consider using a more robust health check if flakiness occurs.


438-442: Good infinite loop protection.

The maxIterations = 10 safety limit prevents runaway tool call loops. This is a sensible default for evaluation scenarios.

packages/testing/src/eval/mcp/reporter.ts (1)

50-260: LGTM on MCPEvalReporter class.

The reporter provides well-structured output with appropriate color handling, TTY detection, and CI environment awareness. Console statements are expected and appropriate for this reporting module's purpose.

packages/testing/src/eval/mcp/retry.ts (3)

203-224: Potential token accounting edge case in RateLimiter.

After waiting for a token (lines 217-219), the refill calculation uses waitTime which was computed before sleeping. If actual sleep duration differs (common due to timer precision), token accounting could drift slightly. For a testing library this is acceptable, but worth noting.


149-182: LGTM on withRetry implementation.

The retry logic correctly:

  • Checks isTransientError before retrying
  • Uses configurable backoff strategies with jitter
  • Respects maxAttempts limit
  • The "unreachable" throw on line 180 satisfies TypeScript's control flow analysis

290-317: LGTM on createResilientWrapper.

The composition order (rate limit → timeout → retry) is correct: rate limiting gates entry, timeout bounds individual attempts, and retry handles transient failures across attempts.

packages/testing/src/eval/mcp/batch.ts (2)

122-163: LGTM on tool assertion logic.

The checkToolAssertion helper correctly validates:

  • Tool call count when specified
  • Tool name presence
  • Partial argument matching (only checking specified keys)

The partial match design is appropriate for flexible test assertions.


190-362: LGTM on runBatch implementation.

The batch runner handles both parallel and sequential execution modes correctly, with proper progress reporting, cost aggregation, and summary generation. The sequential mode's stopOnFailure option provides useful control for fail-fast scenarios.

packages/testing/src/index.ts (1)

1-175: Excellent public API organization and TypeScript best practices.

This index file demonstrates strong TypeScript patterns and excellent organization:

  • ✅ Correctly uses export type for type-only exports (lines 21-97, 138) and regular export for runtime values/classes (lines 103-171)
  • ✅ Clear section markers separate concerns (types, errors, debug, main API)
  • ✅ Properly splits value and type exports from the same module (e.g., lines 137-138 from "./eval/behavior")
  • ✅ Comprehensive JSDoc with usage example (lines 1-15)
  • ✅ Documents subpath exports in comments (lines 173-175)

The file follows all coding guidelines and learnings, providing a clean, well-structured public API surface.

As per coding guidelines: correctly exports only public API and uses export type for type-only exports.

Comment on lines +88 to +91

```
OPENAI_API_KEY=sk-your-actual-api-key
```

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add language identifier to fenced code block.

The code fence starting at line 88 is missing a language specifier. While this is a minor markdown linting issue, it should be fixed for consistency with the other code block at line 73.

📝 Proposed fix for markdown formatting
 2. Add your OpenAI API key to `.env`:

-   ```
+   ```bash
    OPENAI_API_KEY=sk-your-actual-api-key
    ```
🧰 Tools
🪛 markdownlint-cli2 (0.18.1)

89-89: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
In @examples/minimal/README.md around lines 88 - 91, The fenced code block
containing "OPENAI_API_KEY=sk-your-actual-api-key" is missing a language
identifier; update the opening triple-backtick for that block to include a
language (e.g., "bash") so it matches the other block and satisfies markdown
linting/consistency.

Comment on lines +84 to +109
// Parse response
const parsed = JSON.parse(content) as {
criteria: Array<{ name: string; score: number; explanation: string }>;
overall: { score: number };
};

// Build evaluation result as a record keyed by criterion name
const criteriaResults: CriteriaResults = {};
for (const criterion of options.criteria) {
const result = parsed.criteria.find((c) => c.name === criterion.name);
if (!result) {
throw new Error(`Missing evaluation result for criterion: ${criterion.name}`);
}

const threshold = criterion.threshold ?? 0.7;
criteriaResults[criterion.name] = {
name: criterion.name,
score: result.score,
pass: result.score >= threshold,
explanation: result.explanation,
};
}

const overallScore = parsed.overall.score;
const passThreshold = 0.7; // Default overall threshold
const overallPass = overallScore >= passThreshold;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add runtime validation for parsed JSON response structure.

The parsed JSON response is cast directly to the expected type without validation. If the LLM returns valid JSON with an unexpected structure (e.g., missing criteria array or overall object), this will cause confusing runtime errors like TypeError: Cannot read properties of undefined.

🔧 Suggested validation pattern
       // Parse response
-      const parsed = JSON.parse(content) as {
-        criteria: Array<{ name: string; score: number; explanation: string }>;
-        overall: { score: number };
-      };
+      let parsed: unknown;
+      try {
+        parsed = JSON.parse(content);
+      } catch {
+        throw new Error(`Invalid JSON response from OpenAI: ${content.slice(0, 200)}`);
+      }
+
+      // Validate structure
+      const parsedObj = parsed as Record<string, unknown>;
+      if (!Array.isArray(parsedObj.criteria)) {
+        throw new Error(`Invalid response structure: missing or invalid 'criteria' array`);
+      }
+      if (!parsedObj.overall || typeof (parsedObj.overall as Record<string, unknown>).score !== "number") {
+        throw new Error(`Invalid response structure: missing or invalid 'overall.score'`);
+      }
+
+      const criteriaArray = parsedObj.criteria as Array<{ name: string; score: number; explanation: string }>;
+      const overallScore = (parsedObj.overall as { score: number }).score;

       // Build evaluation result as a record keyed by criterion name
       const criteriaResults: CriteriaResults = {};
       for (const criterion of options.criteria) {
-        const result = parsed.criteria.find((c) => c.name === criterion.name);
+        const result = criteriaArray.find((c) => c.name === criterion.name);

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In @packages/testing/src/eval/llm/providers/openai.ts around lines 84 - 109, The
code blindly casts JSON.parse(content) to the expected shape and then assumes
parsed.criteria and parsed.overall exist; add runtime validation after parsing
to guard against malformed responses by checking parsed is an object,
parsed.criteria is an array, each parsed.criteria item has name (string), score
(number) and explanation (string), and parsed.overall exists with a numeric
score before using them in the loop that builds CriteriaResults and before
computing overallScore; if any check fails, throw a clear Error that includes
the raw content (or a snippet) and a message like "Invalid LLM response: missing
or malformed 'criteria'/'overall'". Ensure you reference and validate parsed
(the variable from JSON.parse), parsed.criteria, parsed.overall, and the loop
over options.criteria/CriteriaResults to avoid TypeError at runtime.

Comment on lines +70 to +71
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const content: any[] = [];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Remove unused eslint-disable directive.

The pipeline indicates this eslint-disable is unnecessary. Remove it to keep the codebase clean.

-            // eslint-disable-next-line @typescript-eslint/no-explicit-any
             const content: any[] = [];
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const content: any[] = [];
const content: any[] = [];
🧰 Tools
🪛 GitHub Actions: PR Check

[warning] 70-70: Unused eslint-disable directive (no problems were reported from '@typescript-eslint/no-explicit-any')

🤖 Prompt for AI Agents
In @packages/testing/src/eval/mcp/providers/anthropic.ts around lines 70 - 71,
Remove the unused eslint-disable directive preceding the declaration of the
content array: delete the line "// eslint-disable-next-line
@typescript-eslint/no-explicit-any" and leave the declaration "const content:
any[] = [];" as-is (or replace the any type with a more specific type if
desired) so the file no longer contains the unnecessary eslint-disable comment.

Comment on lines +117 to +119
end(): void {
this.reset();
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Potential issue with this context in end() method.

Using this.reset() inside an object literal method relies on the caller invoking end() with the correct this context. If end is destructured or passed as a callback, this may be undefined.

🔧 Safer implementation
     end(): void {
-      this.reset();
+      history = [];
+      results = [];
+      aggregatedUsage = {
+        promptTokens: 0,
+        completionTokens: 0,
+        totalTokens: 0,
+      };
     },

Alternatively, define reset as a closure variable and call it directly:

+  const resetSession = (): void => {
+    history = [];
+    results = [];
+    aggregatedUsage = {
+      promptTokens: 0,
+      completionTokens: 0,
+      totalTokens: 0,
+    };
+  };
+
   return {
     // ... other methods ...
     reset(): void {
-      history = [];
-      results = [];
-      aggregatedUsage = {
-        promptTokens: 0,
-        completionTokens: 0,
-        totalTokens: 0,
-      };
+      resetSession();
     },

     end(): void {
-      this.reset();
+      resetSession();
     },
   };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
end(): void {
this.reset();
},
end(): void {
history = [];
results = [];
aggregatedUsage = {
promptTokens: 0,
completionTokens: 0,
totalTokens: 0,
};
},
🤖 Prompt for AI Agents
In @packages/testing/src/eval/mcp/session.ts around lines 117 - 119, The end()
method calls this.reset() which will break if end is extracted or used as a
callback; change its implementation so it doesn't rely on dynamic this: either
convert end into a bound arrow property (e.g., end = () => { this.reset(); }) or
move reset into a closure/local function and call that directly from end; update
the declaration of end() in the same object/class where reset() is defined to
use one of these approaches so calling end without the original this still
invokes reset.

Comment on lines +32 to +42
const defaultOutput: EvalReporterOutput = {
log: (msg: string) => {
console.log(msg);
},
warn: (msg: string) => {
console.warn(msg);
},
error: (msg: string) => {
console.error(msg);
},
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Address ESLint no-console warning from pipeline.

The pipeline reports a warning for unexpected console statements. Since this is a reporter that intentionally outputs to console, consider suppressing the lint rule for this specific block.

🔧 Suggested fix
 /**
  * Default console output implementation
  */
+/* eslint-disable no-console */
 const defaultOutput: EvalReporterOutput = {
   log: (msg: string) => {
     console.log(msg);
   },
   warn: (msg: string) => {
     console.warn(msg);
   },
   error: (msg: string) => {
     console.error(msg);
   },
 };
+/* eslint-enable no-console */
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const defaultOutput: EvalReporterOutput = {
log: (msg: string) => {
console.log(msg);
},
warn: (msg: string) => {
console.warn(msg);
},
error: (msg: string) => {
console.error(msg);
},
};
/* eslint-disable no-console */
const defaultOutput: EvalReporterOutput = {
log: (msg: string) => {
console.log(msg);
},
warn: (msg: string) => {
console.warn(msg);
},
error: (msg: string) => {
console.error(msg);
},
};
/* eslint-enable no-console */
🧰 Tools
🪛 GitHub Actions: PR Check

[warning] 34-40: Unexpected console statement (no-console)

🤖 Prompt for AI Agents
In @packages/testing/src/eval/reporter/reporter.ts around lines 32 - 42, The
ESLint no-console warning comes from the console calls in the defaultOutput
object; suppress it by disabling the rule only for this block (e.g., add a
block-level suppression like /* eslint-disable no-console */ immediately before
the defaultOutput definition and /* eslint-enable no-console */ immediately
after) so the EvalReporterOutput defaultOutput (log/warn/error) can
intentionally use console without triggering pipeline warnings.

Comment on lines +54 to +70
/**
* Summary of all evaluations
*/
export interface EvalReportSummary {
/** Total evaluations run */
total: number;
/** Evaluations that passed */
passed: number;
/** Evaluations that failed */
failed: number;
/** Average overall score */
averageScore: number;
/** Total duration in ms */
totalDuration: number;
/** Individual entries */
entries: EvalReportEntry[];
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Check usages of EvalReportSummary across the codebase
rg -n "EvalReportSummary" --type=ts -C2

Repository: AndurilCode/mcp-apps-kit

Length of output: 1632


🏁 Script executed:

# Check what's exported from the testing package's index.ts
fd -n "index.ts" packages/testing/src -type f | head -5

Repository: AndurilCode/mcp-apps-kit

Length of output: 298


🏁 Script executed:

# Examine the main index.ts files in the testing package
find packages/testing/src -name "index.ts" -type f | xargs cat

Repository: AndurilCode/mcp-apps-kit

Length of output: 12319


Export EvalReportSummary from the public API.

EvalReportSummary is the return type of public methods printSummary() and getSummary() on the exported EvalReporter class, but it's not exported from packages/testing/src/index.ts. Consumers need access to this type to properly type their code. Add it to the eval/reporter exports in index.ts.

Additionally, consider using export type syntax instead of export interface to explicitly mark it as a type-only export, per the TypeScript guidelines.

🤖 Prompt for AI Agents
In @packages/testing/src/eval/reporter/types.ts around lines 54 - 70, The
EvalReportSummary type is used as the return type for EvalReporter methods
printSummary() and getSummary() but isn't exported from the package public API;
export it from the eval/reporter exports in your public index (add
EvalReportSummary to the exported symbols alongside EvalReporter) so consumers
can import it, and optionally convert the declaration from "export interface
EvalReportSummary" to "export type EvalReportSummary = { ... }" to mark it as a
type-only export per TypeScript guidelines.

- Clean up the error-injection.ts file by removing unnecessary trailing whitespace, improving code readability and consistency.
@claude

claude Bot commented Jan 7, 2026

Copy link
Copy Markdown
Contributor

Code Review: Testing Library Implementation

This PR introduces a comprehensive testing framework for MCP applications. Overall, this is a high-quality implementation with excellent architecture and thorough testing. Below is detailed feedback organized by category.


Strengths

Architecture & Design

  • Excellent lazy loading pattern (lazy-loader.ts): The optional dependency management is well-designed with proper caching and error handling
  • Strong separation of concerns: Test server, client, matchers, and evaluators are cleanly separated
  • Provider abstraction: The multi-provider LLM support (OpenAI/Anthropic) is well-architected
  • Comprehensive error hierarchy: Custom error types provide good context and debugging info

Code Quality

  • TypeScript strict mode compliance: No unsafe any types except where interfacing with dynamic SDK modules (properly documented with eslint-disable comments)
  • Memory leak prevention: Proper cleanup of timeout handlers (test-server.ts:108-109, test-client.ts:65-68, 103-105)
  • Resilience patterns: Retry logic, rate limiting, and error injection for production-like testing
  • Good documentation: Extensive JSDoc comments and comprehensive README

Testing

  • 17 test files covering unit, integration, and contract tests
  • Examples demonstrate usage: The minimal example tests showcase the library's capabilities well

🔍 Issues & Concerns

1. Security: Command Injection Risk ⚠️

Location: packages/testing/src/server/test-server.ts:150

const childProcess = spawn(command, args, {
  env: { ...process.env, ...env },  // User-controlled env vars
  stdio: ['ignore', 'pipe', 'pipe'],
});

Issue: The ExternalServerOptions.env allows arbitrary environment variable injection without sanitization. While this is a testing library, malicious tests could potentially leak secrets or execute unintended code.

Recommendation: Add a warning in documentation about environment variable handling, or consider validating/sanitizing environment variable names.


2. Potential Memory Leak in External Server

Location: packages/testing/src/server/test-server.ts:174-177

while (!serverReady && Date.now() - startTime < timeout) {
  await new Promise((resolve) => setTimeout(resolve, 100));
}

Issue: If the child process exits early (line 179-188), the stdout/stderr listeners (lines 159-172) are never cleaned up.

Recommendation: Add childProcess.removeAllListeners() in error paths.


3. Error Handling: Generic Catch Block

Location: Multiple files including packages/testing/src/utils/lazy-loader.ts:73

} catch {
  // Empty catch - only throws ConfigurationError
}

Issue: Silently catching all errors makes debugging harder when the real issue isn't a missing dependency (e.g., syntax errors in the imported module).

Recommendation: Log or inspect the error before replacing it with ConfigurationError.


4. Test Coverage Gaps

Missing Coverage:

  • Error injection edge cases (what happens when mockErrors probability is invalid?)
  • Concurrent test client usage (race conditions in history tracking?)
  • Rate limiting boundary conditions

Recommendation: Add property-based tests for numerical boundaries (probabilities, timeouts, retry counts).


5. Type Safety: Loose Any Types

Location: packages/testing/src/eval/mcp/providers/anthropic.ts:36-39, 59-60

Issue: While documented with eslint-disable comments, the extensive use of any when interfacing with Anthropic SDK reduces type safety.

Recommendation: Consider creating minimal type definitions for the Anthropic SDK interface you actually use, rather than relying on any.


6. Console Usage in Production Code

Locations: Multiple files including reporter.ts, evaluator.ts:774

Issue: Direct console.log/warn/error calls bypass the debug logger infrastructure and can't be controlled by users.

Example:

// packages/testing/src/eval/mcp/evaluator.ts:774
console.warn("[describeEval] No test framework detected. Skipping:", name);

Recommendation: Use the llmLogger or testingLogger consistently. The reporters should optionally use console, but make it configurable.


7. Unbounded Resource Usage

Location: packages/testing/src/eval/mcp/batch.ts

Issue: No visible limit on batch size. Running 10,000+ evaluations could exhaust memory/API quotas.

Recommendation: Add batch size validation or at least document limits in README.


8. Missing Cleanup in TestEnvironmentBuilder

Location: packages/testing/src/ui/test-environment.ts

Issue: The TestEnvironmentBuilder creates resources but doesn't guarantee cleanup if .build() throws partway through.

Recommendation: Implement cleanup in a try-catch or use a resource disposal pattern.


9. Naming Inconsistency

  • describeEval vs runTestSuite vs forAllInputs - different naming conventions
  • Some functions use create* prefix, others don't

Recommendation: Standardize on a consistent naming pattern (e.g., create* for factories, run* for executors).


10. Documentation: Missing Migration Guide

Issue: No guide for migrating from other MCP testing approaches to this library.

Recommendation: Add a "Migration" section to README showing before/after examples.


🎯 Performance Considerations

Positive

  • Lazy loading reduces initial bundle size ✅
  • Client caching prevents redundant SDK initialization ✅
  • Proper use of Promise.race for timeouts ✅

Concerns

  • Sequential batch processing: batch.ts appears to run evaluations sequentially. For large batches, consider parallel execution with concurrency limits.
  • History tracking overhead: When trackHistory: true, every call creates new array entries. Consider a circular buffer for long-running tests.

🔒 Security Assessment

Low Risk

  • No SQL injection vectors (no database layer)
  • No XSS vectors (no HTML generation)
  • Proper error sanitization in most places

Medium Risk

  1. Environment variable injection in external server spawning (see Issue Add Claude Code GitHub Workflow #1)
  2. API key exposure risk: API keys are passed around as strings. Consider using a secure credential store pattern.
  3. Test code could call arbitrary tools: While expected for testing, malicious MCP servers could exploit test clients. Document security boundaries.

📊 Test Coverage

Estimated Coverage: ~85-90% based on file count and test structure (excellent!)

Gaps:

  • Edge cases in error handling
  • Concurrent usage scenarios
  • Boundary conditions in rate limiting/retry logic

🚀 Recommendations

High Priority

  1. Fix command injection risk - Add documentation warning or sanitization
  2. Clean up event listeners in external server error paths
  3. Replace console usage with debug loggers

Medium Priority

  1. Add batch size limits/validation
  2. Improve error logging in lazy loader
  3. Standardize naming conventions
  4. Add migration guide to docs

Low Priority

  1. Consider parallel batch execution
  2. Optimize history tracking for large test suites
  3. Create minimal type definitions for SDK interfaces

📝 Adherence to CLAUDE.md

Follows conventions:

  • Strict TypeScript with no unintended any types
  • No unused variables
  • Proper public API exports in index.ts
  • Tool definition pattern using defineTool/defineUI
  • Zod schema validation
  • 80%+ test coverage

⚠️ Minor deviations:


🎉 Conclusion

This is an excellent PR that adds significant value to the MCP AppsKit ecosystem. The architecture is solid, the code quality is high, and the test coverage is comprehensive.

Recommendation: Approve with minor revisions

The issues identified are mostly minor and can be addressed in follow-up PRs. The security concerns are low-risk given this is a testing library, but should be documented.

Great work! 🚀


Suggested Next Steps

  1. Address high-priority security/memory leak issues
  2. Add batch size validation
  3. Update documentation with security notes
  4. Consider adding a SECURITY.md file for the testing package

Let me know if you'd like me to elaborate on any of these points or if you need help implementing specific fixes.

@gabrypavanello
gabrypavanello merged commit 46aaff8 into main Jan 7, 2026
3 of 4 checks passed
@gabrypavanello
gabrypavanello deleted the 001-testing-library branch January 7, 2026 22:19
@claude

claude Bot commented Jan 7, 2026

Copy link
Copy Markdown
Contributor

Code Review: Testing Library Implementation

Overall Assessment

This is an impressive and comprehensive testing library implementation. The code is well-structured, thoroughly documented, and demonstrates strong engineering practices. The scope is ambitious (~13,800 additions) but well-executed.

✅ Strengths

1. Excellent Architecture

  • Clean separation of concerns across modules
  • Lazy loading pattern for optional dependencies
  • Proper error hierarchy with custom error types
  • Good use of TypeScript type system

2. Documentation

  • Comprehensive README with examples
  • Inline JSDoc comments throughout
  • Good test examples in examples/minimal/tests/
  • Clear .env.example for configuration

3. Code Quality

  • Proper use of debug loggers (mostly)
  • Good error handling with custom error classes
  • Resilience patterns (retry, rate limiting, error injection)
  • Clean async/await usage

4. Testing Coverage

  • Extensive test suite (unit, integration, contract)
  • Property-based testing support
  • LLM evaluation capabilities
  • Mock host environment for UI testing

🔍 Critical Issues

1. Use of any type in Anthropic provider

  • Location: packages/testing/src/eval/mcp/providers/anthropic.ts:36-37, 59, 71
  • Issue: Violates CLAUDE.md strict TypeScript requirement
  • Fix: Replace with unknown and type guards

2. Console.log usage

  • Found in 6 files (mock-host.ts, reporters, evaluator.ts)
  • Fix: Replace with debug loggers per project guidelines

3. Error swallowing in lazy-loader

  • Location: packages/testing/src/utils/lazy-loader.ts:73
  • Issue: Empty catch blocks lose error context
  • Fix: Capture and include error details

4. Race condition in cachedClientFactory

  • Location: packages/testing/src/utils/lazy-loader.ts:139-144
  • Issue: No .catch() on promise - pendingPromise may stay set on error
  • Fix: Add error handler to clear pendingPromise

📋 Medium Priority

5. Peer dependency version

  • packages/testing/package.json:70 - fast-check: ^3.0.0 but examples use v4
  • Fix: Update to ^4.0.0

6. Test environment detection

  • examples/minimal/src/index.ts:169 uses both NODE_ENV and VITEST
  • Consider: Document why both needed or simplify

7. Hardcoded LLM pricing

  • examples/minimal/tests/eval.test.ts:48
  • Consider: Extract to config with date/source comment

🎯 Security & Best Practices ✅

  • No hardcoded secrets
  • Proper environment variable usage
  • Good error messages without leaks
  • Lazy loading reduces bundle size
  • Proper async patterns
  • Clear module boundaries

Actionable Summary

Must Fix (2-3 hours):

  1. Remove any types - use unknown with type guards
  2. Replace console.log with debug loggers
  3. Fix error handling in lazy-loader
  4. Fix cachedClientFactory race condition

Should Fix:
5. Update fast-check to ^4.0.0
6. Document or simplify environment detection

Nice to Have:
7. Extract LLM pricing constants
8. Remove .cursor/ from gitignore


🎉 Conclusion

Production-ready with minor fixes needed. Excellent architecture, solid testing, great documentation. Once critical items addressed, this will be a valuable addition to mcp-apps-kit.

Test coverage is exemplary. Maintain 80% target per CLAUDE.md.

Great work! 🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants