diff --git a/agent-roadmap/archive/phase/message-based-build-agent/milestones/agent-message-protocol.md b/agent-roadmap/archive/phase/message-based-build-agent/milestones/agent-message-protocol.md new file mode 100644 index 0000000..1165c5d --- /dev/null +++ b/agent-roadmap/archive/phase/message-based-build-agent/milestones/agent-message-protocol.md @@ -0,0 +1,86 @@ +# Milestone: agent 메시지 프로토콜 + +## 위치 + +- Roadmap: `agent-roadmap/ROADMAP.md` +- Phase: `agent-roadmap/phase/message-based-build-agent/PHASE.md` + +## 목표 + +`oto agent` 또는 `oto daemon` 모드에서 OTO Server와 runner가 양방향 메시지 통신을 하기 위한 기본 메시지 범위와 계약을 정의한다. + +## 상태 + +[완료] + +## 구현 잠금 + +- 상태: 해제 +- 결정 필요: 없음 + +## 범위 + +- runner register, capabilities, run request, step event, log stream, artifact event, cancel, status, self-update 메시지의 최소 필드를 정의한다. +- 메시지 버전, 호환성, 에러 표현 방식을 정리한다. +- OTO runner가 OTO Server의 build/deploy 전용 실행 표면이며 generic remote shell 하위 실행물이 아니라는 경계를 명시한다. + +## 메시지 계약 요약 + +기본 메시지 목록과 방향성은 `proto/oto/runner.proto`의 `oto.runner.v1` 패키지를 기준으로 한다. + +| 방향 | 메시지 | 목적 | +|------|--------|------| +| runner -> server | `RegisterRunnerRequest`, `RegisterRunnerResponse` | enrollment token, runner id, protocol version, capability, command catalog로 runner 등록과 거절 사유를 주고받는다. | +| runner -> server | `HeartbeatRequest`, `HeartbeatResponse` | 등록된 runner의 생존 상태를 갱신하고 unknown runner 또는 terminal state를 거절한다. | +| server -> operator -> runner | `BootstrapCommandRequest`, `BootstrapCommandResponse` | OTO Server가 agent 설치와 등록에 필요한 bootstrap command를 발급한다. | +| runner -> server | `JobClaimRequest`, `JobClaimResponse` | runner가 OTO Server 소유 job/execution을 claim하고 `RunRequest`를 받는다. | +| server -> runner | `RunRequest` | YAML path/content, 변수, 허용 command type 목록으로 OTO pipeline 실행을 지시한다. | +| runner -> server | `ExecutionReportRequest`, `ExecutionReportResponse` | 실행 성공 여부, exit code, message, step event를 보고한다. | +| runner -> server | `LogAppendRequest`, `LogAppendResponse` | 실행 로그 라인을 OTO Server execution log로 append한다. | +| runner -> server | `ArtifactReportRequest`, `ArtifactReportResponse` | 실행 산출물 이름과 경로를 OTO Server execution artifact로 보고한다. | +| server -> runner | `CancelRunRequest`, `CancelRunResponse` | 실행 취소 요청과 실패 사유를 표현한다. | +| server -> runner | `RunnerStatusRequest`, `RunnerStatusResponse` | runner 상태와 현재 execution id 조회를 표현한다. | +| server -> runner | `SelfUpdateRequest`, `SelfUpdateResponse` | runner self-update 버전과 다운로드 위치, 실패 사유를 표현한다. | + +제품 경계는 OTO runner를 generic remote shell의 하위 실행물이 아니라 build/deploy 전용 실행 표면으로 두는 것이다. 원격 메시지는 기존 YAML pipeline과 command catalog를 호출하는 계약이며, 로컬 CLI 실행 경로와 YAML 형식을 대체하지 않는다. 따라서 새 메시지는 `RunRequest.command_types`, `StepEventReport`, log/artifact/report 메시지처럼 OTO pipeline 실행 의미를 보존하는 범위에서만 추가한다. + +## 기능 + +### Epic: [message-contract] Server-runner 메시지 계약 + +OTO Server와 runner가 같은 의미로 해석할 기본 메시지 범위와 호환성 기준을 묶는다. + +- [x] [message-list] 기본 메시지 목록과 방향성이 정의되어 있다. +- [x] [message-fields] 각 메시지의 최소 필드와 실패 응답 형식이 정의되어 있다. 검증: OTO Server와 runner 구현자가 같은 메시지 계약을 기준으로 병렬 작업을 시작할 수 있고, YAML 파이프라인과 커맨드 모델이 메시지 위에서 어떻게 호출되는지 설명된다. +- [x] [version-capability] 프로토콜 버전과 capability 협상 기준이 정의되어 있다. +- [x] [domain-boundary] generic node 하위 실행물이 아니라는 제품 경계가 문서화되어 있다. 검증: 메시지 추가가 기존 CLI 실행 경로를 깨지 않는다는 경계가 명확하다. + +## 완료 리뷰 + +- 상태: 승인됨 +- 요청일: 2026-06-06 +- 완료 근거: 모든 `message-contract` Task가 충족되었다. `message-fields`는 `agent-task/archive/2026/06/m-agent-message-protocol/01_proto_contract/complete.log`, `version-capability`는 `agent-task/archive/2026/06/m-agent-message-protocol/02+01_core_compatibility/complete.log`의 Roadmap Completion으로 충족되었다. `message-list`와 `domain-boundary`는 이 문서의 `메시지 계약 요약`에 직접 정리했다. +- 리뷰 필요: + - [x] 사용자가 완료 결과를 확인했다 + - [x] archive 이동을 승인했다 +- 리뷰 코멘트: 2026-06-06 사용자 요청에 따라 현 Milestone을 종료하고 다음 Milestone으로 전환한다. + +## 범위 제외 + +- 웹 UI 또는 OTO Server 관리 화면을 구현하지 않는다. +- 기존 YAML 파이프라인 형식을 대체하지 않는다. +- 모든 운영 보안 정책을 완성하지 않는다. + +## 작업 컨텍스트 + +- 관련 경로: `apps/runner/lib/oto/**`, `apps/runner/test/**`, `proto/oto/**`, `services/core/**` +- CLI 자동화 표면 정리 단계의 구조화 실행 결과와 step event 기준을 먼저 확인한다. +- 표준선(선택): 메시지 형태는 `proto/oto`의 OTO Server/Runner 계약과 구조화 실행 결과/step event 계약을 우선 활용하고, YAML 파이프라인 실행 경로를 대체하지 않는 방향으로 잡는다. +- 선행 Milestone: 독립 Control Plane 분리 마이그레이션 +- 후속 의존: `control-plane-separation-migration`의 `oto-owns-cicd` Task가 OTO Server의 job/execution/log/artifact 소유권을 먼저 확정해야 한다. +- 선행 완료 근거(2026-06-06): `agent-roadmap/archive/phase/independent-control-plane/milestones/control-plane-separation-migration.md`가 `[완료]`로 archive되었고, `agent-task/archive/2026/06/m-control-plane-separation-migration/07+05,06_cicd_smoke_evidence/complete.log`의 Roadmap Completion이 `oto-owns-cicd` 완료를 기록한다. +- 책임 경계: iop는 이 Phase의 wire/protocol 소유자가 아니며, AI provider가 필요한 후속 기능에서만 선택적으로 소비된다. +- 책임 경계: OTO는 build/deploy runner가 노출할 capabilities, YAML run request 연결, step/log/artifact event 의미를 정리한다. +- 프로토콜 파일이나 생성 코드가 추가되면 관련 도메인 rule 또는 새 도메인 rule 필요 여부를 검토한다. +- 완료 동기화(2026-06-06): `message-fields`와 `version-capability`는 각각 proto contract foundation과 Core/Dart compatibility review PASS 완료 로그를 근거로 반영했다. +- 완료 동기화(2026-06-06): `message-list`와 `domain-boundary`는 큰 구현 계획 없이 현재 Milestone 문서의 메시지 계약 요약으로 직접 반영했다. diff --git a/agent-roadmap/phase/message-based-build-agent/PHASE.md b/agent-roadmap/phase/message-based-build-agent/PHASE.md index 8f6edbb..169f9a4 100644 --- a/agent-roadmap/phase/message-based-build-agent/PHASE.md +++ b/agent-roadmap/phase/message-based-build-agent/PHASE.md @@ -14,10 +14,10 @@ OTO를 generic remote shell이 아니라 build/deploy 전용 runner로 노출하 완료된 Milestone은 archive 경로를 가리키고, 검토중, 진행중, 계획 또는 보류 Milestone은 이 Phase 하위 `milestones/` 경로를 가리킨다. 완료, 검토중, 진행중, 계획 순서로 두어 아래로 갈수록 미래 작업에 가까워지게 정렬한다. -- [진행중] agent 메시지 프로토콜 - - 경로: `agent-roadmap/phase/message-based-build-agent/milestones/agent-message-protocol.md` +- [완료] agent 메시지 프로토콜 + - 경로: `agent-roadmap/archive/phase/message-based-build-agent/milestones/agent-message-protocol.md` - 요약: OTO Server와 runner가 공유할 capabilities, run request, event/reporting 메시지 프로토콜을 정의한다. -- [계획] 원격 실행 생명주기 +- [진행중] 원격 실행 생명주기 - 경로: `agent-roadmap/phase/message-based-build-agent/milestones/remote-run-lifecycle.md` - 요약: OTO Server가 소유하는 원격 실행, 로그, artifact, cancel, status, self-update 생명주기를 완성한다. diff --git a/agent-roadmap/phase/message-based-build-agent/milestones/agent-message-protocol.md b/agent-roadmap/phase/message-based-build-agent/milestones/agent-message-protocol.md deleted file mode 100644 index 90a266c..0000000 --- a/agent-roadmap/phase/message-based-build-agent/milestones/agent-message-protocol.md +++ /dev/null @@ -1,65 +0,0 @@ -# Milestone: agent 메시지 프로토콜 - -## 위치 - -- Roadmap: `agent-roadmap/ROADMAP.md` -- Phase: `agent-roadmap/phase/message-based-build-agent/PHASE.md` - -## 목표 - -`oto agent` 또는 `oto daemon` 모드에서 OTO Server와 runner가 양방향 메시지 통신을 하기 위한 기본 메시지 범위와 계약을 정의한다. - -## 상태 - -[진행중] - -## 구현 잠금 - -- 상태: 해제 -- 결정 필요: 없음 - -## 범위 - -- runner register, capabilities, run request, step event, log stream, artifact event, cancel, status, self-update 메시지의 최소 필드를 정의한다. -- 메시지 버전, 호환성, 에러 표현 방식을 정리한다. -- OTO runner가 OTO Server의 build/deploy 전용 실행 표면이며 generic remote shell 하위 실행물이 아니라는 경계를 명시한다. - -## 기능 - -### Epic: [message-contract] Server-runner 메시지 계약 - -OTO Server와 runner가 같은 의미로 해석할 기본 메시지 범위와 호환성 기준을 묶는다. - -- [ ] [message-list] 기본 메시지 목록과 방향성이 정의되어 있다. -- [ ] [message-fields] 각 메시지의 최소 필드와 실패 응답 형식이 정의되어 있다. 검증: OTO Server와 runner 구현자가 같은 메시지 계약을 기준으로 병렬 작업을 시작할 수 있고, YAML 파이프라인과 커맨드 모델이 메시지 위에서 어떻게 호출되는지 설명된다. -- [ ] [version-capability] 프로토콜 버전과 capability 협상 기준이 정의되어 있다. -- [ ] [domain-boundary] generic node 하위 실행물이 아니라는 제품 경계가 문서화되어 있다. 검증: 메시지 추가가 기존 CLI 실행 경로를 깨지 않는다는 경계가 명확하다. - -## 완료 리뷰 - -- 상태: 없음 -- 요청일: 없음 -- 완료 근거: 계획 상태이며 기능 Task와 Task 안의 검증이 아직 충족되지 않았다. -- 리뷰 필요: - - [ ] 사용자가 완료 결과를 확인했다 - - [ ] archive 이동을 승인했다 -- 리뷰 코멘트: 없음 - -## 범위 제외 - -- 웹 UI 또는 OTO Server 관리 화면을 구현하지 않는다. -- 기존 YAML 파이프라인 형식을 대체하지 않는다. -- 모든 운영 보안 정책을 완성하지 않는다. - -## 작업 컨텍스트 - -- 관련 경로: `apps/runner/lib/oto/**`, `apps/runner/test/**`, `proto/oto/**`, `services/core/**` -- CLI 자동화 표면 정리 단계의 구조화 실행 결과와 step event 기준을 먼저 확인한다. -- 표준선(선택): 메시지 형태는 `proto/oto`의 OTO Server/Runner 계약과 구조화 실행 결과/step event 계약을 우선 활용하고, YAML 파이프라인 실행 경로를 대체하지 않는 방향으로 잡는다. -- 선행 Milestone: 독립 Control Plane 분리 마이그레이션 -- 후속 의존: `control-plane-separation-migration`의 `oto-owns-cicd` Task가 OTO Server의 job/execution/log/artifact 소유권을 먼저 확정해야 한다. -- 선행 완료 근거(2026-06-06): `agent-roadmap/archive/phase/independent-control-plane/milestones/control-plane-separation-migration.md`가 `[완료]`로 archive되었고, `agent-task/archive/2026/06/m-control-plane-separation-migration/07+05,06_cicd_smoke_evidence/complete.log`의 Roadmap Completion이 `oto-owns-cicd` 완료를 기록한다. -- 책임 경계: iop는 이 Phase의 wire/protocol 소유자가 아니며, AI provider가 필요한 후속 기능에서만 선택적으로 소비된다. -- 책임 경계: OTO는 build/deploy runner가 노출할 capabilities, YAML run request 연결, step/log/artifact event 의미를 정리한다. -- 프로토콜 파일이나 생성 코드가 추가되면 관련 도메인 rule 또는 새 도메인 rule 필요 여부를 검토한다. -- 확인 필요: 구현 착수 전 `oto-owns-cicd` 완료 근거를 확인한다. diff --git a/agent-roadmap/phase/message-based-build-agent/milestones/remote-run-lifecycle.md b/agent-roadmap/phase/message-based-build-agent/milestones/remote-run-lifecycle.md index 88df10c..e317951 100644 --- a/agent-roadmap/phase/message-based-build-agent/milestones/remote-run-lifecycle.md +++ b/agent-roadmap/phase/message-based-build-agent/milestones/remote-run-lifecycle.md @@ -11,7 +11,7 @@ OTO Server가 runner에 파이프라인 실행을 요청하고, runner가 실행 ## 상태 -[계획] +[진행중] ## 구현 잠금 @@ -58,6 +58,7 @@ OTO Server의 run request가 기존 YAML 파이프라인 실행, 진행 보고, - agent 메시지 프로토콜과 CLI 자동화 표면 정리 단계의 구조화 출력 기준을 먼저 확인한다. - 표준선(선택): run request는 기존 YAML 파이프라인 입력으로 변환하고, 진행 상태는 기존 step event/log/result 표면을 우선 재사용한다. cancel/status/self-update는 OTO Server 정책이 정해지기 전까지 build/deploy runner의 최소 안전 동작으로만 정리한다. - 선행 Milestone: agent 메시지 프로토콜 +- 선행 Milestone 완료 근거(2026-06-06): `agent-roadmap/archive/phase/message-based-build-agent/milestones/agent-message-protocol.md`가 `[완료]`로 archive되었고, `message-contract` Epic의 모든 Task가 완료 리뷰 승인 상태로 정리되었다. - 선행 Task: `control-plane-separation-migration`의 `oto-owns-cicd` - pipeline, command, core 도메인 rule이 관련될 수 있다. -- 확인 필요: 구현 착수 전 `oto-owns-cicd`와 `agent-message-protocol` 완료 근거를 확인한다. +- 확인 필요: 구현 착수 전 `oto-owns-cicd` 완료 근거를 확인한다. diff --git a/agent-task/archive/2026/06/m-remote-run-lifecycle/01_run_request_pipeline/code_review_cloud_G07_0.log b/agent-task/archive/2026/06/m-remote-run-lifecycle/01_run_request_pipeline/code_review_cloud_G07_0.log new file mode 100644 index 0000000..e44d91d --- /dev/null +++ b/agent-task/archive/2026/06/m-remote-run-lifecycle/01_run_request_pipeline/code_review_cloud_G07_0.log @@ -0,0 +1,209 @@ + + +# Code Review Reference - API + +> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.** +> The task is NOT complete until every implementation-owned section below is filled in. +> Complete the `구현 체크리스트`; the final checklist item is mandatory before saving. +> Fill implementation-owned sections, then stop with active files in place and report ready for review. +> If implementation is blocked by a user-only decision, user-owned external environment prerequisite, or scope conflict, fill `사용자 리뷰 요청` with evidence and stop with active files in place; code-review decides whether to write `USER_REVIEW.md`. Evidence gaps that a follow-up agent can close by rerunning commands or collecting artifacts are normal follow-up issues, not user-review blockers by themselves. +> Do not ask the user directly, present choices in chat, or call `request_user_input` during implementation; record the needed decision in `사용자 리뷰 요청` and stop for code-review. +> Finalization (`코드리뷰 결과`, log rename, `complete.log`, archive moves, `코드리뷰 전용 체크리스트`) is review-agent-only, even after compaction/resume. +> Follow the ownership table at the bottom of this file for which sections you own. + +## 개요 + +date=2026-06-06 +task=m-remote-run-lifecycle/01_run_request_pipeline, plan=0, tag=API + +## 이 파일을 읽는 리뷰 에이전트에게 + +> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다. + +각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요. +리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다. + +1. 판정을 append한다. +2. `CODE_REVIEW-cloud-G07.md` -> `code_review_cloud_G07_N.log`, `PLAN-cloud-G07.md` -> `plan_cloud_G07_M.log`로 아카이브한다. +3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-remote-run-lifecycle/01_run_request_pipeline/`로 이동한다. WARN/FAIL이면 user-review gate를 확인한 뒤 다음 active plan/review 파일 또는 `USER_REVIEW.md`를 작성한다. `USER_REVIEW.md`가 사용자 결정으로 완료/PASS 해소되면 code-review가 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log` 작성 후 archive 이동한다. +4. PASS이고 task group이 `m-remote-run-lifecycle`이면 완료 이벤트 메타데이터를 보고한다. roadmap 상태 체크와 `update-roadmap` 호출은 런타임 책임이다. +5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다. + +--- + +## 구현 항목별 완료 여부 + +| 항목 | 완료 여부 | +|------|---------| +| [API-1] Store Run Input | [x] | +| [API-2] HTTP Job Create And Claim Contract | [x] | +| [API-3] Dart Claim Result Parses RunRequest | [x] | + +## 구현 체크리스트 + +- [x] 서버 Store가 run request 입력을 job에 저장하고 copy/snapshot에서 누락하지 않는다. +- [x] `POST /api/v1/jobs`가 `run_request`를 받아 검증하고, `GET /jobs/{id}`가 저장된 입력을 노출한다. +- [x] runner claim 응답이 저장된 `RunRequest`를 채우며, missing/invalid run input은 명확한 오류로 거부한다. +- [x] Dart `JobClaimResult`가 `runRequest`를 파싱하고 기존 명시 claim 테스트를 깨지 않는다. +- [x] Go unit/smoke와 Dart agent/server smoke 테스트를 갱신한다. +- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. + +## 코드리뷰 전용 체크리스트 + +> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다. +> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다. + +- [x] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다. +- [x] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다. +- [x] active `CODE_REVIEW-*-G??.md`를 `code_review_cloud_G07_N.log`로 아카이브한다. +- [x] active `PLAN-*-G??.md`를 `plan_cloud_G07_M.log`로 아카이브한다. +- [x] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md`와 `agent-task/**/*.log`를 unignore하고 `agent-roadmap/current.md`를 ignore하는지 확인한다. +- [x] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다. +- [x] PASS이면 active task 디렉터리 `agent-task/m-remote-run-lifecycle/01_run_request_pipeline/`를 `agent-task/archive/YYYY/MM/m-remote-run-lifecycle/01_run_request_pipeline/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다. +- [x] PASS이고 task group이 `m-remote-run-lifecycle`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다. +- [x] PASS split 작업이면 이동 후 빈 active parent `agent-task/m-remote-run-lifecycle/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다. +- [ ] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-{build_lane}-GNN.md`와 `CODE_REVIEW-{review_lane}-GNN.md`를 작성하고 `complete.log`를 작성하지 않는다. +- [ ] USER_REVIEW이면 `agent-ops/skills/common/code-review/templates/user-review-template.md` 기준으로 `USER_REVIEW.md`를 작성하고 active `PLAN-*.md`, `CODE_REVIEW-*.md`, `complete.log`를 남기지 않는다. +- [ ] USER_REVIEW가 사용자 결정으로 완료/PASS 해소되면 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log`를 작성한 뒤 task directory를 archive로 이동한다. + +## 계획 대비 변경 사항 + +- `CreateJob`은 이제 저장 포인터가 아니라 `copyJob(job)`을 반환한다. 계획은 "copy해서 저장"만 명시했으나, store 소유 객체가 호출자에게 그대로 노출되면 `RunInput`을 통해 저장값이 외부에서 변형될 수 있어 `GetJob`/`Snapshot`과 동일하게 반환도 deep copy로 통일했다. 기존 호출부(`handleCreateJob`)는 반환값을 즉시 `jobToJSON`에만 쓰므로 영향이 없다. +- exactly-one(`pipeline_yaml` xor `pipeline_yaml_path`) 검증을 **create가 아니라 claim 시점**에서 강제했다. 계획의 "`pipeline_yaml`와 `pipeline_yaml_path` 중 정확히 하나를 remote claim 기준으로 인정"과 테스트 이름 `TestHandleRunnerCicdRejectsInvalidRunInput`(claim 핸들러)이 claim 기준 검증을 가리키기 때문이다. create는 backward 호환을 위해 `run_request` 없이도 `201`을 유지하고 `RunInput`을 `nil`로 둔다. +- 거부 코드는 저장된 run input이 없으면 `409 Conflict`("job has no run request"), `pipeline_yaml`/`pipeline_yaml_path`가 둘 다이거나 둘 다 없으면 `400 Bad Request`로 분리했다. 계획이 허용한 "400 또는 409" 범위 안이다. +- 계획이 명명한 `runRequestFromJSON` 헬퍼는 raw map이 아니라 create body에서 proto json 태그로 디코드된 `*otopb.RunRequest`를 입력으로 받아 `*cicdstate.RunInput`으로 변환한다. 서버의 다른 핸들러처럼 proto의 snake_case json 태그를 재사용해 별도 키 매핑 코드를 줄였다. + +## 주요 설계 결정 + +- **proto 의존성 격리**: store는 proto를 import하지 않고 `cicdstate.RunInput`만 보유한다. `otopb.RunRequest` <-> `RunInput` 변환은 HTTP boundary(`runRequestFromJSON`/`runRequestToProto`)에서만 수행한다. +- **deep copy 경계**: `copyRunInput`이 `Variables` map과 `CommandTypes` slice를 복제하고, `CreateJob`(입력)·`copyJob`(출력, `GetJob`/`Snapshot` 경유)에서 모두 적용해 store 값이 호출자 측 변형으로부터 양방향 격리된다. +- **claim 응답 id 주입**: 저장된 `RunInput`은 pipeline 입력만 보관하고, runner/job/execution id는 claim 시점에 `runRequestToProto(in, runnerID, jobID, execID)`로 채운다. 따라서 어떤 runner가 claim해도 응답 `RunRequest`의 식별자가 실제 claim 컨텍스트와 일치한다. +- **부작용 없는 거부**: run input 검증을 `CreateExecution`/`TransitionJob` 이전에 배치해, 거부된 claim이 execution 생성이나 상태 전이를 남기지 않는다(`TestHandleRunnerCicdRejectsInvalidRunInput`에서 job이 `queued` 유지·`ExecutionID` 비어있음을 확인). +- **Dart 양식 호환 파싱**: `_parseRunRequest`가 snake_case와 camelCase 키를 모두 받아 `oto.RunRequest`를 구성하고, 기존 명시 claim 응답(`run_request` 없음)에서는 `runRequest=null`을 반환해 기존 흐름을 깨지 않는다. + +## 사용자 리뷰 요청 + +_기본값은 `없음`이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 변경 없이는 안전하게 진행할 수 없으면 아래 항목을 실제 내용으로 교체하고, 구현을 중단한 뒤 active 파일을 그대로 둔 채 리뷰를 요청한다. 구현 에이전트는 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 증거 공백만으로는 사용자 리뷰 요청을 작성하지 않는다._ + +- 상태: 없음 +- 사유 유형: 없음 +- 결정 필요: 없음 +- 차단 근거: 없음 +- 실행한 검증/명령: 없음 +- 자동 후속 불가 이유: 없음 +- 재개 조건: 없음 + +## 리뷰어를 위한 체크포인트 + +- `RunInput`이 proto dependency 없이 store에 저장되고 deep copy되는지 확인한다. +- claim 응답의 `run_request`가 서버 저장값과 runner/job/execution id를 모두 포함하는지 확인한다. +- 기존 job/log/artifact smoke가 유지되는지 확인한다. + +## 검증 결과 + +_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._ + +필수 규칙: +- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다. +- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다. +- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다. +- 사용자 리뷰 요청으로 명령을 끝까지 실행하지 못했다면 `사용자 리뷰 요청`에 실행한 명령, 실제 출력, 미실행 명령의 사유를 기록한다. +- mobile/UI hang, timeout, 또는 2분 무진행은 blind retry를 중단하고 focused rerun 명령과 screenshot/window/UI-tree evidence path를 남기며, 불가능하면 정확한 사유를 남긴다. + +### API-1 중간 검증 +```bash +$ go test ./internal/cicdstate +ok github.com/toki/oto/services/core/internal/cicdstate 0.002s + +# 신규 테스트 확인 (-v) +$ go test ./internal/cicdstate -run TestStoreCreatesJobWithRunInputCopy -v +=== RUN TestStoreCreatesJobWithRunInputCopy +--- PASS: TestStoreCreatesJobWithRunInputCopy (0.00s) +PASS +ok github.com/toki/oto/services/core/internal/cicdstate 0.002s +``` + +### API-2 중간 검증 +```bash +$ go test ./internal/httpserver +ok github.com/toki/oto/services/core/internal/httpserver 0.005s + +# 신규/갱신 테스트 확인 (-v) +$ go test ./internal/httpserver -run 'RunRequest|RejectsInvalidRunInput|ClaimReturns|ClaimReport' -v +=== RUN TestHandleCreateJobStoresRunRequest +--- PASS: TestHandleCreateJobStoresRunRequest (0.00s) +=== RUN TestHandleRunnerCicdClaimReturnsRunRequest +--- PASS: TestHandleRunnerCicdClaimReturnsRunRequest (0.00s) +=== RUN TestHandleRunnerCicdRejectsInvalidRunInput +--- PASS: TestHandleRunnerCicdRejectsInvalidRunInput (0.00s) +=== RUN TestHandleRunnerCicdClaimReportLogsAndArtifacts +--- PASS: TestHandleRunnerCicdClaimReportLogsAndArtifacts (0.00s) +PASS +ok github.com/toki/oto/services/core/internal/httpserver 0.005s +``` + +### API-3 중간 검증 +```bash +$ dart analyze lib/oto/agent/oto_server_job_client.dart test/oto_agent_registration_test.dart test/oto_server_connection_smoke_test.dart +Analyzing oto_server_job_client.dart, oto_agent_registration_test.dart, oto_server_connection_smoke_test.dart... +No issues found! + +$ dart test test/oto_agent_registration_test.dart test/oto_server_connection_smoke_test.dart +... (생략) ... +00:03 +19: All tests passed! +``` + +### 최종 검증 +```bash +$ cd services/core && go test ./... +? github.com/toki/oto/services/core/cmd/oto-core [no test files] +ok github.com/toki/oto/services/core/internal/cicdstate (cached) +ok github.com/toki/oto/services/core/internal/httpserver (cached) +ok github.com/toki/oto/services/core/internal/runnerregistry (cached) +? github.com/toki/oto/services/core/oto [no test files] + +$ cd apps/runner && dart analyze +Analyzing runner... +No issues found! + +$ cd apps/runner && dart test test/oto_agent_registration_test.dart test/oto_server_connection_smoke_test.dart test/oto_application_test.dart test/oto_context_test.dart test/oto_core_test.dart +... (진행 로그 생략) ... +00:04 +83: All tests passed! +``` + +> 참고: `oto_application_test.dart` 진행 중 출력되는 `There are no files in path /tmp/oto_missing_*.yaml`는 기존 "missing file without LateInitializationError" 테스트가 의도적으로 검증하는 정상 경로 메시지이며 실패가 아니다(최종 `All tests passed!`). + +--- + +> **[IMPLEMENTING AGENT — BEFORE SAVING] Have you filled in every implementation-owned section: completion table, implementation checklist, changes from plan, design decisions, and verification output?** +> If anything is blank, go back and fill it in before saving this file. +> Leave review-agent-only sections unchanged. + +## Section Ownership + +| Section | Owner | Note | +|---------|-------|------| +| Header comment, 개요, 리뷰 에이전트 지시 | Fixed at stub creation | Implementing agent must not modify or execute these | +| Roadmap Targets | Fixed at stub creation from plan | Implementing agent must not modify | +| 구현 항목별 완료 여부 | Implementing agent | Check `[ ]` to `[x]` only | +| 구현 체크리스트 | Implementing agent | Check `[ ]` to `[x]` only | +| 코드리뷰 전용 체크리스트 | Review agent only | Implementing agent must not modify | +| 계획 대비 변경 사항, 주요 설계 결정 | Implementing agent | Replace placeholder text | +| 사용자 리뷰 요청 | Implementing agent | Keep `상태: 없음` unless user input is required | +| 리뷰어를 위한 체크포인트 | Fixed at stub creation | Review focus | +| 검증 결과 | Implementing agent | Paste actual command output | +| 코드리뷰 결과 | Review agent appends | Not included in stub | + +## 코드리뷰 결과 + +- 종합 판정: PASS +- 차원별 평가: + - correctness: Pass + - completeness: Pass + - test coverage: Pass + - API contract: Pass + - code quality: Pass + - plan deviation: Pass + - verification trust: Pass +- 발견된 문제: 없음 +- 다음 단계: PASS - active plan/review를 로그로 아카이브하고 `complete.log` 작성 후 task directory를 archive로 이동한다. diff --git a/agent-task/archive/2026/06/m-remote-run-lifecycle/01_run_request_pipeline/complete.log b/agent-task/archive/2026/06/m-remote-run-lifecycle/01_run_request_pipeline/complete.log new file mode 100644 index 0000000..90021c6 --- /dev/null +++ b/agent-task/archive/2026/06/m-remote-run-lifecycle/01_run_request_pipeline/complete.log @@ -0,0 +1,37 @@ +# Complete - m-remote-run-lifecycle/01_run_request_pipeline + +## 완료 일시 + +2026-06-06 + +## 요약 + +원격 실행 job의 run request 저장, HTTP create/claim 계약, Dart claim result 파싱을 1회 리뷰로 PASS 완료. + +## 루프 이력 + +| Plan | Review | Verdict | 메모 | +|------|--------|---------|------| +| `plan_cloud_G07_0.log` | `code_review_cloud_G07_0.log` | PASS | Required/Suggested 이슈 없음; 계획된 서버 저장/claim/Dart 파싱 및 검증 통과 | + +## 구현/정리 내용 + +- `cicdstate.Job`에 proto 비의존 `RunInput` 저장 모델과 deep copy 경계를 추가했다. +- job create/get 및 runner claim HTTP 계약에 `run_request` 저장, 노출, claim 응답 주입, invalid run input 거부를 연결했다. +- Dart `JobClaimResult`가 snake_case/camelCase `run_request`를 `oto.RunRequest`로 파싱하도록 확장하고 관련 smoke 테스트를 갱신했다. + +## 최종 검증 + +- `cd services/core && go test ./...` - PASS; 전체 Go package 테스트 통과. +- `cd services/core && go test ./internal/cicdstate -run TestStoreCreatesJobWithRunInputCopy -count=1 -v` - PASS; 신규 RunInput deep copy 테스트 실제 실행 통과. +- `cd services/core && go test ./internal/httpserver -run 'RunRequest|RejectsInvalidRunInput|ClaimReturns|ClaimReport' -count=1 -v` - PASS; 신규/갱신 HTTP run_request 테스트 실제 실행 통과. +- `cd apps/runner && dart analyze` - PASS; `No issues found!`. +- `cd apps/runner && dart test test/oto_agent_registration_test.dart test/oto_server_connection_smoke_test.dart test/oto_application_test.dart test/oto_context_test.dart test/oto_core_test.dart` - PASS; `+83: All tests passed!`. + +## 잔여 Nit + +- 없음 + +## 후속 작업 + +- 없음 diff --git a/agent-task/archive/2026/06/m-remote-run-lifecycle/01_run_request_pipeline/plan_cloud_G07_0.log b/agent-task/archive/2026/06/m-remote-run-lifecycle/01_run_request_pipeline/plan_cloud_G07_0.log new file mode 100644 index 0000000..ba200b5 --- /dev/null +++ b/agent-task/archive/2026/06/m-remote-run-lifecycle/01_run_request_pipeline/plan_cloud_G07_0.log @@ -0,0 +1,245 @@ + + +# Plan - API + +## 이 파일을 읽는 구현 에이전트에게 + +구현 완료의 마지막 단계는 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채우는 것이다. 검증 명령을 실행하고 실제 출력, 계획 대비 변경, 주요 설계 결정을 기록한 뒤 active 파일을 그대로 두고 리뷰 준비를 보고한다. 구현 중 사용자 전용 결정, 사용자 소유 환경/secret, 범위 충돌이 생기면 review stub의 `사용자 리뷰 요청` 섹션에 정확한 근거를 남기고 중단한다. 구현 에이전트는 사용자에게 직접 묻거나 선택지를 제시하거나 `request_user_input`을 호출하거나, `USER_REVIEW.md`/`complete.log`를 만들거나, 로그 아카이브를 수행하지 않는다. 재실행으로 채울 수 있는 증거 공백은 사용자 리뷰 요청이 아니다. + +## 배경 + +현재 OTO Server는 job을 만들고 runner가 claim할 수 있지만, job에 원격 실행 입력이 저장되지 않는다. `proto/oto/runner.proto`에는 `RunRequest`와 `JobClaimResponse.run_request`가 이미 있으나 서버 claim 응답은 이를 채우지 않는다. 이 계획은 run request를 서버 job 상태에 저장하고 runner가 claim할 때 기존 YAML 파이프라인 입력으로 받을 수 있는 계약을 만든다. + +## 사용자 리뷰 요청 흐름 + +구현 중 차단은 active review stub의 `사용자 리뷰 요청` 섹션에 기록한다. 이 섹션은 `agent-ops/skills/common/_templates/implementation-user-review-request-section.md` 형식을 따른다. 직접 사용자 프롬프트는 금지되며, code-review가 요청 타당성을 검증하고 실제 `USER_REVIEW.md` 작성 여부를 결정한다. + +## 분석 결과 + +### 읽은 파일 + +- `agent-ops/rules/project/rules.md` +- `agent-ops/rules/common/rules-roadmap.md` +- `agent-ops/skills/common/router.md` +- `agent-ops/skills/common/plan/SKILL.md` +- `agent-ops/rules/project/domain/agent/rules.md` +- `agent-ops/rules/project/domain/core/rules.md` +- `agent-ops/rules/project/domain/pipeline/rules.md` +- `agent-ops/rules/project/domain/command/rules.md` +- `agent-test/local/rules.md` +- `agent-test/local/agent-smoke.md` +- `agent-test/local/core-smoke.md` +- `agent-test/local/pipeline-smoke.md` +- `agent-test/local/command-smoke.md` +- `agent-roadmap/current.md` +- `agent-roadmap/phase/message-based-build-agent/PHASE.md` +- `agent-roadmap/phase/message-based-build-agent/milestones/remote-run-lifecycle.md` +- `.gitignore` +- `Makefile` +- `proto/oto/runner.proto` +- `services/core/go.mod` +- `services/core/internal/cicdstate/store.go` +- `services/core/internal/cicdstate/store_test.go` +- `services/core/internal/httpserver/server.go` +- `services/core/internal/httpserver/server_test.go` +- `services/core/internal/runnerregistry/registry.go` +- `apps/runner/pubspec.yaml` +- `apps/runner/lib/oto/agent/oto_server_job_client.dart` +- `apps/runner/lib/oto/agent/edge_registration_client.dart` +- `apps/runner/lib/oto/agent/agent_runner.dart` +- `apps/runner/lib/oto/application.dart` +- `apps/runner/lib/oto/core/build_result.dart` +- `apps/runner/lib/oto/core/data_composer.dart` +- `apps/runner/lib/oto/core/execution_context.dart` +- `apps/runner/lib/oto/pipeline/pipeline.dart` +- `apps/runner/lib/cli/commands/command_exe.dart` +- `apps/runner/test/oto_agent_registration_test.dart` +- `apps/runner/test/oto_server_connection_smoke_test.dart` +- `apps/runner/test/oto_application_test.dart` +- `apps/runner/test/oto_context_test.dart` +- `apps/runner/test/oto_core_test.dart` + +### 테스트 환경 규칙 + +- test_env: `local`. +- `agent-test/local/rules.md`를 읽었다. +- 적용 프로파일: `agent-smoke`, `core-smoke`, `pipeline-smoke`, `command-smoke`. +- agent/core/pipeline/command 프로파일의 Dart 기준은 `dart analyze`와 관련 `dart test`다. +- `services/core/**`와 `proto/oto/**`에 대한 agent-test 프로파일은 없다. 이 계획은 `Makefile`의 `core-test`와 `proto-go`/`proto-dart` 타깃을 fallback 검증 근거로 쓴다. +- `<확인 필요>` 값은 없었다. +- 사전 검증으로 `go test ./...`, `dart test test/oto_agent_registration_test.dart test/oto_application_test.dart test/oto_context_test.dart test/oto_core_test.dart`, `dart test test/oto_server_connection_smoke_test.dart`가 통과했다. + +### 테스트 커버리지 공백 + +- 서버 job 생성은 `id/name`만 저장하며 run request 저장 테스트가 없다. +- runner claim 응답은 `RunRequest` 필드를 프로토콜에 노출하지만 서버 응답에 채우는 테스트가 없다. +- queue claim 또는 서버 소유 execution id 선택 기준이 없다. 기존 client는 명시 `jobId/executionId` claim만 검증한다. +- YAML 입력 변환 자체는 `Application.build(BuildType.file, yamlContent: ...)` 테스트가 커버하지만, 서버 claim 응답과 연결되어 있지 않다. + +### 심볼 참조 + +- renamed/removed symbol: none. +- 변경 후보 call site: `Store.CreateJob` 호출은 `services/core/internal/httpserver/server.go`, `services/core/internal/httpserver/server_test.go`, `services/core/internal/cicdstate/store_test.go`에서 확인했다. + +### 분할 판단 + +- split decision policy를 먼저 평가했다. +- 공유 task group: `m-remote-run-lifecycle`. +- `01_run_request_pipeline`: 선행 없음. 서버 job/run request 저장과 claim 응답 계약을 먼저 만든다. +- `02+01_step_log_stream`: `01`의 active/archived `complete.log`가 아직 없으므로 predecessor `01`은 missing이다. +- `03+01_artifact_event`: predecessor `01`은 missing이다. +- `04+01_cancel_status_update`: predecessor `01`은 missing이다. +- 서버 계약이 없으면 runner loop, artifact, cancel/status/self-update 구현이 추측으로 흐르므로 단일 plan보다 split이 안전하다. + +### 범위 결정 근거 + +- 이 계획은 서버 job/run request 저장, claim 응답, Dart job client의 claim result 파싱까지만 다룬다. +- 이 plan은 후속 구현을 가능하게 하는 선행 계약이며, PASS되어도 roadmap Task를 체크하지 않는다. +- runner가 claim된 request를 실제 실행하는 loop는 `02+01_step_log_stream`으로 분리한다. +- artifact 자동 수집은 `03+01_artifact_event`로 분리한다. +- cancel/status/self-update endpoint와 상태 전이는 `04+01_cancel_status_update`로 분리한다. +- 새 protobuf 메시지는 만들지 않는다. 기존 `RunRequest`와 `JobClaimResponse.run_request`를 사용한다. + +### 빌드 등급 + +- build: `cloud-G07`. Go service 상태 저장, Dart client 파싱, cross-language protocol JSON 계약을 함께 바꾸는 작업이다. +- review: `cloud-G07`. server/client/test가 같은 계약을 구현했는지 교차 확인해야 한다. + +## 구현 체크리스트 + +- [ ] 서버 Store가 run request 입력을 job에 저장하고 copy/snapshot에서 누락하지 않는다. +- [ ] `POST /api/v1/jobs`가 `run_request`를 받아 검증하고, `GET /jobs/{id}`가 저장된 입력을 노출한다. +- [ ] runner claim 응답이 저장된 `RunRequest`를 채우며, missing/invalid run input은 명확한 오류로 거부한다. +- [ ] Dart `JobClaimResult`가 `runRequest`를 파싱하고 기존 명시 claim 테스트를 깨지 않는다. +- [ ] Go unit/smoke와 Dart agent/server smoke 테스트를 갱신한다. +- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. + +### [API-1] Store Run Input + +문제: [services/core/internal/cicdstate/store.go](/config/workspace/oto/services/core/internal/cicdstate/store.go:25)의 `Job`은 execution id만 저장하고 run input을 저장하지 않는다. + +Before: + +```go +// services/core/internal/cicdstate/store.go:25 +type Job struct { + ID string + Name string + State string + CreatedAt time.Time + UpdatedAt time.Time + ExecutionID string +} +``` + +해결 방법: `cicdstate.RunInput`을 추가하고 `Job`에 `RunInput *RunInput`을 둔다. proto package를 store에 직접 끌어오지 말고 HTTP boundary에서 `otopb.RunRequest` <-> `cicdstate.RunInput` 변환을 수행한다. `CreateJob`은 run input pointer를 optional로 받되 copy해서 저장한다. + +수정 파일 및 체크리스트: + +- [ ] `services/core/internal/cicdstate/store.go`: `RunInput` 구조체, deep copy helper, `Job.RunInput`, `CreateJob` signature 추가. +- [ ] `services/core/internal/cicdstate/store_test.go`: 저장/복사/변경 누수 방지 테스트 추가. + +테스트 작성: `TestStoreCreatesJobWithRunInputCopy`를 추가한다. variables/commandTypes slice/map을 외부에서 바꿔도 store 값이 바뀌지 않아야 한다. + +중간 검증: + +```bash +go test ./internal/cicdstate +``` + +### [API-2] HTTP Job Create And Claim Contract + +문제: [services/core/internal/httpserver/server.go](/config/workspace/oto/services/core/internal/httpserver/server.go:485)의 job create request는 `id/name`만 받고, [services/core/internal/httpserver/server.go](/config/workspace/oto/services/core/internal/httpserver/server.go:776)의 claim response는 `RunRequest`를 비워 둔다. + +Before: + +```go +// services/core/internal/httpserver/server.go:487 +var req struct { + ID string `json:"id"` + Name string `json:"name"` +} +``` + +```go +// services/core/internal/httpserver/server.go:776 +writeResponse(w, http.StatusOK, &otopb.JobClaimResponse{ + Accepted: true, + RunnerId: runnerID, + JobId: jobID, + ExecutionId: execID, + State: cicdstate.StateRunning, +}) +``` + +해결 방법: create body에 `run_request`를 추가한다. `pipeline_yaml`와 `pipeline_yaml_path` 중 정확히 하나를 remote claim 기준으로 인정하고, `variables`와 `command_types`는 optional로 보존한다. claim 시 저장된 input이 없거나 둘 다/둘 다 없음이면 `400` 또는 `409`로 거부하고, 응답에는 runner/job/execution id를 채운 `RunRequest`를 포함한다. + +수정 파일 및 체크리스트: + +- [ ] `services/core/internal/httpserver/server.go`: `runRequestFromJSON`, `runRequestToProto`, `runInputToJSON` helper 추가. +- [ ] `services/core/internal/httpserver/server.go`: `handleCreateJob`, `jobToJSON`, `handleRunnerClaimJob` 갱신. +- [ ] `services/core/internal/httpserver/server_test.go`: create/get/claim run_request 테스트와 invalid input 테스트 추가. + +테스트 작성: `TestHandleCreateJobStoresRunRequest`, `TestHandleRunnerCicdClaimReturnsRunRequest`, `TestHandleRunnerCicdRejectsInvalidRunInput`를 추가한다. + +중간 검증: + +```bash +go test ./internal/httpserver +``` + +### [API-3] Dart Claim Result Parses RunRequest + +문제: [apps/runner/lib/oto/agent/oto_server_job_client.dart](/config/workspace/oto/apps/runner/lib/oto/agent/oto_server_job_client.dart:9)의 `JobClaimResult`는 `run_request`를 보존하지 않는다. 이후 runner loop가 서버 입력을 실행할 수 없다. + +Before: + +```dart +// apps/runner/lib/oto/agent/oto_server_job_client.dart:9 +class JobClaimResult { + final bool accepted; + final String? errorMessage; + final String jobId; + final String executionId; + final String state; +``` + +해결 방법: `JobClaimResult`에 `oto.RunRequest? runRequest`를 추가하고 snake_case/camelCase JSON 모두 파싱한다. `claimJob`의 기존 API는 유지하되, 후속 `02`가 쓸 `claimNextJob()` 추가 여지를 남긴다. + +수정 파일 및 체크리스트: + +- [ ] `apps/runner/lib/oto/agent/oto_server_job_client.dart`: run request JSON parser helper 추가. +- [ ] `apps/runner/test/oto_agent_registration_test.dart`: claim response에 `run_request`가 있을 때 Dart 객체에 보존되는 테스트 추가. +- [ ] `apps/runner/test/oto_server_connection_smoke_test.dart`: 서버 create job body에 inline YAML `run_request`를 넣고 claim 결과를 확인한다. + +테스트 작성: 기존 `job client claims jobs and reports build results` 테스트를 확장한다. + +중간 검증: + +```bash +dart test test/oto_agent_registration_test.dart test/oto_server_connection_smoke_test.dart +``` + +## 수정 파일 요약 + +| 파일 | 항목 | +|------|------| +| `services/core/internal/cicdstate/store.go` | API-1 | +| `services/core/internal/cicdstate/store_test.go` | API-1 | +| `services/core/internal/httpserver/server.go` | API-2 | +| `services/core/internal/httpserver/server_test.go` | API-2 | +| `apps/runner/lib/oto/agent/oto_server_job_client.dart` | API-3 | +| `apps/runner/test/oto_agent_registration_test.dart` | API-3 | +| `apps/runner/test/oto_server_connection_smoke_test.dart` | API-3 | + +## 최종 검증 + +```bash +cd services/core && go test ./... +cd apps/runner && dart analyze +cd apps/runner && dart test test/oto_agent_registration_test.dart test/oto_server_connection_smoke_test.dart test/oto_application_test.dart test/oto_context_test.dart test/oto_core_test.dart +``` + +예상 결과: 모든 명령이 exit code 0으로 종료한다. Go test cache 출력은 이 계획에서 허용한다. Dart test는 실제 stdout/stderr를 review stub에 붙인다. + +모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다. diff --git a/agent-task/m-remote-run-lifecycle/02+01_step_log_stream/CODE_REVIEW-cloud-G06.md b/agent-task/m-remote-run-lifecycle/02+01_step_log_stream/CODE_REVIEW-cloud-G06.md new file mode 100644 index 0000000..5cd0bd2 --- /dev/null +++ b/agent-task/m-remote-run-lifecycle/02+01_step_log_stream/CODE_REVIEW-cloud-G06.md @@ -0,0 +1,157 @@ + + +# Code Review Reference - API + +> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.** +> The task is NOT complete until every implementation-owned section below is filled in. +> Complete the `구현 체크리스트`; the final checklist item is mandatory before saving. +> Fill implementation-owned sections, then stop with active files in place and report ready for review. +> If implementation is blocked by a user-only decision, user-owned external environment prerequisite, or scope conflict, fill `사용자 리뷰 요청` with evidence and stop with active files in place; code-review decides whether to write `USER_REVIEW.md`. +> Do not ask the user directly, present choices in chat, or call `request_user_input` during implementation; record the needed decision in `사용자 리뷰 요청` and stop for code-review. +> Finalization (`코드리뷰 결과`, log rename, `complete.log`, archive moves, `코드리뷰 전용 체크리스트`) is review-agent-only, even after compaction/resume. +> Follow the ownership table at the bottom of this file for which sections you own. + +## 개요 + +date=2026-06-06 +task=m-remote-run-lifecycle/02+01_step_log_stream, plan=0, tag=API + +## 이 파일을 읽는 리뷰 에이전트에게 + +> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다. + +각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요. +리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다. + +1. 판정을 append한다. +2. `CODE_REVIEW-cloud-G06.md` -> `code_review_cloud_G06_N.log`, `PLAN-local-G07.md` -> `plan_local_G07_M.log`로 아카이브한다. +3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-remote-run-lifecycle/02+01_step_log_stream/`로 이동한다. WARN/FAIL이면 user-review gate를 확인한 뒤 다음 active plan/review 파일 또는 `USER_REVIEW.md`를 작성한다. +4. PASS이고 task group이 `m-remote-run-lifecycle`이면 완료 이벤트 메타데이터를 보고한다. roadmap 상태 체크와 `update-roadmap` 호출은 런타임 책임이다. +5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다. + +--- + +## Roadmap Targets + +- Milestone: `agent-roadmap/phase/message-based-build-agent/milestones/remote-run-lifecycle.md` +- Task ids: + - `run-input` + - `step-log` +- Completion mode: check-on-pass + +## 구현 항목별 완료 여부 + +| 항목 | 완료 여부 | +|------|---------| +| [API-1] RunRequest To DataBuild Input | [ ] | +| [API-2] Remote Job Loop | [ ] | +| [API-3] Step Events And Logs Report | [ ] | + +## 구현 체크리스트 + +- [ ] `01_run_request_pipeline`의 `complete.log`가 존재하는지 확인하고 없으면 구현을 시작하지 않는다. +- [ ] `RunRequest`를 inline YAML 또는 workspace-confined file path 중 하나로 해석하고 variables를 pipeline property 입력으로 병합한다. +- [ ] runner job loop가 claim -> 실행 -> log append -> execution report 순서를 보장한다. +- [ ] `BuildResult.stepEvents`가 `ExecutionReportRequest.step_events`와 `execution_result.stepEvents`에 보존된다. +- [ ] agent/core/pipeline 테스트와 server connection smoke를 갱신한다. +- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. + +## 코드리뷰 전용 체크리스트 + +> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다. +> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다. + +- [ ] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다. +- [ ] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다. +- [ ] active `CODE_REVIEW-cloud-G06.md`를 `code_review_cloud_G06_N.log`로 아카이브한다. +- [ ] active `PLAN-local-G07.md`를 `plan_local_G07_M.log`로 아카이브한다. +- [ ] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md`와 `agent-task/**/*.log`를 unignore하고 `agent-roadmap/current.md`를 ignore하는지 확인한다. +- [ ] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다. +- [ ] PASS이면 active task 디렉터리 `agent-task/m-remote-run-lifecycle/02+01_step_log_stream/`를 `agent-task/archive/YYYY/MM/m-remote-run-lifecycle/02+01_step_log_stream/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다. +- [ ] PASS이고 task group이 `m-remote-run-lifecycle`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다. +- [ ] PASS split 작업이면 이동 후 빈 active parent `agent-task/m-remote-run-lifecycle/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다. +- [ ] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-{build_lane}-GNN.md`와 `CODE_REVIEW-{review_lane}-GNN.md`를 작성하고 `complete.log`를 작성하지 않는다. +- [ ] USER_REVIEW이면 `agent-ops/skills/common/code-review/templates/user-review-template.md` 기준으로 `USER_REVIEW.md`를 작성하고 active `PLAN-*.md`, `CODE_REVIEW-*.md`, `complete.log`를 남기지 않는다. +- [ ] USER_REVIEW가 사용자 결정으로 완료/PASS 해소되면 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log`를 작성한 뒤 task directory를 archive로 이동한다. + +## 계획 대비 변경 사항 + +_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._ + +## 주요 설계 결정 + +_구현 에이전트가 주요 설계 결정 사항을 기록한다._ + +## 사용자 리뷰 요청 + +_기본값은 `없음`이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 변경 없이는 안전하게 진행할 수 없으면 아래 항목을 실제 내용으로 교체하고, 구현을 중단한 뒤 active 파일을 그대로 둔 채 리뷰를 요청한다._ + +- 상태: 없음 +- 사유 유형: 없음 +- 결정 필요: 없음 +- 차단 근거: 없음 +- 실행한 검증/명령: 없음 +- 자동 후속 불가 이유: 없음 +- 재개 조건: 없음 + +## 리뷰어를 위한 체크포인트 + +- predecessor `01`의 완료 산출물 없이는 구현이 진행되지 않았는지 확인한다. +- remote variables 병합이 기존 YAML validation과 command registry 경로를 우회하지 않는지 확인한다. +- job loop가 실패 결과도 report하고 step events/logs를 누락하지 않는지 확인한다. + +## 검증 결과 + +_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._ + +필수 규칙: +- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다. +- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다. +- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다. + +### API-1 중간 검증 +```bash +$ dart test test/oto_agent_registration_test.dart test/oto_application_test.dart +(output) +``` + +### API-2 중간 검증 +```bash +$ dart test test/oto_agent_registration_test.dart +(output) +``` + +### API-3 중간 검증 +```bash +$ dart test test/oto_server_connection_smoke_test.dart +(output) +``` + +### 최종 검증 +```bash +$ cd apps/runner && dart analyze +(output) +$ cd apps/runner && dart test test/oto_agent_registration_test.dart test/oto_application_test.dart test/oto_context_test.dart test/oto_core_test.dart test/oto_server_connection_smoke_test.dart +(output) +``` + +--- + +> **[IMPLEMENTING AGENT — BEFORE SAVING] Have you filled in every implementation-owned section: completion table, implementation checklist, changes from plan, design decisions, and verification output?** +> If anything is blank, go back and fill it in before saving this file. +> Leave review-agent-only sections unchanged. + +## Section Ownership + +| Section | Owner | Note | +|---------|-------|------| +| Header comment, 개요, 리뷰 에이전트 지시 | Fixed at stub creation | Implementing agent must not modify or execute these | +| Roadmap Targets | Fixed at stub creation from plan | Implementing agent must not modify | +| 구현 항목별 완료 여부 | Implementing agent | Check `[ ]` to `[x]` only | +| 구현 체크리스트 | Implementing agent | Check `[ ]` to `[x]` only | +| 코드리뷰 전용 체크리스트 | Review agent only | Implementing agent must not modify | +| 계획 대비 변경 사항, 주요 설계 결정 | Implementing agent | Replace placeholder text | +| 사용자 리뷰 요청 | Implementing agent | Keep `상태: 없음` unless user input is required | +| 리뷰어를 위한 체크포인트 | Fixed at stub creation | Review focus | +| 검증 결과 | Implementing agent | Paste actual command output | +| 코드리뷰 결과 | Review agent appends | Not included in stub | diff --git a/agent-task/m-remote-run-lifecycle/02+01_step_log_stream/PLAN-local-G07.md b/agent-task/m-remote-run-lifecycle/02+01_step_log_stream/PLAN-local-G07.md new file mode 100644 index 0000000..b324ecd --- /dev/null +++ b/agent-task/m-remote-run-lifecycle/02+01_step_log_stream/PLAN-local-G07.md @@ -0,0 +1,223 @@ + + +# Plan - API + +## 이 파일을 읽는 구현 에이전트에게 + +구현 완료의 마지막 단계는 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채우는 것이다. 검증 명령을 실행하고 실제 출력, 계획 대비 변경, 주요 설계 결정을 기록한 뒤 active 파일을 그대로 두고 리뷰 준비를 보고한다. 구현 중 사용자 전용 결정, 사용자 소유 환경/secret, 범위 충돌이 생기면 review stub의 `사용자 리뷰 요청` 섹션에 정확한 근거를 남기고 중단한다. 구현 에이전트는 사용자에게 직접 묻거나 선택지를 제시하거나 `request_user_input`을 호출하거나, `USER_REVIEW.md`/`complete.log`를 만들거나, 로그 아카이브를 수행하지 않는다. + +## 배경 + +서버와 Dart client에는 claim/report/log API 조각이 있지만, `DefaultAgentRunner`의 기본 실행 경로는 등록 후 job loop를 실행하지 않는다. `Application.build(BuildType.file, yamlContent: ...)`는 기존 YAML 파이프라인 실행과 step event 결과를 이미 제공한다. 이 계획은 claim된 `RunRequest`를 YAML 실행 입력으로 변환하고 결과와 step events를 OTO Server에 보고하는 runner loop를 만든다. + +## 사용자 리뷰 요청 흐름 + +구현 중 차단은 active review stub의 `사용자 리뷰 요청` 섹션에 기록한다. 직접 사용자 프롬프트는 금지되며, code-review가 요청 타당성을 검증하고 실제 `USER_REVIEW.md` 작성 여부를 결정한다. + +## Roadmap Targets + +- Milestone: `agent-roadmap/phase/message-based-build-agent/milestones/remote-run-lifecycle.md` +- Task ids: + - `run-input`: run request가 pipeline 실행 입력으로 변환되는 기준이 정의되어 있다. + - `step-log`: step event와 log stream이 원격 소비자에게 전달되는 흐름이 정의되어 있다. +- Completion mode: check-on-pass + +## 분석 결과 + +### 읽은 파일 + +- `agent-task/m-remote-run-lifecycle/01_run_request_pipeline/PLAN-cloud-G07.md` +- `agent-ops/rules/project/rules.md` +- `agent-ops/rules/common/rules-roadmap.md` +- `agent-ops/skills/common/plan/SKILL.md` +- `agent-ops/rules/project/domain/agent/rules.md` +- `agent-ops/rules/project/domain/core/rules.md` +- `agent-ops/rules/project/domain/pipeline/rules.md` +- `agent-test/local/rules.md` +- `agent-test/local/agent-smoke.md` +- `agent-test/local/core-smoke.md` +- `agent-test/local/pipeline-smoke.md` +- `agent-roadmap/current.md` +- `agent-roadmap/phase/message-based-build-agent/PHASE.md` +- `agent-roadmap/phase/message-based-build-agent/milestones/remote-run-lifecycle.md` +- `apps/runner/pubspec.yaml` +- `apps/runner/lib/oto/agent/agent_runner.dart` +- `apps/runner/lib/oto/agent/edge_registration_client.dart` +- `apps/runner/lib/oto/agent/oto_server_job_client.dart` +- `apps/runner/lib/oto/application.dart` +- `apps/runner/lib/oto/core/build_result.dart` +- `apps/runner/lib/oto/core/data_composer.dart` +- `apps/runner/lib/oto/core/execution_context.dart` +- `apps/runner/lib/oto/pipeline/pipeline.dart` +- `apps/runner/lib/cli/commands/command_exe.dart` +- `apps/runner/test/oto_agent_registration_test.dart` +- `apps/runner/test/oto_server_connection_smoke_test.dart` +- `apps/runner/test/oto_application_test.dart` +- `apps/runner/test/oto_context_test.dart` +- `apps/runner/test/oto_core_test.dart` + +### 테스트 환경 규칙 + +- test_env: `local`. +- 읽은 프로파일: `agent-smoke`, `core-smoke`, `pipeline-smoke`. +- 필수 baseline: `dart analyze`, agent 관련 `dart test`, core 관련 `dart test`, pipeline 영향 시 `dart test` 전체 또는 관련 회귀. +- Go service는 이 plan의 직접 변경 대상이 아니며, `01` 완료 결과를 전제로 한다. +- 사전 검증으로 `dart test test/oto_agent_registration_test.dart test/oto_application_test.dart test/oto_context_test.dart test/oto_core_test.dart`와 `dart test test/oto_server_connection_smoke_test.dart`가 통과했다. + +### 테스트 커버리지 공백 + +- `AgentRunner`는 주입된 `runJobs` 훅 실행만 테스트하고 기본 remote job loop는 없다. +- `OtoServerJobClient`는 claim/report/log/artifact 호출만 테스트하며, claim result의 `RunRequest`를 실행하지 않는다. +- `Application.build`의 step events는 테스트되지만 OTO Server report payload로 자동 전송되는 end-to-end 테스트가 없다. +- log stream은 현재 명시 `appendLog` 호출만 검증하고, runner 실행 시작/완료 로그와 BuildResult message 보고 흐름은 자동화되어 있지 않다. + +### 심볼 참조 + +- renamed/removed symbol: none. +- 변경 후보 call site: `DefaultAgentRunner` 생성은 agent CLI와 테스트에서 간접 사용된다. `OtoServerJobSession.jobs`는 `oto_agent_registration_test.dart`와 `oto_server_connection_smoke_test.dart`에서 확인했다. + +### 분할 판단 + +- split decision policy를 먼저 평가했다. +- 이 subtask는 `02+01_step_log_stream`이므로 predecessor `01`이 필요하다. +- 현재 active `agent-task/m-remote-run-lifecycle/01_run_request_pipeline/complete.log`가 없고 archive predecessor도 확인하지 않았다. predecessor `01`: missing. +- `01`이 claim response에 `RunRequest`를 보장해야 이 plan을 구현할 수 있다. + +### 범위 결정 근거 + +- 이 계획은 runner가 claim한 run request를 실행하고 logs/step events/result를 보고하는 데 집중한다. +- artifact 자동 추출/보고는 `03+01_artifact_event`로 분리한다. +- cancel/status/self-update는 `04+01_cancel_status_update`로 분리한다. +- server claim 계약 변경은 `01_run_request_pipeline`의 완료 결과를 사용한다. + +### 빌드 등급 + +- build: `local-G07`. Dart runner/core/pipeline 경계가 넓지만 기존 `Application.build`와 deterministic tests로 검증 가능하다. +- review: `cloud-G06`. 서버 계약 선행 여부와 runner loop 증거를 함께 확인해야 한다. + +## 구현 체크리스트 + +- [ ] `01_run_request_pipeline`의 `complete.log`가 존재하는지 확인하고 없으면 구현을 시작하지 않는다. +- [ ] `RunRequest`를 inline YAML 또는 workspace-confined file path 중 하나로 해석하고 variables를 pipeline property 입력으로 병합한다. +- [ ] runner job loop가 claim -> 실행 -> log append -> execution report 순서를 보장한다. +- [ ] `BuildResult.stepEvents`가 `ExecutionReportRequest.step_events`와 `execution_result.stepEvents`에 보존된다. 검증: OTO Server에서 요청한 빌드/배포 작업의 시작, 진행, 완료, 실패, 취소 상태를 추적할 수 있다. +- [ ] agent/core/pipeline 테스트와 server connection smoke를 갱신한다. +- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. + +## 의존 관계 및 구현 순서 + +- Runtime dependency: sibling `01_run_request_pipeline` must produce `complete.log`. +- 이 plan은 `agent-task/m-remote-run-lifecycle/02+01_step_log_stream` 이름의 `+01` 의존성만 가진다. 추가 의존성을 plan 본문에 숨기지 않는다. + +### [API-1] RunRequest To DataBuild Input + +문제: [apps/runner/lib/oto/application.dart](/config/workspace/oto/apps/runner/lib/oto/application.dart:76)는 `BuildType.file`에서 YAML 문자열 실행은 지원하지만 remote variables 병합과 workspace-confined path 해석 기준은 없다. + +Before: + +```dart +// apps/runner/lib/oto/application.dart:76 +Future build(BuildType buildType, + {String? yamlContent, + DataBuild? buildData, + bool logEnable = true, + String? logPath}) async { +``` + +해결 방법: 새 helper `RemoteRunInput` 또는 `OtoRemoteRunExecutor`를 만들어 `oto.RunRequest`를 검증한다. `pipelineYaml`이 있으면 inline을 우선하고, `pipelineYamlPath`는 `AgentRuntimeConfig.workspaceRoot` 아래의 정규화된 파일만 허용한다. `variables`는 parsed YAML map의 `property`에 병합한 뒤 기존 `DataBuild.fromJson`/`Application.build` 경로를 재사용한다. 필요하면 `BuildType.remote`를 추가해 `DataBuild`를 log file 없이 실행한다. + +수정 파일 및 체크리스트: + +- [ ] `apps/runner/lib/oto/agent/remote_run_executor.dart` 추가 또는 기존 agent 파일에 작게 포함한다. +- [ ] `apps/runner/lib/oto/application.dart`: `BuildType.remote`가 필요하면 최소 분기 추가. +- [ ] `apps/runner/test/oto_agent_registration_test.dart`: inline YAML, path confinement, variables merge 테스트 추가. + +테스트 작성: 정상 inline YAML, both inline/path rejection, path traversal rejection, variables property merge 테스트를 추가한다. + +중간 검증: + +```bash +dart test test/oto_agent_registration_test.dart test/oto_application_test.dart +``` + +### [API-2] Remote Job Loop + +문제: [apps/runner/lib/oto/agent/agent_runner.dart](/config/workspace/oto/apps/runner/lib/oto/agent/agent_runner.dart:63)는 `_runJobs`가 주입된 경우만 실행한다. 기본 runner는 등록 후 remote job을 claim하지 않는다. + +Before: + +```dart +// apps/runner/lib/oto/agent/agent_runner.dart:63 +if (_runJobs != null) { + await _runJobs(config, session); +} +``` + +해결 방법: `OtoRemoteRunLoop`를 기본 job loop로 연결한다. loop는 accepted `OtoServerJobSession`에서만 동작하고, `claimNextJob()` 또는 `claimJob()` 결과가 empty/no job이면 interval 후 재시도한다. 테스트 가능한 `runOnce`를 제공해 infinite loop 없이 검증한다. + +수정 파일 및 체크리스트: + +- [ ] `apps/runner/lib/oto/agent/agent_runner.dart`: default run loop 주입 또는 생성. +- [ ] `apps/runner/lib/oto/agent/oto_server_job_client.dart`: `claimNextJob()`와 no-job response 파싱 추가. +- [ ] `apps/runner/test/oto_agent_registration_test.dart`: default runner가 run loop를 호출하고 session close를 유지하는 테스트 수정. + +테스트 작성: fake client/server로 `claim -> execute -> report` 순서와 no-job no-op을 검증한다. + +중간 검증: + +```bash +dart test test/oto_agent_registration_test.dart +``` + +### [API-3] Step Events And Logs Report + +문제: [apps/runner/lib/oto/pipeline/pipeline.dart](/config/workspace/oto/apps/runner/lib/oto/pipeline/pipeline.dart:77)는 started/completed/failed step events를 기록하지만, [apps/runner/lib/oto/agent/oto_server_job_client.dart](/config/workspace/oto/apps/runner/lib/oto/agent/oto_server_job_client.dart:107)의 report는 호출자가 직접 불러야 한다. + +Before: + +```dart +// apps/runner/lib/oto/agent/oto_server_job_client.dart:107 +Future reportExecution({ + required String jobId, + required String executionId, + required BuildResult result, +}) async { +``` + +해결 방법: remote loop가 실행 시작 로그를 `appendLog`로 보내고, `Application.build`가 반환한 `BuildResult`를 항상 `reportExecution`으로 보낸다. 실패 결과도 success=false, exitCode=10, stepEvents 포함으로 보고한다. report message는 server가 기존처럼 execution log에 append한다. + +수정 파일 및 체크리스트: + +- [ ] `apps/runner/lib/oto/agent/remote_run_executor.dart`: 시작/완료/실패 로그와 report 순서 구현. +- [ ] `apps/runner/test/oto_server_connection_smoke_test.dart`: 실제 Go server와 Dart runner loop가 inline YAML job을 실행하고 logs/stepEvents/result를 확인하는 smoke 추가. + +테스트 작성: smoke에서 `POST /api/v1/jobs`에 inline YAML run_request를 넣고 runner `runOnce`가 job state succeeded, logs, step_events를 남기는지 확인한다. + +중간 검증: + +```bash +dart test test/oto_server_connection_smoke_test.dart +``` + +## 수정 파일 요약 + +| 파일 | 항목 | +|------|------| +| `apps/runner/lib/oto/agent/remote_run_executor.dart` | API-1, API-2, API-3 | +| `apps/runner/lib/oto/agent/agent_runner.dart` | API-2 | +| `apps/runner/lib/oto/agent/oto_server_job_client.dart` | API-2, API-3 | +| `apps/runner/lib/oto/application.dart` | API-1 | +| `apps/runner/test/oto_agent_registration_test.dart` | API-1, API-2 | +| `apps/runner/test/oto_application_test.dart` | API-1 | +| `apps/runner/test/oto_server_connection_smoke_test.dart` | API-3 | + +## 최종 검증 + +```bash +cd apps/runner && dart analyze +cd apps/runner && dart test test/oto_agent_registration_test.dart test/oto_application_test.dart test/oto_context_test.dart test/oto_core_test.dart test/oto_server_connection_smoke_test.dart +``` + +예상 결과: 모든 명령이 exit code 0으로 종료한다. Dart test cache 개념은 적용하지 않는다. + +모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다. diff --git a/agent-task/m-remote-run-lifecycle/03+01,02_artifact_event/CODE_REVIEW-local-G06.md b/agent-task/m-remote-run-lifecycle/03+01,02_artifact_event/CODE_REVIEW-local-G06.md new file mode 100644 index 0000000..0840478 --- /dev/null +++ b/agent-task/m-remote-run-lifecycle/03+01,02_artifact_event/CODE_REVIEW-local-G06.md @@ -0,0 +1,157 @@ + + +# Code Review Reference - API + +> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.** +> The task is NOT complete until every implementation-owned section below is filled in. +> Complete the `구현 체크리스트`; the final checklist item is mandatory before saving. +> Fill implementation-owned sections, then stop with active files in place and report ready for review. +> If implementation is blocked by a user-only decision, user-owned external environment prerequisite, or scope conflict, fill `사용자 리뷰 요청` with evidence and stop with active files in place; code-review decides whether to write `USER_REVIEW.md`. +> Do not ask the user directly, present choices in chat, or call `request_user_input` during implementation; record the needed decision in `사용자 리뷰 요청` and stop for code-review. +> Finalization (`코드리뷰 결과`, log rename, `complete.log`, archive moves, `코드리뷰 전용 체크리스트`) is review-agent-only, even after compaction/resume. +> Follow the ownership table at the bottom of this file for which sections you own. + +## 개요 + +date=2026-06-06 +task=m-remote-run-lifecycle/03+01,02_artifact_event, plan=0, tag=API + +## 이 파일을 읽는 리뷰 에이전트에게 + +> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다. + +각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요. +리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다. + +1. 판정을 append한다. +2. `CODE_REVIEW-local-G06.md` -> `code_review_local_G06_N.log`, `PLAN-local-G06.md` -> `plan_local_G06_M.log`로 아카이브한다. +3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-remote-run-lifecycle/03+01,02_artifact_event/`로 이동한다. WARN/FAIL이면 user-review gate를 확인한 뒤 다음 active plan/review 파일 또는 `USER_REVIEW.md`를 작성한다. +4. PASS이고 task group이 `m-remote-run-lifecycle`이면 완료 이벤트 메타데이터를 보고한다. roadmap 상태 체크와 `update-roadmap` 호출은 런타임 책임이다. +5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다. + +--- + +## Roadmap Targets + +- Milestone: `agent-roadmap/phase/message-based-build-agent/milestones/remote-run-lifecycle.md` +- Task ids: + - `artifact-event` +- Completion mode: check-on-pass + +## 구현 항목별 완료 여부 + +| 항목 | 완료 여부 | +|------|---------| +| [API-1] BuildResult Artifact Model | [ ] | +| [API-2] Artifact Declaration Extraction | [ ] | +| [API-3] Remote Artifact Reporting | [ ] | + +## 구현 체크리스트 + +- [ ] `01_run_request_pipeline`과 `02+01_step_log_stream`의 `complete.log`가 존재하는지 확인하고 없으면 구현을 시작하지 않는다. +- [ ] artifact metadata는 name/path 필수, path는 빈 값 금지로 검증한다. +- [ ] `BuildResult`가 artifact 목록을 보존하고 `toJson()`에 포함한다. +- [ ] remote loop가 build 성공/실패와 별개로 산출된 artifact 목록을 서버에 보고한다. +- [ ] artifact report 실패는 execution report와 구분되는 명확한 실패 결과 또는 로그로 남긴다. +- [ ] agent/core/pipeline/server smoke 테스트를 갱신한다. +- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. + +## 코드리뷰 전용 체크리스트 + +> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다. +> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다. + +- [ ] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다. +- [ ] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다. +- [ ] active `CODE_REVIEW-local-G06.md`를 `code_review_local_G06_N.log`로 아카이브한다. +- [ ] active `PLAN-local-G06.md`를 `plan_local_G06_M.log`로 아카이브한다. +- [ ] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md`와 `agent-task/**/*.log`를 unignore하고 `agent-roadmap/current.md`를 ignore하는지 확인한다. +- [ ] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다. +- [ ] PASS이면 active task 디렉터리 `agent-task/m-remote-run-lifecycle/03+01,02_artifact_event/`를 `agent-task/archive/YYYY/MM/m-remote-run-lifecycle/03+01,02_artifact_event/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다. +- [ ] PASS이고 task group이 `m-remote-run-lifecycle`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다. +- [ ] PASS split 작업이면 이동 후 빈 active parent `agent-task/m-remote-run-lifecycle/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다. +- [ ] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-{build_lane}-GNN.md`와 `CODE_REVIEW-{review_lane}-GNN.md`를 작성하고 `complete.log`를 작성하지 않는다. +- [ ] USER_REVIEW이면 `agent-ops/skills/common/code-review/templates/user-review-template.md` 기준으로 `USER_REVIEW.md`를 작성하고 active `PLAN-*.md`, `CODE_REVIEW-*.md`, `complete.log`를 남기지 않는다. +- [ ] USER_REVIEW가 사용자 결정으로 완료/PASS 해소되면 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log`를 작성한 뒤 task directory를 archive로 이동한다. + +## 계획 대비 변경 사항 + +_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._ + +## 주요 설계 결정 + +_구현 에이전트가 주요 설계 결정 사항을 기록한다._ + +## 사용자 리뷰 요청 + +_기본값은 `없음`이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 변경 없이는 안전하게 진행할 수 없으면 아래 항목을 실제 내용으로 교체하고, 구현을 중단한 뒤 active 파일을 그대로 둔 채 리뷰를 요청한다._ + +- 상태: 없음 +- 사유 유형: 없음 +- 결정 필요: 없음 +- 차단 근거: 없음 +- 실행한 검증/명령: 없음 +- 자동 후속 불가 이유: 없음 +- 재개 조건: 없음 + +## 리뷰어를 위한 체크포인트 + +- predecessor `01`, `02`의 완료 산출물 없이는 구현이 진행되지 않았는지 확인한다. +- artifact path 검증이 workspace 밖 경로를 조용히 허용하지 않는지 확인한다. +- remote loop가 build result artifacts를 server artifact endpoint에 실제로 남기는지 smoke evidence를 확인한다. + +## 검증 결과 + +_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._ + +필수 규칙: +- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다. +- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다. +- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다. + +### API-1 중간 검증 +```bash +$ dart test test/oto_core_test.dart test/oto_application_test.dart +(output) +``` + +### API-2 중간 검증 +```bash +$ dart test test/oto_agent_registration_test.dart +(output) +``` + +### API-3 중간 검증 +```bash +$ dart test test/oto_server_connection_smoke_test.dart +(output) +``` + +### 최종 검증 +```bash +$ cd apps/runner && dart analyze +(output) +$ cd apps/runner && dart test test/oto_agent_registration_test.dart test/oto_application_test.dart test/oto_context_test.dart test/oto_core_test.dart test/oto_server_connection_smoke_test.dart +(output) +``` + +--- + +> **[IMPLEMENTING AGENT — BEFORE SAVING] Have you filled in every implementation-owned section: completion table, implementation checklist, changes from plan, design decisions, and verification output?** +> If anything is blank, go back and fill it in before saving this file. +> Leave review-agent-only sections unchanged. + +## Section Ownership + +| Section | Owner | Note | +|---------|-------|------| +| Header comment, 개요, 리뷰 에이전트 지시 | Fixed at stub creation | Implementing agent must not modify or execute these | +| Roadmap Targets | Fixed at stub creation from plan | Implementing agent must not modify | +| 구현 항목별 완료 여부 | Implementing agent | Check `[ ]` to `[x]` only | +| 구현 체크리스트 | Implementing agent | Check `[ ]` to `[x]` only | +| 코드리뷰 전용 체크리스트 | Review agent only | Implementing agent must not modify | +| 계획 대비 변경 사항, 주요 설계 결정 | Implementing agent | Replace placeholder text | +| 사용자 리뷰 요청 | Implementing agent | Keep `상태: 없음` unless user input is required | +| 리뷰어를 위한 체크포인트 | Fixed at stub creation | Review focus | +| 검증 결과 | Implementing agent | Paste actual command output | +| 코드리뷰 결과 | Review agent appends | Not included in stub | diff --git a/agent-task/m-remote-run-lifecycle/03+01,02_artifact_event/PLAN-local-G06.md b/agent-task/m-remote-run-lifecycle/03+01,02_artifact_event/PLAN-local-G06.md new file mode 100644 index 0000000..d6eba87 --- /dev/null +++ b/agent-task/m-remote-run-lifecycle/03+01,02_artifact_event/PLAN-local-G06.md @@ -0,0 +1,203 @@ + + +# Plan - API + +## 이 파일을 읽는 구현 에이전트에게 + +구현 완료의 마지막 단계는 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채우는 것이다. 검증 명령을 실행하고 실제 출력, 계획 대비 변경, 주요 설계 결정을 기록한 뒤 active 파일을 그대로 두고 리뷰 준비를 보고한다. 구현 중 사용자 전용 결정, 사용자 소유 환경/secret, 범위 충돌이 생기면 review stub의 `사용자 리뷰 요청` 섹션에 정확한 근거를 남기고 중단한다. 구현 에이전트는 사용자에게 직접 묻거나 선택지를 제시하거나 `request_user_input`을 호출하거나, `USER_REVIEW.md`/`complete.log`를 만들거나, 로그 아카이브를 수행하지 않는다. + +## 배경 + +OTO Server와 Dart job client에는 artifact report API가 이미 있다. 하지만 `Application.build` 결과에는 artifact 목록이 없고, remote job loop가 빌드 결과에서 artifact event를 자동 보고하는 기준도 없다. 이 계획은 기존 서버 API를 유지하면서 runner 결과 모델과 remote loop에 artifact event 전달 흐름을 붙인다. + +## 사용자 리뷰 요청 흐름 + +구현 중 차단은 active review stub의 `사용자 리뷰 요청` 섹션에 기록한다. 직접 사용자 프롬프트는 금지되며, code-review가 요청 타당성을 검증하고 실제 `USER_REVIEW.md` 작성 여부를 결정한다. + +## Roadmap Targets + +- Milestone: `agent-roadmap/phase/message-based-build-agent/milestones/remote-run-lifecycle.md` +- Task ids: + - `artifact-event`: artifact event와 최종 artifact metadata가 원격 소비자에게 전달되는 흐름이 정의되어 있다. +- Completion mode: check-on-pass + +## 분석 결과 + +### 읽은 파일 + +- `agent-task/m-remote-run-lifecycle/01_run_request_pipeline/PLAN-cloud-G07.md` +- `agent-task/m-remote-run-lifecycle/02+01_step_log_stream/PLAN-local-G07.md` +- `agent-ops/rules/project/rules.md` +- `agent-ops/rules/common/rules-roadmap.md` +- `agent-ops/skills/common/plan/SKILL.md` +- `agent-ops/rules/project/domain/agent/rules.md` +- `agent-ops/rules/project/domain/core/rules.md` +- `agent-ops/rules/project/domain/pipeline/rules.md` +- `agent-test/local/rules.md` +- `agent-test/local/agent-smoke.md` +- `agent-test/local/core-smoke.md` +- `agent-test/local/pipeline-smoke.md` +- `agent-roadmap/current.md` +- `agent-roadmap/phase/message-based-build-agent/PHASE.md` +- `agent-roadmap/phase/message-based-build-agent/milestones/remote-run-lifecycle.md` +- `services/core/internal/httpserver/server.go` +- `apps/runner/lib/oto/agent/agent_runner.dart` +- `apps/runner/lib/oto/agent/oto_server_job_client.dart` +- `apps/runner/lib/oto/application.dart` +- `apps/runner/lib/oto/core/build_result.dart` +- `apps/runner/lib/oto/core/data_composer.dart` +- `apps/runner/lib/oto/pipeline/pipeline.dart` +- `apps/runner/test/oto_agent_registration_test.dart` +- `apps/runner/test/oto_server_connection_smoke_test.dart` +- `apps/runner/test/oto_application_test.dart` +- `apps/runner/test/oto_core_test.dart` + +### 테스트 환경 규칙 + +- test_env: `local`. +- 읽은 프로파일: `agent-smoke`, `core-smoke`, `pipeline-smoke`. +- 직접 변경 대상은 Dart runner/core/pipeline이다. +- 서버 artifact endpoint는 이미 존재하므로 Go 변경은 기본 범위가 아니다. 서버 응답 계약이 부족한 것이 발견되면 계획 대비 변경에 기록하고 `cd services/core && go test ./...`를 최종 검증에 추가한다. +- 사전 검증으로 Dart agent/core/pipeline/server smoke 테스트가 통과했다. + +### 테스트 커버리지 공백 + +- `BuildResult`는 success/exitCode/error/stepEvents만 직렬화하고 artifact metadata를 담지 않는다. +- `OtoServerJobClient.reportArtifact`는 단건 보고만 제공하며 build result의 artifacts 목록을 일괄 보고하지 않는다. +- server smoke는 수동 artifact report를 검증하지만, remote job loop가 build 결과 artifact를 보고하는 end-to-end 테스트가 없다. + +### 심볼 참조 + +- renamed/removed symbol: none. +- 변경 후보 call site: `BuildResult.success`, `BuildResult.failure`, `BuildResult.toJson`, `OtoServerJobClient.reportArtifact`, 후속 `02`의 remote loop executor. + +### 분할 판단 + +- split decision policy를 먼저 평가했다. +- 이 subtask는 `03+01,02_artifact_event`이므로 predecessor `01`, `02`가 필요하다. +- 현재 active `agent-task/m-remote-run-lifecycle/01_run_request_pipeline/complete.log`와 `agent-task/m-remote-run-lifecycle/02+01_step_log_stream/complete.log`가 없고 archive predecessor도 확인하지 않았다. predecessor `01`: missing, predecessor `02`: missing. +- `01`은 claim/run request 계약을 제공하고, `02`는 remote loop의 claim/execution/report 흐름을 제공한다. artifact event는 그 흐름에 붙어야 하므로 추가 숨은 의존성을 두지 않는다. + +### 범위 결정 근거 + +- 이 계획은 artifact metadata 모델, artifact 선언 파싱, remote loop의 reportArtifact 호출까지 다룬다. +- artifact 파일 업로드/보관소 구현은 현재 server API가 path metadata만 받으므로 범위 밖이다. +- cancel/status/self-update는 `04+01,02_cancel_status_update`로 분리한다. + +### 빌드 등급 + +- build: `local-G06`. Dart 모델과 runner loop 보강이며 기존 server artifact API를 재사용한다. +- review: `local-G06`. predecessor 산출물과 server smoke 결과를 확인해야 한다. + +## 구현 체크리스트 + +- [ ] `01_run_request_pipeline`과 `02+01_step_log_stream`의 `complete.log`가 존재하는지 확인하고 없으면 구현을 시작하지 않는다. +- [ ] artifact metadata는 name/path 필수, path는 빈 값 금지로 검증한다. +- [ ] `BuildResult`가 artifact 목록을 보존하고 `toJson()`에 포함한다. +- [ ] remote loop가 build 성공/실패와 별개로 산출된 artifact 목록을 서버에 보고한다. +- [ ] artifact report 실패는 execution report와 구분되는 명확한 실패 결과 또는 로그로 남긴다. +- [ ] agent/core/pipeline/server smoke 테스트를 갱신한다. +- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. + +## 의존 관계 및 구현 순서 + +- Runtime dependency: sibling `01_run_request_pipeline` must produce `complete.log`. +- Runtime dependency: sibling `02+01_step_log_stream` must produce `complete.log`. +- 이 plan은 `agent-task/m-remote-run-lifecycle/03+01,02_artifact_event` 이름의 `+01,02` 의존성만 가진다. 추가 의존성을 plan 본문에 숨기지 않는다. + +### [API-1] BuildResult Artifact Model + +문제: [apps/runner/lib/oto/core/build_result.dart](/config/workspace/oto/apps/runner/lib/oto/core/build_result.dart:3)의 `BuildResult`는 artifact metadata를 보존하지 않는다. + +Before: + +```dart +class BuildResult { + final bool success; + final int exitCode; + final Object? error; + final StackTrace? stackTrace; + final List stepEvents; +``` + +해결 방법: `BuildArtifact` value class를 추가하고 `BuildResult.success`/`failure`에 `artifacts = const []` optional parameter를 둔다. `toJson()`에는 `artifacts` 배열을 포함한다. 기존 생성자 호출은 모두 source compatible하게 유지한다. + +수정 파일 및 체크리스트: + +- [ ] `apps/runner/lib/oto/core/build_result.dart`: `BuildArtifact`와 `BuildResult.artifacts` 추가. +- [ ] `apps/runner/test/oto_core_test.dart` 또는 `apps/runner/test/oto_application_test.dart`: `toJson()` artifact serialization 테스트 추가. + +중간 검증: + +```bash +dart test test/oto_core_test.dart test/oto_application_test.dart +``` + +### [API-2] Artifact Declaration Extraction + +문제: YAML pipeline 실행 결과에서 어떤 파일을 artifact로 보고할지에 대한 runner-side 기준이 없다. + +해결 방법: remote executor가 build YAML의 `property.artifacts`를 artifact 선언으로 인정한다. 형식은 list of map이며 각 entry는 `name`, `path`를 가진다. `path`는 `Application.defaultWorkspace` 기준 상대 경로 또는 workspace-confined 절대 경로만 허용한다. 유효하지 않은 artifact 선언은 build 실패로 처리해 조용히 누락하지 않는다. + +예시: + +```yaml +property: + artifacts: + - name: smoke-report + path: dist/report.json +``` + +수정 파일 및 체크리스트: + +- [ ] `apps/runner/lib/oto/agent/remote_run_executor.dart`: artifact declaration parser 추가. +- [ ] `apps/runner/lib/oto/application.dart`: 필요 시 build result 생성 시 artifact 목록을 받을 수 있는 내부 hook 추가. +- [ ] `apps/runner/test/oto_agent_registration_test.dart`: artifact parser 정상/오류/path confinement 테스트 추가. + +중간 검증: + +```bash +dart test test/oto_agent_registration_test.dart +``` + +### [API-3] Remote Artifact Reporting + +문제: [apps/runner/lib/oto/agent/oto_server_job_client.dart](/config/workspace/oto/apps/runner/lib/oto/agent/oto_server_job_client.dart:151)는 단건 `reportArtifact`만 제공하고 remote loop가 자동 호출하지 않는다. + +해결 방법: `OtoServerJobClient.reportArtifacts` helper를 추가하고 remote loop가 `BuildResult.artifacts`를 순서대로 보고한다. 보고 순서는 execution report 이후로 고정한다. artifact report 실패는 log append 후 execution failure로 재보고할 수 없으므로, 실패를 throw하고 caller test에서 HTTP 오류가 드러나게 한다. + +수정 파일 및 체크리스트: + +- [ ] `apps/runner/lib/oto/agent/oto_server_job_client.dart`: `reportArtifacts` 추가. +- [ ] `apps/runner/lib/oto/agent/remote_run_executor.dart`: build result artifacts 보고 연결. +- [ ] `apps/runner/test/oto_server_connection_smoke_test.dart`: inline YAML job이 artifact metadata를 server artifacts endpoint에 남기는 smoke 추가. + +중간 검증: + +```bash +dart test test/oto_server_connection_smoke_test.dart +``` + +## 수정 파일 요약 + +| 파일 | 항목 | +|------|------| +| `apps/runner/lib/oto/core/build_result.dart` | API-1 | +| `apps/runner/lib/oto/agent/remote_run_executor.dart` | API-2, API-3 | +| `apps/runner/lib/oto/application.dart` | API-2 | +| `apps/runner/lib/oto/agent/oto_server_job_client.dart` | API-3 | +| `apps/runner/test/oto_core_test.dart` | API-1 | +| `apps/runner/test/oto_application_test.dart` | API-1, API-2 | +| `apps/runner/test/oto_agent_registration_test.dart` | API-2 | +| `apps/runner/test/oto_server_connection_smoke_test.dart` | API-3 | + +## 최종 검증 + +```bash +cd apps/runner && dart analyze +cd apps/runner && dart test test/oto_agent_registration_test.dart test/oto_application_test.dart test/oto_context_test.dart test/oto_core_test.dart test/oto_server_connection_smoke_test.dart +``` + +예상 결과: 모든 명령이 exit code 0으로 종료한다. + +모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다. diff --git a/agent-task/m-remote-run-lifecycle/04+01,02_cancel_status_update/CODE_REVIEW-cloud-G07.md b/agent-task/m-remote-run-lifecycle/04+01,02_cancel_status_update/CODE_REVIEW-cloud-G07.md new file mode 100644 index 0000000..b3bdab3 --- /dev/null +++ b/agent-task/m-remote-run-lifecycle/04+01,02_cancel_status_update/CODE_REVIEW-cloud-G07.md @@ -0,0 +1,168 @@ + + +# Code Review Reference - API + +> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.** +> The task is NOT complete until every implementation-owned section below is filled in. +> Complete the `구현 체크리스트`; the final checklist item is mandatory before saving. +> Fill implementation-owned sections, then stop with active files in place and report ready for review. +> If implementation is blocked by a user-only decision, user-owned external environment prerequisite, or scope conflict, fill `사용자 리뷰 요청` with evidence and stop with active files in place; code-review decides whether to write `USER_REVIEW.md`. +> Do not ask the user directly, present choices in chat, or call `request_user_input` during implementation; record the needed decision in `사용자 리뷰 요청` and stop for code-review. +> Finalization (`코드리뷰 결과`, log rename, `complete.log`, archive moves, `코드리뷰 전용 체크리스트`) is review-agent-only, even after compaction/resume. +> Follow the ownership table at the bottom of this file for which sections you own. + +## 개요 + +date=2026-06-06 +task=m-remote-run-lifecycle/04+01,02_cancel_status_update, plan=0, tag=API + +## 이 파일을 읽는 리뷰 에이전트에게 + +> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다. + +각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요. +리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다. + +1. 판정을 append한다. +2. `CODE_REVIEW-cloud-G07.md` -> `code_review_cloud_G07_N.log`, `PLAN-cloud-G07.md` -> `plan_cloud_G07_M.log`로 아카이브한다. +3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-remote-run-lifecycle/04+01,02_cancel_status_update/`로 이동한다. WARN/FAIL이면 user-review gate를 확인한 뒤 다음 active plan/review 파일 또는 `USER_REVIEW.md`를 작성한다. +4. PASS이고 task group이 `m-remote-run-lifecycle`이면 완료 이벤트 메타데이터를 보고한다. roadmap 상태 체크와 `update-roadmap` 호출은 런타임 책임이다. +5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다. + +--- + +## Roadmap Targets + +- Milestone: `agent-roadmap/phase/message-based-build-agent/milestones/remote-run-lifecycle.md` +- Task ids: + - `cancel-status-update` +- Completion mode: check-on-pass + +## 구현 항목별 완료 여부 + +| 항목 | 완료 여부 | +|------|---------| +| [API-1] Cancellable State Transitions | [ ] | +| [API-2] HTTP Cancel And Status Endpoints | [ ] | +| [API-3] Self-Update Stability Contract | [ ] | +| [API-4] Dart Lifecycle Client Helpers | [ ] | + +## 구현 체크리스트 + +- [ ] `01_run_request_pipeline`과 `02+01_step_log_stream`의 `complete.log`가 존재하는지 확인하고 없으면 구현을 시작하지 않는다. +- [ ] running job/execution을 canceled로 전이할 수 있고 terminal state는 변경하지 않는다. +- [ ] cancel endpoint는 runner/job/execution id mismatch를 거부하고, 성공 시 job과 execution state를 모두 `canceled`로 맞춘다. +- [ ] status endpoint는 runner registry 상태와 현재/마지막 execution 상태를 반환한다. +- [ ] self-update endpoint는 active execution 중에는 defer/reject하고, download URL은 HTTPS만 허용한다. 실제 바이너리 교체는 하지 않는다. +- [ ] Dart job client가 cancel/status/self-update helper를 제공하고 snake_case/camelCase JSON을 파싱한다. +- [ ] Go service와 Dart agent/server smoke 테스트를 갱신한다. +- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. + +## 코드리뷰 전용 체크리스트 + +> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다. +> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다. + +- [ ] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다. +- [ ] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다. +- [ ] active `CODE_REVIEW-cloud-G07.md`를 `code_review_cloud_G07_N.log`로 아카이브한다. +- [ ] active `PLAN-cloud-G07.md`를 `plan_cloud_G07_M.log`로 아카이브한다. +- [ ] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md`와 `agent-task/**/*.log`를 unignore하고 `agent-roadmap/current.md`를 ignore하는지 확인한다. +- [ ] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다. +- [ ] PASS이면 active task 디렉터리 `agent-task/m-remote-run-lifecycle/04+01,02_cancel_status_update/`를 `agent-task/archive/YYYY/MM/m-remote-run-lifecycle/04+01,02_cancel_status_update/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다. +- [ ] PASS이고 task group이 `m-remote-run-lifecycle`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다. +- [ ] PASS split 작업이면 이동 후 빈 active parent `agent-task/m-remote-run-lifecycle/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다. +- [ ] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-{build_lane}-GNN.md`와 `CODE_REVIEW-{review_lane}-GNN.md`를 작성하고 `complete.log`를 작성하지 않는다. +- [ ] USER_REVIEW이면 `agent-ops/skills/common/code-review/templates/user-review-template.md` 기준으로 `USER_REVIEW.md`를 작성하고 active `PLAN-*.md`, `CODE_REVIEW-*.md`, `complete.log`를 남기지 않는다. +- [ ] USER_REVIEW가 사용자 결정으로 완료/PASS 해소되면 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log`를 작성한 뒤 task directory를 archive로 이동한다. + +## 계획 대비 변경 사항 + +_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._ + +## 주요 설계 결정 + +_구현 에이전트가 주요 설계 결정 사항을 기록한다._ + +## 사용자 리뷰 요청 + +_기본값은 `없음`이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 변경 없이는 안전하게 진행할 수 없으면 아래 항목을 실제 내용으로 교체하고, 구현을 중단한 뒤 active 파일을 그대로 둔 채 리뷰를 요청한다._ + +- 상태: 없음 +- 사유 유형: 없음 +- 결정 필요: 없음 +- 차단 근거: 없음 +- 실행한 검증/명령: 없음 +- 자동 후속 불가 이유: 없음 +- 재개 조건: 없음 + +## 리뷰어를 위한 체크포인트 + +- predecessor `01`, `02`의 완료 산출물 없이는 구현이 진행되지 않았는지 확인한다. +- running -> canceled 전이는 허용하되 terminal state 변경은 막는지 확인한다. +- self-update endpoint가 실제 바이너리 교체를 수행하지 않고 최소 안정성 규칙만 정의하는지 확인한다. +- Dart smoke가 실제 server endpoint와 같은 JSON 계약을 검증하는지 확인한다. + +## 검증 결과 + +_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._ + +필수 규칙: +- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다. +- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다. +- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다. + +### API-1 중간 검증 +```bash +$ go test ./internal/cicdstate +(output) +``` + +### API-2 중간 검증 +```bash +$ go test ./internal/httpserver +(output) +``` + +### API-3 중간 검증 +```bash +$ go test ./internal/httpserver +(output) +``` + +### API-4 중간 검증 +```bash +$ dart test test/oto_agent_registration_test.dart test/oto_server_connection_smoke_test.dart +(output) +``` + +### 최종 검증 +```bash +$ cd services/core && go test ./... +(output) +$ cd apps/runner && dart analyze +(output) +$ cd apps/runner && dart test test/oto_agent_registration_test.dart test/oto_server_connection_smoke_test.dart +(output) +``` + +--- + +> **[IMPLEMENTING AGENT — BEFORE SAVING] Have you filled in every implementation-owned section: completion table, implementation checklist, changes from plan, design decisions, and verification output?** +> If anything is blank, go back and fill it in before saving this file. +> Leave review-agent-only sections unchanged. + +## Section Ownership + +| Section | Owner | Note | +|---------|-------|------| +| Header comment, 개요, 리뷰 에이전트 지시 | Fixed at stub creation | Implementing agent must not modify or execute these | +| Roadmap Targets | Fixed at stub creation from plan | Implementing agent must not modify | +| 구현 항목별 완료 여부 | Implementing agent | Check `[ ]` to `[x]` only | +| 구현 체크리스트 | Implementing agent | Check `[ ]` to `[x]` only | +| 코드리뷰 전용 체크리스트 | Review agent only | Implementing agent must not modify | +| 계획 대비 변경 사항, 주요 설계 결정 | Implementing agent | Replace placeholder text | +| 사용자 리뷰 요청 | Implementing agent | Keep `상태: 없음` unless user input is required | +| 리뷰어를 위한 체크포인트 | Fixed at stub creation | Review focus | +| 검증 결과 | Implementing agent | Paste actual command output | +| 코드리뷰 결과 | Review agent appends | Not included in stub | diff --git a/agent-task/m-remote-run-lifecycle/04+01,02_cancel_status_update/PLAN-cloud-G07.md b/agent-task/m-remote-run-lifecycle/04+01,02_cancel_status_update/PLAN-cloud-G07.md new file mode 100644 index 0000000..28ac072 --- /dev/null +++ b/agent-task/m-remote-run-lifecycle/04+01,02_cancel_status_update/PLAN-cloud-G07.md @@ -0,0 +1,216 @@ + + +# Plan - API + +## 이 파일을 읽는 구현 에이전트에게 + +구현 완료의 마지막 단계는 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채우는 것이다. 검증 명령을 실행하고 실제 출력, 계획 대비 변경, 주요 설계 결정을 기록한 뒤 active 파일을 그대로 두고 리뷰 준비를 보고한다. 구현 중 사용자 전용 결정, 사용자 소유 환경/secret, 범위 충돌이 생기면 review stub의 `사용자 리뷰 요청` 섹션에 정확한 근거를 남기고 중단한다. 구현 에이전트는 사용자에게 직접 묻거나 선택지를 제시하거나 `request_user_input`을 호출하거나, `USER_REVIEW.md`/`complete.log`를 만들거나, 로그 아카이브를 수행하지 않는다. + +## 배경 + +`proto/oto/runner.proto`에는 cancel, runner status, self-update 메시지가 이미 정의되어 있다. 현재 서버는 runner claim/report/log/artifact endpoint를 제공하지만 cancel/status/self-update HTTP endpoint와 running -> canceled 상태 전이가 없다. 이 계획은 기존 proto를 재사용해 원격 실행 생명주기의 운영 제어면을 닫는다. + +## 사용자 리뷰 요청 흐름 + +구현 중 차단은 active review stub의 `사용자 리뷰 요청` 섹션에 기록한다. 직접 사용자 프롬프트는 금지되며, code-review가 요청 타당성을 검증하고 실제 `USER_REVIEW.md` 작성 여부를 결정한다. + +## Roadmap Targets + +- Milestone: `agent-roadmap/phase/message-based-build-agent/milestones/remote-run-lifecycle.md` +- Task ids: + - `cancel-status-update`: cancel, status, self-update의 최소 안정성 규칙이 정의되어 있다. +- Completion mode: check-on-pass + +## 분석 결과 + +### 읽은 파일 + +- `agent-task/m-remote-run-lifecycle/01_run_request_pipeline/PLAN-cloud-G07.md` +- `agent-task/m-remote-run-lifecycle/02+01_step_log_stream/PLAN-local-G07.md` +- `agent-ops/rules/project/rules.md` +- `agent-ops/rules/common/rules-roadmap.md` +- `agent-ops/skills/common/plan/SKILL.md` +- `agent-ops/rules/project/domain/agent/rules.md` +- `agent-ops/rules/project/domain/core/rules.md` +- `agent-test/local/rules.md` +- `agent-test/local/agent-smoke.md` +- `agent-test/local/core-smoke.md` +- `agent-roadmap/current.md` +- `agent-roadmap/phase/message-based-build-agent/PHASE.md` +- `agent-roadmap/phase/message-based-build-agent/milestones/remote-run-lifecycle.md` +- `proto/oto/runner.proto` +- `services/core/internal/cicdstate/store.go` +- `services/core/internal/cicdstate/store_test.go` +- `services/core/internal/httpserver/server.go` +- `services/core/internal/httpserver/server_test.go` +- `services/core/internal/runnerregistry/registry.go` +- `apps/runner/lib/oto/agent/agent_runner.dart` +- `apps/runner/lib/oto/agent/edge_registration_client.dart` +- `apps/runner/lib/oto/agent/oto_server_job_client.dart` +- `apps/runner/test/oto_agent_registration_test.dart` +- `apps/runner/test/oto_server_connection_smoke_test.dart` + +### 테스트 환경 규칙 + +- test_env: `local`. +- 읽은 프로파일: `agent-smoke`, `core-smoke`. +- Go service 변경은 `go test ./...`로 검증한다. +- Dart agent client 변경은 `dart analyze`와 agent/server smoke로 검증한다. +- 사전 검증으로 `go test ./...`, Dart agent/core/server smoke 테스트가 통과했다. + +### 테스트 커버리지 공백 + +- store 상태 전이는 queued -> canceled만 허용하고 running -> canceled를 허용하지 않는다. +- 서버는 cancel/status/self-update proto 메시지를 HTTP endpoint로 받지 않는다. +- Dart job client는 cancel/status/self-update request helper가 없다. +- self-update는 실제 바이너리 교체를 수행하기 전에 active execution과 URL scheme을 확인하는 최소 안정성 규칙이 없다. + +### 심볼 참조 + +- renamed/removed symbol: none. +- 변경 후보 call site: `validTransitions`, `Store.TransitionJob`, `Store.TransitionExecution`, `Server` route switch, `OtoServerJobClient`. + +### 분할 판단 + +- split decision policy를 먼저 평가했다. +- 이 subtask는 `04+01,02_cancel_status_update`이므로 predecessor `01`, `02`가 필요하다. +- 현재 active `agent-task/m-remote-run-lifecycle/01_run_request_pipeline/complete.log`와 `agent-task/m-remote-run-lifecycle/02+01_step_log_stream/complete.log`가 없고 archive predecessor도 확인하지 않았다. predecessor `01`: missing, predecessor `02`: missing. +- `01`은 execution/job identity 계약을 제공하고, `02`는 runner loop의 active execution 개념을 제공한다. cancel/status/self-update는 이 생명주기 위에서 동작하므로 추가 숨은 의존성을 두지 않는다. + +### 범위 결정 근거 + +- 이 계획은 HTTP/proto JSON boundary, store 상태 전이, Dart client helper, smoke 테스트를 다룬다. +- 실제 self-update 바이너리 다운로드/교체/재시작은 위험도가 높고 플랫폼별 정책이 필요하므로 범위 밖이다. 이 milestone에서는 최소 안정성 규칙과 accepted/deferred response만 정의한다. +- UI, auth, persistent DB migration은 범위 밖이다. + +### 빌드 등급 + +- build: `cloud-G07`. Go server state machine과 Dart client protocol을 함께 바꾸며 remote lifecycle 제어 의미가 크다. +- review: `cloud-G07`. 상태 전이, HTTP status, Dart smoke가 서로 맞는지 교차 확인해야 한다. + +## 구현 체크리스트 + +- [ ] `01_run_request_pipeline`과 `02+01_step_log_stream`의 `complete.log`가 존재하는지 확인하고 없으면 구현을 시작하지 않는다. +- [ ] running job/execution을 canceled로 전이할 수 있고 terminal state는 변경하지 않는다. +- [ ] cancel endpoint는 runner/job/execution id mismatch를 거부하고, 성공 시 job과 execution state를 모두 `canceled`로 맞춘다. +- [ ] status endpoint는 runner registry 상태와 현재/마지막 execution 상태를 반환한다. +- [ ] self-update endpoint는 active execution 중에는 defer/reject하고, download URL은 HTTPS만 허용한다. 실제 바이너리 교체는 하지 않는다. +- [ ] Dart job client가 cancel/status/self-update helper를 제공하고 snake_case/camelCase JSON을 파싱한다. +- [ ] Go service와 Dart agent/server smoke 테스트를 갱신한다. +- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. + +## 의존 관계 및 구현 순서 + +- Runtime dependency: sibling `01_run_request_pipeline` must produce `complete.log`. +- Runtime dependency: sibling `02+01_step_log_stream` must produce `complete.log`. +- 이 plan은 `agent-task/m-remote-run-lifecycle/04+01,02_cancel_status_update` 이름의 `+01,02` 의존성만 가진다. 추가 의존성을 plan 본문에 숨기지 않는다. + +### [API-1] Cancellable State Transitions + +문제: [services/core/internal/cicdstate/store.go](/config/workspace/oto/services/core/internal/cicdstate/store.go:17)의 `validTransitions`는 `StateRunning -> StateCanceled`를 허용하지 않는다. + +Before: + +```go +var validTransitions = map[string][]string{ + StateQueued: {StateRunning, StateFailed, StateCanceled}, + StateRunning: {StateSucceeded, StateFailed}, + StateSucceeded: nil, + StateFailed: nil, + StateCanceled: nil, +} +``` + +해결 방법: `StateRunning`에서 `StateCanceled`로 전이 가능하게 하고, job/execution을 함께 취소하는 store helper를 추가한다. 이미 succeeded/failed/canceled인 terminal state는 그대로 거부한다. + +수정 파일 및 체크리스트: + +- [ ] `services/core/internal/cicdstate/store.go`: running cancel 전이와 `CancelExecution` 또는 `CancelJobExecution` helper 추가. +- [ ] `services/core/internal/cicdstate/store_test.go`: queued/running cancel, terminal cancel rejection, job/execution 동시 갱신 테스트 추가. + +중간 검증: + +```bash +go test ./internal/cicdstate +``` + +### [API-2] HTTP Cancel And Status Endpoints + +문제: [services/core/internal/httpserver/server.go](/config/workspace/oto/services/core/internal/httpserver/server.go:420)의 runner route switch는 claim/report/log/artifact만 처리하고 cancel/status endpoint가 없다. + +해결 방법: 기존 proto JSON 필드를 쓰는 endpoint를 추가한다. 제안 route: + +- `POST /api/v1/runners/{runner_id}/executions/{execution_id}/cancel` +- `GET /api/v1/runners/{runner_id}/status` + +cancel은 `CancelRunRequest`의 runner/job/execution id가 path와 일치하는지 확인한다. status는 `RunnerStatusResponse`와 호환되는 JSON을 반환하고, 최소한 `accepted`, `runner_id`, `status`, `current_job_id`, `current_execution_id`, `message`를 포함한다. + +수정 파일 및 체크리스트: + +- [ ] `services/core/internal/httpserver/server.go`: route, request decoder, response writer 추가. +- [ ] `services/core/internal/httpserver/server_test.go`: cancel 성공/불일치/terminal rejection/status 조회 테스트 추가. + +중간 검증: + +```bash +go test ./internal/httpserver +``` + +### [API-3] Self-Update Stability Contract + +문제: proto에는 `SelfUpdateRequest`/`SelfUpdateResponse`가 있으나 서버/runner 경계의 안정성 규칙이 없다. + +해결 방법: `POST /api/v1/runners/{runner_id}/self-update` endpoint를 추가한다. request의 `download_url`은 HTTPS만 허용하고, active running execution이 있으면 `accepted=false`, `deferred=true`, `message`를 반환한다. active execution이 없으면 `accepted=true`, `deferred=false`, `target_version`과 `message`를 반환한다. 실제 다운로드와 교체는 하지 않는다. + +수정 파일 및 체크리스트: + +- [ ] `services/core/internal/httpserver/server.go`: self-update endpoint와 validation 추가. +- [ ] `services/core/internal/httpserver/server_test.go`: invalid URL, active execution defer, idle accepted 테스트 추가. + +중간 검증: + +```bash +go test ./internal/httpserver +``` + +### [API-4] Dart Lifecycle Client Helpers + +문제: [apps/runner/lib/oto/agent/oto_server_job_client.dart](/config/workspace/oto/apps/runner/lib/oto/agent/oto_server_job_client.dart:68)는 claim/report/log/artifact만 제공한다. + +해결 방법: `cancelRun`, `fetchRunnerStatus`, `requestSelfUpdate` helper와 response result classes를 추가한다. 기존 helper의 timeout/error handling 패턴을 재사용한다. + +수정 파일 및 체크리스트: + +- [ ] `apps/runner/lib/oto/agent/oto_server_job_client.dart`: lifecycle helper와 result parser 추가. +- [ ] `apps/runner/test/oto_agent_registration_test.dart`: request payload/response parser/error handling 테스트 추가. +- [ ] `apps/runner/test/oto_server_connection_smoke_test.dart`: 실제 server cancel/status/self-update smoke 추가. + +중간 검증: + +```bash +dart test test/oto_agent_registration_test.dart test/oto_server_connection_smoke_test.dart +``` + +## 수정 파일 요약 + +| 파일 | 항목 | +|------|------| +| `services/core/internal/cicdstate/store.go` | API-1 | +| `services/core/internal/cicdstate/store_test.go` | API-1 | +| `services/core/internal/httpserver/server.go` | API-2, API-3 | +| `services/core/internal/httpserver/server_test.go` | API-2, API-3 | +| `apps/runner/lib/oto/agent/oto_server_job_client.dart` | API-4 | +| `apps/runner/test/oto_agent_registration_test.dart` | API-4 | +| `apps/runner/test/oto_server_connection_smoke_test.dart` | API-4 | + +## 최종 검증 + +```bash +cd services/core && go test ./... +cd apps/runner && dart analyze +cd apps/runner && dart test test/oto_agent_registration_test.dart test/oto_server_connection_smoke_test.dart +``` + +예상 결과: 모든 명령이 exit code 0으로 종료한다. + +모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다. diff --git a/apps/runner/lib/oto/agent/oto_server_job_client.dart b/apps/runner/lib/oto/agent/oto_server_job_client.dart index 7b447ed..66fcd2e 100644 --- a/apps/runner/lib/oto/agent/oto_server_job_client.dart +++ b/apps/runner/lib/oto/agent/oto_server_job_client.dart @@ -12,6 +12,7 @@ class JobClaimResult { final String jobId; final String executionId; final String state; + final oto.RunRequest? runRequest; const JobClaimResult({ required this.accepted, @@ -19,6 +20,7 @@ class JobClaimResult { required this.executionId, required this.state, this.errorMessage, + this.runRequest, }); factory JobClaimResult.fromJson(Map json) { @@ -37,6 +39,7 @@ class JobClaimResult { executionId: (json['execution_id'] ?? json['executionId'] ?? '') .toString(), state: (json['state'] ?? '').toString(), + runRequest: _parseRunRequest(json['run_request'] ?? json['runRequest']), ); } } @@ -288,3 +291,36 @@ oto.StepEventReport _stepEventReport(StepEvent event) { } String? _emptyToNull(String value) => value.isEmpty ? null : value; + +/// Parses a claim response `run_request` payload (snake_case or camelCase) into +/// an [oto.RunRequest]. Returns null when the server omitted the run request. +oto.RunRequest? _parseRunRequest(dynamic raw) { + if (raw is! Map) { + return null; + } + final request = oto.RunRequest(); + final runnerId = (raw['runner_id'] ?? raw['runnerId'] ?? '').toString(); + if (runnerId.isNotEmpty) request.runnerId = runnerId; + final jobId = (raw['job_id'] ?? raw['jobId'] ?? '').toString(); + if (jobId.isNotEmpty) request.jobId = jobId; + final executionId = (raw['execution_id'] ?? raw['executionId'] ?? '') + .toString(); + if (executionId.isNotEmpty) request.executionId = executionId; + final pipelineYamlPath = + (raw['pipeline_yaml_path'] ?? raw['pipelineYamlPath'] ?? '').toString(); + if (pipelineYamlPath.isNotEmpty) request.pipelineYamlPath = pipelineYamlPath; + final pipelineYaml = (raw['pipeline_yaml'] ?? raw['pipelineYaml'] ?? '') + .toString(); + if (pipelineYaml.isNotEmpty) request.pipelineYaml = pipelineYaml; + final variables = raw['variables']; + if (variables is Map) { + variables.forEach((key, value) { + request.variables[key.toString()] = value?.toString() ?? ''; + }); + } + final commandTypes = raw['command_types'] ?? raw['commandTypes']; + if (commandTypes is List) { + request.commandTypes.addAll(commandTypes.map((e) => e.toString())); + } + return request; +} diff --git a/apps/runner/test/oto_agent_registration_test.dart b/apps/runner/test/oto_agent_registration_test.dart index 1821786..824a6be 100644 --- a/apps/runner/test/oto_agent_registration_test.dart +++ b/apps/runner/test/oto_agent_registration_test.dart @@ -414,6 +414,14 @@ runtime: 'job_id': 'job-123', 'execution_id': 'exec-123', 'state': 'running', + 'run_request': { + 'runner_id': 'agent-123', + 'job_id': 'job-123', + 'execution_id': 'exec-123', + 'pipeline_yaml': 'commands:\n - type: Shell', + 'variables': {'FLAVOR': 'release'}, + 'command_types': ['Shell', 'Git'], + }, }), ); } else if (request.method == 'POST' && @@ -460,6 +468,13 @@ runtime: ); expect(claim.accepted, isTrue); expect(claim.state, 'running'); + expect(claim.runRequest, isNotNull); + expect(claim.runRequest!.jobId, 'job-123'); + expect(claim.runRequest!.executionId, 'exec-123'); + expect(claim.runRequest!.runnerId, 'agent-123'); + expect(claim.runRequest!.pipelineYaml, 'commands:\n - type: Shell'); + expect(claim.runRequest!.variables['FLAVOR'], 'release'); + expect(claim.runRequest!.commandTypes, ['Shell', 'Git']); final result = BuildResult.success( stepEvents: [ diff --git a/apps/runner/test/oto_server_connection_smoke_test.dart b/apps/runner/test/oto_server_connection_smoke_test.dart index 25e6623..914fbfb 100644 --- a/apps/runner/test/oto_server_connection_smoke_test.dart +++ b/apps/runner/test/oto_server_connection_smoke_test.dart @@ -397,10 +397,19 @@ fi final httpClient = http.Client(); try { + const inlineYaml = 'commands:\n - type: Shell'; final createJobResponse = await httpClient.post( Uri.parse('http://$serverAddr/api/v1/jobs'), headers: {'content-type': 'application/json'}, - body: jsonEncode({'id': jobId, 'name': 'smoke build'}), + body: jsonEncode({ + 'id': jobId, + 'name': 'smoke build', + 'run_request': { + 'pipeline_yaml': inlineYaml, + 'variables': {'FLAVOR': 'release'}, + 'command_types': ['Shell', 'Git'], + }, + }), ); expect( createJobResponse.statusCode, @@ -414,6 +423,10 @@ fi ); expect(claim.accepted, isTrue); expect(claim.state, 'running'); + expect(claim.runRequest, isNotNull); + expect(claim.runRequest!.pipelineYaml, inlineYaml); + expect(claim.runRequest!.jobId, jobId); + expect(claim.runRequest!.executionId, executionId); await jobs.appendLog( executionId: executionId, diff --git a/services/core/internal/cicdstate/store.go b/services/core/internal/cicdstate/store.go index fed54aa..8a9b287 100644 --- a/services/core/internal/cicdstate/store.go +++ b/services/core/internal/cicdstate/store.go @@ -22,6 +22,17 @@ var validTransitions = map[string][]string{ StateCanceled: nil, } +// RunInput holds the remote run request payload stored on a job so that a +// runner can receive it when it claims the job. It deliberately avoids any +// protobuf dependency; conversion to/from otopb.RunRequest happens at the HTTP +// boundary. +type RunInput struct { + PipelineYAMLPath string + PipelineYAML string + Variables map[string]string + CommandTypes []string +} + type Job struct { ID string Name string @@ -29,6 +40,7 @@ type Job struct { CreatedAt time.Time UpdatedAt time.Time ExecutionID string + RunInput *RunInput } func (j *Job) TransitionTo(newState string) error { @@ -96,7 +108,7 @@ func NewStore() *Store { } } -func (s *Store) CreateJob(id, name string) (*Job, error) { +func (s *Store) CreateJob(id, name string, runInput *RunInput) (*Job, error) { s.mu.Lock() defer s.mu.Unlock() @@ -111,9 +123,10 @@ func (s *Store) CreateJob(id, name string) (*Job, error) { State: StateQueued, CreatedAt: now, UpdatedAt: now, + RunInput: copyRunInput(runInput), } s.jobs[id] = job - return job, nil + return copyJob(job), nil } func (s *Store) GetJob(id string) (*Job, error) { @@ -307,6 +320,25 @@ func copyJob(j *Job) *Job { return nil } cp := *j + cp.RunInput = copyRunInput(j.RunInput) + return &cp +} + +func copyRunInput(in *RunInput) *RunInput { + if in == nil { + return nil + } + cp := *in + if in.Variables != nil { + cp.Variables = make(map[string]string, len(in.Variables)) + for k, v := range in.Variables { + cp.Variables[k] = v + } + } + if in.CommandTypes != nil { + cp.CommandTypes = make([]string, len(in.CommandTypes)) + copy(cp.CommandTypes, in.CommandTypes) + } return &cp } diff --git a/services/core/internal/cicdstate/store_test.go b/services/core/internal/cicdstate/store_test.go index e2a2063..002dae9 100644 --- a/services/core/internal/cicdstate/store_test.go +++ b/services/core/internal/cicdstate/store_test.go @@ -12,7 +12,7 @@ func TestStoreCreatesJobAndExecution(t *testing.T) { // 1. Create a job mockTime = time.Date(2026, 6, 5, 12, 0, 0, 0, time.UTC) - job, err := store.CreateJob("job-1", "build") + job, err := store.CreateJob("job-1", "build", nil) if err != nil { t.Fatalf("CreateJob failed: %v", err) } @@ -27,7 +27,7 @@ func TestStoreCreatesJobAndExecution(t *testing.T) { } // 2. Try duplicate job creation - _, err = store.CreateJob("job-1", "duplicate") + _, err = store.CreateJob("job-1", "duplicate", nil) if err == nil { t.Fatal("expected error for duplicate job") } @@ -84,13 +84,66 @@ func TestStoreCreatesJobAndExecution(t *testing.T) { } } +func TestStoreCreatesJobWithRunInputCopy(t *testing.T) { + store := NewStore() + + variables := map[string]string{"FLAVOR": "release"} + commandTypes := []string{"Shell", "Git"} + input := &RunInput{ + PipelineYAML: "commands:\n - type: Shell", + Variables: variables, + CommandTypes: commandTypes, + } + + job, err := store.CreateJob("job-1", "build", input) + if err != nil { + t.Fatalf("CreateJob failed: %v", err) + } + if job.RunInput == nil || job.RunInput.PipelineYAML != "commands:\n - type: Shell" { + t.Fatalf("job RunInput = %+v, want stored pipeline yaml", job.RunInput) + } + + // Mutating the caller-owned input must not leak into the store. + variables["FLAVOR"] = "debug" + commandTypes[0] = "Mutated" + input.PipelineYAML = "mutated" + + stored, err := store.GetJob("job-1") + if err != nil { + t.Fatalf("GetJob failed: %v", err) + } + if stored.RunInput == nil { + t.Fatal("stored RunInput is nil") + } + if stored.RunInput.PipelineYAML != "commands:\n - type: Shell" { + t.Fatalf("stored PipelineYAML leaked mutation: %q", stored.RunInput.PipelineYAML) + } + if stored.RunInput.Variables["FLAVOR"] != "release" { + t.Fatalf("stored Variables leaked mutation: %q", stored.RunInput.Variables["FLAVOR"]) + } + if stored.RunInput.CommandTypes[0] != "Shell" { + t.Fatalf("stored CommandTypes leaked mutation: %q", stored.RunInput.CommandTypes[0]) + } + + // Mutating the returned copy must not leak back into the store either. + stored.RunInput.Variables["FLAVOR"] = "leak" + stored.RunInput.CommandTypes[0] = "Leak" + reread, err := store.GetJob("job-1") + if err != nil { + t.Fatalf("GetJob after mutation failed: %v", err) + } + if reread.RunInput.Variables["FLAVOR"] != "release" || reread.RunInput.CommandTypes[0] != "Shell" { + t.Fatalf("GetJob did not return a deep copy of RunInput: %+v", reread.RunInput) + } +} + func TestStoreAppendsLogsAndArtifacts(t *testing.T) { var mockTime time.Time store := NewStore() store.now = func() time.Time { return mockTime } mockTime = time.Date(2026, 6, 5, 12, 0, 0, 0, time.UTC) - store.CreateJob("job-1", "build") + store.CreateJob("job-1", "build", nil) // Append log to non-existent execution err := store.AppendLog("exec-unknown", "line 1") @@ -223,7 +276,7 @@ func TestStoreJobExecutionNotFound(t *testing.T) { func TestStoreStateTransitions(t *testing.T) { store := NewStore() - store.CreateJob("job-1", "build") + store.CreateJob("job-1", "build", nil) job, _ := store.GetJob("job-1") if job.State != StateQueued { @@ -255,7 +308,7 @@ func TestStoreStateTransitions(t *testing.T) { } // Reset: queued -> running -> failed - store.CreateJob("job-2", "test") + store.CreateJob("job-2", "test", nil) job2, _ := store.GetJob("job-2") err = job2.TransitionTo(StateRunning) if err != nil { @@ -270,7 +323,7 @@ func TestStoreStateTransitions(t *testing.T) { } // queued -> canceled - store.CreateJob("job-3", "lint") + store.CreateJob("job-3", "lint", nil) job3, _ := store.GetJob("job-3") err = job3.TransitionTo(StateCanceled) if err != nil { @@ -281,7 +334,7 @@ func TestStoreStateTransitions(t *testing.T) { } // queued -> failed (direct from queued) - store.CreateJob("job-4", "deploy") + store.CreateJob("job-4", "deploy", nil) job4, _ := store.GetJob("job-4") err = job4.TransitionTo(StateFailed) if err != nil { @@ -294,7 +347,7 @@ func TestStoreStateTransitions(t *testing.T) { func TestStoreExecutionStateTransitions(t *testing.T) { store := NewStore() - store.CreateJob("job-1", "build") + store.CreateJob("job-1", "build", nil) store.CreateExecution("job-1", "exec-1") exec, _ := store.GetExecution("exec-1") @@ -326,7 +379,7 @@ func TestStorePersistedJobTransition(t *testing.T) { store := NewStore() store.now = func() time.Time { return time.Date(2026, 6, 5, 12, 0, 0, 0, time.UTC) } - store.CreateJob("job-1", "build") + store.CreateJob("job-1", "build", nil) // queued -> running -> succeeded err := store.TransitionJob("job-1", StateRunning) @@ -385,7 +438,7 @@ func TestStorePersistedExecutionTransition(t *testing.T) { store := NewStore() store.now = func() time.Time { return time.Date(2026, 6, 5, 12, 0, 0, 0, time.UTC) } - store.CreateJob("job-1", "build") + store.CreateJob("job-1", "build", nil) store.CreateExecution("job-1", "exec-1") // queued -> running -> failed @@ -441,7 +494,7 @@ func TestStorePersistedExecutionTransition(t *testing.T) { } // queued -> running -> succeeded - store.CreateJob("job-2", "deploy") + store.CreateJob("job-2", "deploy", nil) store.CreateExecution("job-2", "exec-2") err = store.TransitionExecution("exec-2", StateRunning) if err != nil { diff --git a/services/core/internal/httpserver/server.go b/services/core/internal/httpserver/server.go index 030cceb..846d520 100644 --- a/services/core/internal/httpserver/server.go +++ b/services/core/internal/httpserver/server.go @@ -485,8 +485,9 @@ func handleRouter(store *cicdstate.Store, registry *runnerregistry.Registry) htt func handleCreateJob(store *cicdstate.Store) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var req struct { - ID string `json:"id"` - Name string `json:"name"` + ID string `json:"id"` + Name string `json:"name"` + RunRequest *otopb.RunRequest `json:"run_request"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeResponse(w, http.StatusBadRequest, map[string]string{"error": "invalid request body"}) @@ -496,7 +497,7 @@ func handleCreateJob(store *cicdstate.Store) http.HandlerFunc { writeResponse(w, http.StatusBadRequest, map[string]string{"error": "id and name are required"}) return } - job, err := store.CreateJob(req.ID, req.Name) + job, err := store.CreateJob(req.ID, req.Name, runRequestFromJSON(req.RunRequest)) if err != nil { writeResponse(w, http.StatusConflict, map[string]string{"error": err.Error()}) return @@ -745,6 +746,23 @@ func handleRunnerClaimJob(store *cicdstate.Store, registry *runnerregistry.Regis }) return } + if job.RunInput == nil { + writeResponse(w, http.StatusConflict, &otopb.JobClaimResponse{ + RunnerId: runnerID, + JobId: jobID, + ErrorMessage: "job has no run request", + State: job.State, + }) + return + } + if err := validateRunInput(job.RunInput); err != nil { + writeResponse(w, http.StatusBadRequest, &otopb.JobClaimResponse{ + RunnerId: runnerID, + JobId: jobID, + ErrorMessage: err.Error(), + }) + return + } if _, err := store.CreateExecution(jobID, execID); err != nil { writeResponse(w, http.StatusBadRequest, &otopb.JobClaimResponse{ RunnerId: runnerID, @@ -779,6 +797,7 @@ func handleRunnerClaimJob(store *cicdstate.Store, registry *runnerregistry.Regis JobId: jobID, ExecutionId: execID, State: cicdstate.StateRunning, + RunRequest: runRequestToProto(job.RunInput, runnerID, jobID, execID), }) } } @@ -980,7 +999,7 @@ type jobResponse struct { } func jobToJSON(j *cicdstate.Job) map[string]interface{} { - return map[string]interface{}{ + out := map[string]interface{}{ "id": j.ID, "name": j.Name, "state": j.State, @@ -988,6 +1007,74 @@ func jobToJSON(j *cicdstate.Job) map[string]interface{} { "updated_at": j.UpdatedAt.Format(time.RFC3339), "execution_id": j.ExecutionID, } + if j.RunInput != nil { + out["run_request"] = runInputToJSON(j.RunInput) + } + return out +} + +// validateRunInput enforces the remote claim contract: exactly one of +// pipeline_yaml or pipeline_yaml_path must be provided. +func validateRunInput(in *cicdstate.RunInput) error { + hasYAML := strings.TrimSpace(in.PipelineYAML) != "" + hasPath := strings.TrimSpace(in.PipelineYAMLPath) != "" + if hasYAML == hasPath { + return fmt.Errorf("run request must set exactly one of pipeline_yaml or pipeline_yaml_path") + } + return nil +} + +// runRequestFromJSON converts an incoming otopb.RunRequest decoded from the job +// create body into a store-level RunInput. It returns nil when no run request +// was supplied, keeping job creation backward compatible. +func runRequestFromJSON(rr *otopb.RunRequest) *cicdstate.RunInput { + if rr == nil { + return nil + } + in := &cicdstate.RunInput{ + PipelineYAMLPath: rr.GetPipelineYamlPath(), + PipelineYAML: rr.GetPipelineYaml(), + CommandTypes: append([]string(nil), rr.GetCommandTypes()...), + } + if len(rr.GetVariables()) > 0 { + in.Variables = make(map[string]string, len(rr.GetVariables())) + for k, v := range rr.GetVariables() { + in.Variables[k] = v + } + } + return in +} + +// runRequestToProto builds the claim response RunRequest from the stored input, +// filling in the runner/job/execution identifiers resolved at claim time. +func runRequestToProto(in *cicdstate.RunInput, runnerID, jobID, execID string) *otopb.RunRequest { + if in == nil { + return nil + } + rr := &otopb.RunRequest{ + RunnerId: runnerID, + JobId: jobID, + ExecutionId: execID, + PipelineYamlPath: in.PipelineYAMLPath, + PipelineYaml: in.PipelineYAML, + CommandTypes: append([]string(nil), in.CommandTypes...), + } + if len(in.Variables) > 0 { + rr.Variables = make(map[string]string, len(in.Variables)) + for k, v := range in.Variables { + rr.Variables[k] = v + } + } + return rr +} + +func runInputToJSON(in *cicdstate.RunInput) map[string]interface{} { + return map[string]interface{}{ + "pipeline_yaml_path": in.PipelineYAMLPath, + "pipeline_yaml": in.PipelineYAML, + "variables": in.Variables, + "command_types": in.CommandTypes, + } } type execResponse struct { diff --git a/services/core/internal/httpserver/server_test.go b/services/core/internal/httpserver/server_test.go index 80b93e1..04ea506 100644 --- a/services/core/internal/httpserver/server_test.go +++ b/services/core/internal/httpserver/server_test.go @@ -557,9 +557,151 @@ func TestHandleCreateJob(t *testing.T) { } } +func TestHandleCreateJobStoresRunRequest(t *testing.T) { + store := cicdstate.NewStore() + + body := bytes.NewBufferString(`{ + "id":"job-run", + "name":"build", + "run_request":{ + "pipeline_yaml":"commands:\n - type: Shell", + "variables":{"FLAVOR":"release"}, + "command_types":["Shell","Git"] + } + }`) + req := httptest.NewRequest(http.MethodPost, "/api/v1/jobs", body) + rr := httptest.NewRecorder() + handleRouter(store, nil)(rr, req) + if rr.Code != http.StatusCreated { + t.Fatalf("create status = %v, want %v; body=%s", rr.Code, http.StatusCreated, rr.Body.String()) + } + + // Stored input must survive on the store. + job, err := store.GetJob("job-run") + if err != nil { + t.Fatalf("GetJob failed: %v", err) + } + if job.RunInput == nil || job.RunInput.PipelineYAML != "commands:\n - type: Shell" { + t.Fatalf("stored RunInput = %+v", job.RunInput) + } + if job.RunInput.Variables["FLAVOR"] != "release" { + t.Fatalf("stored variables = %+v", job.RunInput.Variables) + } + + // GET /jobs/{id} must expose the stored input. + req = httptest.NewRequest(http.MethodGet, "/api/v1/jobs/job-run", nil) + rr = httptest.NewRecorder() + handleRouter(store, nil)(rr, req) + if rr.Code != http.StatusOK { + t.Fatalf("get status = %v, want %v", rr.Code, http.StatusOK) + } + var res map[string]interface{} + if err := json.Unmarshal(rr.Body.Bytes(), &res); err != nil { + t.Fatalf("decode response: %v", err) + } + runReq, ok := res["run_request"].(map[string]interface{}) + if !ok { + t.Fatalf("run_request missing in GET job: %v", res["run_request"]) + } + if runReq["pipeline_yaml"] != "commands:\n - type: Shell" { + t.Fatalf("GET run_request pipeline_yaml = %v", runReq["pipeline_yaml"]) + } +} + +func TestHandleRunnerCicdClaimReturnsRunRequest(t *testing.T) { + store := cicdstate.NewStore() + registry := runnerregistry.New() + registry.Register(&otopb.RegisterRunnerRequest{ + EnrollmentToken: "token-123", + RunnerId: "runner-123", + ProtocolVersion: "oto.runner.v1", + Capability: &otopb.RunnerCapability{Name: "oto-runner", Version: "1.0.0"}, + }) + if _, err := store.CreateJob("job-1", "build", &cicdstate.RunInput{ + PipelineYAMLPath: "/pipelines/build.yaml", + CommandTypes: []string{"Shell"}, + }); err != nil { + t.Fatalf("CreateJob failed: %v", err) + } + + claimBody := bytes.NewBufferString(`{ + "runner_id":"runner-123", + "job_id":"job-1", + "execution_id":"exec-1" + }`) + req := httptest.NewRequest(http.MethodPost, "/api/v1/runners/runner-123/jobs/claim", claimBody) + rr := httptest.NewRecorder() + handleRouter(store, registry)(rr, req) + if rr.Code != http.StatusOK { + t.Fatalf("claim status = %v, want %v; body=%s", rr.Code, http.StatusOK, rr.Body.String()) + } + var claim otopb.JobClaimResponse + if err := json.Unmarshal(rr.Body.Bytes(), &claim); err != nil { + t.Fatalf("decode claim response: %v", err) + } + if claim.GetRunRequest() == nil { + t.Fatal("claim run_request is nil") + } + if claim.GetRunRequest().GetPipelineYamlPath() != "/pipelines/build.yaml" { + t.Fatalf("claim run_request pipeline_yaml_path = %q", claim.GetRunRequest().GetPipelineYamlPath()) + } + if claim.GetRunRequest().GetRunnerId() != "runner-123" || + claim.GetRunRequest().GetJobId() != "job-1" || + claim.GetRunRequest().GetExecutionId() != "exec-1" { + t.Fatalf("claim run_request ids = %+v", claim.GetRunRequest()) + } +} + +func TestHandleRunnerCicdRejectsInvalidRunInput(t *testing.T) { + store := cicdstate.NewStore() + registry := runnerregistry.New() + registry.Register(&otopb.RegisterRunnerRequest{ + EnrollmentToken: "token-123", + RunnerId: "runner-123", + ProtocolVersion: "oto.runner.v1", + Capability: &otopb.RunnerCapability{Name: "oto-runner", Version: "1.0.0"}, + }) + + // Job with no run request → claim is rejected with 409. + if _, err := store.CreateJob("job-missing", "build", nil); err != nil { + t.Fatalf("CreateJob failed: %v", err) + } + missingBody := bytes.NewBufferString(`{"runner_id":"runner-123","job_id":"job-missing","execution_id":"exec-1"}`) + req := httptest.NewRequest(http.MethodPost, "/api/v1/runners/runner-123/jobs/claim", missingBody) + rr := httptest.NewRecorder() + handleRouter(store, registry)(rr, req) + if rr.Code != http.StatusConflict { + t.Fatalf("missing run input claim status = %v, want %v; body=%s", rr.Code, http.StatusConflict, rr.Body.String()) + } + + // Job with both pipeline_yaml and pipeline_yaml_path → claim is rejected with 400. + if _, err := store.CreateJob("job-both", "build", &cicdstate.RunInput{ + PipelineYAML: "commands: []", + PipelineYAMLPath: "/pipelines/build.yaml", + }); err != nil { + t.Fatalf("CreateJob failed: %v", err) + } + bothBody := bytes.NewBufferString(`{"runner_id":"runner-123","job_id":"job-both","execution_id":"exec-2"}`) + req = httptest.NewRequest(http.MethodPost, "/api/v1/runners/runner-123/jobs/claim", bothBody) + rr = httptest.NewRecorder() + handleRouter(store, registry)(rr, req) + if rr.Code != http.StatusBadRequest { + t.Fatalf("invalid run input claim status = %v, want %v; body=%s", rr.Code, http.StatusBadRequest, rr.Body.String()) + } + + // The rejected job must remain queued (no state mutation / execution created). + job, err := store.GetJob("job-both") + if err != nil { + t.Fatalf("GetJob failed: %v", err) + } + if job.State != cicdstate.StateQueued || job.ExecutionID != "" { + t.Fatalf("rejected job mutated state = %+v", job) + } +} + func TestHandleCreateExecution(t *testing.T) { store := cicdstate.NewStore() - store.CreateJob("job-1", "build") + store.CreateJob("job-1", "build", nil) body := bytes.NewBufferString(`{"id":"exec-1"}`) req := httptest.NewRequest(http.MethodPost, "/api/v1/jobs/job-1/executions", body) @@ -611,7 +753,7 @@ func TestHandleCreateExecution(t *testing.T) { func TestHandleExecutionLogsAndArtifacts(t *testing.T) { store := cicdstate.NewStore() - store.CreateJob("job-1", "build") + store.CreateJob("job-1", "build", nil) store.CreateExecution("job-1", "exec-1") // Append log @@ -762,7 +904,11 @@ func TestHandleRunnerCicdClaimReportLogsAndArtifacts(t *testing.T) { Version: "1.0.0", }, }) - if _, err := store.CreateJob("job-1", "build"); err != nil { + if _, err := store.CreateJob("job-1", "build", &cicdstate.RunInput{ + PipelineYAML: "commands:\n - type: Shell", + Variables: map[string]string{"FLAVOR": "release"}, + CommandTypes: []string{"Shell", "Git"}, + }); err != nil { t.Fatalf("CreateJob failed: %v", err) } @@ -784,6 +930,12 @@ func TestHandleRunnerCicdClaimReportLogsAndArtifacts(t *testing.T) { if !claim.GetAccepted() || claim.GetState() != cicdstate.StateRunning { t.Fatalf("claim response = %+v, want accepted running", claim) } + if claim.GetRunRequest().GetPipelineYaml() != "commands:\n - type: Shell" { + t.Fatalf("claim run_request pipeline yaml = %q", claim.GetRunRequest().GetPipelineYaml()) + } + if claim.GetRunRequest().GetJobId() != "job-1" || claim.GetRunRequest().GetExecutionId() != "exec-1" || claim.GetRunRequest().GetRunnerId() != "runner-123" { + t.Fatalf("claim run_request ids = %+v", claim.GetRunRequest()) + } job, err := store.GetJob("job-1") if err != nil { @@ -866,7 +1018,7 @@ func TestHandleRunnerCicdClaimReportLogsAndArtifacts(t *testing.T) { func TestHandleRunnerCicdRejectsUnknownRunner(t *testing.T) { store := cicdstate.NewStore() - if _, err := store.CreateJob("job-1", "build"); err != nil { + if _, err := store.CreateJob("job-1", "build", nil); err != nil { t.Fatalf("CreateJob failed: %v", err) }