EVD-334  기존 18개 리프의 test lane 재실행 (HEAD 21234e38)
측정일 2026-08-31 · workmachine

────────────────────────────────────────────────────────────────────────
1. 결과 요약
────────────────────────────────────────────────────────────────────────
모듈                                       classes  tests  fail  err  skip
adapter/outbound/persistence-jpa                94    477     0    0     0
adapter/outbound/persistence-mongo              14     72     0    0     0
adapter/outbound/cache-redis                    52    435     0    0     0
adapter/outbound/fileserver                     31    398     1    0     3
adapter/outbound/objectstorage                  47    140     0    0     0
adapter/outbound/notification                   54    327     0    0     1
adapter/inbound/web                            131    895     0    0     0
adapter/inbound/graphql                         92    793     0    0     0
adapter/inbound/websocket                       64    513     0    0     0
app-bootstrap                                  169   1016     1    0     4
adapter/outbound/httpclient                     61    283     3    0     0   (EVD-332)

1차 배치(별도 실행, 전부 통과): domain-core · shared-contract · application-core ·
adapter:outbound:support · identifier · messaging · adapter:inbound:grpc
    BUILD SUCCESSFUL in 11s

합계 실패 5건 = httpclient 3 + fileserver 1 + app-bootstrap 1.
아래 셋 모두 프로덕션 결함이 아니라 실행 환경 결손이다. 각각 원인을 측정으로 확정했다.

────────────────────────────────────────────────────────────────────────
2. fileserver 1건 — JVM 경로 인코딩
────────────────────────────────────────────────────────────────────────
LocalPersistentPayloadOperationsTest
  > inspectsLegacyRootArtifactByBoundedNoFollowStreamingWithoutRewritingIt()

java.nio.file.InvalidPathException: Malformed input or input contains unmappable characters:
    월간 export -- legacy 01.csv
  at sun.nio.fs.UnixPath.encode(UnixPath.java:129)
  at LocalPersistentPayloadOperationsTest.java:331

컨테이너 기본 환경:
    LANG=            LC_CTYPE="POSIX"
    file.encoding    = UTF-8
    native.encoding  = ANSI_X3.4-1968
    sun.jnu.encoding = ANSI_X3.4-1968        ← 경로 인코딩은 이쪽을 쓴다

동일 컨테이너에 LANG=C.utf8 / LC_ALL=C.utf8 을 주고 같은 test 만 재실행:
    sun.jnu.encoding = UTF-8
    gradle exit=0                            ← 통과

판정: 환경. 한글 파일명을 쓰는 test 가 UTF-8 로케일을 전제하는데, 그 전제를 선언하지 않는다.
저장소는 sun.jnu.encoding 을 test JVM 에 고정하지 않는다.

────────────────────────────────────────────────────────────────────────
3. app-bootstrap 1건 — jq 미설치가 "계약 위반"으로 보고된다
────────────────────────────────────────────────────────────────────────
ComposeMergeCharacterizationTest > every lane renders exactly the services its contract names

org.opentest4j.AssertionFailedError: [verify-compose-profile-contracts.sh said:
jq is required
]
expected: 0
 but was: 78
  at ComposeMergeCharacterizationTest.java:62

which jq → NOT INSTALLED

스크립트는 전제 결손과 계약 위반을 다른 코드로 구분한다:
    scripts/verify-compose-profile-contracts.sh:28-30
      command -v jq     >/dev/null 2>&1 || { echo "jq is required" >&2; exit 78; }
      command -v docker >/dev/null 2>&1 || { echo "docker is required" >&2; exit 78; }
      [[ -r "${CONTRACTS}" ]] || { echo "missing ${CONTRACTS}" >&2; exit 78; }
    …
    스크립트 끝:  FAILURES > 0 이면 exit 1

test 는 docker compose 존재만 assumption 으로 막고, 나머지 두 전제는 막지 않는다:
    ComposeMergeCharacterizationTest.java:58-62
      Assumptions.assumeTrue(dockerComposeIsAvailable(), "docker compose is not on this machine");
      ProcessResult result = run(List.of("./scripts/verify-compose-profile-contracts.sh"));
      assertThat(result.exitCode()) … .isZero();

exit 78(sysexits.h 의 EX_CONFIG, "확인할 수 없음")과 exit 1("계약이 틀렸음")이 같은 실패로 뭉개진다.
jq 없는 기계에서 이 suite 는 아무것도 검사하지 않고 "레인이 계약과 다르다"고 보고한다.

────────────────────────────────────────────────────────────────────────
4. 그 계약이 실제로 성립하는지 — 독립 경로 검증
────────────────────────────────────────────────────────────────────────
jq 를 설치하지 않았다(사용자 기계의 시스템 변경). 대신 스크립트의 6개 검사를 파이썬으로 이식해
같은 docker compose 렌더링 위에서 돌렸다. 아무것도 기동하지 않는 정적 검사다.

compose version 2.40.3+ds1-0ubuntu1~24.04.1 >= 2.24.4
off-local: app @ local
off-dev: app @ dev
off-prod: app @ prod
local-jpa: app,db @ local
local-mongo: app,mongo,mongo-rs-init @ local
local-messaging: app,kafka @ local
local-messaging-outbox: app,db,kafka @ local
local-notification-ingest: app,db,db-migrate-capabilities,db-promote-capabilities,keycloak,notification-smoke @ local
local-notification-serving: app,db,db-migrate-capabilities,db-promote-capabilities,keycloak,mailpit,notification-smoke @ local
local-notification-handoff: app,db,db-migrate-capabilities,db-promote-capabilities,keycloak,mailpit,notification-smoke @ local
local-graphql: app,auth-smoke,graphql-smoke,keycloak @ local
shared-infra-local: app,auth-smoke,db,keycloak,minio,minio-init,object-storage-smoke @ local
shared-infra-dev: app,auth-smoke,db,keycloak,minio,minio-init,object-storage-smoke @ dev
prod-smoke: app,auth-smoke,db,keycloak,minio,minio-init,object-storage-smoke @ prod
all-adapters: app,auth-smoke,db,db-migrate-capabilities,db-promote-capabilities,graphql-smoke,kafka,keycloak,mailpit,mongo,mongo-rs-init,notification-smoke @ local

PORT RESULT: all 15 lanes match src/config/runtime/compose-profile-contracts.json   (exit 0)

이식본이 검사한 것: ①compose 버전 하한 ②레인별 정확한 서비스 집합 ③렌더된
SPRING_PROFILES_ACTIVE ④병합 모델의 마운트 대상 중복 ⑤서비스-역할 분할과 --wait 대상 정합
⑥Keycloak realm 의 밑줄 키. 원본 스크립트와 같은 순서·같은 판정 기준이다.

주의: 이것은 두 번째 의견이다. test 자신의 주석이 경계하는 바로 그것 —
"a test that re-derived the same checks in Java would be a second opinion that can agree with the
contract while the script disagrees" — 이므로, jq 가 있는 기계에서 원본 스크립트로 확인하는 것이
여전히 정본이다. 여기서 얻은 것은 "계약 위반의 증거가 없다"이지 "스크립트가 통과한다"가 아니다.

────────────────────────────────────────────────────────────────────────
5. 이 재실행에서 나온 finding
────────────────────────────────────────────────────────────────────────
P3 · app-bootstrap — everyLaneMatchesItsContract 가 스크립트의 전제 결손(exit 78)과
     계약 위반(exit 1)을 구분하지 않는다. docker compose 만 assumption 으로 막혀 있고
     jq·계약 파일 가독성은 막혀 있지 않다. 확인하지 못한 것이 틀린 것으로 보고된다.
P3 · fileserver — 한글 파일명을 쓰는 test 가 UTF-8 sun.jnu.encoding 을 전제하면서
     그 전제를 test JVM 에 고정하지도, assumption 으로 선언하지도 않는다.

애플리케이션 소스는 변경하지 않았다. jq 도 설치하지 않았다.
