AgentSkillsCN

outfitter-testing

使用测试套件、固定装置以及 Bun 测试运行器,为 @outfitter/* 包编写测试。适用于编写测试、设置测试套件、模拟上下文,或当提及“测试处理器”、“测试 CLI”、“测试 MCP”或“@outfitter/testing”时使用。

SKILL.md
--- frontmatter
name: outfitter-testing
version: 0.1.0
description: Testing patterns for @outfitter/* packages using test harnesses, fixtures, and Bun test runner. Use when writing tests, setting up test harnesses, mocking context, or when "test handler", "test CLI", "test MCP", or "@outfitter/testing" are mentioned.
allowed-tools: Read Write Edit Glob Grep Bash(bun test *)

Stack Testing

Test patterns for @outfitter/* packages.

Test Structure

code
src/
├── handlers/
│   └── get-user.ts
└── __tests__/
    ├── get-user.test.ts
    └── __snapshots__/
        └── get-user.test.ts.snap

Handler Testing

Test handlers directly without transport layer:

typescript
import { describe, test, expect } from "bun:test";
import { createContext } from "@outfitter/contracts";
import { getUser } from "../handlers/get-user.js";

describe("getUser", () => {
  test("returns user when found", async () => {
    const ctx = createContext({});
    const result = await getUser({ id: "user-1" }, ctx);

    expect(result.isOk()).toBe(true);
    expect(result.value).toEqual({
      id: "user-1",
      name: "Alice",
    });
  });

  test("returns NotFoundError when user missing", async () => {
    const ctx = createContext({});
    const result = await getUser({ id: "missing" }, ctx);

    expect(result.isErr()).toBe(true);
    expect(result.error._tag).toBe("NotFoundError");
    expect(result.error.resourceId).toBe("missing");
  });

  test("returns ValidationError for invalid input", async () => {
    const ctx = createContext({});
    const result = await getUser({ id: "" }, ctx);

    expect(result.isErr()).toBe(true);
    expect(result.error._tag).toBe("ValidationError");
  });
});

Test Fixtures

Use createFixture for deep-merged test data:

typescript
import { createFixture } from "@outfitter/testing";

interface User {
  id: string;
  name: string;
  email: string;
  settings: { theme: string; notifications: boolean };
}

const createUser = createFixture<User>({
  id: "user-1",
  name: "Test User",
  email: "test@example.com",
  settings: { theme: "light", notifications: true },
});

test("user with custom settings", async () => {
  const user = createUser({ settings: { theme: "dark" } });
  // user.settings.notifications is still true (deep merge)
});

Temporary Directories

Use withTempDir for isolated file operations:

typescript
import { withTempDir } from "@outfitter/testing";

test("writes config file", async () => {
  await withTempDir(async (dir) => {
    const result = await writeConfig({ dir, data: { key: "value" } }, ctx);

    expect(result.isOk()).toBe(true);
    const content = await Bun.file(`${dir}/config.json`).json();
    expect(content).toEqual({ key: "value" });
  });
});

Environment Mocking

Use withEnv for environment variable testing:

typescript
import { withEnv } from "@outfitter/testing";

test("uses custom log level", async () => {
  await withEnv({ LOG_LEVEL: "debug" }, async () => {
    const config = loadConfig();
    expect(config.logLevel).toBe("debug");
  });
});

CLI Testing

Use createCliHarness for CLI command testing:

typescript
import { createCliHarness } from "@outfitter/testing";
import { listCommand } from "../commands/list.js";

const harness = createCliHarness(listCommand);

test("lists items in JSON mode", async () => {
  const result = await harness.run(["--json"]);

  expect(result.exitCode).toBe(0);
  expect(result.stdout).toContain('"items"');
});

test("exits with error for invalid flag", async () => {
  const result = await harness.run(["--invalid"]);

  expect(result.exitCode).toBe(1);
  expect(result.stderr).toContain("Unknown option");
});

MCP Testing

Use createMcpHarness for MCP tool testing:

typescript
import { createMcpHarness } from "@outfitter/testing";
import { searchTool } from "../tools/search.js";

const harness = createMcpHarness(searchTool);

test("returns search results", async () => {
  const result = await harness.invoke({
    query: "test",
    limit: 10,
  });

  expect(result.isOk()).toBe(true);
  expect(result.value.results).toHaveLength(3);
});

test("validates input schema", async () => {
  const result = await harness.invoke({
    query: "",  // Invalid: min length 1
  });

  expect(result.isErr()).toBe(true);
  expect(result.error._tag).toBe("ValidationError");
});

Context Mocking

Create mock context with logger spy:

typescript
import { createContext } from "@outfitter/contracts";
import { createMockLogger } from "@outfitter/testing";

test("logs debug messages", async () => {
  const mockLogger = createMockLogger();
  const ctx = createContext({ logger: mockLogger });

  await myHandler({ id: "1" }, ctx);

  expect(mockLogger.calls.debug).toContainEqual([
    "Processing",
    { id: "1" },
  ]);
});

Snapshot Testing

Use Bun's snapshot testing:

typescript
import { expect, test } from "bun:test";

test("output matches snapshot", async () => {
  const result = await formatOutput(data);
  expect(result).toMatchSnapshot();
});

Snapshots stored in __snapshots__/*.snap.

Result Assertions

Custom matchers for Result types:

typescript
// Check success
expect(result.isOk()).toBe(true);
expect(result.value).toEqual(expected);

// Check failure
expect(result.isErr()).toBe(true);
expect(result.error._tag).toBe("NotFoundError");
expect(result.error.category).toBe("not_found");

// Error details
expect(result.error.details).toMatchObject({
  field: "email",
});

Running Tests

bash
# All tests
bun test

# Single file
bun test src/__tests__/get-user.test.ts

# Watch mode
bun test --watch

# With coverage
bun test --coverage

# Update snapshots
bun test --update-snapshots

Best Practices

  1. Test handlers directly — Skip transport layer for unit tests
  2. Use fixtures — Create reusable test data with createFixture
  3. Isolate side effects — Use withTempDir and withEnv
  4. Mock context — Inject mock logger to verify logging
  5. Test error paths — Verify correct error types and categories
  6. Snapshot outputs — Use snapshots for complex output verification

Related Skills

  • stack:patterns — Handler contract reference
  • stack:scaffold — Handler templates
  • stack:debug — Troubleshooting test failures