<!-- task=01_command_runtime_contract plan=0 tag=REVIEW_REFACTOR -->

# Command Runtime Contract Hardening

## 이 파일을 읽는 구현 에이전트에게

**구현의 마지막 단계는 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채우는 것이다. 이 파일이 채워지기 전에는 작업이 완료된 것이 아니다.** 구현 체크리스트를 기준으로 작업하고, 중간/최종 검증을 실행한 뒤 실제 구현 내용과 stdout/stderr를 review stub에 기록한다. review 파일의 archive/complete.log 지시는 코드리뷰 스킬 전용이며 구현 에이전트가 실행하거나 `코드리뷰 전용 체크리스트`를 수정하면 안 된다.

## 배경

최근 리팩토링으로 `CommandRuntime`이 도입되어 프로세스 실행을 fake runtime으로 테스트할 수 있게 됐다. 다만 SFTP 계열은 `runExecutable` 결과의 exit code를 확인하지 않아 실패한 `scp`, `ssh`, `mkdir`도 성공처럼 처리될 수 있다. AI-first 관점에서는 외부 프로세스 성공/실패 계약을 명확히 하고 테스트로 고정해야 한다.

## 분석 결과

### 읽은 파일

- `lib/oto/commands/command_runtime.dart`
- `lib/oto/commands/command.dart`
- `lib/oto/commands/ftp/ftp.dart`
- `lib/oto/commands/git/git_branch.dart`
- `lib/oto/commands/git/git_revision.dart`
- `test/oto_command_runtime_test.dart`

### 테스트 커버리지 공백

- SFTP `runExecutable` 실패 전파: 기존 테스트는 호출 인자만 확인하고 non-zero exit code를 실패로 전파하는지 확인하지 않는다.
- `CommandRuntime` exit-status 계약: fake runtime의 `ProcessResult.exitCode`가 실제 커맨드 실패로 이어지는 테스트가 부족하다.

### 심볼 참조

- renamed/removed symbols: none.

### 분할 판단

분할 정책을 먼저 평가했다. 이 작업은 `01_command_runtime_contract`, `02_pipeline_yaml_validation`, `03_command_catalog_samples` 중 프로세스 실행 계약만 다루는 독립 task다. 다른 두 sibling task와 런타임 의존성이 없으므로 `01_` 독립 디렉터리로 둔다.

### 범위 결정 근거

이 작업은 `lib/oto/commands/ftp/ftp.dart`, 필요 시 `CommandRuntime`/공통 helper, 관련 command runtime tests만 수정한다. CLI, DataComposer, scheduler, 실제 FTPConnect 주입 구조 변경은 제외한다.

### 빌드 등급

build=`cloud-G07`, review=`cloud-G07`: 외부 CLI 실행, exit-status contract, stdout/stderr 검증이 포함되는 terminal-agent 성격의 작업이다.

## 구현 체크리스트

- [ ] [REVIEW_REFACTOR-1] SFTP 외부 프로세스 실패가 Upload/Download/Delete/CreateFolder 실패로 전파되도록 실행 계약을 정리한다.
- [ ] [REVIEW_REFACTOR-2] 실패 전파와 성공 경로를 fake runtime 테스트로 고정한다.
- [ ] 중간 검증과 최종 검증 명령을 실행하고 결과를 확인한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [REVIEW_REFACTOR-1] SFTP 실행 실패 전파

문제: [ftp.dart](/config/workspace/oto/lib/oto/commands/ftp/ftp.dart:277), [ftp.dart](/config/workspace/oto/lib/oto/commands/ftp/ftp.dart:319), [ftp.dart](/config/workspace/oto/lib/oto/commands/ftp/ftp.dart:384), [ftp.dart](/config/workspace/oto/lib/oto/commands/ftp/ftp.dart:399)의 `runExecutable` 결과가 확인되지 않는다.

해결 방법: SFTP 내부에 `Future<void> _runSftpCommand(...)` 같은 작은 helper를 만들고, `ProcessResult.exitCode != 0`이면 stderr/stdout을 포함해 예외를 던진다.

Before:

```dart
// lib/oto/commands/ftp/ftp.dart:277
await _runtime.runExecutable('sshpass', [
  '-p',
  _pass,
  'scp',
```

After:

```dart
await _runSftpCommand([
  '-p',
  _pass,
  'scp',
```

수정 파일 및 체크리스트:

- [ ] `lib/oto/commands/ftp/ftp.dart`: SFTP helper 추가.
- [ ] `lib/oto/commands/ftp/ftp.dart`: upload/download/delete/createFolder의 `runExecutable` 호출을 helper로 교체.
- [ ] 실패 메시지에 실행 대상과 stderr를 포함한다.

테스트 작성: 작성. `test/oto_command_runtime_test.dart`에 non-zero fake runtime이 `Download.execute()` 실패로 이어지는 테스트를 추가한다.

중간 검증:

```bash
dart test test/oto_command_runtime_test.dart
```

기대 결과: All tests passed.

### [REVIEW_REFACTOR-2] 성공/실패 계약 테스트 보강

문제: [test/oto_command_runtime_test.dart](/config/workspace/oto/test/oto_command_runtime_test.dart:65)의 `FakeRuntime`은 exitCode를 설정할 수 있지만 SFTP 실패 path를 검증하지 않는다.

해결 방법: 성공 테스트는 유지하고, 실패 테스트에서 `FakeRuntime(exitCode: 1)`과 stderr 텍스트를 지원하도록 필요한 최소 필드만 확장한다.

수정 파일 및 체크리스트:

- [ ] `test/oto_command_runtime_test.dart`: FakeRuntime stderr fixture 필요 여부 확인.
- [ ] `test/oto_command_runtime_test.dart`: SFTP download failure test 추가.
- [ ] `test/oto_command_runtime_test.dart`: 성공 경로 테스트가 기존 인자 계약을 계속 검증하는지 확인.

테스트 작성: 작성. 실패 시 `throwsA`와 stderr 포함 여부를 검증한다.

중간 검증:

```bash
dart test test/oto_command_runtime_test.dart
```

기대 결과: All tests passed.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `lib/oto/commands/ftp/ftp.dart` | REVIEW_REFACTOR-1 |
| `test/oto_command_runtime_test.dart` | REVIEW_REFACTOR-2 |

## 최종 검증

```bash
dart analyze
dart test
```

기대 결과: `No issues found!`, `All tests passed!`

모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
