Skip to main content

a-check-that-cannot-flip-is-not-measuring-anything

테스트가 아닌 검사 — 부재 가드 · 완료 감지기 · 문서 전체를 훑는 단언 — 은 RED 를 먼저 보는 습관이 없어서, **구조상 한쪽 판정만 낼 수 있는 채로** 배포된다. 초록이면 "결함 없음"이 아니라 "빨개질 수 없음"이고, 타임아웃이면 "전파 안 됨"이 아니라 "성공을 감지할 수 없음"이다. 검사마다 **반대 판정을 한 번 강제로 보라.** 트리거 - `until`/폴링 대기 루프, 삭제 PR 의 부재 가드, dict/응답을 문자열로 합쳐 매칭하는 단언, `$(cmd || echo 0)`, "왜 계속 타임아웃이지", 고치기 전인데 통과하는 테스트.

Zur Installation springen

Quellinformationen

Repository
blas1n/claude-skills
Letzte Quellaktivität
9. September 2026 um 09:12
Erkannte Sprache von SKILL.md
Koreanisch
Sterne
2
Forks
0

Installationsoptionen

Standardmäßig ist der Prompt ausgewählt, der zuerst die Quelle prüft. Sie können zu einem direkten Befehl wechseln oder eine lokale Kopie herunterladen.

Quelldateien prüfen

Lesen Sie SKILL.md und alle von SkillsMP angezeigten Begleitdateien, bevor Sie sich für eine Installation entscheiden.

SKILL.md wird angezeigt

SKILL.md
Quellanweisungen · Schreibgeschützte Vorschau
name
a-check-that-cannot-flip-is-not-measuring-anything
description
테스트가 아닌 검사 — 부재 가드 · 완료 감지기 · 문서 전체를 훑는 단언 — 은 RED 를 먼저 보는 습관이 없어서, **구조상 한쪽 판정만 낼 수 있는 채로** 배포된다. 초록이면 "결함 없음"이 아니라 "빨개질 수 없음"이고, 타임아웃이면 "전파 안 됨"이 아니라 "성공을 감지할 수 없음"이다. 검사마다 **반대 판정을 한 번 강제로 보라.** 트리거 - `until`/폴링 대기 루프, 삭제 PR 의 부재 가드, dict/응답을 문자열로 합쳐 매칭하는 단언, `$(cmd || echo 0)`, "왜 계속 타임아웃이지", 고치기 전인데 통과하는 테스트.
version
1.0.0
task_types
["testing","devops","review"]
triggers
[{"pattern":"폴링 루프/대기 스크립트를 써서 배포·CI·잡 완료를 감지할 때"},{"pattern":"$( ) 안에서 || echo / || true 로 실패를 기본값으로 접을 때"},{"pattern":"dict·JSON 응답을 한 덩어리 문자열로 합쳐 in 으로 단언할 때"},{"pattern":"삭제/금지 가드를 새로 쓸 때"},{"pattern":"대기 루프가 계속 타임아웃인데 대상은 멀쩡해 보일 때"}]
category
trap
# 뒤집히지 못하는 검사는 아무것도 재고 있지 않다 ## Problem BSVibe 2026-09-09 한 세션에서 **검사 셋**을 썼다. 셋 다 그럴듯했고, 셋 다 **시스템이 아니라 자기 구조 때문에** 판정이 미리 정해져 있었다. | 검사 | 낸 판정 | 실제 의미 | |---|---|---| | 배포 완료 감지기 | 30분 **타임아웃** | 성공 조건이 **영원히 거짓** — 전파는 3분 만에 끝나 있었다 | | 삭제 부재 가드 | **초록** | 이름 축 하나만 물어 4개 파일이 그대로 통과 | | Art.30 응답 단언 | **초록** (고치기 전인데) | 옆 항목이 조건을 만족시켰다 | 셋 다 **다른 것이 잡아줬다** — 스위트의 수집 에러, 손으로 친 대조 쿼리, 그리고 "고치기 전인데 왜 초록이지?" 라는 위화감. 검사 자신은 끝까지 아무 말도 안 했다. > 테스트에는 RED 를 먼저 보는 습관이 있다. **가드 · 감지기 · 단언에는 없다.** > 그것들은 산출물이 아니라 "인프라"로 취급돼서, 한 번도 반대 판정을 강제당하지 않는다. ### 사례 1 — `$( )` 안의 `|| fallback` 은 **덮어쓰지 않고 덧붙인다** ```bash # ❌ 성공을 영원히 감지 못 한다 old=$(docker exec c sh -c 'grep -c "needle" /app/f.py' 2>/dev/null || echo 0) if [ "$new" = "1" ] && [ "$old" = "0" ]; then echo "전파됨"; fi ``` `grep -c` 는 **매치가 0건이면 종료코드 1** 을 낸다(개수는 정상 출력하면서). 그래서 성공 케이스(=0건)에서 `grep` 이 `0` 을 찍고 **그 직후 `echo 0` 도 발화**한다. 명령 치환은 둘 다 담는다: ``` old = "0\n0" → [ "$old" = "0" ] 는 영원히 거짓 ``` 폴백은 **명령이 아무것도 안 찍었을 때만** 대체가 된다. 뭔가 찍고 실패하는 명령 (`grep -c`, `grep -q` 아닌 것들, `wc` 파이프, 일부 CLI)에서는 **덧붙는다.** 그리고 로그에는 이렇게 남아 사람 눈에도 잘 안 띈다: ``` === 타임아웃: 30분 내 전파 안 됨 (신규=1 옛=0 0) === ``` **고침** — 실패를 기본값으로 접지 말고, 값을 못 얻은 것과 값이 0 인 것을 갈라라: ```bash # ✅ 폴백 없이. 못 얻으면 빈 문자열이 되고, 그건 "0" 과 다르다 n=$(docker exec c sh -c 'psql ... -tAc "SELECT count(*) ..."' 2>/dev/null | tr -d '[:space:]') [ "$n" = "0" ] && { echo "적용됨"; exit 0; } ``` 가능하면 **개수를 세지 말고 존재를 물어라** — `grep -c` 대신 `grep -q` 는 판정이 곧 종료코드라 이 함정이 없다. ### 사례 2 — 가드는 **자기가 열거한 축**에서만 빨개진다 지운 것에 이름이 여럿이면(테이블 리터럴 · ORM 심볼 · 인덱스 접두사), 한 축만 스캔한 가드는 나머지 축에서 **구조적으로 빨개질 수 없다.** 자세한 건 [[absence-guard-listing-spellings-proves-only-imagination]] §축. ### 사례 3 — 문서를 통째로 합쳐 매칭하면 **옆 항목이 대신 만족시킨다** ```python retention = resp.json()["retention"] blob = " ".join(f"{k} {v}" for k, v in retention.items()) assert "30" in blob # ❌ 고치기 전인데 통과했다 ``` `retention` 의 **다른 항목**이 이미 *"soft-deleted rows are retained 30 days"* 를 담고 있었다. 내가 재려던 항목은 손도 안 댄 상태였는데 단언은 초록이었다. **고침 둘을 같이** — 범위를 항목으로 좁히고, **옆 항목과 충돌할 수 없는 값**을 쓴다: ```python _PATCHED_DAYS = 17 # 30 은 옆 문장이 이미 담고 있다 audit_sentence = resp.json()["retention"]["audit_outbox"] # 문서 아닌 항목 assert str(_PATCHED_DAYS) in audit_sentence ``` 픽스처 값을 아무거나 고르면 **그 값이 우연히 옆에 있는지**는 아무도 안 본다. [[ascii-fixture-cannot-catch-byte-vs-char-confusion]] 과 같은 병이다. ## Solution ### 검사마다 반대 판정을 한 번 강제하고, **본 것을 적어라** | 검사 | 강제할 것 | 안 하면 | |---|---|---| | 테스트 | 고치기 **전** RED (개수까지) | 거짓 초록 | | 부재 가드 | 가드가 **모르는 철자**로 인스턴스 하나 심기 | 거짓 초록 | | 존재 가드 | 그 코드를 실제로 지우기(컴파일 되게) | 거짓 초록 | | **완료 감지기 · 대기 루프** | **조건을 손으로 참으로 만들기** | 거짓 **타임아웃** | | 문서/컬렉션 단언 | 대상 항목만 비우고 나머지 유지 | 옆 항목이 대신 만족 | 대기 루프는 특히 **반대 방향**으로 실패한다는 점이 함정이다 — 다른 검사는 거짓 초록을 내지만 대기 루프는 **거짓 빨강(타임아웃)** 을 낸다. 그래서 "아직 안 됐나 보다" 로 읽히고 조사되지 않는다. ### 대기 루프는 파싱한 값을 **매 회 찍어라** ```bash for i in $(seq 1 60); do n=$(... | tr -d '[:space:]') echo "[poll $i] n=<$n>" # ← 이 한 줄이 "0\n2" 를 즉시 드러낸다 [ "$n" = "0" ] && exit 0 sleep 30 done ``` `<$n>` 처럼 **경계를 감싸서** 찍어라. 개행·공백이 낀 값은 그냥 찍으면 정상으로 보인다. ### 실패 메시지에 **비교한 값 둘을 다 담아라** ```bash echo "타임아웃 (기대=0 실제=<$n>)" ``` 이번 건은 실패 메시지가 값을 담고 있었기 때문에 사후에 원인을 알 수 있었다. 담지 않았으면 "배포가 느리네" 로 끝났을 것이다. ## Key Insights - **초록과 타임아웃은 둘 다 "아무 일 없음"처럼 읽힌다.** 그래서 뒤집히지 못하는 검사는 소리 없이 오래 산다. 소리를 내는 건 오직 반대 판정을 강제해 볼 때다. - **`|| fallback` 은 대체가 아니라 덧붙임일 수 있다.** 명령이 실패하면서도 출력을 낸다면 그렇다. `grep -c` 가 대표적이고, 이건 "실패를 빈 값으로 접는" 익숙한 결함([[a-failed-read-degraded-to-empty-becomes-a-measurement]])의 **사촌**이다 — 거기서는 값이 사라지고, 여기서는 값이 **오염된다.** - **인프라 코드에는 RED 습관이 없다.** 테스트는 TDD 가 강제하지만 가드·감지기· 대기 루프는 "돌아가면 된 것"으로 취급된다. 그 셋이 정확히 스스로를 검증하지 않는 코드다. - **다른 것이 잡아줬다면, 그건 이 검사가 안 잡은 것이다.** 스위트가 잡았고 손으로 친 쿼리가 잡았다면 그 검사는 이번에 **점수가 0점**이다 — 결과가 맞았어도. ## Red Flags - 대기 루프가 **매번** 타임아웃까지 간다 (한 번도 조기 종료를 본 적 없다) - `$(cmd || echo <기본값>)` — 특히 `cmd` 가 개수를 세는 것일 때 - 실패 로그에 값이 **여러 줄로** 찍힌다 (`옛=0` 다음 줄에 `0`) - 고치기 **전에** 새 테스트가 통과한다 - dict/JSON 을 `" ".join(...)` · `str(...)` · `json.dumps(...)` 로 합쳐 `in` 으로 단언 - 픽스처 값이 그 자료구조의 **다른 곳에도** 나타나는 흔한 수(30, 1, 0, "test") ## Related - [[absence-guard-listing-spellings-proves-only-imagination]] — 부재 가드가 축을 놓치는 쪽 - [[completion-detector-fires-on-an-earlier-success-string]] — 감지기가 **너무 일찍** 발화하는 쪽. 이 스킬은 **영원히 발화 못 하는** 쪽 - [[a-failed-read-degraded-to-empty-becomes-a-measurement]] — 실패를 기본값으로 접는 것의 원형 - [[a-cut-that-does-not-compile-is-not-a-wire-cut]] — 반대 판정을 강제할 때 그 절단 자체가 유효해야 한다 - [[piped-gate-masks-exit-code]] — 파이프 뒤 `$?` 가 다른 명령 것인 사촌 함정
Auf GitHub ansehen