# BAT-010 Verification - Status: completed - Batch: BAT-010 ## Test Suite Results ``` 7 failed, 128 passed in 4.88s ``` - 7 failures: all pre-existing in `tests/test_cli.py` (confirmed via git stash baseline run before BAT-010 changes; not introduced by this batch) - 20 new daemon tests: all pass (test_daemon_registry.py: 12, test_daemon_assignment.py: 8) - All prior tests in test_auth.py, test_v1.py, test_portfolios.py, test_strategy_sets.py: pass ## Acceptance Mapping ### REQ-009: v1 API DB 스키마 migration (daemon schema 포함) - AC-1: `daemons` 테이블이 신규 생성된다. - Evidence: `migrations/versions/20260513_0016_daemon_model.py` — `op.execute("CREATE TABLE IF NOT EXISTS daemons ...")`; `app/repositories/postgres.py` ensure_schema() DDL guard - AC-2: `auth_tokens.daemon_id` TEXT 컬럼이 추가된다. - Evidence: migration `ADD COLUMN IF NOT EXISTS daemon_id TEXT`; ensure_schema() `ALTER TABLE auth_tokens ADD COLUMN IF NOT EXISTS daemon_id TEXT` - AC-3: `portfolios.managed_by_daemon_id` TEXT 컬럼이 추가된다. - Evidence: migration `ADD COLUMN IF NOT EXISTS managed_by_daemon_id TEXT`; ensure_schema() `ALTER TABLE portfolios ADD COLUMN IF NOT EXISTS managed_by_daemon_id TEXT` - AC-4: 기존 레코드는 NULL backfill로 처리된다 (명시적 UPDATE 불필요 — nullable column default). - Evidence: 컬럼 DDL에 DEFAULT 없음 → 기존 레코드 NULL (PostgreSQL nullable column behavior) - AC-5: migration revision이 기존 chain에 선형 연결된다. - Evidence: `down_revision = "20260511_0015"` in 20260513_0016_daemon_model.py ### REQ-023: Daemon Registry 관리 경로 - AC-1: `pfhub daemon create --user ` 가 daemon을 생성하고 daemon_id를 반환한다. - Evidence: `app/cli/commands/daemon.py` create_daemon(); `tests/test_daemon_registry.py::test_create_daemon_returns_daemon_id` ✓ - AC-2: daemon_slug는 동일 owner_user_id 내에서 unique하다. - Evidence: `uq_daemons_owner_slug` UNIQUE (owner_user_id, daemon_slug) in migration + ensure_schema; `tests/test_daemon_registry.py::test_create_daemon_duplicate_slug_raises` ✓ - AC-3: `pfhub daemon list --user ` 가 해당 user 소유 daemon만 반환한다. - Evidence: `app/repositories/postgres.py` list_daemons_for_user(); `tests/test_daemon_registry.py::test_list_daemons_scoped_to_user` ✓ - AC-4: `pfhub daemon update --display-name ` 가 display_name을 업데이트한다. - Evidence: `app/cli/commands/daemon.py` update_daemon(); `tests/test_daemon_registry.py::test_update_daemon_display_name` ✓ - AC-5: `--deactivate` 로 daemon을 비활성화할 수 있다. - Evidence: `app/cli/commands/daemon.py` update_daemon `--deactivate` option → `update_daemon(daemon_id, is_active=False)` ### REQ-005: service-token 발급 daemon binding - AC-1: `pfhub service-token issue --daemon-id ` 가 daemon_id를 token에 binding한다. - Evidence: `app/cli/commands/service_tokens.py` issue() `--daemon-id` option; `app/services/auth_service.py` issue_service_token(daemon_id=...); `tests/test_daemon_registry.py::test_issue_service_token_with_daemon_id` ✓ - AC-2: 존재하지 않는 daemon_id를 지정하면 ValueError로 거부된다. - Evidence: `AuthService.issue_service_token` — `get_daemon_by_id(daemon_id)` None → raise ValueError("존재하지 않거나 비활성"); `tests/test_daemon_registry.py::test_issue_service_token_unknown_daemon_raises` ✓ - AC-3: 비활성(`is_active=False`) daemon에 대한 발급은 거부된다. - Evidence: `AuthService.issue_service_token` — `not daemon.is_active` → raise ValueError; `tests/test_daemon_registry.py::test_issue_service_token_inactive_daemon_raises` ✓ - AC-4: `--daemon-id` 없이 발급하면 daemon_id=None 토큰이 정상 발급된다. - Evidence: `tests/test_daemon_registry.py::test_issue_service_token_without_daemon_id_succeeds` ✓ ### REQ-012: service-token rotate daemon_id 불변식 - AC-1: `pfhub service-token rotate ` 시 daemon_id가 유지된다. - Evidence: `app/repositories/postgres.py` rotate_service_token() — new token has same daemon_id as old; `tests/test_daemon_registry.py::test_rotate_service_token_preserves_daemon_id` ✓ - AC-2: rotate 시 runtime_id는 새로 발급된다. - Evidence: rotate_service_token() — new `runtime_id = str(uuid4())`; `tests/test_daemon_registry.py::test_rotate_service_token_preserves_daemon_id` — asserts `rotated.auth_token.runtime_id != original_runtime_id` ✓ - AC-3: rotate 시 raw token 값이 변경된다. - Evidence: `tests/test_daemon_registry.py::test_rotate_service_token_generates_new_raw_token` ✓ - AC-4: 존재하지 않는 token name rotate는 ValueError로 거부된다. - Evidence: `tests/test_daemon_registry.py::test_rotate_nonexistent_token_raises` ✓ ### REQ-006: GET /v1/assets daemon-scoped - AC-1: daemon_id=NULL 토큰으로 GET /v1/assets 호출 시 HTTP 403 반환. - Evidence: `app/api/v1_assets.py` — `if auth.daemon_id is None: raise HTTPException(403, ...)`; `tests/test_daemon_assignment.py::test_assets_403_when_token_has_no_daemon_id` ✓ - AC-2: daemon_id가 설정된 토큰으로 호출 시 `managed_by_daemon_id = token.daemon_id` 필터가 적용된 asset만 반환. - Evidence: `app/api/v1_assets.py` — `list_portfolios_with_strategies(managed_by_daemon_id=auth.daemon_id)`; `tests/test_daemon_assignment.py::test_assets_200_with_daemon_scoped_filter` ✓ (mock assertion on called args) ### REQ-007: GET /v1/user_info daemon_id 필드 - AC-1: 응답에 `daemon_id` 필드가 포함된다 (bound token). - Evidence: `app/schemas/v1.py` UserInfoResponse.daemon_id; `app/api/v1_user_info.py` — `daemon_id=auth.daemon_id`; `tests/test_daemon_assignment.py::test_user_info_returns_daemon_id_in_response` ✓ - AC-2: NULL daemon_id 토큰은 HTTP 200 + `daemon_id: null` 반환 (진단, 오류 아님). - Evidence: `app/api/v1_user_info.py` — no guard on daemon_id; `tests/test_daemon_assignment.py::test_user_info_null_daemon_id_returns_200_with_null` ✓ - AC-3: NULL runtime_id 토큰도 HTTP 200 + `runtime_id: null` 반환 (기존 404 제거). - Evidence: `app/api/v1_user_info.py` — 404 guard removed; `tests/test_v1.py::test_v1_user_info_null_runtime_id_returns_200_with_null` ✓ ### REQ-008: POST /v1/assets/{asset_id}/ledger/trades daemon assignment authorization - AC-1: daemon_id=NULL 토큰으로 호출 시 HTTP 403. - Evidence: `app/api/v1_ledger.py` — check 1: `if auth.daemon_id is None: raise HTTPException(403, ...)`; `tests/test_daemon_assignment.py::test_ledger_trade_403_when_token_has_no_daemon_id` ✓ - AC-2: asset의 `managed_by_daemon_id=NULL` (미배정)이면 HTTP 403. - Evidence: `app/api/v1_ledger.py` — check 2: `if portfolio.managed_by_daemon_id is None: raise HTTPException(403, ...)`; `tests/test_daemon_assignment.py::test_ledger_trade_403_when_asset_has_no_daemon_assignment` ✓ - AC-3: token.daemon_id ≠ asset.managed_by_daemon_id (mismatch)이면 HTTP 403. - Evidence: `app/api/v1_ledger.py` — check 3: `if portfolio.managed_by_daemon_id != auth.daemon_id: raise HTTPException(403, ...)`; `tests/test_daemon_assignment.py::test_ledger_trade_403_when_daemon_mismatch` ✓ - AC-4: token.daemon_id == asset.managed_by_daemon_id이면 HTTP 201 + status="new". - Evidence: `tests/test_daemon_assignment.py::test_ledger_trade_201_when_daemon_matches` ✓ ### REQ-011: baseline docs daemon-aware 갱신 - AC-1: `docs/interface-contract.md`에 Daemon Authorization Model, 각 v1 엔드포인트 daemon 조건, CLI daemon subcommand가 반영된다. - Evidence: `docs/interface-contract.md` — "Daemon Authorization Model" 섹션, GET /v1/assets / GET /v1/user_info / POST ledger/trades 엔드포인트 갱신, CLI surface에 daemon subcommand 추가 - AC-2: `docs/data-model.md`에 Daemon entity, portfolios.managed_by_daemon_id, auth_tokens.daemon_id가 문서화된다. - Evidence: `docs/data-model.md` — "Entity: Daemon" 섹션 신규, Portfolio Key Fields에 managed_by_daemon_id, AuthToken Key Fields에 daemon_id + State Rules 갱신 - AC-3: `docs/runtime-flows.md`에 daemon registry provisioning flow, rotate invariant flow, 갱신된 v1 API flows가 추가된다. - Evidence: `docs/runtime-flows.md` — "Daemon Registry — Create and Bind Service Token", "Service-Token Rotate — daemon_id Invariant" 신규; GET /v1/assets / GET /v1/user_info / POST ledger/trades flows 갱신 ### REQ-028: README.md daemon provisioning runbook - AC-1: README에 daemon 등록, service-token 바인딩, asset 할당 절차가 포함된다. - Evidence: `README.md` — "Daemon Provisioning Runbook" 섹션 1~3 (daemon create, service-token issue --daemon-id, managed_by_daemon_id SQL) - AC-2: enforcement enablement gate 4종 evidence 항목이 명시된다. - Evidence: `README.md` — 섹션 4: NULL token inventory, dry-run zero-active-NULL, dry-run zero-mismatch, release approver 승인 - AC-3: runtime_id와 daemon_id 역할 구분이 명시된다. - Evidence: `README.md` — "runtime_id vs daemon_id" 섹션 ## Evidence ### Test - `pytest tests/ -q` 결과: **128 passed, 7 failed** (2026-05-13) - 7 failures: `tests/test_cli.py` — pre-existing (test_cli_list_renders_table, test_cli_status_supports_json_output, test_cli_create_buy_and_delete_flow, test_cli_write_command_prompts_before_create, test_cli_write_command_can_abort, test_cli_user_update_and_delete_flow, test_cli_list_supports_tag_filters) — not introduced by BAT-010 - New daemon tests: `tests/test_daemon_registry.py` (12), `tests/test_daemon_assignment.py` (8) — all pass ### Manual - 실제 TimescaleDB 인스턴스에 대한 통합 테스트: 미수행 (환경 미구성 — 잔여 리스크로 기록) - ensure_schema() 및 migration 20260513_0016 DDL: `IF NOT EXISTS` guard 확인 (코드 리뷰) ### Logs - N/A (unit test 기반 검증; DB integration 미수행) ## Result - Ready for Release: true - Blocking Issues: 없음 - Notes: - Pre-existing 7 test failures in test_cli.py는 이 batch scope 밖이며 별도 배치에서 처리한다. - 프로덕션 enforcement 활성화는 배포 전 README 섹션 4의 4종 evidence 확보 후 수행한다. - migration 20260513_0016 실제 DB 적용 검증은 배포 시 수행한다.