Skip to content

test(api): pin the interrupted run id on the async submit path - #1037

Merged
Eomdahyeon merged 1 commit into
mainfrom
test/issue-1025-async-resubmission
Oct 5, 2026
Merged

Eomdahyeon merged 1 commit into
mainfrom
test/issue-1025-async-resubmission

Conversation

@Eomdahyeon

Copy link
Copy Markdown
Collaborator

Closes #1025

#1027 이 check_existing_run_access 가 제출 기록을 보게 했고, #1008 이 비동기 POST /builds 를 그 가드로 연결했다. 이슈가 적은 경로(다른 사용자가 중단된 run 의 id 로 비동기 제출)는 그 둘이 합쳐져야 닫히므로, #1027 은 Refs 로 두고 이 테스트를 #1008 뒤로 미뤘었다. 코드 변경은 없다.

테스트 (tests/unit/test_interrupted_run_status.py)

  • 다른 사용자가 중단된 run id 로 POST /builds → 403 forbidden: not run owner. 레지스트리에 작업이 생기지 않고, 그 사용자는 여전히 그 run 의 이벤트를 404 로 받으며, 원래 제출자가 읽는 이벤트에 두 번째 run_submitted 가 없고 제출 기록의 소유자가 그대로다.
  • 제출자 본인은 같은 id 로 다시 제출할 수 있다 (202) — 중단된 run 의 메시지가 안내하는 동작.

검증

  • 가드의 제출 기록 확인을 임시로 꺼 두고 돌리면 새 테스트가 실패한다: FAILED …::test_another_user_cannot_submit_under_the_interrupted_run_id (동기 경로 테스트와 함께 2 failed, 9 passed). 되돌린 뒤 통과.
  • pytest tests/unit/test_interrupted_run_status.py tests/unit/test_json_array_reader.py 를 5회 → 5회 모두 28 passed. (이 fixture 가 한 번 메모리 측정 테스트를 건드린 적이 있어 함께 돌렸다.)
  • 처음 쓴 단언 하나는 버렸다: 제출 전후의 이벤트 목록 전체를 비교했더니 3회 모두 실패했다. 테스트의 "첫 프로세스" 워커가 gate 에 닿기 전까지 자기 이벤트를 계속 쓰기 때문이다 — 실제 재시작에서는 죽었을 스레드다. 그래서 run_submitted 개수와 소유자만 본다.
  • ruff check tests, ruff format 통과. 전체 스위트는 CI 에 맡긴다.

CHANGELOG 의 #1025 항목에서 "비동기 경로는 #1008 뒤에" 라는 문장을 지금 사실로 고쳤다.

🤖 Generated with Claude Code

#1027 made check_existing_run_access read the submission record, and #1008
routed POST /builds through that guard. Pin the two together: another user's
POST /builds under an interrupted run id is refused and records nothing, and
the submitter can submit it again.

Closes #1025

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Eomdahyeon
Eomdahyeon merged commit f61c9e2 into main Oct 5, 2026
19 checks passed
yeongseon pushed a commit that referenced this pull request Oct 5, 2026
Refs #1042 — 1부(재사용 거절). `retry_of` 는 이슈의 Notes 에 적은 제안을 확인받은 뒤 따로 올린다.

## 왜

kpubdata#812 §3: **한 `run_id` 는 한 번의 시도다.** 종료 이벤트가 붙으면 바뀌지 않고, 재시도는 새
`run_id` 를 받는다. 그 기록이 직접 짚었듯 제가 넣은 #1027 과 #1037 이 반대로 동작한다 — 재시작으로 중단된
run 의 제출자가 같은 id 로 다시 빌드할 수 있었고, 그러면 두 번째 시도의 이벤트가 첫 시도의 `run_failed` 뒤에
붙어 서로의 종료가 상대의 상태로 읽힌다.

## 변경

- `routes/_guards.py` `check_existing_run_access`: 매니페스트도 레지스트리 항목도 없는데
제출 기록이 있는 id 는 **제출자 본인에게도** 거절한다. 다른 사용자는 전처럼 403.
- `POST /builds` → **409** `{"error": "run_id already ended; submit the
retry under a new run_id", "run_id": …}`. 완료된 run 의 id 가 이미 받는 상태 코드다.
  - `POST /build` → **400**, 같은 본문.
- 중단된 run 의 메시지: "…; submit it again **under a new run_id**".
- 레지스트리가 아직 들고 있는 작업(대기·실행 중, 매니페스트 없이 실패한 것)은 그대로다 — 그 id 를 대면 그 작업을
돌려준다(ADR 0008).
- 계약 1.83.0 (additive): 두 라우트의 설명에 이 경우를 적었다. 이름 붙은 예시는 더하지 않았다 — Studio
의 drift 검사에 새 항목이 필요 없다.

## `POST /build` 가 409 가 아니라 400 인 이유

처음에는 두 라우트 모두 409 로 했다. `check_contract_compat.py` 가 막았다:

```
error: the change breaks existing clients, which needs a MAJOR version raise …
  - POST /build 409: $ref #/components/schemas/BuildSuccessResponse -> None
```

그 라우트의 409 는 "빌드는 됐지만 테이블이 커밋되지 않음"(#788)의 **빌드 응답**으로 선언돼 있어서, 거기에 오류
본문을 섞으면 409 를 빌드 응답으로 파싱하는 클라이언트가 깨진다. 그래서 그 라우트에서는 이미 `Error` 본문으로 선언된
400 을 쓴다. 상태 코드가 두 라우트에서 다른 것은 깔끔하지 않다 — 다른 선택(예: `POST /build` 의 409 를
major 로 바꾸기)이 맞다고 보시면 말씀해 달라.

## 테스트 (`tests/unit/test_interrupted_run_status.py`)

**예전 동작을 고정하던 두 테스트를 바꿨다** —
`test_the_submitter_can_submit_the_interrupted_run_again`(202 기대)와
`test_the_submitter_may_use_the_interrupted_run_id_again`(가드 통과 기대). 결정이
뒤집은 바로 그 동작이다.

- 제출자가 같은 id 로 `POST /builds` → 409, 작업이 생기지 않고, 그 run 은 여전히 `failed` /
`credentials_required` 로 읽히고, `run_submitted` 이벤트는 1개다.
- `POST /build` → 400, 같은 본문.
- **새 id 로는 202** — 재시도 자체는 된다.
- 중단 메시지가 "under a new run_id" 로 끝난다.
- 다른 사용자의 403, 제출 기록이 없는 id 의 통과는 그대로다(기존 테스트).

## 검증

- 가드만 고치고 테스트를 그대로 두면 위 두 테스트가 실패했다(`2 failed, 9 passed`) — 바뀐 것이 그 동작임을
보여 준다.
- 관련 5개 파일 → `659 passed`. 새 테스트 파일을 메모리 측정 테스트와 3회 → 3회 모두 `30 passed`.
- `check_contract_compat.py --base origin/main` → `contract compatible
with origin/main: 1.82.0 -> 1.83.0`.
- Studio `main` 의 `contractDrift.test.ts` 를 이 브랜치의 계약(1.83.0)으로 → `Tests
577 passed (577)`. Studio 쪽 선행 변경은 필요 없다.
- `ruff`, `mypy src` 통과. 전체 스위트는 CI 에 맡긴다.

## 동작 변경 — 알아 둘 것

- 중단된 run 을 예전 id 로 다시 제출하던 클라이언트는 이제 409/400 을 받는다. Studio 는 재시도에 같은
`run_id` 를 보내지 않는 것으로 보인다(`features/` 에서 그런 경로를 찾지 못했다 — 전수 확인은 아니다).
- 제출 기록은 있는데 종료 이벤트가 아직 없는 id(프로세스가 죽은 직후, 재기동 때 중단으로 표시되기 전)도 같은 답을
받는다. 재기동하면 곧 중단으로 표시되므로 따로 가르지 않았다.

🤖 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(api): a run id whose only record is an interrupted submission can be resubmitted by another owner

1 participant