<!-- task=m-macos-distribution plan=1 tag=REVIEW_SECURITY -->

# Review Follow-up - Certified Script Test Process Execution

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

`CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션 작성은 필수다. 구현 후 검증을 실행하고, 실제 구현 내용과 검증 출력을 review stub에 채운 뒤 active 파일을 그대로 둔 채 리뷰 준비를 보고한다. 종결, archive, `complete.log` 작성은 code-review-skill 전용이다.

구현 중 선택된 SDD 결정 또는 선택된 Milestone `구현 잠금 > 결정 필요` 항목 없이는 안전하게 진행할 수 없는 경우 active `CODE_REVIEW-*-G??.md`의 `사용자 리뷰 요청` 섹션에 정확한 근거를 기록하고 멈춘다. 사용자에게 직접 묻거나, 채팅 선택지를 제시하거나, `request_user_input`을 호출하거나, `USER_REVIEW.md`를 직접 만들거나, archive/log/complete.log를 작성하지 않는다. 환경/secret/서비스 준비, 일반 범위 변경, 후속 에이전트가 명령 재실행 또는 산출물 수집으로 해소할 수 있는 증거 공백은 사용자 리뷰 요청 사유가 아니다.

## 배경

이전 SECURITY 루프는 Jenkins SCM URL secret/XML hardening 자체는 스크립트 dry-run으로 확인됐지만, 새 Dart regression test가 `Process.run`을 동기 결과처럼 사용해 컴파일되지 않았다. 따라서 `flutter test test/certified_macos_scripts_test.dart`가 test load 단계에서 실패하고, 계획의 필수 regression test가 완료되지 않았다.

## 사용자 리뷰 요청 흐름

구현 중 차단 조건은 active review stub의 `사용자 리뷰 요청` 섹션에 기록한다. 해당 섹션은 `agent-ops/skills/common/_templates/implementation-user-review-request-section.md` 형식을 따르며, 코드리뷰가 요청 정당성을 검증하고 실제 `USER_REVIEW.md` 작성 여부를 결정한다. 구현 에이전트는 직접 사용자에게 묻지 않는다.

## Archive Evidence Snapshot

- 현재 archived plan: `agent-task/m-macos-distribution/plan_local_G06_0.log`
- 현재 archived review: `agent-task/m-macos-distribution/code_review_local_G06_0.log`
- Verdict: FAIL
- Required:
  - `test/certified_macos_scripts_test.dart:215`와 `test/certified_macos_scripts_test.dart:243`에서 `Process.run(...)` 반환값인 `Future<ProcessResult>`를 동기 `ProcessResult`처럼 사용해 focused Flutter test가 컴파일 실패한다.
- Suggested: 없음
- Nit: 없음
- 영향 파일:
  - `test/certified_macos_scripts_test.dart`
- 확인된 보조 증거:
  - `bash -n scripts/setup-appsok-ci-secrets.sh scripts/build-certified-macos.sh scripts/upsert-jenkins-certified-job.sh`: exit 0.
  - `APPSOK_SCM_URL='https://example.com/toki/appsok.git?foo=1&bar=<tag>' bash scripts/upsert-jenkins-certified-job.sh --dry-run | grep '<url>'`: `<url>https://example.com/toki/appsok.git?foo=1&amp;bar=&lt;tag&gt;</url>`.
  - `APPSOK_SCM_URL='https://user:token@example.com/org/repo.git' bash scripts/upsert-jenkins-certified-job.sh --dry-run`: exit 1, `credential-free` error, `token` 미노출.
  - `git diff --check`: exit 0.
  - `flutter test test/certified_macos_scripts_test.dart`: exit 1. `Future<ProcessResult>`에는 `exitCode`, `stdout`, `stderr` getter가 없다는 컴파일 오류로 실패.
- 추가 확인이 필요할 때는 위 두 log 파일만 좁게 읽는다. `agent-task/archive/**`를 넓게 검색하지 않는다.

## 분석 결과

### 읽은 파일

- `agent-ops/rules/project/rules.md`
- `agent-ops/rules/common/rules-roadmap.md`
- `agent-roadmap/current.md`
- `agent-ops/skills/common/router.md`
- `agent-ops/skills/common/code-review/SKILL.md`
- `agent-ops/skills/common/plan/SKILL.md`
- `agent-ops/skills/common/_templates/implementation-user-review-request-section.md`
- `agent-test/local/rules.md`
- `agent-test/local/macos-platform-smoke.md`
- `agent-test/local/macos-remote-build-smoke.md`
- `agent-ops/rules/project/domain/macos-platform/rules.md`
- `agent-task/m-macos-distribution/plan_local_G06_0.log`
- `agent-task/m-macos-distribution/code_review_local_G06_0.log`
- `scripts/upsert-jenkins-certified-job.sh`
- `test/certified_macos_scripts_test.dart`
- `.gitignore`

### 테스트 환경 규칙

- `test_env=local`.
- `agent-test/local/rules.md`를 읽었다. 기본 완료 검증은 standard remote Mac runner `toki@toki-labs.com`의 `$HOME/docker/services/code-server/data/volume/workspace/appsok` checkout 기준이다.
- `macos-platform-smoke.md`와 `macos-remote-build-smoke.md`를 읽었다. 이번 후속은 `macos/**`를 수정하지 않지만 certified build script test 회귀를 다루므로 focused Flutter test를 최종 검증에 둔다.
- 현재 리뷰에서 local `flutter test test/certified_macos_scripts_test.dart`는 컴파일 오류로 실패했다. 후속 구현은 같은 focused test를 먼저 통과시킨 뒤, 가능하면 remote runner focused test도 실행해 출력 전체를 review stub에 기록한다.

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

- SCM URL XML escaping regression test는 추가됐지만 컴파일되지 않아 아직 유효한 커버리지로 인정할 수 없다.
- HTTP(S) userinfo rejection regression test도 같은 이유로 아직 유효한 커버리지로 인정할 수 없다.

### 심볼 참조

- renamed/removed symbol: none.

### 분할 판단

- split decision policy를 평가했다.
- 변경은 `test/certified_macos_scripts_test.dart`의 두 test body에서 process 실행 결과를 올바르게 얻는 일에 한정된다.
- 스크립트 동작, Jenkins XML 구조, secret 처리, macOS platform 설정과 독립적으로 검증 가능하다.
- 단일 plan이 가장 작고, 실패 시 같은 test file만 다시 보면 된다.

### 범위 결정 근거

- `scripts/upsert-jenkins-certified-job.sh`는 리뷰 중 dry-run으로 XML escaping과 credential rejection이 통과했으므로 기본적으로 수정하지 않는다.
- 실제 Jenkins `--apply`, SOPS secret, notarization, macOS app build는 이 컴파일 오류 해결 범위가 아니다.
- 이전 루프에 `Roadmap Targets`와 `Spec Targets`가 없었으므로 이 후속 plan도 Milestone Task/SDD scenario 완료를 주장하지 않는다.

### 빌드 등급

- `local-G07`: 실패 원인이 명확하고 결정적이지만, 이전 local-G06 루프가 필수 test compile을 놓쳤으므로 한 단계 올린 local review로 검증한다.

## 구현 체크리스트

- [ ] `test/certified_macos_scripts_test.dart`의 두 dry-run regression test에서 `Process.run(...)` 결과를 `ProcessResult`로 올바르게 얻도록 수정한다.
- [ ] XML escaping과 HTTP(S) userinfo rejection assertion을 유지하고, stdout/stderr/token 미노출 검증이 실제 실행 결과를 대상으로 하게 한다.
- [ ] 계획의 중간 검증과 최종 검증 명령을 실행하고 실제 출력을 review stub에 기록한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [REVIEW_SECURITY-1] Fix Process.run async misuse in regression tests

#### 문제

`Process.run`은 `Future<ProcessResult>`를 반환한다. 현재 test body는 `async`가 아니며 `await`도 없어서 `result.exitCode`, `result.stdout`, `result.stderr` 접근이 컴파일되지 않는다.

```dart
// test/certified_macos_scripts_test.dart:215-221
final result = Process.run('bash', [
  '-c',
  'APPSOK_SCM_URL="https://example.com/toki/appsok.git?foo=1&bar=<tag>" bash scripts/upsert-jenkins-certified-job.sh --dry-run',
]);

expect(result.exitCode, 0, reason: 'dry-run should exit 0');
final output = result.stdout as String;
```

```dart
// test/certified_macos_scripts_test.dart:243-251
final result = Process.run('bash', [
  '-c',
  'APPSOK_SCM_URL="https://user:token@example.com/org/repo.git" bash scripts/upsert-jenkins-certified-job.sh --dry-run',
]);

expect(result.exitCode, isNot(0),
    reason: 'should reject URLs with userinfo');
final stderr = (result.stderr as String?) ?? '';
final stdout = (result.stdout as String?) ?? '';
```

#### 해결 방법

두 테스트 모두 `Process.runSync`를 사용해 현재 sync test shape을 유지한다. 또는 두 test callback을 `() async { ... }`로 바꾸고 `await Process.run(...)`을 사용해도 된다. 선택한 방식은 두 테스트에 일관되게 적용한다.

```dart
final result = Process.runSync('bash', [
  '-c',
  'APPSOK_SCM_URL="https://example.com/toki/appsok.git?foo=1&bar=<tag>" bash scripts/upsert-jenkins-certified-job.sh --dry-run',
]);
```

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

- [ ] `test/certified_macos_scripts_test.dart`: XML escaping test의 process 실행 결과 접근을 수정한다.
- [ ] `test/certified_macos_scripts_test.dart`: HTTP(S) userinfo rejection test의 process 실행 결과 접근을 수정한다.
- [ ] `test/certified_macos_scripts_test.dart`: 기존 assertion 목표를 유지한다.

#### 테스트 작성

- 새 테스트명은 추가하지 않는다.
- 기존 두 regression test를 컴파일/실행 가능하게 고친다.

#### 중간 검증

```bash
flutter test test/certified_macos_scripts_test.dart
```

예상 결과: focused test 전체 pass.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `test/certified_macos_scripts_test.dart` | REVIEW_SECURITY-1 |

## 최종 검증

```bash
bash -n scripts/setup-appsok-ci-secrets.sh scripts/build-certified-macos.sh scripts/upsert-jenkins-certified-job.sh
```

예상 결과: exit 0.

```bash
flutter test test/certified_macos_scripts_test.dart
```

예상 결과: focused test 전체 pass.

```bash
ssh toki@toki-labs.com 'zsh -lc "cd \"$HOME/docker/services/code-server/data/volume/workspace/appsok\" && flutter test test/certified_macos_scripts_test.dart"'
```

예상 결과: remote runner focused test 전체 pass. 실행 불가 시 실제 SSH/환경 오류 출력을 review stub에 기록한다.

```bash
git diff --check
```

예상 결과: whitespace error 없음.

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