fix: 분리 검수 2라운드 — 중복 결제(대사 게이트·탭 갱신)·billing=null 원인 분리·codex MCP 안내/권한 - #263
Open
beyondworks wants to merge 7 commits into
Open
fix: 분리 검수 2라운드 — 중복 결제(대사 게이트·탭 갱신)·billing=null 원인 분리·codex MCP 안내/권한#263beyondworks wants to merge 7 commits into
beyondworks wants to merge 7 commits into
Conversation
분리 검수(다른 컨텍스트)가 찾은 것들이다. 자기 검수로는 못 잡았다. MED-1 (중복 결제 유인): bill=null이면 plan이 기기 스코프 sync.plan으로 폴백하는데 (acctPlan ?? sync?.plan), /api/me/billing이 null을 주는 순간 !bill?.hasSub가 무조건 참이 돼 **실구독자에게도** '미리 구독하세요' + 결제 버튼이 떴다. bill 존재를 요구한다. 구조 테스트를 이 형태로 갱신하고 변이로 재확인(bill 가드 제거 시 red). 범위 밖→포함 (v0.1.41 유입): 크루 카드의 mcp 범위 지정이 codex 주입에 안 걸렸다. 안내 목록(cliMcp)만 거르고 실제 주입(cliMcpServers)은 allMcp를 그대로 받아, 범위를 좁혀도 회사의 모든 서버가 config.toml에 실렸다. SDK 경로와 같은 규칙으로 통일하고 회귀 테스트 추가(변이 확인: 필터 되돌리면 red). HIGH-2: 데크의 'deck.budgetStop'도 사라진 설정 화면을 가리켰다(#262는 src만 고쳤다). LOW: settings.budget 死키 3개 제거. settings 재조회의 lang 의존 제거 — cmd+/ 언어 전환이 입력 중이던 회사 이름을 서버값으로 되돌렸다. 검증: 테스트 895건 fail 0, 빌드 통과, 변이 게이트 2건 확인. 발행된 v0.1.42는 743b84d에서 빌드됐다 — 이 수정은 바이너리에 없다(v0.1.43 대상). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
beyondworks
force-pushed
the
fix/review-followups-v0143
branch
from
August 19, 2026 00:49
edd349c to
1108558
Compare
분리 검수(다른 컨텍스트) HIGH-1. `bill &&` 가드는 bill이 **없을 때**만 막는다. 정작 위험한 창은 bill이 있는데 hasSub가 false로 굳는 쪽이었고, 부여 Pro (그랜드파더링 44계정: plan=pro·ends_at=null·구독 식별자 없음)가 거기 갇혀 있었다. 기전: 유실 대사 게이트가 `if (proRowActive(cur)) return false`인데 proRowActive는 구독 유무를 안 본다. 부여 Pro는 항상 참 → 대사가 영구히 꺼짐 → 결제 후 웹훅 1건만 유실돼도(LS 재시도는 3회·155초가 전부) ls_subscription_id가 영영 null → hasSub=false → 설정 카드가 이미 결제한 사람에게 "미리 구독하세요 + 결제 버튼"을 계속 보여준다. intent(clearReconcileEmpty)를 눌러도 풀리는 건 empty 게이트뿐이라 이 코호트엔 무효였다. 게이트 판정을 `reconcileUnneeded`(= 유효 pro **그리고** 구독 식별자 존재)로 분리했다. 자격 판정은 여전히 proRowActive 하나만 쓴다 — is_pro(DB)·fetchPlan(sync)과 갈리면 잠금/복구 비대칭이 생긴다는 기존 제약 그대로다. 방향 확인: 좁히기라 2026-07-30 HIGH(원시 plan==='pro'를 믿어 만료 pro의 복구가 꺼지던 것)와 같은 편이다 — 대사가 도는 집합은 늘어나기만 한다. 강등 위험 없음: 대사는 승격 전용 (lsreconcile "entitlements를 free로 내리는 일은 하지 않는다"). 호출량은 쿨다운 2컬럼 (시도 10분·부정확정 24시간)이 그대로 막아, 44계정이 창당 1회 더 조회하는 게 전부다. 검증: 행동 테스트 추가(소스 문자열 앵커 아님) — 부여 Pro는 자격 유효이면서 대사 적격, 구독 붙은 pro는 여전히 LS 미조회, 만료 pro는 구독 유무 무관 적격. 변이 게이트 확인 (`&& ls_subscription_id` 제거 시 fail 1). billing 계열 63건 pass·fail 0. 미검증: 라이브 아님 — 실계정·실웹훅 유실 재현은 안 했다(코드·테스트 근거). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
분리 검수(다른 컨텍스트) MED-A/MED-B. /api/me/billing이 서로 다른 네 원인
(클라우드 미구성·미로그인/게스트·조회 수단 없음·조회 예외)을 전부 같은 billing:null로
뭉개고, 클라이언트는 1회성 fetch에 .catch(() => {})라 재시도도 없었다.
그래서 일시 장애가 "계정 없음"으로 읽혔다:
- plan이 기기 스코프 sync.plan으로 폴백해 **남의 Pro 배지**가 뜨고(2026-08-05 실사고와
같은 계열이 null 경로로 살아 있었다), free 분기가 죽어 업그레이드 버튼까지 사라진다.
- 앱의 유일한 체크아웃 표면(SyncCard)이 통째로 없어져, 낼 의사가 있어도 못 낸다.
- 한 번 실패하면 페이지를 다시 열기 전엔 복구되지 않았다.
라우트가 reason을 내린다 — 'no-cloud' | 'unauthenticated'는 계정이 **없는** 정상 상태라
기기값 폴백이 옳고, 'unavailable'은 계정이 있을 수 있으므로 메우지 않고 화면이 "못
불러왔다"고 말한다(무음 소실 금지). 클라이언트 fetch 실패도 unavailable로 친다.
MED-B(불변식이 한 분기에만 걸려 있던 것)도 같이 닫는다: `bill &&` 요구가 granted 분기에만
있어 paywalled·free 갈래는 여전히 기기값으로 결제 CTA를 그렸다(공용 PC에서 실구독자에게
두 번째 결제를 권하는 경로). 이제 조회 실패면 세 갈래 모두 결제 문구 대신 재시도 줄이 뜬다.
billingCta 배선은 하지 않았다 — 7상태 중 granted·manage·past-due·trial을 이 환경에서
띄울 수 없어(실계정·실구독 필요), 관측 못 하는 4상태의 렌더를 한꺼번에 바꾸는 건 검수가
지적한 위험을 더 키운다. 결정표는 그대로 두고 배선은 실계정 관측이 가능할 때.
검증(격리 dev :3199 + 임시 ARGO_ROOT, 실데이터·상주 :3001 미접촉):
- 라우트가 세 reason을 실제로 내림 — curl로 no-cloud 확인, unavailable 강제 시 응답 확인.
- 브라우저 실측(aside): unavailable 상태에서 동기화 카드에 안내+"다시 시도"만 뜨고 결제
버튼은 없다. 한국어·영어 양쪽 확인(스크린샷).
- "다시 시도" 클릭 → 재조회 → 안내 사라짐(리로드 없이). 클릭 전 true / 클릭 후 false 관측.
- 테스트 896건 fail 0, next build 통과.
- 구조 테스트의 리터럴 앵커가 이 정정에 거짓 red를 냈다(검수 MED-D가 예고한 형태) →
문장 리터럴 대신 그 문장이 지켜야 할 두 성질(acctPlan 우선·billLost 가드)로 교체하고
라우트의 reason 계약도 함께 잠갔다. 변이 확인: 가드 제거 시 red.
미검증: 실계정·실구독·기기 세션 실패 재현은 안 했다(강제 주입으로 상태만 재현).
설치본 웹뷰·윈도우·리눅스 축 미실측.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
분리 검수(다른 컨텍스트) HIGH-2. 체크아웃은 target=_blank라 결제가 **다른 탭**에서 끝나는데, billing은 폴링이 없고(마운트 시 1회) 유일한 재조회 신호인 reconciling 8초 재시도는 대사가 발사됐을 때만 뜬다. 부여 Pro는 대사 자체가 안 돌던 코호트라(직전 커밋에서 해소) 결제를 마치고 돌아와도 화면에 "미리 구독하세요 + 결제 버튼"이 그대로 남았다. 한 번 더 누르면 LS는 같은 이메일로 두 번째 구독을 만든다 — 대사의 중복 귀속 가드는 귀속만 막지 청구는 못 막는다. "탭으로 돌아옴"(visibilitychange·focus)을 재조회 신호로 쓴다. 두 이벤트가 같이 튀고 창을 오갈 때마다 LS 대사까지 딸려 가면 안 되므로 5초 안의 중복은 접는다(대사엔 DB 쿨다운이 별도). 검증(격리 dev :3199 + 임시 ARGO_ROOT — 실데이터·상주 :3001 미접촉, 브라우저 실측): billing 요청 수를 세어 복귀 1회 → 1건, 5초 내 재발화 → 그대로 1건, 쿨다운 후 → 2건. 트립와이어 테스트 추가(리스너와 재조회 트리거·effect 의존을 잠금), 변이 확인: 리스너 제거 시 red. 897건 fail 0, next build 통과. 주의(자기 기록): 처음 build가 /auth/signout 프리렌더 오류로 깨진 건 코드가 아니라 dev 서버와 .next를 공유한 탓이다 — 서버 정지 후 .next 삭제하고 재빌드해 통과. 격리 서버를 띄운 채로는 빌드하지 말 것. 미검증: 실결제·실구독 계정으로 "결제 → 복귀 → CTA 사라짐"의 전체 왕복은 재현하지 않았다 (재조회가 도는 것까지만 관측). 설치본 웹뷰·윈도우·리눅스 축 미실측. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
분리 검수(다른 컨텍스트) MED-C·MED-2·LOW. MED-C — codex는 v0.1.41부터 config.toml로 실제 MCP를 받는다. 그런데 크루 카드와 마켓은 여전히 "CLI 러너에서는 MCP가 실행되지 않는다 — SDK 러너로 바꾸라"고 말했다. 직전 커밋에서 크루별 `mcp:` 범위 지정을 codex 주입에도 걸어 놓고, 바로 그 UI 밑에 "어차피 안 돈다"가 함께 뜨는 상태였다 — "안내와 실제가 갈리면 안내가 거짓"이라는 그 커밋의 논지가 정면으로 깨진다. 판정을 kind가 아니라 **MCP를 실제로 받는가**로 바꿨다. 카탈로그에 `mcp: true`(codex)를 두고 /api/runners가 실어 나른다. 조건은 `kind === 'cli' && !r.mcp` — gemini·antigravity는 벤더 비대화 경로가 MCP를 안 받으므로 경고가 그대로 뜬다(러너 중립성: 못 하는 건 화면에 정직하게). 문구도 Codex를 막힌 쪽에서 빼고 다시 썼다(ko·en). MED-2 — 범위를 통과한 MCP의 env 토큰이 config.toml에 평문으로 실리는데 기본 모드로 쓰였다. mcp.json을 0600으로 쓰는 것과 같은 근거(PR #258)인데 codex 쪽만 빠져 있어 같은 비밀이 더 느슨하게 복제됐다. config.toml 0600 + 베이스 codex-home 0700(턴 홈 mkdtemp와 같은 등급). Windows는 POSIX 모드가 없어 미적용 — mcp.json과 같은 한계이고 테스트에 사유를 적었다. LOW — 살균 후 이름 충돌 방어. 'my.tool'과 'my tool'이 함께 오면 [mcp_servers.my_tool]이 두 번 찍혀 TOML 파싱이 깨진다 = 이 함수가 피하려던 "턴 전체 사망"의 재현. 뒤엣것을 빼고 로그로 남긴다. 검증: - /api/runners 실응답 확인(격리 :3199) — codex mcp=true, gemini·antigravity·SDK 전부 false. - 마켓 배너 실렌더 확인 — 옛 문구("CLI 러너(Codex…") 사라짐, 새 문구 노출, Codex가 막힌 목록에서 빠짐. - 테스트 2건 추가(0600 실측·중복 테이블 부재), 변이 확인: 두 가드 제거 시 각각 red. - 899건 fail 0, next build 통과. 미검증: codex 실턴으로 config.toml이 실제로 먹히는지는 이 세션에서 안 돌렸다(파일 생성·권한· 내용까지만). 크루 카드 경고의 화면 확인은 CLI 러너 크루 + MCP 설치 픽스처가 필요해 미실시 — 조건이 읽는 데이터(r.mcp)까지만 라이브 확인. Windows 축 미실측. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
분리 검수(다른 컨텍스트) MED-D. 앞선 두 테스트가 행동이 아니라 코드 텍스트를 잠그고 있었다: 식별자 이름(`scoped`)과 줄바꿈·들여쓰기 배치(`\s*\n\s*\?`)까지 고정해, 잡아야 할 결함은 못 보면서 정당한 리팩터에는 거짓 red를 냈다. 실제로 이번 배치에서 한 번 터졌다(billing-plan-scope의 리터럴 앵커가 MED-A 정정에 red). `scopeServers(servers, scope)`를 persona.mjs에 순수 함수로 뽑고 SDK 턴·codex 주입 두 곳이 그 하나를 쓴다. parseScopeList의 반환 계약(null=전부, []=아무것도, 목록=지정)이 그대로 이어진다. 테스트는 값으로 잠근다 — 계약 4종, 범위 밖 서버의 **정의(env 토큰)까지** 빠지는지, 원본 맵 불변(같은 맵을 두 경로가 이어 쓴다). 남긴 소스 검사는 "호출부가 정확히 두 곳이고 chat.mjs가 필터를 다시 구현하지 않는다" 하나뿐 — 사본이 갈리는 것이 이 결함의 본질이라 그것만 잠근다. 변이 2종 확인: 주입 필터 되돌림 → red, none([])을 전부 통과로 → red. billing-cta의 구조 검사도 고쳤다: SyncCard 조각을 다음 최상위 function까지로 끊는다. 안 끊으면 OFF 갈래 조각에 UpgradeButtons 정의 자체가 딸려 들어와 버튼 단언이 **항상 참**이었다(검수가 짚은 그대로). 끊겼는지도 함께 단언한다. 검증: 900건 fail 0, next build 통과, 변경 파일 eslint 0. (레포 전체 lint는 .claude/worktrees의 Tauri 빌드 산출물 2건에서 파싱 에러가 나는데 추적본이 아니고 이 변경과 무관하다.) 미검증: codex 실턴은 안 돌렸다 — 헬퍼 동치성은 값 테스트로만 확인했다. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
유건 지시 2026-08-19(스크린샷 3장). 기본 러너가 한 줄짜리 카드로 AI 섹션에 따로 떠 있는 동안 회사 정보 카드는 이름 입력 하나만 두고 아래가 비어 있었고, 기기 간 동기화 카드는 내용이 세로로 긴데 340px 트랙 하나에 갇혀 있었다. - 기본 러너: 자체 `.card` 껍데기를 벗고 **필드**가 된다 — 회사 이름과 같은 리듬 (마이크로라벨 + 컨트롤)으로 회사 정보 카드 안에 들어간다. 즉시 저장이라 그 카드의 submit(회사 이름 저장)과 무관하고, 연결된 러너가 2개 미만이면 여전히 스스로 안 그린다. - 기기 간 동기화: `gridColumn: '1 / -1'` — 위험 구역 카드와 같은 방식으로 열 전체를 쓴다. 검증(격리 dev :3199 + 임시 ARGO_ROOT — 실데이터·상주 :3001 미접촉, 브라우저 실측): 회사 정보 카드 안에 기본 러너 렌더 확인, AI 섹션의 낱개 카드 사라짐 확인, 폭 측정 동기화 1060px(컨테이너 전체) vs 일반 카드 344px. 스크린샷 2장. 900건 fail 0, 빌드 통과. 미검증: 좁은 폭(모바일) 축 — aside가 뷰포트를 강제하지 못하고 대체 경로의 폭 측정이 0으로 나와 확인하지 못했다. 동기화 카드가 **켜진** 상태의 와이드 렌더도 미확인(이 픽스처는 클라우드 미설정이라 꺼짐 상태만 뜬다) — 켜진 상태에선 본문이 왼쪽에 몰릴 수 있다. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
분리 검수(다른 컨텍스트)가 찾은 것들이다. 자기 검수로는 못 잡았다.
2라운드로 진행됐다. 1라운드 수정(
1108558)을 다시 분리 검수 2건(정합성/회귀 · 결제/신뢰경계)에 걸었더니 그 수정 자체에서 HIGH 2건이 나왔다. 같은 계열 갭은 한 번에 안 닫힌다는 이 레포의 실측이 또 맞았다.1라운드 —
1108558MED-1 · 중복 결제 유인 —
bill=null이면plan이 기기 스코프sync.plan으로 폴백(acctPlan ?? sync?.plan)하는데,/api/me/billing이 null을 주는 순간!bill?.hasSub가 무조건 참이 돼 실구독자에게도 "미리 구독하세요" + 결제 버튼이 떴다. →bill &&요구.크루 MCP 범위 무력화 (v0.1.41 유입) — 카드의
mcp:범위가 안내 목록만 거르고 실제 주입은 안 걸러, 범위를 좁혀도 codex 크루가 회사의 모든 서버를config.toml로 받았다. SDK 경로와 동일 규칙으로 통일.HIGH-2 — 데크의
deck.budgetStop도 제거된 설정 화면을 가리켰다(#262는src/*.mjs만 고쳤다).LOW —
settings.budget死키 3개 제거 / settings 재조회의lang의존 제거(cmd+/전환이 입력 중이던 회사 이름을 되돌렸다).2라운드 — 1라운드 수정을 검수해서 나온 것
819659f· HIGH-1 —bill &&는 정작 위험한 창을 못 막았다bill &&가드는 bill이 없을 때만 막는다. 실제 위험은 bill이 있는데hasSub가 false로 굳는 쪽이고, 부여 Pro 44계정이 거기 갇혀 있었다.유실 대사 게이트가
if (proRowActive(cur)) return false인데proRowActive는 구독 유무를 안 본다. 부여 Pro(plan=pro·ends_at=null·구독 식별자 없음)는 항상 참 → 대사가 영구히 꺼짐 → 결제 후 웹훅 1건만 유실돼도(LS 재시도는 3회·155초가 전부)hasSub=false가 굳어 이미 낸 사람에게 결제 카드가 계속 뜬다.게이트 판정을
reconcileUnneeded(유효 pro 그리고 구독 식별자 존재)로 분리. 자격 판정은 여전히proRowActive하나만 쓴다. 좁히는 방향이라 2026-07-30 검수가 잠근 "만료 pro 복구"와 같은 편이고, 대사는 승격 전용이라 강등 위험 없음. 호출량은 쿨다운 2컬럼이 그대로 막는다.88fa853· MED-A/MED-B —billing=null이 네 원인을 뭉갰다클라우드 미구성 · 미로그인/게스트 · 조회 수단 없음 · 조회 예외가 전부 같은 null이라, 일시 장애가 "계정 없음"으로 읽혔다 → 기기 스코프 폴백으로 남의 Pro 배지가 뜨고(2026-08-05 실사고와 같은 계열) 앱의 유일한 체크아웃 표면이 조용히 사라졌다. 클라이언트는 1회성 fetch에
.catch(() => {})라 재시도도 없었다.라우트가
reason을 내린다 —no-cloud·unauthenticated는 계정이 없는 정상 상태라 기기값 폴백이 옳고,unavailable은 계정이 있을 수 있으므로 메우지 않고 화면이 "못 불러왔다"고 말한다.MED-B도 같이:
bill &&요구가 granted 분기에만 있어 paywalled·free 갈래는 여전히 기기값으로 결제 CTA를 그렸다(공용 PC에서 실구독자에게 두 번째 결제를 권하는 경로).4c139db· HIGH-2 — 결제하고 돌아온 탭이 갱신 안 됐다체크아웃은
target=_blank라 결제가 다른 탭에서 끝나는데 billing은 폴링이 없다. 부여 Pro는 대사가 안 돌던 코호트라 돌아와도 결제 버튼이 그대로 남았고, 재클릭이 두 번째 구독을 만든다(대사의 중복 귀속 가드는 귀속만 막지 청구는 못 막는다). 탭 복귀(visibilitychange·focus)를 재조회 신호로, 5초 내 중복은 접는다.628fc5f· MED-C/MED-2 — 안내가 거짓, 토큰이 느슨하게 저장codex는 v0.1.41부터 실제로 MCP를 받는데 화면은 "CLI 러너에서는 실행되지 않는다"고 말했다 — 이 PR 1라운드가 범위 지정을 codex 주입에 걸어 놓고 바로 그 UI 밑에 "어차피 안 돈다"가 뜨는 상태였다. 판정을
kind가 아니라 MCP를 실제로 받는가(r.mcp)로 바꿨다.config.toml에 MCP env 토큰이 평문으로 실리는데 기본 모드로 쓰였다(mcp.json은 #258로 0600). → 0600 + 베이스codex-home0700. Windows는 POSIX 모드가 없어 미적용(테스트에 사유 명시). 덤으로 살균 후 이름 충돌 시 TOML 중복 테이블 → 턴 사망을 막는다.87220cd· MED-D — 테스트가 행동이 아니라 코드 텍스트를 잠그고 있었다식별자 이름과 줄바꿈 배치까지 고정해 정당한 리팩터에 거짓 red를 냈다(실제로 이번 배치에서 터졌다).
scopeServers()순수 헬퍼로 뽑고 값으로 잠근다.billing-cta의 구조 검사는 SyncCard 조각이 안 끊겨 버튼 단언이 항상 참이던 것도 고쳤다.검증
테스트 900건 fail 0(시작 시 896) ·
next build통과 · 변경 파일 eslint 0 · 새 가드 6종 각각 변이 확인(되돌리면 red).브라우저 실측 (격리 dev :3199 + 임시
ARGO_ROOT— 실데이터·상주 :3001 미접촉):reason을 실제로 내림/api/runners에 codexmcp=true, 나머지false/ 마켓 배너 새 문구 노출미검증 (미완으로 취급)
실계정·실구독·실웹훅 유실 재현 없음(상태만 강제 주입) · codex 실턴 미실행 · 크루 카드 경고 화면은 픽스처 부재로 데이터까지만 · Windows·리눅스·설치본 웹뷰 축 미실측.
범위에서 뺀 것
billingCta배선. 7상태 중 4개(granted·manage·past-due·trial)를 이 환경에서 띄울 수 없어, 관측 못 하는 상태의 렌더를 한꺼번에 바꾸는 게 더 위험하다고 판단했다. 결정표는 그대로 두고entitlement.mjs주석의 "DOM 실측 가능할 때" 조건을 유지.남은 LOW (v0.1.43 범위 판단 필요)
예산 강제는 살아 있고 편집 UI만 사라진 건 · 범위 이름 대소문자 무음 실패 · 422 로그가 정작 422 낼 값을 안 남김 · 가격 표기 불일치(버튼
$12vsentitlement.mjs주석$16) · 포털 폴백 막다른 길 · Settings의lang미사용 변수.중요: 발행된 v0.1.42는
743b84d에서 빌드됐다 — #261·#262와 이 PR은 바이너리에 없다. 다음 발행(v0.1.43)에 실린다.🤖 Generated with Claude Code