본문으로 바로가기
리옵트 핸드북
리옵트 핸드북
에이전틱 테스트 환경 엔지니어링

근본 원리

테스트 포트폴리오 운영 모델환경·상태 통제 전략실행 토폴로지와 러너 아키텍처테스트 하네스 엔지니어링

계약 & 구성

인터페이스 계약과 관측 포인트상태 구성과 시나리오 픽스처

실행 & 게이트

실행 패브릭과 오케스트레이션리스크 기반 릴리즈 게이트

신뢰도 & 운영

비결정성과 Flake 관리관측성·신뢰도 지표테스트 인시던트 대응거버넌스와 소유권

부록

런북 템플릿검증 리포트업데이트 내역
핸드북›에이전틱 테스트 환경 엔지니어링›테스트 하네스 엔지니어링

테스트 하네스 엔지니어링

테스트 코드를 넘어 환경 제어, 증거 수집, 재현성, agent repair boundary를 묶는 테스트 하네스 설계 원칙과 패키지 구조

핵심 요약

  • 테스트 하네스는 스크립트가 아니라 환경·상태·도구·판정 경계를 묶어 신뢰 가능한 증거를 만드는 작업 장치입니다.
  • entry contract·state controller·actuator wrapper·evidence collector·judge boundary·repair boundary 6계층으로 책임을 나눕니다.
  • testops/ 폴더 묶음으로 시작하고 lifecycle 공유·다중 저장소·정책 안정성 조건 2개 이상이면 공용 패키지로 승격합니다.
  • core는 타입·vocabulary·lifecycle만 두고 Playwright import나 DB reset 같은 구현은 절대 넣지 않습니다.
  • agent repair는 allowedPaths/blockedPaths와 release·unknown 시 human approval 정책으로 경계를 강제합니다.

테스트 하네스 엔지니어링은 "테스트를 돌리는 스크립트"를 만드는 일이 아닙니다. 테스트가 신뢰할 증거를 만들도록 환경, 상태, 도구, 판정 경계를 설계하는 일입니다.

이 책에서 말하는 agentic test environment가 시스템 전체 관점이라면, 테스트 하네스는 그 안에서 실제 probe를 안전하게 실행하고 재현하게 만드는 작업 장치에 가깝습니다.

테스트 하네스는 무엇을 맡는가

계층하네스가 맡는 일없으면 생기는 문제
entry contract어떤 seed, 계정, 환경 변수, feature flag로 시작할지 고정같은 테스트가 매번 다른 전제에서 실행됨
state controllerseed/reset, clock, queue, sandbox 상태를 준비flaky와 제품 회귀가 섞여 보임
actuator wrapperbrowser, API, workflow runner를 안전한 범위 안에서 노출probe마다 구현 방식과 권한이 제각각
evidence collectortrace, screenshot, log, event, env fingerprint를 묶어 저장실패해도 왜 실패했는지 설명 불가
judge boundaryretry, quarantine, release gate, human approval를 연결green/red 의미가 사람마다 다름
repair boundaryagent가 어디까지 재실행·수정 가능한지 제한agent가 사실상 무제한 self-approve 통로가 됨

로컬 폴더에서 시작하는 최소 하네스

가장 먼저 필요한 건 거대한 플랫폼이 아니라 아래 정도의 묶음입니다.

entry-contract.yaml
state-controller.ts
evidence-schema.ts
gate-policy.yaml
repair-boundary.md
browser.ts
api.ts
workflow.ts

이 패키지는 "테스트를 예쁘게 정리"하려는 게 아닙니다. probe가 어떤 전제에서 시작했고 어떤 증거를 남겼는지 세션을 바꿔도 복원하려는 것입니다.

한 앱, 한 제품, 한 suite owner 수준에서는 이 구조로도 충분합니다. 중요한 건 폴더 이름이 아니라 entry contract, state, evidence, gate가 분리됐는가입니다.

언제 내부 패키지로 승격할까

아래 조건 중 2개 이상이 생기면 폴더가 아니라 공용 패키지로 올리는 편이 낫습니다.

  • browser, API, workflow probe가 같은 lifecycle을 공유해야 한다.
  • 여러 앱이나 저장소가 같은 seed/reset, evidence schema, gate vocabulary를 써야 한다.
  • release gate와 agent repair loop가 같은 판정 규칙을 참조해야 한다.
  • runner 구현보다 contract, evidence, policy의 안정성이 더 중요해졌다.

이 시점부터는 testops/ 아래 파일 묶음보다 packages/test-harness-*처럼 책임 단위가 분리된 구조가 유지보수에 유리합니다.

모노레포 패키지 구조 예시

아래는 agentic test environment 팀이 실제로 많이 쓰게 되는 분리 방식입니다.

src/contracts.ts
src/lifecycle.ts
src/gate-vocabulary.ts
src/prepare-state.ts
src/reset-state.ts
src/fingerprint.ts
src/browser-runner.ts
src/session.ts
src/locators.ts
src/evidence-bundle.ts
src/artifact-store.ts
src/classification-hints.ts
src/gate-policy.ts
src/quarantine-policy.ts
src/repair-boundary.ts
src/targeted-rerun.ts
src/change-scope.ts

이 구조의 포인트는 "패키지 수를 늘리자"가 아니라, 무엇이 재사용 자산이고 무엇이 probe별 구현인지 드러내자는 데 있습니다.

패키지별 책임 분리

패키지맡는 책임대표 export두면 안 되는 것
@org/test-harness-core하네스 공용 타입, lifecycle, gate vocabularyEntryContract, ProbeResult, GateDecisionPlaywright import, DB reset 구현
@org/test-harness-stateseed/reset, queue drain, clock freeze, env fingerprintStateController, EnvFingerprint제품별 assertion helper
@org/test-harness-browserbrowser actuator wrapper, session bootstrapping, stable page objectBrowserRunner, createSessiongate 판정 로직
@org/test-harness-evidencetrace/log/screenshot/event를 evidence bundle로 정규화EvidenceBundle, EvidenceCollectorrerun policy, human approval 정책
@org/test-harness-policyretry budget, quarantine, release hold, repair boundaryGatePolicy, RepairBoundary브라우저 selector, API client
@org/test-harness-agenttargeted rerun, safe edit scope, verification callbackrerunFailedProbe, allowedChangeSet제품 코드 직접 수정 규칙의 예외 승인

이렇게 나누면 core가 가장 안정적인 계약 계층이 되고, browser나 state 같은 구현 세부는 자주 바뀌어도 위 계층을 흔들지 않습니다.

하네스 실행 루프

핵심은 probe 자체가 아니라, entry -> state -> run -> evidence -> classify -> repair/human gate를 끊기지 않게 잇는 구조입니다.

핵심 인터페이스 1: Entry Contract

테스트 하네스의 시작점은 "어떤 계정으로 로그인했는가"보다 넓어야 합니다. gate tier, seed profile, feature flag, 외부 sandbox, 시간 동결 여부까지 들어가야 같은 실행을 다른 세션에서 복원할 수 있습니다.

export type ProbeKind = 'browser' | 'api' | 'workflow' | 'agent-eval'
export type GateTier = 'pr' | 'main' | 'release' | 'nightly'

export type EntryContract = {
  runId: string
  suite: string
  probe: ProbeKind
  gate: GateTier
  baseUrl: string
  seedProfile: 'smoke' | 'critical' | 'billing'
  users: Array<{
    alias: string
    role: 'admin' | 'member' | 'readonly'
  }>
  flags: Record<string, boolean>
  clock?: {
    frozenAt: string
  }
  externalSandbox: {
    payments: 'sandbox'
    email: 'stub'
    webhookReplay: boolean
  }
}

이 타입은 보통 @org/test-harness-core에 두고, 실제 문서는 YAML이나 JSON으로 저장하되 실행 직전에 이 타입으로 정규화합니다.

핵심 인터페이스 2: State Controller

flake는 상당수가 러너가 아니라 상태 제어 실패에서 나옵니다. 그래서 state-controller.ts는 보조 도구가 아니라 하네스 중심 패키지여야 합니다.

export type EnvFingerprint = {
  appRevision: string
  schemaRevision: string
  seedRevision: string
  queueDepth: Record<string, number>
  featureFlags: Record<string, boolean>
  clock?: string
}

export interface StateController {
  prepare(contract: EntryContract): Promise<EnvFingerprint>
  reset(scope: 'suite' | 'scenario'): Promise<void>
  drainQueue(timeoutMs: number): Promise<void>
  fingerprint(): Promise<EnvFingerprint>
}

좋은 StateController는 단순히 DB를 지우는 함수가 아닙니다. prepare -> reset -> fingerprint로 "왜 이 실행이 저번 실행과 달랐는가"를 설명할 수 있어야 합니다.

핵심 인터페이스 3: Evidence Bundle과 Gate Input

하네스가 없을 때 가장 흔한 실패는 artifact는 많은데 정작 판정할 증거가 없는 상태입니다. trace, screenshot, console log를 그냥 저장하지 말고 같은 vocabulary로 묶어야 합니다.

export type FailureClass = 'product' | 'data' | 'infra' | 'external' | 'unknown'

export type EvidenceBundle = {
  runId: string
  probeId: string
  fingerprint: EnvFingerprint
  artifacts: {
    traceUrl?: string
    screenshots: string[]
    logs: string[]
    events: string[]
  }
  summary: {
    durationMs: number
    failedStep?: string
    failureClass: FailureClass
    hints: string[]
  }
}

export type GateDecision =
  | { action: 'pass' }
  | { action: 'rerun'; reason: string }
  | { action: 'quarantine'; reason: string }
  | { action: 'hold'; approverGroup: 'release-manager' | 'suite-owner' }

여기서 중요한 건 EvidenceBundle이 저장 포맷이면서 동시에 판정 입력이라는 점입니다. 릴리즈 게이트, triage 봇, agent repair loop가 모두 같은 구조를 읽을 수 있어야 합니다.

브라우저 하네스 조립 예시

아래 예시는 @org/test-harness-browser가 @org/test-harness-core, @org/test-harness-state, @org/test-harness-evidence, @org/test-harness-policy 를 조립하는 방식입니다.

type BrowserRunDeps = {
  contract: EntryContract
  state: StateController
  browser: {
    execute(input: {
      contract: EntryContract
      fingerprint: EnvFingerprint
    }): Promise<{
      ok: boolean
      probeId: string
      failedStep?: string
      startedAt: string
      finishedAt: string
    }>
  }
  evidence: {
    collect(input: {
      contract: EntryContract
      fingerprint: EnvFingerprint
      result: {
        ok: boolean
        probeId: string
        failedStep?: string
        startedAt: string
        finishedAt: string
      }
    }): Promise<EvidenceBundle>
  }
  gate: {
    evaluate(bundle: EvidenceBundle): GateDecision
  }
}

export async function runBrowserHarness(deps: BrowserRunDeps) {
  const prepared = await deps.state.prepare(deps.contract)

  const result = await deps.browser.execute({
    contract: deps.contract,
    fingerprint: prepared,
  })

  const bundle = await deps.evidence.collect({
    contract: deps.contract,
    fingerprint: prepared,
    result,
  })

  const decision = deps.gate.evaluate(bundle)

  if (decision.action === 'rerun') {
    await deps.state.reset('scenario')
  }

  return {
    bundle,
    decision,
  }
}

이 예시의 의도는 간단합니다.

  • browser 패키지는 실행을 담당하지만 판정하지 않습니다.
  • state 패키지는 환경을 준비하지만 assertion을 모르고 있어야 합니다.
  • evidence 패키지는 artifact를 모으지만 rerun 의미를 결정하지 않습니다.
  • policy 패키지는 판정하지만 Playwright locator나 API payload를 직접 다루지 않습니다.

이 분리가 깨지면 테스트 하네스는 금방 "큰 util 폴더"가 됩니다.

어떤 하네스가 load-bearing인가

좋은 테스트 하네스는 문서가 많거나 스크립트가 많은 하네스가 아닙니다. 없어지는 순간 실패 반경이 커지는 요소만 남긴 하네스입니다.

항목load-bearing인 경우ritual인 경우
seed/reset wrapper환경 차이와 제품 회귀를 실제로 분리해줌결국 사람이 DB를 수동으로 맞춤
evidence schematriage와 postmortem이 같은 vocabulary를 씀artifact는 많지만 해석 규칙이 없음
repair boundaryagent가 safe scope 안에서만 재실행됨agent가 임의로 gate 의미를 바꿈
gate policyblocker와 예외 승인 기준이 문서와 실행에 일치release 때마다 구두 합의로 바뀜
runner abstractionbrowser/API/workflow probe가 같은 lifecycle을 따름runner마다 설정과 판단 기준이 따로 놈

패키지 경계에서 자주 터지는 실수

안티패턴왜 문제인가바로잡는 기준
core 패키지가 Playwright나 HTTP client를 직접 import가장 안정적이어야 할 계약 계층이 실행 도구 변경에 흔들림core는 타입·vocabulary·lifecycle만 둠
browser 패키지 안에서 flake 분류까지 수행실행과 판정이 섞여 같은 실패를 gate마다 다르게 해석하게 됨분류와 hold/quarantine는 policy로 이동
공용 fixture가 seed/reset까지 몰래 처리상태 변화가 숨겨져 evidence로 재현 불가상태 제어는 state controller를 통해서만 수행
evidence schema에 버전 정보가 없음CI, triage 봇, postmortem 문서가 서로 다른 필드를 기대하게 됨bundle schema와 fingerprint field를 버전 관리
agent가 gate policy 파일까지 수정 가능self-approve 경로가 생겨 테스트 하네스가 통제력을 잃음repair boundary에서 수정 가능 파일을 명시

probe별 하네스 차이

probe하네스에서 특히 중요한 것
browserauth/session, stable locator contract, trace/video, prod-safe 계정
APIidempotent setup, schema contract, response snapshot, tenant isolation
workflowqueue drain, event capture, time control, job replay
agent evalmodel version, prompt/input freeze, tool permission boundary, score rubric

테스트 하네스 엔지니어링은 브라우저 러너 패턴 하나를 확장하는 게 아니라 probe 종류마다 다른 비결정성을 흡수하는 기술입니다.

agent repair loop와 하네스

agent를 테스트 수정 루프에 넣으려면 하네스가 먼저 아래를 제공해야 합니다.

  1. 실패 관련 subset만 재실행할 수 있는 targeted rerun
  2. 환경 fingerprint와 evidence bundle 재수집
  3. 고위험 변경 시 human gate 분리
  4. agent가 바꿔도 되는 파일/설정 경계 명시

예를 들어 @org/test-harness-agent는 아래 같은 정책을 읽어야 합니다.

allowedPaths:
  - apps/web/tests/**
  - packages/test-harness-browser/**
  - packages/test-harness-evidence/**
blockedPaths:
  - packages/test-harness-policy/**
  - infra/production/**
requireHumanApprovalWhen:
  - gate == "release"
  - failureClass == "unknown"
  - changedFiles match "packages/test-harness-state/**"

이게 없으면 agent는 "테스트를 고치는 도구"가 아니라 증거를 덮어버리는 불안정한 자동화가 됩니다.

리뷰 체크리스트

  • @org/test-harness-core가 runner 구현 없이도 독립 설명 가능한가?
  • @org/test-harness-state가 seed/reset/clock/queue/fingerprint를 한 vocabulary로 제공하는가?
  • @org/test-harness-evidence 산출물만으로 triage, release hold, postmortem이 가능한가?
  • @org/test-harness-policy가 CI YAML 바깥에서도 재사용 가능한 판정 엔진으로 존재하는가?
  • @org/test-harness-agent가 rerun 권한과 file change scope를 실제 정책으로 강제하는가?
  • 같은 테스트를 다른 세션에서 다시 실행해도 entry contract가 복원되는가?
  • state controller가 clock, queue, sandbox까지 설명할 수 있는가?
  • evidence bundle만으로 triage와 release 판단이 가능한가?
  • repair boundary가 문서가 아니라 실제 실행 정책으로 연결돼 있는가?
  • browser/API/workflow probe가 같은 gate vocabulary를 공유하는가?

관련 핸드북

  • 하네스 일반론은 하네스 엔지니어링을 같이 보면 좋습니다.
  • 이 책에서는 그 원리를 테스트 환경과 release gate 문맥으로 내려서 다룹니다.

관련 문서

거버넌스와 소유권

DevEx, SRE, 제품팀, QA 플랫폼, AI 플랫폼 사이의 owner와 승인 경계를 정하는 기준

실행 패브릭과 오케스트레이션

PR, main, nightly, release, canary, agent-repair 루프를 어떤 시간 예산과 증거 정책으로 운영할지 설명합니다.

하네스 엔지니어링의 기초

하네스 엔지니어링 · 하네스 엔지니어링의 정의, 범위, 그리고 왜 프롬프트보다 시스템 설계가 중요한지 설명합니다.

하네스의 기술 메커니즘

하네스 엔지니어링 · 왜 하네스가 감각이 아니라 엔지니어링 대상인지, 입력·상태·검증·권한 경계 관점에서 설명합니다.

하네스 엔지니어링

OpenAI·Anthropic·Toss·gstack·revfactory 사례와 Agents SDK·Managed Agents 흐름으로 풀어낸 하네스 설계·검증·운영 가이드

실행 토폴로지와 러너 아키텍처

브라우저 러너, 서비스 클라이언트, 증거 수집기, 판정기를 어떤 구조로 분리해야 하는지 설명합니다.

인터페이스 계약과 관측 포인트

locator, test id, API schema, domain event, log field를 안정적 테스트 계약으로 다루는 방법

On this page

테스트 하네스는 무엇을 맡는가로컬 폴더에서 시작하는 최소 하네스언제 내부 패키지로 승격할까모노레포 패키지 구조 예시패키지별 책임 분리하네스 실행 루프핵심 인터페이스 1: Entry Contract핵심 인터페이스 2: State Controller핵심 인터페이스 3: Evidence Bundle과 Gate Input브라우저 하네스 조립 예시어떤 하네스가 load-bearing인가패키지 경계에서 자주 터지는 실수probe별 하네스 차이agent repair loop와 하네스리뷰 체크리스트관련 핸드북