update: client-validation milestone, FCM ack smoke task, flutter-test fixes, and skill updates

This commit is contained in:
toki 2026-05-29 16:47:09 +09:00
parent eb04e93d45
commit 77d615c6b7
11 changed files with 438 additions and 112 deletions

110
HANDOFF.md Normal file
View file

@ -0,0 +1,110 @@
# Handoff: client-validation / FCM ACK Smoke
Date: 2026-05-29
## Active Route
- Milestone: `agent-roadmap/phase/product-foundation/milestones/client-validation.md`
- Task: `agent-task/m-client-validation/04+03_fcm_ack_smoke/`
- Plan: `agent-task/m-client-validation/04+03_fcm_ack_smoke/PLAN-cloud-G07.md`
- Review stub: `agent-task/m-client-validation/04+03_fcm_ack_smoke/CODE_REVIEW-cloud-G07.md`
- State: incomplete, blocked on valid signed push payload or scope decision.
Do not record raw FCM tokens, auth tokens, signing keys, private endpoint values,
screenshots with secrets, or unredacted logs in tracked files.
## Completed In This Loop
- `agent-ops/skills/common/plan/SKILL.md` and
`agent-ops/skills/common/code-review/SKILL.md` now resolve split predecessor
completion from both active task paths and `agent-task/archive/**`.
- Archived predecessor is valid:
`agent-task/archive/2026/05/m-client-validation/03_smoke_preflight/complete.log`.
- `apps/flutter-test` now initializes Firebase and `NexoMessagingPlugin` at app
startup, after callbacks are registered.
- Widget/integration tests can construct `MyApp(initializePlugin: false)` so
deterministic tests are not tied to Firebase/FCM runtime setup.
- `apps/flutter-test` has direct `firebase_core` dependency.
- Local `cd apps/flutter-test && flutter analyze` passed.
- Local `cd apps/flutter-test && flutter test` passed.
- Remote `cd apps/flutter-test && flutter pub get && flutter analyze && flutter test`
passed.
- Remote app run with
`ORG_GRADLE_PROJECT_nexoSmokeApplicationId=com.tokilabs.mattermost` initialized
Firebase/plugin and saved an FCM token.
- Remote push-proxy direct sends returned `{"status":"OK"}`.
- Device logcat confirmed FCM receipt and ACK request:
- `NexoFCM : >>> onMessageReceived ENTER`
- `NexoFCM : onMessageReceived type=message ... ackId=...`
- `ReceiptDelivery: Send ACK=...`
- `NomadNetwork: POST https://mattermost.toki-labs.com/api/v4/notifications/ack`
- Placeholder and unsigned payloads were rejected with
`Notification skipped: signature verification failed`, which is expected.
## Current Blocker
Notification display and downstream behavior are still blocked because the smoke
does not yet have a valid server-generated signed push payload.
Confirmed PASS:
- Archived preflight dependency.
- Remote readiness.
- Firebase/plugin initialization.
- Device token save, without recording raw token.
- FCM receipt.
- ACK POST request evidence.
- Invalid/unsigned signature rejection.
Still BLOCKED:
- Valid signature pass.
- System notification display.
- Notification tap/opened event.
- Channel/thread routing.
- Inline reply.
- Dismiss action.
Remote integration note: `flutter test integration_test -d emulator-5554` built
and installed the app, then produced no runner output for more than 40 seconds.
The local ssh wrapper was killed. Treat this as inconclusive, not PASS.
## Continue From Here
1. Keep active files in place. Do not write `complete.log` or move the task to
archive until code-review flow decides it.
2. Decide whether to provide a valid signed push trigger or split the remaining
notification UI checks into a follow-up task.
3. If continuing the smoke, use the remote environment from
`agent-ops/rules/private/testing-env.md`.
4. Continue with device `emulator-5554` and:
`ORG_GRADLE_PROJECT_nexoSmokeApplicationId=com.tokilabs.mattermost`.
5. Do not print the raw token. Keep it only in the remote shell env used for the
push request.
6. Re-run push with a valid signature, then collect redacted evidence for
display/opened/routing/reply/dismiss.
7. Update `CODE_REVIEW-cloud-G07.md` first. User-review or final archive
decisions remain code-review responsibilities.
Useful remote evidence paths already referenced in the review stub:
- `/tmp/nexo-flutter-test-run-after-init.log`
- `/tmp/nexo-token-logcat-after-init.txt`
- `/tmp/nexo-smoke-screen-after-init.png`
- `/tmp/nexo-send-response.json`
- `/tmp/nexo-after-send-logcat.txt`
- `/tmp/nexo-send-response-nosig.json`
- `/tmp/nexo-after-send-logcat-nosig.txt`
## Source Files Touched
- `agent-ops/skills/common/plan/SKILL.md`
- `agent-ops/skills/common/code-review/SKILL.md`
- `agent-roadmap/phase/product-foundation/milestones/client-validation.md`
- `agent-task/m-client-validation/04+03_fcm_ack_smoke/PLAN-cloud-G07.md`
- `agent-task/m-client-validation/04+03_fcm_ack_smoke/CODE_REVIEW-cloud-G07.md`
- `apps/flutter-test/lib/main.dart`
- `apps/flutter-test/pubspec.yaml`
- `apps/flutter-test/pubspec.lock`
- `apps/flutter-test/test/widget_test.dart`
- `apps/flutter-test/integration_test/plugin_integration_test.dart`

View file

@ -35,6 +35,7 @@ Implementation may stop early by filling the active review stub's `사용자 리
- If `review-number == 1`, the verdict is `FAIL`, and the blocking issue is a test environment problem rather than a repo-fixable implementation issue, stop the automatic loop and write `USER_REVIEW.md`.
- If the active review file has a filled `사용자 리뷰 요청` with `상태` other than `없음`, and the blocker is a user-only decision, external environment prerequisite, or scope conflict that the repository cannot safely resolve by code changes alone, stop the automatic loop and write `USER_REVIEW.md`.
- Treat a first-review failure as a test environment problem only with evidence: missing SDK/tool/runtime, unavailable external service, absent secret/credential, unsupported OS/device/interactive environment, or another prerequisite outside the repository. Record the exact command and stdout/stderr. Before claiming a tool is missing, require `command -v <tool>` or the project-equivalent check.
- If the blocker claims a split predecessor `complete.log` is missing, check both `agent-task/{task_group}/{predecessor_subtask}/complete.log` and `agent-task/archive/*/*/{task_group}/{predecessor_subtask}/complete.log` before triggering `USER_REVIEW.md`. If an archived predecessor `complete.log` exists, do not use the active-only miss as a user-review reason; write a normal WARN/FAIL follow-up plan that cites the archived path unless independent user-only or external blockers remain.
- Treat an implementation-owned `사용자 리뷰 요청` as valid only with specific decision questions, evidence, attempted commands/output or a reason commands could not run, and clear resume conditions. Vague uncertainty, repo-owned setup bugs, and ordinary incomplete implementation become normal WARN/FAIL follow-up plans instead.
- Do not classify repo-owned setup bugs as environment problems. If a manifest, bootstrap script, test fixture, or documented project entrypoint should provide the dependency, write a normal follow-up plan instead.
- `USER_REVIEW.md` is created from `agent-ops/skills/common/code-review/templates/user-review-template.md`, filled with the archived loop history, current archived plan/review paths, verdict, loop count, blocking evidence, and user decision options.
@ -153,6 +154,7 @@ Before writing the verdict:
- Confirm the implementation marked the matching checklist items in the active review file, including the final mandatory `CODE_REVIEW-*-G??.md` completion item.
- Treat blank placeholder sections, missing actual implementation notes, missing checklist completion, or missing actual stdout/stderr in the active review file as a completeness or verification-trust failure.
- Read the `사용자 리뷰 요청` section. If `상태` is not `없음`, validate the exact decision needed, blocker evidence, command output or not-run reason, and resume condition before deciding whether the user-review gate triggers.
- For split dependency blockers, verify predecessor completion against active and archived `complete.log` paths before accepting the blocker as user-review-worthy.
- Grep renamed/removed symbols for stale references.
- Confirm every required test exists, name matches, and assertions are meaningful.
- Cross-check claimed verification output in the active review file against actual code and project commands.

View file

@ -85,6 +85,15 @@ Task directory naming rules:
- Example: split independent docs/UI plus an integration under one task group as `01_core`, `02+01_db`, `03+02_api`, `04_docs`, `05_ui`, `06+05_integration`; `01_core`, `04_docs`, and `05_ui` can start together, and `06+05_integration` waits only for `05_ui`.
- Preserve task group and subtask directory names verbatim; do not normalize, reinterpret, or choose execution order by agent judgment.
Split predecessor completion lookup:
- For a dependent subtask directory `NN+PP[,QQ...]_{subtask_name}`, resolve each predecessor subtask before writing the plan.
- Check predecessor completion in this order: `agent-task/{task_group}/{predecessor_subtask}/complete.log`, then `agent-task/archive/*/*/{task_group}/{predecessor_subtask}/complete.log`.
- If the predecessor is found only in archive, treat it as complete and cite the actual archived `complete.log` path in `분석 결과` and `의존 관계 및 구현 순서`; do not require the predecessor to remain active.
- If multiple archived exact-path candidates exist, choose the newest `YYYY/MM` path and record all candidates in `분석 결과`.
- If no active or archived `complete.log` exists, treat the predecessor as incomplete and plan the predecessor first or ask for a routing decision. Do not invent marker files for downstream work.
- The downstream implementation must verify the resolved predecessor path from the plan, not a hard-coded active-only path.
Routing rules:
- Build defaults to `local` when the plan is explicit, tests are runnable, and failure is review-detectable.
@ -170,6 +179,7 @@ Complete all items below before creating any files. Work through them in order;
- [ ] **Pre-check compile issues** — identify missing interface implementations, type mismatches, and broken imports.
- [ ] **Verify verification commands** — confirm that the final verification commands actually run in this repository layout.
- [ ] **Stabilize fragile verification** — for search or generated-output checks, choose deterministic commands up front, such as `rg --sort path`, and decide whether cached test output is acceptable or `-count=1` is required.
- [ ] **Resolve split predecessors** — for every `NN+PP[,QQ...]_...` dependency, search active and archived `complete.log` paths using the split predecessor completion lookup rule and record the resolved path or missing state.
## Step 3 - Determine GXX Grade
@ -235,7 +245,7 @@ Each plan item must include:
Include `의존 관계 및 구현 순서` only when order matters.
For split multi-plan work, the `{subtask_dir}` directory name is the runtime source of truth. If a plan has a `NN+PP[,QQ...]_...` subtask directory name, `의존 관계 및 구현 순서` must echo the decoded predecessor subtask directories under the same task group that must produce `complete.log` before implementation starts, and it must not add dependencies that are absent from the directory name.
For split multi-plan work, the `{subtask_dir}` directory name is the runtime source of truth. If a plan has a `NN+PP[,QQ...]_...` subtask directory name, `의존 관계 및 구현 순서` must echo the decoded predecessor subtask directories under the same task group and the resolved active or archived `complete.log` path for each predecessor. It must not add dependencies that are absent from the directory name.
Quality rules:

View file

@ -20,8 +20,9 @@
## 구현 잠금
- 상태: 해제
- 결정 필요: 없음
- 상태: 잠금
- 결정 필요:
- valid signed payload를 제공해 `04+03_fcm_ack_smoke`를 끝까지 진행할지, 이번 loop는 FCM receipt + ACK까지로 축소하고 notification UI 계열은 후속으로 분리할지 결정한다.
## 범위
@ -46,7 +47,7 @@
자동화하기 어려운 FCM과 서버 ACK 검증을 수동 체크로 분리한다.
- [ ] [fcm-manual] 실제 Firebase FCM delivery와 서버 ACK 검증 절차를 문서화한다.
- [ ] [fcm-manual] 실제 Firebase FCM delivery와 서버 ACK 검증 절차를 문서화한다. 진행: `04+03_fcm_ack_smoke`에서 remote FCM receipt와 ACK POST evidence까지 확인했다. 남은 검증: valid signed payload, notification display, opened-routing, inline reply, dismiss.
- [ ] [remote-guide] 로컬 Android SDK가 없을 때 원격 테스트 환경 문서가 client README와 연결되도록 유지한다.
## 완료 리뷰
@ -69,8 +70,8 @@
- 관련 경로: `apps/flutter-test/`, `packages/messaging_flutter/`, `packages/messaging_flutter/docs/android-test-environment.md`
- 표준선(선택): client는 실제 소비 앱 모양의 검증 호스트이며 별도 `/example`이나 `/sandbox`를 두지 않는다. Flutter SDK는 nexo-owned embedded SDK로 유지한다
- 진행 메모: `thin-client``apps/flutter-test/README.md``apps/flutter-test/test/widget_test.dart` 기준을 맞췄다. `sdk-boundary``packages/messaging_flutter/README.md`의 SDK ownership/test strategy 기준을 보강했다.
- 활성 계획: `agent-task/m-client-validation/01_integration_path/`, `agent-task/m-client-validation/02_native_test/`
- 진행 메모: `thin-client``apps/flutter-test/README.md``apps/flutter-test/test/widget_test.dart` 기준을 맞췄다. `sdk-boundary``packages/messaging_flutter/README.md`의 SDK ownership/test strategy 기준을 보강했다. `04+03_fcm_ack_smoke`는 archived `03_smoke_preflight` 완료를 승계해 `apps/flutter-test`의 Firebase/plugin 초기화 누락을 보정했고, local/remote `flutter analyze``flutter test` PASS, remote FCM token 저장, push-proxy send OK, device FCM receipt와 ACK POST까지 확인했다. valid signed payload가 없어 notification display/opened-routing/inline reply/dismiss evidence는 아직 BLOCKED다.
- 활성 계획: `agent-task/m-client-validation/01_integration_path/`, `agent-task/m-client-validation/02_native_test/`, `agent-task/m-client-validation/04+03_fcm_ack_smoke/`
- 선행 작업: client migration과 1차 테스트
- 후속 작업: 메시징 계약 표준화, 알림 파이프라인 고도화
- 확인 필요: 없음
- 후속 작업: valid server-generated signed push trigger 확보 후 notification display, opened-routing, inline reply, dismiss evidence 수집. 이후 메시징 계약 표준화, 알림 파이프라인 고도화
- 확인 필요: valid signed payload를 제공해 현재 smoke를 끝까지 진행할지, 이번 loop는 FCM receipt + ACK까지로 축소하고 notification UI 계열은 후속으로 분리할지 결정 필요

View file

@ -1,6 +1,6 @@
<!-- task=m-client-validation/04+03_fcm_ack_smoke plan=0 tag=SMOKE_RUN -->
<!-- task=m-client-validation/04+03_fcm_ack_smoke plan=1 tag=REVIEW_SMOKE_RUN -->
# Code Review Reference - SMOKE_RUN
# Code Review Reference - REVIEW_SMOKE_RUN
> **[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.
@ -13,7 +13,7 @@
## 개요
date=2026-05-29
task=m-client-validation/04+03_fcm_ack_smoke, plan=0, tag=SMOKE_RUN
task=m-client-validation/04+03_fcm_ack_smoke, plan=1, tag=REVIEW_SMOKE_RUN
## 이 파일을 읽는 리뷰 에이전트에게
@ -23,7 +23,7 @@ task=m-client-validation/04+03_fcm_ack_smoke, plan=0, tag=SMOKE_RUN
리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다.
1. 판정을 append한다.
2. `CODE_REVIEW-cloud-G07.md` -> `code_review_cloud_G07_N.log`, `PLAN-cloud-G07.md` -> `plan_cloud_G07_M.log`로 아카이브한다.
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-client-validation/04+03_fcm_ack_smoke/`로 이동한다. 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-<milestone-slug>`이면 완료 이벤트 메타데이터를 보고한다. roadmap 상태 체크와 `update-roadmap` 호출은 런타임 책임이다.
5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다.
@ -34,16 +34,18 @@ task=m-client-validation/04+03_fcm_ack_smoke, plan=0, tag=SMOKE_RUN
| 항목 | 완료 여부 |
|------|---------|
| [SMOKE_RUN-1] Android test host 실행 | [ ] |
| [SMOKE_RUN-2] FCM delivery와 ACK evidence 수집 | [ ] |
| [SMOKE_RUN-3] Manual Smoke Checklist evidence 정리 | [ ] |
| [REVIEW_SMOKE_RUN-1] Preflight와 remote readiness 승계 | [x] |
| [REVIEW_SMOKE_RUN-2] Android app 실행과 FCM token evidence | [x] |
| [REVIEW_SMOKE_RUN-3] Real push와 ACK evidence 수집 | [ ] |
| [REVIEW_SMOKE_RUN-4] Manual smoke checklist evidence 정리 | [ ] |
## 구현 체크리스트
- [ ] [SMOKE_RUN-1] `03_smoke_preflight` 완료를 확인하고 Android target에서 `apps/flutter-test`를 실행한다.
- [ ] [SMOKE_RUN-2] 실제 server push trigger를 실행하고 FCM receipt, signature handling, notification display, ACK response evidence를 수집한다.
- [ ] [SMOKE_RUN-3] opened-routing, inline reply, dismiss, device-token prefix evidence를 redact해서 review stub에 기록한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
- [x] [REVIEW_SMOKE_RUN-1] Archived preflight evidence와 원격 Android/trigger readiness를 확인하고 smoke 실행 변수를 정한다.
- [x] [REVIEW_SMOKE_RUN-2] 원격 Android target에서 `apps/flutter-test`를 실행하고 FCM device token evidence를 수집한다.
- [ ] [REVIEW_SMOKE_RUN-3] 실제 push trigger 또는 push-proxy `/api/v1/send_push`를 실행하고 FCM receipt, notification display, ACK/log evidence를 수집한다.
- [ ] [REVIEW_SMOKE_RUN-4] opened-routing, inline reply, dismiss, device-token prefix evidence를 redact해서 review stub에 기록한다.
- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
## 코드리뷰 전용 체크리스트
@ -64,29 +66,56 @@ task=m-client-validation/04+03_fcm_ack_smoke, plan=0, tag=SMOKE_RUN
## 계획 대비 변경 사항
_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._
- 사용자 추가 지시("너가 일단 진행해봐")에 따라 plan의 기본 범위를 확장해 repo-owned test host 초기화 누락을 직접 수정했다.
- `apps/flutter-test/lib/main.dart``WidgetsFlutterBinding.ensureInitialized()`, `Firebase.initializeApp()`, `NexoMessagingPlugin.instance.initialize()` 호출을 추가했다. `onDeviceTokenReady` callback이 등록된 뒤 plugin 초기화가 실행되도록 `MyApp` initState에서 비동기로 호출했다.
- Widget/integration tests가 Firebase/FCM 환경에 묶이지 않도록 `MyApp(initializePlugin: false)` 옵션을 추가하고 기존 tests는 이 옵션을 사용하게 했다.
- `firebase_core``apps/flutter-test/pubspec.yaml`의 direct dependency로 추가했다. `firebase_core` import가 app source에 직접 생겼기 때문이다.
- 원격 smoke는 계속 `ORG_GRADLE_PROJECT_nexoSmokeApplicationId=com.tokilabs.mattermost` override로 진행했다. 기본 `com.tokilabs.nexo_client`는 원격 `google-services.json`에 client가 없어 `:app:processDebugGoogleServices`에서 실패한다.
- REVIEW_SMOKE_RUN-3은 부분 진행됐다. raw token 확보 후 push-proxy `/api/v1/send_push``{"status":"OK"}`를 반환했고 device logcat에서 `NexoFCM` receipt와 `ReceiptDelivery` ACK POST를 확인했다. 그러나 placeholder/unsigned signature는 `Notification skipped: signature verification failed`로 막혀 notification display, opened-routing, inline reply, dismiss evidence는 아직 없다.
## 주요 설계 결정
_구현 에이전트가 주요 설계 결정 사항을 기록한다._
- 원격 readiness(REVIEW_SMOKE_RUN-1)는 archived preflight 승계 전제하에 통과: 원격 flutter/adb 존재, `emulator-5554` 연결, `NEXO_SMOKE_TRIGGER_CMD` non-empty, `push_proxy_url=http://localhost:18066`.
- 앱 초기화 수정 후 remote Android에서 `FirebaseInitProvider initialization successful`, `Permission: AuthorizationStatus.authorized`, `DatabaseHelper: Device token saved`, `[PushNotification] Device token saved`를 확인했다. Raw token 값은 출력하지 않고 remote `/tmp/nexo_global.db`에서 env로만 추출했다.
- token prefix는 `android_rn-v2:`가 맞다(`packages/messaging_flutter/lib/src/nexo_messaging_plugin.dart:14,211` → `'$_kDeviceTokenPrefix-v2:$token'`, prefix `android_rn`). Device token DB row length는 156이고 raw token은 review stub에 남기지 않았다.
- Actual push evidence는 두 단계로 나뉜다. FCM receipt/ACK path는 PASS: push-proxy response `{"status":"OK"}`, logcat `NexoFCM : >>> onMessageReceived ENTER`, `ReceiptDelivery: Send ACK=...`, `NomadNetwork: POST .../api/v4/notifications/ack`. Notification display/routing path는 BLOCKED: valid signed payload가 없어 `Notification skipped: signature verification failed`.
## 사용자 리뷰 요청
_기본값은 `없음`이다. 구현 중 사용자 결정, 외부 환경 준비, 또는 계획 범위 변경 없이는 안전하게 진행할 수 없으면 아래 항목을 실제 내용으로 교체하고, 구현을 중단한 뒤 active 파일을 그대로 둔 채 리뷰를 요청한다. code-review가 이 내용을 검증해 `USER_REVIEW.md`를 작성한다._
- 상태: 없음
- 사유 유형: 없음
- 결정 필요: 없음
- 차단 근거: 없음
- 실행한 검증/명령: 없음
- 재개 조건: 없음
- 상태: 차단(BLOCKED)
- 사유 유형: 외부 signed push payload 또는 signing material 부재
- 진행 경과: repo-owned test host 초기화 누락은 수정했고, raw FCM token 확보와 실제 FCM receipt/ACK evidence까지 수집했다. 남은 차단은 valid signature 단계다.
- 결정 필요:
1. 실제 server-generated valid signature가 포함된 push trigger를 제공할지.
2. 또는 smoke 범위를 이번 loop에서는 FCM receipt + ACK까지로 축소하고 notification display/opened/reply/dismiss/signature 항목을 후속으로 분리할지.
- 차단 근거:
- REVIEW_SMOKE_RUN-3: placeholder signature push와 unsigned push 모두 FCM receipt + ACK POST까지 도달했지만 `Notification skipped: signature verification failed`로 notification display 전에 중단됐다.
- 원격 app shared preferences에 `signing_key_https://mattermost.toki-labs.com`가 존재한다. 따라서 bogus/non-server signature는 정상적으로 reject된다.
- `NEXO_SMOKE_TRIGGER_CMD` helper는 현재 valid signed payload를 만들어 보내지 않는다.
- 실행한 검증/명령(실제 출력):
- Local: `flutter analyze` PASS, `flutter test` PASS.
- Remote: `flutter pub get && flutter analyze && flutter test` PASS.
- Remote integration: `flutter test integration_test -d emulator-5554` built and installed the app, then produced no runner output for over 40s; the local ssh wrapper was killed. Treat as inconclusive, not PASS.
- Remote run after init fix: `FirebaseInitProvider initialization successful`, `[PushNotification] Permission: AuthorizationStatus.authorized`, `DatabaseHelper: Device token saved`, `[PushNotification] Device token saved`.
- Remote push with placeholder signature: push-proxy response `{"status":"OK"}`; logcat `NexoFCM : >>> onMessageReceived ENTER`, `ReceiptDelivery: Send ACK=nexo-smoke-1780028173`, `NomadNetwork: POST https://mattermost.toki-labs.com/api/v4/notifications/ack`, then `Notification skipped: signature verification failed`.
- Remote push without signature: push-proxy response `{"status":"OK"}`; logcat `NexoFCM : >>> onMessageReceived ENTER`, `ReceiptDelivery: Send ACK=nexo-smoke-nosig-1780028205`, then `Notification skipped: signature verification failed`.
- Archived preflight `agent-task/archive/2026/05/m-client-validation/03_smoke_preflight/complete.log`: local `test -f` PASS.
- 미완: valid signature PASS, invalid/unsigned drop, notification display, opened-routing, inline reply, dismiss.
- 환경 정리: override 빌드는 emulator-5554의 기존 `com.tokilabs.mattermost`(Detox 자산)를 교체했다(사용자 승인). Detox 재사용이 필요하면 Mattermost 앱 재설치가 필요할 수 있음을 명시한다.
- 재개 조건:
1. Valid server-generated signature가 포함된 push trigger를 준비한다.
2. 같은 remote app/token 상태에서 trigger를 재실행한다.
3. Notification display 이후 tap/opened, inline reply, dismiss evidence를 수집한다.
## 리뷰어를 위한 체크포인트
- `03_smoke_preflight/complete.log` 존재 확인이 실제로 수행되었는지 확인한다.
- trigger/logcat/server evidence가 실제 출력 또는 파일 경로로 기록되었는지 확인한다.
- secret/token/signing key/server endpoint 원문이 review stub에 남지 않았는지 확인한다.
- manual checklist 항목이 PASS/BLOCKED/FAIL로 과장 없이 분류되었는지 확인한다.
- Preflight dependency가 active-only path가 아니라 archived `complete.log`로 확인되었는지 본다.
- `smoke_trigger_ready=1` readiness output을 actual FCM send evidence로 오인하지 않았는지 본다.
- Raw token, auth token, signing key, private endpoint values가 tracked file에 남지 않았는지 본다.
- Manual checklist 항목이 PASS/BLOCKED/FAIL로 과장 없이 분류되었는지 본다.
- Real push evidence와 ACK evidence가 stdout/stderr, logcat, server/push-proxy log, screenshot/recording path 중 재검토 가능한 형태인지 본다.
## 검증 결과
@ -98,35 +127,110 @@ _구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후
- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다.
- 사용자 리뷰 요청으로 명령을 끝까지 실행하지 못했다면 `사용자 리뷰 요청`에 실행한 명령, 실제 출력, 미실행 명령의 사유를 기록한다.
### SMOKE_RUN-1 중간 검증
```
$ test -f agent-task/m-client-validation/03_smoke_preflight/complete.log
$ test -n "${NEXO_ANDROID_DEVICE_ID:-}"
$ cd apps/flutter-test && flutter pub get
$ adb -s "$NEXO_ANDROID_DEVICE_ID" logcat -c
$ cd apps/flutter-test && flutter run -d "$NEXO_ANDROID_DEVICE_ID"
(output)
### REVIEW_SMOKE_RUN-1 중간 검증
```
$ test -f agent-task/archive/2026/05/m-client-validation/03_smoke_preflight/complete.log
exit=0
### SMOKE_RUN-2 중간 검증
```
$ test -n "${NEXO_SMOKE_TRIGGER_CMD:-}"
$ bash -lc "$NEXO_SMOKE_TRIGGER_CMD" 2>&1 | tee /tmp/nexo-smoke-trigger.log
$ adb -s "$NEXO_ANDROID_DEVICE_ID" logcat -d | rg "NexoFirebaseMessagingService|ReceiptDelivery|NotificationReplyBroadcastReceiver|NotificationDismissService|ACK|signature|opened|Navigate" | tee /tmp/nexo-smoke-logcat.txt
(output)
$ ssh toki@toki-labs.com 'zsh -lc '...'
set -e
command -v flutter -> /Users/toki/SDK/flutter/bin/flutter
command -v adb -> /Users/toki/Library/Android/sdk/platform-tools/adb
test -n "${NEXO_SMOKE_TRIGGER_CMD:-}" -> (set -e 통과 = non-empty)
adb devices:
List of devices attached
emulator-5554 device
push_proxy_url=http://localhost:18066
remote_exit=0
```
PASS: 원격 readiness 충족.
### SMOKE_RUN-3 중간 검증
### REVIEW_SMOKE_RUN-2 중간 검증
```
$ rg --sort path -n "FCM payload wakes|ACK request|onNotificationOpened|onNavigateToChannel|onNavigateToThread|Inline reply|Dismiss action|Device token" agent-task/m-client-validation/04+03_fcm_ack_smoke/CODE_REVIEW-cloud-G07.md
(output)
$ flutter analyze
Analyzing flutter-test...
No issues found! (ran in 4.2s)
$ flutter test
00:00 +4: All tests passed!
$ ssh toki@toki-labs.com 'zsh -lc "cd ~/works/nexo/apps/flutter-test && flutter pub get && flutter analyze && flutter test"'
Analyzing flutter-test...
No issues found! (ran in 5.5s)
00:00 +4: All tests passed!
$ ssh toki@toki-labs.com 'zsh -lc "cd ~/works/nexo/apps/flutter-test && flutter test integration_test -d emulator-5554"'
✓ Built build/app/outputs/flutter-apk/app-debug.apk
Installing build/app/outputs/flutter-apk/app-debug.apk...
# 이후 테스트 runner 출력 없이 40초 이상 대기. local ssh wrapper kill, exit=255. Inconclusive.
$ ssh toki@toki-labs.com 'zsh -lc "... flutter run -d emulator-5554 --no-resident ..."'
✓ Built build/app/outputs/flutter-apk/app-debug.apk
Installing build/app/outputs/flutter-apk/app-debug.apk...
FirebaseInitProvider: FirebaseApp initialization successful
[PushNotification] Permission: AuthorizationStatus.authorized
DatabaseHelper: Device token saved
[PushNotification] Device token saved
```
PASS: app initialization fix works locally and on remote. Remote app stores an FCM token. Raw token was not printed; screenshot path `/tmp/nexo-smoke-screen-after-init.png`, run log path `/tmp/nexo-flutter-test-run-after-init.log`, token log path `/tmp/nexo-token-logcat-after-init.txt`.
### REVIEW_SMOKE_RUN-3 중간 검증
```
$ ssh toki@toki-labs.com 'zsh -lc "... curl -fsS -X POST http://localhost:18066/api/v1/send_push ..."'
{"status":"OK"}
# placeholder signature push, ack_id=nexo-smoke-1780028173
NexoFCM : >>> onMessageReceived ENTER, dataSize=20
NexoFCM : onMessageReceived type=message channelId=nexo-smoke-channel ackId=nexo-smoke-1780028173 serverUrl=https://mattermost.toki-labs.com
ReceiptDelivery: Send ACK=nexo-smoke-1780028173 TYPE=message to URL=https://mattermost.toki-labs.com ID-LOADED=false
NomadNetwork: POST https://mattermost.toki-labs.com/api/v4/notifications/ack
NexoFCM : Notification skipped: signature verification failed
$ ssh toki@toki-labs.com 'zsh -lc "... curl -fsS -X POST http://localhost:18066/api/v1/send_push ..."'
{"status":"OK"}
# unsigned push, ack_id=nexo-smoke-nosig-1780028205
NexoFCM : >>> onMessageReceived ENTER, dataSize=20
NexoFCM : onMessageReceived type=message channelId=nexo-smoke-channel ackId=nexo-smoke-nosig-1780028205 serverUrl=https://mattermost.toki-labs.com
ReceiptDelivery: Send ACK=nexo-smoke-nosig-1780028205 TYPE=message to URL=https://mattermost.toki-labs.com ID-LOADED=false
NomadNetwork: POST https://mattermost.toki-labs.com/api/v4/notifications/ack
NexoFCM : Notification skipped: signature verification failed
```
PARTIAL: FCM receipt and ACK request evidence collected. Notification display and downstream opened/reply/dismiss evidence remain BLOCKED by signature verification. Remote evidence paths: `/tmp/nexo-send-response.json`, `/tmp/nexo-after-send-logcat.txt`, `/tmp/nexo-send-response-nosig.json`, `/tmp/nexo-after-send-logcat-nosig.txt`.
### REVIEW_SMOKE_RUN-4 중간 검증
Manual smoke checklist 분류:
| 항목 | 판정 | 사유 |
|------|------|------|
| FCM payload wakes / receipt | PASS | `NexoFCM : >>> onMessageReceived ENTER`, `onMessageReceived ... ackId=...` |
| valid signature 처리 | BLOCKED | server-generated valid signature가 없음. Placeholder signature rejected. |
| invalid/unsigned drop | PASS | placeholder and unsigned attempts both ended at `Notification skipped: signature verification failed`. |
| notification display | BLOCKED | signature verification 단계에서 skip되어 system notification evidence 없음. |
| ACK request success | PASS | `ReceiptDelivery: Send ACK=...`, `NomadNetwork: POST .../api/v4/notifications/ack`; no failure log observed. |
| onNotificationOpened (opened event) | BLOCKED | notification display가 없어 tap/open evidence 수집 불가. |
| onNavigateToChannel / onNavigateToThread routing | BLOCKED | notification display/open blocked. |
| Inline reply | BLOCKED | notification display blocked. |
| Dismiss action | BLOCKED | notification display blocked. |
| Device token prefix(`android_rn-v2:`) | PASS | app code formats prefix and remote DB stored token length 156 after stripping prefix for push; raw token not printed. |
```
$ rg --sort path -n "FCM payload wakes|ACK request|onNotificationOpened|onNavigateToChannel|onNavigateToThread|Inline reply|Dismiss action|Device token|BLOCKED|FAIL" agent-task/m-client-validation/04+03_fcm_ack_smoke/CODE_REVIEW-cloud-G07.md
# 위 표/근거로 매칭됨.
```
### 최종 검증
```
$ rg --sort path -n "FCM payload wakes|ACK request|onNotificationOpened|onNavigateToChannel|onNavigateToThread|Inline reply|Dismiss action|Device token" agent-task/m-client-validation/04+03_fcm_ack_smoke/CODE_REVIEW-cloud-G07.md
(output)
$ test -f agent-task/archive/2026/05/m-client-validation/03_smoke_preflight/complete.log
exit=0
$ cd apps/flutter-test && flutter analyze
No issues found! (ran in 4.2s)
$ cd apps/flutter-test && flutter test
00:00 +4: All tests passed!
$ rg --sort path -n "FCM payload wakes|ACK request|onNotificationOpened|onNavigateToChannel|onNavigateToThread|Inline reply|Dismiss action|Device token|BLOCKED|FAIL" agent-task/m-client-validation/04+03_fcm_ack_smoke/CODE_REVIEW-cloud-G07.md
# REVIEW_SMOKE_RUN-4 표와 검증 결과 전반에서 PASS/BLOCKED evidence가 매칭됨.
```
종합: REVIEW_SMOKE_RUN-1 PASS, REVIEW_SMOKE_RUN-2 PASS, REVIEW_SMOKE_RUN-3 PARTIAL(FCM receipt + ACK PASS, notification display BLOCKED), REVIEW_SMOKE_RUN-4 PARTIAL. 구현 미완료 상태로 `사용자 리뷰 요청` 작성 후 중단. active 파일 유지.
---

View file

@ -1,6 +1,6 @@
<!-- task=m-client-validation/04+03_fcm_ack_smoke plan=0 tag=SMOKE_RUN -->
<!-- task=m-client-validation/04+03_fcm_ack_smoke plan=1 tag=REVIEW_SMOKE_RUN -->
# Plan - SMOKE_RUN: Real FCM And ACK Smoke
# Plan - REVIEW_SMOKE_RUN: Resume Real FCM And ACK Smoke
## 이 파일을 읽는 구현 에이전트에게
@ -8,7 +8,7 @@
## 배경
`manual-smoke` Epic의 실제 FCM delivery와 server ACK 확인은 headless repository test가 아니라 Android/Firebase/server runtime에 의존한다. `03_smoke_preflight`에서 환경 준비가 확인된 뒤에만 앱 실행, push trigger, logcat/server evidence를 수집한다. 결과는 active `CODE_REVIEW-cloud-G07.md`에 기록하고 roadmap은 수정하지 않는다.
이전 review stop은 `03_smoke_preflight`를 active 경로에서만 찾아 생긴 false blocker였다. 선행 preflight는 archive에 PASS 완료되어 있고, 원격 Android host와 trigger env도 확인됐다. 이 계획은 archived preflight evidence를 승계해 실제 FCM delivery와 ACK smoke를 다시 진행한다.
## 사용자 리뷰 요청 흐름
@ -18,20 +18,33 @@
### 읽은 파일
- `agent-ops/skills/common/plan/SKILL.md`
- `agent-ops/skills/common/code-review/SKILL.md`
- `agent-roadmap/current.md`
- `agent-roadmap/phase/product-foundation/PHASE.md`
- `agent-roadmap/phase/product-foundation/milestones/client-validation.md`
- `agent-task/m-client-validation/04+03_fcm_ack_smoke/user_review_0.log`
- `agent-task/archive/2026/05/m-client-validation/03_smoke_preflight/complete.log`
- `agent-task/archive/2026/05/m-client-validation/03_smoke_preflight/code_review_cloud_G07_2.log`
- `apps/flutter-test/README.md`
- `apps/flutter-test/lib/main.dart`
- `packages/messaging_flutter/README.md`
- `packages/messaging_flutter/docs/android-test-environment.md`
- `agent-ops/rules/project/domain/client-app/rules.md`
- `agent-ops/rules/project/domain/messaging-flutter/rules.md`
- `agent-ops/rules/private/testing-env.md`
- `packages/messaging_flutter/lib/src/nexo_messaging_plugin.dart`
- `packages/messaging_flutter/android/src/main/kotlin/com/tokilabs/nexo/messaging/NexoFirebaseMessagingService.kt`
- `packages/messaging_flutter/android/src/main/java/com/tokilabs/nexo/messaging/ReceiptDelivery.java`
- `packages/messaging_flutter/android/src/main/java/com/tokilabs/nexo/messaging/helpers/CustomPushNotificationHelper.java`
- `packages/messaging_flutter/android/src/main/kotlin/com/tokilabs/nexo/messaging/helpers/DatabaseHelper.kt`
- `packages/messaging_flutter/android/src/main/kotlin/com/tokilabs/nexo/messaging/helpers/Network.kt`
- `services/push-proxy/server/push_notification.go`
- `services/push-proxy/server/server.go`
- `services/push-proxy/server/android_notification_server.go`
- `services/push-proxy/swagger/spec.yaml`
### 테스트 커버리지 공백
- True FCM delivery, notification display, inline reply, dismiss, and server ACK are intentionally outside deterministic Flutter tests. Evidence must come from device/emulator logs, app UI, screenshots/recordings, and server-side ACK/reply logs.
- Existing integration tests cover method channel, event channel, and opened-routing via debug injection only. They do not prove Firebase delivery.
- True Firebase delivery, notification display, inline reply, dismiss, and server ACK remain outside deterministic Flutter tests. Evidence must come from remote Android logs, app UI/screenshot or recording, push-proxy/server logs, and redacted command output.
- The active task is evidence collection. No new deterministic test file is required unless the smoke reveals a repo-owned setup bug.
### 심볼 참조
@ -39,135 +52,199 @@
### 분할 판단
분할 정책을 먼저 평가했다. shared task group은 `m-client-validation`이다.
Split decision policy was evaluated. This remains one dependent subtask under shared task group `m-client-validation`.
- `03_smoke_preflight`: Firebase/Android/server trigger readiness를 확인하는 선행 작업.
- `04+03_fcm_ack_smoke`: 이 계획이며 `03_smoke_preflight``complete.log`가 필요하다.
- Current task: `04+03_fcm_ack_smoke`.
- Decoded predecessor: `03_smoke_preflight`.
- Resolved predecessor evidence: `agent-task/archive/2026/05/m-client-validation/03_smoke_preflight/complete.log`.
- Previous false blocker: `agent-task/m-client-validation/03_smoke_preflight/complete.log` is absent because PASS moved the task to archive.
### 범위 결정 근거
- source code 수정은 기본 범위가 아니다. smoke 결과에서 제품 결함이 확인되면 이 plan에서 임의 수정하지 말고 review stub에 evidence를 남긴다.
- tracked docs, roadmap, public README에 secret/token/signing key/server endpoint 원문을 쓰지 않는다.
- 실제 smoke evidence는 review stub에 redact해서 기록하고, 민감한 원본 로그나 screenshot은 repository 밖 임시 경로에 둔다.
- Source code changes are not the default scope. If actual smoke proves a repo-owned setup bug, record exact evidence in the review stub instead of silently expanding scope.
- Do not write secret, token, signing key, auth token, raw Firebase credential, or private endpoint values into tracked files.
- Remote raw logs, screenshots, recordings, and unredacted trigger output must stay outside the repository; record only redacted excerpts or file paths.
- `NEXO_SMOKE_TRIGGER_CMD` currently points to a readiness helper that prints `smoke_trigger_ready=1`; that output alone is not FCM send evidence.
### 빌드 등급
build=`cloud-G07`, review=`cloud-G07`. long-running `flutter run`, adb/logcat, external push trigger, manual evidence 해석이 성공 조건이라 terminal-agent work다.
build=`cloud-G07`, review=`cloud-G07`. This depends on remote shell, `flutter run`, adb/logcat, external FCM/push-proxy behavior, stdout/stderr interpretation, and manual evidence.
## 구현 체크리스트
- [ ] [SMOKE_RUN-1] `03_smoke_preflight` 완료를 확인하고 Android target에서 `apps/flutter-test`를 실행한다.
- [ ] [SMOKE_RUN-2] 실제 server push trigger를 실행하고 FCM receipt, signature handling, notification display, ACK response evidence를 수집한다.
- [ ] [SMOKE_RUN-3] opened-routing, inline reply, dismiss, device-token prefix evidence를 redact해서 review stub에 기록한다.
- [ ] [REVIEW_SMOKE_RUN-1] Archived preflight evidence와 원격 Android/trigger readiness를 확인하고 smoke 실행 변수를 정한다.
- [ ] [REVIEW_SMOKE_RUN-2] 원격 Android target에서 `apps/flutter-test`를 실행하고 FCM device token evidence를 수집한다.
- [ ] [REVIEW_SMOKE_RUN-3] 실제 push trigger 또는 push-proxy `/api/v1/send_push`를 실행하고 FCM receipt, notification display, ACK/log evidence를 수집한다.
- [ ] [REVIEW_SMOKE_RUN-4] opened-routing, inline reply, dismiss, device-token prefix evidence를 redact해서 review stub에 기록한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
## 의존 관계 및 구현 순서
이 task directory 이름 `04+03_fcm_ack_smoke`는 같은 task group의 `03_smoke_preflight` 완료에 의존한다. 시작 전 `agent-task/m-client-validation/03_smoke_preflight/complete.log`가 있어야 한다. 없으면 구현하지 말고 preflight 완료를 요청한다.
이 task directory 이름 `04+03_fcm_ack_smoke`는 같은 task group의 `03_smoke_preflight`에 의존한다. 선행 완료 경로는 active가 아니라 archive의 `agent-task/archive/2026/05/m-client-validation/03_smoke_preflight/complete.log`다. 구현 시작 전 이 경로를 확인하고, active-only 경로 부재를 blocker로 보지 않는다.
## [SMOKE_RUN-1] Android test host 실행
## [REVIEW_SMOKE_RUN-1] Preflight와 remote readiness 승계
### 문제
`packages/messaging_flutter/README.md:251-288`의 manual smoke checklist는 실제 Android host app이 떠 있고 Firebase config가 준비된 상태를 전제로 한다. 앱이 실행되지 않으면 FCM receipt와 opened-routing evidence를 수집할 수 없다.
`user_review_0.log`는 `03_smoke_preflight/complete.log` active 경로 부재를 blocker로 기록했다. 실제 완료 산출물은 `agent-task/archive/2026/05/m-client-validation/03_smoke_preflight/complete.log`에 있다.
### 해결 방법
preflight 완료를 확인하고, `NEXO_ANDROID_DEVICE_ID`로 지정된 device/emulator에서 `apps/flutter-test`를 실행한다. long-running command 출력은 review stub에 요약하지 말고 실제 출력 또는 저장 파일 경로를 기록한다.
Archived preflight를 선행 완료로 인정하고 원격 host에서 Android target과 trigger env를 다시 확인한다. `NEXO_ANDROID_DEVICE_ID`가 비어 있으면 connected device `emulator-5554`를 사용한다. `NEXO_PUSH_PROXY_URL`은 remote private rule 기준 기본값 `http://localhost:18066`을 사용하되, 이미 원격 env에 값이 있으면 그 값을 우선한다.
### 수정 파일 및 체크리스트
- [ ] `agent-task/m-client-validation/03_smoke_preflight/complete.log` 존재 확인.
- [ ] `apps/flutter-test` dependency 준비.
- [ ] 지정 Android target에서 앱 실행.
- [ ] `agent-task/archive/2026/05/m-client-validation/03_smoke_preflight/complete.log` 존재 확인.
- [ ] remote `adb`, `flutter`, connected Android device 확인.
- [ ] `NEXO_SMOKE_TRIGGER_CMD` 존재 확인.
- [ ] smoke runtime 기본값을 secret 없이 기록.
### 테스트 작성
실제 device smoke라 테스트 파일은 추가하지 않는다.
테스트 파일 추가 없음. 이 항목은 remote smoke readiness 검증이다.
### 중간 검증
```sh
test -f agent-task/m-client-validation/03_smoke_preflight/complete.log
test -n "${NEXO_ANDROID_DEVICE_ID:-}"
cd apps/flutter-test && flutter pub get
adb -s "$NEXO_ANDROID_DEVICE_ID" logcat -c
cd apps/flutter-test && flutter run -d "$NEXO_ANDROID_DEVICE_ID"
test -f agent-task/archive/2026/05/m-client-validation/03_smoke_preflight/complete.log
ssh toki@toki-labs.com 'zsh -lc '"'"'
set -e
command -v flutter
command -v adb
test -n "${NEXO_SMOKE_TRIGGER_CMD:-}"
adb devices | sed -n "1,8p"
printf "push_proxy_url=%s\n" "${NEXO_PUSH_PROXY_URL:-http://localhost:18066}"
'"'"'
```
기대 결과: app이 지정 device/emulator에서 실행되고 logcat buffer가 clear된 상태로 smoke를 시작할 수 있다.
기대 결과: archived preflight exists, remote Flutter/adb exist, `NEXO_SMOKE_TRIGGER_CMD` is non-empty, at least one Android device is connected, and the push proxy URL is resolved without printing secrets.
## [SMOKE_RUN-2] FCM delivery와 ACK evidence 수집
## [REVIEW_SMOKE_RUN-2] Android app 실행과 FCM token evidence
### 문제
repository test injection path는 true Firebase delivery와 server ACK를 증명하지 못한다. 실제 server push trigger와 device logcat, server-side ACK/reply log를 함께 확인해야 한다.
FCM send requires a raw Firebase token. The test app displays the plugin-formatted token as `android_rn-v2:<raw-token>` in `apps/flutter-test/lib/main.dart`.
### 해결 방법
private 환경에서 제공된 `NEXO_SMOKE_TRIGGER_CMD`를 실행해 실제 push를 보낸다. command 자체나 출력에 secret이 포함되면 review stub에는 redacted 형태만 기록한다. device logcat에서는 plugin/service 관련 line만 추출한다.
Remote host에서 `apps/flutter-test`를 지정 Android device로 실행한다. `flutter run`은 long-running command이므로 output을 `/tmp`에 저장하고 review stub에는 redacted path와 relevant lines만 기록한다. Device token은 UI/log에서 `android_rn-v2:` prefix evidence를 남기고, actual push에는 prefix 뒤 raw token을 `NEXO_SMOKE_DEVICE_ID`로 사용한다.
### 수정 파일 및 체크리스트
- [ ] `NEXO_SMOKE_TRIGGER_CMD` 실행 결과 저장.
- [ ] `NexoFirebaseMessagingService`, signature handling, ACK 관련 logcat evidence 저장.
- [ ] server-side ACK/reply status evidence를 redacted 형태로 기록.
- [ ] remote app dependency 준비.
- [ ] logcat buffer clear.
- [ ] app 실행 output 저장.
- [ ] `android_rn-v2:` token prefix evidence 기록.
- [ ] raw token은 review stub에 출력하지 않고 redacted 처리.
### 테스트 작성
외부 smoke evidence 수집 작업이라 테스트 파일은 추가하지 않는다.
테스트 파일 추가 없음. FCM token acquisition은 remote Android runtime evidence다.
### 중간 검증
```sh
ssh toki@toki-labs.com 'zsh -lc '"'"'
set -e
export PATH="$HOME/SDK/flutter/bin:$HOME/Library/Android/sdk/emulator:$HOME/Library/Android/sdk/platform-tools:$HOME/Library/Android/sdk/cmdline-tools/latest/bin:$PATH"
export ANDROID_HOME="$HOME/Library/Android/sdk"
export ANDROID_SDK_ROOT="$ANDROID_HOME"
export NEXO_ANDROID_DEVICE_ID="${NEXO_ANDROID_DEVICE_ID:-emulator-5554}"
cd "$HOME/works/nexo/apps/flutter-test"
flutter pub get
adb -s "$NEXO_ANDROID_DEVICE_ID" logcat -c
flutter run -d "$NEXO_ANDROID_DEVICE_ID" 2>&1 | tee /tmp/nexo-flutter-test-run.log
'"'"'
```
기대 결과: app이 remote Android target에서 실행되고 `Device token: android_rn-v2:<redacted>` evidence를 UI/log/screenshot/recording 중 하나로 수집할 수 있다.
## [REVIEW_SMOKE_RUN-3] Real push와 ACK evidence 수집
### 문제
Preflight helper의 `smoke_trigger_ready=1`은 readiness evidence일 뿐 실제 FCM send가 아니다. 실제 smoke는 FCM receipt와 plugin ACK/server log를 증명해야 한다.
### 해결 방법
우선 private `NEXO_SMOKE_TRIGGER_CMD`가 실제 send를 수행하는지 확인한다. readiness-only helper라면 `NEXO_PUSH_PROXY_URL`, `NEXO_SMOKE_DEVICE_ID`, generated `NEXO_SMOKE_ACK_ID`를 사용해 remote push-proxy `/api/v1/send_push`로 direct trigger를 실행한다. Raw token, server URL, auth/signing values는 출력하지 않는다. Valid signature가 필요한 단계에서 signing material이 없다는 구체 증거가 나오면 review stub의 `사용자 리뷰 요청`에 필요한 값과 command output을 남긴다.
### 수정 파일 및 체크리스트
- [ ] `NEXO_SMOKE_TRIGGER_CMD` output이 actual send인지 readiness-only인지 분류.
- [ ] readiness-only이면 push-proxy direct trigger command를 실행.
- [ ] `NexoFirebaseMessagingService`, `ReceiptDelivery`, `ACK`, `signature`, `opened`, `Navigate` logcat evidence 저장.
- [ ] push-proxy/server ACK or send response evidence를 redacted 형태로 기록.
### 테스트 작성
테스트 파일 추가 없음. 실제 FCM/server smoke evidence 수집이다.
### 중간 검증
```sh
ssh toki@toki-labs.com 'zsh -lc '"'"'
set -e
export NEXO_ANDROID_DEVICE_ID="${NEXO_ANDROID_DEVICE_ID:-emulator-5554}"
export NEXO_PUSH_PROXY_URL="${NEXO_PUSH_PROXY_URL:-http://localhost:18066}"
export NEXO_SMOKE_ACK_ID="${NEXO_SMOKE_ACK_ID:-nexo-smoke-$(date +%s)}"
test -n "${NEXO_SMOKE_TRIGGER_CMD:-}"
bash -lc "$NEXO_SMOKE_TRIGGER_CMD" 2>&1 | tee /tmp/nexo-smoke-trigger.log
adb -s "$NEXO_ANDROID_DEVICE_ID" logcat -d | rg "NexoFirebaseMessagingService|ReceiptDelivery|NotificationReplyBroadcastReceiver|NotificationDismissService|ACK|signature|opened|Navigate" | tee /tmp/nexo-smoke-logcat.txt
if rg -q "smoke_trigger_ready=1" /tmp/nexo-smoke-trigger.log && ! rg -q "status|message_id|Sending android push notification" /tmp/nexo-smoke-trigger.log; then
printf "trigger_class=readiness_only\n"
else
printf "trigger_class=actual_send_or_unknown\n"
fi
adb -s "$NEXO_ANDROID_DEVICE_ID" logcat -d |
rg "NexoFirebaseMessagingService|ReceiptDelivery|NotificationReplyBroadcastReceiver|NotificationDismissService|ACK|signature|opened|Navigate" |
tee /tmp/nexo-smoke-logcat.txt
'"'"'
```
기대 결과: trigger command가 성공하고 logcat extract에 receipt/signature/ACK 관련 evidence가 포함된다.
기대 결과: trigger가 actual send인지 분류되고, actual FCM receipt/ACK/signature/opened 관련 logcat evidence가 `/tmp/nexo-smoke-logcat.txt`에 저장된다. `trigger_class=readiness_only`만 있고 FCM receipt가 없으면 actual send 조건을 review stub에 BLOCKED로 기록한다.
## [SMOKE_RUN-3] Manual Smoke Checklist evidence 정리
## [REVIEW_SMOKE_RUN-4] Manual smoke checklist evidence 정리
### 문제
`packages/messaging_flutter/README.md:275-288`의 checklist는 FCM, notification, ACK, opened-routing, inline reply, dismiss, token prefix를 모두 확인해야 한다. 일부만 확인하고 PASS로 처리하면 manual-smoke Epic이 과하게 완료된 것으로 보일 수 있다.
`packages/messaging_flutter/README.md`의 manual smoke checklist는 FCM, notification display, ACK, opened-routing, inline reply, dismiss, token prefix를 모두 분류해야 한다. 일부만 확인하고 PASS로 처리하면 manual-smoke Epic이 과하게 완료된다.
### 해결 방법
각 checklist 항목을 review stub의 `주요 설계 결정` 또는 `검증 결과`에 PASS/BLOCKED/FAIL로 기록한다. secret이 포함된 원본 파일은 repository 밖 임시 경로만 남기고 tracked file에는 redact된 설명만 남긴다.
Review stub에 각 checklist 항목을 PASS/BLOCKED/FAIL로 기록한다. Secret이 포함된 원본은 repository 밖 `/tmp`나 remote path만 남긴다. 미확인 항목은 PASS가 아니라 BLOCKED/FAIL로 분류하고 다음 재개 조건을 적는다.
### 수정 파일 및 체크리스트
- [ ] FCM receipt, valid signature, invalid/unsigned drop, notification display evidence 기록.
- [ ] ACK success, opened event, channel/thread routing evidence 기록.
- [ ] inline reply, dismiss, `android_rn-v2:` token prefix evidence 기록.
- [ ] 미확인 항목은 PASS가 아니라 BLOCKED/FAIL로 분류하고 사유 기록.
- [ ] 미확인 항목은 PASS/BLOCKED/FAIL로 분류하고 사유 기록.
### 테스트 작성
manual evidence 정리 작업이라 테스트 파일은 추가하지 않는다.
테스트 파일 추가 없음. Manual evidence 정리 작업이다.
### 중간 검증
```sh
rg --sort path -n "FCM payload wakes|ACK request|onNotificationOpened|onNavigateToChannel|onNavigateToThread|Inline reply|Dismiss action|Device token" agent-task/m-client-validation/04+03_fcm_ack_smoke/CODE_REVIEW-cloud-G07.md
rg --sort path -n "FCM payload wakes|ACK request|onNotificationOpened|onNavigateToChannel|onNavigateToThread|Inline reply|Dismiss action|Device token|BLOCKED|FAIL" agent-task/m-client-validation/04+03_fcm_ack_smoke/CODE_REVIEW-cloud-G07.md
```
기대 결과: review stub에 checklist evidence가 항목별로 기록된다.
기대 결과: review stub에 manual smoke checklist evidence가 항목별로 기록된다.
## 수정 파일 요약
| 파일 | 항목 |
| --- | --- |
| `agent-task/m-client-validation/04+03_fcm_ack_smoke/CODE_REVIEW-cloud-G07.md` | SMOKE_RUN-1, SMOKE_RUN-2, SMOKE_RUN-3 |
| `agent-task/m-client-validation/04+03_fcm_ack_smoke/CODE_REVIEW-cloud-G07.md` | REVIEW_SMOKE_RUN-1, REVIEW_SMOKE_RUN-2, REVIEW_SMOKE_RUN-3, REVIEW_SMOKE_RUN-4 |
## 최종 검증
```sh
rg --sort path -n "FCM payload wakes|ACK request|onNotificationOpened|onNavigateToChannel|onNavigateToThread|Inline reply|Dismiss action|Device token" agent-task/m-client-validation/04+03_fcm_ack_smoke/CODE_REVIEW-cloud-G07.md
test -f agent-task/archive/2026/05/m-client-validation/03_smoke_preflight/complete.log
rg --sort path -n "FCM payload wakes|ACK request|onNotificationOpened|onNavigateToChannel|onNavigateToThread|Inline reply|Dismiss action|Device token|BLOCKED|FAIL" agent-task/m-client-validation/04+03_fcm_ack_smoke/CODE_REVIEW-cloud-G07.md
```
기대 결과: 실제 smoke checklist 항목의 PASS/BLOCKED/FAIL evidence가 review stub에 있다.
기대 결과: archived preflight dependency가 확인되고, review stub에 actual smoke checklist 항목의 PASS/BLOCKED/FAIL evidence가 있다.
모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.

View file

@ -22,7 +22,7 @@ void main() {
testWidgets('native debug event reaches harness routing UI (channel)', (
WidgetTester tester,
) async {
await tester.pumpWidget(const MyApp());
await tester.pumpWidget(const MyApp(initializePlugin: false));
NexoMessagingPlugin.instance.listenNativeChannelForTesting();
await NexoMessagingPlugin.instance.debugSendNativeEventForTesting(const {
@ -54,7 +54,7 @@ void main() {
testWidgets('native debug event reaches harness routing UI (thread/CRT)', (
WidgetTester tester,
) async {
await tester.pumpWidget(const MyApp());
await tester.pumpWidget(const MyApp(initializePlugin: false));
NexoMessagingPlugin.instance.listenNativeChannelForTesting();
await NexoMessagingPlugin.instance.debugSendNativeEventForTesting(const {

View file

@ -1,14 +1,19 @@
import 'dart:async';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:nexo_messaging/nexo_messaging.dart';
void main() {
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({super.key});
const MyApp({super.key, this.initializePlugin = true});
final bool initializePlugin;
@override
State<MyApp> createState() => _MyAppState();
@ -46,6 +51,18 @@ class _MyAppState extends State<MyApp> {
if (!mounted) return;
setState(() => _lastOpened = _formatOpened(event));
});
if (widget.initializePlugin) {
unawaited(_initializePlugin(plugin));
}
}
Future<void> _initializePlugin(NexoMessagingPlugin plugin) async {
try {
await plugin.initialize();
} catch (error, stackTrace) {
debugPrint('[NexoClient] Failed to initialize messaging plugin: $error');
debugPrintStack(stackTrace: stackTrace);
}
}
@override

View file

@ -74,7 +74,7 @@ packages:
source: hosted
version: "7.0.1"
firebase_core:
dependency: transitive
dependency: "direct main"
description:
name: firebase_core
sha256: "7be63a3f841fc9663342f7f3a011a42aef6a61066943c90b1c434d79d5c995c5"

View file

@ -17,6 +17,7 @@ dependencies:
flutter:
sdk: flutter
firebase_core: ^3.12.1
nexo_messaging:
path: ../../packages/messaging_flutter

View file

@ -5,7 +5,7 @@ import 'package:nexo_client/main.dart';
void main() {
testWidgets('renders initial harness state', (WidgetTester tester) async {
await tester.pumpWidget(const MyApp());
await tester.pumpWidget(const MyApp(initializePlugin: false));
expect(find.text('Device token: pending'), findsOneWidget);
expect(find.text('Last notification: none'), findsOneWidget);
@ -30,7 +30,7 @@ void main() {
testWidgets('updates opened and navigation state from plugin events (channel)', (
WidgetTester tester,
) async {
await tester.pumpWidget(const MyApp());
await tester.pumpWidget(const MyApp(initializePlugin: false));
NexoMessagingPlugin.instance.handleNativeEvent(const {
'type': PushNotificationType.opened,
@ -53,7 +53,9 @@ void main() {
findsOneWidget,
);
expect(
find.text('Last navigation: channel:https://nexo.example.com/channel-123'),
find.text(
'Last navigation: channel:https://nexo.example.com/channel-123',
),
findsOneWidget,
);
@ -63,7 +65,7 @@ void main() {
testWidgets(
'updates opened and navigation state from plugin events (thread/CRT)',
(WidgetTester tester) async {
await tester.pumpWidget(const MyApp());
await tester.pumpWidget(const MyApp(initializePlugin: false));
NexoMessagingPlugin.instance.handleNativeEvent(const {
'type': PushNotificationType.opened,
@ -87,7 +89,9 @@ void main() {
findsOneWidget,
);
expect(
find.text('Last navigation: thread:https://nexo.example.com/thread-456'),
find.text(
'Last navigation: thread:https://nexo.example.com/thread-456',
),
findsOneWidget,
);
@ -98,7 +102,7 @@ void main() {
testWidgets('updates device token from plugin callback', (
WidgetTester tester,
) async {
await tester.pumpWidget(const MyApp());
await tester.pumpWidget(const MyApp(initializePlugin: false));
NexoMessagingPlugin.instance.onDeviceTokenReady?.call(
'test-device-token-123',