<!-- task=m-mattermost-nexo-messaging-alignment/02+01_signed_smoke plan=0 tag=TEST -->

# Plan - TEST Signed Push Smoke

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

`CODE_REVIEW-cloud-G08.md`의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 반드시 채운다. 검증을 실행하고 실제 stdout/stderr를 붙이며, active 파일은 그대로 둔 채 리뷰 준비를 보고한다. 최종 판정, log rename, `complete.log`, archive 이동은 code-review-skill 전용이다. 사용자 결정, 외부 환경 준비, 범위 충돌 없이는 진행할 수 없으면 review stub의 `사용자 리뷰 요청` 섹션에 정확한 증거를 채우고 멈춘다.

## 배경

`Mattermost Nexo Messaging Alignment` 마일스톤은 NomadCode host와 `../nexo/packages/messaging_flutter` 사이의 push 책임 경계를 닫는 중이다. 남은 일은 server-generated signed push를 실제 Android runner에서 다시 보내고 FCM receipt, signature, ACK, notification, opened, navigation, inline reply, dismiss evidence를 redaction 상태로 남기는 것이다. 이 plan은 preflight 완료 후 smoke를 실행하고 local test profile의 Evidence Checklist를 실제 결과로 갱신한다.

## 사용자 리뷰 요청 흐름

구현 중 blocker는 active `CODE_REVIEW-cloud-G08.md`의 `사용자 리뷰 요청` 섹션에 기록한다. 형식은 `agent-ops/skills/common/_templates/implementation-user-review-request-section.md`와 같으며, code-review가 검증 후 실제 `USER_REVIEW.md` 작성 여부를 결정한다.

## 분석 결과

### 읽은 파일

- `agent-roadmap/current.md`
- `agent-roadmap/phase/external-integration/PHASE.md`
- `agent-roadmap/phase/external-integration/milestones/mattermost-nexo-messaging-alignment.md`
- `agent-test/local/rules.md`
- `agent-test/local/mobile-smoke.md`
- `agent-test/local/mobile-android-sdk-smoke.md`
- `agent-test/local/mattermost-server-generated-push-smoke.md`
- `agent-ops/rules/project/domain/mobile/rules.md`
- `agent-ops/rules/project/domain/workspace-ops/rules.md`

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

- Existing Flutter tests do not cover FCM delivery through Mattermost push-proxy, Android notification tap, inline reply, dismiss/clear, or device token prefix evidence.
- The gap is intentional for this task: coverage comes from the remote Android smoke and a redacted Evidence Checklist update.

### 심볼 참조

- none. 이 plan은 symbol rename/remove를 하지 않는다.

### 분할 판단

- split decision policy를 파일 선택 전에 평가했다.
- shared task group: `m-mattermost-nexo-messaging-alignment`
- current subtask: `02+01_signed_smoke`, depends on `01_runner_preflight`.
- 이 task는 real FCM/Mattermost/Android runtime smoke와 local evidence doc update를 함께 수행한다. Preflight failure와 smoke behavior failure를 분리하기 위해 `01_runner_preflight`의 `complete.log`가 먼저 필요하다.

### 범위 결정 근거

- 수정 대상은 `agent-test/local/mattermost-server-generated-push-smoke.md`의 redacted evidence 결과와 active `CODE_REVIEW-cloud-G08.md` 기록으로 제한한다.
- App source, Android native runner, `../nexo/packages/messaging_flutter`, core Mattermost REST adapter, iOS/macOS runtime은 수정하지 않는다.
- Positive path는 Mattermost server-generated payload만 사용한다. Hand-built placeholder signature payload는 positive evidence로 인정하지 않는다.

### 빌드 등급

- build lane: `cloud-G08`, review lane: `cloud-G08`.
- 이유: auth-bearing external service, remote Android runtime, FCM, notification UI interaction, redaction correctness가 모두 성공 조건인 high-risk smoke다.

## 구현 체크리스트

- [ ] [TEST-1] `01_runner_preflight` 완료를 확인하고 remote Android baseline commands를 실행한다.
- [ ] [TEST-2] Mattermost server-generated signed push를 발생시키고 redacted FCM/notification evidence를 수집한다.
- [ ] [TEST-3] `agent-test/local/mattermost-server-generated-push-smoke.md`의 Evidence Checklist를 PASS/PARTIAL/BLOCKED/N/A 결과로 갱신한다. 검증: `agent-test/local/mattermost-server-generated-push-smoke.md`의 Evidence Checklist가 PASS/PARTIAL/BLOCKED로 기록된다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## 의존 관계 및 구현 순서

- `02+01_signed_smoke`는 같은 task group의 `01_runner_preflight`에 의존한다.
- 구현 시작 전 `find agent-task -path '*/m-mattermost-nexo-messaging-alignment/01_runner_preflight/complete.log' -print -quit | grep .`가 complete log 경로를 출력해야 한다.
- Remote runner의 direct SSH command는 non-login shell이라 `flutter` PATH를 못 볼 수 있다. Flutter/Android/adb commands는 `zsh -lc '<command>'`로 실행한다.

### [TEST-1] Remote Android Baseline

#### 문제

`agent-test/local/mobile-android-sdk-smoke.md`는 Android SDK dependent 검증에 `flutter build apk --debug`와 device/runtime smoke를 요구한다. `agent-test/local/mattermost-server-generated-push-smoke.md:48`-`63`은 Mattermost signed push smoke 전에 analyze, test, Android device run을 포함한다.

#### 해결 방법

Preflight 완료 후 remote runner에서 검증 대상 checkout으로 이동해 Flutter baseline, Android build, device 목록을 확인한다.

Before (`agent-test/local/mattermost-server-generated-push-smoke.md:48`-`55`):

```markdown
- lint: `cd apps/client && flutter analyze --no-fatal-infos`
- unit: `cd apps/client && flutter test`
- smoke:
  - `flutter doctor -v`
  - `echo "$ANDROID_HOME"`
  - `echo "$ANDROID_SDK_ROOT"`
  - `flutter devices`
  - `cd apps/client && flutter run -d emulator-5554 --no-resident`
```

After: no source change. Record the actual command output in `CODE_REVIEW-cloud-G08.md`.

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

- [ ] `CODE_REVIEW-cloud-G08.md`: dependency complete log path 기록
- [ ] `CODE_REVIEW-cloud-G08.md`: remote `flutter analyze`, `flutter test`, `flutter build apk --debug`, `flutter devices` output 기록

#### 테스트 작성

- Skip. 이 항목은 existing Flutter/Android commands를 실행하는 verification task다.

#### 중간 검증

```bash
find agent-task -path '*/m-mattermost-nexo-messaging-alignment/01_runner_preflight/complete.log' -print -quit | grep .
```

Expected: `01_runner_preflight/complete.log` path is printed.

```bash
ssh toki@toki-labs.com 'set -eu
if [ -d /config/workspace/nomadcode/.git ]; then
  cd /config/workspace/nomadcode
elif [ -d "$HOME/works/nomadcode/.git" ]; then
  cd "$HOME/works/nomadcode"
else
  echo "nomadcode checkout missing"
  exit 2
fi
REPO_DIR="$PWD"
zsh -lc "cd \"$REPO_DIR/apps/client\" && flutter pub get && flutter analyze --no-fatal-infos && flutter test && flutter build apk --debug && flutter devices"
'
```

Expected: commands complete successfully and device/emulator availability is printed.

### [TEST-2] Server-Generated Signed Push Smoke

#### 문제

`agent-roadmap/phase/external-integration/milestones/mattermost-nexo-messaging-alignment.md:42` says FCM receipt, signature verification, ACK, and notification display have partial evidence, while opened event, channel navigation, inline reply, dismiss/clear, and device token prefix still need actual evidence. `agent-test/local/mattermost-server-generated-push-smoke.md:67`-`70` requires private input, server-generated positive push, redacted evidence, and PASS/PARTIAL/BLOCKED/N/A judgment.

#### 해결 방법

Remote runner에서 app을 실행한 뒤, private secrets에서 필요한 Mattermost values만 local shell env로 export하고 REST API로 direct message를 생성한다. Raw token/password/FCM token/signing key/signature는 출력하지 않는다. Remote logcat/dumpsys evidence는 redaction 후 `/tmp`에 저장하고 review stub에는 sanitized excerpts만 붙인다.

Before (`agent-test/local/mattermost-server-generated-push-smoke.md:56`-`61`):

```markdown
  - Mattermost REST API로 server-generated DM 또는 mention을 생성한다. Positive path는 hand-built placeholder signature를 사용하지 않는다.
  - Resolve bot user and receiver user: `GET /api/v4/users/me`, `GET /api/v4/users/email/<receiver-email>`.
  - Open or create a direct channel: `POST /api/v4/channels/direct` with `["<bot-user-id>", "<receiver-user-id>"]`.
  - Post the smoke message: `POST /api/v4/posts` with `{"channel_id":"<direct-channel-id>","message":"nomadcode signed push smoke <timestamp>"}`.
  - REST calls use `Authorization: Bearer <bot-access-token-from-local-test-secrets>`.
- e2e: FCM receipt, valid signature, ACK, notification display, opened event, channel navigation, inline reply, dismiss/clear, device token prefix evidence를 redaction 상태로 수집한다.
```

After: no source change. Record redacted command output and evidence excerpts in `CODE_REVIEW-cloud-G08.md`.

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

- [ ] `CODE_REVIEW-cloud-G08.md`: app launch command output 기록
- [ ] `CODE_REVIEW-cloud-G08.md`: Mattermost REST trigger output 기록, token 원문 제외
- [ ] `CODE_REVIEW-cloud-G08.md`: redacted logcat/dumpsys evidence path와 sanitized excerpts 기록

#### 테스트 작성

- Skip. 이 항목은 external e2e smoke이며, regression is the smoke evidence itself.

#### 중간 검증

```bash
ssh toki@toki-labs.com 'set -eu
if [ -d /config/workspace/nomadcode/.git ]; then
  cd /config/workspace/nomadcode
elif [ -d "$HOME/works/nomadcode/.git" ]; then
  cd "$HOME/works/nomadcode"
else
  echo "nomadcode checkout missing"
  exit 2
fi
DEVICE_ID="${DEVICE_ID:-emulator-5554}"
REPO_DIR="$PWD"
zsh -lc "cd \"$REPO_DIR/apps/client\" && flutter run -d \"$DEVICE_ID\" --no-resident"
'
```

Expected: app launches on the selected Android device/emulator. If `emulator-5554` is not the preflight device id, record the replacement `DEVICE_ID` in `계획 대비 변경 사항`.

```bash
: "${MM_BASE_URL:?export from agent-test/local/testing-secrets.md without printing the value}"
: "${MM_BOT_TOKEN:?export from agent-test/local/testing-secrets.md without printing the value}"
: "${MM_RECEIVER_EMAIL:?export from agent-test/local/testing-secrets.md without printing the value}"
python3 - <<'PY'
import datetime
import json
import os
import urllib.parse
import urllib.request

base = os.environ["MM_BASE_URL"].rstrip("/")
token = os.environ["MM_BOT_TOKEN"]
receiver = os.environ["MM_RECEIVER_EMAIL"]
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}

def req(method, path, data=None):
    body = None if data is None else json.dumps(data).encode()
    request = urllib.request.Request(base + path, data=body, headers=headers, method=method)
    with urllib.request.urlopen(request, timeout=30) as response:
        return json.load(response)

bot = req("GET", "/api/v4/users/me")
user = req("GET", "/api/v4/users/email/" + urllib.parse.quote(receiver, safe=""))
channel = req("POST", "/api/v4/channels/direct", [bot["id"], user["id"]])
message = "nomadcode signed push smoke " + datetime.datetime.utcnow().isoformat(timespec="seconds") + "Z"
post = req("POST", "/api/v4/posts", {"channel_id": channel["id"], "message": message})
print(json.dumps({"post_id": post.get("id"), "channel_id": channel.get("id"), "message_prefix": "nomadcode signed push smoke"}, ensure_ascii=False))
PY
```

Expected: a Mattermost post id and channel id are printed without token/password/signature values.

```bash
ssh toki@toki-labs.com 'set -eu
zsh -lc '"'"'
adb logcat -d |
  sed -E "s/(Bearer )[A-Za-z0-9._-]+/\1<redacted>/g; s/(token=)[A-Za-z0-9:_-]+/\1<redacted>/g; s/(signature=)[A-Za-z0-9=:+/_-]+/\1<redacted>/g" |
  grep -E "FirebaseApp|MattermostAuth|NexoFCM|ReceiptDelivery|Notification skipped|inline|dismiss|clear|android_rn-v2|notification" \
  > /tmp/nomadcode-mm-smoke-logcat.redacted.txt
adb shell dumpsys notification |
  sed -E "s/(Bearer )[A-Za-z0-9._-]+/\1<redacted>/g; s/(token=)[A-Za-z0-9:_-]+/\1<redacted>/g; s/(signature=)[A-Za-z0-9=:+/_-]+/\1<redacted>/g" \
  > /tmp/nomadcode-mm-smoke-notification.redacted.txt
printf "/tmp/nomadcode-mm-smoke-logcat.redacted.txt\n"
printf "/tmp/nomadcode-mm-smoke-notification.redacted.txt\n"
'"'"'
'
```

Expected: redacted evidence file paths are printed. Review excerpts before copying into `CODE_REVIEW-cloud-G08.md`.

### [TEST-3] Evidence Checklist Update

#### 문제

`agent-test/local/mattermost-server-generated-push-smoke.md:124`-`138` currently lists expected outcomes only. The milestone requires the checklist to record PASS/PARTIAL/BLOCKED results after the actual Android runner smoke.

#### 해결 방법

Preserve the expected outcome column and add actual result/evidence columns using only redacted evidence. Use `PASS`, `PARTIAL`, `BLOCKED`, or `N/A`; do not paste raw secrets or payload signatures.

Before (`agent-test/local/mattermost-server-generated-push-smoke.md:124`-`138`):

```markdown
## Evidence Checklist

| 항목 | 기대 |
|------|------|
| FCM payload wakes / receipt | PASS |
| valid server-generated signature 처리 | PASS |
| invalid / unsigned drop | PASS |
| notification display | PASS |
| ACK request | PASS |
| opened event | PASS |
| channel navigation | PASS |
| thread navigation | N/A unless the payload has `root_id` |
| inline reply | PASS expected for NomadCode host auth wiring |
| dismiss / clear | PASS |
| device token prefix | PASS |
```

After:

```markdown
## Evidence Checklist

| 항목 | 기대 | 결과 | Evidence |
|------|------|------|----------|
| FCM payload wakes / receipt | PASS | PASS/PARTIAL/BLOCKED | Redacted logcat excerpt or blocker reason |
| valid server-generated signature 처리 | PASS | PASS/PARTIAL/BLOCKED | Redacted logcat excerpt or blocker reason |
| invalid / unsigned drop | PASS | PASS/PARTIAL/BLOCKED/N/A | Negative-control evidence, N/A reason, or blocker |
| notification display | PASS | PASS/PARTIAL/BLOCKED | Redacted dumpsys/UI evidence or blocker reason |
| ACK request | PASS | PASS/PARTIAL/BLOCKED | Redacted ACK excerpt or blocker reason |
| opened event | PASS | PASS/PARTIAL/BLOCKED | Redacted event excerpt or blocker reason |
| channel navigation | PASS | PASS/PARTIAL/BLOCKED | Redacted navigation excerpt or blocker reason |
| thread navigation | N/A unless the payload has `root_id` | N/A | No `root_id` in smoke payload |
| inline reply | PASS expected for NomadCode host auth wiring | PASS/PARTIAL/BLOCKED | Redacted reply evidence or blocker reason |
| dismiss / clear | PASS | PASS/PARTIAL/BLOCKED | Redacted dismiss/cancel evidence or blocker reason |
| device token prefix | PASS | PASS/PARTIAL/BLOCKED | Redacted prefix evidence, never raw FCM token |
```

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

- [ ] `agent-test/local/mattermost-server-generated-push-smoke.md`: Evidence Checklist 결과 column 추가
- [ ] `agent-test/local/mattermost-server-generated-push-smoke.md`: 각 row에 PASS/PARTIAL/BLOCKED/N/A와 redacted evidence 또는 blocker reason 기록
- [ ] `CODE_REVIEW-cloud-G08.md`: evidence table 변경 요약과 secret redaction 확인 기록

#### 테스트 작성

- Skip. Evidence Checklist 자체가 이 task의 verification artifact다.

#### 중간 검증

```bash
rg --sort path '^\| (FCM payload wakes / receipt|valid server-generated signature 처리|invalid / unsigned drop|notification display|ACK request|opened event|channel navigation|thread navigation|inline reply|dismiss / clear|device token prefix) \| [^|]+ \| (PASS|PARTIAL|BLOCKED|N/A) \|' agent-test/local/mattermost-server-generated-push-smoke.md | wc -l
```

Expected: `11`.

```bash
rg --sort path -n 'Bearer [A-Za-z0-9._-]+|[A-Za-z0-9_-]{120,}' agent-test/local/mattermost-server-generated-push-smoke.md
```

Expected: no output.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `agent-test/local/mattermost-server-generated-push-smoke.md` | TEST-3 |
| `agent-task/m-mattermost-nexo-messaging-alignment/02+01_signed_smoke/CODE_REVIEW-cloud-G08.md` | TEST-1, TEST-2, TEST-3 |

## 최종 검증

```bash
find agent-task -path '*/m-mattermost-nexo-messaging-alignment/01_runner_preflight/complete.log' -print -quit | grep .
rg --sort path '^\| (FCM payload wakes / receipt|valid server-generated signature 처리|invalid / unsigned drop|notification display|ACK request|opened event|channel navigation|thread navigation|inline reply|dismiss / clear|device token prefix) \| [^|]+ \| (PASS|PARTIAL|BLOCKED|N/A) \|' agent-test/local/mattermost-server-generated-push-smoke.md | wc -l
rg --sort path -n 'Bearer [A-Za-z0-9._-]+|[A-Za-z0-9_-]{120,}' agent-test/local/mattermost-server-generated-push-smoke.md
```

Expected: dependency complete log path is printed, checklist count is `11`, and secret scan has no output.

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