npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@dichovsky/testrail-api-client

v9.0.0

Published

Type-safe ESM TestRail API client and CLI for Node.js

Readme

TestRail API Client

CI npm version TypeScript License: MIT

Type-safe TypeScript client and testrail CLI for the TestRail REST API, with a single runtime dependency: Zod. ESM only.

Compatibility is tracked through TestRail 10.7.0 (Default 1021), including the cumulative API additions from 10.4–10.7 and the 10.7 repeated refs[] filters for case and BDD lists. The compatibility surface also covers BDD/case-title/version discovery, result editing, dynamic filters, label management, and current run/plan/test filters and scheduling fields. Older scalar refs queries remain supported.

See the TestRail 10.7.0 compatibility audit for the primary-source corpus, endpoint counts, corrected mismatches, and remaining upstream documentation ambiguities.

Install

npm install @dichovsky/testrail-api-client

Requires Node.js 24+. CI verifies Node 24 and 26 across Linux, Windows, and macOS. Other Node majors satisfy the engine range but are not currently in the CI matrix.

Published declarations are smoke-tested with TypeScript 6 and 7. The repository build and primary type-check use native TypeScript 7; compiler-API-based generators and typed lint tooling use the TypeScript 6 compatibility compiler until the native compiler exposes that API.

Repository verification

After npm ci, run npm run verify to build, run both compiler checks, lint/format/generated-document checks, execute the coverage suite, and smoke-test the packed package. npm test runs only Vitest. The repository disables implicit npm lifecycle hooks, so verification is an explicit command. Additional release fuzz and registry audit gates are listed in the release guide.

30-second example

import { TestRailClient } from '@dichovsky/testrail-api-client';

const client = new TestRailClient({
    baseUrl: process.env.TESTRAIL_BASE_URL!, // https://your-domain.testrail.io
    email: process.env.TESTRAIL_EMAIL!,
    apiKey: process.env.TESTRAIL_API_KEY!,
});

try {
    const project = await client.projects.getProject(1);
    console.log(project.name);
} finally {
    client.destroy(); // release timers, clear cache, zero the credential
}

The client surfaces the supported TestRail REST API endpoints. See docs/API-MAPPING.md for the endpoint-to-method matrix and CODEMAP.md for exact signatures and file:line locations of every symbol.

Write example

With a live client instance, write payloads are typed from the Zod payload schemas (compile-time only — the SDK does not re-validate them at runtime; the CLI does):

const run = await client.runs.addRun(5, {
    suite_id: 12,
    name: 'CI build',
    include_all: false,
    case_ids: [42, 43, 44],
});

await client.results.addResultForCase(run.id, 42, { status_id: 1, comment: 'Passed' });

status_id is optional on a new result: it needs at least one of status_id, comment or assignedto_id, so a comment-only result is valid. Only the CLI enforces that rule; through the SDK a typed {} compiles and is sent to TestRail as is.

CLI quick tour

The package ships a testrail binary. Authenticate with environment variables, then read, write, or delete:

export TESTRAIL_BASE_URL="https://your-domain.testrail.io"
export TESTRAIL_EMAIL="[email protected]"
export TESTRAIL_API_KEY="…"                       # never pass the key on argv

npx testrail project list                              # read (JSON to stdout)
npx testrail run add 5 --data '{"name":"CI build","include_all":true}'   # write (Zod-validated)

# Destructive: needs BOTH the per-invocation --yes flag AND the process-wide env unlock
TESTRAIL_ALLOW_DESTRUCTIVE=1 npx testrail run close 100 --yes

Prefer TESTRAIL_API_KEY. If an environment variable is not an option, pipe the key with echo "$KEY" | npx testrail ... --api-key-stdin. That flag consumes stdin, so write bodies must come from --data or --data-file.

--dry-run previews any write or delete client-side with no API call. Output format is selectable with --format <json|table|yaml|csv>. npx testrail <resource> --help (for example npx testrail case --help) lists one resource's commands; npx testrail --help lists them all. See skill/SKILL.md and the command and recipe references beside it for the complete command surface and recipes.

String options require their own value: --filename --dry-run is rejected before the command runs, including when a later occurrence supplies a valid filename. For a literal value beginning with --, use the inline form, such as --filter=--all. Boolean options take no value: pass --dry-run, not --dry-run=true.

Attachment and BDD downloads accept regular files as --out destinations. --force permits replacing a regular file; it rejects symlinks, FIFOs, and devices, including /dev/null. To discard a download, send its payload to stdout and let the shell discard it:

testrail attachment get 42 --out - > /dev/null

The JSON acknowledgement still goes to stderr. Use the same --out - pattern with testrail bdd get <case-id>; do not use --out /dev/null --force.

Features

| Capability | What it does | Documented in | | ------------------ | ------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | Response caching | GET-only in-process LRU cache with TTL; any write invalidates it | docs/ARCHITECTURE.md §2.3 | | Bounded pagination | Preserve page metadata or collect every page with explicit safety limits | Pagination | | Rate limiting | Sliding-window limiter (default 100 req/60s); rejects over-limit before fetch | docs/ARCHITECTURE.md §2.2 | | Retry with backoff | Exponential backoff with Retry-After; GET retries 5xx/429/network, JSON writes only 429, multipart uploads never retry | docs/ARCHITECTURE.md §2.4 | | SSRF guard | Validated DNS addresses bound to each connection, private-host blocking, manual-redirect rejection | docs/ARCHITECTURE.md §2.5 | | Response-body caps | Byte ceiling + wall-clock deadline on every body read | docs/ARCHITECTURE.md §2.2 | | Streaming uploads | Attachment uploads stream from disk, so large files don't buffer in heap | docs/ARCHITECTURE.md §2.4 | | CLI | testrail binary: read / write / destructive actions, four output formats | skill/SKILL.md | | AI-agent skill | Bundled Claude Code skill; install it with npx testrail install-skill | skill/SKILL.md |

For a project-scoped Claude Code installation, run npx testrail install-skill. Add --global to install it under ~/.claude/skills/. It installs SKILL.md and the reference/ files the body links to; after upgrading the package, re-run it with --force to replace an earlier install.

Note on rate-limit headers. A live-instance check found that TestRail Cloud does not emit rate-limit headers (Retry-After, X-RateLimit-*) under normal serial load — a burst of requests all returned 200 with no such headers. The client's Retry-After handling is therefore dormant in practice and only engages if the server starts sending the header (e.g. under heavy throttling or on a future TestRail version); it is fully covered by synthetic tests. The effective throttle is the client's own sliding-window limiter (rateLimiter, default 100 req/60s), which rejects over-limit requests before they leave the process — tune it to your instance's quota.

Configuration

All options except baseUrl / email / apiKey are optional:

const client = new TestRailClient({
    baseUrl: 'https://your-domain.testrail.io',
    email: '[email protected]',
    apiKey: 'your-api-key',

    timeout: 30000, // request timeout (ms)
    maxRetries: 3, // retry attempts for retryable failures
    enableCache: true, // cache GET responses
    cacheTtl: 300000, // cache TTL (ms)
    rateLimiter: { maxRequests: 100, windowMs: 60000 },
});

| Option | Type | Default | Description | | ------------------------- | ------------------- | ------------------ | ------------------------------------------------------ | | baseUrl | string | required | HTTPS TestRail URL; HTTP requires allowInsecure | | email | string | required | TestRail user email (validated format) | | apiKey | string | required | TestRail API key | | timeout | number | 30000 | Per-attempt timeout in ms (max 5 min); covers DNS | | maxRetries | number | 3 | Max retry attempts for failed requests; integer 0-10 | | enableCache | boolean | true | Enable caching for GET requests | | cacheTtl | number | 300000 | Cache time-to-live in milliseconds | | cacheCleanupInterval | number | 60000 | Integer 0–2,147,483,647 ms; 0 disables cleanup | | maxCacheSize | number | 1000 | Maximum number of entries in cache | | rateLimiter | RateLimiterConfig | 100 / 60s | { maxRequests, windowMs } sliding window | | allowInsecure | boolean | false | Permit cleartext HTTP (credentials sent in Base64) | | allowPrivateHosts | boolean | false | Permit private/loopback/link-local hosts | | maxJsonResponseBytes | number | 10485760 | JSON/text response body cap (10 MiB; ceiling 1 GiB) | | maxBinaryResponseBytes | number | 104857600 | Binary response body cap (100 MiB; ceiling 1 GiB) | | bodyTimeout | number | = timeout | Wall-clock deadline for the body read (0 disables) | | registerProcessHandlers | boolean | false | Install exit/SIGINT/SIGTERM handlers (opt-in) | | fetch | typeof fetch | globalThis.fetch | Trusted transport; honor the supplied dispatcher | | dnsLookup | function | system DNS | Custom resolver for SSRF checks and connection pinning | | onSchemaMismatch | function | none (silent) | Notified when a response does not match its schema |

Library consumers should leave registerProcessHandlers off and call client.destroy() from their own shutdown hook. The testrail CLI opts in on your behalf.

To retain a concurrency slot or staged upload until driver work has actually finished, use trackOperation:

const operation = client.trackOperation(() => client.projects.getAllProjects({ maxDurationMs: 1000 }));
try {
    const projects = await operation.result;
    console.log(projects.length);
} finally {
    await operation.settled;
    // Release the operation's slot or staged files here.
}

result preserves the normal method value, error, and deadline. settled always resolves, after the callback and every started or joined driver task finish, including DNS, fetch, response reads and cancellation, upload streams, retries, and shared requests. A deadline can reject result while settled is still pending. An abort request alone does not release ownership; custom transports or cancellation that never complete keep it pending. Nested operations are included. Return the promise for your complete callback workflow; unawaited application timers are outside the driver's accounting. trackOperation does not add cancellation, and destroy() does not abort in-flight work or settle its handles. Settlement tracking switches on with the first trackOperation call and stays on. Multipart uploads also enter a separate async context to identify their native transport requests, even without trackOperation. A process that neither tracks operations nor uploads avoids the library's context-propagation cost. A request already in flight at the first trackOperation call can be joined for its result but not for its post-result cleanup.

Multipart cleanup requests abort and then requires transport evidence before closing active upload streams normally. The pinned dispatcher or native request diagnostics can provide that evidence. When an injected or globally replaced fetch supplies neither, cleanup errors the source instead, preventing an ignored abort from sending a truncated file with a valid multipart closing boundary. Pending source reads and cancellation still delay settled on every path.

A cache hit is isolated, not free. Entries are deep-copied with structuredClone on both write and read, so a cached caller can mutate what it receives without corrupting the entry the next caller will get. The cost scales with payload size: a hit on a 250-case getCases() page copies that whole payload again. This is the right trade for correctness, but it means maxCacheSize bounds entry count, not memory — size it against the responses you actually cache, and lower maxJsonResponseBytes (default 10 MiB) if a bulk-export endpoint would otherwise pin large bodies for the full TTL.

reports.runReport() and reports.runCrossProjectReport() generate a new report for each call. Although the API routes use GET, these methods bypass cache reads, writes, and request coalescing. A 5xx or network failure is never retried — the report may already have been generated and the template may have sent email, so re-running an uncertain outcome must be your own explicit new invocation. A 429 is retried (honoring Retry-After), because the rate limiter rejects the request before execution.

By default, the host guard rejects private, loopback, link-local, CGNAT, benchmarking (198.18.0.0/15), multicast, and reserved IPv4 addresses, including their IPv4-mapped IPv6 spellings, plus IPv6 transition ranges such as 6to4 and the well-known and local-use NAT64 prefixes. Literal URLs and DNS answers use the same address classifier. The default transport connects directly to the validated addresses without a second DNS lookup, preserving the original hostname for TLS certificate checks and SNI. Injected fetch implementations are trusted: they must honor the supplied Node dispatcher or enforce equivalent destination checks, abort signals, and manual redirects. On-premise SDK deployments that need these addresses must explicitly set allowPrivateHosts: true; this also disables DNS host validation and connection pinning. See the host guard details for the exact ranges.

Concurrent GETs share an in-flight request only when their effective header and body timeouts match. Timeout views still share completed cached responses.

Custom DNS resolvers

Each dnsLookup answer must contain a valid IP literal and its matching numeric family: 4 for IPv4 or 6 for IPv6. Missing families, 0, and mismatches now fail before any request is sent, even when the address itself is public. Use the system resolver's complete answer objects, or supply the correct family when adapting another resolver:

import { lookup } from 'node:dns/promises';

const client = new TestRailClient({
    ...config,
    dnsLookup: (hostname) => lookup(hostname, { all: true }),
});
// A fixed IPv4 answer has this shape:
// dnsLookup: async () => [{ address: '203.0.113.10', family: 4 }]
// IPv6 answers must use family: 6.

Proxies and custom certificate authorities

With allowPrivateHosts: false (the default), the client now supplies a direct dispatcher on each request. This is a breaking change for applications that previously relied on Undici's setGlobalDispatcher() with a ProxyAgent, EnvHttpProxyAgent, or custom TLS settings. It also bypasses NODE_USE_ENV_PROXY=1 / --use-env-proxy; the private HTTP/HTTPS agents do not inherit http.globalAgent or https.globalAgent customization. Node documents the separate per-request dispatcher and global proxy configuration.

For additional CA trust on direct connections, set NODE_EXTRA_CA_CERTS to a PEM bundle before starting Node. This preserves certificate and hostname verification while retaining DNS pinning; a custom global agent's ca setting is not copied into the client's agents.

For a required proxy, inject an application-owned fetch adapter that explicitly selects that proxy and preserves the supplied abort signal and manual-redirect policy. Replacing the dispatcher also replaces connection pinning: the client's local DNS check cannot verify a second lookup performed by the proxy. Before using this migration, configure the trusted proxy to permit only the approved TestRail origin and enforce the same private-address restrictions on its actual connection destination. An origin allowlist alone does not prevent DNS rebinding.

For example, after establishing that proxy policy, an application that already uses Undici's ProxyAgent can select it explicitly:

import { ProxyAgent } from 'undici'; // Application dependency; not bundled by this client.

const approvedOrigin = 'https://your-domain.testrail.io';
const proxy = new ProxyAgent('https://approved-proxy.example:8443');
const proxyFetch: typeof globalThis.fetch = (input, init) => {
    const url = new URL(input instanceof Request ? input.url : input);
    if (url.origin !== approvedOrigin) throw new Error('Unexpected TestRail origin');
    return globalThis.fetch(input, { ...init, dispatcher: proxy });
};
const client = new TestRailClient({
    ...config,
    baseUrl: approvedOrigin,
    allowPrivateHosts: false,
    fetch: proxyFetch,
});
try {
    console.log(await client.projects.getProjects());
} finally {
    client.destroy();
    await proxy.close();
}

The injected adapter and proxy now own destination enforcement. If that policy cannot be established, this package has no safe automatic proxy fallback. allowPrivateHosts: true restores the configured fetch's connection handling but disables both DNS validation and pinning; it is not a proxy fix. Reserve it for intentionally trusted on-premise destinations with separate network controls.

Pagination

The 24 endpoints in the pagination registry expose three projections. Existing methods keep their backward-compatible behavior: get*() performs one request and returns that response's item array. get*Page() performs one request and returns a discriminated Page<T> with the server's offset, limit, size, and _links when an envelope was returned. getAll*() follows every response continuation and returns one concatenated array:

const firstResponse = await client.runs.getRuns(5); // Run[]; one response
const page = await client.runs.getRunsPage(5, { limit: 25, offset: 50 }); // Page<Run>
const all = await client.runs.getAllRuns(5, { pageSize: 100, maxItems: 10_000 }); // Run[]

An envelope's _links.next decides whether another request is required. The client extracts only a validated offset and optional limit. Each descriptor declares its allowed operation names; its adapter supplies one selected operation, validated numeric path parameters, and normalized filters. The shared executor rejects undeclared operations and rebuilds the known TestRail endpoint itself, so it never follows the continuation's host or path. A legacy bare-array response is necessarily one terminal page. All-page reads bypass GET cache reads, writes, and request coalescing so one aggregate cannot combine pages captured at different times. They also do not enumerate wider runtime option objects: only declared filters and public aggregate controls are read, while injected limit/offset, internal collector fields, and unrelated properties are ignored. A page read uses normal GET caching in a separate validated namespace, so a permissive legacy one-response wrapper cannot poison the stricter Page<T> projection.

Aggregation is fail-closed and never returns partial results. Defaults are a page size of 250, start offset 0, 100 pages, 25,000 items, five minutes, and 100 MiB of UTF-8 serialized items. Page size is capped at 250, duration at five minutes, and the byte bound at 1 GiB. A safety, continuation, or page-structure failure throws TestRailPaginationError with reason, pagesFetched, and itemsFetched. Reasons are max_pages, max_items, max_duration, max_bytes, invalid_page, invalid_continuation, and non_progress.

Registry scope is deliberately finite: cases, case history, and project BDDs; projects, suites, sections, plans, runs, tests, and milestones; the three result lists; labels; shared-step lists and history; case/run/plan attachment lists; datasets, variables, roles, groups, and case statuses. Shared-step history, datasets, variables, roles, groups, and case statuses expose response-driven pagination but no caller-controlled page size or start offset. Test attachments, plan-entry attachments, users, and ordinary metadata/configuration/report lists remain one-response-only; some legacy methods may still accept limit/offset, but that is not a get*Page()/getAll*() guarantee.

The CLI mirrors the projections. Its default output remains an item array; --page emits the normalized page object, and --all emits the complete array. --all uses --page-size, --start-offset, --max-pages, --max-items, --max-duration-ms, and --max-bytes. It cannot be combined with --page, --limit, or --offset; aggregate controls require --all. Response-driven endpoints reject caller-controlled size/offset flags.

Response types are a description, not a guarantee

Since 6.0.0, Zod response validation is advisory. When entity fields do not match their schema, the client normally returns the raw body unchanged and notifies onSchemaMismatch; the Zod mismatch alone does not throw. With the default or another non-throwing hook, domain methods still enforce list/page outer structure so a malformed collection cannot masquerade as a successful empty result. A caller-supplied hook that throws takes precedence over downstream structural decoding.

In non-throwing advisory mode, the hard protocol exceptions are structural rather than entity-field drift. A malformed default list response throws TestRailApiError; explicit page/all reads throw TestRailPaginationError. An unrecognized successful response from cases.addCases() or cases.updateCases() also throws TestRailApiError with an explicit “write outcome is indeterminate” message. Those non-idempotent writes may already have changed server state, so reporting an empty result could prompt a duplicate retry.

The CLI makes advisory mismatches visible without copying the response into logs. By default it writes at most 10 unique, deduplicated warnings to stderr, then a safe suppressed-count summary. Warnings contain only the request method, CLI resource/action, Zod issue codes, and shape-only paths whose segments are all masked as *; they never include the endpoint, field/record keys, issue messages, or raw response. Pass --strict-responses or set TESTRAIL_STRICT_RESPONSES=1 to stop at the first mismatch with exit code 1. Read mismatches use TestRailValidationError; a successful mutating request with a mismatched response uses a privacy-safe TestRailApiError that says the write outcome is indeterminate, and must not be retried blindly. One-shot commands emit no value for the mismatched response, and bounded aggregates emit no partial array. A streaming run watch may already have emitted completed earlier polls before a later mismatch; those events cannot be retracted. The environment variable accepts only 1, 0, an empty value, or unset; any other value is rejected before authentication or a network request. The flag is boolean-only, so forms such as --strict-responses=true are rejected instead of guessed. --quiet suppresses advisory warnings as well as normal output.

This is deliberate. TestRail's published API documentation is not a reliable description of what the API actually sends: it documents a {step_history} wrapper for an endpoint that returns a bare array, a boolean mfa_required that arrives as integer 0, and an is_untested field on the wrong endpoint entirely. Because list endpoints validate a whole page at once, a single unmodelled row used to discard up to 250 valid ones — so strict validation reliably converted a working response into an outage, and never once caught a server-side regression.

Two consequences worth knowing:

  • Exported response types state the expected shape, not a runtime guarantee. A field typed number can hold whatever TestRail sent. Fields are widened to match reality as wire evidence arrives, in any release — pin an exact version if you need frozen types.
  • Caller-supplied input still fails closed. Client configuration and CLI write payloads are validated on a separate path and reject invalid input as before.
  • The hook must be synchronous, and what it logs can contain personal data. An async hook cannot restore fail-closed validation and is rejected with TestRailValidationError; see the comment in the example below before logging endpoint or data.

Public response types are derived from the declared Zod response shapes, so a runtime .passthrough() no longer leaks a broad [key: string]: unknown into every entity. Flat response custom fields are modeled only where TestRail emits them: Case, Test, and Result support bracket access such as test['custom_browser'], typed unknown; narrow before use. Their older custom_fields container remains deprecated for compatibility. Stable fields added in 6.0 include Test.refs_data/case_title, Result.case_title/case_refs, CaseField.is_indexed/is_system, and ResultField.is_system; milestone children are now recursively typed.

// Observe drift without changing behavior.
// Never log `endpoint`, `error`, or `data` wholesale. Endpoint path/query values
// and the raw body can contain personal data, while Zod paths can contain
// response-controlled record/catchall keys. Keep only the operation token,
// issue code, and path depth; mask every path segment.
const client = new TestRailClient({
    ...config,
    onSchemaMismatch: ({ method, endpoint, error }) =>
        log.warn({
            method,
            operation: endpoint.replace(/[\/&].*$/, ''),
            issues: error.issues.map(({ code, path }) => ({
                code,
                path: path.length === 0 ? '$' : `$.${path.map(() => '*').join('.')}`,
            })),
        }),
});

// Or restore strict, fail-closed validation — useful in CI.
// `handleZodError` reproduces the exact TestRailValidationError older versions
// threw, so existing `instanceof` handlers keep matching; throw `error` as-is
// if you would rather have the raw Zod issue tree.
import { handleZodError } from '@dichovsky/testrail-api-client';

const strict = new TestRailClient({
    ...config,
    onSchemaMismatch: ({ error }) => {
        throw handleZodError(error);
    },
});

Error handling

The client exposes three primary error classes:

import { TestRailApiError, TestRailPaginationError, TestRailValidationError } from '@dichovsky/testrail-api-client';

try {
    await client.projects.getProject(999);
} catch (error) {
    if (error instanceof TestRailApiError) {
        // HTTP/network/protocol failure, including a malformed successful default-list response
        console.error(error.status, error.statusText, error.response);
    } else if (error instanceof TestRailPaginationError) {
        // Bounded aggregation or structural page/continuation failure
        console.error(error.reason, error.pagesFetched, error.itemsFetched);
    } else if (error instanceof TestRailValidationError) {
        // Bad config, invalid ID, or invalid params
        console.error(error.message);
    }
}

TestRailApiError carries status, statusText, and response (the raw body lives only in response, never in message). In non-throwing advisory mode, it also represents an unrecognized successful outer structure from a default list read or non-idempotent bulk case write. CLI strict mode uses the same class for any successful mutating request whose response mismatches, with an indeterminate outcome message and no raw response attached. TestRailPaginationError extends TestRailValidationError; catch it first when its structured reason matters. Ordinary entity-field schema mismatches remain advisory. Explicit page/all projections require complete envelope metadata, valid links, and a safe continuation. A throwing SDK mismatch hook takes precedence over these downstream decoders; never treat its error as proof that a write did not happen. Other TestRailValidationErrors signal caller mistakes — bad config, an invalid ID, or an invalid parameter. Calling any method after destroy() throws a plain Error.

For list filters that carry numeric IDs, validation also happens before any request is sent. Arrays such as createdBy, statusId, and milestoneId must contain positive integers; invalid values fail locally with TestRailValidationError instead of reaching the API.

For CLI error details, opt into a private diagnostic file on the original invocation:

testrail case-field add --data-file field.json --diagnostic-file ./field-error.json

For real invocations, --diagnostic-file reserves a new regular file before the command handler runs and rejects existing paths, including symlinks, and destinations shared with --out. Its directory must already exist. The file has mode 0600. The flag currently rejects Windows before stdin or authentication work because the client cannot establish an equivalent private ACL there. --dry-run ignores the diagnostic destination on every platform: previews create no diagnostic file and do not validate or modify an existing destination. On macOS, the client clears inherited ACLs on a private staging directory before creating the file, then reserves the requested destination with an exclusive link to that private file. Bounded calls to the system chmod secure the staging directory and remove ACLs added to the file before writing diagnostics. The destination directory's ACL is preserved. If securing the file fails, the client rejects the destination or omits the diagnostic record. Normal completion removes an unused reservation; process exit also attempts cleanup, including exits caused by SIGINT/SIGTERM. A permission or I/O failure can leave an empty reservation; the command's exit status still describes the operation, and --quiet suppresses cleanup warnings. File existence alone does not establish API failure. Failures after reservation produce a version-1 JSON record with kind, HTTP status when available, an explicit operationOutcome, and server containing state, messages, and truncated. Early argument/auth failures have no diagnostic artifact. operationOutcome is "not_dispatched" for a known failure before invoking the command handler, such as invalid client configuration. Once the handler starts it is conservatively "failed_or_indeterminate"; the error's class alone cannot prove whether an API write happened.

Only recognized structured validation details are included. Known credentials and common encoded variants are redacted; sensitive nested keys are omitted. Raw bodies, headers, request payloads, and stacks are excluded. messages contains nonblank redacted string values in encounter order, without object keys or field-to-message mappings. Input processing is capped at 64 KiB and the record at 16 KiB; malformed/non-JSON or oversized bodies yield a safe omission state. When extracted messages exceed the record limit, the first complete redacted messages are retained with truncated: true. Safe validation text preserves the server's original escaping. Diagnostic failures preserve the command's exit status and report that the file could not be saved or cleaned up. --quiet also suppresses these warnings. Default output and request/retry behavior are unchanged. Never replay a write just to obtain diagnostics.

After successful custom-field creation, inventory visibility can lag. See the GET-only readiness guide for bounded, cancellable polling that retains the creation result and verifies scope/options before dependent case writes.

Links

License

MIT — see LICENSE. If this saved you time, you can buy me a coffee.