Files
agentic-dev/packages/core-testing/AGENTS.md
Danijel Martinek 2bd882e0b0 feat(core-testing): git-serving fixture helpers
serveFixtureRepo(fixtureDir) copies a fixture into a temp dir, commits
it as a fresh single-commit repo, bare-clones it, and serves it over
`git daemon --export-all` on an ephemeral localhost port (default) or
as a file:// URL (fallback for daemon-less environments). Returns
{ cloneUrl, bareRepoPath, protocol, stop } with idempotent teardown.
Tests exercise a real `git clone` of fixtures/vite-kitchen over both
protocols. New subpath export @repo/core-testing/git — node-only, so
deliberately not on the jsdom root barrel. No new runtime deps: node
built-ins + system git (present in git's exec-path on macOS and the
ubuntu CI image).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016j8z4VHjedXDTjEDNg7qHK
2026-07-12 21:40:52 +02:00

4.4 KiB

@repo/core-testing

Shared testing utilities. Tag: tooling. May be depended on by any package as a devDependency.

Subpath exports

  • @repo/core-testing/factorydefineFactory<T>(builder) for test data factories
  • @repo/core-testing/contractdefineContractSuite<T>(name, suite) for cross-impl contract tests
  • @repo/core-testing/gitserveFixtureRepo(fixtureDir, { protocol? }) serves a fixture directory (e.g. fixtures/vite-kitchen) as a real git remote for integration tests: copies it to a temp dir, commits it, bare-clones it, and exposes it via git daemon on an ephemeral localhost port (default) or a file:// URL (fallback). Returns { cloneUrl, bareRepoPath, protocol, stop }; always await stop() in afterEach. Node-only (node:child_process + system git) — intentionally not re-exported from the root barrel
  • @repo/core-testing/reactrenderWithProviders, createMockTrpcClient
    • renderWithProviders does NOT include a tRPC provider. Consumers needing tRPC should wire their own TRPCProvider (from their app's tRPC client setup) and use createMockTrpcClient as the client. This constraint exists because tooling packages cannot import AppRouter from @repo/core-api.
  • @repo/core-testing/payloadstubPayloadConfig, mockPayloadModule
  • @repo/core-testing/setup/jsdom — vitest setupFile (jest-dom + cleanup)
  • @repo/core-testing/setup/node — vitest setupFile (no-op placeholder)

Adding a factory

import { defineFactory } from "@repo/core-testing/factory";

export const articleFactory = defineFactory<Article>(({ sequence }) => ({
  id: `article-${sequence}`,
  title: `Article ${sequence}`,
  // stable defaults — overrides drive variation
}));

Using createMockTrpcClient

For component tests that consume tRPC procedures, mock the responses by procedure path (dot-separated):

import { createMockTrpcClient } from "@repo/core-testing/react";
import type { AppRouter } from "@repo/core-api"; // import in your app/feature, not in core-testing

const trpcClient = createMockTrpcClient<AppRouter>({
  "blog.articleBySlug": { id: "1", title: "Hello", slug: "hello" },
  "blog.listArticles": [],
});

Combine with your app's TRPCProvider for components that need a tRPC client in the render tree.

Adding a contract suite

See docs/guides/tdd-workflow.md §"Contract suite usage".

Test patterns

These test obligations apply to every feature package. The examples below show the minimal shape — adapt to the feature's actual types.

Output validation (use case)

Every non-void use case must have a test that injects a mock returning malformed data and asserts the use case rejects with a ZodError. This proves xOutputSchema.parse(result) is actually called.

it("throws ZodError when repository returns malformed data", async () => {
  const badRepo = { getArticleBySlug: async () => ({ id: 1 }) }; // id should be string
  await expect(
    getArticleBySlugUseCase(badRepo as any)({ slug: "x" }),
  ).rejects.toBeInstanceOf(ZodError);
});

Void use cases (signOut, deleteMedia) are exempt — they have no xOutputSchema.

Router error mapping (tRPC)

Each feature's router.test.ts must assert the correct TRPCError.code for at least one mapped domain error, using xRouter.createCaller({}).

it("returns NOT_FOUND when article is missing", async () => {
  const caller = blogRouter.createCaller({});
  const error = await caller.articleBySlug({ slug: "missing" }).catch((e) => e);
  expect(error).toBeInstanceOf(TRPCError);
  expect(error.code).toBe("NOT_FOUND");
});

Also assert BAD_REQUEST for at least one invalid-input call (exercises the strict() schema boundary).

Presenter shape (controller tests)

When a controller's presenter reshapes the use-case output (e.g., signInController extracts cookie from { session, cookie }), the controller test must assert against the view shape, not the use-case output shape.

// signInController: presenter returns value.cookie (a Cookie object)
const result = await signInController(mockUseCase)({
  username: "u",
  password: "p",
});
expect(result.name).toBe(SESSION_COOKIE); // Cookie.name
expect(result.value).toBeDefined(); // Cookie.value
// NOT: expect(result.session).toBeDefined() — that's the use-case output, not the view

Identity presenters (return value;) skip this obligation — the view shape equals the use-case output shape.