httpclient 모듈 완료 검증 — lane 실행과 실패 진단
revision=a24ece9cf797f7ea647e33bf846b115208ed1ba5
generatedAt=2026-08-30T05:40:58+00:00

=== 1. :check 는 archive-hygiene 게이트에서 실패한다 (소스와 무관) ===
$ ./gradlew :adapter:outbound:httpclient:check  → BUILD FAILED
원인: build/libs 에 이전 리비전 JAR 이 남아 있음
$ ls src/adapter/outbound/httpclient/build/libs
httpclient-0.0.1+0137263441f6.jar
httpclient-0.0.1+a24ece9cf797.jar
httpclient-0.0.1+e98b56eb03ec.jar
exit=0

게이트 정의: src/gradle/archive-hygiene.gradle — 저장소가 remedy 태스크 cleanStaleTraceableJars 를 제공한다.
이 분석은 사용자 워크스페이스의 빌드 산출물을 삭제하지 않았다.

=== 2. 다섯 lane 개별 실행 ===
$ ./gradlew :adapter:outbound:httpclient:test :…:httpClientStableContractTest :…:httpClientSecurityTest :…:httpClientBlockHoundTest :…:spring62ApiSurfaceScan
GRADLE_EXIT=1 — :test 에서 283 tests completed, 3 failed

=== 3. JUnit 집계 ===
test: classes=61 tests=283 failures=3 errors=0 skipped=0
httpClientStableContractTest: classes=7 tests=24 failures=0 errors=0 skipped=0
httpClientSecurityTest: classes=2 tests=9 failures=0 errors=0 skipped=0
httpClientBlockHoundTest: classes=1 tests=3 failures=0 errors=0 skipped=0
spring62ApiSurfaceScan: classes=1 tests=6 failures=0 errors=0 skipped=0

=== 4. 실패 3건의 정확한 내용 ===
anUntrustedAuthorityIsAPermanentTlsFailure() -> org.opentest4j.AssertionFailedError:  expected: TLS_HANDSHAKE  but was: CONNECT
anExpiredCertificateIsAPermanentTlsFailure() -> org.opentest4j.AssertionFailedError:  expected: TLS_HANDSHAKE  but was: CONNECT
aHostnameMismatchIsAPermanentTlsFailure() -> org.opentest4j.AssertionFailedError:  expected: TLS_HANDSHAKE  but was: CONNECT

=== 5. 원인: 원인 사슬을 바깥부터 훑고 CONNECT 분기가 TLS 분기보다 앞에 있다 ===
$ sed -n '30,42p' src/adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/apache/ApacheFailureClassifier.java

  @Override
  public TransportFailure classify(Throwable failure, AttemptStage lastObservedStage) {
    // The whole cause chain is inspected, not just the outermost throwable: Spring wraps engine
    // exceptions, and a wrapped ConnectException still proves the request was never sent. Matching
    // only the outer type would downgrade a provable NOT_SENT to an ambiguous SENT_NO_RESPONSE.
    for (Throwable cause : chain(failure)) {
      TransportFailure recognized = recognize(cause, lastObservedStage);
      if (recognized != null) {
        return recognized;
      }
    }
    return conservative(lastObservedStage, FailureCategory.UNKNOWN, "TRANSPORT_FAILURE");
exit=0

$ sed -n '58,73p' src/adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/apache/ApacheFailureClassifier.java
    if (cause instanceof ConnectTimeoutException
        || cause instanceof HttpHostConnectException
        || cause instanceof ConnectException
        || cause instanceof NoRouteToHostException) {
      return TransportFailure.notSent(
          AttemptStage.CONNECT, FailureCategory.CONNECT, "CONNECT_FAILED");
    }
    if (cause instanceof SSLPeerUnverifiedException || cause instanceof CertificateException) {
      return TransportFailure.notSent(
          AttemptStage.TLS_HANDSHAKE, FailureCategory.TLS_PERMANENT, "TLS_TRUST_FAILED");
    }
    if (cause instanceof SSLHandshakeException) {
      return TransportFailure.notSent(
          AttemptStage.TLS_HANDSHAKE, FailureCategory.TLS_PERMANENT, "TLS_HANDSHAKE_FAILED");
    }
    if (cause instanceof SSLException
exit=0

--- 재시도 결과 대비 ---
$ grep -n 'case CONNECT\|case TLS_TRANSIENT\|permanent()' src/adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/resilience/DefaultRetryEligibilityEngine.java
37:    if (context.failureCategory().permanent()) {
107:      case CONNECT -> RetryAllowed.of("CONNECT");
109:      case TLS_TRANSIENT -> RetryAllowed.of("TLS_TRANSIENT");
exit=0

$ grep -n 'TLS_PERMANENT' src/adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/api/operation/FailureCategory.java
18:  TLS_PERMANENT,
38:        || this == TLS_PERMANENT
exit=0


=== 6. 작업 트리 ===
$ git status --short | wc -l
0
exit=0

