Files
portfolio-hub/docs/batches/bat-021_20260628_req047-step2-integration-and-runbook/verification.md
T
genichin 3e1b571906 feat(bat-021): REQ-047 Step2 동반 코드 수정 + 실 DB 통합검증 + 런북
- ensure_schema() broker_credentials: daemon_id NOT NULL, PK=(daemon_id,broker)로 갱신
- _row_to_broker_credential: Step2 컬럼 순서(daemon_id 선두)로 갱신
- upsert_broker_credential: ON CONFLICT (daemon_id,broker)로 수정, fallback try/except 제거
- list_broker_credentials: fallback 제거, SELECT 컬럼 순서 갱신
- test_bat020_broker_credential_domain.py: AC-3/AC-4 행 순서를 Step2 포맷으로 갱신
- tests/test_bat021_step2_ensure_schema_and_upsert.py: 12개 신규 테스트
- docs/runbooks/bat-021-req047-step2-migration-runbook.md: 운영 적용 런북
- docs/batches/bat-021: batch 문서(index/planning/design/implementation/verification)

실 PostgreSQL 통합검증(AC-1~7 전건 PASS):
  Step1 upgrade → backfill → Step2 upgrade → downgrade 시나리오 포함
  NULL 사전검증(upgrade 거부) 및 downgrade 안전장치(중복 시 RuntimeError) 확인

521 passed, 0 failed (기존 509 + 신규 12)
2026-06-28 10:19:27 +09:00

4.0 KiB

BAT-021 Verification

pytest 실행 결과

python3 -m pytest tests/ --tb=short
521 passed, 11 warnings in 26.67s

기존 테스트: 509 passed (BAT-020 기준) 신규 테스트: 12 added (test_bat021_step2_ensure_schema_and_upsert.py) 기존 테스트 회귀: 0건


REQ-047 AC 검증 (실 PostgreSQL)

검증 환경

  • Docker: portfolio-hub-timescaledb (timescale/timescaledb:latest-pg16)
  • DB: portfolio_hub (127.0.0.1:5432)
  • 시작 alembic 상태: 20260601_0024 (ensure_schema()로 스키마 구성, alembic 미동기 상태)
  • stamp 조치: alembic stamp 20260605_0025 (DB 실제 스키마 = 0025 적용 상태 반영)

AC-1: Step1 후 daemon_id TEXT NULL 컬럼 존재, PK (user_id,broker) 유지

alembic upgrade 20260628_0026

결과:

daemon_id  | text | nullable=YES
PK: broker_credentials_pkey PRIMARY KEY btree (user_id, broker)

PASS

AC-2: Step1 후 기존 데이터 daemon_id=NULL 보존

INSERT INTO broker_credentials ... VALUES ('test-user-1', 'kis', ...)
SELECT daemon_id FROM broker_credentials WHERE user_id='test-user-1';
-- 결과: NULL

PASS

AC-3: Step2 후 PK (daemon_id, broker) 전환, daemon_id NOT NULL

# backfill: UPDATE broker_credentials SET daemon_id='test-daemon-1' WHERE daemon_id IS NULL
alembic upgrade 20260628_0027

결과:

daemon_id  | text | nullable=NO
PK: broker_credentials_pkey PRIMARY KEY btree (daemon_id, broker)

PASS

AC-4: Step2 후 user_id 컬럼 유지

SELECT column_name FROM information_schema.columns
WHERE table_name='broker_credentials' AND column_name='user_id';
-- 결과: user_id 존재

PASS

AC-5: Step2 후 ensure_schema() 정합

repo.ensure_schema()  # 오류 없음
# DB 조회 결과:
# daemon_id: NOT NULL, PK=(daemon_id, broker), user_id 존재

PASS

AC-6: Step1 downgrade — daemon_id 컬럼 제거, PK (user_id,broker) 복원

alembic downgrade 20260605_0025

결과:

broker_credentials: daemon_id 컬럼 없음
PK: broker_credentials_pkey PRIMARY KEY btree (user_id, broker)

PASS

AC-7: Step2 downgrade 안전장치 — 중복 (user_id,broker) 존재 시 RuntimeError

# 2개 daemon이 같은 user/broker 사용하도록 데이터 구성 후 downgrade 시도
alembic downgrade 20260628_0026

결과:

RuntimeError: downgrade 불가: 1개 (user_id, broker) 조합이 중복됩니다.
같은 user의 여러 daemon이 동일 broker를 사용하고 있어
(user_id, broker) PK로 복원 시 데이터 손실이 발생합니다.
수동으로 데이터를 정리한 후 downgrade하세요.

PASS


동반 코드 검증

upsert_broker_credential (ON CONFLICT + row mapper)

cred = repo.upsert_broker_credential(
    'bat021-test-user', 'kis',
    account_ref='BAT021-ACCOUNT',
    encrypted_payload='bat021-encrypted-payload',
    daemon_id='bat021-daemon-1'
)
# 결과: daemon_id='bat021-daemon-1', user_id='bat021-test-user', broker='kis', account_ref='BAT021-ACCOUNT'
cred2 = repo.upsert_broker_credential(..., account_ref='BAT021-ACCOUNT-UPDATED')
# 결과: version=2, account_ref='BAT021-ACCOUNT-UPDATED' (idempotent update OK)

PASS

list_broker_credentials (Step2 컬럼 순서)

creds = repo.list_broker_credentials('bat021-test-user', daemon_id='bat021-daemon-1')
# 결과: 1건, daemon_id='bat021-daemon-1', broker='kis'

PASS


Step2 NULL 사전 검증 (REQ-048 미수행 시 거부)

# daemon_id IS NULL 행 1건 존재 상태에서 Step2 upgrade 시도
RuntimeError: broker_credentials에 daemon_id IS NULL인 행이 1건 있습니다.
REQ-048 backfill-daemon-id CLI로 모든 행을 백필한 후 다시 실행하세요.

PASS


미해결/위험 사항

없음 — 이 batch의 TODO 2건 모두 해소됨:

  1. ensure_schema() broker_credentials 갱신: 완료
  2. upsert ON CONFLICT (user_id,broker) → (daemon_id,broker) + fallback 제거: 완료

실 운영 DB 적용은 런북에 따라 사장님 결재 후 운영자가 수행한다.