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>
8.3 KiB
kind, slug, title, topic, project, status, sourceRevision, rootTreeNode, evidenceCapturedOn, assets, evidence, source
| kind | slug | title | topic | project | status | sourceRevision | rootTreeNode | evidenceCapturedOn | assets | evidence | source | |||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| CASE | a-test-that-passed-on-the-wrong-guard | 다중 타깃 검증을 확인한다는 테스트가 다른 가드에 걸려 통과했다 | what-a-gate-does-not-prove | clean-architecture-backend-template | 게시 전 | 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 | case:a-test-that-passed-on-the-wrong-guard | 2026-09-02 |
|
|
|
다중 타깃 검증을 확인한다는 테스트가 다른 가드에 걸려 통과했다
경로 형태 검증기의 첫 가드가 SINGLE 이 아닌 전략을 전부 거부한다. 그 뒤 switch 의 나머지 세 갈래와 폴백 그래프 검사는 도달할 수 없고, 그것을 확인한다는 테스트는 앞 가드의 예외로 통과한다.
관계
- 빠뜨림이 통과가 되는 게이트는 게이트가 아니다 통과의 이유가 의도한 것과 다른 형태다.
- 계약 테스트는 어댑터가 실제로 돌리는 statement를 실행해야 한다 계약 테스트가 어댑터의 실제 statement 를 돌려야 한다는 규칙이다.
문제
경로 형태 검증기 안에서 전략을 보는 자리가 한 메서드에 둘이다.
첫 번째는 가드다. 전략이 SINGLE 이 아니면 초기 카탈로그는 SINGLE 만 지원한다는 예외를 던진다.
두 번째는 switch 다. 네 갈래를 갖고, 그 뒤에 폴백 그래프 검사가 온다.
결론
첫 가드를 통과한 경로는 전략이 SINGLE 뿐이다. switch 의 나머지 세 갈래 — 팬아웃, 순서 폴백, 기본 — 에 들어갈 값이 없다. SINGLE 갈래가 폴백을 금지하므로 그 뒤 폴백 그래프 검사의 두 문구도 도달할 수 없다.
그것을 확인한다는 테스트가 있다. 이름은 폴백 범위와 순환이 거부된다고 말하고, 순환 경로와 팬아웃 경로를 각각 만든다. 단언 둘 다 타입을 확인한 뒤 메시지에서 전략이라는 단어를 찾는다.
두 경로 다 첫 가드에 걸린다. 그 문구에 그 단어가 있으므로 단언이 통과한다. 테스트가 실제로 확인하는 것은 SINGLE 이 아닌 전략이 거부된다는 성질 하나이고, 그것을 두 번 확인한다.
이름이 약속한 두 성질 중 하나는 확인되지 않는다.
첫 가드와 switch 사이에는 전략을 보지 않는 검사가 둘 있다. 타깃 수 일치와 증폭 한계다. 도달 불가를 만든 것은 위치가 아니라 switch 갈래 안에 들어갔다는 사실이다.
검증 환경
OpenJDK : 21.0.12 Gradle : 9.0.0 확인 방식 : 코드 경로와 테스트 단언 비교, 해당 테스트 실행 소스 수정 : x
재현 조건
- 경로 형태 검증기의 첫 가드와 그 뒤 전략을 보지 않는 검사 둘을 읽는다.
- switch 네 갈래와 그 뒤 폴백 그래프 검사가 던지는 두 문구를 확인한다.
- 테스트가 만든 두 라우트의 전략과 범위 값을 읽는다.
- 두 단언이 기대하는 문자열과 각 예외 문구를 대조한다.
- 이 두 갈래를 태울 라우트를 만드는 곳을 저장소 전체에서 센다.
- 그 테스트를 실행한다.
본문
경로 형태 검증기가 같은 메서드 안에서 전략을 두 번 본다. 앞은 가드이고 뒤는 switch 다.
앞의 가드가 뒤의 갈래들을 비운다
:::evidence key="a-test-that-passed-on-the-wrong-guard" alt="코드베이스에서 경로 형태 검증기의 첫 가드와 그 뒤 전략을 보지 않는 검사 둘, switch 네 갈래 전체와 기본 갈래의 문구, 폴백 그래프 검사가 던질 수 있는 두 문구의 위치, 순환을 거부한다는 테스트의 이름과 그 테스트가 만든 팬아웃 라우트와 순환 라우트의 전략과 범위 값과 두 단언, 이 두 분기를 태울 라우트를 만드는 곳의 저장소 전수 목록과 설정 파일의 비-SINGLE 전략 수, 그리고 그 테스트를 돌린 결과를 뽑은 출력 106줄. 순환 라우트의 범위가 그 갈래의 검사를 통과하는 값이라는 것이 그 출력에 보인다." caption="첫 가드와 전략을 보지 않는 검사 둘 · switch 네 갈래 · 폴백 그래프의 두 문구 · 두 라우트의 전략과 범위 · 전수 목록 · 5건 통과 — 106줄" zoom="true" :::
첫 가드는 전략이 SINGLE 이 아니면 예외를 던진다. 문구는 초기 카탈로그가 SINGLE 만 지원한다는 것이다.
그 뒤 switch 에 네 갈래가 있다. SINGLE 은 타깃 하나와 폴백 없음을 요구하고, 팬아웃은 폴백 정의를 금지하고, 순서 폴백은 타깃 둘 이상과 활성화 범위를 요구하고, 기본 갈래는 알 수 없는 전략을 거부한다.
가드를 통과한 값은 SINGLE 뿐이다. 나머지 세 갈래에 들어갈 값이 없다.
폴백 그래프 검사도 그 뒤에 있다
switch 가 끝나면 폴백 그래프 검사를 부른다. 던질 수 있는 문구가 둘이다. 폴백이 없는 타깃을 가리킬 때와, 순환일 때다.
SINGLE 갈래는 폴백이 하나라도 있으면 그 앞에서 거부한다. 그래서 이 검사에 도달하는 경로에는 폴백 간선이 없고, 두 문구 다 나올 수 없다.
테스트는 두 라우트를 만들고 같은 것을 묻는다
테스트 이름이 폴백 범위와 순환이 거부된다고 말한다.
하나는 팬아웃 라우트다. 다른 하나는 순서 폴백 라우트이고, 두 타깃이 서로를 폴백으로 가리키는 순환이다.
두 단언 모두 예외 타입과 메시지에 전략이라는 단어가 들어 있는지를 본다. 두 라우트 다 첫 가드에 걸리고, 그 문구에 그 단어가 있다.
그래서 이 테스트가 실제로 확인하는 것은 하나다. SINGLE 이 아닌 전략이 거부된다는 것. 그것을 두 번 확인한다.
가드가 사라지는 날 두 단언이 갈린다
순환 라우트는 타깃 둘과 폴백 활성화 하나를 갖는다. 순서 폴백 갈래의 범위 검사는 타깃 둘 미만이거나, 활성화가 하나 미만이거나, 활성화가 타깃 수보다 하나 이상 적지 않으면 거부한다. 이 값들은 셋 다 통과한다.
그러면 폴백 그래프 검사에 닿고 순환 문구를 던진다. 그 문구에는 전략이라는 단어가 없다. 이 단언은 그날 실패한다.
팬아웃 라우트는 다르다. 공급자 능력 카드의 전략이 라우트와 어긋나면 그것을 지목하는 예외가 나오는데, 그 문구에도 전략이라는 단어가 있다. 이 단언은 그날도 통과한다.
한쪽은 실패로 존재를 알리고, 다른 한쪽은 계속 조용하다.
이 갈래들을 태우는 라우트는 이 테스트뿐이다
저장소 전체에서 비 SINGLE 전략으로 라우트를 만드는 자바 코드를 찾으면 두 곳이다. 이 테스트의 두 라우트다.
같은 열거형을 쓰는 다른 파일들은 종류 정책 클래스와 그 테스트인데, 그 두 파일은 자기 클래스의 검증을 다룬다.
설정 파일에서 비 SINGLE 전략을 찾으면 0 이다.
도달 불가를 만든 것은 위치가 아니다
첫 가드와 switch 사이에도 검사가 둘 있다. 타깃 수가 상한과 정확히 같은지 보는 것과, 최악의 경우 호출 수를 계산해 총 한계와 비교하는 것이다.
둘 다 전략을 보지 않으므로 SINGLE 경로에서도 돈다. 갈라놓은 것은 switch 갈래 안에 들어갔는지 여부다.
고치는 방법
단언을 메시지 전문이나 그 갈래에만 있는 부분 문자열로 좁히면 된다. 순환이라는 단어나 잘못된 범위라는 문구가 그것이다.
지금 그 테스트를 돌리면 다섯 건이 통과한다.
확인하지 못한 것
첫 가드를 지운 사본을 만들어 돌려 보지는 않았다. 가드를 뺐을 때 각 라우트가 어디에 걸리는지는 실행 대신 범위 값과 갈래 조건으로 계산했다.