agentleFS
ficaviolaodorata520ficaviolaodorata520/met-museum-mcp-server/skills/add-test/SKILL.md

add-test

Skill0 starsChanged 6 months agoApache-2.0View on GitHub

Scaffold a test file for an existing tool, resource, or service. Use when the user asks to add tests, improve coverage, or when a definition exists without a matching test file.

18 other projects use a version of this.

Adopted by

Most recently taken up by

Used with

Projects with it also use review-pr, triage and 1 more.

Other files people use alongside this one.

Files found in the same public projects as a version of this one, more often than they turn up anywhere else. Each project counts once, large collections count for less, and this file's own repository is left out.
  • DockerSkill

    review-pr

    Review one or more incoming Docker documentation pull requests as a maintainer. Independently validate technical claims, assess editorial fit and information architecture, choose a verdict, and draft exact inline or PR-wide feedback behind a confirmation gate. Use for requests such as "review PR 123", "is this PR correct?", "does this information belong here?", "validate this PR", or "help review backlog PRs". Do not use to maintain or fix a PR you own; use maintain-pr for that.

    Also in 3 projects that use this

  • Google CloudSkill

    triage

    Gather context on a change or bug and plan the implementation.

    Also in 3 projects that use this

  • SentrySkill

    review

    Fixture review skill.

    Also in 3 projects that use this

The file

We split this skill into the rules it gives an agent

3 rules. 0 are common, 2 are distinctive, and 1 is very specific to this project.

  • 0

    Common

    Most of the 17 edited versions we read keep these.

  • 2

    Distinctive

    This author's own choices. Worth a look.

  • 1

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Read the handler and identify:

    line 31
  2. Very specific:

    If you need to test the accessor's "not initialized" guard, do it in a separate isolated-module test (vi.resetModules() before importing …

    line 184
  3. Distinctive:

    When scaffolding tests for an existing handler, use the Zod schemas to generate meaningful test cases:

    line 272

“Common” is measured: kept by at least 6 in 10 of the 17 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Context

Tests use Vitest and createMockContext from @cyanheads/mcp-ts-core/testing. If the repo already has tests, match the existing layout. If the repo has no existing tests, create a root tests/ directory that mirrors the src/ structure (e.g. tests/mcp-server/tools/definitions/echo.tool.test.ts for src/mcp-server/tools/definitions/echo.tool.ts).

For the full createMockContext API and testing patterns, read:

skills/api-testing/SKILL.md

Steps

  1. Identify the target — which tool, resource, or service needs tests
  2. Read the source file — understand the handler's logic, input/output schemas, error paths, and which ctx features it uses
  3. Create the test file in the repo's existing test layout — search for existing *.test.ts files to confirm whether tests are colocated with source or under a root tests/ directory
  4. Write test cases covering happy path, error paths, and edge cases
  5. Run bun run test to verify
  6. Run bun run devcheck to verify lint, types, and MCP definitions

Determining What to Test

Read the handler and identify:

AspectTest Strategy
Happy pathValid input → expected output. Include at least one.
Input variationsOptional fields omitted, defaults applied, boundary values
Error pathsInvalid state, missing resources, service failures → correct error thrown
ctx.state usageUse createMockContext({ tenantId: 'test' }) to enable storage
ctx.elicit / ctx.sampleMock with vi.fn(), also test the absent case (undefined)
ctx.progressUse createMockContext({ progress: true }) for task tools
ctx.fail (typed contract)Definitions with errors[] need fail attached to the mock ctx — createMockContext({ errors: myTool.errors }) does it for you. Assert on data.reason (stable per-contract entry), not just code.
format functionTest separately if defined — it's pure, no ctx needed. Verify it renders the IDs and fields the model needs, not just a count or title. For projection-style tools, test non-default field selections.
Sparse upstream payloadsFor third-party API integrations, build a fixture with omitted fields. Assert normalized output still validates and format() preserves unknown values instead of inventing facts.
Form-client payloadsIf handler has optional fields: test with empty-string inner values (form clients send "" instead of undefined). Assert handler doesn't break or produce invalid output.
Auth scopesNot tested at handler level (framework enforces) — skip

Templates

Tool test

/**
 * @fileoverview Tests for {{TOOL_NAME}} tool.
 * @module tests/tools/{{TOOL_NAME}}.tool.test
 */

import { describe, expect, it } from 'vitest';
import { createMockContext } from '@cyanheads/mcp-ts-core/testing';
import { {{TOOL_EXPORT}} } from '@/mcp-server/tools/definitions/{{tool-name}}.tool.js';

describe('{{TOOL_EXPORT}}', () => {
  it('returns expected output for valid input', async () => {
    const ctx = createMockContext();
    const input = {{TOOL_EXPORT}}.input.parse({
      // valid input matching the Zod schema
    });
    const result = await {{TOOL_EXPORT}}.handler(input, ctx);
    expect(result).toMatchObject({
      // expected output shape
    });
  });

  it('throws on invalid state', async () => {
    const ctx = createMockContext();
    const input = {{TOOL_EXPORT}}.input.parse({
      // input that triggers an error path
    });
    await expect({{TOOL_EXPORT}}.handler(input, ctx)).rejects.toThrow();
  });

  // Only when the tool declares `errors: [...]`. Drop this block otherwise.
  it('throws ctx.fail("{{REASON}}") for the declared failure mode', async () => {
    const ctx = createMockContext({ errors: {{TOOL_EXPORT}}.errors });
    const input = {{TOOL_EXPORT}}.input.parse({
      // input that triggers the declared failure mode
    });
    await expect({{TOOL_EXPORT}}.handler(input, ctx)).rejects.toMatchObject({
      data: { reason: '{{REASON}}' },
    });
  });

  it('formats output completely', () => {
    const output = { /* mock output matching the output schema */ };
    const blocks = {{TOOL_EXPORT}}.format!(output);
    expect(blocks.some((block) => block.type === 'text')).toBe(true);
    // Assert the rendered text includes the IDs/fields the LLM needs to act on.
  });
});

Resource test

/**
 * @fileoverview Tests for {{RESOURCE_NAME}} resource.
 * @module tests/resources/{{RESOURCE_NAME}}.resource.test
 */

import { describe, expect, it } from 'vitest';
import { createMockContext } from '@cyanheads/mcp-ts-core/testing';
import { {{RESOURCE_EXPORT}} } from '@/mcp-server/resources/definitions/{{resource-name}}.resource.js';

describe('{{RESOURCE_EXPORT}}', () => {
  it('returns data for valid params', async () => {
    const ctx = createMockContext({ tenantId: 'test-tenant' });
    const params = {{RESOURCE_EXPORT}}.params.parse({
      // valid params matching the Zod schema
    });
    const result = await {{RESOURCE_EXPORT}}.handler(params, ctx);
    expect(result).toBeDefined();
  });

  it('throws when resource not found', async () => {
    const ctx = createMockContext({ tenantId: 'test-tenant' });
    const params = {{RESOURCE_EXPORT}}.params.parse({
      // params for a non-existent resource
    });
    await expect({{RESOURCE_EXPORT}}.handler(params, ctx)).rejects.toThrow();
  });

  // For resources that declare an `errors: [...]` contract, pass the contract via
  // `createMockContext` so the typed `ctx.fail` is wired automatically:
  //   const ctx = createMockContext({ errors: {{RESOURCE_EXPORT}}.errors });
  //   const err = await {{RESOURCE_EXPORT}}.handler(params, ctx).catch((e) => e);
  //   expect(err.code).toBe(JsonRpcErrorCode.NotFound);
  //   expect(err.data.reason).toBe('no_match');

  // Include this block only when the resource definition exports a `list` function.
  // Check the source — `list` is optional on resource definitions.
  it('lists available resources', async () => {
    const listing = await {{RESOURCE_EXPORT}}.list!();
    expect(listing.resources).toBeInstanceOf(Array);
    expect(listing.resources.length).toBeGreaterThan(0);
    for (const r of listing.resources) {
      expect(r).toHaveProperty('uri');
      expect(r).toHaveProperty('name');
    }
  });
});

Service test

/**
 * @fileoverview Tests for {{SERVICE_NAME}} service.
 * @module tests/services/{{domain}}/{{domain}}-service.test
 */

import { beforeEach, describe, expect, it } from 'vitest';
import { createMockContext } from '@cyanheads/mcp-ts-core/testing';
import { StorageService } from '@cyanheads/mcp-ts-core/storage';
import { get{{ServiceClass}}, init{{ServiceClass}} } from '@/services/{{domain}}/{{domain}}-service.js';

// Derive the minimal mock config from src/config/server-config.ts — read
// the server's Zod schema to see which fields init{{ServiceClass}}() needs.
const mockConfig = { /* fields from server config schema */ } as AppConfig;

describe('{{ServiceClass}}', () => {
  beforeEach(async () => {
    const mockStorage = await StorageService.create({ type: 'in-memory' });
    init{{ServiceClass}}(mockConfig, mockStorage);
  });

  it('performs the expected operation', async () => {
    const ctx = createMockContext({ tenantId: 'test-tenant' });
    const service = get{{ServiceClass}}();
    const result = await service.doWork('input', ctx);
    expect(result).toBeDefined();
  });
});

If you need to test the accessor's "not initialized" guard, do it in a separate isolated-module test (vi.resetModules() before importing the service module). Don't mix that assertion into a suite that already calls init{{ServiceClass}}() in beforeEach().

Task tool test

For tools with task: true, use createMockContext({ progress: true }):

it('reports progress during execution', async () => {
  const ctx = createMockContext({ progress: true });
  const input = {{TOOL_EXPORT}}.input.parse({ count: 3, delayMs: 10 });
  await {{TOOL_EXPORT}}.handler(input, ctx);

  const progress = ctx.progress as ContextProgress & {
    _total: number;
    _completed: number;
    _messages: string[];
  };
  expect(progress._total).toBe(3);
  expect(progress._completed).toBe(3);
});

it('respects cancellation', async () => {
  const controller = new AbortController();
  const ctx = createMockContext({ progress: true, signal: controller.signal });
  const input = {{TOOL_EXPORT}}.input.parse({ count: 100, delayMs: 10 });

  // Abort after a short delay
  setTimeout(() => controller.abort(), 50);
  const result = await {{TOOL_EXPORT}}.handler(input, ctx);

  // Should have returned a partial result rather than throwing on cancellation.
  // Assert on a field from the tool's actual output schema.
  expect(result).toBeDefined();
});

Prompt test

/**
 * @fileoverview Tests for {{PROMPT_NAME}} prompt.
 * @module tests/prompts/{{PROMPT_NAME}}.prompt.test
 */

import { describe, expect, it } from 'vitest';
import { {{PROMPT_EXPORT}} } from '@/mcp-server/prompts/definitions/{{prompt-name}}.prompt.js';

describe('{{PROMPT_EXPORT}}', () => {
  it('generates valid messages for valid args', () => {
    const args = {{PROMPT_EXPORT}}.args!.parse({
      // valid args matching the Zod schema
    });
    const messages = {{PROMPT_EXPORT}}.generate(args);
    expect(messages).toBeInstanceOf(Array);
    expect(messages.length).toBeGreaterThan(0);
    for (const msg of messages) {
      expect(msg).toHaveProperty('role');
      expect(msg).toHaveProperty('content');
    }
  });

  // Include only when the prompt has no required args (args is optional or all fields optional).
  it('generates messages with no args', () => {
    const messages = {{PROMPT_EXPORT}}.generate({});
    expect(messages.length).toBeGreaterThan(0);
  });
});

Fuzz Testing

For schema-heavy or input-validation-critical handlers, the framework ships fuzz helpers that generate valid + adversarial inputs from your Zod schemas via fast-check and assert handler invariants (no crashes, no prototype pollution, no stack-trace leaks):

import { fuzzTool } from '@cyanheads/mcp-ts-core/testing/fuzz';

it('survives fuzz testing', async () => {
  const report = await fuzzTool({{TOOL_EXPORT}}, { numRuns: 100 });
  expect(report.crashes).toHaveLength(0);
  expect(report.leaks).toHaveLength(0);
  expect(report.prototypePollution).toBe(false);
});

Available helpers from @cyanheads/mcp-ts-core/testing/fuzz: fuzzTool, fuzzResource, fuzzPrompt, zodToArbitrary (custom property-based tests), adversarialArbitrary and ADVERSARIAL_STRINGS (targeted injection sets). Returns a FuzzReport you can assert against. Options: numRuns, numAdversarial, seed (reproducibility), timeout, ctx (MockContextOptions for stateful handlers).

Generating Tests from Schemas

When scaffolding tests for an existing handler, use the Zod schemas to generate meaningful test cases:

  1. Read input schema — identify required fields, optional fields with defaults, constrained types (enums, min/max, patterns)
  2. Read output schema — know what shape to assert against
  3. Happy path — construct the simplest valid input, assert output matches schema
  4. Defaults — omit optional fields, verify defaults are applied in the output
  5. Boundaries — if the schema has .min(), .max(), .length(), test at the boundaries
  6. Error paths — trace the handler logic for throw conditions, construct inputs that trigger each
  7. Sparse upstream fixtures — if the handler/service wraps a third-party API, add at least one fixture where upstream omits optional fields entirely. Assert that the output still validates and that format() renders uncertainty honestly (Not available, omitted badge, etc.) instead of fabricating values.

Checklist

  • Test file created in the repo's existing layout (tests/... or colocated with source)
  • JSDoc @fileoverview and @module header present
  • Happy path tested with valid input → expected output
  • Error paths tested (at least one .rejects.toThrow())
  • format function tested if defined
  • createMockContext options match handler's ctx usage (tenantId, progress, elicit, sample)
  • Service re-initialized in beforeEach if handler depends on a service singleton
  • If handler has optional fields: tested with empty-string inner values (form-client simulation)
  • If wrapping external API: sparse-payload case tested — fixture omits at least one optional upstream field; output still validates and format() renders uncertainty honestly instead of inventing values
  • If target is a prompt: generate() tested with valid args and (when applicable) no args
  • bun run test passes
  • bun run devcheck passes

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.