feat: REQ-005 assets table tag apply/remove SQL alignment (BAT-013)
- REQ-005 CHG-20260521-02 follow-up: clarify AC-1/AC-5 to target assets table - Add append_asset_tag_id/remove_asset_tag_id on PostgresPortfolioRepository - Replace internal calls in apply_asset_tag/remove_asset_tag routes (HTTP contract unchanged) - 7 new regression tests: method existence, SQL target table, error branches, idempotency guard - Update existing AC-1/AC-5 assertions to verify new methods + portfolios methods uncalled - Documentation: runtime-flows.md flow body + change log; rel-001 includes BAT-013 - REQ-005 transition: Approved → Implemented (CHG-20260521-03) via BAT-013 verification - Total tests: 362 → 369 passed Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
# BAT-013 Design
|
||||
|
||||
- Status: confirmed
|
||||
- Batch: BAT-013
|
||||
|
||||
## Architecture Summary
|
||||
- 본 batch는 신규 구조나 모듈을 도입하지 않는다. 기존 `app/repositories/postgres.py`에서 portfolios 테이블 대상 tag_ids mutation 메서드(`append_portfolio_tag_id`/`remove_portfolio_tag_id`)를 미러링하여 **assets 테이블 대상 메서드**를 신규 추가하고, `app/api/v1_assets.py`의 `apply_asset_tag`/`remove_asset_tag` 라우트 내부 호출을 새 메서드로 교체한다.
|
||||
- HTTP contract, request/response schema, 라우트 시그니처, 정책 검증(404/403 분기), AuthContext 흐름은 전부 불변. 변경 표면은 두 라우트 내부의 repository 메서드 1줄씩 + 신규 repository 메서드 정의 + 테스트.
|
||||
- 본질적으로 dcy-002 FR-2의 원래 의도(admin이 assets에 태그 부여)를 실제 SQL 경로에 반영하는 **정합화** 작업이며, BAT-012에서 도입한 `assets.tag_ids` 컬럼이 처음으로 정상 채워지게 된다.
|
||||
|
||||
## Changed Areas
|
||||
- Docs:
|
||||
- `docs/runtime-flows.md` — Flow `v1 API — Tag Apply/Remove`의 L161, L165 `repo.append_portfolio_tag_id`/`repo.remove_portfolio_tag_id` → `repo.append_asset_tag_id`/`repo.remove_asset_tag_id`로 교체. Change Log 1줄 추가.
|
||||
- Modules:
|
||||
- `app/repositories/postgres.py` — `append_asset_tag_id(asset_id, tag_id)`, `remove_asset_tag_id(asset_id, tag_id)` 메서드 신규.
|
||||
- `app/api/v1_assets.py` — L175, L200 내부 호출 교체 (라우트 시그니처/응답 schema/error handling 불변).
|
||||
- Interfaces:
|
||||
- HTTP: 무변. `POST /v1/assets/{asset_id}/tags`, `DELETE /v1/assets/{asset_id}/tags/{tag_id}` request/response 동일.
|
||||
- Repository: `PostgresRepository`에 2개 메서드 신규. MemoryRepository 등 다른 구현체는 본 batch 범위 외 (실 사용 경로가 Postgres만 의존).
|
||||
- Tests:
|
||||
- `tests/test_asset_catalog.py` 또는 `tests/test_v1.py` — DB-integration fixture로 POST 후 `assets.tag_ids` mutation 확인 + DELETE 후 제거 확인 회귀 케이스 추가.
|
||||
|
||||
## Key Decisions
|
||||
1. **메서드 명명**: `append_asset_tag_id`/`remove_asset_tag_id` — 기존 `append_portfolio_tag_id`/`remove_portfolio_tag_id` 명명 패턴과 일관(테이블명_컬럼명_id). 분리된 메서드명을 채택하는 이유는 라우트 코드에서 호출 의도를 명확히 표현하기 위함 + 미래 정책 차이(예: assets에는 admin tag만, portfolios에는 owner tag 허용) 발생 시 두 경로의 분기를 안전하게 다룰 수 있게 하기 위함이다. 단일 `_append_tag_id(table, id, tag_id)` 헬퍼로 합치는 안은 채택하지 않는다 — 동적 테이블명 SQL은 가독성·정적 분석 모두 손해.
|
||||
2. **멱등성 가드**: `append_asset_tag_id`는 기존 `append_portfolio_tag_id`와 동일하게 `WHERE asset_id = %s AND NOT (%s::uuid = ANY(tag_ids))` 가드를 적용한다. 동일 tag_id 재적용은 no-op (HTTP 200 그대로 반환). AC-1은 "tag_ids 배열에 추가된다"만 요구하고 중복 정책을 명시하지 않으므로 기존 portfolios 측 동작과 평행하게 유지.
|
||||
3. **에러 분기**: rowcount=0인 경우 기존 portfolios 패턴과 동일하게 asset 존재 여부 추가 확인 후 미존재 시 `ValueError(f"asset을 찾을 수 없습니다: {asset_id}")` raise. 라우트 측은 이미 `repo.get(asset_id)`를 선행 호출해 404를 분기하므로 ValueError가 라우트까지 올라올 가능성은 사실상 race condition 한정. 그래도 가드는 유지(deletion 사이의 race로 빈 update가 발생할 경우 silent success로 빠지지 않도록).
|
||||
4. **MemoryRepository 보강 여부**: 본 batch에서는 보강하지 않는다. 실제 운영 경로는 Postgres만 사용하며, MemoryRepository에 `append_portfolio_tag_id` 정의가 있는지 확인 후 동일 위치에 신규 메서드 정의가 필요한지만 implementation 단계에서 점검. 만약 MemoryRepository에 portfolios 측 메서드가 없다면 assets 측도 추가하지 않아 일관성 유지.
|
||||
5. **테스트 위치**: `tests/test_asset_catalog.py`에 추가. BAT-012에서 동일 파일에 DB-integration 패턴(`test_get_v1_assets_*`)이 자리 잡았으므로 fixture·marker 재사용 최적. `tests/test_v1.py`는 기존에 portfolios.tag_ids 의존 fixture가 잔존할 가능성이 있어 신규 작성은 catalog 파일에 모은다.
|
||||
|
||||
## Edge Cases
|
||||
- **EC-1**: 존재하지 않는 `asset_id`로 POST → 라우트 선행 `repo.get(asset_id)` 단계에서 404 (기존 동작 그대로 회귀).
|
||||
- **EC-2**: 존재하지 않는 `tag_id`로 POST → 라우트 선행 `repo.get_tag_by_id` 단계에서 404 (기존 동작 회귀).
|
||||
- **EC-3**: `is_admin_tag=FALSE` 또는 `is_active=FALSE` → 403 (기존 동작 회귀).
|
||||
- **EC-4**: 동일 tag_id 중복 POST → 멱등 (no-op + HTTP 200) — Key Decision 2.
|
||||
- **EC-5**: 존재하지 않는 tag_id로 DELETE → 현재 `remove_portfolio_tag_id` 동작과 평행하게, asset이 존재하면 array_remove no-op (HTTP 200, status=removed)로 처리. 라우트 측 사전 검증(`repo.get(asset_id)`)이 asset 존재만 보장하고 tag 존재는 검증하지 않는 기존 의도 유지.
|
||||
- **EC-6**: 동일 UUID가 우연히 asset과 portfolio 양쪽에 존재 → 본 batch 이후 POST `/v1/assets/{id}/tags`는 assets만 mutation, portfolios는 무변동. 회귀 테스트로 명시 검증.
|
||||
- **EC-7**: `tag_ids` 컬럼 부재(스키마 마이그레이션 미적용 환경) → BAT-012 선행 가정. 본 batch는 컬럼 존재를 전제로 한다. (회귀 시 DB-integration fixture가 alembic upgrade head를 거치는지 implementation 단계에서 확인.)
|
||||
|
||||
## Architecture Impact
|
||||
- runtime-flows
|
||||
|
||||
## Reference Doc Update Plan
|
||||
- project-structure.md: none (파일·디렉토리·모듈 경계 무변).
|
||||
- runtime-flows.md: L161, L165의 `repo.append_portfolio_tag_id(asset_id, ...)`/`repo.remove_portfolio_tag_id(asset_id, ...)`를 `repo.append_asset_tag_id`/`repo.remove_asset_tag_id`로 교체. Change Log에 `2026-05-21 KST: BAT-013 (REQ-005 CHG-20260521-02) — /v1/assets/{id}/tags apply/remove SQL 대상 assets 테이블로 정합화. portfolios 테이블 호출은 portfolio 라우트 전용으로 한정.` 추가.
|
||||
- **별도 후속 항목 (out of scope, follow-up REQ 후보)**: `docs/runtime-flows.md` L67-77 (CLI Flow `pfhub tag apply asset` / `pfhub tag remove asset`)도 동일 `repo.append_portfolio_tag_id`/`repo.remove_portfolio_tag_id`를 호출하고 있어 동일한 mismatch가 CLI 레이어에 잔존. REQ-007/008 또는 신규 REQ로 처리. 본 batch는 REQ-005 HTTP API 범위에 한정.
|
||||
@@ -0,0 +1,48 @@
|
||||
# BAT-013 Implementation
|
||||
|
||||
- Status: completed
|
||||
- Batch: BAT-013
|
||||
|
||||
## Plan Summary
|
||||
- `app/repositories/postgres.py`에 `append_asset_tag_id`/`remove_asset_tag_id` 메서드 신규 (기존 portfolios 패턴 미러링, assets 테이블 대상).
|
||||
- `app/api/v1_assets.py` `apply_asset_tag`(L175) / `remove_asset_tag`(L200) 내부 호출 교체.
|
||||
- `tests/test_asset_catalog.py`에 BAT-013 회귀 케이스 추가 (메서드 존재·SQL 대상 테이블·에러 분기·멱등 가드 검증).
|
||||
- `tests/test_tags_http_api.py` 기존 AC-1/AC-5 mock 단언을 신규 메서드로 갱신 + portfolios 메서드 무호출 단언 추가.
|
||||
- `docs/runtime-flows.md` Flow `v1 API — Tag Apply/Remove` L161/L165 갱신 + Change Log 1줄 추가.
|
||||
|
||||
## Changed Files
|
||||
- `app/repositories/postgres.py` — `append_asset_tag_id`/`remove_asset_tag_id` 메서드 신규 (총 30줄 추가). 기존 portfolios 메서드 무변동.
|
||||
- `app/api/v1_assets.py` — L175 / L200 호출 교체 (2줄). 라우트 시그니처·정책 검증·에러 분기 무변동.
|
||||
- `tests/test_asset_catalog.py` — BAT-013 회귀 7건 신규 (메서드 존재 2건 + SQL 대상 검증 2건 + 에러 분기 2건 + 멱등 가드 1건).
|
||||
- `tests/test_tags_http_api.py` — `_make_db_repo` 헬퍼에 `append_asset_tag_id`/`remove_asset_tag_id` MagicMock 보강; `test_apply_asset_tag_admin_success` / `test_remove_asset_tag_admin_success` 단언 교체 + portfolios 메서드 무호출 단언 추가.
|
||||
- `docs/runtime-flows.md` — Flow `v1 API — Tag Apply/Remove` L161 / L165 메서드명 교체; Change Log에 BAT-013 항목 1줄 추가.
|
||||
|
||||
## Execution Log
|
||||
1. Repository 신규 메서드 추가 (`postgres.py` L1755 직후, `rotate_service_token` 앞에 삽입).
|
||||
2. 라우트 호출 교체 (`apply_asset_tag` → `append_asset_tag_id`; `remove_asset_tag` → `remove_asset_tag_id`).
|
||||
3. 기존 BAT-004 mock 테스트(`test_tags_http_api.py:343, 437`) 단언 갱신 — `mock_repo.append_portfolio_tag_id.assert_called_once_with` → `mock_repo.append_asset_tag_id.assert_called_once_with`. portfolios 호출은 `assert_not_called()`로 명시.
|
||||
4. `_make_db_repo` 헬퍼에 신규 메서드 MagicMock 보강 (`return_value = None`).
|
||||
5. BAT-013 회귀 테스트 7건 추가 (`tests/test_asset_catalog.py` 말미). 클래스명은 `PostgresPortfolioRepository`(실제 모듈 클래스). `__new__`로 인스턴스 생성 후 `_connect`/`ensure_schema` 패치하여 cursor mock으로 SQL 문자열 캡처.
|
||||
6. `docs/runtime-flows.md` Flow 본문 L161/L165 갱신 + Change Log 추가.
|
||||
7. pytest 1차: `tests/test_asset_catalog.py tests/test_tags_http_api.py` — 처음에 `ImportError: cannot import name 'PostgresRepository'` 발견 → 실제 클래스명 `PostgresPortfolioRepository`로 정정 후 79 passed.
|
||||
8. pytest 전체: **369 passed in 11.48s** (BAT-012 직후 362 baseline 대비 +7 신규).
|
||||
|
||||
## Validation
|
||||
- 단위/회귀 검증:
|
||||
- `tests/test_asset_catalog.py::test_postgres_repository_has_append_asset_tag_id_method` ✅
|
||||
- `tests/test_asset_catalog.py::test_postgres_repository_has_remove_asset_tag_id_method` ✅
|
||||
- `tests/test_asset_catalog.py::test_append_asset_tag_id_targets_assets_table_not_portfolios` ✅
|
||||
- `tests/test_asset_catalog.py::test_remove_asset_tag_id_targets_assets_table_not_portfolios` ✅
|
||||
- `tests/test_asset_catalog.py::test_append_asset_tag_id_raises_when_asset_not_found` ✅
|
||||
- `tests/test_asset_catalog.py::test_remove_asset_tag_id_raises_when_asset_not_found` ✅
|
||||
- `tests/test_asset_catalog.py::test_append_asset_tag_id_idempotent_guard_present` ✅
|
||||
- `tests/test_tags_http_api.py::test_apply_asset_tag_admin_success` (갱신) — `append_asset_tag_id`로 호출 단언 + portfolios 미호출 ✅
|
||||
- `tests/test_tags_http_api.py::test_remove_asset_tag_admin_success` (갱신) — `remove_asset_tag_id`로 호출 단언 + portfolios 미호출 ✅
|
||||
- 기존 AC-2~4, AC-6~11 케이스 회귀 ✅ (정책 검증 분기 무영향 확인)
|
||||
- 전체 회귀: `pytest -q` → **369 passed** (BAT-012 직후 362 baseline 대비 +7 신규).
|
||||
- DB-integration 실 mutation 검증: 본 batch는 cursor mock 기반 SQL 문자열 검증으로 대체. 실 PostgreSQL 환경 검증은 REL 배포 시 smoke로 수행 예정.
|
||||
|
||||
## Remaining Risks
|
||||
- **R1 (Low, 잔존)**: `pfhub tag apply asset` / `pfhub tag remove asset` CLI도 `append_portfolio_tag_id`/`remove_portfolio_tag_id`를 호출함(`docs/runtime-flows.md` L67-77 참조). REQ-005 scope는 HTTP API 한정이라 본 batch는 손대지 않음. **후속 REQ 후보**로 별도 발의 필요 — Feedback Handoff 또는 confirm-batch-verification 결과에 명시.
|
||||
- **R2 (Low, 잔존)**: 실 PostgreSQL 환경에서 `assets.tag_ids` 실 mutation 검증은 REL 배포 시 smoke로 처리. unit-level SQL 문자열 검증으로 1차 안전망 확보.
|
||||
- **R3 (Resolved)**: 클래스명 오류(`PostgresRepository` vs `PostgresPortfolioRepository`)는 implementation 단계에서 즉시 정정.
|
||||
@@ -0,0 +1,35 @@
|
||||
# BAT-013: asset tag apply/remove SQL 정합화 (REQ-005)
|
||||
|
||||
- Status: release-candidate
|
||||
- Profile: batch-lite
|
||||
- Source Discovery: dcy-002 (REQ-005)
|
||||
- Included REQ: REQ-005
|
||||
- Excluded REQ: none
|
||||
|
||||
## Summary
|
||||
- REQ-005 CHG-20260521-02 후속. `POST /v1/assets/{asset_id}/tags`와 `DELETE /v1/assets/{asset_id}/tags/{tag_id}` 라우트가 내부적으로 `append_portfolio_tag_id`/`remove_portfolio_tag_id`를 호출해 **portfolios 테이블**을 mutation하는 불일치를 해소한다.
|
||||
- `app/repositories/postgres.py`에 `append_asset_tag_id`/`remove_asset_tag_id` 메서드를 신규 추가하고, `app/api/v1_assets.py`의 라우트 호출을 새 메서드로 교체한다.
|
||||
- BAT-012로 도입된 `assets.tag_ids` 컬럼이 비로소 정상 채워지도록 apply/remove 경로를 정합화한다.
|
||||
|
||||
## Scope
|
||||
- In Scope:
|
||||
- `app/repositories/postgres.py` — `append_asset_tag_id(asset_id, tag_id)`, `remove_asset_tag_id(asset_id, tag_id)` 메서드 신규 (assets 테이블 대상 `array_append`/`array_remove`)
|
||||
- `app/api/v1_assets.py` — `apply_asset_tag`(L175) / `remove_asset_tag`(L200) 내부 호출 교체 (HTTP contract·라우트 surface 불변)
|
||||
- `tests/test_asset_catalog.py` 또는 `tests/test_v1.py` — assets.tag_ids 실 mutation 회귀 케이스 추가; portfolios.tag_ids 무변동 확인
|
||||
- Out of Scope:
|
||||
- HTTP API surface 변경 (요청/응답 schema 동일)
|
||||
- portfolios 태그 apply/remove 경로(REQ-004) — 기존 동작 그대로 유지
|
||||
- BAT-004의 portfolio tagging 회귀 — 영향 없음
|
||||
- DB 마이그레이션 (assets.tag_ids 컬럼은 BAT-012/20260521_0023에서 이미 도입됨)
|
||||
|
||||
## Documents
|
||||
- Planning: [planning.md](./planning.md)
|
||||
- Design: [design.md](./design.md)
|
||||
- Implementation: [implementation.md](./implementation.md)
|
||||
- Verification: [verification.md](./verification.md)
|
||||
|
||||
## Notes
|
||||
- 선행 완료: BAT-012(assets.tag_ids 컬럼/GIN 인덱스 도입) ✅, BAT-004(라우트·정책 검증 시점의 기존 구현) ✅
|
||||
- profile 선정 근거: 단일 Approved REQ + HTTP contract 무변 + DB 스키마 무변 → minor-change 후보. 라우트 내부 SQL 대상 테이블만 교체하는 좁은 범위 변경이라 `batch-lite`로 출발.
|
||||
- design 생성 여부는 planning Design Gate에서 판단; 기존 `append_portfolio_tag_id` 패턴을 그대로 미러링하므로 design 생략 후보가 유력.
|
||||
- BAT-004 trace는 REQ-005 evidence 무효(`Implemented Invalidated: yes`, REQ-005 CHG-20260521-02 참조). 본 batch 완료 시 REQ-005 trace는 BAT-013으로 갱신; BAT-004의 REQ-005 trace는 superseded로 표기.
|
||||
@@ -0,0 +1,58 @@
|
||||
# BAT-013 Planning
|
||||
|
||||
- Status: confirmed
|
||||
- Batch: BAT-013
|
||||
- Profile: batch-lite
|
||||
|
||||
## Included REQ
|
||||
- REQ-005 (Interface, Approved) — 태그 적용·해제 HTTP API. CHG-20260521-02 augmentation으로 AC-1/AC-5의 "assets 테이블 tag_ids" 의미 명문화. BAT-013에서 실제 SQL 대상 테이블 교체.
|
||||
|
||||
## Delivery Plan
|
||||
1. **Repository 메서드 추가** (`app/repositories/postgres.py`)
|
||||
- `append_asset_tag_id(asset_id: str, tag_id: str) -> None`
|
||||
- 기존 `append_portfolio_tag_id` 패턴 미러링 (UPDATE assets SET tag_ids = array_append..., NOT (tag_id = ANY(tag_ids)) 가드, rowcount=0 시 존재 확인 후 ValueError).
|
||||
- `remove_asset_tag_id(asset_id: str, tag_id: str) -> None`
|
||||
- 기존 `remove_portfolio_tag_id` 패턴 미러링 (UPDATE assets SET tag_ids = array_remove..., rowcount=0 시 ValueError).
|
||||
2. **라우트 호출 교체** (`app/api/v1_assets.py`)
|
||||
- L175: `repo.append_portfolio_tag_id(asset_id, body.tag_id)` → `repo.append_asset_tag_id(asset_id, body.tag_id)`
|
||||
- L200: `repo.remove_portfolio_tag_id(asset_id, tag_id)` → `repo.remove_asset_tag_id(asset_id, tag_id)`
|
||||
3. **회귀 테스트 추가** (`tests/test_asset_catalog.py` 우선; 적합치 않으면 `tests/test_v1.py`)
|
||||
- AC-1 회귀: POST /v1/assets/{id}/tags 200 후 PostgresRepository.get(asset_id).tag_ids 배열에 tag_id 포함 확인.
|
||||
- AC-5 회귀: DELETE 후 동일 배열에서 tag_id 제거 확인.
|
||||
- 부수 확인: 동일 호출 후 portfolios.tag_ids는 무변동(또는 해당 portfolio_id가 없어 mutation 안 일어남).
|
||||
4. **BAT-004 기존 fixture 점검**
|
||||
- BAT-004 verification에서 사용한 AC-1/AC-5 케이스가 portfolios.tag_ids 의존 fixture에 묻혀 통과한 흔적이 있는지 확인.
|
||||
- 발견 시 assets 전용 fixture로 교체 또는 명확히 split.
|
||||
5. **회귀 suite 실행**
|
||||
- `pytest -q` 전체 → 362 baseline 유지 + 신규 회귀 통과 확인.
|
||||
|
||||
## Dependencies
|
||||
- Internal:
|
||||
- REQ-002 / BAT-012 (assets.tag_ids 컬럼 + GIN 인덱스) — 완료. 본 batch의 SQL 대상 컬럼.
|
||||
- REQ-001 / BAT-003 (tags 마스터 테이블) — 완료. 정책 검증(tag 존재/admin/active)에 사용.
|
||||
- External:
|
||||
- 없음.
|
||||
|
||||
## Design Gate
|
||||
- Design Required Before Implementation: **no**
|
||||
- Reason:
|
||||
- 변경은 기존 `append_portfolio_tag_id`/`remove_portfolio_tag_id` 패턴의 1:1 미러링이며, 새 구조나 설계 결정 없음.
|
||||
- HTTP contract·schema·라우트 시그니처 모두 불변.
|
||||
- DB 스키마 변경 없음 (assets.tag_ids는 BAT-012에서 도입 완료).
|
||||
- 정책 검증 로직(tag is_admin_tag/is_active 확인, 404/403 분기)은 기존 코드 그대로 유지.
|
||||
- implementation 진입 전 design 산출물 생성 시 잉여 작업이 됨.
|
||||
|
||||
## Milestones
|
||||
1. Repository 메서드 2종 추가 + 단위 동작 확인
|
||||
2. 라우트 호출 교체 (2 lines)
|
||||
3. 회귀 테스트 추가 (AC-1/AC-5, 최소 2~4 케이스)
|
||||
4. pytest 전체 통과 + BAT-004 잔존 fixture 점검 결과 기록
|
||||
5. `draft-batch-verification` → `confirm-batch-verification` → `confirm-req-implemented`
|
||||
|
||||
## Risks
|
||||
- **R1 (Low)**: BAT-004의 AC-1/AC-5 기존 테스트가 portfolios.tag_ids 검증으로 통과한 흔적이 남아 있어 본 batch 회귀 케이스와 의도 충돌.
|
||||
- **대응**: 기존 케이스 식별 후 assets 전용 회귀로 교체 또는 split. 의도가 명확하지 않으면 BAT-004 측 케이스 제거 후 본 batch 케이스로 일원화.
|
||||
- **R2 (Low)**: PostgresRepository가 없는 in-memory 환경에서 회귀 테스트 미작동.
|
||||
- **대응**: 기존 BAT-012 회귀(test_asset_catalog.py)가 사용하는 DB-integration fixture 패턴 재사용. DB-integration이 어렵다면 `MemoryRepository`에도 동일 메서드를 보강(필요 시).
|
||||
- **R3 (Low)**: 동일 호출자가 동일 tag_id를 중복 append 시 멱등성 가드 부재.
|
||||
- **대응**: 기존 `append_portfolio_tag_id`가 `NOT (... = ANY(tag_ids))` 가드를 가짐 → 동일 패턴 채택.
|
||||
@@ -0,0 +1,47 @@
|
||||
# BAT-013 Verification
|
||||
|
||||
- Status: confirmed
|
||||
- Batch: BAT-013
|
||||
|
||||
## Acceptance Mapping
|
||||
- REQ-005 (태그 적용·해제 HTTP API) — CHG-20260521-02 명문화 범위:
|
||||
- AC-1 (admin + admin 활성 tag → POST /v1/assets/{id}/tags 200, **assets 테이블 tag_ids에 추가**):
|
||||
- HTTP 200 + status="applied" → `tests/test_tags_http_api.py::test_apply_asset_tag_admin_success` (라우트 호출 단언 갱신: `mock_repo.append_asset_tag_id.assert_called_once_with(...)` + `mock_repo.append_portfolio_tag_id.assert_not_called()`).
|
||||
- SQL 대상 테이블 검증 → `tests/test_asset_catalog.py::test_append_asset_tag_id_targets_assets_table_not_portfolios` (`UPDATE assets` 포함, `UPDATE portfolios` 부재).
|
||||
- 메서드 존재 → `tests/test_asset_catalog.py::test_postgres_repository_has_append_asset_tag_id_method`.
|
||||
- 멱등 가드 → `tests/test_asset_catalog.py::test_append_asset_tag_id_idempotent_guard_present` (`NOT ... = ANY(tag_ids)`).
|
||||
- AC-2 (admin + is_admin_tag=FALSE → 403): `tests/test_tags_http_api.py::test_apply_asset_tag_non_admin_tag_returns_403` (BAT-004 기존 케이스, 정책 검증 라우트 분기 무영향 회귀).
|
||||
- AC-3 (admin + is_active=FALSE → 403): `tests/test_tags_http_api.py::test_apply_asset_tag_inactive_returns_403`.
|
||||
- AC-4 (non-admin → 403): `tests/test_tags_http_api.py::test_apply_asset_tag_non_admin_token_returns_403`.
|
||||
- AC-5 (admin → DELETE 200, **assets 테이블 tag_ids에서 제거**):
|
||||
- HTTP 200 + status="removed" → `tests/test_tags_http_api.py::test_remove_asset_tag_admin_success` (라우트 호출 단언 갱신: `mock_repo.remove_asset_tag_id.assert_called_once_with(...)` + `mock_repo.remove_portfolio_tag_id.assert_not_called()`).
|
||||
- SQL 대상 테이블 검증 → `tests/test_asset_catalog.py::test_remove_asset_tag_id_targets_assets_table_not_portfolios`.
|
||||
- 메서드 존재 → `tests/test_asset_catalog.py::test_postgres_repository_has_remove_asset_tag_id_method`.
|
||||
- asset 부재 시 ValueError → `tests/test_asset_catalog.py::test_remove_asset_tag_id_raises_when_asset_not_found` (라우트는 사전 `repo.get(asset_id)`로 404 분기).
|
||||
- AC-6~11 (portfolio 측 apply/remove + 404 분기): 본 batch 범위 외(portfolios 경로). BAT-004 기존 케이스 회귀 — `tests/test_tags_http_api.py::test_apply_portfolio_tag_owner_own_tag / test_apply_portfolio_tag_owner_admin_tag / test_apply_portfolio_tag_other_user_non_admin_tag_returns_403 / test_remove_portfolio_tag_owner_success / test_apply_portfolio_tag_nonexistent_tag_returns_404 / *_nonexistent_portfolio_*` PASS (assets 경로 변경이 portfolios 경로에 무영향임을 회귀 확인).
|
||||
- EC-1 (asset_id 부재 시 404): 라우트 사전 `repo.get(asset_id)` 분기 무변동 + `tests/test_asset_catalog.py::test_append_asset_tag_id_raises_when_asset_not_found` (repository 레이어 안전망).
|
||||
|
||||
## Evidence
|
||||
- Test:
|
||||
- `.venv/bin/python -m pytest tests/test_asset_catalog.py tests/test_tags_http_api.py -q` → **79 passed in 2.65s** (BAT-013 신규 7건 + BAT-004 기존 회귀 + BAT-012 기존 회귀 모두 통과).
|
||||
- `.venv/bin/python -m pytest -q` (전체) → **369 passed in 11.48s** (BAT-012 직후 362 baseline 대비 +7 신규, 회귀 0).
|
||||
- Manual:
|
||||
- 로컬 smoke 미실행 — TestClient 기반 mock + cursor-mock SQL 검증으로 1차 안전망 확보. 실 PostgreSQL `assets.tag_ids` mutation 검증은 REL 배포 시 smoke로 수행 예정.
|
||||
- Logs:
|
||||
- pytest 출력은 implementation.md Execution Log 항목 7~8 참조.
|
||||
|
||||
## Architecture Impact Sync
|
||||
- design.md `Architecture Impact: runtime-flows` 명시됨.
|
||||
- `docs/runtime-flows.md` 갱신 완료:
|
||||
- Flow `v1 API — Tag Apply/Remove` L161, L165의 `repo.append_portfolio_tag_id`/`repo.remove_portfolio_tag_id` → `repo.append_asset_tag_id`/`repo.remove_asset_tag_id`로 교체 + BAT-013 정합화 사유 inline 주석.
|
||||
- Change Log entry 추가 (2026-05-21 KST BAT-013).
|
||||
- `docs/project-structure.md`: Architecture Impact = none, 갱신 불요.
|
||||
|
||||
## Result
|
||||
- Ready for Release: true
|
||||
- Blocking Issues:
|
||||
- 없음 (본 batch 범위 내 AC 전수 evidence 확보)
|
||||
- Follow-up (release 또는 후속 REQ):
|
||||
- **CLI 동일 mismatch 잔존**: `pfhub tag apply asset` / `pfhub tag remove asset` CLI(`docs/runtime-flows.md` L67-77)도 `append_portfolio_tag_id`/`remove_portfolio_tag_id`를 호출함. REQ-005 scope는 HTTP API 한정이라 본 batch는 손대지 않음. **REQ-007/008 augmentation 또는 신규 REQ 후보**.
|
||||
- 실 PostgreSQL `assets.tag_ids` mutation 회귀: REL 배포 직후 smoke로 admin POST → GET /v1/assets에서 tag_ids 채워짐 확인.
|
||||
- BAT-004의 REQ-005 trace는 본 batch 완료 후 `confirm-req-implemented` 단계에서 `superseded by BAT-013`로 표기 권장.
|
||||
@@ -20,3 +20,4 @@
|
||||
| BAT-010 | standard | release-candidate | REQ-030, REQ-031, REQ-032 | dcy-003 | [bat-010](./bat-010_20260521_v1-portfolios-sub-resources/) |
|
||||
| BAT-011 | standard | release-candidate | REQ-029 | dcy-003 | [bat-011](./bat-011_20260521_v1-portfolios-ledger-path-migration/) |
|
||||
| BAT-012 | standard | release-candidate | REQ-002, REQ-010 | dcy-001, dcy-002, dcy-003 | [bat-012](./bat-012_20260521_assets-tagging-and-admin-get-redefinition/) |
|
||||
| BAT-013 | batch-lite | release-candidate | REQ-005 | dcy-002 | [bat-013](./bat-013_20260521_asset-tag-apply-remove-fix/) |
|
||||
|
||||
Reference in New Issue
Block a user