docs(country-fixture): 국가 강제 지정 테스트 격리와 검증 결과를 기록한다
This commit is contained in:
@@ -0,0 +1,110 @@
|
|||||||
|
# 국가 강제 지정 테스트 fixture 격리 구현 계획
|
||||||
|
|
||||||
|
> 실행: subagent-driven-development. 기존 승인 범위를 구현하며 추가 인터뷰 없이 진행한다.
|
||||||
|
|
||||||
|
## 현재 상태와 목표
|
||||||
|
- 작성일: 2026-11-06
|
||||||
|
- 상태: 구현 완료 (기능 범위 검증 완료, 전체 테스트는 사용자 실행)
|
||||||
|
- 요구사항: [prd.md](prd.md)
|
||||||
|
- 활성 Goal: 없음 (P1-T1, P1-GATE 완료)
|
||||||
|
- 목표: 운영 국가 ID 추가 시 일반 회원 fixture의 목록을 수정하지 않아도 되고, 기존 독립된 정책 검증은 유지된다.
|
||||||
|
- 기술: Kotlin 1.6.21, JDK 17, JUnit 5, Spring Boot 2.7.14, Gradle Wrapper.
|
||||||
|
- 시작 조건: 사용자 수정 승인, 관련 테스트/가이드 읽기 완료.
|
||||||
|
- 범위 밖: production, API, DB schema, dependency, 사용자 기존 변경, 전체 테스트 자동 실행, commit/push.
|
||||||
|
|
||||||
|
### Phase 1: 목록 복제 없는 fixture
|
||||||
|
|
||||||
|
- [x] **Task 1.1: 공통 fixture 판별과 세 호출부 전환 (P1-T1)**
|
||||||
|
|
||||||
|
**Objective:** 세 테스트가 강제 지정 ID 목록을 복사하지 않고 일반 회원을 준비한다.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `src/test/kotlin/kr/co/vividnext/sodalive/support/CountryMemberFixtures.kt`
|
||||||
|
- Create: `src/test/kotlin/kr/co/vividnext/sodalive/support/CountryMemberFixturesTest.kt`
|
||||||
|
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/member/contentpreference/MemberContentPreferenceIntegrationTest.kt`
|
||||||
|
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/HomeRecommendationControllerTest.kt`
|
||||||
|
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/content/recommendation/adapter/in/web/AudioRecommendationEndToEndTest.kt`
|
||||||
|
- Verify unchanged: `src/test/kotlin/kr/co/vividnext/sodalive/member/contentpreference/MemberContentPreferenceServiceTest.kt`
|
||||||
|
|
||||||
|
**Interface:** test-only `internal fun Member.hasForcedCountryMapping(): Boolean`. 기존 resolver에 KR/JP를 각각 전달해 요청 국가가 유지되는지 확인하며, 어느 한 결과라도 다르면 true. 예상 응답 계산에는 사용 금지.
|
||||||
|
|
||||||
|
- [x] **Baseline:** 기존 service/integration 테스트와 아래 두 HTTP 메서드가 수정 전 통과하는지 확인한다.
|
||||||
|
- [x] **RED:** helper와 리터럴 기대값의 parameterized test를 작성한다. 기존 통합 테스트의 복제 목록 동작을 helper로 옮긴 상태에서 17958이 제외되지 않는 실패를 재현한다.
|
||||||
|
- [x] **RED 확인:** `CountryMemberFixturesTest`에서 17958의 expected true / actual false를 확인한다.
|
||||||
|
- [x] **GREEN:** helper를 resolver 기반 판별로 바꾸고 16/17958/29721은 true, 100은 false인지 확인한다.
|
||||||
|
- [x] **GREEN 확인:** helper focused test 통과.
|
||||||
|
- [x] **REFACTOR:** 세 호출부 조건을 helper로 교체하고 목록 복사본을 제거한다. 기존 저장 루프와 정책/응답 assertions는 보존한다.
|
||||||
|
|
||||||
|
**완료 증거:** 다음 세 시나리오 PASS 및 아래 명령 결과 기록.
|
||||||
|
|
||||||
|
| 시나리오 | PASS 조건 | 실제 검증 표면 |
|
||||||
|
|---|---|---|
|
||||||
|
| 일반 회원 | 100은 강제 지정 아님, 기존 US/JP/기본값 처리 보존 | helper JUnit + H2 integration |
|
||||||
|
| 강제 지정/누락 경계 | 16/17958/29721 제외, 헤더보다 강제 지정 우선 | helper JUnit + 기존 service policy assertions |
|
||||||
|
| 인접 HTTP 회귀 | 홈/오디오의 기존 8개 국가·인증 조합별 상태/스키마/이벤트 ID assertion 통과 | 기존 Spring MockMvc 요청과 실제 H2 persistence |
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gradlew test --tests 'kr.co.vividnext.sodalive.support.CountryMemberFixturesTest' --console=plain
|
||||||
|
./gradlew test --tests 'kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreferenceServiceTest' --tests 'kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreferenceIntegrationTest' --console=plain
|
||||||
|
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.home.HomeRecommendationControllerTest.shouldApplyEventBannerDetailPolicyForHomeAggregate' --tests 'kr.co.vividnext.sodalive.v2.api.content.recommendation.adapter.in.web.AudioRecommendationEndToEndTest.shouldApplyEventBannerDetailPolicyAndKeepAudioResponseSchema' --console=plain
|
||||||
|
```
|
||||||
|
|
||||||
|
**P1-T1 실행 결과 (2026-11-06):**
|
||||||
|
- 모든 실제 Gradle 실행은 `JAVA_HOME`을 Liberica 17.0.15로 지정하고 `--no-daemon --console=plain`을 사용했다. Baseline은 기존 두 클래스와 위 HTTP 두 메서드를 한 번에 선택해 74건 통과했다(service 36, integration 22, 홈 8, 오디오 8).
|
||||||
|
- RED/GREEN 명령은 `./gradlew --no-daemon test --tests 'kr.co.vividnext.sodalive.support.CountryMemberFixturesTest' --console=plain`이다. 기존 통합 테스트의 복제 목록 동작에서는 4건 중 17958만 expected true / actual false로 실패했고(exit 1), KR/JP resolver probe 전환 후 4건 모두 통과했다(exit 0).
|
||||||
|
- 최종 focused 회귀는 baseline의 네 `--tests` 선택자에 helper를 추가해 78건 통과했다(실패/오류/skip 0). 실제 H2 persistence와 홈/오디오 MockMvc의 기존 국가·인증별 상태/스키마/이벤트 ID assertion을 유지한 채 검증했다. 세 시나리오 모두 PASS다.
|
||||||
|
- `compileTestKotlin`이 변경 후 실제 실행되어 통과했다. `./gradlew --no-daemon ktlintTestSourceSetCheck -PinternalKtlintGitFilter=... --console=plain`은 변경한 다섯 Kotlin 파일을 줄바꿈으로 지정해 통과했다(빈 위반 보고서). LSP 도구가 없어 `lsp_diagnostics`는 미실행이다.
|
||||||
|
- production 전체 diff가 시작 시점과 같고 resolver/service test/PRD의 SHA-256도 보존됐다. 저장 재시도 루프와 기존 assertion은 변경하지 않았다. 작업 소유 Gradle/Kotlin/test JVM 및 Redis 잔류 프로세스는 없었다. 전체 테스트, build/check, 독립 리뷰, 임시 증거 삭제는 실행하지 않았고 P1-GATE는 변경하지 않았다. 전체 테스트는 사용자가 `./gradlew test`로 실행한다.
|
||||||
|
|
||||||
|
실제 전체 명령·결과·RED 상세: `/var/folders/yh/8xsbvpsj5wg2qnxzxdp11_gm0000gn/T/opencode/country-fixture.zPNREy/implementation-report.md`.
|
||||||
|
로그와 단계별 원본 JUnit XML: 같은 경로의 `implementation.igrAaj/{baseline,red,green,focused}-test-results/` 및 `*.log`. Baseline과 최종 회귀에서 Redis 종료 중 Lettuce 재연결 경고가 있었고, Gradle deprecation 경고도 보존했다. 해당 경고로 인한 테스트 실패는 없었다.
|
||||||
|
|
||||||
|
- [x] **Phase Gate (P1-GATE): 검증과 독립 리뷰**
|
||||||
|
- 시작 조건: P1-T1 검증 통과.
|
||||||
|
- 완료 증거: 변경 test Kotlin compile 및 scoped lint, diff 검사, 독립 리뷰 승인, 소유 임시 자원 정리.
|
||||||
|
- TDD 예외 사유: Gate는 읽기 전용 검증이다. 기존 테스트/XML과 코드 diff로 판정한다.
|
||||||
|
- LSP 도구는 현재 미등록이므로 실행 불가를 기록하고 Kotlin test compilation/scoped lint로 대체한다.
|
||||||
|
- 전체 테스트는 미실행이며 사용자 명령은 `./gradlew test`이다. 전체 성공으로 표시하지 않는다.
|
||||||
|
- 종료 조건: 세 시나리오와 focused tests 통과, 독립 reviewer 승인, 범위 밖 변경 없음, 검증 기록과 QA 자원 정리 완료 시 즉시 종료한다.
|
||||||
|
|
||||||
|
## 검증 기록
|
||||||
|
- 시작 시 production resolver에 사용자 수정 `45459L`이 있었다. 이번 작업은 해당 파일을 변경하지 않는다.
|
||||||
|
- 기본 macOS Java 검색은 실패했지만 `$JAVA_HOME/bin/java -version`은 Liberica 17.0.15로 성공했다.
|
||||||
|
|
||||||
|
### 부모 세션 재검증 (2026-11-06)
|
||||||
|
|
||||||
|
아래 명령으로 캐시된 test 결과를 사용하지 않고 같은 78건을 직접 재실행했다.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gradlew --no-daemon test --rerun \
|
||||||
|
--tests 'kr.co.vividnext.sodalive.support.CountryMemberFixturesTest' \
|
||||||
|
--tests 'kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreferenceServiceTest' \
|
||||||
|
--tests 'kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreferenceIntegrationTest' \
|
||||||
|
--tests 'kr.co.vividnext.sodalive.v2.api.home.HomeRecommendationControllerTest.shouldApplyEventBannerDetailPolicyForHomeAggregate' \
|
||||||
|
--tests 'kr.co.vividnext.sodalive.v2.api.content.recommendation.adapter.in.web.AudioRecommendationEndToEndTest.shouldApplyEventBannerDetailPolicyAndKeepAudioResponseSchema' \
|
||||||
|
--console=plain
|
||||||
|
```
|
||||||
|
|
||||||
|
- 결과: exit 0, `BUILD SUCCESSFUL in 2m 30s`, test task 실제 실행. 원본 `build/test-results/test/TEST-*.xml`의 4/36/22/8/8건 합계 78건, 실패/오류/skip 모두 0을 직접 확인했다.
|
||||||
|
- 홈/오디오 두 메서드의 MockMvc 요청과 H2 persistence가 실제 실행됐다. 운영 서버 호출이나 외부 DB 접근은 없다.
|
||||||
|
- 변경된 Kotlin 다섯 파일을 모두 읽고 diff를 대조했다. 기존 assertions와 저장 루프는 그대로이고 세 조건만 공통 helper를 사용한다.
|
||||||
|
- 운영 resolver SHA-256은 시작과 동일한 `5b562826c7cef1da40406c49c5105a534afb39a64fb915ea2a4ca9ac44dab9df`다.
|
||||||
|
- `git diff --check` 통과. 테스트 후 Gradle/test/Kotlin JVM과 Redis 프로세스 검색 결과 0건.
|
||||||
|
- scoped lint 재현 명령(다섯 변경 파일 전체 포함):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gradlew --no-daemon ktlintTestSourceSetCheck \
|
||||||
|
-PinternalKtlintGitFilter=$'src/test/kotlin/kr/co/vividnext/sodalive/support/CountryMemberFixtures.kt\nsrc/test/kotlin/kr/co/vividnext/sodalive/support/CountryMemberFixturesTest.kt\nsrc/test/kotlin/kr/co/vividnext/sodalive/member/contentpreference/MemberContentPreferenceIntegrationTest.kt\nsrc/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/HomeRecommendationControllerTest.kt\nsrc/test/kotlin/kr/co/vividnext/sodalive/v2/api/content/recommendation/adapter/in/web/AudioRecommendationEndToEndTest.kt' \
|
||||||
|
--console=plain
|
||||||
|
```
|
||||||
|
|
||||||
|
- 위 lint는 구현 세션에서 exit 0, `BUILD SUCCESSFUL in 1m 11s`; 원본 로그와 빈 위반 보고서를 부모가 확인했다.
|
||||||
|
- 기존 종료 시 Lettuce 재연결 경고와 Gradle deprecation 경고가 재현됐다. 테스트 실패는 없으며 이번 범위 밖 코드는 수정하지 않았다.
|
||||||
|
- 전체 테스트와 LSP는 미실행이다. 전체 테스트 사용자 실행 명령: `./gradlew test`.
|
||||||
|
|
||||||
|
### 독립 리뷰 (2026-11-06)
|
||||||
|
- [fixture 격리 리뷰](reviews/fixture격리-review.md): 명세 준수 및 코드 품질 승인. 차단 사항/수정 권고 없음.
|
||||||
|
- reviewer가 코드/diff와 원본 RED/GREEN/XML/lint 로그를 직접 대조했다. 부모의 fresh 78건 재실행 결과도 위에 기록했다.
|
||||||
|
- P1-T1에 적힌 임시 증거 경로는 실행 당시 기록이다. 소유 임시 파일 정리 후에는 위 재현 명령·결과와 리뷰 문서를 영구 기록으로 사용한다. 마지막 JUnit XML/HTML은 `build/test-results/test/`와 `build/reports/tests/test/`에 있으며 다음 Gradle 실행으로 갱신될 수 있다.
|
||||||
|
- QA 정리 완료: Gradle/test/Kotlin JVM 및 Redis 프로세스 0건을 확인한 뒤 이번 작업의 `country-fixture.zPNREy` 디렉터리(하위 implementation/review 포함)와 임시 notepad만 삭제했다. 두 경로의 부재를 `test ! -e`로 재확인했다. 공유 cache와 작업 트리는 변경하지 않았으며 commit/push는 하지 않았다.
|
||||||
|
- Gate 판정: 세 시나리오와 focused 78건, scoped lint, 독립 리뷰, 사용자 변경 보존 및 임시 자원 정리 완료. LSP와 전체 suite 미실행 범위는 위와 같다.
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# 국가 강제 지정 테스트 fixture 격리 PRD
|
||||||
|
|
||||||
|
## 문서 정보
|
||||||
|
- 상태: 구현 완료
|
||||||
|
- 작성일: 2026-11-06
|
||||||
|
- 결정권자: 사용자. 기존 리뷰의 권장 방향으로 수정 승인.
|
||||||
|
- 구현 계획: [plan-task.md](plan-task.md)
|
||||||
|
|
||||||
|
## 문제와 목표
|
||||||
|
일반 회원 fixture를 만드는 세 테스트가 운영 국가 강제 지정 ID 목록을 복사한다.
|
||||||
|
복사본 누락으로 일반 회원이 강제 지정 회원으로 선택될 수 있으며, 운영 목록 추가가 관련 없는 테스트 수정으로 이어진다.
|
||||||
|
목표는 세 fixture가 운영 ID를 중복 관리하지 않고 일반 회원을 선택하면서 독립된 정책/응답 기대값을 유지하는 것이다.
|
||||||
|
|
||||||
|
## 요구사항
|
||||||
|
| ID | 상태 | 요구사항 | 수용 기준 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| COUNTRY-FIXTURE-001 | 확정 | 운영 ID 목록 복제 제거 | 세 fixture가 공통 test-only 판별 함수를 사용하고 목록 복사본이 없다. |
|
||||||
|
| COUNTRY-FIXTURE-002 | 확정 | 일반 회원과 강제 지정 회원 구분 | 일반 회원 100은 허용하고 KR 16/17958 및 JP 29721은 제외한다. 17958은 기존 통합 테스트 복사본 누락 회귀 사례다. |
|
||||||
|
| COUNTRY-FIXTURE-003 | 확정 | 독립된 검증 유지 | 기존 강제 지정 우선순위, 정규화, 기본값 및 홈/오디오 응답 assertion을 변경하지 않는다. |
|
||||||
|
| COUNTRY-FIXTURE-004 | 확정 | 운영 정책 보존 | production 파일/API/DB/config 변경이 없고 사용자의 기존 JP 45459 추가를 보존한다. |
|
||||||
|
|
||||||
|
## 구현 결정과 범위 밖
|
||||||
|
- test-only `Member.hasForcedCountryMapping()`은 서로 다른 KR/JP 헤더에 대해 기존 resolver가 헤더를 그대로 반환하는지 판별한다. 어느 하나라도 다르면 일반 회원 후보에서 제외한다.
|
||||||
|
- 이 함수는 fixture 선택용이다. 국가 정책과 응답의 기대값 계산에는 사용하지 않는다.
|
||||||
|
- 기존 세 후보 생성 반복문과 persistence 경로는 유지한다. 신규 dependency, 설정, production 추상화는 만들지 않는다.
|
||||||
|
- 개별 운영 ID 전체를 테스트에 열거하지 않는다. 회귀 입력 ID는 대표 사례이지 동기화할 운영 목록이 아니다.
|
||||||
|
- 전체 테스트는 저장소 규칙에 따라 사용자가 실행한다. 이번 작업은 focused policy 및 직접 영향받는 HTTP 테스트를 실행한다.
|
||||||
|
|
||||||
|
## 완료 기준
|
||||||
|
- [x] 세 fixture의 운영 목록 복사본 제거.
|
||||||
|
- [x] 누락 ID 회귀 RED/GREEN과 일반/강제 회원 판별 검증.
|
||||||
|
- [x] 기존 policy/DB 및 홈/오디오 MockMvc 검증 통과.
|
||||||
|
- [x] production 보존, 독립 리뷰, 검증 한계 기록 및 소유 QA 자원 정리.
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# 국가 fixture 격리 리뷰
|
||||||
|
|
||||||
|
## 리뷰 정보
|
||||||
|
- 일자: 2026-11-06
|
||||||
|
- 범위: P1-T1의 test-only Kotlin 5개 파일과 기존 정책/HTTP 검증 보존.
|
||||||
|
- 기준: [PRD](../prd.md), [구현 계획](../plan-task.md), working tree diff.
|
||||||
|
- 리뷰어: 별도 read-only reviewer (`ses_e4f9ca68effegn7SKxWexCfjMl`).
|
||||||
|
- 판정: 명세 준수 승인, 코드 품질 승인. 확정 발견 사항 없음, 회귀 수정 Task 전환 항목 없음.
|
||||||
|
|
||||||
|
## 근거
|
||||||
|
- 세 호출부에서 운영 ID 복사본을 제거했다. 저장·재시도·index·flush/clear와 기존 assertion은 변경하지 않았다.
|
||||||
|
- test-only helper는 KR/JP 요청값 유지 여부로 강제 지정을 판별한다. 일반 회원 100, KR 16, JP 29721, 기존 복사본 누락 17958을 독립 리터럴 기대값으로 검증한다.
|
||||||
|
- helper 결과는 일반 회원 fixture 선택에만 쓰인다. 국가 우선순위·정규화·기본값 및 HTTP 응답의 기대값은 resolver에서 계산하지 않는다.
|
||||||
|
- production diff는 시작 시점과 동일한 사용자의 JP 45459 추가뿐이다. Resolver SHA-256 `5b562826c7cef1da40406c49c5105a534afb39a64fb915ea2a4ca9ac44dab9df` 일치를 직접 확인했다.
|
||||||
|
- 원본 XML/log에서 baseline 74 PASS, RED 4 중 17958만 expected true / actual false 실패, GREEN 4 PASS, 최종 78 PASS 및 scoped lint 5파일 PASS를 대조했다.
|
||||||
|
- 부모도 동일 78건을 `test --rerun`으로 재실행하여 PASS를 확인했다. 상세 명령과 결과는 구현 계획에 보존한다.
|
||||||
|
|
||||||
|
## 한계
|
||||||
|
- reviewer는 읽기 전용으로 검토했으며 테스트를 중복 실행하지 않았다.
|
||||||
|
- LSP 도구는 미등록이다. 실제 Kotlin test compilation과 scoped lint를 확인했다.
|
||||||
|
- 전체 suite는 미실행이다. 사용자 실행 명령은 `./gradlew test`.
|
||||||
Reference in New Issue
Block a user