Skip to main content

add-test

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.

Ir a la instalación

Datos de origen

Repositorio
cyanheads/obsidian-mcp-server
Última actividad en el origen
13 de septiembre de 2026 a las 18:16
Idioma detectado de SKILL.md
inglés
Estrellas
682
Forks
103

Opciones de instalación

De forma predeterminada está seleccionado el prompt que primero revisa el origen. Puedes cambiar a un comando directo o descargar una copia local.

Revisa los archivos de origen

Lee SKILL.md y los archivos complementarios que muestra SkillsMP antes de decidir si quieres instalarlo.

Mostrando SKILL.md

SKILL.md
Instrucciones de origen · Vista previa de solo lectura
name
add-test
description
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.
metadata
{"author":"cyanheads","version":"1.7","audience":"external","type":"reference"}
## 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: framework-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: | Aspect | Test Strategy | |:-------|:-------------| | **Happy path** | Valid input → expected output. Include at least one. | | **Input variations** | Optional fields omitted, defaults applied, boundary values | | **Error paths** | Invalid state, missing resources, service failures → correct error thrown | | **`ctx.state` usage** | Available on any mock context (tenant `'default'` unless `{ tenantId }` says otherwise). It runs the production storage path, so use storage-legal keys (`cache/v1/abc`, never `cache:v1:abc`) and assert TTL expiry with fake timers. | | **`ctx.requestInput` / `ctx.inputs`** | Two rounds. First round: assert the handler throws the input-required signal (`.rejects.toSatisfy(isInputRequiredSignal)`), or catch it and assert on `error.result.inputRequests`. Second round: seed `createMockContext({ inputResponses })` and assert the handler completes. Cover the decline/cancel branch too. | | **`ctx.signal`** | Pass `createMockContext({ signal: controller.signal })` and assert a long loop stops early rather than running to completion. | | **`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` function** | Test 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 payloads** | For 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 payloads** | If 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 scopes** | Not tested at handler level (framework enforces) — skip | ## Templates ### Tool test ```typescript /** * @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 ```typescript /** * @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 ```typescript /** * @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()`. ### Multi-round-trip tool test A handler that calls `ctx.requestInput(...)` throws an `InputRequiredSignal` — in production the handler factory converts it to an `input_required` result; in a unit test it surfaces as a thrown value. Test both rounds. ```typescript import { isInputRequiredSignal } from '@cyanheads/mcp-ts-core'; import { createMockContext } from '@cyanheads/mcp-ts-core/testing'; it('requests the missing input on the first round', async () => { const ctx = createMockContext(); const input = {{TOOL_EXPORT}}.input.parse({ path: '/tmp/x' }); try { await {{TOOL_EXPORT}}.handler(input, ctx); throw new Error('Expected the handler to request input.'); } catch (error) { if (!isInputRequiredSignal(error)) throw error; expect(Object.keys(error.result.inputRequests ?? {})).toEqual(['confirm']); } }); it('completes once the response is supplied', async () => { const ctx = createMockContext({ inputResponses: { confirm: { action: 'accept', content: { confirm: true } } }, }); const input = {{TOOL_EXPORT}}.input.parse({ path: '/tmp/x' }); await expect({{TOOL_EXPORT}}.handler(input, ctx)).resolves.toMatchObject({ deleted: '/tmp/x' }); }); it('does not re-ask after a decline', async () => { const ctx = createMockContext({ inputResponses: { confirm: { action: 'decline' } } }); const input = {{TOOL_EXPORT}}.input.parse({ path: '/tmp/x' }); // Terminal, not another round — re-asking would burn the round budget. await expect({{TOOL_EXPORT}}.handler(input, ctx)).rejects.toThrow(McpError); }); ``` ### Cancellation test ```typescript it('respects cancellation', async () => { const controller = new AbortController(); const ctx = createMockContext({ signal: controller.signal }); const input = {{TOOL_EXPORT}}.input.parse({ count: 100, delayMs: 10 }); 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 ```typescript /** * @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);
Ver en GitHub
Este SKILL.md es muy grande, por eso SkillsMP muestra aqui solo la primera seccion. Ver en GitHub