// docs

Importing tests & CI

Bring decks in without retyping: upload a .feature file, migrate from TestRail, or post to the token-authenticated machine-import API from CI.

Most decks start in the Case Editor — someone typing a title, a few steps, an expected result. This page covers the other four ways a case or a deck arrives without that: uploading a Gherkin .feature file, migrating a TestRail suite, posting to the token-authenticated machine-import API, and drafting a case straight out of a caught fail's recording. All four feed the same case model. All four obey the same rule, no exceptions: importing a deck does not run it. It lands inert on the shelf until a person opens it and steps through it by hand, card by card.

Upload a .feature file

Admin-only, at /packagesImport from file (POST /api/packages/import, multipart). Teasynaer parses the file with @cucumber/gherkin and turns each Scenario/Scenario Outline into a case:

Feature: Checkout

  Background:
    Given I am signed in as a customer

  Scenario: Apply a valid discount code
    Given my basket total is £50.00
    When I apply the code "SAVE10"
    Then the basket total is £45.00
    And a "code applied" confirmation is shown

That produces a deck named Checkout with one case. The mapping, in short:

  • Feature: → the deck name (overridable at upload time; falls back to "Untitled import").
  • Each Scenario: / Scenario Outline: row → one case; the scenario title becomes the case title and its key.
  • Given / When → the case's Steps; every Then (plus trailing And/But) → Expected result. A scenario with no Then is rejected outright — the outcome step is never inferred.
  • Background: steps are prepended to every case in the file.
  • @suite:<name> on a scenario sets that case's suite label (optional; any other tag is ignored).

The whole file is validated before anything is written. One over-limit scenario, one missing Then, one Gherkin syntax error — the entire import is rejected, nothing partial gets created. The hard limits:

LimitValue
Scenarios per file200
File size512 KB
Scenario title500 characters
Each step2,000 characters
Expected result (all Then lines joined)4,000 characters
Suite name64 characters
Deck name1–255 characters

Every upload creates a new deck — re-uploading an edited file makes another deck, not an update to the old one. Retiring the previous version is a manual step.

Handing the authoring off to a coding agent instead of writing the .feature yourself? The repo ships an agent skill, authoring-gherkin-for-teasynaer, that mirrors this exact contract (the mapping table, the limits, the no-Then-rejection rule) — point the agent at it instead of re-explaining the shape from scratch.

Validate before you spend a deck slot

Both the Gherkin route above and the TestRail route below accept ?validate=1. It runs the identical parser against the identical limits — same rejections, same error text — but never writes to the database. A clean file gets back the deck name, case count, and the limits it was checked against; a bad one gets the same error the real import would have thrown. Use it as a pre-flight check in an editor or a CI step before you commit to an actual import.

Bring over a TestRail suite

A different source, same destination: /packagesImport from TestRail, or POST /api/packages/import/testrail (+ ?validate=1), takes a TestRail CSV export and turns each case row into a steps_expected case — same 200-row cap, same 512 KB file cap, same all-or-nothing validation as the Gherkin path. It reads either TestRail's plain "Steps" + "Expected Result" columns or the step-templated "Steps (Step)" / "Steps (Expected Result)" pairs, tolerates the usual Section/Suite and Expected Result header renames, and folds Priority/References into a labelled note at the end of Expected — there's no dedicated field for them.

This is a one-off migration of existing TestRail case definitions, not an ongoing sync — it never talks to the TestRail API and imports nothing about runs or results, only the case text.

The machine-import API — for CI and coding agents

POST /api/packages/import/machine lets a pipeline or an agent post a .feature directly, skipping the hand-off to a human uploader. It's off by default: with no token configured, the endpoint returns an opaque 404 — indistinguishable from not existing at all.

Turning it on. An admin creates a token at /settings/integrations/machine-import (DB-managed, hashed at rest, listed with a last-used timestamp, revocable without a restart), or an instance can be bootstrapped with the env var TEASYNAER_MACHINE_IMPORT_TOKEN. Either way, store the token as a CI secret — never in the repo, never in a log line.

The request:

curl -X POST https://your-teasynaer-host/api/packages/import/machine \
  -H "Authorization: Bearer $TEASYNAER_MACHINE_IMPORT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "feature": "Feature: Checkout\n  Scenario: Apply discount code\n    Given a cart with items\n    When I enter code \"SAVE10\"\n    Then the total is reduced by 10%\n",
    "name": "Checkout — regression",
    "sourceRef": "repo@abc1234:tests/checkout.feature",
    "idempotencyKey": "ci-run-8421"
  }'
FieldRequiredNotes
featureyesThe raw .feature source, ≤ 512 KB — same parser, same limits as the human upload
namenoDeck-name override, 1–255 chars; falls back to the Feature: name
sourceRefnoProvenance string, ≤ 512 chars — e.g. repo@sha:path/to.feature — and the upsert key
idempotencyKeyno≤ 128 chars — a retry with the same key is a no-op, not a duplicate

Unknown fields are rejected outright, not silently dropped, so a typo'd key is a clean 400 instead of a payload that quietly did less than you asked.

Upsert by sourceRef, dedup by idempotencyKey. First import of a sourceRef creates a deck (201). Re-importing the same sourceRef replaces that deck's case set in place (200, outcome: "updated") — it does not pile up a new deck per CI run. A hand-authored deck, or one created under a different sourceRef, is never touched. If idempotencyKey matches an earlier import, the endpoint returns the original result and writes nothing (200, outcome: "noop") — safe for a CI job that retries on a flaky network step.

Limits and posture: request body ≤ 640 KB (checked before the JSON even gets parsed), 60 imports an hour per instance with a burst allowance of 10 in any rolling minute, plus a separate budget that locks out repeated wrong tokens before any database lookup happens — so guessing at a token costs nothing beyond that lockout, and never eats into the real import quota above. Every import attempt — success or failure — is audited (package.imported), and imported decks are attributed to a dedicated, non-login service account (machine-import@service.teasynaer.invalid), never to a real admin's identity.

The CLI. scripts/import-deck.mjs is a dependency-free wrapper around the same endpoint, reading the token from the environment so it's never typed into a command line a shell history remembers:

export TEASYNAER_URL=https://your-teasynaer-host
export TEASYNAER_MACHINE_IMPORT_TOKEN=tsnmi_your-token-here

./scripts/import-deck.mjs --file checkout.feature \
  --name "Checkout — regression" \
  --source-ref "repo@$(git rev-parse HEAD):checkout.feature" \
  --idempotency-key "ci-${CI_RUN_ID:-local}"

Wiring it into CI on merge, so decks stay current without anyone opening the app:

- name: Sync QA decks into Teasynaer
  run: |
    for f in qa/*.feature; do
      jq -Rs --arg ref "${{ github.repository }}@${{ github.sha }}:$f" \
        '{feature: ., sourceRef: $ref}' "$f" \
      | curl -fsS -X POST "$TEASYNAER_URL/api/packages/import/machine" \
          -H "Authorization: Bearer $TEASYNAER_MACHINE_IMPORT_TOKEN" \
          -H "Content-Type: application/json" --data @-
    done
  env:
    TEASYNAER_URL: ${{ vars.TEASYNAER_URL }}
    TEASYNAER_MACHINE_IMPORT_TOKEN: ${{ secrets.TEASYNAER_MACHINE_IMPORT_TOKEN }}

A non-2xx fails the job — a malformed .feature blocks the merge instead of quietly landing a broken or half-imported deck.

Turn a caught fail into a draft case

The fourth path doesn't build a deck — it grows one. When a case fails, its evidence bundle already holds a recorded interaction trail (navigate/type/click). An admin viewing that fail — from the On-Fail screen or the fail's card on the Run Summary — gets a "Draft scenario from trail →" button that turns that trail into a new, editable case (case key REC-XXXXXXXX, suite recorded) sitting in the run's own deck, ready to open in the Case Editor.

Keep the scope honest: this is fails-only, it's deterministic text mapping, not generation — the interaction summaries are rendered as-captured, nothing is invented — and it is never auto-filed or auto-run. It's a draft. A human still has to open the Case Editor, read it, edit it, and save it before it's a real case anyone runs. If the trail has no captured interactions yet (the Runner's own tab only logs "case shown" and verdict cues; richer click/type capture needs the Connect extension linked to the app under test), the draft comes back with a plain placeholder step telling you to write it by hand — never a fabricated one. Drafting the same fail twice hits the same REC-… key rather than stacking duplicates.

Imported doesn't mean executed

Say this one plainly, because every path above makes it easy to assume otherwise: none of this runs a step. A machine-imported deck sits exactly as inert as one a human uploaded by hand — someone still has to open it and mark pass, fail, or skip by eye. There is no CI gate that imports a deck and then gates a build on whatever it "found", because nothing runs unattended to find anything.

The other direction — getting a completed run's human-recorded verdicts back onto the PR as a comment and a commit status — is a separate, already-documented recipe in the project repo (`/docs/integrations): a formatter, not an auto-poster, that a CI step calls once a tester has actually finished the deck.

And if you're picturing the ⌖-target chips mentioned on the writing cases page turning imports into an unattended test run: they don't, today or by design. The only thing built against them is a deterministic dry-run plan preview — a coverage estimate, not an execution engine — covered on the agent runs page. Nothing drives a browser against a live target yet, and when something eventually does, it will still hand you a provisional result for a human to confirm, never a verdict of its own.

← All docs