The keycloak project ended with four open questions that design could not
settle. A two-VM lab was built to answer them by measurement, and this is
that material: 26 experiments, 125 raw command outputs, 22 browser captures.
Follows the import procedure in README.md.
source/ the originating repository verbatim — 78 documents, 28 SVGs,
8 manifests, plus .source-revision recording the commit
final/ the SSOT
document.md 729 lines written from the 29 experiment documents, not
concatenated: what was predicted, what was measured, and
where the measurement itself was wrong
evidence/raw 125 outputs, flattened to <experiment>__<file> because
the originals collided (01-baseline.txt appeared three
times) and the audit only globs the top level
evidence/meta one per raw file; command and exitCode are null and the
README says why rather than inventing them
evidence/browser 22 captures
assets/ three diagrams through techviz
.techviz/ their VizSpecs
A separate project rather than an addition to keycloak: the B-layer answers
that project's four questions, but the A, C and D layers are about cluster
failure, SSO and operations, and one document.md should hold one subject.
The four question records there can point here through 관계.
Recorded rather than papered over: only three of the 28 diagrams were
remade. The repository forbids hand-drawn SVG and forbids titles inside the
canvas; all 28 originals carry both, so converting them is redrawing, not
reformatting. They stay in source/ and the gap is written into the document.
verify-pipeline.py passes. audit-records.py reports no issues.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
14 KiB
kind, slug, title, topic, project, status, sourceRevision, rootTreeNode, evidenceCapturedOn, body, assets, evidence, source
| kind | slug | title | topic | project | status | sourceRevision | rootTreeNode | evidenceCapturedOn | body | assets | evidence | source | |||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| CASE | a20-f003-claude | grpc 레인 시험 25개는 check 로 돌고, 레인만 거는 실행 0 검사는 돌지 않는다 | grpc-and-streaming | clean-architecture-backend-template | 게시 전 | 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 | case:a20-f003-claude | 2026-09-04 | case-a20-f003-claude.body.md |
|
|
|
grpc 레인 시험 25개는 check 로 돌고, 레인만 거는 실행 0 검사는 돌지 않는다
원문은 증거 레인의 시험 스물다섯 개에 자동 실행 경로가 없다고 적었다. :grpc:grpc-testkit 의 두 태스크를 돌려 시험 이름을 집합으로 비교하니 기본 test 의 56 개가 그 스물다섯을 전부 포함한다. 자동 경로에 없는 것은 시험이 아니라 레인이 거는 실행 0 검사다.
관계
- 아무도 돌리지 않는 레인의 게이트는 마지막으로 돌린 사람이 본 것을 보고한다 원문이 이 사례를 그 규칙으로 읽었다. 시험 쪽에는 붙지 않고, 레인만 거는 실행 0 검사 쪽에 붙는다.
- 네 레인이 check 에 붙지 않고, 이 가족을 이름으로 부르는 워크플로가 없다
그 기록이 25개 시험을 직접 입력할 때만 도는 것으로 적었는데, 기본
test가 그 25개를 포함한다. - release gate가 실제로 차단하는 것은 hermetic test 3개이고, mongo용 CI workflow는 없다
mongo 가족에서도 레인을 이름으로 부르는 워크플로가 없어서, 게이트가 실제로 차단하는 것은 기본
test에 들어 있는 밀폐 클래스뿐이다.
문제
src/grpc/CLAUDE.md 가 세 레인의 현재 상태를 통과로 서술한다.
그 문장의 주어가 무엇이고 어느 경로로 실행되는지 확인했다.
결론
앞 절반은 사실이다. grpcInProcessContractTest·grpcNettyContractTest·grpcFaultTest 를 이름으로 실행하니 모두 BUILD SUCCESSFUL 이고 시험 수는 7·9·9 다.
이어서 grpc-testkit 의 기본 test 를 실행했다. 7 클래스 56 개가 통과하는데, 시험 이름을 하나씩 맞춰 보면 레인 쪽 25 개가 빠짐없이 그 안에 들어 있다.
겹치는 이유는 제외 태그 목록에 있다. 레인은 태그로 시험을 고르고 그 시험들은 공유 test 소스 세트에 있는데, 기본 test 가 제외하는 것은 루트 규약의 quarantine 과 grpc-testkit/build.gradle:42 의 grpc-performance 뿐이다. ci-quality-gates.yml:50 이 ./gradlew check 를 돌리므로 이 25 개에는 사람이 부르지 않아도 도는 경로가 있다.
넷째 레인만 다르다. GrpcPerformanceLaneTest 는 @Tag("grpc-performance") 를 달아 기본 test 결과에 0 건이다.
자동 경로에 없는 것은 따로 있다. ca.strict-test-lane.gradle:293 의 최신 재사용 거부와, :246·:257 이 실행한 시험 수를 세어 0 이면 GradleException 을 던지는 검사다. failOnNoDiscoveredTests 는 이 둘에 들어가지 않는다. 레인을 쓰지 않는 leaf 의 테스트 태스크에서도 같은 값이라 빌드 도구 기본값이다.
@Tag 가 지워지면 기본 test 는 같은 수를 계속 돌리고, 클래스가 지워지면 더 적은 수를 돌리며 그대로 통과한다. :grpc:grpc-testkit:check 의 dry-run 그래프에 레인 태스크는 0 건이고, grpc-testkit/build.gradle 에 check 가 0 번 나온다.
.github/workflows 의 파일 어디에도 grpc 라는 문자열이 없다. 다만 ci-quality-gates.yml:61 의 conditionalTransportQualification 이 src/build.gradle:606 을 통해 :adapter:inbound:grpc:grpcTransportQualificationTest 를 요구하므로, 자동으로 도는 grpc 레인이 다른 프로젝트에 하나 있다.
원본 분석의 등급은 P2 이고 이 기록은 새로 매기지 않는다. 그 등급을 떠받치던 근거 — 스물다섯 개 시험이 자동 실행 밖에 있다 — 는 성립하지 않는다. 남는 것은 레인만 거는 두 보증이 자동 경로에 없다는 더 좁은 결함이다.
검증 환경
Gradle : 9.0.0 확인 방식 : 레인 등록 블록과 태그 확인, 기본 test 에 걸리는 제외 태그를 루트 규약과 leaf 양쪽에서 확인, 태그가 클래스 단위인지 확인, :grpc:grpc-testkit:check dry-run 으로 태스크 그래프 확인, 결과 디렉터리를 비우고 실행 전 XML 0 건 확인 후 세 레인과 기본 test 를 각각 실행하고 종료 코드로 집계를 막음, 두 실행의 시험 이름 집합 비교, 성능 레인 클래스의 포함 여부 확인, 레인 규약의 보증 구현 확인, failOnNoDiscoveredTests 를 레인 없는 leaf 와 대조, check 언급과 워크플로 검색, 지속 통합이 부르는 다른 grpc 레인 확인 소스 수정 : x
재현 조건
- 레인을 등록하는 블록에서 이름과 태그를 모은다.
- 기본 테스트 태스크에 걸리는 제외 태그를 루트 규약과 leaf 빌드 파일 양쪽에서 찾는다.
- 레인 태그가 클래스 선언에 붙는지 확인한다.
- 검사 태스크를 dry-run 해 그래프에 기본 테스트가 있고 레인 태스크가 없는 것을 본다.
- 결과 디렉터리를 비우고 남은 XML 이 0 인지 찍는다.
- 세 레인을 이름으로 실행하고 종료 코드를 확인한 뒤에만 결과를 집계한다.
- 같은 leaf 의 기본 테스트 태스크를 같은 방식으로 실행하고 집계한다.
- 두 실행이 남긴 시험 이름을 모아 한쪽이 다른 쪽에 포함되는지 판정한다.
- 제외 태그를 단 클래스가 기본 실행 결과에 있는지 센다.
- 레인 규약에서 기본 태스크에 없는 보증을 찾고, failOnNoDiscoveredTests 를 레인이 없는 leaf 와 대조한다.
- 이 leaf 의 빌드 파일이 검사 태스크를 언급하는지, 워크플로가 이 가족을 이름으로 부르는지 세고, 다른 프로젝트의 grpc 레인이 자동으로 도는지 확인한다.
본문
src/grpc/CLAUDE.md 는 in-process·Netty·fault 레인이 실제로 실행되어 통과한다고 적는다. 원본 분석은 앞 절반을 사실로 확인한 뒤, 그 레인들이 check 에 없고 어떤 워크플로도 이름으로 부르지 않으므로 스물다섯 개 시험이 누군가 명령을 입력할 때만 돈다고 적었다.
레인 넷과 기본 test 가 제외하는 태그
:::evidence key="a20-f003-claude" alt="src 디렉터리에서 돌린 gradle 실행과 정적 검색을 합친 출력 134줄. 먼저 gradle 판이 나오고, grpc-testkit 의 build.gradle 이 등록하는 레인 넷과 태그, 그리고 기본 test 에 걸리는 제외 태그가 루트 규약의 quarantine 과 leaf 의 grpc-performance 두 곳에서 온다는 것이 원문과 함께 실린다. 세 레인 태그가 클래스 선언 바로 위에 붙어 있는 것이 보인다. 이어서 check 를 dry-run 한 태스크 그래프가 나오는데 test 와 check 는 있고 레인 태스크는 0 건이다. 그 다음 결과 디렉터리를 지우고 실행 전 남은 XML 이 0 건임을 찍은 뒤 세 레인을 이름으로 돌려 GRADLE_EXIT=0 과 클래스별 7·9·9 가 실패 0 으로 나오고, 이어서 기본 test 를 돌려 GRADLE_EXIT=0 과 7 클래스 56 개가 나온다. 두 실행의 시험 이름을 집합으로 비교해 레인 25 개가 기본 test 56 개 안에 전부 있다는 판정이 True 로 찍히고 기본 test 에만 있는 시험이 31 개라는 것, 성능 레인 클래스가 기본 test 결과에 0 건이라는 것이 나온다. 마지막으로 레인 규약에서 보증에 해당하는 줄들이 인용되고, grpc-testkit 의 build.gradle 이 check 를 0 번 언급하며 워크플로 28 개 중 grpc 문자열을 담은 것이 0 건인데 ci-quality-gates 의 48번부터 61번 줄에는 check 가 모든 레인을 덮지 않는다는 주석과 별도 스텝 둘이 있고, 그중 마지막 스텝이 부르는 태스크가 grpc 이름의 레인을 요구한다는 것이 나온다." caption="레인 넷과 태그 · 기본 test 의 제외 태그 두 곳 · check 그래프에 레인 0 건 · 실행 전 XML 0 건과 GRADLE_EXIT · 레인 25 개가 기본 test 56 개의 부분집합 True · 레인 규약의 보증 · check 가 레인을 덮지 않는다는 주석과 grpc 레인을 부르는 별도 스텝 — 134줄 · exit 0" zoom="true" :::
grpc-testkit/build.gradle:14~:35 가 레인 넷을 등록한다. grpcInProcessContractTest, grpcNettyContractTest, grpcFaultTest, grpcPerformanceTest 이고 태그는 grpc-inprocess, grpc-netty, grpc-fault, grpc-performance 다. 세 태그는 클래스 선언 바로 위에 붙어 클래스 전체를 덮는다.
기본 test 에 걸리는 제외 태그는 두 곳에서 온다. 루트 규약 src/build.gradle:551 의 quarantine 과 grpc-testkit/build.gradle:42 의 grpc-performance 다. grpc-testkit 의 시험에 quarantine 태그는 0 건이므로 이 leaf 에서 실효 제외는 성능 태그 하나다. 나머지 세 태그는 어느 쪽에도 없다.
레인 25 개는 기본 test 56 개 안에 있다
:grpc:grpc-testkit:check 를 dry-run 했다. 그래프에 :grpc:grpc-testkit:test 가 있고 레인 태스크는 0 건이다. 시험을 돌리지 않고 얻은 결과다.
그다음 결과 디렉터리를 지우고 돌렸다. 실행 전 남은 XML 이 0 건임을 찍은 뒤, 세 레인을 이름으로 돌려 GRADLE_EXIT=0 과 GrpcInProcessContractFixtureTest 7, GrpcNettyContractProfileTest 9, GrpcTransportEvidenceClassifierTest 9 를 얻었다. 이어서 기본 test 를 돌려 GRADLE_EXIT=0 과 7 클래스 56 개를 얻었다.
두 실행의 시험 이름을 집합으로 비교했다. 레인이 돌린 25 개가 기본 test 의 56 개 안에 전부 있다. 기본 test 에만 있는 시험은 31 개다.
GrpcPerformanceLaneTest 는 기본 test 결과에 0 건이다. 제외 태그가 그 클래스를 걸러낸다.
ci-quality-gates.yml:50 이 ./gradlew check 를 돌린다. 그러므로 이 25 개에는 사람이 이름을 입력하지 않아도 도는 경로가 있다.
레인만 거는 보증 둘은 check 에 없다
레인 태스크가 기본 test 와 다른 점은 둘이다.
하나는 ca.strict-test-lane.gradle:293 의 outputs.upToDateWhen { false } 다. 레인은 결과를 최신으로 재사용하지 않는다.
다른 하나는 실행한 시험 수를 세어 0 을 거부하는 것이다. :246 이 executedTests 를 두고 :250 이 시험마다 증가시키며, :257 이 그 값이 0 이면 GradleException 을 던진다. :239~:245 는 왜 그것이 필요한지 적는다 — failOnNoDiscoveredTests 는 발견 단계에 적용되는데 태그 필터는 발견 이후에 시험을 걸러 내므로, 태그가 아무것도 맞히지 못한 레인은 그것만으로는 성공으로 끝난다는 것이다.
failOnNoDiscoveredTests 자체는 레인만의 것이 아니다. 레인이 없는 :grpc:grpc-core-api:test 에서도 true 이므로 Gradle 9.0.0 의 기본값이다. :23 이 적는 레인의 몫은 그 값을 갖는 것이 아니라 leaf 가 그것을 끄지 못하게 하는 것이다.
@Tag("grpc-netty") 가 지워지면 그 클래스는 여전히 test 소스 세트에 있으므로 기본 test 는 같은 수를 돌린다. 클래스 자체가 지워지면 기본 test 는 더 적은 수를 돌리고 그대로 통과한다. 두 경우 모두 레인은 실행 0 으로 실패하는데, 레인을 부르는 자동 경로가 없다.
grpc-testkit/build.gradle 은 check 를 한 번도 언급하지 않는다. 워크플로 스물여덟 개 중 grpc 문자열을 담은 것은 0 건이다.
다만 그 0 건이 이 가족 전체를 뜻하지는 않는다. ci-quality-gates.yml:61 이 conditionalTransportQualification 을 돌리고, src/build.gradle:606 이 그 태스크에 :adapter:inbound:grpc:grpcTransportQualificationTest 를 건다. 지속 통합이 자동으로 부르는 grpc 이름의 레인이 하나 있고, 그것은 :grpc:grpc-testkit 의 레인 넷이 아니다.
원문과 갈리는 자리
원문은 :286 에서 ci-quality-gates.yml 이 check 를 돌리므로 각 leaf 의 기본 test 는 지속 통합에서 실행된다고 스스로 적었다. 그리고 바로 다음 문단에서 그 25 개가 누군가 명령을 직접 입력할 때만 돈다고 적었다. 두 문장이 함께 설 수 없다.
성립하는 쪽은 앞 문장이다. 세 레인의 시험은 태그가 붙은 채 공유 test 소스 세트에 있고, 기본 test 가 제외하는 것은 성능 태그와 quarantine 뿐이다. 집합 비교가 25 개 전부의 포함을 보인다.
원문이 인용한 문장 — 아무도 지역에서 돌리지 않는 레인의 붉은 게이트는 마지막으로 돌린 사람이 본 것을 보고한다 — 은 시험 자체에는 붙지 않는다. 붙는 것은 레인이 거는 실행 0 검사 쪽이다.
원문은 레인을 :286 에서 "네 증거 레인"으로, :8 과 :465 에서 "증거 레인 3종"으로 부른다. 등록된 것은 넷이고 돌린 것은 셋이며 넷째는 기본 test 에서 의도적으로 제외된 성능 레인이므로 두 표기 모두 대상이 다를 뿐 틀리지 않았다.
등급에 대해
원본 분석의 등급은 P2 다. 제목이 든 근거 — 증거 등급 모델 전체가 자동 실행 경로 밖에 있다 — 는 이 리비전에서 성립하지 않는다.
남는 것은 더 좁은 결함이다. 레인이 존재하는 이유인 실행 0 검사와 최신 재사용 거부가 자동 경로에 없다. 이 기록은 등급을 새로 매기지 않고 상류가 P2 로 둔 근거와 여기서 확인한 반대 근거를 함께 남긴다.
확인하지 못한 것
태그 삭제와 클래스 삭제를 실제로 해 보고 두 태스크가 어떻게 갈리는지 관측하지는 않았다. 필터 구성과 규약 코드를 읽었고, 겹치는 시험은 이름 대조로 확인했다.
GitHub 러너 위에서 검사 태스크를 돌리지 않았다. 워크플로에 적힌 명령과 로컬 dry-run 그래프를 이어 붙여 판단했다.
지속 부하와 성능 기준선은 확인 범위 밖이다. 지침 문서도 그것이 없다고 적는다.
@Tag 를 지우거나 클래스를 지운 상태를 만들어 두 태스크의 결과가 갈리는 것을 실행으로 보이지 않았다. 두 태스크의 필터 구성과 레인 규약의 실행 0 검사를 읽고, 같은 시험이 양쪽에서 도는 것을 집합 비교로 확인한 데까지다.