docs · zero → ci
개발자 문서
0에서 초록 파이프라인까지의 여정, 에이전트로 구동하는 법 — Peira(피라)를 쓰는 기본 방식입니다 —, 전체 CLI, 그리고 컴파일된 케이스의 실제 모습. 아래 모든 것은 실제입니다: 명령도, 플래그도, 출력 형태도 마케팅이 아니라 도구에서 나왔습니다.
시작하기
설치
Node ≥ 18, 퍼스트파티 의존성 하나 — 신뢰 경로에 서드파티 코드가 없습니다. 실행, 검증, 리포트는 그 외에 아무것도 필요하지 않습니다. 오직 작성 계열 명령(compile, adopt)과 오프라인 triage만 모델을 사용하며, 이들은 당신이 이미 로그인한 Claude Code CLI 세션으로 넘깁니다: 발급할 API 키도, CI에 넣을 것도 없습니다.
스캐폴딩 — 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가 에이전트에게 맥락을 줍니다.
서비스를 서술합니다 — 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): 같은 케이스, 다른 대상.
인텐트 작성 — 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가 한 번만 재구조화하고 — 결코 다시 쓰지 않습니다 — 내용 보존 리포트를 출력합니다. 검토하고 커밋하면, 그때부터 그것은 당신의 문서입니다.
컴파일
당신의 Claude 세션에서 동작합니다. 모든 후보 케이스는 손으로 쓴 케이스와 똑같은 스키마 게이트를 통과하고, 계보는 기계적으로 각인되며, 컴파일 매니페스트가 모든 섹션을 설명합니다(컴파일됨 / 사유와 함께 건너뜀 / 거부됨). 생성된 케이스는 diff로 검토하세요 — 그 검토가 신뢰 모델이 서 있는 사람의 확인 지점입니다.
peira compile intent --out cases --bed bed.json peira compile intent --dry-run # 리포트만, 아무것도 쓰지 않음
--dry-run은 당신의 문서에 대한 피드백 루프입니다: 몇 개 섹션이 컴파일되었는지, 그리고 어떤 섹션이 왜 건너뛰어졌는지("검증 가능한 동작을 진술하지 않음; 라우트도 상태 코드도 명시하지 않음") 또는 어떤 후보가 왜 거부되었는지 알려 줍니다. 건너뜀은 도구의 실패가 아니라 당신의 인텐트에 대한 메모입니다.
로컬 실행 — 그리고 루프 닫기
판정은 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 실행 리포트; 단방향 산출물이니 고치지 말고 다시 만드세요).
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 세션에서 돌고, 결정론적 러너야말로 에이전트 주도 테스트를 신뢰할 수 있게 만드는 것입니다. 실제로는 인텐트의 언어로 에이전트와 대화하면 — 에이전트가 계획을 고치고, 컴파일하고, 실행하고, 리포트를 만들고, 당신의 판단을 위해 트리아지 초안을 씁니다.
대화로 보는 루프
커버리지를 추가해 줘: 이미 배송된 주문의 취소는 거부되어야 해.
intent/orders.md에 태그된 섹션을 추가하고, 그 섹션만 컴파일한 뒤 스위트를 실행했습니다:
실패 1건 — 인텐트는 409를 요구하는데 서비스는 200을 반환합니다. 트리아지는 요청/응답 증거를 붙여 BUG로 제안합니다. 리포트를 첨부합니다. 버그로 등록할까요, 아니면 인텐트를 바꿔야 할까요?
버그야. 등록하고 실행을 기록해 줘.
등록했습니다. 실행은 증거 원장에 기록되었고, 해당 섹션은 applied로 남았습니다(위반을 잡아내는 제 몫을 했습니다).
드롭인 에이전트 지침
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