AI

무인 루프의 게이트가 못 잡는 것들

검문은 "주어진 테스트가 통과하는가"만 잰다 — 테스트가 틀렸거나 테스트가 안 보는 곳에서 깨지면 초록이 나온다

한 줄로

검문을 세 겹으로 세워도 뚫리는 자리가 있다. 검문은 “주어진 테스트가 통과하는가” 를 잰다. 테스트 자체가 틀렸거나 테스트가 보지 않는 곳에서 깨졌으면 검문은 초록을 낸다. 그리고 완주는 배포 가능을 뜻하지 않는다 — 태스크 11개가 전부 통과한 뒤 보안 점검에서 공격 7건 중 6건이 성공했다.

테스트 자체가 틀렸을 때 갈 곳이 없었다

인수 테스트가 틀린 경우가 생각보다 많았다. 에러 코드를 코드값 대신 이름 문자열로 비교했고, 타입이 맞지 않았고, 계약과 반대 조합을 검사했다.

처음 절차표에는 “테스트 실패 = 구현 실패” 뿐이었다. 테스트 파일은 보호돼 있어(2편) 구현이 고칠 길도 없었다.

그대로 따르면 시도 세 번을 같은 벽에 태우고, 일지에는 “구현이 못 했다”는 거짓 진단이 세 줄 남는다.

실제로는 루프가 절차표를 벗어나 스스로 보류했는데, 그건 규격이 아니라 운이었다.

그래서 “테스트가 틀렸다(spec_defect)”는 보류 종류를 따로 두었다. 시도를 태우지 않고 사람에게 넘긴다.

남은 구멍도 있다. 4편에서 첫 red 는 사람이 한 번 읽는다고 썼지만, 그 뒤로 매번 일어나는 실패가 구현 결함 때문인지 테스트 결함 때문인지는 기계가 판정하지 못한다. 그래서 spec_defect 를 올릴지는 세션의 판단에 남는다.

태스크는 다 초록인데 전체는 빨갈 때

게이트는 태스크의 테스트 명령 하나만 돌린다. 한 태스크가 형제 태스크의 테스트를 깨도 그 사이클은 초록이다.

모듈의 태스크가 전부 완료됐는데 메인 브랜치가 빨간 일이 있었다.

전체 테스트를 매 사이클 돌리면 되지 않냐고 묻게 되는데, 해 보니 메모리 부족으로 죽었다. 남는 선택지는 셋이다.

선택지 비용
모듈 끝에 전체 테스트를 한 번 도는 태스크 발견이 늦다
바뀐 것에 닿는 테스트만 고르는 선택기 선택기를 만들어야 한다
머지 때 CI 가 돌게 한다 CI 자원이 든다

어느 쪽이든 전체 테스트가 도는 상태를 먼저 만들어야 한다.

끝난 태스크를 소급해서 깨는 변경

한 태스크가 기본값 없는 NOT NULL 칸을 더했다. 그 표에 INSERT 하는 형제 태스크의 테스트 8개가 이미 초록이었는데도 전부 깨졌다.

루프는 원인을 정확히 진단했지만, 테스트 보호 규칙 때문에 고칠 권한이 없었다. “시험 문제 조작”과 “선행 변경을 픽스처가 따라가야 하는 정당한 수정”을 규칙이 구분하지 못했다.

처방 후보는 셋이었다 — 사람이 형제 픽스처를 고치거나, 신설 칸에 기본값을 주거나, 신설 칸을 후속 태스크로 쪼개는 것. 우리는 첫 번째를 세 번 택했다.

최고 권한으로 돌린 테스트는 권한 결손을 못 잡는다

애플리케이션의 DB 계정에는 UPDATE 권한이 없었는데, 인수 테스트는 기본 접속(수퍼유저)으로 돌아 초록이었다.

인수 테스트는 프로덕션 코드가 실제로 도는 권한으로 돌아야 한다. 픽스처 준비만 높은 권한으로 한다.

리뷰 에이전트가 쓸 수 없었다

두 번째 검문으로 별도 리뷰 에이전트를 두었는데, 고른 에이전트에 판정 파일을 쓸 권한도 git diff 를 돌릴 권한도 없었다.

  • 어떤 사이클은 쓰기 가능한 리뷰어를 다시 띄웠다
  • 어떤 사이클은 작성자가 리뷰 판정을 옮겨 적었다 → 뒤의 것은 “자기가 쓴 것을 자기가 승인하지 않는다”는 원칙을 무너뜨린다

리뷰어의 도구 권한을 먼저 확인한다. 그리고 리뷰 직전에 판정 파일을 지워서, 지난 사이클의 “승인”이 새 리뷰 없이 통과되지 않게 한다.

리뷰 요청 프롬프트의 diff 자리표시가 채워지지 않은 채 나가 첫 리뷰가 무효가 된 적도 있다. 리뷰어에게 실제 diff 가 들어갔는지 확인한다.

그래도 두 번째 검문은 값을 했다

게이트 1을 다 통과한 뒤 남은 결함을 리뷰가 잡았고, 그중 상당수는 실제로 돌려 봐야 보였다. 그러니 리뷰는 “나중에 붙일 장치” 중에서도 가장 먼저 붙일 후보다.

다만 태스크마다 붙인 리뷰는 그 태스크의 변경만 본다.

완주는 배포 가능을 뜻하지 않는다

태스크 11개가 검문을 통과한 뒤, 마지막 보안 점검 태스크가 테스트용 DB 컨테이너에 붙어 공격 7건을 돌렸고 6건이 성공했다.

모듈을 가로지르는 권한 결함이라 어느 태스크의 리뷰에도 걸리지 않았다. 태스크별 검문은 구조적으로 이걸 볼 수 없다.

모듈 끝에 전체를 보는 점검을 두고, 그것이 찾은 것을 고칠 자리를 함께 둔다 — 7편의 처방 반영 태스크가 그것이다.

보류하기 전에 문서부터 찾게 한다

루프가 “사람이 정해 달라”며 올린 보류 가운데 서른넷쯤이 이미 문서에 답이 있는 것이었다.

그래서 판단을 요청하는 보류는 같은 시도 안에서 문서를 찾아본 기록과 찾은 내용을 함께 내야 받도록 바꿨다.

한계도 분명하다. 세션이 쓸 수 있는 파일이라 위조는 못 막고, 막는 것은 생략이다.

남는 교훈

게이트가 결국 재는 것은 “이 태스크에 주어진 테스트가 통과하는가”다. 테스트 자체의 오류와 그 테스트가 보지 않는 범위는 구멍으로 남는다.

게이트가 재는 것 재지 않는 것
이 태스크의 테스트가 통과하는가 테스트가 옳은가
이 태스크의 변경이 맞는가 형제 태스크를 깼는가
테스트가 초록인가 어느 권한으로 돌았는가
이 변경에 결함이 있는가 모듈을 가로지르는 결함이 있는가
  • 보호 규칙은 “시험 문제 조작”과 “정당한 픽스처 수정”을 구분하지 못한다. 지금은 사람이 가른다
  • 리뷰 에이전트는 도구 권한부터 확인한다. 쓸 수 없는 리뷰어는 작성자가 대신 적게 만든다
  • 모듈 끝에 전체를 보는 점검을 두고, 찾은 것을 고칠 자리를 함께 둔다

남겨둔 것 — 전체 테스트가 메모리 부족으로 죽는 문제는 아직 안 풀었다. 지금은 모듈 끝에 한 번 도는 태스크로 미뤄 두었고, 발견이 늦는 비용을 받아들이고 있다.