feat(publish): settle a publish whose outcome is unknown from the publish page - #744
Conversation
ee71749 to
57fd9c8
Compare
|
#743 이 머지되어 main 위로 rebase 했습니다. 이제 순서: kpubdata-builder#1009 머지 → 이 PR 의 drift 잡 재실행 → 리뷰. 그 전에는 머지하지 않습니다. |
yeongseon
left a comment
There was a problem hiding this comment.
리뷰 (코멘트 — 승인은 builder#1009 가 main 에 들어가고 이 PR 의 드리프트 검사가 실제 계약과 대조된 뒤에).
diff 를 읽었습니다. 걸리는 로직은 없습니다.
- 어느 것도 자동으로 실행되지 않고, reset 은 두 번째 클릭과 경고 뒤에만 나갑니다. 테스트가 "누르기 전에는 fetch 0회" 를 확인합니다.
- 원격을 읽지 못한 503 과
confirmed에서는 재게시 버튼을 주지 않습니다. 재게시가 허용되는 것은absent·reset·nothing_to_settle뿐입니다. - 토큰은 헤더로만 가고, 다른 run 의 응답은 믿지 않고, Builder 의 오류 문장을 그대로 보여 주지 않습니다 — 각각 테스트가 있습니다.
승인을 미루는 이유 두 가지:
- 계약 대조가 아직 없습니다.
publishReconcileResponseSchema·publishReceiptResetSchema가 짝지을 계약 스키마는 builder#1009 가 처음 선언합니다. 그 PR 은 지금 충돌 상태이고 버전도 다시 잡아야 합니다. 본문대로 #743 이 먼저 들어가면 이 PR 은 그때까지 경로 검사에서 실패합니다. - 화면을 본 사람이 없습니다. 본문이 밝힌 대로 브라우저에서 확인하지 않았고 실제 Builder·Hugging Face 로도 돌리지 않았습니다. 기록을 지우는 동작이 있는 R3 화면이라, 머지 전에 한 번은 눈으로 봐야 합니다.
…lish page Closes #728 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
57fd9c8 to
fbcd696
Compare
…ares Builder's contract now declares the publish recovery routes (builder#1009), with named examples for their responses and errors. Map the two operations Studio does not call as skipped, check the reconcile and reset errors against recoveryFailure, and list the receipt 404 Studio never requests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
kpubdata-builder#1009 가 머지되어(계약 1.81.0) 이 PR 을 그 계약에 맞췄습니다 ( 지금 Studio 추가한 것:
확인: Builder R3 라 승인을 기다립니다. |
… test (#746) Refs kpubdata-lab/kpubdata-builder#994, #728 ## 문제 kpubdata-lab/kpubdata-builder#1009 가 Builder `main` 에 들어가면서 계약이 1.81.0 이 됐고, publish 복구 라우트 네 개(`getPublishReceipt`, `resetPublishReceipt`, `reconcilePublish`, `getPublishAudit`)가 이름 붙은 예시와 함께 선언됐다. Studio 의 `Builder contract drift` 잡은 Builder `main` 의 계약을 읽고, 파싱하지도 목록에 올리지도 않은 예시가 있으면 실패한다. 그래서 지금 `main` 기준의 모든 PR 에서 이 잡이 실패한다. #744 의 잡 로그(재실행, Builder 1.81.0)가 근거다: `no Studio schema for getPublishReceipt; map it in OPERATION_SCHEMAS`, 그리고 `ERROR_READERS` 에 없는 에러 예시 5개. 순서를 제가 잘못 잡았다 — builder#1023 때처럼 Studio 가 예시를 먼저 알고 있어야 했는데 builder#1009 를 먼저 머지했다. ## 변경 `src/shared/lib/contractDrift.test.ts` 만 바꾼다. Studio 는 `main` 에서 네 라우트 중 어느 것도 부르지 않는다. - `OPERATION_SCHEMAS`: 네 operation 을 `skip` 으로 (부르지 않으므로 파싱할 본문이 없다). - `ERROR_READERS`: 에러 예시 5개를 `notHandled` 로, `code`(`receipt_not_found`, `reconcile_unavailable`)와 `since: "1.81.0"` 과 함께. - CHANGELOG `[Unreleased]`. #728(#744)이 `reconcilePublish`·`resetPublishReceipt` 를 부르기 시작하면 그 둘의 `skip` 과 `notHandled` 항목을 실제 스키마·reader 로 바꿔야 한다. ## 검증 **로컬에서 테스트를 돌리지 못했다** — 이 환경에서 `npm ci` 가 실패한다. 이 PR 의 `Builder contract drift` 잡이 Builder `main`(1.81.0)으로 도는 것이 첫 실행이자 판정이다. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
|
kpubdata-builder#1009 를 머지했습니다 (계약 1.81.0). 그 결과 이 PR 뿐 아니라 이 PR 에서 할 일:
그 뒤 drift 가 녹색이면 다시 리뷰하겠습니다. 화면 확인은 여전히 필요합니다. |
…h-recovery # Conflicts: # src/shared/lib/contractDrift.test.ts
|
#746 이 머지되어
Builder |
…twork (#747) Refs #741 ## #745 의 추적이 잡은 것 #744 의 `Coverage gate` 잡에서 같은 테스트가 다시 실패했고, 이번에는 `[#741]` 블록이 찍혔다: ``` [#741] requests of the failed test, in order: 5ms fetch rejected GET /warehouse/tables: TypeError: fetch failed (cause: AggregateError); signal aborted: false 10ms msw request:start GET /datasets/air 10ms msw request:match GET /datasets/air 11ms msw response:mocked GET /datasets/air … 44ms msw request:unhandled GET /builds/run-2/spec 86ms fetch rejected GET /builds/run-2/spec: TypeError: fetch failed (cause: AggregateError); signal aborted: false 87ms fetch rejected GET /builds/run-2/stages: TypeError: fetch failed (cause: SocketError: closed); signal aborted: false ``` 핸들러가 등록된 `GET /warehouse/tables` 가 **`request:start` 없이** `fetch failed` 로 끝났다. #745 본문이 적은 세 갈래 중 "그 순간 요청이 MSW 를 거치지 않았다" 쪽이다. `AggregateError` 는 실제 `localhost` (::1, 127.0.0.1) 연결 시도가 실패할 때의 원인이다. ## 왜 그럴 수 있는가 (가설) - 이 MSW(3.0.2, `@mswjs/interceptors` 0.45.6)는 Node 에서 `fetch` 가 아니라 **소켓 수준**에서 가로챈다(`lib/node/net-*.js`). - 기본 전략 `onUnhandledFrame: "warn"` 은 경고 뒤 요청을 그대로 통과시킨다 — 실제 `localhost:8000` 에 연결한다. - 이 파일의 테스트들은 `/builds/{run}/spec`, `/stages` 등에 핸들러가 없어 매번 통과 요청을 만들고, 클라이언트가 그것을 0.5초·1초 뒤 재시도한다. 그 재시도가 다음 테스트 시작과 겹친다. - 통과된 실제 연결이 살아 있는 동안 같은 origin 으로 가는 다음 요청이 그 연결에 실려 MSW 를 거치지 않고 함께 실패한 것으로 본다. **마지막 단계는 확인하지 못했다.** 로컬에서는 재현되지 않는다: 미처리 요청과 처리 요청을 시점을 바꿔 가며 1,500회 겹쳐 봤지만 처리 요청은 한 번도 실패하지 않았다. 로컬의 `localhost` 는 IPv4 하나로만 풀리고(`[{"address":"127.0.0.1","family":4}]`) CI 는 두 주소를 시도한다는 차이가 있다. ## 변경 - `vitest.setup.ts`: 핸들러가 없는 요청을 전처럼 경고한 뒤, 통과시키지 않고 MSW 안에서 네트워크 오류로 끝낸다 (`onUnhandledFrame` 콜백이 `HttpResponse.error()` 를 던진다). 앱이 보는 결과는 같다 — `fetch` 가 reject 되고 연결 실패로 읽힌다. 달라지는 것은 테스트가 실제 소켓을 열지 않는다는 점뿐이다. - `__tests__/unhandledRequests.test.ts`: 실제 HTTP 서버를 띄워 놓고 핸들러 없는 요청을 보내, 요청이 reject 되고 경고가 나오고 **서버에는 아무것도 도착하지 않음**을 확인한다. - CHANGELOG. ## 검증 - 새 테스트는 고치기 전 설정에서 실패한다: `promise resolved "Response { status: 200 … }" instead of rejecting` — 요청이 실제 서버에 닿았다. - `npx vitest run` 전체 → `Test Files 237 passed (237)`, `Tests 2391 passed | 12 skipped (2408)`. (건너뛴 것은 계약 없이 돌린 drift 테스트다.) 미처리 요청이 연결 실패로 끝난다는 데 기대던 테스트는 그대로 통과한다. - `npx tsc --noEmit`, eslint 출력 없음. ## 알아 둘 것 - 이것이 flake 를 끝내는지는 증명되지 않았다. #741 은 열어 두고, #745 의 추적도 그대로 둔다 — 다시 나면 로그가 말해 준다. - 이 PR 의 `Builder contract drift` 잡은 #744 가 머지될 때까지 실패한다(Builder 계약 1.81.0, #744 코멘트 참고). 이 변경과는 무관하다. - `__tests__/builderApi.e2e.test.ts` 는 서버를 닫았다가 `"warn"` 으로 다시 여는 곳이 있다. 건드리지 않았다. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Eomdahyeon <213566566+Eomdahyeon@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
yeongseon
left a comment
There was a problem hiding this comment.
다시 읽었습니다. 승인합니다.
main을 합치면서 #746 의 임시 항목을 예고대로 바꿨습니다:reconcilePublish·resetPublishReceipt는 실제 스키마 대조와recoveryFailurereader 로,getPublishReceipt·getPublishAudit은 부르지 않으므로skip/not-handled 그대로.- 첫 번째 조건(계약 대조)은 충족됐습니다 —
Builder contract drift가 Buildermain(1.81.0)으로 녹색입니다. - 두 번째 조건(화면을 눈으로 보는 것)은 충족되지 않았습니다. 저도 이 환경에서 Studio 를 띄울 수 없습니다. 그래도 머지하는 쪽으로 판단했습니다: 패널의 동작 — 누르기 전에는 아무것도 부르지 않음, reset 은 두 번 클릭, 503·confirmed 에서는 재게시 버튼 없음 — 은 컴포넌트를 실제로 렌더하는 테스트가 고정하고 있고, 눈으로만 잡을 수 있는 것은 배치·문구의 어색함이지 잘못된 동작이 아닙니다. 화면 확인은 후속 이슈로 남깁니다.
Closes #749 ## 문제 게시 자격 증명을 요청에서 받는 배포(`publish_credential: request`)에서 복구 패널의 **원격 확인**은 토큰이 다시 필요하다. 패널은 "위에 토큰을 다시 입력하라"고 안내했는데, 페이지의 토큰 칸은 값이 바뀌면 게시 폼을 처음으로 되돌린다(`setConfirmation(undefined); publish.reset()`). 패널은 바로 그 두 상태에 걸려 있어서, 안내를 따르면 패널이 사라지고 남는 것은 원격을 보지 않는 **기록 초기화**뿐이었다. #744 에서 제가 넣은 결함이다. ## 변경 - `features/publish/PublishRecoveryPanel.tsx` - `needsCredential` 일 때 패널 안에 자체 토큰 칸을 둔다. 페이지의 토큰과 같은 규칙이다: 메모리에만 두고, 원격 확인 한 번에 실어 보낸 뒤 바로 비운다. 헤더에 실을 수 없는 값이면 버튼을 막고 보내지 않는다. - **기록 초기화는 토큰을 보내지도 소모하지도 않는다.** Builder 의 reset 은 원격을 읽지 않는다(`publish_api.py` 의 `reset_publish_receipt` 본문에 자격 증명을 쓰는 곳이 없다 — 코드를 읽어 확인했고 실제 Builder 로 돌려 보지는 않았다). - `takeCredential` prop 은 없앴다. - `pages/BuildPublishPage.tsx`: `takePublishCredential` 제거. 페이지의 토큰 칸과 게시 흐름은 그대로다. - 문구(ko/en): 패널 토큰 칸의 라벨·설명을 더하고, 안내를 "바로 위 칸에 입력"으로 고쳤다. - CHANGELOG. 이슈가 제시한 두 방법 중 "패널에 자체 토큰 칸"을 골랐다. 다른 쪽(토큰이 바뀌어도 `publish_state_unknown` 상태 유지)은 페이지 토큰 칸의 기존 규칙 — 토큰이 바뀌면 준비 상태를 다시 확인한다(#615) — 과 얽힌다. ## 테스트 (`__tests__/publishRecovery.test.tsx`) - 페이지 수준(이슈가 요구한 것): 게시 → `publish_state_unknown` → 토큰 없이 원격 확인은 나가지 않고 안내만 → **패널의 칸에 토큰 입력 후에도 패널이 남아 있음** → 원격 확인이 그 토큰을 헤더에, destination 을 본문에 실어 한 번 나감 → `confirmed` 표시, 재게시 버튼 없음, 토큰은 storage 에 없음. - 패널: 자격 증명을 서버가 가진 배포에서는 칸이 없다 / 토큰은 한 요청 뒤 비워지고 두 번째 확인은 토큰 없이 나가지 않는다 / 형식이 틀린 토큰은 보내지 않는다 / reset 은 헤더 없이 `DELETE` 로 나가고 입력한 토큰은 칸에 남는다. ## 검증 - `npx vitest run __tests__/publishRecovery.test.tsx` → `Tests 18 passed (18)`. - publish·locale 관련 8개 파일 → `Tests 131 passed (131)`. - `npx tsc --noEmit`, eslint 출력 없음. - 전체 스위트는 로컬에서 돌리지 않았다 — CI 에 맡긴다. - **브라우저에서 보지 않았다.** 새 칸의 배치와 문구는 #748 의 화면 확인 범위에 들어간다. ## 남는 것 사용자가 복구 패널이 떠 있는 동안 **페이지의** 토큰 칸이나 destination 을 바꾸면 패널은 여전히 사라진다(게시 폼을 처음으로 되돌리는 기존 동작). 기록은 Builder 에 남아 있으므로 같은 게시를 다시 실행하면 `publish_state_unknown` 과 함께 패널이 돌아오지만, 그 경로를 테스트로 고정하지는 않았다. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Eomdahyeon <213566566+Eomdahyeon@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Yeongseon Choe <yeongseon.choe@gmail.com>
Closes #728
문제
publish 가
publish_state_unknown으로 끝나면 Studio 는 "자동 재시도하지 마세요, 운영자에게 요청하세요"만 보여 준다. Builder 에는 복구 경로(reconcile, receiptreset)가 있지만 Studio 가 부르지 않는다. 사용자가 UI 에서 할 수 있는 일이 없다.변경 내용
builderApi.reconcilePublish(POST …/publish/reconcile),builderApi.resetPublishReceipt(DELETE …/publish/receipt?target&destination). 둘 다 재시도 없음. 자격 증명은X-Publish-Credential헤더로만.publishReconcileResponseSchema,publishReceiptResetSchema— 계약의PublishReconcileResponse,PublishReceiptReset와 이름으로 짝지어진다.PublishRecoveryPanel),publish_state_unknown일 때만:receipt_not_found) 정리할 것이 없다고 말한다.EXPECTED_OPERATIONS에 두 호출 추가.의존 — kpubdata-lab/kpubdata-builder#1009
네 라우트는 Builder 에서 1.19/1.20 부터 응답해 왔지만 계약
paths에는 #1009 가 처음 선언한다. 그 PR 이 Buildermain에 들어가기 전까지:#1009 의 계약 파일로 드리프트 검사를 로컬에서 돌려 봤다: 스키마 대조 461개 통과. 실패한 2개는 계약 파일만 복사하고 fixture 디렉터리를 같이 두지 않아서 난 "fixture 가 옆에 있는가" 검사다 — 스키마와는 무관하다.
검증
__tests__/publishRecovery.test.tsx(16개): 요청의 method·path·body·헤더, 토큰이 URL·body 에 없음, 404/503 분류와 재시도 없음, 다른 run 의 응답 불신, Builder 오류 문구를 그대로 보여 주지 않음, 패널이 누르기 전에는 아무것도 부르지 않음, reset 의 두 단계, 토큰 없이는 원격 확인을 하지 않음.npx vitest run:238 files passed, 2405 passed | 9 skipped.tsc --noEmit, eslint 통과.review:R3— 작성자가 아닌 사람의 승인이 필요하다.🤖 Generated with Claude Code