Skip to content

docs · zero → ci

개발자 문서

0에서 초록 파이프라인까지의 여정, 에이전트로 구동하는 법 — Peira(피라)를 쓰는 기본 방식입니다 —, 전체 CLI, 그리고 컴파일된 케이스의 실제 모습. 아래 모든 것은 실제입니다: 명령도, 플래그도, 출력 형태도 마케팅이 아니라 도구에서 나왔습니다.

시작하기

00

설치

Node ≥ 18, 퍼스트파티 의존성 하나 — 신뢰 경로에 서드파티 코드가 없습니다. 실행, 검증, 리포트는 그 외에 아무것도 필요하지 않습니다. 오직 작성 계열 명령(compile, adopt)과 오프라인 triage만 모델을 사용하며, 이들은 당신이 이미 로그인한 Claude Code CLI 세션으로 넘깁니다: 발급할 API 키도, CI에 넣을 것도 없습니다.

01

스캐폴딩 — peira init

명령 하나로 프로젝트가 준비됩니다: bed.json, 예시 인텐트 한 쌍(인수 조건 하나, 인바리언트 하나), 빈 cases/, 그리고 AGENTS.md — Claude·Cursor·Copilot 계열 에이전트가 함께 읽는 크로스툴 규약이며, Claude Code를 위한 한 줄짜리 CLAUDE.md 임포트가 딸려 옵니다. 신뢰 경로의 다른 모든 것처럼 결정론적이고 LLM을 쓰지 않습니다. 프롬프트를 묻지 않으므로 에이전트가 대신 실행할 수 있고, 기존 파일을 덮어쓰지 않습니다 — 파일마다 created 또는 kept를 보고합니다.

peira init          # bed.json, intent/example.md, AGENTS.md (+ CLAUDE.md import), cases/
peira init --ci     # …여기에 LLM을 쓰지 않는 GitHub Actions 워크플로까지

이후 두 단계는 이것으로 줄어듭니다: bed.json이 당신의 서비스를 가리키게 하고, 예시 인텐트를 실제 약속으로 바꾸는 것 — 또는 서비스가 무엇을 약속하는지 에이전트에게 말하면 됩니다. AGENTS.md가 에이전트에게 맥락을 줍니다.

02

서비스를 서술합니다 — bed.json

베드 설정은 Peira가 당신의 서비스에 대해 배우는 유일한 곳입니다. baseUrl을 제외하면 모두 선택 사항입니다: users는 케이스가 $users.alice로 참조하는 이름 붙은 주체입니다(자격 증명은 케이스에 들어가지 않습니다) — Basic 인증, 토큰을 돌려주는 로그인 요청, 또는 고정 API 키 중 하나이며, 어느 쪽이든 케이스는 $users.staff라고만 씁니다; reset은 매 실행 전에 당신의 서비스가 가진 상태 초기화 엔드포인트를 한 번 호출합니다; drain은 비동기 작업이 끝났는지 묻는 방법을 러너에게 알려 주어, 한 케이스의 잔여물이 다음 케이스의 타이밍을 오염시키지 못하게 합니다; timeouts는 느린 환경의 지연 허용치를 선언합니다(상한일 뿐이며, 걸리면 fail이 아니라 error 판정입니다); service는 테스트 대상 앱을 peira run이 어떻게 띄울지 알려 줍니다 — 이미 응답 중인 baseUrl은 그대로 재사용하고, Peira가 띄운 서버는 실행이 끝날 때 프로세스 그룹째 정리합니다.

{
  "baseUrl": "http://localhost:8080",
  "users": {
    "alice": { "username": "alice", "password": "test-pw" },
    "staff": { "login": { "route": "/api/login", "body": { "email": "staff@example.com", "password": "…" },
                          "token": "body.token", "send": { "header": "Authorization", "format": "Bearer {{token}}" } } }
  },
  "reset": { "method": "post", "url": "/test/reset" },
  "drain": { "route": "/orders/status", "idParam": "id",
             "statusPath": "body.state", "terminal": ["SHIPPED", "CANCELLED"] },
  "service": { "command": "npm run dev", "cwd": "../orders-service" }
}

환경마다 베드 파일을 하나씩 두세요(bed.json, bed.ci.json): 같은 케이스, 다른 대상.

03

인텐트 작성 — intent/*.md

인텐트는 사람이 소유하는 유일한 진실의 출처입니다: 평범한 마크다운이고, ## 섹션 하나가 인수 조건 또는 인바리언트 하나입니다. 태그는 선택이지만 권장합니다 — id는 영구적인 계보 앵커여서, 태그가 붙은 섹션은 문장을 자유롭게 고쳐 써도 케이스가 미아가 되지 않습니다. kind=invariant 섹션은 템플릿으로 컴파일되어 실행할 때마다 새 시드 프로브를 생성합니다. 파일은 엔드포인트가 아니라 능력 단위로 나누세요: 모든 것의 단위는 섹션입니다.

## Creating an order
<!-- peira: id=order-create kind=ac -->
POST /orders with a valid payment method returns 201 with the new order's id.

## Order isolation
<!-- peira: id=order-isolation kind=invariant -->
For all orders o, for all users u ≠ owner(o): GET /orders/{o} as u → 403.

이미 정리되지 않은 테스트 계획이 있나요? peira adopt가 한 번만 재구조화하고 — 결코 다시 쓰지 않습니다 — 내용 보존 리포트를 출력합니다. 검토하고 커밋하면, 그때부터 그것은 당신의 문서입니다.

04

컴파일

당신의 Claude 세션에서 동작합니다. 모든 후보 케이스는 손으로 쓴 케이스와 똑같은 스키마 게이트를 통과하고, 계보는 기계적으로 각인되며, 컴파일 매니페스트가 모든 섹션을 설명합니다(컴파일됨 / 사유와 함께 건너뜀 / 거부됨). 생성된 케이스는 diff로 검토하세요 — 그 검토가 신뢰 모델이 서 있는 사람의 확인 지점입니다.

peira compile intent --out cases --bed bed.json
peira compile intent --dry-run    # 리포트만, 아무것도 쓰지 않음

--dry-run은 당신의 문서에 대한 피드백 루프입니다: 몇 개 섹션이 컴파일되었는지, 그리고 어떤 섹션이 왜 건너뛰어졌는지("검증 가능한 동작을 진술하지 않음; 라우트도 상태 코드도 명시하지 않음") 또는 어떤 후보가 왜 거부되었는지 알려 줍니다. 건너뜀은 도구의 실패가 아니라 당신의 인텐트에 대한 메모입니다.

05

로컬 실행 — 그리고 루프 닫기

판정은 pass | fail | error입니다 — 단정 실패와 인프라 실패를 결코 뒤섞지 않습니다. 시드는 항상 출력됩니다: 어떤 실패든 같은 시드와 같은 서비스 상태에서 그대로 재현됩니다. 첫 실행은 대개 진짜 버그와 낡은 인텐트를 동시에 드러냅니다 — 그것이 요점입니다. 한 가지 주의: 초기화하지 않는 서비스에 같은 시드로 실행하면 이전 실행이 만든 데이터와 충돌하므로, 상태를 만드는 케이스에는 reset이나 새 시드가 필요합니다 — CI는 이미 run id를 쓰고 있으니, 로컬에서도 시드를 바꾸세요.

peira run cases --bed bed.json --seed 42 --evidence run.jsonl
peira run cases --bed bed.json --seed 42 --only CASE-order-cancel-001   # 실패한 케이스 하나만 재실행
peira run cases --bed bed.json --parallel 8   # 워커 풀; 판정과 증거 순서는 순차 실행과 동일
peira run cases --bed bed.json --intent intent --watch   # 변경 시 재실행, 계보 기반
peira triage --evidence run.jsonl --intent intent   # bug | drift | flake 제안; 적용은 하지 않음
# 당신이 판단합니다: 서비스를 고치거나, 인텐트를 고치거나…
peira validate cases --bed bed.json --intent intent # stale 표시가 영향받은 케이스를 지목
peira compile intent --out cases --bed bed.json --section <changed-section>

watch 모드는 임포트 그래프가 아니라 계보로 변경을 해석합니다: 케이스를 고치면 정확히 그 케이스만 재실행되고, 인텐트를 고치면 stale 여부를 다시 확인해 영향받은 케이스를 지목합니다 — 재컴파일은 저장 훅이 아니라 당신의 결정입니다. 읽을 수 있는 문서는 언제든 공유하세요: peira render cases --intent intent --evidence run.jsonl (Given/When/Then 또는 완전한 HTML 실행 리포트; 단방향 산출물이니 고치지 말고 다시 만드세요).

06

CI — LLM 호출 0

intent/, cases/, 베드 설정을 커밋하세요. CI에는 키도 세션도 필요 없습니다. 머지를 막는 것은 종료 코드이고, --junit은 표준 JUnit XML을 씁니다(pass/fail/error가 testcase/failure/error로 대응됩니다). 그래서 어떤 CI 리포트 UI든 래퍼 스크립트 없이 실행 결과를 그려 줍니다. CI가 빨개지면 증거 아티팩트를 받아 로컬에서 트리아지하세요 — 판단은 파이프라인의 봇이 아니라 사람의 몫입니다.

# .github/workflows/api-tests.yml
- run: npm ci
- run: docker compose up -d orders-service
- run: npx peira validate cases --bed bed.ci.json --intent intent
- run: npx peira run cases --bed bed.ci.json --seed ${{ github.run_id }} --evidence run.jsonl --junit junit.xml
- if: always()
  uses: actions/upload-artifact@v4
  with: { name: evidence, path: run.jsonl }

그다음 판단이 끝난 실행을 기록하세요: peira evidence --evidence run.jsonl --triage run-triage.json --intent intent. 통과한 섹션은 applied를, 판단된 drift는 사유가 그대로 인용된 채 contradicted를 남깁니다. 현황은 peira trust로 봅니다. 신뢰는 손이 아니라 실행이 만듭니다.

에이전트로 Peira 쓰기

Peira를 쓰는 기본 방식입니다. 설계부터 에이전트 친화적입니다: 작성 계열 표면은 이미 당신의 Claude 세션에서 돌고, 결정론적 러너야말로 에이전트 주도 테스트를 신뢰할 수 있게 만드는 것입니다. 실제로는 인텐트의 언어로 에이전트와 대화하면 — 에이전트가 계획을 고치고, 컴파일하고, 실행하고, 리포트를 만들고, 당신의 판단을 위해 트리아지 초안을 씁니다.

대화로 보는 루프

you

커버리지를 추가해 줘: 이미 배송된 주문의 취소는 거부되어야 해.

agent

intent/orders.md에 태그된 섹션을 추가하고, 그 섹션만 컴파일한 뒤 스위트를 실행했습니다:

$ peira compile intent --out cases --bed bed.json --section order-cancel-shipped
$ peira run cases --bed bed.json --evidence run.jsonl
agent

실패 1건 — 인텐트는 409를 요구하는데 서비스는 200을 반환합니다. 트리아지는 요청/응답 증거를 붙여 BUG로 제안합니다. 리포트를 첨부합니다. 버그로 등록할까요, 아니면 인텐트를 바꿔야 할까요?

$ peira triage --evidence run.jsonl --intent intent
$ peira render cases --intent intent --evidence run.jsonl --format html --out report.html
you

버그야. 등록하고 실행을 기록해 줘.

agent

등록했습니다. 실행은 증거 원장에 기록되었고, 해당 섹션은 applied로 남았습니다(위반을 잡아내는 제 몫을 했습니다).

$ peira evidence --evidence run.jsonl --triage run-triage.json --intent intent

드롭인 에이전트 지침

peira init이 이것을 AGENTS.md로 만들어 줍니다 — Claude, Cursor, Copilot 계열 에이전트가 함께 읽는 크로스툴 규약이며, Claude Code를 위한 CLAUDE.md 임포트가 딸려 옵니다 — 그러면 위 워크플로가 그대로 동작합니다:

# API testing with Peira

Peira compiles a markdown test plan (intent/*.md) into JSON cases and runs them with
no model in the loop. Everything the tool can say is in one place — read it before you
write a case, and again after the tool is upgraded:

    peira reference

## The loop

- Intent is the source of truth. To change a test, edit its intent section, then
  recompile exactly that section:
    peira compile intent --out cases --bed bed.json --section <id>
- A case written by hand is fine; bind it to its section without a model:
    peira stamp cases --intent intent        (--check in CI: exit 1 if anything is unstamped or stale)
- Run and keep the evidence (the printed seed replays any failure exactly):
    peira run cases --bed bed.json --evidence run.jsonl
- On failures, triage and PRESENT the proposals — adjudication belongs to the
  human, never to you:
    peira triage --evidence run.jsonl --intent intent
- When the human wants to see results:
    peira render cases --intent intent --evidence run.jsonl --format html --out report.html
- After adjudication, record the run so intent sections earn trust:
    peira evidence --evidence run.jsonl --triage run-triage.json --intent intent

## Rules the gate enforces (validate says so, with the fix in the message)

- Never edit a compiled case to make a run green; fix the service or propose an intent change.
- from.intent is yours; from.hash never is — compile stamps it, `peira stamp` fills it.
- Inside a string use {{alias}}; a bare $alias is only the whole value.
- No wall-clock sleeps. Eventual consistency is pollUntil; cleanup is teardown {"drain": true}.
- Matchers stand alone: $any, $contains (string or all-of list), $notContains, $absent, $text, null.
  Negative claims are where the bugs are — assert what a user must NOT see or hold.
- Cases never contain credentials: auth is "$users.<alias>"; the bed defines the alias.
- A red run is pass | fail | error and the kinds are never conflated: error means the
  environment failed before the claim was judged — say so, do not report it as a bug.

에이전트에게 맡겨도 안전한 이유

러너는 설득되지 않습니다

판정은 결정론적입니다 — (케이스, 시드, 서비스 상태)의 함수입니다. 런타임에 LLM이 없다는 것은 에이전트가 빨간 실행을 초록으로 구슬릴 수 없다는 뜻입니다. 할 수 있는 일은 서비스를 고치거나, 당신이 승인할 인텐트 변경을 제안하는 것뿐입니다.

게이트는 거부할 뿐, 고치지 않습니다

모델이 내놓는 모든 것 — 컴파일된 케이스, 트리아지 제안, 채택된 인텐트 — 은 결정론적 스키마 게이트를 지납니다. 형식이 어긋난 출력은 사유와 함께 거부되며, 조용히 교정되지 않습니다.

스스로 적용되는 것은 없습니다

트리아지는 bug | drift | flake를 제안하고, 판단은 사람이 합니다. 인텐트는 당신의 것이고, 케이스는 다시 만들 수 있는 산출물이며, 증거 원장은 무엇이 결정되었는지 사유를 그대로 인용해 기록합니다.

당신의 세션에서 돕니다

compile, triage, adopt는 당신이 이미 로그인한 Claude Code CLI로 넘깁니다 — 에이전트가 사는 바로 그 세션입니다. 발급할 API 키도, 따로 지킬 것도 없습니다.

CLI 레퍼런스

명령 열두 개. 모델을 건드리는 것은 compile, triage, adopt뿐이며 — 당신의 세션에서 돌고, CI에서는 결코 돌지 않습니다.

init

peira init [dir] [--ci]

프로젝트 스캐폴딩: bed.json, 예시 인텐트, AGENTS.md 에이전트 지침(+ CLAUDE.md 임포트), cases/. --ci는 LLM을 쓰지 않는 GitHub Actions 워크플로를 추가합니다. 결정론적이고, 묻지 않으며, 덮어쓰지 않습니다.

validate

peira validate [casesDir] [--bed <path>] [--intent <dir>]

모든 케이스에 대한 스키마 + 정적 검사. --intent를 주면 낡은 케이스를 표시하고 인텐트 구조를 린트합니다.

run

peira run [casesDir] --bed <path> [--seed <n>] [--evidence <path>] [--only <id>]… [--grep <substr>] [--parallel <n>] [--junit <path>] [--shard <i>/<n>] [--watch]

결정론적 러너입니다. LLM 호출 0; 시드 기반이라 재현되며, 자격 증명은 기록 시점에 가려진 채 증거 JSONL로 남습니다. --only/--grep은 지정한 케이스만 재실행하고, --parallel은 판정과 증거 순서가 순차 실행과 동일한 워커 풀을 돌리며, --junit은 CI 표준 XML을 냅니다. --shard는 서로 겹치지 않는 결정론적 조각으로 머신을 나누고, --watch는 계보 기반으로 변경 시 재실행합니다.

compile

peira compile [intentDir] --out <dir> [--bed <path>] [--section <id>]… [--dry-run]

인텐트 섹션을 당신의 Claude 세션을 통해 스키마 게이트를 통과한 JSON 케이스로 만듭니다. --section은 지정한 섹션만 재컴파일하고 매니페스트를 병합합니다. --dry-run은 아무것도 쓰지 않고 보고합니다: 인텐트가 얼마나 컴파일되는지, 그리고 어떤 섹션이 왜 건너뛰어지거나 거부되었는지.

stats

peira stats [casesDir] [--openapi <spec.json>]

DSL 커버리지, 반복되는 이스케이프 해치 형태, 그리고 거부 균형 — 인텐트마다 케이스 수, 긍정(기대 상태 < 400), 부정(≥ 400), 부정 오라클($absent / $notContains)을 표로 내고, 해피 패스만 테스트하는 인텐트를 한 줄로 지목합니다. 스위트는 해피 패스 하나씩 긍정 쪽으로 기울어 갑니다; 이것이 매 실행마다의 가드입니다. --openapi를 주면 엔드포인트 커버리지 — 케이스가 없는 엔드포인트 — 를 냅니다.

triage

peira triage --evidence <run.jsonl> --intent <dir>

오프라인 실패 분류: 인텐트 문장을 기준으로 판단한 bug | drift | flake. 제안일 뿐이며, 무엇도 적용되지 않습니다.

evidence

peira evidence --evidence <run.jsonl> [--triage <file>] --intent <dir>

판단이 끝난 실행을 증거 원장에 기록합니다(휴대 가능한 JSONL 내보내기 포함). 섹션은 실행마다 applied 또는 contradicted를 얻습니다.

trust

peira trust

원장 현황 — 인텐트 섹션별 applied, contradicted, 실행 횟수, 마지막 applied 시점.

render

peira render [casesDir] [--evidence <run.jsonl>] [--format md|html]

단방향으로 읽을 수 있는 문서: Given/When/Then 마크다운, 또는 실패 시 관찰된 교환까지 담은 자체 완결형 HTML 실행 리포트.

adopt

peira adopt <messy.md> --out <intent/name.md>

일회성 작성 보조: 임의의 문서를 태그가 붙은 인텐트로 재구조화하고 내용 보존 리포트를 냅니다. 검토도 소유도 당신의 몫입니다.

stamp

peira stamp [casesDir] --intent <dir> [--check]

모델 없이 손으로 쓴 케이스를 인텐트에 묶습니다: 살아 있는 섹션 텍스트로부터 from.hash를 채우거나 갱신합니다. from.intent는 당신의 것이고, from.hash는 결코 아닙니다. --check는 바뀔 케이스가 하나라도 있으면 1로 종료합니다 — 계보를 위한 LLM 없는 CI 게이트입니다.

reference

peira reference

설치된 버전의 전체 어휘 — 모든 속성에 설명이 붙은 케이스·베드 스키마, 매처, 보간, 주체, 응답, 판정, CLI — 를 스키마 자체에서 생성한 마크다운으로 출력합니다. 에이전트가 dist/ 대신 읽는 것이며, AGENTS.md 스캐폴드가 여기를 가리킵니다.

전체 플래그 목록: peira help

케이스 해부

케이스는 JSON입니다: 선택적인 setup 스텝들, test 스텝 하나, 선택적인 teardown. 다섯 개 프리미티브가 손으로 쓴 스위트에 필요한 것을 감당합니다 — 실제 레거시 스위트를 이스케이프 해치 없이 27/27 재표현했습니다.

{
  "id": "CASE-order-isolation-001",
  "title": "Another user cannot read my order",
  "from": { "intent": "order-isolation", "hash": "ae5ab7a63816" },
  "setup": [{
    "request": { "method": "post", "route": "/orders",
                 "auth": "$users.alice",
                 "body": { "note": "x {{unique.nonce}}" } },
    "capture": { "orderId": "body.id" }
  }],
  "test": {
    "request": { "method": "get", "route": "/orders/$orderId",
                 "auth": "$users.bob" },
    "expect": { "status": 403 }
  },
  "teardown": { "drain": true }
}
from
Lineage, stamped mechanically at compile time — never trusted from the model. When the intent section's text changes, this hash mismatch flags the case stale.
$users.alice
A bed principal by name. Cases never contain credentials; the bed maps names to auth per environment.
{{unique.nonce}}
Seed-derived discriminator: hash(seed, case id, key). Same seed → same value; no fixture files.
capture
Maps an alias to a response path (body.id). Later steps reference it as $orderId (whole value) or {{orderId}} inside strings.
expect
Subset matching, Jest toMatchObject parity, on status, headers (case-insensitive — {"content-type": {"$contains": "application/json"}}), and body. Matchers: {"$any": "string" | "number" | "boolean"}, {"$contains": "s" | ["a", "b"]}, {"$notContains": …}, {"$absent": true}, and literal null. Add pollUntil for eventual consistency — never wall-clock sleeps.
teardown.drain
Declares that this case must clean up; the bed's drain probe knows how. The runner polls every captured job to a terminal state before the next case runs.

케이스는 다시 만들 수 있는 산출물입니다 — 손으로 고쳐 어긋나게 두지 마세요. 인텐트를 바꾸고, 섹션을 재컴파일하고, diff를 검토하세요. 그것이 규율의 전부입니다.

레퍼런스

프로그래밍 가능한 표면 전체를 한 페이지에 담았습니다 — DSL이 의도적으로 닫혀 있기에 유한합니다. 여기 없는 것은 스키마 게이트가 거부하며, 어휘는 확장 훅이 아니라 개정을 통해서만(stats 텔레메트리를 근거로) 자랍니다.

케이스

JSON 파일 하나입니다. id, from, test는 필수입니다.

id
CASE-<kebab-slug> — 케이스 집합 안에서 유일해야 하며, 중복은 거부됩니다.
from
계보 {intent, hash}: 어떤 섹션이 어떤 내용 해시일 때 이 케이스를 만들었는지. 기계적으로 각인되며 모델의 말을 믿지 않습니다. 해시가 어긋나면 케이스는 stale로 표시됩니다. 템플릿에서 생성된 인스턴스는 {template, seed, instance}를 더합니다.
setup
선택적인 스텝 배열 — 요청 스텝 또는 레지스트리 스텝 호출 — 을 순서대로 실행합니다.
test
정확히 하나의 요청 스텝이며, 검증하려는 주장이 여기 있습니다.
teardown
{"drain": true} — 판정 후, 캡처된 모든 id를 베드의 drain 프로브로, 그것을 캡처한 자격 증명으로 종료 상태까지 폴링합니다.

요청 스텝

request
method(get | post | put | delete | patch), route(/로 시작), 선택적 query와 body. auth는 네 가지 형태입니다: "$users.<alias>"(베드 주체), 부정 Basic 인증 테스트를 위한 리터럴 {username, password}, 부정 토큰 테스트를 위한 리터럴 {token}(send는 선택, 기본은 Bearer), 또는 익명을 뜻하는 생략.
headers
케이스 자체의 요청 헤더 — {"Accept": "text/html", "Accept-Language": "ko"} — 보간됩니다. 가장 낮은 우선순위로 적용됩니다: 주체의 인증 헤더가 이를 덮어쓰고(케이스는 자신의 Authorization을 바꿀 수 없습니다) content-type은 본문 종류가 소유합니다. content-length, transfer-encoding, host는 거부되며, HTTP 런타임이 소리 없이 고쳐 쓰는 Sec-* / Proxy-*도 거부됩니다.
form
{name: value}을 application/x-www-form-urlencoded로 보냅니다 — 하이드레이션 전 브라우저가 보내는 네이티브 폼 전송입니다. body·multipart와 배타적이며, password 필드는 증거에서 가려집니다.
multipart
{fields?, files?: [{field, path | bytes, mimetype?, filename?}]} — JSON 본문 대신 multipart/form-data를 보냅니다(둘 다는 불가). files[].path는 케이스 디렉터리 기준 상대 경로입니다: 저장소 안의 보통 파일이며 바이트를 인라인으로 담지 않습니다; 파일이 없거나 256 KB를 넘으면 validate가 거부합니다. mimetype을 명시하므로 잘못된 형식 거부도 케이스가 됩니다. 증거 로그에는 이름과 크기만 남고 내용은 결코 남지 않습니다.
followRedirects
기본값 true. false이면 스텝이 자신의 3xx를 직접 봅니다 — expect.status 307과 expect.headers.location을 단정할 수 있고, capture: {next: "headers.location"}이 의미를 갖습니다.
capture
별칭 → status, body, headers를 루트로 하는 점 표기 응답 경로(body.id, headers.location). 응답에 없는 경로는 그 경로를 지목하며 케이스를 실패시킵니다.
pollUntil
expect 블록이 일치할 때까지 요청을 다시 보냅니다 — 고정된 100ms 간격, timeoutMs 상한(기본 10초 또는 베드의 pollUntilMs). 수렴하지 않으면 fail입니다. 거부되는 sleep을 대신하는 선언적 수단입니다.

expect — 오라클

Jest toMatchObject와 동일한 부분 일치: 객체는 모든 깊이에서 부분집합으로, 배열은 길이가 같은 상태에서 인덱스별로, 원시값은 엄격하게 비교합니다.

status
정확한 상태 코드.
headers
이름 대소문자를 구분하지 않고(RFC 9110) 응답 헤더를 검사합니다. 값은 리터럴 문자열 또는 매처이며, 그 외는 거부됩니다. 없는 헤더는 이름이 붙은 diff가 됩니다.
body
JSON 본문에 대한 부분 일치.
oracle
{statusOnly: "<이유>"} — 단정이 아니라 메모입니다: 이 케이스는 일부러 상태 코드만 단정합니다(추론할 수 없도록 동일한 404들). 약한 오라클 린트가 이를 받아들이고, render가 출력하며, body나 headers 단정 옆에서는 거부됩니다.
bodySchema
본문 전체가 만족해야 하는 JSON 스키마 부분집합(type, required, properties, additionalProperties, enum, items, pattern, anyOf) — "모든 원소가 X 형태"류의 주장에 씁니다.
{"$any": …}
매처: 존재하며 "string" | "number" | "boolean" 타입일 것.
{"$contains": …}
매처: 부분 문자열을 포함하는 문자열 — 또는 목록의 모든 부분 문자열을 포함(all of; 빠진 것마다 별도 diff). content-type 매처이자, 텍스트 본문의 오라클입니다: HTML 등 JSON이 아닌 응답은 문자열로 도착합니다. 함정 하나: 서버 렌더링 React는 인접한 텍스트 표현식 사이에 <!-- -->를 끼우므로, 보간을 가로지르는 보이는 텍스트가 아니라 한 표현식에서 나온 텍스트나 안정적인 속성값을 단정하세요.
{"$notContains": …}
매처: 나열한 부분 문자열을 하나도 포함하지 않는 문자열 — "X가 새면 안 된다". 오픈 리다이렉트 가드: location: {$notContains: "evil.example"}. 긍정형만으로는 https://evil.example/?back=/hub에 속습니다.
{"$text": {contains, notContains}}
매처: 본문을 텍스트로 — 태그와 주석을 벗기고 공백을 접은 뒤 — 모두 포함하고 하나도 포함하지 않아야 합니다. 본문 전체에만 씁니다. 서버 렌더링 React는 텍스트 사이에 <!-- -->를 끼우므로 원문에 대한 $contains는 "총 2건"을 놓칩니다; $text는 찾아내며, 같은 텍스트 본문에 contains와 notContains를 함께 쓸 수 있는 유일한 자리입니다.
{"$absent": true}
매처: 키 또는 헤더가 존재하지 않아야 합니다. null과는 다르며, 본문 전체에는 쓸 수 없습니다. 대표적인 쓰임은 거부된 권한을 생략으로 표현하는 접근 맵입니다 — 편집자로서 GET /api/access → {"tenants": {"create": {"$absent": true}}}. 허용이 아니라 생략을 단정하세요: 이 사용자가 가져서는 안 되는 것이 무엇인지를 묻는 질문이며, 긍정형 검사는 결코 던지지 않는 질문입니다.
null
매처: 존재하며 정확히 null. 매처는 단독으로 쓰이며 body, pollUntil.until, 헤더 값에서 동작합니다. 사용자 정의 매처는 설계상 없습니다 — 어휘는 개정을 통해 자랍니다.

보간

"$alias"
문자열 전체가 참조이면 캡처된 값으로, 타입을 보존한 채 치환됩니다.
{{alias}}
임의의 문자열 안, 임의의 깊이에서 String(value)로 끼워집니다. {{{{는 리터럴 {{를 뜻합니다.
unique.<key>
시드에서 파생된 판별자: hash(seed, caseId, key). 같은 시드 → 같은 값이며, 픽스처 파일이 필요 없습니다.
$users.<alias>
베드 주체 — 요청의 auth 자리에서만 유효하며, 데이터에 끼워지지 않습니다.

베드 — bed.json

Peira가 당신의 서비스에 대해 배우는 유일한 곳입니다. baseUrl 외에는 모두 선택 사항입니다.

baseUrl
서비스가 응답하는 주소이며, 호출마다 --base-url로 덮어쓸 수 있습니다.
users
이름 붙은 주체 — 케이스는 $users.alice라고만 쓰고 자격 증명을 담지 않습니다. 별칭 하나에 정확히 한 형태: Basic {username, password}; 로그인 {login: {method?, route, body?, token, send}} — 처음 쓰일 때 실행당 한 번 로그인하고(--parallel에서도 한 번), body.token 같은 경로에서 토큰을 캡처해 send대로 붙입니다; 또는 API 키를 위한 고정 {token, send}. send는 {header, {{token}}을 포함한 format} 또는 {cookie}입니다. 거부된 로그인은 그 주체를 쓰는 모든 케이스를 fail이 아닌 error로 만듭니다. 토큰과 비밀번호는 값 기준으로 증거 로그에서 지워집니다.
reset
{url, method?} — 매 실행 전 상태 초기화 호출 한 번.
drain
{route, idParam, statusPath, terminal[]} — 비동기 작업이 끝났는지 묻는 방법이며, teardown.drain의 동력입니다.
timeouts
지연 허용 상한 {requestMs?, pollUntilMs?, drainMs?, stepMs?}. 걸리면 fail이 아니라 error이며, 폴링 간격은 결정성을 위해 고정입니다.
service
{command, cwd?, readyMs?, reuse?} — peira run이 테스트 대상 앱을 띄우는 방법입니다. reuse(기본값)는 이미 응답 중인 baseUrl을 그대로 쓰고 죽이지 않습니다. Peira가 띄운 서버는 실행이 끝날 때 프로세스 그룹째 정리됩니다.

판정, 종료 코드, 증거

pass | fail | error
fail은 단정이 성립하지 않은 것이고, error는 단정을 판단하기 전에 인프라가 실패한 것입니다. 결코 뒤섞이지 않으며 — --junit은 이를 testcase/failure/error로 손실 없이 대응시킵니다.
종료 코드
0 전부 통과 · 1 fail/error 발생(또는 케이스 집합이 거부됨, 또는 서비스가 응답하지 않음) · 2 사용법 오류.
run.jsonl
한 줄에 이벤트 하나인 추가 전용 JSONL: run-start, minted, case-start, http(모든 교환의 요청·응답·elapsedMs), step, case-verdict, drain-*, run-end(counts, wallMs, httpMs). 트리아지도, 원장도, 리포트도 이것을 읽습니다 — 연동 지점입니다.
가림 처리
Authorization, Cookie, Set-Cookie 값은 기록 시점에 [REDACTED:<sha256-prefix>]로 저장됩니다 — 이벤트 간 동일성은 남고, 비밀은 로그에 남지 않습니다.

이스케이프 해치와 템플릿

steps
타입이 있는 계약을 가진 생성된 절차입니다: {id, reads[], produces[], code}. setup에서만 {"step": "STEP-…", "bind": {…}}로 호출되며 — 호출은 구조적으로 expect나 capture를 가질 수 없습니다. 주장은 선언적인 곳에 남습니다. 모든 사용은 DSL에 어떤 프리미티브가 빠졌는지 묻는 텔레메트리입니다.
holes
인바리언트 템플릿은 타입이 있는 구멍을 선언합니다 — principal(선택적으로 다른 구멍과 distinctFrom), expression({{holes.x.code}} / {{holes.x.result}}), unique — 그리고 실행마다 시드 기반 케이스 5개를 생성합니다. (template, seed, instance)로 언제든 정확히 재현됩니다.

기준: schema/case.schema.json · 전체 문서: 저장소의 docs/REFERENCE.md