<!-- task=03+01_sftp_smb_runtime plan=0 tag=REFACTOR -->

# SFTP SMB Runtime Plan

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

**구현 마지막 단계에서 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 반드시 채워야 한다. 해당 파일의 모든 구현 소유 섹션을 채우기 전까지 작업은 완료가 아니다.** 구현 체크리스트와 의존 관계를 지키고, 검증 출력은 review stub에 실제 stdout/stderr로 기록한다. review 파일의 아카이브 지시와 `코드리뷰 전용 체크리스트`는 코드리뷰 에이전트 전용이다.

## 배경

SFTP helper와 SMBAuth는 `Process.run`을 직접 호출한다. 이 호출들은 외부 `sshpass`/Windows `cmd` 환경이 있어야만 검증 가능하므로 AI-first 방식의 fake runtime 테스트 경계 밖에 있다. `01_command_runtime_api`에서 추가한 executable 포트를 사용해 외부 명령 호출을 테스트 가능한 경계로 옮긴다.

## 분석 결과

### 읽은 파일

- `lib/oto/commands/command_runtime.dart`
- `lib/oto/commands/ftp/ftp.dart`
- `lib/oto/commands/ftp/upload.dart`
- `lib/oto/commands/ftp/download.dart`
- `lib/oto/commands/infra/smb_authorize.dart`
- `test/oto_command_runtime_test.dart`

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

- SFTP `sshpass scp/ssh` 인자 생성 테스트가 없다.
- SFTP `ls -l` stdout 파싱 테스트가 없다.
- SMBAuth `cmd /C net use` 인자 생성 테스트가 없다.

### 심볼 참조

- 제거/이름 변경 없음.
- 이전 대상 직접 호출은 `lib/oto/commands/ftp/ftp.dart`의 `Process.run('sshpass', ...)` 7곳과 `lib/oto/commands/infra/smb_authorize.dart`의 `Process.run('cmd', ...)` 1곳이다.

### 범위 결정 근거

- 이 plan은 `Process.run` 기반 command 도메인 외부 명령 호출만 다룬다.
- FTPConnect API 자체는 프로세스 실행이 아니므로 유지한다.
- `ProcessExecutor.start/run` 직접 호출은 `02+01_process_executor_commands`에서 별도로 처리한다.

### 빌드 등급

- build=`cloud-G07`, review=`cloud-G07`: 외부 CLI 인자, stdout 파싱, exit-code 계약을 다루는 터미널 에이전트 작업이다.

## 의존 관계 및 구현 순서

- 이 디렉터리 이름의 `+01`은 `agent-task/01_command_runtime_api/complete.log`가 먼저 있어야 함을 뜻한다.
- `01_command_runtime_api` 완료 후 이 plan을 구현한다. `02+01_process_executor_commands`와는 독립적으로 병렬 가능하다.

## 구현 체크리스트

- [ ] `SFTP`와 FTP 생성 흐름에 `CommandRuntime` 주입 경로를 추가한다.
- [ ] SFTP의 `Process.run('sshpass', ...)` 호출을 `runtime.runExecutable`으로 이전한다.
- [ ] SMBAuth의 `Process.run('cmd', ...)` 호출을 `runtime.runExecutable`으로 이전한다.
- [ ] SFTP/SMB fake runtime 회귀 테스트를 추가하고 검증 명령을 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [REFACTOR-1] SFTP runtime 주입

#### 문제

`SFTP`는 command 인스턴스의 `runtime`을 받지 않아 helper 내부에서 직접 `Process.run`을 호출한다.

Before:

```dart
// lib/oto/commands/ftp/ftp.dart:269
await Process.run('sshpass', [
  '-p',
  _pass,
  'scp',
```

#### 해결 방법

`FTP`/`SFTP` 생성자에 optional `CommandRuntime runtime = const DefaultCommandRuntime()`를 추가한다. `Upload`/`Download`는 command의 `runtime`을 SFTP 생성자에 전달한다. SFTP 내부 7개 호출은 `runtime.runExecutable('sshpass', args)`로 이전한다.

#### 수정 파일 및 체크리스트

- [ ] `lib/oto/commands/ftp/ftp.dart`
- [ ] `lib/oto/commands/ftp/upload.dart`
- [ ] `lib/oto/commands/ftp/download.dart`
- [ ] `test/oto_command_runtime_test.dart`

#### 테스트 작성

작성한다. SFTP `list`는 fake stdout으로 `FileType` map 파싱을 검증한다. SFTP `delete`는 executable과 arguments에 `sshpass`, `ssh`, `rm -rf`가 포함되는지 검증한다.

#### 중간 검증

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

예상: SFTP fake runtime 테스트 통과.

### [REFACTOR-2] SMBAuth runtime 이전

#### 문제

SMBAuth는 Windows 전용 `Process.run('cmd', ...)`을 직접 호출한다.

Before:

```dart
// lib/oto/commands/infra/smb_authorize.dart:12
var process = await Process.run('cmd', [
  '/C',
  'net',
```

#### 해결 방법

`runtime.runExecutable('cmd', [...])`를 사용하고 stdout/stderr 로깅 및 exit code 처리 의미를 유지한다.

#### 수정 파일 및 체크리스트

- [ ] `lib/oto/commands/infra/smb_authorize.dart`
- [ ] `test/oto_command_runtime_test.dart`

#### 테스트 작성

작성한다. SMBAuth는 fake runtime으로 executable=`cmd`, arguments에 `/C`, `net`, `use`, UNC domain, user/password가 포함되는지 검증한다.

#### 중간 검증

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

예상: SMBAuth fake runtime 테스트 통과.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `lib/oto/commands/ftp/ftp.dart` | REFACTOR-1 |
| `lib/oto/commands/ftp/upload.dart` | REFACTOR-1 |
| `lib/oto/commands/ftp/download.dart` | REFACTOR-1 |
| `lib/oto/commands/infra/smb_authorize.dart` | REFACTOR-2 |
| `test/oto_command_runtime_test.dart` | REFACTOR-1, REFACTOR-2 |

## 최종 검증

```bash
test -f agent-task/01_command_runtime_api/complete.log
dart format lib/oto/commands/ftp/ftp.dart lib/oto/commands/ftp/upload.dart lib/oto/commands/ftp/download.dart lib/oto/commands/infra/smb_authorize.dart test/oto_command_runtime_test.dart
dart analyze
dart test test/oto_command_runtime_test.dart
rg --sort path -n "Process\\.run|Process\\.start" lib/oto/commands -g '!command_runtime.dart' -g '!*.g.dart'
```

예상: dependency check 성공, format 정상, analyze/test 통과, 마지막 `rg`는 출력 없음. Dart test는 캐시가 없으므로 fresh 실행만 인정한다.

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