Copybook
Guides

Testing snippets and commands

Parse, typecheck, and run what you show users.

npx copybook test                          # syntax + typecheck
npx copybook test --tier syntax,typecheck,run
npx copybook test --changed --base origin/main

Tiers

Every command and snippet variant gets the tiers its verify option allows, capped by what its language supports.

TierWhat runsWhen to use it
syntaxbash -n for shell, esbuild for JS and TS, a dotenv parser, Python's ast, and JSON.parseEvery pull request. It's fast and needs nothing.
typecheckInstalls the variant's deps.npm into a cached sandbox, then runs tsc --strictEvery pull request. It catches SDK API drift.
runExecutes the code in a sandbox, with values from a fixtureNightly, or for changed definitions
defineSnippet({
  // …
  variants: [
    {
      when: { framework: "ai-sdk" },
      lang: "ts",
      deps: { npm: ["ai", "@neondatabase/ai-sdk-provider"] },
      verify: "typecheck",
      template,
    },
    { when: { framework: "curl" }, lang: "bash", verify: "run", template },
  ],
});

Prompts don't have a verify option. Their embedded definitions are tested on their own, and literal commands such as t.cmd("npm run build") get a syntax check. To check that a prompt works, use evals.

Secrets never reach the syntax and typecheck tiers. They get stand-in values like copybook_test_token.

Running for real

The run tier needs real values, and usually a real resource. Put defineTest and defineFixture in files your product never imports, and list them in the config:

copybook.config.ts
export default defineConfig({
  content: ["src/**/*.prompts.ts"],
  verify: ["verify/**/*.verify.ts"],
});
verify/gateway.verify.ts
import { defineFixture, defineTest } from "@copybook/cli";

import { tryIt } from "../src/ai-gateway.prompts";

export const branch = defineFixture({
  id: "neon.branch",
  setup: async ({ runId }) => {
    const created = await createBranch(`copybook-${runId}`); // your API client
    return {
      value: created,
      teardown: () => deleteBranch(created.id),
    };
  },
});

export const tryItCurl = defineTest(tryIt, {
  fixture: branch,
  params: (created) => ({
    endpoint: created.aiGatewayUrl,
    model: "claude-haiku-5-5",
  }),
  variants: ["framework=curl;scenario=chat"],
  expect: { stdout: /content/ },
  timeoutMs: 60_000,
});
  • A fixture is set up once per run, only if a selected test uses it. Its teardown always runs, even when tests fail.
  • Name what you create copybook-<runId>, so anything a crashed run leaves behind is easy to find and clean up.
  • Secrets that params doesn't provide come from each secret's env variable. If neither has a value, the case is skipped and the output says which variable to set.
  • Secret values are redacted from all output.

examples/acme runs its cURL snippet against a local mock gateway, so the run tier works without any accounts.

Sandboxes

  • Environment: only PATH, HOME, LANG, TERM, TMPDIR, USER, and npm, pip, and uv registry settings are passed through, plus the env that a fixture or test adds.
  • npm: installs go into node_modules/.cache/copybook/sandboxes, cached by dependency list, with lifecycle scripts disabled.
  • Python: dependencies are installed with uv run --with when uv is available.
  • Isolation: sandboxes are temp directories, not containers. Run the run tier in CI or another disposable environment.

Reports

FlagDescription
-t, --tier <tiers>Comma-separated tiers. Defaults to syntax,typecheck.
--only <ids...>Only these definitions.
--changed, --base <ref>Only definitions whose output changed since the ref.
--reporter <name>pretty (default), json, or junit.
-o, --output <file>Write the JSON or JUnit report to a file.
--concurrency <n>How many cases run at once.

Every run also writes .copybook/results/test.json, which Studio reads. test exits with status 1 if any case fails.

On this page